[FFmpeg-devel] Add segment_copyts to generic file segmenter?

Steffen Deusch steffen at deusch.me
Fri Feb 26 23:21:56 CET 2016


Hi all,
this is my first message ever to the ffmpeg mailing list, so please forgive me any noob
mistakes.

I’m currently working on a little project which uses ffmpeg to live transcode video files for 
streaming to a web browser. 
Basically it’s like Plex just at a very early stage. For streaming to the client I’m using HLS.
The first problem I had was letting the client know the final video length because while ffmpeg 
runs the client only showed "live“ as current time (which is absolutely normal, how should
the client know the duration?). 
Plex just computes the total number of segments using the original file’s length and gene-
rates a "fake“ m3u8 playlist which already contains the urls to the media file.

e.g. when using ffmpeg with -f hls you’d get the following playlist:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:17
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:16.240000,
out00000.ts
#EXTINF:9.960000,
out00001.ts
#EXTINF:9.960000,
out00002.ts
#EXTINF:13.200000,
out00003.ts
#EXTINF:9.920000,
out00004.ts
#EXTINF:4.400000,
out00005.ts
...
#EXTINF:9.960000,
out00138.ts
#EXT-X-ENDLIST

Plex looks at the video duration (in this example 1391.232000 seconds -> 138 segments
with 10 seconds and 1 segment with 1 sec when using 10 sec segment time) and 
generates a playlist like this:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10, nodesc
out00000.ts
#EXTINF:10, nodesc
out00001.ts
#EXTINF:10, nodesc
out00002.ts
#EXTINF:10, nodesc
out00003.ts
...
#EXTINF:10, nodesc
out00138.ts
#EXTINF:1, nodesc
out00139.ts
#EXT-X-ENDLIST

The client shows the correct video duration, but you can see a little issue:
the calculated amount of segments is larger than files exist.
In this case Plex just sends out a blank segment, which strangely works perfect.
The browser now shows the correct video duration and even seek-
ing works, because if the client requests a segments that doesn’t exist yet the server will 
just restart ffmpeg with an additional seek (ss) and segment_start_number parameter.
Plex uses the following Parameters to achieve this:
/path/to/plextranscoder -noaccurate_seek -i /path/to/file.mkv -map 0:0
-metadata:s:0 language=eng -codec:0 copy -bsf:0 h264_mp4toannexb,h264_plex
-map 0:1 -metadata:s:1 language=ger -codec:1 aac -strict:1 experimental
-cutoff:1 15000 -channel_layout:1 stereo -b:1 256k -segment_format mpegts
-f segment -flags -global_header -segment_time 10 -segment_start_number 0
-segment_copyts 1 -segment_time_delta 0.0625 -max_delay 5000000
-avoid_negative_ts disabled -map_metadata -1 media-%05d.ts -start_at_zero
-copyts -vsync cfr -y -nostats

If the client requests segment 66 and it does not already exist, Plex will run:
/path/to/plextranscoder -ss 660 -noaccurate_seek -i /path/to/file.mkv 
-map 0:0 -metadata:s:0 language=eng -codec:0 copy -bsf:0 h264_mp4toannexb,
h264_plex -map 0:1 -metadata:s:1 language=ger -codec:1 aac -strict:1
experimental -cutoff:1 15000 -channel_layout:1 stereo -b:1 256k -segment_format
mpegts -f segment -flags -global_header -segment_time 10 -segment_start_number
66 -segment_copyts 1 -segment_time_delta 0.0625 -max_delay 5000000
-avoid_negative_ts disabled -map_metadata -1 media-%05d.ts -start_at_zero
-copyts -vsync cfr -y -nostats

I tried to achieve the same for my project with ffmpeg 3.0 on Ubuntu (without the h264_plex
bitstream filter, which obviously does not exist), but it complained that the parameter 
segment_copyts doesn’t exist:

Unrecognized option 'segment_copyts'.
Error splitting the argument list: Option not found

Without using this parameter, the transcoding process works fine and generates most of 
the time the correct calculated number of segments, but when trying to seek (ss and
segment_start_number) ffmpeg will suddenly start to generate more segments (e.g.
160+ instead of 138).
I looked at the source code of the Plex Transcoder
(http://files.plexapp.com/elan/ffmpeg/PlexNewTranscoder.tar.bz2) and found some 
differences in the libavformat/segment.c file. The changes to make the segment_copyts 
option work as expected (ffmpeg will correctly generate 138 segments even when seeking) 
are:
(This will also create temporary files when the segment is not finished, because the
server should not send the file out if it’s not yet finished. I don’t know if this is acutally
necessary...)

diff --git a/libavformat/segment.c b/libavformat/segment.c
index dd3b092..c560a87 100644
--- a/libavformat/segment.c
+++ b/libavformat/segment.c
@@ -123,6 +123,9 @@ typedef struct SegmentContext {
     SegmentListEntry cur_entry;
     SegmentListEntry *segment_list_entries;
     SegmentListEntry *segment_list_entries_end;
+
+    int segment_copyts;    ///< PLEX
+
 } SegmentContext;
 
 static void print_csv_escaped_str(AVIOContext *ctx, const char *str)
@@ -219,6 +222,12 @@ static int set_segment_filename(AVFormatContext *s)
              seg->entry_prefix ? seg->entry_prefix : "",
              av_basename(oc->filename));
 
+    // PLEX
+    // Write segment data to temp file, so we don't accidentally grab a partial segment.
+    if(!seg->list)
+      av_strlcatf(oc->filename, sizeof(oc->filename), ".tmp");
+    // PLEX
+
     return 0;
 }
 
@@ -392,6 +401,17 @@ static int segment_end(AVFormatContext *s, int write_trailer, int is_last)
 
 end:
     ff_format_io_close(oc, &oc->pb);
+    // PLEX
+
+    // Now rename the temporary file.
+    if (!seg->list) {
+        char* final_filename = av_strdup(oc->filename);
+        final_filename[strlen(final_filename)-4] = '\0';
+        rename(oc->filename, final_filename);
+        av_free(final_filename);
+    }
+
+    // PLEX
 
     return ret;
 }
@@ -614,6 +634,11 @@ static int seg_write_header(AVFormatContext *s)
         seg->individual_header_trailer = 0;
     }
 
+    //PLEX
+    if (seg->segment_copyts)
+        seg->segment_count = seg->segment_idx;
+    //PLEX
+
     if (!!seg->time_str + !!seg->times_str + !!seg->frames_str > 1) {
         av_log(s, AV_LOG_ERROR,
                "segment_time, segment_times, and segment_frames options "
@@ -947,6 +972,7 @@ static const AVOption options[] = {
     { "segment_list_entry_prefix", "set base url prefix for segments", OFFSET(entry_prefix), AV_OPT_TYPE_STRING,  {.str = NULL}, 0, 0, E },
     { "segment_start_number", "set the sequence number of the first segment", OFFSET(segment_idx), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, E },
     { "segment_wrap_number", "set the number of wrap before the first segment", OFFSET(segment_idx_wrap_nb), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, E },
+    { "segment_copyts",    "adjust timestamps for -copyts setting",      OFFSET(segment_copyts), AV_OPT_TYPE_INT,   {.i64 = 0}, 0, 1,       E }, //PLEX
     { "strftime",          "set filename expansion with strftime at segment creation", OFFSET(use_strftime), AV_OPT_TYPE_BOOL, {.i64 = 0 }, 0, 1, E },
     { "break_non_keyframes", "allow breaking segments on non-keyframes", OFFSET(break_non_keyframes), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, E },


Currently I’m using the following parameters to call ffmpeg:
command = [
                "ffmpeg",
                "-ss", str(self.__ss),
                "-noaccurate_seek",
                "-i", tfile,
                "-map", "0:0",
                "-metadata:s:0", "language="+self.getlang(0),
                "-codec:0", vidcodec,
                "-bsf:0", "h264_mp4toannexb",
                "-map", "0:"+("1" if self.__audio is None else str(self.__audio)),
                "-metadata:s:1", "language="+self.getlang(1 if self.__audio is None else int(self.__audio)),
                "-codec:1", audcodec,
                "-strict:1", "experimental",
                "-ac", "2",
                "-b:1", "256k",
                "-f", "hls",
                "-hls_time", "10",
                "-hls_list_size", "0",
                "-start_number", str(self.__segment_start_number),
                "-segment_time_delta", "0.0625",
                "-max_delay", "5000000",
                "-avoid_negative_ts", "disabled",
                "-map_metadata", "-1",
                "-hls_segment_filename", temppath+"/out%05d.ts",
                temppath+"/playlist.m3u8",
                "-start_at_zero",
                "-copyts",
                "-y",
                "-nostats“
]
This also generates the calculated number of segments, but the problem is that 
sometimes the client stops playing ~5 seconds before the video is actually finished 
which does not happen when using the standard segmenter with the segment_copyts
parameter.

I’m absolutely no expert of ffmpeg and actually don’t know what some of these parameters
really do (for example segment_time_delta or avoid_negative_ts), but maybe you could
help me getting this to work. Would it be possible to add the segment_copyts parameter
upstream?
Is there a better way to achieve what I want to do (HLS is needed)?

I’m surprised that this hacky way of generating a playlist with wrong time values actually
works.
If you are interested in the source code of my project, I think I’ll release it on GitHub soon
(will be open source).

Sorry, if I wasted your time with this very long email,
thanks for any help!
Steffen Deusch



More information about the ffmpeg-devel mailing list