0

I need to create multiple copies of a source video file stamped with a logo. Also, each copy is overlaid with a unique version number, intermittently appearing throughout the video. Since I’m transcoding to HLS, I thought I could use a master HLS package and stitch the numbered segments in the video, thus transcode only a fractions of the source for each numbered copy.

I created two FFmpeg scripts. Script one creates the ‘main” HLS video of the whole file, with a logo overlay. Script two generates 2 segments at specified intervals, which are overlaid with unique numbers. The final variant playlist looks something like this:
Seg001.ts
Seg002.ts
Seg003_alt.ts
Seg004_alt.ts

Seg005.ts
seg006.ts…

Command to create the ‘main’ HLS package is: (all vars expanded for debugging. The actual command is dynamic to encompass different input file size, fps, vmaf score…)

$ffmpeg -y -analyzeduration 1000000 -copyts -r 25 -hide_banner -i $inputFile -i $logo -t 120 -filter_complex \
"[0:a]aformat=channel_layouts=stereo,aresample=async=1000[a1]; \
[0:v]setpts=PTS-STARTPTS,yadif=0:-1:1,overlay,setdar=16/9,split=3[v1][v2][v3]; \
[v1]scale=640x360:force_original_aspect_ratio=decrease:sws_dither=ed:flags=lanczos[v1]; \
[v2]scale=1024x576:force_original_aspect_ratio=decrease:sws_dither=ed:flags=lanczos[v2]; \
[v3]scale=1600x900:force_original_aspect_ratio=decrease:sws_dither=ed:flags=lanczos[v3]" \
-map "[a1]" -vn -c:a libfdk_aac -ac 2 -b:a 96k -ar 48000 -bitexact -r 25 -y -f segment -segment_list_type m3u8 -segment_format mpegts -segment_time 6 -segment_list path/to/playlist0.m3u8 path/to/segments0.aac \<br>
-map "[v1]" -an -c:v libx264 -crf 23 -muxpreload 0 -vsync 0 -write_tmcd 0 -maxrate 550k -bufsize 1100k -bsf:v h264_mp4toannexb -forced-idr 1 -sc_threshold 0 -shortest -r 25 -g 75 -profile:v main -level 3.1 -preset medium -tune film -coder 0 -pix_fmt yuv420p -individual_header_trailer 0 -flags +loop+mv4+cgop -flags2 +local_header -movflags faststart -cmp chroma -keyint_min 75 -threads 14 -y -f segment -segment_list_type m3u8 -segment_list_flags -cache -segment_format mpegts -segment_time 6 -segment_list path/to/playlist1.m3u8 path/to/segments1.ts \
-map "[v2]" -an -c:v libx264 -crf 23 -muxpreload 0 -vsync 0 -write_tmcd 0 -maxrate 1400k -bufsize 2800k -bsf:v h264_mp4toannexb -forced-idr 1 -sc_threshold 0 -shortest -r 25 -g 75 -profile:v main -level 4 -preset medium -tune film -coder 1 -pix_fmt yuv420p -individual_header_trailer 0 -flags +loop+mv4+cgop -flags2 +local_header -movflags faststart -cmp chroma -keyint_min 75 -threads 14 -y -f segment -segment_list_type m3u8 -segment_list_flags -cache -segment_format mpegts -segment_time 6 -segment_list path/to/playlist2.m3u8 path/to/segments2.ts \
-map "[v3]" -an -c:v libx264 -crf 23 -muxpreload 0 -vsync 0 -write_tmcd 0 -maxrate 3100k -bufsize 6200k -bsf:v h264_mp4toannexb -forced-idr 1 -sc_threshold 0 -shortest -r 25 -g 75 -profile:v high -level 4 -preset medium -tune film -coder 1 -pix_fmt yuv420p -individual_header_trailer 0 -flags +loop+mv4+cgop -flags2 +local_header -movflags faststart -cmp chroma -keyint_min 75 -threads 14 -y -f segment -segment_list_type m3u8 -segment_list_flags -cache -segment_format mpegts -segment_time 6 -segment_list path/to/playlist3.m3u8 path/to/segments3.ts

After the above run, I run the following command for the ‘child’ (stitched) segments:

$ffmpeg -y -analyzeduration 1000000 -r 25 -hide_banner -ss $from -t $time -i $inputFile -i $logo -filter_complex \
"[0:v]setpts=PTS,yadif=0:-1:0,overlay,setdar=16/9[logo]; \
[logo]drawbox=x=400:y=52:w=210:h=80:t=fill:c=black@1,drawtext=fontfile=$font:text='1234':fontcolor=c1ff30:fontsize=42:x=410:y=76,split=3[v1][v2][v3]; \
[v1]scale=640x360:force_original_aspect_ratio=decrease:sws_dither=ed:flags=lanczos[v1]; \
[v2]scale=1024x576:force_original_aspect_ratio=decrease:sws_dither=ed:flags=lanczos[v2]; \
[v3]scale=1600x900:force_original_aspect_ratio=decrease:sws_dither=ed:flags=lanczos[v3]" \
-map "[v1]" -an -c:v libx264 -crf 23 -r 25 -muxpreload 0 -async 0 -maxrate 550k -bufsize 1100k -bsf:v h264_mp4toannexb -forced-idr 1 -sc_threshold 0 -shortest -g 75 -profile:v main -level 3.1 -preset medium -tune film -coder 0 -pix_fmt yuv420p -individual_header_trailer 0 -flags +loop+mv4+cgop -flags2 +local_header -movflags faststart -cmp chroma -keyint_min 75 -threads 0 -y -f segment -segment_format mpegts -segment_time path/to/playlist1.m3u8 path/to/segments1.ts \
-map "[v2]" -an -c:v libx264 -crf 23 -r 25 -muxpreload 0 -async 0 -maxrate 1400k -bufsize 2800k -bsf:v h264_mp4toannexb -forced-idr 1 -sc_threshold 0 -shortest -g 75 -profile:v main -level 4 -preset medium -tune film -coder 1 -pix_fmt yuv420p -individual_header_trailer 0 -flags +loop+mv4+cgop -flags2 +local_header -movflags faststart -cmp chroma -keyint_min 75 -threads 0 -y -f segment -segment_format mpegts -segment_time path/to/playlist2.m3u8 path/to/segments2.ts \
-map "[v3]" -an -c:v libx264 -crf 23 -r 25 -muxpreload 0 -async 0 -maxrate 3100k -bufsize 6200k -bsf:v h264_mp4toannexb -forced-idr 1 -sc_threshold 0 -shortest -g 75 -profile:v high -level 4 -preset medium -tune film -coder 1 -pix_fmt yuv420p -individual_header_trailer 0 -flags +loop+mv4+cgop -flags2 +local_header -movflags faststart -cmp chroma -keyint_min 75 -threads 0 -y -f segment -segment_format mpegts -segment_time path/to/playlist3.m3u8 path/to/segments3.ts

The ‘child’ command generates 2 segments which I stitch in the ‘main’ playlist, using #EXT-X-DISCONTINUITY where needed.

The generated playlists look like this:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-DISCONTINUITY-SEQUENCE:0
#EXT-X-ALLOW-CACHE:NO
#EXT-X-TARGETDURATION:7
#EXTINF:6.080000,
Master_3100_0000.ts
#EXTINF:6.000000,
Master_3100_0001.ts
#EXTINF:6.000000,
Master_3100_0002.ts
#EXT-X-DISCONTINUITY
#EXTINF:6.000000,
Master_3100_0003.ts
#EXTINF:6.000000,
Master_3100_0004.ts
#EXT-X-DISCONTINUITY
#EXTINF:6.000000,
Master_3100_0005.ts
#EXTINF:6.000000,
Master_3100_0006.ts
#EXT-X-ENDLIST

I also write the master.m3u8 file like this:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS

#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="a1",LANGUAGE="en",NAME="a1",DEFAULT=YES,AUTOSELECT=YES,URI="96/Master_96.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=550000,NAME="Main",CODECS="avc1.4d001f,mp4a.40.2",RESOLUTION=0,AUDIO="a1"
550/Master_550.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1400000,NAME="Main",CODECS="avc1.4d0028,mp4a.40.2",RESOLUTION=1024x576,AUDIO="a1"
1400/Master_1400.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3100000,NAME="Main",CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1600x900,AUDIO="a1"
3100/Master_3100.m3u8

In practice, the HLS package plays back fine in hls.js. In Safari/iOS the video starts fine but gets stuck at the first stitched segment (debugger doesn’t show anything).

I believe the problem is that the timestamps on the ‘main’ videos are different from the ‘child’ segments. When I add -copyts to the ‘child’s input parameters, the command just starts encoding from the beginning of the input file, ignoring the -ss, -to switches.
Also, in the pantos HLS RFC it says that the DISCONTINUITY tag is supposed to help with timestamp discontinuity. It looks like it doesn't.

So my questions are:
How can I copy an input video’s timestamps, while using -ss (before the input)?
Why doesn't iOS work the DISCONTINUITY tag as expected (as written in the RFC) and doesn't recover from timestamp discontinuity?
Any other suggestions of making those commands work in iOS?
Any other creative method to achieve my goal?

Thanks!

  • Re-encode the selected main segments with copyts. Use a slightly lower CRF if needed. – Gyan Mar 4 at 9:14
  • Thanks @Gyan for a valid workaround. Though I'm looking for a real, stable solution to my problem, if one exists. – o_ren Mar 6 at 9:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.