bentasker / HLS-Stream-Creator

Simple Bash Script to take a media file, segment it and create an M3U8 playlist for serving using HLS
BSD 3-Clause "New" or "Revised" License
273 stars 101 forks source link

Error: Measured peak bitrate compared to master playlist declared value exceeds error tolerance #39

Open samueleastdev opened 4 years ago

samueleastdev commented 4 years ago

Hi @bentasker

Not sure if this is an issue because I get the same error if I test with various streams but curious on why you think this may be occurring.

If you download the hls validation tools from here https://developer.apple.com/download/more/?=http%20live%20streaming%20tools

And then run the mediastreamvalidator tool on a created playlist you should see errors similar to this.

--------------------------------------------------------------------------------
MUST fix issues
--------------------------------------------------------------------------------

Error: Measured peak bitrate compared to master playlist declared value exceeds error tolerance
--> Detail:  Measured: 590.99 kb/s, Master playlist: 256.00 kb/s, Error: 130.86%
--> Source:  mermaid.mov_master.m3u8
--> Compare: mermaid.mov_256.m3u8

--> Detail:  Measured: 395.00 kb/s, Master playlist: 128.00 kb/s, Error: 208.59%
--> Source:  mermaid.mov_master.m3u8
--> Compare: mermaid.mov_128.m3u8

--> Detail:  Measured: 260.27 kb/s, Master playlist: 28.00 kb/s, Error: 829.52%
--> Source:  mermaid.mov_master.m3u8
--> Compare: mermaid.mov_28.m3u8

--> Detail:  Measured: 282.60 kb/s, Master playlist: 64.00 kb/s, Error: 341.55%
--> Source:  mermaid.mov_master.m3u8
--> Compare: mermaid.mov_64.m3u8

Is this something to be concerned about or is it just a guide to follow.

Thanks

bentasker commented 4 years ago

Hi,

It seems you've been quite unlucky :)

First, to explain what it's warning about.

In your encode, you've specified 4 bitrates - -b 28,64,128,256. These (obviously) get passed through to ffmpeg and also written into the master playlist.

But, by default ffmpeg will use a Variable Bit-Rate. In effect, it tries to keep the average bitrate to the one you've specified, so there may be spikes above it, and dips below it.

The apple tool checks the maximum bitrate observed within the stream and compares it to that declared in the manifest. As you can see, sometimes it's spiked much higher than that declared.

A bit of variance isn't something to be too concerned about, but significant variance could cause playback issues. Adaptive players will try to calculate the bandwidth available to them and playback the most appropriate stream (actually, most take resolution into consideration too, but lets ignore that).

One problem comes when the player decides there's (say) 80Kbps bandwidth, and selects your 64Kbps representation. Because there are peaks up to 260Kbps (i.e. 3.25x the available bandwidth) playback may stall. Whether it actually does depends on a variety of things (like what the bitrates leading up to that spike were - if you've a long succession of lows followed by a spike, the player may have had time to fetch it ahead of time).

When encoding video, though, most will go with VBR because it traditionally gives higher quality files (being able to switch the bitrate down when "safe" allows for higher spikes later etc etc)

If you run HLS-Stream-Creator with -C it will tell ffmpeg to use CBR instead, which should please the tool.

I've not personally had any delivery issues from using VBR, but as described above it is a technical possibility

samueleastdev commented 4 years ago

Thanks, @bentasker really appreciate the detailed answer.

I have been testing a few different FFmpeg commands mainly this one.

https://docs.peer5.com/guides/production-ready-hls-vod/

I don't think I was really understanding the relationship between the bitrate in the master manifest and -b:v option in the FFmpeg command.

In the option at the link above the master, manifest bandwidths are.

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
360p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=842x480
480p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720
720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p.m3u8

Obviously I understand now that the BANDWIDTH value is set by the -b:v option

ffmpeg -hide_banner -y -i beach.mkv \
  -vf scale=w=640:h=360:force_original_aspect_ratio=decrease -c:a aac -ar 48000 -c:v h264 -profile:v main -crf 20 -sc_threshold 0 -g 48 -keyint_min 48 -hls_time 4 -hls_playlist_type vod  -b:v 800k -maxrate 856k -bufsize 1200k -b:a 96k -hls_segment_filename beach/360p_%03d.ts beach/360p.m3u8 \
  -vf scale=w=842:h=480:force_original_aspect_ratio=decrease -c:a aac -ar 48000 -c:v h264 -profile:v main -crf 20 -sc_threshold 0 -g 48 -keyint_min 48 -hls_time 4 -hls_playlist_type vod -b:v 1400k -maxrate 1498k -bufsize 2100k -b:a 128k -hls_segment_filename beach/480p_%03d.ts beach/480p.m3u8 \
  -vf scale=w=1280:h=720:force_original_aspect_ratio=decrease -c:a aac -ar 48000 -c:v h264 -profile:v main -crf 20 -sc_threshold 0 -g 48 -keyint_min 48 -hls_time 4 -hls_playlist_type vod -b:v 2800k -maxrate 2996k -bufsize 4200k -b:a 128k -hls_segment_filename beach/720p_%03d.ts beach/720p.m3u8 \
  -vf scale=w=1920:h=1080:force_original_aspect_ratio=decrease -c:a aac -ar 48000 -c:v h264 -profile:v main -crf 20 -sc_threshold 0 -g 48 -keyint_min 48 -hls_time 4 -hls_playlist_type vod -b:v 5000k -maxrate 5350k -bufsize 7500k -b:a 192k -hls_segment_filename beach/1080p_%03d.ts beach/1080p.m3u8

And your generated manifest is the same.

#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=28000
mertime.mp4_28.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=64000
mertime.mp4_64.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=128000
mertime.mp4_128.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=256000
mertime.mp4_256.m3u8

Relating to -b 28,64,128,256

I also use the AWS Media Convert encoder which outputs this in the manifest.

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=513609,AVERAGE-BANDWIDTH=498366,CODECS="avc1.4d401f,mp4a.40.5",RESOLUTION=480x270,FRAME-RATE=14.985
master_Ott_Hls_Ts_Avc_Aac_16x9_480x270p_15Hz_400Kbps.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=745121,AVERAGE-BANDWIDTH=718453,CODECS="avc1.4d401f,mp4a.40.5",RESOLUTION=640x360,FRAME-RATE=29.970
master_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_30Hz_600Kbps.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3912327,AVERAGE-BANDWIDTH=3738736,CODECS="avc1.640028,mp4a.40.5",RESOLUTION=960x540,FRAME-RATE=29.970
master_Ott_Hls_Ts_Avc_Aac_16x9_960x540p_30Hz_3500Kbps.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5413455,AVERAGE-BANDWIDTH=5269104,CODECS="avc1.640028,mp4a.40.5",RESOLUTION=1280x720,FRAME-RATE=29.970
master_Ott_Hls_Ts_Avc_Aac_16x9_1280x720p_30Hz_5000Kbps.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=9498225,AVERAGE-BANDWIDTH=8904479,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=29.970
master_Ott_Hls_Ts_Avc_Aac_16x9_1920x1080p_30Hz_8500Kbps.m3u8

Not sure how they are grabbing the bandwidth, whether it is done via a two pass as they don't seem to be set values?

bentasker commented 4 years ago

Hi,

After generation, they probably check the stream with the equivalent of a ffprobe -i [stream] to extract those details.

If you're so minded, you can get ffprobe to dump details out in JSON to make extracting things a little easier.

I do something similar (though it's on the input rather than the output) here - https://snippets.bentasker.co.uk/page-1804131128-Automating-HLS-Stream-Creator-Python.html

I think, if you wanted to declare AVERAGE-BANDWIDTH and FRAME-RATE in your manifest, you could probably write a little bit of code to iterate over entries in the manifest and probe them

This

for manifest in $MASTER
do

ffprobe -show_streams -print_format json -loglevel quiet "$manifest"
done

Will dump out info, but you're probably best doing the probe (and manifest rewrite) in Python. One thing to watch for is the difference in fields (both names and presence) depending on the codec.

samueleastdev commented 4 years ago

Hi @bentasker

Yes, that's exactly what I was initially trying to do with ffprobe.

I would get the following output when running on the input file.

{
    "streams": [
        {
            "index": 0,
            "codec_name": "h264",
            "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
            "profile": "High",
            "codec_type": "video",
            "codec_time_base": "1001/48000",
            "codec_tag_string": "avc1",
            "codec_tag": "0x31637661",
            "width": 1920,
            "height": 816,
            "coded_width": 1920,
            "coded_height": 816,
            "has_b_frames": 2,
            "pix_fmt": "yuv420p",
            "level": 40,
            "chroma_location": "left",
            "refs": 1,
            "is_avc": "true",
            "nal_length_size": "4",
            "r_frame_rate": "24000/1001",
            "avg_frame_rate": "24000/1001",
            "time_base": "1/24000",
            "start_pts": 0,
            "start_time": "0.000000",
            "duration_ts": 4060056,
            "duration": "169.169000",
            "bit_rate": "2317499",
            "bits_per_raw_sample": "8",
            "nb_frames": "4056",
            "disposition": {
                "default": 1,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "language": "eng",
                "handler_name": "Apple Video Media Handler"
            }
        },
        {
            "index": 1,
            "codec_name": "aac",
            "codec_long_name": "AAC (Advanced Audio Coding)",
            "profile": "LC",
            "codec_type": "audio",
            "codec_time_base": "1/44100",
            "codec_tag_string": "mp4a",
            "codec_tag": "0x6134706d",
            "sample_fmt": "fltp",
            "sample_rate": "44100",
            "channels": 2,
            "channel_layout": "stereo",
            "bits_per_sample": 0,
            "r_frame_rate": "0/0",
            "avg_frame_rate": "0/0",
            "time_base": "1/44100",
            "start_pts": 0,
            "start_time": "0.000000",
            "duration_ts": 7460838,
            "duration": "169.180000",
            "bit_rate": "127635",
            "max_bit_rate": "128000",
            "nb_frames": "7287",
            "disposition": {
                "default": 1,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "language": "eng",
                "handler_name": "Apple Sound Media Handler"
            }
        }
    ],
    "format": {
        "filename": "mertime.mp4",
        "nb_streams": 2,
        "nb_programs": 0,
        "format_name": "mov,mp4,m4a,3gp,3g2,mj2",
        "format_long_name": "QuickTime / MOV",
        "start_time": "0.000000",
        "duration": "169.204000",
        "size": "51835599",
        "bit_rate": "2450797",
        "probe_score": 100,
        "tags": {
            "major_brand": "isom",
            "minor_version": "512",
            "compatible_brands": "isomiso2avc1mp41",
            "encoder": "Lavf58.34.101"
        }
    }
}

This outputs everything needed but the bit_rate is (2317499) obviously a single value for the input video I wasn't sure how to work out the BANDWIDTH master.m3u8 manifest values for the different resolutions 360p, 480p, 720p, 1080p.

Also if I run ffprobe in the master.m3u8 file after encoding to hls I get this.

{
    "streams": [{
            "index": 0,
            "codec_name": "h264",
            "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
            "profile": "Main",
            "codec_type": "video",
            "codec_time_base": "1001/48000",
            "codec_tag_string": "[27][0][0][0]",
            "codec_tag": "0x001b",
            "width": 640,
            "height": 272,
            "coded_width": 640,
            "coded_height": 272,
            "has_b_frames": 2,
            "pix_fmt": "yuv420p",
            "level": 21,
            "chroma_location": "left",
            "refs": 1,
            "is_avc": "false",
            "nal_length_size": "0",
            "r_frame_rate": "24000/1001",
            "avg_frame_rate": "24000/1001",
            "time_base": "1/90000",
            "start_pts": 7508,
            "start_time": "0.083422",
            "bits_per_raw_sample": "8",
            "disposition": {
                "default": 0,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "variant_bitrate": "800000"
            }
        },
        {
            "index": 1,
            "codec_name": "aac",
            "codec_long_name": "AAC (Advanced Audio Coding)",
            "profile": "LC",
            "codec_type": "audio",
            "codec_time_base": "1/48000",
            "codec_tag_string": "[15][0][0][0]",
            "codec_tag": "0x000f",
            "sample_fmt": "fltp",
            "sample_rate": "48000",
            "channels": 2,
            "channel_layout": "stereo",
            "bits_per_sample": 0,
            "r_frame_rate": "0/0",
            "avg_frame_rate": "0/0",
            "time_base": "1/90000",
            "start_pts": 5588,
            "start_time": "0.062089",
            "disposition": {
                "default": 0,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "variant_bitrate": "800000"
            }
        },
        {
            "index": 2,
            "codec_name": "h264",
            "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
            "profile": "Main",
            "codec_type": "video",
            "codec_time_base": "1001/48000",
            "codec_tag_string": "[27][0][0][0]",
            "codec_tag": "0x001b",
            "width": 842,
            "height": 358,
            "coded_width": 848,
            "coded_height": 368,
            "has_b_frames": 2,
            "pix_fmt": "yuv420p",
            "level": 30,
            "chroma_location": "left",
            "refs": 1,
            "is_avc": "false",
            "nal_length_size": "0",
            "r_frame_rate": "24000/1001",
            "avg_frame_rate": "24000/1001",
            "time_base": "1/90000",
            "start_pts": 7508,
            "start_time": "0.083422",
            "bits_per_raw_sample": "8",
            "disposition": {
                "default": 0,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "variant_bitrate": "1400000"
            }
        },
        {
            "index": 3,
            "codec_name": "aac",
            "codec_long_name": "AAC (Advanced Audio Coding)",
            "profile": "LC",
            "codec_type": "audio",
            "codec_time_base": "1/48000",
            "codec_tag_string": "[15][0][0][0]",
            "codec_tag": "0x000f",
            "sample_fmt": "fltp",
            "sample_rate": "48000",
            "channels": 2,
            "channel_layout": "stereo",
            "bits_per_sample": 0,
            "r_frame_rate": "0/0",
            "avg_frame_rate": "0/0",
            "time_base": "1/90000",
            "start_pts": 5588,
            "start_time": "0.062089",
            "disposition": {
                "default": 0,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "variant_bitrate": "1400000"
            }
        },
        {
            "index": 4,
            "codec_name": "h264",
            "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
            "profile": "Main",
            "codec_type": "video",
            "codec_time_base": "1001/48000",
            "codec_tag_string": "[27][0][0][0]",
            "codec_tag": "0x001b",
            "width": 1280,
            "height": 544,
            "coded_width": 1280,
            "coded_height": 544,
            "has_b_frames": 2,
            "pix_fmt": "yuv420p",
            "level": 31,
            "chroma_location": "left",
            "refs": 1,
            "is_avc": "false",
            "nal_length_size": "0",
            "r_frame_rate": "24000/1001",
            "avg_frame_rate": "24000/1001",
            "time_base": "1/90000",
            "start_pts": 7508,
            "start_time": "0.083422",
            "bits_per_raw_sample": "8",
            "disposition": {
                "default": 0,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "variant_bitrate": "2800000"
            }
        },
        {
            "index": 5,
            "codec_name": "aac",
            "codec_long_name": "AAC (Advanced Audio Coding)",
            "profile": "LC",
            "codec_type": "audio",
            "codec_time_base": "1/48000",
            "codec_tag_string": "[15][0][0][0]",
            "codec_tag": "0x000f",
            "sample_fmt": "fltp",
            "sample_rate": "48000",
            "channels": 2,
            "channel_layout": "stereo",
            "bits_per_sample": 0,
            "r_frame_rate": "0/0",
            "avg_frame_rate": "0/0",
            "time_base": "1/90000",
            "start_pts": 5588,
            "start_time": "0.062089",
            "disposition": {
                "default": 0,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "variant_bitrate": "2800000"
            }
        },
        {
            "index": 6,
            "codec_name": "h264",
            "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
            "profile": "Main",
            "codec_type": "video",
            "codec_time_base": "1001/48000",
            "codec_tag_string": "[27][0][0][0]",
            "codec_tag": "0x001b",
            "width": 1920,
            "height": 816,
            "coded_width": 1920,
            "coded_height": 816,
            "has_b_frames": 2,
            "pix_fmt": "yuv420p",
            "level": 40,
            "chroma_location": "left",
            "refs": 1,
            "is_avc": "false",
            "nal_length_size": "0",
            "r_frame_rate": "24000/1001",
            "avg_frame_rate": "24000/1001",
            "time_base": "1/90000",
            "start_pts": 7508,
            "start_time": "0.083422",
            "bits_per_raw_sample": "8",
            "disposition": {
                "default": 0,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "variant_bitrate": "5000000"
            }
        },
        {
            "index": 7,
            "codec_name": "aac",
            "codec_long_name": "AAC (Advanced Audio Coding)",
            "profile": "LC",
            "codec_type": "audio",
            "codec_time_base": "1/48000",
            "codec_tag_string": "[15][0][0][0]",
            "codec_tag": "0x000f",
            "sample_fmt": "fltp",
            "sample_rate": "48000",
            "channels": 2,
            "channel_layout": "stereo",
            "bits_per_sample": 0,
            "r_frame_rate": "0/0",
            "avg_frame_rate": "0/0",
            "time_base": "1/90000",
            "start_pts": 5588,
            "start_time": "0.062089",
            "disposition": {
                "default": 0,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "variant_bitrate": "5000000"
            }
        }
    ],
    "format": {
        "filename": "master.m3u8",
        "nb_streams": 8,
        "nb_programs": 4,
        "format_name": "hls",
        "format_long_name": "Apple HTTP Live Streaming",
        "start_time": "0.062089",
        "duration": "169.180238",
        "size": "575",
        "bit_rate": "27",
        "probe_score": 100
    }
}

And the validator output is.

Error: Measured peak bitrate compared to master playlist declared value exceeds error tolerance
--> Detail:  Measured: 5939.35 kb/s, Master playlist: 5000.00 kb/s, Error: 18.79%
--> Source:  https://d25hd5yfabpc2n.cloudfront.net/s3test1/master.m3u8
--> Compare: 1080/1080p.m3u8

--> Detail:  Measured: 1694.70 kb/s, Master playlist: 1400.00 kb/s, Error: 21.05%
--> Source:  https://d25hd5yfabpc2n.cloudfront.net/s3test1/master.m3u8
--> Compare: 480/480p.m3u8

--> Detail:  Measured: 3192.97 kb/s, Master playlist: 2800.00 kb/s, Error: 14.03%
--> Source:  https://d25hd5yfabpc2n.cloudfront.net/s3test1/master.m3u8
--> Compare: 720/720p.m3u8

--> Detail:  Measured: 1066.33 kb/s, Master playlist: 800.00 kb/s, Error: 33.29%
--> Source:  https://d25hd5yfabpc2n.cloudfront.net/s3test1/master.m3u8
--> Compare: 360/360p.m3u8

I can't figure out where the validator is getting its bitrate values from.

--> Detail:  Measured: 5939.35 kb/s, Master playlist: 5000.00 kb/s, Error: 18.79%

But maybe this isn't something to worry about.

Thanks