Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
892 lines (763 sloc) 27.8 KB
/* -*- Mode: c; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
NOTE: the first line of this file sets up source code indentation rules
for Emacs; it is also a hint to anyone modifying this file.
*/
/* $Id: segmenter.c 17 2011-11-08 15:14:56Z espendiller@gmail.com $
* $HeadURL
*
* Copyright (c) 2009 Chase Douglas
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <assert.h>
#include <float.h>
#include <math.h>
#include "libavformat/avformat.h"
static AVStream *add_output_stream(AVFormatContext *output_format_context, AVStream *input_stream) {
AVCodecContext *input_codec_context;
AVCodecContext *output_codec_context;
AVStream *output_stream;
output_stream = av_new_stream(output_format_context, 0);
if (!output_stream) {
fprintf(stderr, "Could not allocate stream\n");
exit(1);
}
input_codec_context = input_stream->codec;
output_codec_context = output_stream->codec;
output_codec_context->codec_id = input_codec_context->codec_id;
output_codec_context->codec_type = input_codec_context->codec_type;
output_codec_context->codec_tag = input_codec_context->codec_tag;
output_codec_context->bit_rate = input_codec_context->bit_rate;
output_codec_context->extradata = input_codec_context->extradata;
output_codec_context->extradata_size = input_codec_context->extradata_size;
if(av_q2d(input_codec_context->time_base) * input_codec_context->ticks_per_frame > av_q2d(input_stream->time_base) && av_q2d(input_stream->time_base) < 1.0/1000) {
output_codec_context->time_base = input_codec_context->time_base;
output_codec_context->time_base.num *= input_codec_context->ticks_per_frame;
}
else {
output_codec_context->time_base = input_stream->time_base;
}
switch (input_codec_context->codec_type) {
case AVMEDIA_TYPE_AUDIO:
output_codec_context->channel_layout = input_codec_context->channel_layout;
output_codec_context->sample_rate = input_codec_context->sample_rate;
output_codec_context->channels = input_codec_context->channels;
output_codec_context->frame_size = input_codec_context->frame_size;
if ((input_codec_context->block_align == 1 && input_codec_context->codec_id == CODEC_ID_MP3) || input_codec_context->codec_id == CODEC_ID_AC3) {
output_codec_context->block_align = 0;
}
else {
output_codec_context->block_align = input_codec_context->block_align;
}
break;
case AVMEDIA_TYPE_VIDEO:
output_codec_context->pix_fmt = input_codec_context->pix_fmt;
output_codec_context->width = input_codec_context->width;
output_codec_context->height = input_codec_context->height;
output_codec_context->has_b_frames = input_codec_context->has_b_frames;
if (output_format_context->oformat->flags & AVFMT_GLOBALHEADER) {
output_codec_context->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
break;
default:
break;
}
return output_stream;
}
typedef struct SMSegmentInfo
{
unsigned int index;
double duration;
char * filename;
} TSMSegmentInfo;
typedef struct SMPlaylist
{
/* a ring buffer of segments */
TSMSegmentInfo * buffer;
/* maximum number of segments that can be stored in the ring buffer */
unsigned int bufferCapacity;
/* index of the first segment on the ring buffer */
unsigned int first;
/* how many segments are currently in the ring buffer */
unsigned int count;
/* shortcuts */
unsigned int targetDuration;
char * httpPrefix;
/* playlist file used for non-live streaming */
FILE * file;
} TSMPlaylist;
static char *
duplicateString(const char * str)
{
/* unfortunately strdup isn't always available */
size_t strSize = strlen(str) + 1;
char * copy = (char *) malloc(strSize);
memcpy(copy, str, strSize);
return copy;
}
static TSMPlaylist *
createPlaylist(const unsigned int max_segments,
const unsigned int target_segment_duration,
const char * http_prefix)
{
TSMPlaylist * playlist = (TSMPlaylist *) malloc(sizeof(TSMPlaylist));
memset(playlist, 0, sizeof(TSMPlaylist));
if (max_segments)
{
playlist->buffer = (TSMSegmentInfo *) malloc(sizeof(TSMSegmentInfo) *
max_segments);
}
playlist->bufferCapacity = max_segments;
playlist->targetDuration = target_segment_duration;
playlist->httpPrefix = duplicateString(http_prefix);
return playlist;
}
static void
updateLivePlaylist(TSMPlaylist * playlist,
const char * playlistFileName,
const char * outputFileName,
const unsigned int segmentIndex,
const double segmentDuration)
{
unsigned int bufferIndex = 0;
TSMSegmentInfo * nextSegment = NULL;
TSMSegmentInfo removeMe;
memset(&removeMe, 0, sizeof(removeMe));
assert(!playlist->file);
if (playlist->count == playlist->bufferCapacity)
{
/* keep track of the segment that should be removed */
removeMe = playlist->buffer[playlist->first];
/* make room for the new segment */
playlist->first++;
playlist->first %= playlist->bufferCapacity;
}
else
{
playlist->count++;
}
/* store the new segment info */
bufferIndex = ((playlist->first + playlist->count - 1) %
playlist->bufferCapacity);
nextSegment = &playlist->buffer[bufferIndex];
nextSegment->filename = duplicateString(outputFileName);
nextSegment->duration = segmentDuration;
nextSegment->index = segmentIndex;
/* live streaming -- write full playlist from scratch */
playlist->file = fopen(playlistFileName, "w+b");
if (playlist->file)
{
const TSMSegmentInfo * first = &playlist->buffer[playlist->first];
char tmp[1024] = { 0 };
snprintf(tmp,
sizeof(tmp),
"#EXTM3U\n"
"#EXT-X-TARGETDURATION:%u\n"
"#EXT-X-MEDIA-SEQUENCE:%u\n",
playlist->targetDuration,
first->index);
fwrite(tmp, strlen(tmp), 1, playlist->file);
for (unsigned int i = 0; i < playlist->count; i++)
{
unsigned int j = ((playlist->first + i) %
playlist->bufferCapacity);
const TSMSegmentInfo * segment = &playlist->buffer[j];
snprintf(tmp,
sizeof(tmp),
"#EXTINF:%u,\n%s%s\n",
(int)(segment->duration + 0.5),
playlist->httpPrefix,
segment->filename);
fwrite(tmp, strlen(tmp), 1, playlist->file);
}
//snprintf(tmp, sizeof(tmp), "#EXT-X-ENDLIST\n");
//fwrite(tmp, strlen(tmp), 1, playlist->file);
fclose(playlist->file);
playlist->file = NULL;
}
else
{
fprintf(stderr,
"Could not open m3u8 index file (%s), "
"no index file will be created\n",
playlistFileName);
}
if (removeMe.filename)
{
/* remove the oldest segment file */
remove(removeMe.filename);
free(removeMe.filename);
}
}
static void
updatePlaylist(TSMPlaylist * playlist,
const char * playlistFileName,
const char * segmentFileName,
const unsigned int segmentIndex,
const int segmentDuration)
{
if (playlist->bufferCapacity > 0)
{
/* create a live streaming playlist */
updateLivePlaylist(playlist,
playlistFileName,
segmentFileName,
segmentIndex,
segmentDuration);
}
else
{
/* append to the existing playlist */
char tmp[1024] = { 0 };
if (!playlist->file)
{
playlist->file = fopen(playlistFileName, "w+b");
snprintf(tmp,
sizeof(tmp),
"#EXTM3U\n"
"#EXT-X-TARGETDURATION:%u\n",
playlist->targetDuration);
fwrite(tmp, strlen(tmp), 1, playlist->file);
}
if (!playlist->file)
{
fprintf(stderr,
"Could not open m3u8 index file (%s), "
"no index file will be created\n",
playlistFileName);
}
snprintf(tmp,
sizeof(tmp),
"#EXTINF:%u,\n%s%s\n",
segmentDuration,
playlist->httpPrefix,
segmentFileName);
fwrite(tmp, strlen(tmp), 1, playlist->file);
fflush(playlist->file);
}
}
static void
closePlaylist(TSMPlaylist * playlist)
{
if (playlist->file)
{
/* append to the existing playlist */
char tmp[1024] = { 0 };
snprintf(tmp, sizeof(tmp), "#EXT-X-ENDLIST\n");
fwrite(tmp, strlen(tmp), 1, playlist->file);
fclose(playlist->file);
playlist->file = NULL;
}
}
static void
releasePlaylist(TSMPlaylist ** playlistRef)
{
TSMPlaylist * playlist = *playlistRef;
closePlaylist(playlist);
for (unsigned int i = 0; i < playlist->bufferCapacity; i++)
{
TSMSegmentInfo * segmentInfo = &playlist->buffer[i];
if (segmentInfo->filename)
{
free(segmentInfo->filename);
}
}
free(playlist->buffer);
free(playlist->httpPrefix);
free(playlist);
*playlistRef = NULL;
}
typedef struct SMPacketLink
{
/* packet start time in seconds */
double timeStamp;
/* the packet */
AVPacket packet;
/* a link to the next packet */
struct SMPacketLink * next;
} TSMPacketLink;
typedef struct SMPacketList
{
TSMPacketLink * head;
TSMPacketLink * tail;
unsigned int size;
} TSMPacketList;
typedef struct SMStreamLace
{
TSMPacketList ** streams;
unsigned int numStreams;
} TSMStreamLace;
static TSMPacketLink *
createLink(const AVPacket * packet, double timeStamp)
{
TSMPacketLink * link = (TSMPacketLink *) malloc(sizeof(TSMPacketLink));
link->timeStamp = timeStamp;
link->next = NULL;
memcpy(&link->packet, packet, sizeof(AVPacket));
return link;
}
static void
fifoPush(TSMPacketList * packets, const AVPacket * packet, double timeStamp)
{
TSMPacketLink * link = createLink(packet, timeStamp);
if (!packets->head)
{
assert(!packets->tail);
assert(!packets->size);
packets->head = link;
packets->tail = link;
packets->size = 1;
}
else
{
/* attach at the tail */
assert(packets->size > 0);
packets->tail->next = link;
packets->tail = link;
packets->size++;
}
}
static int
fifoPop(TSMPacketList * packets, AVPacket * packet)
{
TSMPacketLink * link = packets->head;
if (!link)
{
return 0;
}
memcpy(packet, &link->packet, sizeof(AVPacket));
packets->head = link->next;
packets->size--;
if (!packets->head)
{
packets->tail = NULL;
}
free(link);
return 1;
}
static TSMPacketList *
createPacketList()
{
TSMPacketList * packets = (TSMPacketList *)malloc(sizeof(TSMPacketList));
memset(packets, 0, sizeof(TSMPacketList));
return packets;
}
static TSMStreamLace *
createStreamLace(unsigned int numStreams)
{
TSMStreamLace * lace = (TSMStreamLace *)malloc(sizeof(TSMStreamLace));
lace->streams = (TSMPacketList **)malloc(sizeof(TSMPacketList *) * numStreams);
for (unsigned int i = 0; i < numStreams; i++)
{
lace->streams[i] = createPacketList();
}
lace->numStreams = numStreams;
return lace;
}
static void
insertPacket(TSMStreamLace * lace, const AVPacket * packet, double timeStamp)
{
fifoPush(lace->streams[packet->stream_index], packet, timeStamp);
}
static TSMPacketList *
chooseNextStream(TSMStreamLace * lace)
{
/* improve lacing so that that audio/video packets that should be
together do not get stuck into separate segments. */
TSMPacketList * nextStream = NULL;
double earliestTimeStamp = DBL_MAX;
for (unsigned int i = 0; i < lace->numStreams; i++)
{
TSMPacketList * stream = lace->streams[i];
if (stream->size && stream->head->timeStamp < earliestTimeStamp)
{
nextStream = stream;
earliestTimeStamp = stream->head->timeStamp;
}
}
return nextStream;
}
static int
removePacket(TSMStreamLace * lace, AVPacket * packet)
{
TSMPacketList * stream = chooseNextStream(lace);
if (!stream)
{
return 0;
}
return fifoPop(stream, packet);
}
static unsigned int
countPackets(const TSMStreamLace * lace)
{
unsigned int numPackets = 0;
for (unsigned int i = 0; i < lace->numStreams; i++)
{
const TSMPacketList * stream = lace->streams[i];
numPackets += stream->size;
}
return numPackets;
}
static void
removeAllPackets(TSMStreamLace * lace)
{
AVPacket packet;
for (unsigned int i = 0; i < lace->numStreams; i++)
{
TSMPacketList * stream = lace->streams[i];
while (stream->size)
{
fifoPop(stream, &packet);
av_free_packet(&packet);
}
}
}
int main(int argc, char **argv)
{
const char *input = NULL;
const char *output_prefix = "";
double target_segment_duration = 0.0;
char *segment_duration_check = NULL;
const char *playlist_filename = NULL;
const char *http_prefix = "";
long max_tsfiles = 0;
char *max_tsfiles_check = NULL;
double prev_segment_time = 0.0;
double segment_duration = 0.0;
unsigned int output_index = 0;
AVOutputFormat *ofmt = NULL;
AVFormatContext *ic = NULL;
AVFormatContext *oc = NULL;
AVStream *video_st = NULL;
AVStream *audio_st = NULL;
AVCodec *codec = NULL;
char *output_filename = NULL;
int video_index = -1;
int audio_index = -1;
int kill_file = 0;
int decode_done = 0;
int ret = 0;
int i = 0;
FILE * pid_file = NULL;
TSMStreamLace * streamLace = NULL;
AVBitStreamFilterContext * vbsf_h264_mp4toannexb = NULL;
TSMPlaylist * playlist = NULL;
const double segment_duration_error_tolerance = 0.05;
double extra_duration_needed = 0;
if (argc < 6 || argc > 8) {
fprintf(stderr,
"Usage: %s <input MPEG-TS file> "
"<segment duration in seconds> "
"<output MPEG-TS file prefix> "
"<output m3u8 index file> "
"<http prefix> "
"[<segment window size>] "
"[<search kill file>]\n\n"
"Compiled by Daniel Espendiller - www.espend.de\n"
"build on %s %s with %s\n\n"
"Took some code from:\n"
" - source:http://svn.assembla.com/svn/legend/segmenter/\n"
" - iStreamdev:http://projects.vdr-developer.org/git/?p=istreamdev.git;a=tree;f=segmenter;hb=HEAD\n"
" - live_segmenter:http://github.com/carsonmcdonald/HTTP-Live-Video-Stream-Segmenter-and-Distributor\n",
argv[0],
__DATE__,
__TIME__,
__VERSION__);
exit(1);
}
// Create PID file
pid_file=fopen("./segmenter.pid", "wb");
if (pid_file)
{
fprintf(pid_file, "%d", getpid());
fclose(pid_file);
}
av_register_all();
input = argv[1];
if (!strcmp(input, "-")) {
input = "pipe:";
}
target_segment_duration = strtod(argv[2], &segment_duration_check);
if (segment_duration_check == argv[2] || target_segment_duration == HUGE_VAL || target_segment_duration == -HUGE_VAL) {
fprintf(stderr, "Segment duration time (%s) invalid\n", argv[2]);
goto error;
}
output_prefix = argv[3];
playlist_filename = argv[4];
http_prefix=argv[5];
if (argc >= 7) {
max_tsfiles = strtol(argv[6], &max_tsfiles_check, 10);
if (max_tsfiles_check == argv[6] || max_tsfiles < 0 || max_tsfiles >= INT_MAX) {
fprintf(stderr, "Maximum number of ts files (%s) invalid\n", argv[6]);
goto error;
}
}
// end programm when it found a file with name 'kill'
if (argc >= 8) kill_file = atoi(argv[7]);
output_filename = malloc(sizeof(char) * (strlen(output_prefix) + 15));
if (!output_filename) {
fprintf(stderr, "Could not allocate space for output filenames\n");
goto error;
}
playlist = createPlaylist(max_tsfiles,
target_segment_duration,
http_prefix);
if (!playlist)
{
fprintf(stderr, "Could not allocate space for m3u8 playlist structure\n");
goto error;
}
ret = avformat_open_input(&ic, input, NULL, NULL);
if (ret != 0) {
fprintf(stderr, "Could not open input file, make sure it is an mpegts or mp4 file: %d\n", ret);
goto error;
}
if (av_find_stream_info(ic) < 0) {
fprintf(stderr, "Could not read stream information\n");
goto error;
}
// NOTE(patrick@ooyala.com): Does not work. Pre-mux with ffmpeg instead.
if (strstr(ic->iformat->name, "mp4") != NULL)
{
// need to filter the bitstream when re-formatting mp4 to mpeg-ts:
vbsf_h264_mp4toannexb = av_bitstream_filter_init("h264_mp4toannexb");
}
#if LIBAVFORMAT_VERSION_MAJOR > 52 || (LIBAVFORMAT_VERSION_MAJOR == 52 && \
LIBAVFORMAT_VERSION_MINOR >= 45)
ofmt = av_guess_format("mpegts", NULL, NULL);
#else
ofmt = guess_format("mpegts", NULL, NULL);
#endif
if (!ofmt) {
fprintf(stderr, "Could not find MPEG-TS muxer\n");
goto error;
}
oc = avformat_alloc_context();
if (!oc) {
fprintf(stderr, "Could not allocated output context\n");
goto error;
}
oc->oformat = ofmt;
video_index = -1;
audio_index = -1;
for (i = 0; i < ic->nb_streams && (video_index < 0 || audio_index < 0); i++) {
switch (ic->streams[i]->codec->codec_type) {
case AVMEDIA_TYPE_VIDEO:
video_index = i;
ic->streams[i]->discard = AVDISCARD_NONE;
video_st = add_output_stream(oc, ic->streams[i]);
break;
case AVMEDIA_TYPE_AUDIO:
audio_index = i;
ic->streams[i]->discard = AVDISCARD_NONE;
audio_st = add_output_stream(oc, ic->streams[i]);
break;
default:
ic->streams[i]->discard = AVDISCARD_ALL;
break;
}
}
av_dump_format(oc, 0, output_prefix, 1);
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_open2(video_st->codec, codec, NULL) < 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);
if (avio_open(&oc->pb, output_filename, URL_WRONLY) < 0) {
fprintf(stderr, "Could not open '%s'\n", output_filename);
goto error;
}
if (avformat_write_header(oc, NULL)) {
fprintf(stderr, "Could not write mpegts header to first output file\n");
goto error;
}
prev_segment_time = (double)(ic->start_time) / (double)(AV_TIME_BASE);
streamLace = createStreamLace(ic->nb_streams);
do {
double segment_time = 0.0;
AVPacket packet;
double packetStartTime = 0.0;
double packetDuration = 0.0;
if (!decode_done)
{
decode_done = av_read_frame(ic, &packet);
if (!decode_done)
{
if (packet.stream_index != video_index &&
packet.stream_index != audio_index)
{
av_free_packet(&packet);
continue;
}
double timeStamp =
(double)(packet.pts) *
(double)(ic->streams[packet.stream_index]->time_base.num) /
(double)(ic->streams[packet.stream_index]->time_base.den);
if (av_dup_packet(&packet) < 0)
{
fprintf(stderr, "Could not duplicate packet\n");
av_free_packet(&packet);
break;
}
insertPacket(streamLace, &packet, timeStamp);
}
}
if (countPackets(streamLace) < 50 && !decode_done)
{
/* allow the queue to fill up so that the packets can be sorted properly */
continue;
}
if (!removePacket(streamLace, &packet))
{
if (decode_done)
{
/* the queue is empty, we are done */
break;
}
assert(decode_done);
continue;
}
packetStartTime =
(double)(packet.pts) *
(double)(ic->streams[packet.stream_index]->time_base.num) /
(double)(ic->streams[packet.stream_index]->time_base.den);
packetDuration =
(double)(packet.duration) *
(double)(ic->streams[packet.stream_index]->time_base.num) /
(double)(ic->streams[packet.stream_index]->time_base.den);
#if !defined(NDEBUG) && (defined(DEBUG) || defined(_DEBUG))
fprintf(stderr,
"stream %i, packet [%f, %f)\n",
packet.stream_index,
packetStartTime,
packetStartTime + packetDuration);
#endif
segment_duration = packetStartTime + packetDuration - prev_segment_time;
// Allow segmenting between keyframes to produce more accurate chunk lengths, which provides
// better seeking behavior.
if (packet.stream_index == video_index /*&& (packet.flags & AV_PKT_FLAG_KEY)*/) {
segment_time = packetStartTime;
}
else if (video_index < 0) {
segment_time = packetStartTime;
}
else {
segment_time = prev_segment_time;
}
if (segment_time - prev_segment_time + segment_duration_error_tolerance > target_segment_duration + extra_duration_needed)
{
avio_flush(oc->pb);
avio_close(oc->pb);
// Keep track of accumulated rounding error to account for it in later chunks.
double segment_duration = segment_time - prev_segment_time;
int rounded_segment_duration = (int)(segment_duration + 0.5);
extra_duration_needed += (double)rounded_segment_duration - segment_duration;
updatePlaylist(playlist,
playlist_filename,
output_filename,
output_index,
rounded_segment_duration);
snprintf(output_filename, strlen(output_prefix) + 15, "%s-%u.ts", output_prefix, ++output_index);
if (avio_open(&oc->pb, output_filename, URL_WRONLY) < 0) {
fprintf(stderr, "Could not open '%s'\n", output_filename);
break;
}
// close when we find the 'kill' file
if (kill_file) {
FILE* fp = fopen("kill", "rb");
if (fp) {
fprintf(stderr, "user abort: found kill file\n");
fclose(fp);
remove("kill");
decode_done = 1;
removeAllPackets(streamLace);
}
}
prev_segment_time = segment_time;
}
if (vbsf_h264_mp4toannexb != NULL &&
packet.stream_index == video_index)
{
AVPacket filteredPacket = packet;
int a = av_bitstream_filter_filter(vbsf_h264_mp4toannexb,
video_st->codec,
NULL,
&filteredPacket.data,
&filteredPacket.size,
packet.data,
packet.size,
packet.flags & AV_PKT_FLAG_KEY);
if (a > 0)
{
av_free_packet(&packet);
filteredPacket.destruct = av_destruct_packet;
packet = filteredPacket;
}
else if (a < 0)
{
fprintf(stderr,
"%s failed for stream %d, codec %s",
vbsf_h264_mp4toannexb->filter->name,
packet.stream_index,
video_st->codec->codec ?
video_st->codec->codec->name : "copy");
av_free_packet(&packet);
continue;
}
}
ret = av_interleaved_write_frame(oc, &packet);
if (ret < 0) {
fprintf(stderr, "Warning: Could not write frame of stream\n");
}
else if (ret > 0) {
fprintf(stderr, "End of stream requested\n");
av_free_packet(&packet);
break;
}
av_free_packet(&packet);
} while (!decode_done || countPackets(streamLace) > 0);
av_write_trailer(oc);
if (video_index >= 0) {
avcodec_close(video_st->codec);
}
for(i = 0; i < oc->nb_streams; i++) {
av_freep(&oc->streams[i]->codec);
av_freep(&oc->streams[i]);
}
avio_close(oc->pb);
av_free(oc);
updatePlaylist(playlist,
playlist_filename,
output_filename,
output_index,
segment_duration);
closePlaylist(playlist);
releasePlaylist(&playlist);
remove("./segmenter.pid");
return 0;
error:
remove("./segmenter.pid");
return 1;
}
// vim:sw=4:tw=4:ts=4:ai:expandtab