I've been working a lot with Apple's HTTP Live Streaming specification that defines how audio & video are streamed to Apple's mobile devices (e.g. iPhone, iPod Touch, iPad). There's an open-source segmenter circulating on the Internet. The segmenter uses FFMpeg to process an MPEG transport-stream into segments of a fixed duration. The most widely duration is 10 seconds. The transport-stream segments are pulled together through an M3U8 playlist that is passed to the QuickTime player on the device. QuickTime will retrieve and play the segments in the appropriate order. I've provided support for the segmenter in my MythPodcaster project that transcodes MythTV recordings.
There are some quirks to the open-source segmenter. First, it doesn't support the segmenting of audio-only input files (i.e. an AAC or MP3 input file). Second, the durations of the transport-stream segments that are reported in the M3U8 playlist are often incorrect. The small duration deviations accumulate when applied to a clip that is 30 minutes or more in total duration.
I've made some improvements to the open-source segmenter to address the issues stated earlier. The complete patched source file can be accessed here. Many thanks go to Chase Douglas (original author of the segmenter) and Carson McDonald (made the segmenter popular).
The unified diff of the segmenter source file is shown below:
--- segmenter.c 2010-05-18 09:51:57.932132172 -0700
+++ segmenter-accurate_durations.c 2010-05-18 09:50:19.765611032 -0700
@@ -20,6 +20,8 @@
#include "libavformat/avformat.h"
+#define MAX_TS_FILES_DEFAULT 1000
+
static AVStream *add_output_stream(AVFormatContext *output_format_context, AVStream *input_stream) {
AVCodecContext *input_codec_context;
AVCodecContext *output_codec_context;
@@ -79,7 +81,7 @@
return output_stream;
}
-int write_index_file(const char index[], const char tmp_index[], const unsigned int segment_duration, const char output_prefix[], const char http_prefix[], const unsigned int first_segment, const unsigned int last_segment, const int end, const int window) {
+int write_index_file(const char index[], const char tmp_index[], const unsigned int segment_duration, const char output_prefix[], const char http_prefix[], const unsigned int first_segment, const unsigned int last_segment, const int end, const int window, const unsigned int actual_segment_durations[]) {
FILE *index_fp;
char *write_buf;
unsigned int i;
@@ -111,7 +113,7 @@
}
for (i = first_segment; i <= last_segment; i++) {
- snprintf(write_buf, 1024, "#EXTINF:%u,\n%s%s-%u.ts\n", segment_duration, http_prefix, output_prefix, i);
+ snprintf(write_buf, 1024, "#EXTINF:%u,\n%s%s-%u.ts\n", actual_segment_durations[i-1], http_prefix, output_prefix, i);
if (fwrite(write_buf, strlen(write_buf), 1, index_fp) != 1) {
fprintf(stderr, "Could not write to m3u8 index file, will not continue writing to index file\n");
free(write_buf);
@@ -194,6 +196,8 @@
fprintf(stderr, "Maximum number of ts files (%s) invalid\n", argv[6]);
exit(1);
}
+ } else {
+ max_tsfiles = MAX_TS_FILES_DEFAULT;
}
remove_filename = malloc(sizeof(char) * (strlen(output_prefix) + 15));
@@ -280,13 +284,15 @@
dump_format(oc, 0, output_prefix, 1);
- codec = avcodec_find_decoder(video_st->codec->codec_id);
- if (!codec) {
+ if (video_index >=0) {
+ codec = avcodec_find_decoder(video_st->codec->codec_id);
+ if (!codec) {
fprintf(stderr, "Could not find video decoder, key frames will not be honored\n");
- }
+ }
- if (avcodec_open(video_st->codec, codec) < 0) {
+ if (avcodec_open(video_st->codec, codec) < 0) {
fprintf(stderr, "Could not open video decoder, key frames will not be honored\n");
+ }
}
snprintf(output_filename, strlen(output_prefix) + 15, "%s-%u.ts", output_prefix, output_index++);
@@ -300,9 +306,11 @@
exit(1);
}
- write_index = !write_index_file(index, tmp_index, segment_duration, output_prefix, http_prefix, first_segment, last_segment, 0, max_tsfiles);
+ unsigned int *actual_segment_durations = malloc(sizeof(unsigned int) * MAX_TS_FILES_DEFAULT);
+ write_index = !write_index_file(index, tmp_index, segment_duration, output_prefix, http_prefix, first_segment, last_segment, 0, max_tsfiles, actual_segment_durations);
do {
+ unsigned int current_segment_duration;
double segment_time;
AVPacket packet;
@@ -327,6 +335,8 @@
segment_time = prev_segment_time;
}
+ current_segment_duration = (int) round(segment_time - prev_segment_time);
+ actual_segment_durations[last_segment] = (current_segment_duration > 0 ? current_segment_duration: 1);
if (segment_time - prev_segment_time >= segment_duration) {
put_flush_packet(oc->pb);
url_fclose(oc->pb);
@@ -340,7 +350,7 @@
}
if (write_index) {
- write_index = !write_index_file(index, tmp_index, segment_duration, output_prefix, http_prefix, first_segment, ++last_segment, 0, max_tsfiles);
+ write_index = !write_index_file(index, tmp_index, segment_duration, output_prefix, http_prefix, first_segment, ++last_segment, 0, max_tsfiles, actual_segment_durations);
}
if (remove_file) {
@@ -372,7 +382,9 @@
av_write_trailer(oc);
- avcodec_close(video_st->codec);
+ if (video_index >= 0) {
+ avcodec_close(video_st->codec);
+ }
for(i = 0; i < oc->nb_streams; i++) {
av_freep(&oc->streams[i]->codec);
@@ -391,7 +403,7 @@
}
if (write_index) {
- write_index_file(index, tmp_index, segment_duration, output_prefix, http_prefix, first_segment, ++last_segment, 1, max_tsfiles);
+ write_index_file(index, tmp_index, segment_duration, output_prefix, http_prefix, first_segment, ++last_segment, 1, max_tsfiles, actual_segment_durations);
}
if (remove_file) {
@@ -399,6 +411,8 @@
remove(remove_filename);
}
+ free(actual_segment_durations);
+
return 0;
}