1

I'm streaming H264 NALs from a server, wrapping them as FLV tags and passing them into a NetStream with appendBytes (Data Generation Mode). However, while the video is playing normally the stream is delayed by around a second.

I've tried setting bufferTime, bufferTimeMax but with no luck to prevent the buffering going on.

I've also tried various combinations of NetStream.seek() and NetStream.appendBytesAction() with RESET_SEEK and END_SEQUENCE, again to no avail.

Is there is a trick I'm missing here, is there a way to prevent that delay?

Interestingly I don't see the delay on the audio I'm passing in (PCMU) so I end up with lip sync issues.

Updated: Still stuck, so posting the code I'm using:

        var timestamp : uint = networkPayload.readUnsignedInt();
        if (videoTimestampBase == 0) {
            videoTimestampBase = timestamp;
        }
        timestamp = timestamp - videoTimestampBase;
        timestamp = timestamp / 90.0;

        // skip 7 bytes of marker
        networkPayload.position = 7;
        var nalType : int = networkPayload.readByte();
        nalType &= 0x1F;
        networkPayload.position = 7;

        // reformat Annex B bitstream encoding, to Mp4 - remove timestamp and bitstream marker (3 bytes)
        var mp4Payload : ByteArray = new ByteArray();
        var mp4PayloadLength : int = networkPayload.bytesAvailable;
        mp4Payload.writeUnsignedInt(mp4PayloadLength);
        mp4Payload.writeBytes(networkPayload, 7, mp4PayloadLength);
        mp4Payload.position = 0;

        if (nalType == 8) {
            // PPS
            ppsNAL = new ByteArray();
            // special case for PPS/SPS - don't length encode
            ppsLength = mp4Payload.bytesAvailable - 4;
            ppsNAL.writeBytes(mp4Payload, 4, mp4Payload.bytesAvailable - 4);
            if (spsNAL == null) {
                return;
            }
        } else if (nalType == 7) {
            // SPS
            spsNAL = new ByteArray();
            // special case for PPS/SPS - don't length encode
            spsLength = mp4Payload.bytesAvailable - 4;
            spsNAL.writeBytes(mp4Payload, 4, mp4Payload.bytesAvailable - 4);

            if (ppsNAL == null) {
                return;
            }
        }

        if ((spsNAL != null) && (ppsNAL != null)) {
            Log.debug(TAG, "Writing sequence header: " + spsLength + "," + ppsLength + "," + timestamp);

            var sequenceHeaderTag : FLVTagVideo = new FLVTagVideo();
            sequenceHeaderTag.codecID = FLVTagVideo.CODEC_ID_AVC;
            sequenceHeaderTag.frameType = FLVTagVideo.FRAME_TYPE_KEYFRAME;
            sequenceHeaderTag.timestamp = timestamp;
            sequenceHeaderTag.avcPacketType = FLVTagVideo.AVC_PACKET_TYPE_SEQUENCE_HEADER;

            spsNAL.position = 1;
            var profile : int = spsNAL.readByte();
            var compatibility : int = spsNAL.readByte();
            var level : int = spsNAL.readByte();
            Log.debug(TAG, profile + "," + compatibility + "," + level + "," + spsLength);

            var avcc : ByteArray = new ByteArray();
            avcc.writeByte(0x01); // avcC version 1
            // profile, compatibility, level
            avcc.writeByte(profile);
            avcc.writeByte(compatibility);
            avcc.writeByte(0x20); //level);
            avcc.writeByte(0xff); // 111111 + 2 bit NAL size - 1
            avcc.writeByte(0xe1); // number of SPS
            avcc.writeByte(spsLength >> 8); // 16-bit SPS byte count
            avcc.writeByte(spsLength);
            avcc.writeBytes(spsNAL, 0, spsLength); // the SPS
            avcc.writeByte(0x01); // number of PPS
            avcc.writeByte(ppsLength >> 8); // 16-bit PPS byte count
            avcc.writeByte(ppsLength);
            avcc.writeBytes(ppsNAL, 0, ppsLength);
            sequenceHeaderTag.data = avcc;

            // clear the pps/sps til next buffer
            var bytes : ByteArray = new ByteArray();
            sequenceHeaderTag.write(bytes);

            stream.appendBytes(bytes);

            ppsNAL = null;
            spsNAL = null;
        } else {
            if ((timestamp != currentTimestamp) || (currentVideoTag == null)) {
                if (currentVideoTag != null) {
                    currentVideoTag.data = currentSegment;

                    var tagData : ByteArray = new ByteArray();
                    currentVideoTag.write(tagData);

                    stream.appendBytes(tagData);
                }

                currentVideoTag = new FLVTagVideo();
                currentVideoTag.codecID = FLVTagVideo.CODEC_ID_AVC;
                currentVideoTag.frameType = FLVTagVideo.FRAME_TYPE_INTER;
                if (nalType == 5) {
                    currentVideoTag.frameType = FLVTagVideo.FRAME_TYPE_KEYFRAME;
                }
                lastNalType = nalType;
                currentVideoTag.avcPacketType = FLVTagVideo.AVC_PACKET_TYPE_NALU;
                currentVideoTag.timestamp = timestamp;
                currentVideoTag.avcCompositionTimeOffset = 0;

                currentSegment = new ByteArray();
                currentTimestamp = timestamp;
            }

            mp4Payload.position = 0;
            currentSegment.writeBytes(mp4Payload);
        }

Update, bit more detail, here's the timestamps being passed:

DEBUG: StreamPlayback: 66,-32,20,19
DEBUG: StreamPlayback: Timestamp: 0
DEBUG: StreamPlayback: Timestamp: 63
DEBUG: StreamPlayback: stream status update: netStatus NetStream.Buffer.Full
DEBUG: StreamPlayback: Timestamp: 137
DEBUG: StreamPlayback: Timestamp: 200
DEBUG: StreamPlayback: Timestamp: 264
DEBUG: StreamPlayback: Timestamp: 328
DEBUG: StreamPlayback: Timestamp: 403
DEBUG: StreamPlayback: Timestamp: 467
DEBUG: StreamPlayback: Timestamp: 531
DEBUG: StreamPlayback: Timestamp: 595
DEBUG: StreamPlayback: Timestamp: 659
DEBUG: StreamPlayback: Timestamp: 723
DEBUG: StreamPlayback: Timestamp: 830
DEBUG: StreamPlayback: Timestamp: 894
DEBUG: StreamPlayback: Timestamp: 958
DEBUG: StreamPlayback: Timestamp: 1021
DEBUG: StreamPlayback: Timestamp: 1086
DEBUG: StreamPlayback: Timestamp: 1161
DEBUG: StreamPlayback: Timestamp: 1225
DEBUG: StreamPlayback: Timestamp: 1289
DEBUG: StreamPlayback: Timestamp: 1353
DEBUG: StreamPlayback: Timestamp: 1418
DEBUG: StreamPlayback: Timestamp: 1491
DEBUG: StreamPlayback: Timestamp: 1556
DEBUG: StreamPlayback: Timestamp: 1633
DEBUG: StreamPlayback: Timestamp: 1684
DEBUG: StreamPlayback: Timestamp: 1747
DEBUG: StreamPlayback: stream status update: netStatus NetStream.Video.DimensionChange
DEBUG: StreamPlayback: Timestamp: 1811

Cheers,

Kev

  • 1
    Possibly a timestamps issue? Feed two audio tags (consecutive appends) first then followed by a video tag (frame) in that order... `bufferTime` etc just takes care of any "ahead" decoding so content is ready by the time your playhead reaches it. With **H.264** it can't be stopped cos sometimes a decoder needs a "group of pictures" (for reference) before showing a current frame's image. – VC.One Mar 02 '16 at 19:07
  • I've actually turned the audio off atm, so it's just video streaming. Timestamps are being generated from the RTP timestamp divided by 90 to bring it to milliseconds. I'll take a look and see if there's something mixing up in there. Net result though, you're saying I don't need to flush the stream in anyway, it should just play immediately anyway. – Kevin Glass Mar 02 '16 at 19:10
  • Yes don't flush per append. Just keep appending and the Flash decoder takes care of things. If you use `Reset_Seek` the decoder will now expect a **keyframe** video tag. All audio tags are audio keyframes though. – VC.One Mar 02 '16 at 19:27
  • Checked the timestamp, tried pushing it with audio tags in front, still no dice. If you have a moment, I've posted the code above to see if I've done something wrong. – Kevin Glass Mar 02 '16 at 20:21
  • 1
    Also why not make that `var tagData : ByteArray = new ByteArray();` into a **public var**? Then instead of making multiple new bytearrays, you simply do a `tagData.clear();` before writing next data into it. Basically keep blanking and recycling that one bytearray. It might help with subtle sync issues since more memory will be now be available. Same logic for `networkPayload`, `mp4Payload` and `currentSegment`.. using a `= new` each time just eats up more RAM. – VC.One Mar 03 '16 at 09:56
  • With respect to writing the timestamp, I'm using the OSMF classes so FlvTag is https://sourceforge.net/adobe/osmf/svn/2065/tree/osmf/trunk/framework/OSMF/org/osmf/net/httpstreaming/flv/FLVTag.as#l168 (it looks like it's writing the bytes in the right order - just checking again). In terms of re-using the arrays, absolutely, I'm just not optimizing it out yet. Though I guess I'll do that while I'm stuck :) – Kevin Glass Mar 03 '16 at 10:21
  • 1
    OK, hopefully optimizing those arrays will help. Yeah those OSMF guys are correct. I just checked one of my old **[appendBytes code](https://gist.github.com/Valerio-Charles-VC1/657054b773dba9ba1cbc)** and even I do it like they show too (see **function get_TAG_timestamp**). Dunno why I worried about it here. Probably need sleep, that's why. – VC.One Mar 03 '16 at 10:47
  • Seriously, thanks for the help here and on the other ticket, very much appreciated. I'll keep plugging on. – Kevin Glass Mar 03 '16 at 10:53
  • Optimized arrays - no change. Starting to wonder if the OSMF implementation shipped with Apache Flex is actually right, going to manually encode the timestamps to see if that helps. – Kevin Glass Mar 03 '16 at 11:24
  • Ah ha, the media doesn't actually start playing until that NetStream.Video.DimensionChange is fired. – Kevin Glass Mar 03 '16 at 11:44
  • 1
    Ok, so what appears to happen is it buffers data until it gets that dimension change, where it's worked out what resolution the video should be displayed at. At that point, (~2 seconds in), in then starts playing the stream from the start. Hence the delay. – Kevin Glass Mar 03 '16 at 12:56
  • 1
    If that's the solution then please add it as Answer. Useful to know that waiting for the DimensionChange event fixes it. PS: Also I learnt a new thing today, usually when I've used PCM in FLV it's always been 44.1khz 16 bit etc in a non-RTMP setup. So I forced PCMU like your audio and realised that Netstream (and even desktop MPC-HC player) played the FLV sound in ultra slow-motion and the video framerate was bad. Never seen that before from those decoders. I guess it's somehow auto-fixed by RTMP mode?? – VC.One Mar 03 '16 at 18:10
  • its a symptom, not the answer yet. I can see what its doing, I dont' know how to cause the dimension change event to happen sooner tho, it's generated once the encoder has enough information to fire it. Is there a way to fake it? – Kevin Glass Mar 03 '16 at 18:46
  • 1
    It fires when dimesions become available (or change). So maybe your prior timestamps are only for the audio tags filling the buffer? If not, then its odd that the buffer gets full (has content to play) yet no picture is detected until later appends. Does your FLV metadata have a correct width & height set to match video resolution? – VC.One Mar 04 '16 at 08:03
  • 1
    To fake it... You could try sending a video tag of bitmapdata of same width/height as your video. Eg: Make a bitmapdata, write that to a temp bytearray, make a video tag with `0x09` + 3 bytes for BMD bytes length + 4 bytes `0x00` as timestamp + 3 bytes of `0x00`. That's video tag header so now write either `0x10`(RGB) or else `x14` (screenVideo) as codec type, then copy BMD bytes into this tag, wrap it up by writing an Unsigned Integer of following amount `11 + 1 + BMD_bytes.length`. Append this tag only after appending FLV header + ScriptData tag (the metadata). – VC.One Mar 04 '16 at 08:24
  • Don't seem to be able to get that to work, the stream just stops when I try and write that content in. Also just passing a byte array won't tell it the width/height will it - I mean a block of bytes could represent lots of different resolutions? – Kevin Glass Mar 04 '16 at 11:13
  • I'll knock up an example code over the weekend. Basically when you do a `RESET_BEGIN` your next line of code should immediately append the FLV header tag and next line should append video's metadata tag then after (or later) you can start appending actual a/v tags. It can jam if you do the appending of FLV header and metadata tags each via separate functions (dunno why, but it just stops if done like that). The metadata tells decoder the expected width/height so the BMD bytes will be decoded correctly. I've done it with BMD as screenVideo codec but noticed RGB codec so recommended that – VC.One Mar 04 '16 at 20:43
  • Sorry to keep you hanging on pal... I hope the solutions below are helpful at the least.. PS: I mistakenly said "of bitmapdata of same width/height as your video" when I really meant _of a **different** size_. I've tested the example code. You should append the dynamic video frame AFTER you append the FLV header & Meta data but BEFORE you start appending your RTMP A/V tags. Dynamic should be the first frame (tag) sent to netStream followe by any audio or video tags. – VC.One Mar 11 '16 at 08:28
  • Thanks mate, I'll give it a go asap and report back. – Kevin Glass Mar 12 '16 at 14:51

1 Answers1

0

Solution one :

The media doesn't actually start playing until that NetStream.Video.DimensionChange is fired

Why not pause NetStream before starting any appends? You then append tags until NetStream confirms a "dimension change". In the Net Status handler for this you then unPause the NetStream.
Hopefully it will play synchronised since the playhead for neither sound or video has not moved whilst in pause mode.

stream.addEventListener(NetStatusEvent.NET_STATUS, stream_StatusHandler);

stream.play(null);
stream.appendBytesAction(NetStreamAppendBytesAction.RESET_BEGIN);
stream.pause(); //# pause before beginning FLV A/V Tag appends

public function stream_StatusHandler (evt:NetStatusEvent) : void
{
    trace("DEBUG: StreamPlayback : NEW evt.info.code : " + evt.info.code );

    switch (evt.info.code) 
    {
        //# in case its "NetStream.Buffer.Full"
        case "NetStream.Buffer.Full"  :     
        trace("DEBUG: StreamPlayback : NetStream.Buffer.Full...");
        break;

        //# in case its "NetStream.Video.DimensionChange" :
        case "NetStream.Video.DimensionChange"  :
        trace("DEBUG: StreamPlayback : #### Video Dimensions have changed...");         
        trace("DEBUG: StreamPlayback : #### NEW Detected video.videoHeight : " + video.videoHeight ); 
        stream.resume(); //# resume playback
        //# or use :  stream.togglePause();
        break;

    }
}

If that doesn't work, then you could try...

Solution two :

I don't know how to cause the dimension change event to happen sooner though...
Is there a way to fake it?

Use bitmap data to create a dynamic video frame made of just a simple colour block. The block has a resolution size different to your video stream. You append the block first and the difference with your own video frame will trigger the dimension change.

Note : If your video triggers it too late (ie: the A/V is not synchronised then it means your are sending too many audio tags at first (possibly with a incorrect timestamp of after the video's time?)... Try checking timestamps. The audio is always before the video and must not exceed the related video tag's timestamp).

The example code below makes a 100 width x 50 height video frame (Bitmap data is encoded to Screen-Video format and appended as a video tag).

// 1) ## Setup Video Object + Append FLV header + Append Metadata etc
// 2) ## Run function below before appending your first Video tag...

force_Dimension_Adjust();

// 3) ## Do your usual appends...

Here is the related code for : force_Dimension_Adjust();

public var BA_BMD_Frame : ByteArray;
public var BA_Temp : ByteArray;

public function force_Dimension_Adjust() : void
{
    trace("DEBUG: #### doing function : force_Dimension_Adjust");

    //create BMD frame for dimension change
    generate_Frame_BMPdata(); //# Puts result video tag into BA_BMD_Frame

    BA_BMD_Frame.position = 0;
    stream.appendBytes( BA_BMD_Frame ); //should trigger "dimesion change" for video picture size
    trace("DEBUG: StreamPlayback : #### APPENDED :::: BA_BMD_Frame : " );

}

public function generate_Frame_BMPdata() : void
{
    //## Simple colour block as video frame content 
    //## (pW = Picture Width, pH = Picture Height)

    var pW : int = 100; var pH : int = 50; 
    var temp_BMD = new BitmapData(pW, pH, false, 0x5500AA ); //R-G-B 5500AA = purple 
    var temp_BMP = new Bitmap(temp_BMD);

    // 1) #### encode BitmapData to codec Screen Video 
    BA_BMD_Frame.clear(); BA_Temp.clear(); BA_Temp.position = 0; //# Resets 
    encode_SCREENVIDEO (BA_Temp, temp_BMD); //# Put encoded BMD into a temp ByteArray

    // 2) #### Create Video Frame TAG to hold encoded frame 
    BA_BMD_Frame.writeByte(0x09); //# is video TAG

    writeUInt24( BA_BMD_Frame, BA_Temp.length ); //# Write 3 bytes  : size of BMD bytes length 

    BA_BMD_Frame.writeUnsignedInt(0x00); //# Write  4 byte timestamp : 0x00 0x00 0x00 0x00

    writeUInt24( BA_BMD_Frame, 0x00 ); //# Write 3 bytes (stream ID etc) : 0x00 0x00 0x00

    BA_BMD_Frame.position = BA_BMD_Frame.length;
    BA_BMD_Frame.writeBytes( BA_Temp ); //# Write encoded BMD bytes here

    BA_BMD_Frame.position = BA_BMD_Frame.length;
    BA_BMD_Frame.writeUnsignedInt( BA_BMD_Frame.length - 4 ); //# Close : total size of this byteArray (TAG) length minus 4

    BA_BMD_Frame.position = 0; //# Reset position

}


public function encode_SCREENVIDEO ( input_BA : ByteArray , input_BMD : BitmapData ) : void //ByteArray
{
    var w:int = input_BMD.width; var h:int = input_BMD.height;

    //# Video Type = 1 (Keyframe) |&&| Codec ID = 3 (Screen-Video)
    input_BA.writeByte(0x13); 

    //# SCREENVIDEOPACKET 'header'
    writeUI4_12( input_BA, int( (BLOCK_WIDTH  /16) - 1 ),  w ); //12 bits for width
    writeUI4_12( input_BA, int( (BLOCK_HEIGHT /16) - 1 ),  h ); //12 bits for height

    //# Create IMAGEBLOCKS

    const BLOCK_WIDTH:int = input_BMD.width; //# is 100;
    const BLOCK_HEIGHT:int = input_BMD.height; //# is 50;

    var rowMax:int = int(h / BLOCK_HEIGHT);
    var rowRemainder:int = h % BLOCK_HEIGHT; 
    if (rowRemainder > 0) rowMax += 1;

    var colMax:int = int(w / BLOCK_WIDTH);
    var colRemainder:int = w % BLOCK_WIDTH;             
    if (colRemainder > 0) colMax += 1;

    var block:ByteArray = new ByteArray();
    block.endian = Endian.LITTLE_ENDIAN;

    for (var row:int = 0; row < rowMax; row++)
    {
        for (var col:int = 0; col < colMax; col++) 
        {
            var xStart:uint = col * BLOCK_WIDTH;
            var xLimit:int = (colRemainder > 0 && col + 1 == colMax) ? colRemainder : BLOCK_WIDTH;
            var xEnd:int = xStart + xLimit;

            var yStart:uint = h - (row * BLOCK_HEIGHT); //# Read BMP Data from bottom to top
            var yLimit:int = (rowRemainder > 0 && row + 1 == rowMax) ? rowRemainder : BLOCK_HEIGHT; 
            var yEnd:int = yStart - yLimit;

            block.clear(); //# re-use ByteArray

            for (var y:int = yStart-1; y >= yEnd; y--) //# FLV stores Bitmap Data from bottom to top)
            {
                for (var x:int = xStart; x < xEnd; x++) 
                {
                    var p:uint = input_BMD.getPixel(x, y);
                    writeUInt24( block, p ); //# write B-G-R pixel values 
                }
            }

            block.compress();

            input_BA.writeShort(block.length); // write block length (2 bytes == 16 bits)
            input_BA.writeBytes( block ); // write block
        }
    }

    block.length = 0; block = null;
    input_BA.position = input_BA.length;
}

//// Supporting functions

public function writeUInt24( input_BA:ByteArray, val:uint ) : void
{
    input_BA.position = input_BA.position;

    temp_Int_1 = val >> 16;
    temp_Int_2 = val >> 8 & 0xff;
    temp_Int_3 = val & 0xff;

    input_BA.writeByte(temp_Int_1); input_BA.writeByte(temp_Int_2);
    input_BA.writeByte(temp_Int_3);
}

public function writeUI4_12(input_BA:ByteArray, p1:uint, p2:uint):void
{
    // writes a 4-bit value followed by a 12-bit value in a total of 16 bits (2 bytes)

    var byte1a:int = p1 << 4;
    var byte1b:int = p2 >> 8;
    var byte1:int = byte1a + byte1b;
    var byte2:int = p2 & 0xff;

    input_BA.writeByte(byte1);  input_BA.writeByte(byte2);
}   
VC.One
  • 14,790
  • 4
  • 25
  • 57