w3c / webcodecs

WebCodecs is a flexible web API for encoding and decoding audio and video.
https://w3c.github.io/webcodecs/
Other
934 stars 131 forks source link

Consider marking an I-frame with Recovery Point SEI message as h264 key frame #650

Open reinhrst opened 1 year ago

reinhrst commented 1 year ago

At start of decode (and after a flush), WebCodecs VideoDecoder demands a keyframe which at the moment is defined as an IDR frame.

H264 has the concept of a Recovery Point SEI Message (D.2.8 in the (08.21) h264 spec): "The recovery point SEI message assists a decoder in determining when the decoding process will produce acceptable pictures for display after the decoder initiates random access or after the encoder indicates a broken link in the coded video sequence.".

So (afaict) an I-frame with a such a SEI message is meant to be usable as start frame for a decoding operation.

ffprobe also marks these frames as key-frames.

I don't have enough data to comment on how often this happens in real-live video streams; personally I have 1000s of hours of videos taken with different JVC / Sony camcorders (timelaps recordings, used in animal conservation projects), which have the following properties:

Not being able to start decoding on I-frame + SEI means that:

Solution on client side (short of recoding, which results in unacceptable quality loss) that kind of seems to work (but probably a very bad idea) is to add a dummy-IDR frame that I offer to the decoder before feeding the real stream (and then dropping the first frame of the output).

orange4glace commented 1 year ago

I have a similar question, I'm trying to decode h264 stream from mp4 file. In STSS box, it says such sample is sync_frame but when inspecting its actual sample data, it is consisted with 2 NALU, one is 5 byte SEI (0x06) and another one is non-IDR (0x41) with picture data. But when inspecting with ffprobe, it says it is I-frame (and also key-frame) even though I don't know why (since I'm newbie to media processing). I want to start decoding with such sample but it errors that VideoDecoder needs key-frame. Is it also related with this issue?

reinhrst commented 1 year ago

Very likely. FFProbe returns the recovery point SEI messages as IFrames (whereas technically they are not, and the VideoDecoder spec does not edit (edits in bold): sorry, it's been a while since I dove into the details here. they ARE I frames, just not IDR frames. iirc, ffprobe labels them as key frames, whereas for VideoDecoder they are not enough of a keyframe).

I had limited success with rewriting the first frame to identify as an IDR-Frame; the decoder will show a green screen, but after a couple of frames I got an image (using the software decoder in Chrome). Although, this is obviously very hacky and should (probably) not be tried in production.

I do feel the first frame should have enough data to actually be an IDR-Frame, so it should (in theory) be possible to reencode only the first frame to be an IDR-Frame, but no idea how complex this is (without external tools like ffmpeg).

seflless commented 1 year ago

@reinhrst Do you have an example video file that exhibits this? I'd love to test if our video playback system handles it. Would be much appreciated.

reinhrst commented 1 year ago

@seflless I have a whole bunch of 4GB video files with this behaviour, however I can see if I can convince ffmpeg to cut out the first couple of minutes :). Will send them to you in a PM, since I'm not 100% sure the copyright owner would agree with me making them public.

Out of interest, when you say "our video playback system", what system are you talking about?

sandersdan commented 1 year ago

Not all decoders support starting at SEI recovery points, so if this feature were to be added it would likely need to be an optional extension. I'm not immediately sure what such an API would look like, it could be as simple as allowing feeding a non-keyframe and you take your chances as to whether the decode will fail.

That said, there is little difference between recovery_frame_cnt=0 and an IDR, so I'm a little confused as to why the camera wouldn't just make a real IDR here. It's plausible that almost all decoders would support decoding from such an I frame.

reinhrst commented 1 year ago

That said, there is little difference between recovery_frame_cnt=0 and an IDR, so I'm a little confused as to why the camera wouldn't just make a real IDR here. It's plausible that almost all decoders would support decoding from such an I frame.

@sandersdan I was struggling with the same question, and tried to ask it on stack-overflow, did not get a conclusive answer...

My hunch right now is this:

Hence, playback can start at the SEI recovery point (and all frames that come after in presentation order can be decoded), however there may be frames with earlier presentation order that need to be dropped by the decoder (in other words, a decoder can not drop the decoded frame cache on SEI recovery point).

This means that you can (usually) have 2 additional B frames in your GOP (also see the "updated" section in the linked stackoverflow question), meaning you can get better compression for the same quality.

I would be more than happy for someone with more knowledge on the subject to confirm/reject my theory.

seflless commented 1 year ago

@reinhrst That'd be awesome if you could send over a smaller version, big versions are fine if you are strapped for time. I can't say what I'm building just yet, will be public soon enough, definitely not in a public comment at least.

marcello3d commented 1 year ago

We have come across a file that seems to have this issue (I believe it was downloaded off YouTube).

Because we're demuxing using libav.js, it considers the frames keyframes and I don't see a way to figure out this "keyframe but not really a keyframe" distinction from it.

If we seek to start decoding from one of them, VideoDecoder.decode synchronously throws DOMException: Failed to execute 'decode' on 'VideoDecoder': A key frame is required after configure() or flush(). (I confirmed that the decoder state is 'configured' and EncodedVideoChunk.type is 'key').

try {
    this.decoder.decode(chunk);
} catch (e) {
    console.error(`[${id}] error decoding chunk (decoder state = ${this.decoder.state})`, chunk, e);
    throw e;
}
reinhrst commented 1 year ago

@seflless I emailed you a video last week that I now can confirm indeed starts with 216 frames before the first IDR frame (18 of those 216 frames were I-Frames with Recovery Point).

In the links below I share the first 10 seconds (250 frames) of this video:

Considering that the first IDR frame is only in second 8.5, if you see anything more than 1.5 seconds of video, your video player starts decoding at the first I-frame with Recovery Info (all desktop players I have tried, do so, but I'm sure I did not test excessively). The first timestamp you see (burned into the video) is around 5.8.2022 10:21:52.

Note that the video is a timelapse (it was recorded at 1 frame per second, shown at 25 frames per second), and it's an interlaced format (which is why ffprobe sometimes claims the mp4 file is 50 fps.

The original files from the camcorder are .MTS, however the h264 frames have been copied 1:1 into these new files.

seflless commented 1 year ago

@seflless I missed that email, very helpful, thank you. I'll dig into this more when I'm back on the task, busy with some other priorities at the moment. These are some scary files, engineering wise :)

sandersdan commented 1 year ago

The interpretation in https://github.com/w3c/webcodecs/issues/650#issuecomment-1539740092 makes sense to me. I'm not sure if we would want such a frame to be called "key", but if not we could also make a new type, perhaps "recovery". This makes UA support detectable and lets us specify extra rules if we need to.

I don't know whether we need per-codec feature detection for this, but if we do then we can make it a configuration flag, eg. {codec: 'avc1.420034', recoveryChunks: true}.

reinhrst commented 7 months ago

After a discussion with the maintainer of libav.js, I understand that there is no way in libav to distinguish between a proper IDR frame and an I-frame with a Recovery Point message. Both have the AV_PKT_FLAG_KEY flag set.

This means that right now the solution seems to be to either manually decode the packet, or just feed it to WebCodecs, and try another packet in case of an error until you find a packet that works. Afterwards you can then feed the original packet. Quite messy....

The interpretation in #650 (comment) makes sense to me. I'm not sure if we would want such a frame to be called "key", but if not we could also make a new type, perhaps "recovery". This makes UA support detectable and lets us specify extra rules if we need to.

I don't know whether we need per-codec feature detection for this, but if we do then we can make it a configuration flag, eg. {codec: 'avc1.420034', recoveryChunks: true}.

I see how it makes sense to have a different type for this if e.g. you output them (through VideoEncoder). It would be great though if during input to VideoDecoder, we would not have to set these types (or maybe have something different like type: "key_or_recovery" or type: "auto"), since demuxers (like libav) may not share this information.

marcello3d commented 7 months ago

Not all decoders support starting at SEI recovery points, so if this feature were to be added it would likely need to be an optional extension.

Is it possible this isn't true? If I download any YouTube video into a mp4 it seems to have these special keyframes, how does the browser handle them in the <video> tag?

sandersdan commented 7 months ago

Is it possible this isn't true?

Any H.264 decoder can decode them, what isn't guaranteed is starting playback at (ie. seeking to) them.

It is possible that ~every decoder in active use on desktop/mobile can support this. I suspect that is not true for embedded, but I am not certain.

When recovery_frame_cnt = 0, it should be sufficient for a decoder to simply allow decoding to start at a non-keyframe (it must handle gaps_in_frame_num for the first frame). When recovery_frame_cnt > 0, a decoder must additionally support some form of error resiliency (it must be able to track or recover from missing reference frames).

how does the browser handle them in the <video> tag?

For quite some time Chrome's implementation of hardware decoding did not support SEI recovery on any platform, and would fall back to software decoding when it was detected. There are still cases where recovery_frame_cnt > 0 is not supported.

It's also possible to start playback at an earlier true IDR; by spec there must be one (although I have seen media that violates this requirement). This wastes resources decoding additional unused frames when seeking.

reinhrst commented 7 months ago

When recovery_frame_cnt = 0, it should be sufficient for a decoder to simply allow decoding to start at a non-keyframe (it must handle gaps_in_frame_num for the first frame). When recovery_frame_cnt > 0, a decoder must additionally support some form of error resiliency (it must be able to track or recover from missing reference frames).

Would it be too simple to think that this could be solved easily by handing the decoder a completely green key frame (or multiple if recovery_frame_cnt > 0) and then ignore this frame in the output?

It's also possible to start playback at an earlier true IDR; by spec there must be one (although I have seen media that violates this requirement). This wastes resources decoding additional unused frames when seeking.

Both true. The files that originally sparked this topic are a large MT2S video stream, that gets cut by the camcorder (on an I-frame but not necessarily an IDR-frame) so that files don't grow larger than 4GB. I guess in theory (if stop recording right after the cut is made), you may even end up with a file without any IDR frames.

sandersdan commented 7 months ago

Would it be too simple to think that this could be solved easily by handing the decoder a completely green key frame (or multiple if recovery_frame_cnt > 0) and then ignore this frame in the output?

I believe this can be made to work, but you have to create frames with very specific headers, and the complexity of doing that is substantial (similar to writing an unoptimized H.264 encoder that supports all possible profiles). It is frustratingly a lot easier to implement this sort of recovery inside of a decoder.

jyavenard commented 7 months ago

It's also possible to start playback at an earlier true IDR; by spec there must be one (although I have seen media that violates this requirement). This wastes resources decoding additional unused frames when seeking.

Streams with no IDR but only SEI recovery are not uncommon in the broadcasting world. I've seen plenty of HLS content / MPEG-TS from some broadcaster (particularly satellite broadcast ones) with them. It allows for consistent bitrate.

Yahweasel commented 7 months ago

Just to be clear, the reason why libav.js marks these frames as keyframes, aside from the fact that they are, is that it's just taking that data from the demuxer. It does not invoke any packet parser to determine keyframe status. This choice in WebCodecs's definition makes it exceedingly difficult to actually use WebCodecs with any real file formats, because when I read a packet from an MP4 file, or a Matroska file, or a MPEG-TS file, or anything else, those chunks are marked in the format header as being keyframes. Munging them into WebCodecs's definition would require changing the entire stack and all preexisting files, or parsing frames twice (once to determine if they're a keyframe or a super-ultra keyframe, and once to actually decode them).

sandersdan commented 7 months ago

Just to be clear, the reason why libav.js marks these frames as keyframes, aside from the fact that they are, is that it's just taking that data from the demuxer.

Terminology is perhaps ambiguous here. SEI recovery frames are I frames but not IDR frames (H.264 terminology). This makes them recovery points (roll=0) but not sync samples (ISO BMFF terminology). Right now, WebCodecs is equating "key" to "sync sample".

WebCodecs is strict about this because MSE was originally not, and that led to content with intentionally mis-marked keyframes. I'm confident that we'll eventually find the right set of tradeoffs, but it's going to be by cautiously removing restrictions.

Based on experience with MSE, I would not expect all muxers to correctly mark recovery points, but at least some do, and in that case a demuxer can distinguish them without parsing the bitstream.

sandersdan commented 7 months ago

Streams with no IDR but only SEI recovery are not uncommon in the broadcasting world. [..] It allows for consistent bitrate.

Recovery frames with recovery_frame_cnt = 0 shouldn't affect bitrate much compared to full IDR. There is also rolling intra where recovery_frame_cnt > 0; that's much more consistent but does require compatible decoders. (Conveniently for cable providers they do get to control the decode hardware. I don't know if similar applies to OTA, but it would make sense to standardize support.)

reinhrst commented 6 months ago

That said, there is little difference between recovery_frame_cnt=0 and an IDR, so I'm a little confused as to why the camera wouldn't just make a real IDR here. It's plausible that almost all decoders would support decoding from such an I frame.

Much later, and I have a better answer (also posted in detail here on stackoverflow).

In short (unless noted otherwise, all orders are presentation order):

Note that this is also the reason that the stream is completely happy starting with two B frames (in PO).

aged-urchin commented 3 months ago

i think an open GOP should start with an i frame with a recovery point SEI instead of an IDR frame.