blob: 2a0fa45aedbd13be4c3ef523cbbab3fcdbe1a81b [file] [log] [blame]
/* GStreamer
* Copyright (C) <2007> Julien Moutte <julien@moutte.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
/**
* SECTION:element-flvdemux
*
* flvdemux demuxes an FLV file into the different contained streams.
*
* <refsect2>
* <title>Example launch line</title>
* |[
* gst-launch-1.0 -v filesrc location=/path/to/flv ! flvdemux ! audioconvert ! autoaudiosink
* ]| This pipeline demuxes an FLV file and outputs the contained raw audio streams.
* </refsect2>
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstflvdemux.h"
#include "gstflvmux.h"
#include <string.h>
#include <stdio.h>
#include <gst/base/gstbytereader.h>
#include <gst/base/gstbytewriter.h>
#include <gst/pbutils/descriptions.h>
#include <gst/pbutils/pbutils.h>
#include <gst/audio/audio.h>
#include <gst/video/video.h>
#include <gst/tag/tag.h>
/* FIXME: don't rely on own GstIndex */
#include "gstindex.c"
#include "gstmemindex.c"
#define GST_ASSOCIATION_FLAG_NONE GST_INDEX_ASSOCIATION_FLAG_NONE
#define GST_ASSOCIATION_FLAG_KEY_UNIT GST_INDEX_ASSOCIATION_FLAG_KEY_UNIT
#define GST_ASSOCIATION_FLAG_DELTA_UNIT GST_INDEX_ASSOCIATION_FLAG_DELTA_UNIT
static GstStaticPadTemplate flv_sink_template = GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS ("video/x-flv")
);
static GstStaticPadTemplate audio_src_template =
GST_STATIC_PAD_TEMPLATE ("audio",
GST_PAD_SRC,
GST_PAD_SOMETIMES,
GST_STATIC_CAPS
("audio/x-adpcm, layout = (string) swf, channels = (int) { 1, 2 }, rate = (int) { 5512, 11025, 22050, 44100 }; "
"audio/mpeg, mpegversion = (int) 1, layer = (int) 3, channels = (int) { 1, 2 }, rate = (int) { 5512, 8000, 11025, 22050, 44100 }, parsed = (boolean) TRUE; "
"audio/mpeg, mpegversion = (int) 4, stream-format = (string) raw, framed = (boolean) TRUE; "
"audio/x-nellymoser, channels = (int) { 1, 2 }, rate = (int) { 5512, 8000, 11025, 16000, 22050, 44100 }; "
"audio/x-raw, format = (string) { U8, S16LE }, layout = (string) interleaved, channels = (int) { 1, 2 }, rate = (int) { 5512, 11025, 22050, 44100 }; "
"audio/x-alaw, channels = (int) { 1, 2 }, rate = (int) { 5512, 11025, 22050, 44100 }; "
"audio/x-mulaw, channels = (int) { 1, 2 }, rate = (int) { 5512, 11025, 22050, 44100 }; "
"audio/x-speex, channels = (int) 1, rate = (int) 16000;")
);
static GstStaticPadTemplate video_src_template =
GST_STATIC_PAD_TEMPLATE ("video",
GST_PAD_SRC,
GST_PAD_SOMETIMES,
GST_STATIC_CAPS ("video/x-flash-video, flvversion=(int) 1; "
"video/x-flash-screen; "
"video/x-vp6-flash; " "video/x-vp6-alpha; "
"video/x-h264, stream-format=avc;")
);
GST_DEBUG_CATEGORY_STATIC (flvdemux_debug);
#define GST_CAT_DEFAULT flvdemux_debug
#define gst_flv_demux_parent_class parent_class
G_DEFINE_TYPE (GstFlvDemux, gst_flv_demux, GST_TYPE_ELEMENT);
/* 9 bytes of header + 4 bytes of first previous tag size */
#define FLV_HEADER_SIZE 13
/* 1 byte of tag type + 3 bytes of tag data size */
#define FLV_TAG_TYPE_SIZE 4
/* two seconds - consider dts are resynced to another base if this different */
#define RESYNC_THRESHOLD 2000
/* how much stream time to wait for audio tags to appear after we have video, or vice versa */
#define NO_MORE_PADS_THRESHOLD (6 * GST_SECOND)
static gboolean flv_demux_handle_seek_push (GstFlvDemux * demux,
GstEvent * event);
static gboolean gst_flv_demux_handle_seek_pull (GstFlvDemux * demux,
GstEvent * event, gboolean seeking);
static gboolean gst_flv_demux_query (GstPad * pad, GstObject * parent,
GstQuery * query);
static gboolean gst_flv_demux_src_event (GstPad * pad, GstObject * parent,
GstEvent * event);
static GstIndex *gst_flv_demux_get_index (GstElement * element);
static void gst_flv_demux_push_tags (GstFlvDemux * demux);
static void
gst_flv_demux_parse_and_add_index_entry (GstFlvDemux * demux, GstClockTime ts,
guint64 pos, gboolean keyframe)
{
GstIndexAssociation associations[2];
GstIndex *index;
GstIndexEntry *entry;
GST_LOG_OBJECT (demux,
"adding key=%d association %" GST_TIME_FORMAT "-> %" G_GUINT64_FORMAT,
keyframe, GST_TIME_ARGS (ts), pos);
/* if upstream is not seekable there is no point in building an index */
if (!demux->upstream_seekable)
return;
index = gst_flv_demux_get_index (GST_ELEMENT (demux));
if (!index)
return;
/* entry may already have been added before, avoid adding indefinitely */
entry = gst_index_get_assoc_entry (index, demux->index_id,
GST_INDEX_LOOKUP_EXACT, GST_ASSOCIATION_FLAG_NONE, GST_FORMAT_BYTES, pos);
if (entry) {
#ifndef GST_DISABLE_GST_DEBUG
gint64 time = 0;
gboolean key;
gst_index_entry_assoc_map (entry, GST_FORMAT_TIME, &time);
key = ! !(GST_INDEX_ASSOC_FLAGS (entry) & GST_ASSOCIATION_FLAG_KEY_UNIT);
GST_LOG_OBJECT (demux, "position already mapped to time %" GST_TIME_FORMAT
", keyframe %d", GST_TIME_ARGS (time), key);
/* there is not really a way to delete the existing one */
if (time != ts || key != ! !keyframe)
GST_DEBUG_OBJECT (demux, "metadata mismatch");
#endif
gst_object_unref (index);
return;
}
associations[0].format = GST_FORMAT_TIME;
associations[0].value = ts;
associations[1].format = GST_FORMAT_BYTES;
associations[1].value = pos;
gst_index_add_associationv (index, demux->index_id,
(keyframe) ? GST_ASSOCIATION_FLAG_KEY_UNIT :
GST_ASSOCIATION_FLAG_DELTA_UNIT, 2,
(const GstIndexAssociation *) &associations);
if (pos > demux->index_max_pos)
demux->index_max_pos = pos;
if (ts > demux->index_max_time)
demux->index_max_time = ts;
gst_object_unref (index);
}
static gchar *
FLV_GET_STRING (GstByteReader * reader)
{
guint16 string_size = 0;
gchar *string = NULL;
const guint8 *str = NULL;
g_return_val_if_fail (reader != NULL, NULL);
if (G_UNLIKELY (!gst_byte_reader_get_uint16_be (reader, &string_size)))
return NULL;
if (G_UNLIKELY (string_size > gst_byte_reader_get_remaining (reader)))
return NULL;
string = g_try_malloc0 (string_size + 1);
if (G_UNLIKELY (!string)) {
return NULL;
}
if (G_UNLIKELY (!gst_byte_reader_get_data (reader, string_size, &str))) {
g_free (string);
return NULL;
}
memcpy (string, str, string_size);
if (!g_utf8_validate (string, string_size, NULL)) {
g_free (string);
return NULL;
}
return string;
}
static void
gst_flv_demux_check_seekability (GstFlvDemux * demux)
{
GstQuery *query;
gint64 start = -1, stop = -1;
demux->upstream_seekable = FALSE;
query = gst_query_new_seeking (GST_FORMAT_BYTES);
if (!gst_pad_peer_query (demux->sinkpad, query)) {
GST_DEBUG_OBJECT (demux, "seeking query failed");
gst_query_unref (query);
return;
}
gst_query_parse_seeking (query, NULL, &demux->upstream_seekable,
&start, &stop);
gst_query_unref (query);
/* try harder to query upstream size if we didn't get it the first time */
if (demux->upstream_seekable && stop == -1) {
GST_DEBUG_OBJECT (demux, "doing duration query to fix up unset stop");
gst_pad_peer_query_duration (demux->sinkpad, GST_FORMAT_BYTES, &stop);
}
/* if upstream doesn't know the size, it's likely that it's not seekable in
* practice even if it technically may be seekable */
if (demux->upstream_seekable && (start != 0 || stop <= start)) {
GST_DEBUG_OBJECT (demux, "seekable but unknown start/stop -> disable");
demux->upstream_seekable = FALSE;
}
GST_DEBUG_OBJECT (demux, "upstream seekable: %d", demux->upstream_seekable);
}
static GstDateTime *
parse_flv_demux_parse_date_string (const gchar * s)
{
static const gchar months[12][4] = {
"Jan", "Feb", "Mar", "Apr",
"May", "Jun", "Jul", "Aug",
"Sep", "Oct", "Nov", "Dec"
};
GstDateTime *dt = NULL;
gchar **tokens;
guint64 d;
gchar *endptr, *stripped;
gint i, hh, mm, ss;
gint year = -1, month = -1, day = -1;
gint hour = -1, minute = -1, seconds = -1;
stripped = g_strstrip (g_strdup (s));
/* "Fri Oct 15 15:13:16 2004" needs to be parsed */
tokens = g_strsplit (stripped, " ", -1);
g_free (stripped);
if (g_strv_length (tokens) != 5)
goto out;
/* year */
d = g_ascii_strtoull (tokens[4], &endptr, 10);
if (d == 0 && *endptr != '\0')
goto out;
year = d;
/* month */
if (strlen (tokens[1]) != 3)
goto out;
for (i = 0; i < 12; i++) {
if (!strcmp (tokens[1], months[i])) {
break;
}
}
if (i == 12)
goto out;
month = i + 1;
/* day */
d = g_ascii_strtoull (tokens[2], &endptr, 10);
if (d == 0 && *endptr != '\0')
goto out;
day = d;
/* time */
hh = mm = ss = 0;
if (sscanf (tokens[3], "%d:%d:%d", &hh, &mm, &ss) < 2)
goto out;
if (hh >= 0 && hh < 24 && mm >= 0 && mm < 60 && ss >= 0 && ss < 60) {
hour = hh;
minute = mm;
seconds = ss;
}
out:
if (tokens)
g_strfreev (tokens);
if (year > 0)
dt = gst_date_time_new (0.0, year, month, day, hour, minute, seconds);
return dt;
}
static gboolean
gst_flv_demux_parse_metadata_item (GstFlvDemux * demux, GstByteReader * reader,
gboolean * end_marker)
{
gchar *tag_name = NULL;
guint8 tag_type = 0;
/* Initialize the end_marker flag to FALSE */
*end_marker = FALSE;
/* Name of the tag */
tag_name = FLV_GET_STRING (reader);
if (G_UNLIKELY (!tag_name)) {
GST_WARNING_OBJECT (demux, "failed reading tag name");
return FALSE;
}
/* What kind of object is that */
if (!gst_byte_reader_get_uint8 (reader, &tag_type))
goto error;
GST_DEBUG_OBJECT (demux, "tag name %s, tag type %d", tag_name, tag_type);
switch (tag_type) {
case 0: /* Double */
{ /* Use a union to read the uint64 and then as a double */
gdouble d = 0;
if (!gst_byte_reader_get_float64_be (reader, &d))
goto error;
GST_DEBUG_OBJECT (demux, "%s => (double) %f", tag_name, d);
if (!strcmp (tag_name, "duration")) {
demux->duration = d * GST_SECOND;
gst_tag_list_add (demux->taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_DURATION, demux->duration, NULL);
} else if (!strcmp (tag_name, "AspectRatioX")) {
demux->par_x = d;
demux->got_par = TRUE;
} else if (!strcmp (tag_name, "AspectRatioY")) {
demux->par_y = d;
demux->got_par = TRUE;
} else if (!strcmp (tag_name, "width")) {
demux->w = d;
} else if (!strcmp (tag_name, "height")) {
demux->h = d;
} else if (!strcmp (tag_name, "framerate")) {
demux->framerate = d;
} else if (!strcmp (tag_name, "audiodatarate")) {
gst_tag_list_add (demux->audio_tags, GST_TAG_MERGE_REPLACE,
GST_TAG_NOMINAL_BITRATE, (guint) (d * 1024), NULL);
} else if (!strcmp (tag_name, "videodatarate")) {
gst_tag_list_add (demux->video_tags, GST_TAG_MERGE_REPLACE,
GST_TAG_NOMINAL_BITRATE, (guint) (d * 1024), NULL);
} else {
GST_INFO_OBJECT (demux, "Tag \'%s\' not handled", tag_name);
}
break;
}
case 1: /* Boolean */
{
guint8 b = 0;
if (!gst_byte_reader_get_uint8 (reader, &b))
goto error;
GST_DEBUG_OBJECT (demux, "%s => (boolean) %d", tag_name, b);
GST_INFO_OBJECT (demux, "Tag \'%s\' not handled", tag_name);
break;
}
case 2: /* String */
{
gchar *s = NULL;
s = FLV_GET_STRING (reader);
if (s == NULL)
goto error;
GST_DEBUG_OBJECT (demux, "%s => (string) %s", tag_name, s);
if (!strcmp (tag_name, "creationdate")) {
GstDateTime *dt;
dt = parse_flv_demux_parse_date_string (s);
if (dt == NULL) {
GST_DEBUG_OBJECT (demux, "Failed to parse '%s' into datetime", s);
} else {
gst_tag_list_add (demux->taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_DATE_TIME, dt, NULL);
gst_date_time_unref (dt);
}
} else if (!strcmp (tag_name, "creator")) {
gst_tag_list_add (demux->taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_ARTIST, s, NULL);
} else if (!strcmp (tag_name, "title")) {
gst_tag_list_add (demux->taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_TITLE, s, NULL);
} else if (!strcmp (tag_name, "metadatacreator")) {
gst_tag_list_add (demux->taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_ENCODER, s, NULL);
} else {
GST_INFO_OBJECT (demux, "Tag \'%s\' not handled", tag_name);
}
g_free (s);
break;
}
case 3: /* Object */
{
gboolean end_of_object_marker = FALSE;
while (!end_of_object_marker) {
gboolean ok = gst_flv_demux_parse_metadata_item (demux, reader,
&end_of_object_marker);
if (G_UNLIKELY (!ok)) {
GST_WARNING_OBJECT (demux, "failed reading a tag, skipping");
goto error;
}
}
break;
}
case 8: /* ECMA array */
{
guint32 nb_elems = 0;
gboolean end_of_object_marker = FALSE;
if (!gst_byte_reader_get_uint32_be (reader, &nb_elems))
goto error;
GST_DEBUG_OBJECT (demux, "there are approx. %d elements in the array",
nb_elems);
while (!end_of_object_marker) {
gboolean ok = gst_flv_demux_parse_metadata_item (demux, reader,
&end_of_object_marker);
if (G_UNLIKELY (!ok)) {
GST_WARNING_OBJECT (demux, "failed reading a tag, skipping");
goto error;
}
}
break;
}
case 9: /* End marker */
{
GST_DEBUG_OBJECT (demux, "end marker ?");
if (tag_name[0] == '\0') {
GST_DEBUG_OBJECT (demux, "end marker detected");
*end_marker = TRUE;
}
break;
}
case 10: /* Array */
{
guint32 nb_elems = 0;
if (!gst_byte_reader_get_uint32_be (reader, &nb_elems))
goto error;
GST_DEBUG_OBJECT (demux, "array has %d elements", nb_elems);
if (!strcmp (tag_name, "times")) {
if (demux->times) {
g_array_free (demux->times, TRUE);
}
demux->times = g_array_new (FALSE, TRUE, sizeof (gdouble));
} else if (!strcmp (tag_name, "filepositions")) {
if (demux->filepositions) {
g_array_free (demux->filepositions, TRUE);
}
demux->filepositions = g_array_new (FALSE, TRUE, sizeof (gdouble));
}
while (nb_elems--) {
guint8 elem_type = 0;
if (!gst_byte_reader_get_uint8 (reader, &elem_type))
goto error;
switch (elem_type) {
case 0:
{
gdouble d;
if (!gst_byte_reader_get_float64_be (reader, &d))
goto error;
GST_DEBUG_OBJECT (demux, "element is a double %f", d);
if (!strcmp (tag_name, "times") && demux->times) {
g_array_append_val (demux->times, d);
} else if (!strcmp (tag_name, "filepositions") &&
demux->filepositions) {
g_array_append_val (demux->filepositions, d);
}
break;
}
default:
GST_WARNING_OBJECT (demux, "unsupported array element type %d",
elem_type);
}
}
break;
}
case 11: /* Date */
{
gdouble d = 0;
gint16 i = 0;
if (!gst_byte_reader_get_float64_be (reader, &d))
goto error;
if (!gst_byte_reader_get_int16_be (reader, &i))
goto error;
GST_DEBUG_OBJECT (demux,
"%s => (date as a double) %f, timezone offset %d", tag_name, d, i);
GST_INFO_OBJECT (demux, "Tag \'%s\' not handled", tag_name);
break;
}
default:
GST_WARNING_OBJECT (demux, "unsupported tag type %d", tag_type);
}
g_free (tag_name);
return TRUE;
error:
g_free (tag_name);
return FALSE;
}
static void
gst_flv_demux_clear_tags (GstFlvDemux * demux)
{
GST_DEBUG_OBJECT (demux, "clearing taglist");
if (demux->taglist) {
gst_tag_list_unref (demux->taglist);
}
demux->taglist = gst_tag_list_new_empty ();
gst_tag_list_set_scope (demux->taglist, GST_TAG_SCOPE_GLOBAL);
if (demux->audio_tags) {
gst_tag_list_unref (demux->audio_tags);
}
demux->audio_tags = gst_tag_list_new_empty ();
if (demux->video_tags) {
gst_tag_list_unref (demux->video_tags);
}
demux->video_tags = gst_tag_list_new_empty ();
}
static GstFlowReturn
gst_flv_demux_parse_tag_script (GstFlvDemux * demux, GstBuffer * buffer)
{
GstFlowReturn ret = GST_FLOW_OK;
GstByteReader reader;
guint8 type = 0;
GstMapInfo map;
g_return_val_if_fail (gst_buffer_get_size (buffer) >= 7, GST_FLOW_ERROR);
gst_buffer_map (buffer, &map, GST_MAP_READ);
gst_byte_reader_init (&reader, map.data, map.size);
gst_byte_reader_skip_unchecked (&reader, 7);
GST_LOG_OBJECT (demux, "parsing a script tag");
if (!gst_byte_reader_get_uint8 (&reader, &type))
goto cleanup;
/* Must be string */
if (type == 2) {
gchar *function_name;
guint i;
function_name = FLV_GET_STRING (&reader);
GST_LOG_OBJECT (demux, "function name is %s", GST_STR_NULL (function_name));
if (function_name != NULL && strcmp (function_name, "onMetaData") == 0) {
gboolean end_marker = FALSE;
GST_DEBUG_OBJECT (demux, "we have a metadata script object");
gst_flv_demux_clear_tags (demux);
if (!gst_byte_reader_get_uint8 (&reader, &type)) {
g_free (function_name);
goto cleanup;
}
switch (type) {
case 8:
{
guint32 nb_elems = 0;
/* ECMA array */
if (!gst_byte_reader_get_uint32_be (&reader, &nb_elems)) {
g_free (function_name);
goto cleanup;
}
/* The number of elements is just a hint, some files have
nb_elements == 0 and actually contain items. */
GST_DEBUG_OBJECT (demux, "there are approx. %d elements in the array",
nb_elems);
}
/* fallthrough to read data */
case 3:
{
/* Object */
while (!end_marker) {
gboolean ok =
gst_flv_demux_parse_metadata_item (demux, &reader, &end_marker);
if (G_UNLIKELY (!ok)) {
GST_WARNING_OBJECT (demux, "failed reading a tag, skipping");
break;
}
}
}
break;
default:
GST_DEBUG_OBJECT (demux, "Unhandled script data type : %d", type);
g_free (function_name);
goto cleanup;
}
gst_flv_demux_push_tags (demux);
}
g_free (function_name);
if (demux->times && demux->filepositions) {
guint num;
/* If an index was found, insert associations */
num = MIN (demux->times->len, demux->filepositions->len);
for (i = 0; i < num; i++) {
guint64 time, fileposition;
time = g_array_index (demux->times, gdouble, i) * GST_SECOND;
fileposition = g_array_index (demux->filepositions, gdouble, i);
gst_flv_demux_parse_and_add_index_entry (demux, time, fileposition,
TRUE);
}
demux->indexed = TRUE;
}
}
cleanup:
gst_buffer_unmap (buffer, &map);
return ret;
}
static gboolean
have_group_id (GstFlvDemux * demux)
{
GstEvent *event;
event = gst_pad_get_sticky_event (demux->sinkpad, GST_EVENT_STREAM_START, 0);
if (event) {
if (gst_event_parse_group_id (event, &demux->group_id))
demux->have_group_id = TRUE;
else
demux->have_group_id = FALSE;
gst_event_unref (event);
} else if (!demux->have_group_id) {
demux->have_group_id = TRUE;
demux->group_id = gst_util_group_id_next ();
}
return demux->have_group_id;
}
static gboolean
gst_flv_demux_audio_negotiate (GstFlvDemux * demux, guint32 codec_tag,
guint32 rate, guint32 channels, guint32 width)
{
GstCaps *caps = NULL, *old_caps;
gboolean ret = FALSE;
guint adjusted_rate = rate;
GstEvent *event;
gchar *stream_id;
switch (codec_tag) {
case 1:
caps = gst_caps_new_simple ("audio/x-adpcm", "layout", G_TYPE_STRING,
"swf", NULL);
break;
case 2:
case 14:
caps = gst_caps_new_simple ("audio/mpeg",
"mpegversion", G_TYPE_INT, 1, "layer", G_TYPE_INT, 3,
"parsed", G_TYPE_BOOLEAN, TRUE, NULL);
break;
case 0:
case 3:
{
GstAudioFormat format;
/* Assuming little endian for 0 (aka endianness of the
* system on which the file was created) as most people
* are probably using little endian machines */
format = gst_audio_format_build_integer ((width == 8) ? FALSE : TRUE,
G_LITTLE_ENDIAN, width, width);
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, gst_audio_format_to_string (format),
"layout", G_TYPE_STRING, "interleaved", NULL);
break;
}
case 4:
case 5:
case 6:
caps = gst_caps_new_empty_simple ("audio/x-nellymoser");
break;
case 10:
{
GstMapInfo map;
if (!demux->audio_codec_data) {
GST_DEBUG_OBJECT (demux, "don't have AAC codec data yet");
ret = TRUE;
goto done;
}
gst_buffer_map (demux->audio_codec_data, &map, GST_MAP_READ);
/* use codec-data to extract and verify samplerate */
if (map.size >= 2) {
gint freq_index;
freq_index = GST_READ_UINT16_BE (map.data);
freq_index = (freq_index & 0x0780) >> 7;
adjusted_rate =
gst_codec_utils_aac_get_sample_rate_from_index (freq_index);
if (adjusted_rate && (rate != adjusted_rate)) {
GST_LOG_OBJECT (demux, "Ajusting AAC sample rate %d -> %d", rate,
adjusted_rate);
} else {
adjusted_rate = rate;
}
}
gst_buffer_unmap (demux->audio_codec_data, &map);
caps = gst_caps_new_simple ("audio/mpeg",
"mpegversion", G_TYPE_INT, 4, "framed", G_TYPE_BOOLEAN, TRUE,
"stream-format", G_TYPE_STRING, "raw", NULL);
break;
}
case 7:
caps = gst_caps_new_empty_simple ("audio/x-alaw");
break;
case 8:
caps = gst_caps_new_empty_simple ("audio/x-mulaw");
break;
case 11:
{
GValue streamheader = G_VALUE_INIT;
GValue value = G_VALUE_INIT;
GstByteWriter w;
GstStructure *structure;
GstBuffer *buf;
GstTagList *tags;
caps = gst_caps_new_empty_simple ("audio/x-speex");
structure = gst_caps_get_structure (caps, 0);
GST_DEBUG_OBJECT (demux, "generating speex header");
/* Speex decoder expects streamheader to be { [header], [comment] } */
g_value_init (&streamheader, GST_TYPE_ARRAY);
/* header part */
gst_byte_writer_init_with_size (&w, 80, TRUE);
gst_byte_writer_put_data (&w, (guint8 *) "Speex ", 8);
gst_byte_writer_put_data (&w, (guint8 *) "1.1.12", 7);
gst_byte_writer_fill (&w, 0, 13);
gst_byte_writer_put_uint32_le (&w, 1); /* version */
gst_byte_writer_put_uint32_le (&w, 80); /* header_size */
gst_byte_writer_put_uint32_le (&w, 16000); /* rate */
gst_byte_writer_put_uint32_le (&w, 1); /* mode: Wideband */
gst_byte_writer_put_uint32_le (&w, 4); /* mode_bitstream_version */
gst_byte_writer_put_uint32_le (&w, 1); /* nb_channels: 1 */
gst_byte_writer_put_uint32_le (&w, -1); /* bitrate */
gst_byte_writer_put_uint32_le (&w, 0x50); /* frame_size */
gst_byte_writer_put_uint32_le (&w, 0); /* VBR */
gst_byte_writer_put_uint32_le (&w, 1); /* frames_per_packet */
gst_byte_writer_put_uint32_le (&w, 0); /* extra_headers */
gst_byte_writer_put_uint32_le (&w, 0); /* reserved1 */
gst_byte_writer_put_uint32_le (&w, 0); /* reserved2 */
g_assert (gst_byte_writer_get_size (&w) == 80);
g_value_init (&value, GST_TYPE_BUFFER);
g_value_take_boxed (&value, gst_byte_writer_reset_and_get_buffer (&w));
gst_value_array_append_value (&streamheader, &value);
g_value_unset (&value);
/* comment part */
g_value_init (&value, GST_TYPE_BUFFER);
tags = gst_tag_list_new_empty ();
buf = gst_tag_list_to_vorbiscomment_buffer (tags, NULL, 0, "No comments");
gst_tag_list_unref (tags);
g_value_take_boxed (&value, buf);
gst_value_array_append_value (&streamheader, &value);
g_value_unset (&value);
gst_structure_take_value (structure, "streamheader", &streamheader);
channels = 1;
adjusted_rate = 16000;
break;
}
default:
GST_WARNING_OBJECT (demux, "unsupported audio codec tag %u", codec_tag);
break;
}
if (G_UNLIKELY (!caps)) {
GST_WARNING_OBJECT (demux, "failed creating caps for audio pad");
goto beach;
}
gst_caps_set_simple (caps, "rate", G_TYPE_INT, adjusted_rate,
"channels", G_TYPE_INT, channels, NULL);
if (demux->audio_codec_data) {
gst_caps_set_simple (caps, "codec_data", GST_TYPE_BUFFER,
demux->audio_codec_data, NULL);
}
old_caps = gst_pad_get_current_caps (demux->audio_pad);
if (!old_caps) {
stream_id =
gst_pad_create_stream_id (demux->audio_pad, GST_ELEMENT_CAST (demux),
"audio");
event = gst_event_new_stream_start (stream_id);
if (have_group_id (demux))
gst_event_set_group_id (event, demux->group_id);
gst_pad_push_event (demux->audio_pad, event);
g_free (stream_id);
}
if (!old_caps || !gst_caps_is_equal (old_caps, caps))
ret = gst_pad_set_caps (demux->audio_pad, caps);
else
ret = TRUE;
if (old_caps)
gst_caps_unref (old_caps);
done:
if (G_LIKELY (ret)) {
/* Store the caps we got from tags */
demux->audio_codec_tag = codec_tag;
demux->rate = rate;
demux->channels = channels;
demux->width = width;
if (caps) {
GST_DEBUG_OBJECT (demux->audio_pad, "successfully negotiated caps %"
GST_PTR_FORMAT, caps);
gst_flv_demux_push_tags (demux);
} else {
GST_DEBUG_OBJECT (demux->audio_pad, "delayed setting caps");
}
} else {
GST_WARNING_OBJECT (demux->audio_pad, "failed negotiating caps %"
GST_PTR_FORMAT, caps);
}
if (caps)
gst_caps_unref (caps);
beach:
return ret;
}
static gboolean
gst_flv_demux_push_src_event (GstFlvDemux * demux, GstEvent * event)
{
gboolean ret = TRUE;
if (demux->audio_pad)
ret |= gst_pad_push_event (demux->audio_pad, gst_event_ref (event));
if (demux->video_pad)
ret |= gst_pad_push_event (demux->video_pad, gst_event_ref (event));
gst_event_unref (event);
return ret;
}
static void
gst_flv_demux_add_codec_tag (GstFlvDemux * demux, const gchar * tag,
GstPad * pad)
{
if (pad) {
GstCaps *caps = gst_pad_get_current_caps (pad);
if (caps) {
gchar *codec_name = gst_pb_utils_get_codec_description (caps);
if (codec_name) {
gst_tag_list_add (demux->taglist, GST_TAG_MERGE_REPLACE,
tag, codec_name, NULL);
g_free (codec_name);
}
gst_caps_unref (caps);
}
}
}
static void
gst_flv_demux_push_tags (GstFlvDemux * demux)
{
gst_flv_demux_add_codec_tag (demux, GST_TAG_AUDIO_CODEC, demux->audio_pad);
gst_flv_demux_add_codec_tag (demux, GST_TAG_VIDEO_CODEC, demux->video_pad);
GST_DEBUG_OBJECT (demux, "pushing %" GST_PTR_FORMAT, demux->taglist);
gst_flv_demux_push_src_event (demux,
gst_event_new_tag (gst_tag_list_copy (demux->taglist)));
if (demux->audio_pad) {
GST_DEBUG_OBJECT (demux->audio_pad, "pushing audio %" GST_PTR_FORMAT,
demux->audio_tags);
gst_pad_push_event (demux->audio_pad,
gst_event_new_tag (gst_tag_list_copy (demux->audio_tags)));
}
if (demux->video_pad) {
GST_DEBUG_OBJECT (demux->video_pad, "pushing video %" GST_PTR_FORMAT,
demux->video_tags);
gst_pad_push_event (demux->video_pad,
gst_event_new_tag (gst_tag_list_copy (demux->video_tags)));
}
}
static gboolean
gst_flv_demux_update_resync (GstFlvDemux * demux, guint32 dts, gboolean discont,
guint32 * last, GstClockTime * offset)
{
gboolean ret = FALSE;
gint32 ddts = dts - *last;
if (!discont && ddts <= -RESYNC_THRESHOLD) {
/* Theoretically, we should use substract the duration of the last buffer,
but this demuxer sends no durations on buffers, not sure if it cannot
know, or just does not care to calculate. */
*offset -= ddts * GST_MSECOND;
GST_WARNING_OBJECT (demux,
"Large dts gap (%" G_GINT32_FORMAT " ms), assuming resync, offset now %"
GST_TIME_FORMAT "", ddts, GST_TIME_ARGS (*offset));
ret = TRUE;
}
*last = dts;
return ret;
}
static GstFlowReturn
gst_flv_demux_parse_tag_audio (GstFlvDemux * demux, GstBuffer * buffer)
{
GstFlowReturn ret = GST_FLOW_OK;
guint32 pts = 0, codec_tag = 0, rate = 5512, width = 8, channels = 1;
guint32 codec_data = 0, pts_ext = 0;
guint8 flags = 0;
GstMapInfo map;
GstBuffer *outbuf;
guint8 *data;
GST_LOG_OBJECT (demux, "parsing an audio tag");
if (G_UNLIKELY (!demux->audio_pad && demux->no_more_pads)) {
#ifndef GST_DISABLE_DEBUG
if (G_UNLIKELY (!demux->no_audio_warned)) {
GST_WARNING_OBJECT (demux,
"Signaled no-more-pads already but had no audio pad -- ignoring");
demux->no_audio_warned = TRUE;
}
#endif
return GST_FLOW_OK;
}
g_return_val_if_fail (gst_buffer_get_size (buffer) == demux->tag_size,
GST_FLOW_ERROR);
/* Error out on tags with too small headers */
if (gst_buffer_get_size (buffer) < 11) {
GST_ERROR_OBJECT (demux, "Too small tag size (%" G_GSIZE_FORMAT ")",
gst_buffer_get_size (buffer));
return GST_FLOW_ERROR;
}
gst_buffer_map (buffer, &map, GST_MAP_READ);
data = map.data;
/* Grab information about audio tag */
pts = GST_READ_UINT24_BE (data);
/* read the pts extension to 32 bits integer */
pts_ext = GST_READ_UINT8 (data + 3);
/* Combine them */
pts |= pts_ext << 24;
GST_LOG_OBJECT (demux, "pts bytes %02X %02X %02X %02X (%d)", data[0], data[1],
data[2], data[3], pts);
/* Skip the stream id and go directly to the flags */
flags = GST_READ_UINT8 (data + 7);
/* Silently skip buffers with no data */
if (map.size == 11)
goto beach;
/* Channels */
if (flags & 0x01) {
channels = 2;
}
/* Width */
if (flags & 0x02) {
width = 16;
}
/* Sampling rate */
if ((flags & 0x0C) == 0x0C) {
rate = 44100;
} else if ((flags & 0x0C) == 0x08) {
rate = 22050;
} else if ((flags & 0x0C) == 0x04) {
rate = 11025;
}
/* Codec tag */
codec_tag = flags >> 4;
if (codec_tag == 10) { /* AAC has an extra byte for packet type */
codec_data = 2;
} else {
codec_data = 1;
}
/* codec tags with special rates */
if (codec_tag == 5 || codec_tag == 14)
rate = 8000;
else if ((codec_tag == 4) || (codec_tag == 11))
rate = 16000;
GST_LOG_OBJECT (demux, "audio tag with %d channels, %dHz sampling rate, "
"%d bits width, codec tag %u (flags %02X)", channels, rate, width,
codec_tag, flags);
if (codec_tag == 10) {
guint8 aac_packet_type = GST_READ_UINT8 (data + 8);
switch (aac_packet_type) {
case 0:
{
/* AudioSpecificConfig data */
GST_LOG_OBJECT (demux, "got an AAC codec data packet");
if (demux->audio_codec_data) {
gst_buffer_unref (demux->audio_codec_data);
}
demux->audio_codec_data =
gst_buffer_copy_region (buffer, GST_BUFFER_COPY_MEMORY,
7 + codec_data, demux->tag_data_size - codec_data);
/* Use that buffer data in the caps */
if (demux->audio_pad)
gst_flv_demux_audio_negotiate (demux, codec_tag, rate, channels,
width);
goto beach;
}
case 1:
if (!demux->audio_codec_data) {
GST_ERROR_OBJECT (demux, "got AAC audio packet before codec data");
ret = GST_FLOW_OK;
goto beach;
}
/* AAC raw packet */
GST_LOG_OBJECT (demux, "got a raw AAC audio packet");
break;
default:
GST_WARNING_OBJECT (demux, "invalid AAC packet type %u",
aac_packet_type);
}
}
/* If we don't have our audio pad created, then create it. */
if (G_UNLIKELY (!demux->audio_pad)) {
demux->audio_pad =
gst_pad_new_from_template (gst_element_class_get_pad_template
(GST_ELEMENT_GET_CLASS (demux), "audio"), "audio");
if (G_UNLIKELY (!demux->audio_pad)) {
GST_WARNING_OBJECT (demux, "failed creating audio pad");
ret = GST_FLOW_ERROR;
goto beach;
}
/* Set functions on the pad */
gst_pad_set_query_function (demux->audio_pad,
GST_DEBUG_FUNCPTR (gst_flv_demux_query));
gst_pad_set_event_function (demux->audio_pad,
GST_DEBUG_FUNCPTR (gst_flv_demux_src_event));
gst_pad_use_fixed_caps (demux->audio_pad);
/* Make it active */
gst_pad_set_active (demux->audio_pad, TRUE);
/* Negotiate caps */
if (!gst_flv_demux_audio_negotiate (demux, codec_tag, rate, channels,
width)) {
gst_object_unref (demux->audio_pad);
demux->audio_pad = NULL;
ret = GST_FLOW_ERROR;
goto beach;
}
#ifndef GST_DISABLE_GST_DEBUG
{
GstCaps *caps;
caps = gst_pad_get_current_caps (demux->audio_pad);
GST_DEBUG_OBJECT (demux, "created audio pad with caps %" GST_PTR_FORMAT,
caps);
if (caps)
gst_caps_unref (caps);
}
#endif
/* We need to set caps before adding */
gst_element_add_pad (GST_ELEMENT (demux),
gst_object_ref (demux->audio_pad));
gst_flow_combiner_add_pad (demux->flowcombiner, demux->audio_pad);
/* We only emit no more pads when we have audio and video. Indeed we can
* not trust the FLV header to tell us if there will be only audio or
* only video and we would just break discovery of some files */
if (demux->audio_pad && demux->video_pad) {
GST_DEBUG_OBJECT (demux, "emitting no more pads");
gst_element_no_more_pads (GST_ELEMENT (demux));
demux->no_more_pads = TRUE;
}
}
/* Check if caps have changed */
if (G_UNLIKELY (rate != demux->rate || channels != demux->channels ||
codec_tag != demux->audio_codec_tag || width != demux->width)) {
GST_DEBUG_OBJECT (demux, "audio settings have changed, changing caps");
gst_buffer_replace (&demux->audio_codec_data, NULL);
/* Negotiate caps */
if (!gst_flv_demux_audio_negotiate (demux, codec_tag, rate, channels,
width)) {
ret = GST_FLOW_ERROR;
goto beach;
}
}
/* Check if we have anything to push */
if (demux->tag_data_size <= codec_data) {
GST_LOG_OBJECT (demux, "Nothing left in this tag, returning");
goto beach;
}
/* Create buffer from pad */
outbuf = gst_buffer_copy_region (buffer, GST_BUFFER_COPY_MEMORY,
7 + codec_data, demux->tag_data_size - codec_data);
/* detect (and deem to be resyncs) large pts gaps */
if (gst_flv_demux_update_resync (demux, pts, demux->audio_need_discont,
&demux->last_audio_pts, &demux->audio_time_offset)) {
GST_BUFFER_FLAG_SET (outbuf, GST_BUFFER_FLAG_RESYNC);
}
/* Fill buffer with data */
GST_BUFFER_PTS (outbuf) = pts * GST_MSECOND + demux->audio_time_offset;
GST_BUFFER_DTS (outbuf) = GST_BUFFER_PTS (outbuf);
GST_BUFFER_DURATION (outbuf) = GST_CLOCK_TIME_NONE;
GST_BUFFER_OFFSET (outbuf) = demux->audio_offset++;
GST_BUFFER_OFFSET_END (outbuf) = demux->audio_offset;
if (demux->duration == GST_CLOCK_TIME_NONE ||
demux->duration < GST_BUFFER_TIMESTAMP (outbuf))
demux->duration = GST_BUFFER_TIMESTAMP (outbuf);
/* Only add audio frames to the index if we have no video,
* and if the index is not yet complete */
if (!demux->has_video && !demux->indexed) {
gst_flv_demux_parse_and_add_index_entry (demux,
GST_BUFFER_TIMESTAMP (outbuf), demux->cur_tag_offset, TRUE);
}
if (G_UNLIKELY (demux->audio_need_discont)) {
GST_BUFFER_FLAG_SET (outbuf, GST_BUFFER_FLAG_DISCONT);
demux->audio_need_discont = FALSE;
}
demux->segment.position = GST_BUFFER_TIMESTAMP (outbuf);
/* Do we need a newsegment event ? */
if (G_UNLIKELY (demux->audio_need_segment)) {
if (!demux->new_seg_event) {
GST_DEBUG_OBJECT (demux, "pushing newsegment from %"
GST_TIME_FORMAT " to %" GST_TIME_FORMAT,
GST_TIME_ARGS (demux->segment.position),
GST_TIME_ARGS (demux->segment.stop));
demux->segment.start = demux->segment.time = demux->segment.position;
demux->new_seg_event = gst_event_new_segment (&demux->segment);
} else {
GST_DEBUG_OBJECT (demux, "pushing pre-generated newsegment event");
}
gst_pad_push_event (demux->audio_pad, gst_event_ref (demux->new_seg_event));
demux->audio_need_segment = FALSE;
}
GST_LOG_OBJECT (demux,
"pushing %" G_GSIZE_FORMAT " bytes buffer at pts %" GST_TIME_FORMAT
" with duration %" GST_TIME_FORMAT ", offset %" G_GUINT64_FORMAT,
gst_buffer_get_size (outbuf),
GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (outbuf)),
GST_TIME_ARGS (GST_BUFFER_DURATION (outbuf)), GST_BUFFER_OFFSET (outbuf));
if (!GST_CLOCK_TIME_IS_VALID (demux->audio_start)) {
demux->audio_start = GST_BUFFER_TIMESTAMP (outbuf);
}
if (!GST_CLOCK_TIME_IS_VALID (demux->audio_first_ts)) {
demux->audio_first_ts = GST_BUFFER_TIMESTAMP (outbuf);
}
if (G_UNLIKELY (!demux->no_more_pads
&& (GST_CLOCK_DIFF (demux->audio_start,
GST_BUFFER_TIMESTAMP (outbuf)) > NO_MORE_PADS_THRESHOLD))) {
GST_DEBUG_OBJECT (demux,
"Signalling no-more-pads because no video stream was found"
" after 6 seconds of audio");
gst_element_no_more_pads (GST_ELEMENT_CAST (demux));
demux->no_more_pads = TRUE;
}
/* Push downstream */
ret = gst_pad_push (demux->audio_pad, outbuf);
if (G_UNLIKELY (ret != GST_FLOW_OK) &&
demux->segment.rate < 0.0 && ret == GST_FLOW_EOS &&
demux->segment.position > demux->segment.stop) {
/* In reverse playback we can get a GST_FLOW_EOS when
* we are at the end of the segment, so we just need to jump
* back to the previous section. */
GST_DEBUG_OBJECT (demux, "downstream has reached end of segment");
demux->audio_done = TRUE;
ret = GST_FLOW_OK;
goto beach;
}
ret = gst_flow_combiner_update_pad_flow (demux->flowcombiner,
demux->audio_pad, ret);
beach:
gst_buffer_unmap (buffer, &map);
return ret;
}
static gboolean
gst_flv_demux_video_negotiate (GstFlvDemux * demux, guint32 codec_tag)
{
gboolean ret = FALSE;
GstCaps *caps = NULL, *old_caps;
GstEvent *event;
gchar *stream_id;
/* Generate caps for that pad */
switch (codec_tag) {
case 2:
caps =
gst_caps_new_simple ("video/x-flash-video", "flvversion", G_TYPE_INT,
1, NULL);
break;
case 3:
caps = gst_caps_new_empty_simple ("video/x-flash-screen");
break;
case 4:
caps = gst_caps_new_empty_simple ("video/x-vp6-flash");
break;
case 5:
caps = gst_caps_new_empty_simple ("video/x-vp6-alpha");
break;
case 7:
if (!demux->video_codec_data) {
GST_DEBUG_OBJECT (demux, "don't have h264 codec data yet");
ret = TRUE;
goto done;
}
caps =
gst_caps_new_simple ("video/x-h264", "stream-format", G_TYPE_STRING,
"avc", NULL);
break;
/* The following two are non-standard but apparently used, see in ffmpeg
* https://git.videolan.org/?p=ffmpeg.git;a=blob;f=libavformat/flvdec.c;h=2bf1e059e1cbeeb79e4af9542da23f4560e1cf59;hb=b18d6c58000beed872d6bb1fe7d0fbe75ae26aef#l254
* https://git.videolan.org/?p=ffmpeg.git;a=blob;f=libavformat/flvdec.c;h=2bf1e059e1cbeeb79e4af9542da23f4560e1cf59;hb=b18d6c58000beed872d6bb1fe7d0fbe75ae26aef#l282
*/
case 8:
caps = gst_caps_new_empty_simple ("video/x-h263");
break;
case 9:
caps =
gst_caps_new_simple ("video/mpeg", "mpegversion", G_TYPE_INT, 4,
"systemstream", G_TYPE_BOOLEAN, FALSE, NULL);
break;
default:
GST_WARNING_OBJECT (demux, "unsupported video codec tag %u", codec_tag);
}
if (G_UNLIKELY (!caps)) {
GST_WARNING_OBJECT (demux, "failed creating caps for video pad");
goto beach;
}
if (demux->got_par) {
gst_caps_set_simple (caps, "pixel-aspect-ratio", GST_TYPE_FRACTION,
demux->par_x, demux->par_y, NULL);
}
if (G_LIKELY (demux->w)) {
gst_caps_set_simple (caps, "width", G_TYPE_INT, demux->w, NULL);
}
if (G_LIKELY (demux->h)) {
gst_caps_set_simple (caps, "height", G_TYPE_INT, demux->h, NULL);
}
if (G_LIKELY (demux->framerate)) {
gint num = 0, den = 0;
gst_video_guess_framerate (GST_SECOND / demux->framerate, &num, &den);
GST_DEBUG_OBJECT (demux->video_pad,
"fps to be used on caps %f (as a fraction = %d/%d)", demux->framerate,
num, den);
gst_caps_set_simple (caps, "framerate", GST_TYPE_FRACTION, num, den, NULL);
}
if (demux->video_codec_data) {
gst_caps_set_simple (caps, "codec_data", GST_TYPE_BUFFER,
demux->video_codec_data, NULL);
}
old_caps = gst_pad_get_current_caps (demux->video_pad);
if (!old_caps) {
stream_id =
gst_pad_create_stream_id (demux->video_pad, GST_ELEMENT_CAST (demux),
"video");
event = gst_event_new_stream_start (stream_id);
g_free (stream_id);
if (have_group_id (demux))
gst_event_set_group_id (event, demux->group_id);
gst_pad_push_event (demux->video_pad, event);
}
if (!old_caps || !gst_caps_is_equal (old_caps, caps))
ret = gst_pad_set_caps (demux->video_pad, caps);
else
ret = TRUE;
if (old_caps)
gst_caps_unref (old_caps);
done:
if (G_LIKELY (ret)) {
/* Store the caps we have set */
demux->video_codec_tag = codec_tag;
if (caps) {
GST_DEBUG_OBJECT (demux->video_pad, "successfully negotiated caps %"
GST_PTR_FORMAT, caps);
gst_flv_demux_push_tags (demux);
} else {
GST_DEBUG_OBJECT (demux->video_pad, "delayed setting caps");
}
} else {
GST_WARNING_OBJECT (demux->video_pad, "failed negotiating caps %"
GST_PTR_FORMAT, caps);
}
if (caps)
gst_caps_unref (caps);
beach:
return ret;
}
static GstFlowReturn
gst_flv_demux_parse_tag_video (GstFlvDemux * demux, GstBuffer * buffer)
{
GstFlowReturn ret = GST_FLOW_OK;
guint32 dts = 0, codec_data = 1, dts_ext = 0;
gint32 cts = 0;
gboolean keyframe = FALSE;
guint8 flags = 0, codec_tag = 0;
GstBuffer *outbuf;
GstMapInfo map;
guint8 *data;
g_return_val_if_fail (gst_buffer_get_size (buffer) == demux->tag_size,
GST_FLOW_ERROR);
GST_LOG_OBJECT (demux, "parsing a video tag");
if G_UNLIKELY
(!demux->video_pad && demux->no_more_pads) {
#ifndef GST_DISABLE_DEBUG
if G_UNLIKELY
(!demux->no_video_warned) {
GST_WARNING_OBJECT (demux,
"Signaled no-more-pads already but had no video pad -- ignoring");
demux->no_video_warned = TRUE;
}
#endif
return GST_FLOW_OK;
}
if (gst_buffer_get_size (buffer) < 12) {
GST_ERROR_OBJECT (demux, "Too small tag size");
return GST_FLOW_ERROR;
}
gst_buffer_map (buffer, &map, GST_MAP_READ);
data = map.data;
/* Grab information about video tag */
dts = GST_READ_UINT24_BE (data);
/* read the dts extension to 32 bits integer */
dts_ext = GST_READ_UINT8 (data + 3);
/* Combine them */
dts |= dts_ext << 24;
GST_LOG_OBJECT (demux, "dts bytes %02X %02X %02X %02X (%d)", data[0], data[1],
data[2], data[3], dts);
/* Skip the stream id and go directly to the flags */
flags = GST_READ_UINT8 (data + 7);
/* Keyframe */
if ((flags >> 4) == 1) {
keyframe = TRUE;
}
/* Codec tag */
codec_tag = flags & 0x0F;
if (codec_tag == 4 || codec_tag == 5) {
codec_data = 2;
} else if (codec_tag == 7) {
codec_data = 5;
cts = GST_READ_UINT24_BE (data + 9);
cts = (cts + 0xff800000) ^ 0xff800000;
if (cts < 0 && ABS (cts) > dts) {
GST_ERROR_OBJECT (demux, "Detected a negative composition time offset "
"'%d' that would lead to negative PTS, fixing", cts);
cts += ABS (cts) - dts;
}
GST_LOG_OBJECT (demux, "got cts %d", cts);
}
GST_LOG_OBJECT (demux, "video tag with codec tag %u, keyframe (%d) "
"(flags %02X)", codec_tag, keyframe, flags);
if (codec_tag == 7) {
guint8 avc_packet_type = GST_READ_UINT8 (data + 8);
switch (avc_packet_type) {
case 0:
{
if (demux->tag_data_size < codec_data) {
GST_ERROR_OBJECT (demux, "Got invalid H.264 codec, ignoring.");
break;
}
/* AVCDecoderConfigurationRecord data */
GST_LOG_OBJECT (demux, "got an H.264 codec data packet");
if (demux->video_codec_data) {
gst_buffer_unref (demux->video_codec_data);
}
demux->video_codec_data = gst_buffer_copy_region (buffer,
GST_BUFFER_COPY_MEMORY, 7 + codec_data,
demux->tag_data_size - codec_data);;
/* Use that buffer data in the caps */
if (demux->video_pad)
gst_flv_demux_video_negotiate (demux, codec_tag);
goto beach;
}
case 1:
/* H.264 NALU packet */
if (!demux->video_codec_data) {
GST_ERROR_OBJECT (demux, "got H.264 video packet before codec data");
ret = GST_FLOW_OK;
goto beach;
}
GST_LOG_OBJECT (demux, "got a H.264 NALU video packet");
break;
default:
GST_WARNING_OBJECT (demux, "invalid video packet type %u",
avc_packet_type);
}
}
/* If we don't have our video pad created, then create it. */
if (G_UNLIKELY (!demux->video_pad)) {
demux->video_pad =
gst_pad_new_from_template (gst_element_class_get_pad_template
(GST_ELEMENT_GET_CLASS (demux), "video"), "video");
if (G_UNLIKELY (!demux->video_pad)) {
GST_WARNING_OBJECT (demux, "failed creating video pad");
ret = GST_FLOW_ERROR;
goto beach;
}
/* Set functions on the pad */
gst_pad_set_query_function (demux->video_pad,
GST_DEBUG_FUNCPTR (gst_flv_demux_query));
gst_pad_set_event_function (demux->video_pad,
GST_DEBUG_FUNCPTR (gst_flv_demux_src_event));
gst_pad_use_fixed_caps (demux->video_pad);
/* Make it active */
gst_pad_set_active (demux->video_pad, TRUE);
/* Needs to be active before setting caps */
if (!gst_flv_demux_video_negotiate (demux, codec_tag)) {
gst_object_unref (demux->video_pad);
demux->video_pad = NULL;
ret = GST_FLOW_ERROR;
goto beach;
}
/* When we ve set pixel-aspect-ratio we use that boolean to detect a
* metadata tag that would come later and trigger a caps change */
demux->got_par = FALSE;
#ifndef GST_DISABLE_GST_DEBUG
{
GstCaps *caps;
caps = gst_pad_get_current_caps (demux->video_pad);
GST_DEBUG_OBJECT (demux, "created video pad with caps %" GST_PTR_FORMAT,
caps);
if (caps)
gst_caps_unref (caps);
}
#endif
/* We need to set caps before adding */
gst_element_add_pad (GST_ELEMENT (demux),
gst_object_ref (demux->video_pad));
gst_flow_combiner_add_pad (demux->flowcombiner, demux->video_pad);
/* We only emit no more pads when we have audio and video. Indeed we can
* not trust the FLV header to tell us if there will be only audio or
* only video and we would just break discovery of some files */
if (demux->audio_pad && demux->video_pad) {
GST_DEBUG_OBJECT (demux, "emitting no more pads");
gst_element_no_more_pads (GST_ELEMENT (demux));
demux->no_more_pads = TRUE;
}
}
/* Check if caps have changed */
if (G_UNLIKELY (codec_tag != demux->video_codec_tag || demux->got_par)) {
GST_DEBUG_OBJECT (demux, "video settings have changed, changing caps");
gst_buffer_replace (&demux->video_codec_data, NULL);
if (!gst_flv_demux_video_negotiate (demux, codec_tag)) {
ret = GST_FLOW_ERROR;
goto beach;
}
/* When we ve set pixel-aspect-ratio we use that boolean to detect a
* metadata tag that would come later and trigger a caps change */
demux->got_par = FALSE;
}
/* Check if we have anything to push */
if (demux->tag_data_size <= codec_data) {
GST_LOG_OBJECT (demux, "Nothing left in this tag, returning");
goto beach;
}
/* Create buffer from pad */
outbuf = gst_buffer_copy_region (buffer, GST_BUFFER_COPY_MEMORY,
7 + codec_data, demux->tag_data_size - codec_data);
/* detect (and deem to be resyncs) large dts gaps */
if (gst_flv_demux_update_resync (demux, dts, demux->video_need_discont,
&demux->last_video_dts, &demux->video_time_offset)) {
GST_BUFFER_FLAG_SET (outbuf, GST_BUFFER_FLAG_RESYNC);
}
/* Fill buffer with data */
GST_LOG_OBJECT (demux, "dts %u pts %u cts %d", dts, dts + cts, cts);
GST_BUFFER_PTS (outbuf) =
(dts + cts) * GST_MSECOND + demux->video_time_offset;
GST_BUFFER_DTS (outbuf) = dts * GST_MSECOND + demux->video_time_offset;
GST_BUFFER_DURATION (outbuf) = GST_CLOCK_TIME_NONE;
GST_BUFFER_OFFSET (outbuf) = demux->video_offset++;
GST_BUFFER_OFFSET_END (outbuf) = demux->video_offset;
if (demux->duration == GST_CLOCK_TIME_NONE ||
demux->duration < GST_BUFFER_TIMESTAMP (outbuf))
demux->duration = GST_BUFFER_TIMESTAMP (outbuf);
if (!keyframe)
GST_BUFFER_FLAG_SET (outbuf, GST_BUFFER_FLAG_DELTA_UNIT);
if (!demux->indexed) {
gst_flv_demux_parse_and_add_index_entry (demux,
GST_BUFFER_TIMESTAMP (outbuf), demux->cur_tag_offset, keyframe);
}
if (G_UNLIKELY (demux->video_need_discont)) {
GST_BUFFER_FLAG_SET (outbuf, GST_BUFFER_FLAG_DISCONT);
demux->video_need_discont = FALSE;
}
demux->segment.position = GST_BUFFER_TIMESTAMP (outbuf);
/* Do we need a newsegment event ? */
if (G_UNLIKELY (demux->video_need_segment)) {
if (!demux->new_seg_event) {
GST_DEBUG_OBJECT (demux, "pushing newsegment from %"
GST_TIME_FORMAT " to %" GST_TIME_FORMAT,
GST_TIME_ARGS (demux->segment.position),
GST_TIME_ARGS (demux->segment.stop));
demux->segment.start = demux->segment.time = demux->segment.position;
demux->new_seg_event = gst_event_new_segment (&demux->segment);
} else {
GST_DEBUG_OBJECT (demux, "pushing pre-generated newsegment event");
}
gst_pad_push_event (demux->video_pad, gst_event_ref (demux->new_seg_event));
demux->video_need_segment = FALSE;
}
GST_LOG_OBJECT (demux,
"pushing %" G_GSIZE_FORMAT " bytes buffer at dts %" GST_TIME_FORMAT
" with duration %" GST_TIME_FORMAT ", offset %" G_GUINT64_FORMAT
", keyframe (%d)", gst_buffer_get_size (outbuf),
GST_TIME_ARGS (GST_BUFFER_DTS (outbuf)),
GST_TIME_ARGS (GST_BUFFER_DURATION (outbuf)), GST_BUFFER_OFFSET (outbuf),
keyframe);
if (!GST_CLOCK_TIME_IS_VALID (demux->video_start)) {
demux->video_start = GST_BUFFER_TIMESTAMP (outbuf);
}
if (!GST_CLOCK_TIME_IS_VALID (demux->audio_first_ts)) {
demux->video_first_ts = GST_BUFFER_TIMESTAMP (outbuf);
}
if (G_UNLIKELY (!demux->no_more_pads
&& (GST_CLOCK_DIFF (demux->video_start,
GST_BUFFER_TIMESTAMP (outbuf)) > NO_MORE_PADS_THRESHOLD))) {
GST_DEBUG_OBJECT (demux,
"Signalling no-more-pads because no audio stream was found"
" after 6 seconds of video");
gst_element_no_more_pads (GST_ELEMENT_CAST (demux));
demux->no_more_pads = TRUE;
}
/* Push downstream */
ret = gst_pad_push (demux->video_pad, outbuf);
if (G_UNLIKELY (ret != GST_FLOW_OK) &&
demux->segment.rate < 0.0 && ret == GST_FLOW_EOS &&
demux->segment.position > demux->segment.stop) {
/* In reverse playback we can get a GST_FLOW_EOS when
* we are at the end of the segment, so we just need to jump
* back to the previous section. */
GST_DEBUG_OBJECT (demux, "downstream has reached end of segment");
demux->video_done = TRUE;
ret = GST_FLOW_OK;
goto beach;
}
ret = gst_flow_combiner_update_pad_flow (demux->flowcombiner,
demux->video_pad, ret);
beach:
gst_buffer_unmap (buffer, &map);
return ret;
}
static GstClockTime
gst_flv_demux_parse_tag_timestamp (GstFlvDemux * demux, gboolean index,
GstBuffer * buffer, size_t * tag_size)
{
guint32 dts = 0, dts_ext = 0;
guint32 tag_data_size;
guint8 type;
gboolean keyframe = TRUE;
GstClockTime ret = GST_CLOCK_TIME_NONE;
GstMapInfo map;
guint8 *data;
gsize size;
g_return_val_if_fail (gst_buffer_get_size (buffer) >= 12,
GST_CLOCK_TIME_NONE);
gst_buffer_map (buffer, &map, GST_MAP_READ);
data = map.data;
size = map.size;
type = data[0];
if (type != 9 && type != 8 && type != 18) {
GST_WARNING_OBJECT (demux, "Unsupported tag type %u", data[0]);
goto exit;
}
if (type == 9)
demux->has_video = TRUE;
else if (type == 8)
demux->has_audio = TRUE;
tag_data_size = GST_READ_UINT24_BE (data + 1);
if (size >= tag_data_size + 11 + 4) {
if (GST_READ_UINT32_BE (data + tag_data_size + 11) != tag_data_size + 11) {
GST_WARNING_OBJECT (demux, "Invalid tag size");
goto exit;
}
}
if (tag_size)
*tag_size = tag_data_size + 11 + 4;
data += 4;
GST_LOG_OBJECT (demux, "dts bytes %02X %02X %02X %02X", data[0], data[1],
data[2], data[3]);
/* Grab timestamp of tag tag */
dts = GST_READ_UINT24_BE (data);
/* read the dts extension to 32 bits integer */
dts_ext = GST_READ_UINT8 (data + 3);
/* Combine them */
dts |= dts_ext << 24;
if (type == 9) {
data += 7;
keyframe = ((data[0] >> 4) == 1);
}
ret = dts * GST_MSECOND;
GST_LOG_OBJECT (demux, "dts: %" GST_TIME_FORMAT, GST_TIME_ARGS (ret));
if (index && !demux->indexed && (type == 9 || (type == 8
&& !demux->has_video))) {
gst_flv_demux_parse_and_add_index_entry (demux, ret, demux->offset,
keyframe);
}
if (demux->duration == GST_CLOCK_TIME_NONE || demux->duration < ret)
demux->duration = ret;
exit:
gst_buffer_unmap (buffer, &map);
return ret;
}
static GstFlowReturn
gst_flv_demux_parse_tag_type (GstFlvDemux * demux, GstBuffer * buffer)
{
GstFlowReturn ret = GST_FLOW_OK;
guint8 tag_type = 0;
GstMapInfo map;
g_return_val_if_fail (gst_buffer_get_size (buffer) >= 4, GST_FLOW_ERROR);
gst_buffer_map (buffer, &map, GST_MAP_READ);
tag_type = map.data[0];
/* Tag size is 1 byte of type + 3 bytes of size + 7 bytes + tag data size +
* 4 bytes of previous tag size */
demux->tag_data_size = GST_READ_UINT24_BE (map.data + 1);
demux->tag_size = demux->tag_data_size + 11;
GST_LOG_OBJECT (demux, "tag data size is %" G_GUINT64_FORMAT,
demux->tag_data_size);
gst_buffer_unmap (buffer, &map);
switch (tag_type) {
case 9:
demux->state = FLV_STATE_TAG_VIDEO;
demux->has_video = TRUE;
break;
case 8:
demux->state = FLV_STATE_TAG_AUDIO;
demux->has_audio = TRUE;
break;
case 18:
demux->state = FLV_STATE_TAG_SCRIPT;
break;
default:
GST_WARNING_OBJECT (demux, "unsupported tag type %u", tag_type);
demux->state = FLV_STATE_SKIP;
}
return ret;
}
static GstFlowReturn
gst_flv_demux_parse_header (GstFlvDemux * demux, GstBuffer * buffer)
{
GstFlowReturn ret = GST_FLOW_OK;
GstMapInfo map;
g_return_val_if_fail (gst_buffer_get_size (buffer) >= 9, GST_FLOW_ERROR);
gst_buffer_map (buffer, &map, GST_MAP_READ);
/* Check for the FLV tag */
if (map.data[0] == 'F' && map.data[1] == 'L' && map.data[2] == 'V') {
GST_DEBUG_OBJECT (demux, "FLV header detected");
} else {
if (G_UNLIKELY (demux->strict)) {
GST_WARNING_OBJECT (demux, "invalid header tag detected");
ret = GST_FLOW_EOS;
goto beach;
}
}
if (map.data[3] == '1') {
GST_DEBUG_OBJECT (demux, "FLV version 1 detected");
} else {
if (G_UNLIKELY (demux->strict)) {
GST_WARNING_OBJECT (demux, "invalid header version detected");
ret = GST_FLOW_EOS;
goto beach;
}
}
/* Now look at audio/video flags */
{
guint8 flags = map.data[4];
demux->has_video = demux->has_audio = FALSE;
if (flags & 1) {
GST_DEBUG_OBJECT (demux, "there is a video stream");
demux->has_video = TRUE;
}
if (flags & 4) {
GST_DEBUG_OBJECT (demux, "there is an audio stream");
demux->has_audio = TRUE;
}
}
/* do a one-time seekability check */
gst_flv_demux_check_seekability (demux);
/* We don't care about the rest */
demux->need_header = FALSE;
beach:
gst_buffer_unmap (buffer, &map);
return ret;
}
static void
gst_flv_demux_flush (GstFlvDemux * demux, gboolean discont)
{
GST_DEBUG_OBJECT (demux, "flushing queued data in the FLV demuxer");
gst_adapter_clear (demux->adapter);
demux->audio_need_discont = TRUE;
demux->video_need_discont = TRUE;
demux->flushing = FALSE;
/* Only in push mode and if we're not during a seek */
if (!demux->random_access && demux->state != FLV_STATE_SEEK) {
/* After a flush we expect a tag_type */
demux->state = FLV_STATE_TAG_TYPE;
/* We reset the offset and will get one from first push */
demux->offset = 0;
}
}
static void
gst_flv_demux_cleanup (GstFlvDemux * demux)
{
GST_DEBUG_OBJECT (demux, "cleaning up FLV demuxer");
demux->state = FLV_STATE_HEADER;
demux->have_group_id = FALSE;
demux->group_id = G_MAXUINT;
demux->flushing = FALSE;
demux->need_header = TRUE;
demux->audio_need_segment = TRUE;
demux->video_need_segment = TRUE;
demux->audio_need_discont = TRUE;
demux->video_need_discont = TRUE;
demux->has_audio = FALSE;
demux->has_video = FALSE;
demux->got_par = FALSE;
demux->indexed = FALSE;
demux->upstream_seekable = FALSE;
demux->file_size = 0;
demux->index_max_pos = 0;
demux->index_max_time = 0;
demux->audio_start = demux->video_start = GST_CLOCK_TIME_NONE;
demux->last_audio_pts = demux->last_video_dts = 0;
demux->audio_time_offset = demux->video_time_offset = 0;
demux->no_more_pads = FALSE;
#ifndef GST_DISABLE_DEBUG
demux->no_audio_warned = FALSE;
demux->no_video_warned = FALSE;
#endif
gst_segment_init (&demux->segment, GST_FORMAT_TIME);
demux->w = demux->h = 0;
demux->framerate = 0.0;
demux->par_x = demux->par_y = 1;
demux->video_offset = 0;
demux->audio_offset = 0;
demux->offset = demux->cur_tag_offset = 0;
demux->tag_size = demux->tag_data_size = 0;
demux->duration = GST_CLOCK_TIME_NONE;
if (demux->new_seg_event) {
gst_event_unref (demux->new_seg_event);
demux->new_seg_event = NULL;
}
gst_adapter_clear (demux->adapter);
if (demux->audio_codec_data) {
gst_buffer_unref (demux->audio_codec_data);
demux->audio_codec_data = NULL;
}
if (demux->video_codec_data) {
gst_buffer_unref (demux->video_codec_data);
demux->video_codec_data = NULL;
}
if (demux->audio_pad) {
gst_flow_combiner_remove_pad (demux->flowcombiner, demux->audio_pad);
gst_element_remove_pad (GST_ELEMENT (demux), demux->audio_pad);
gst_object_unref (demux->audio_pad);
demux->audio_pad = NULL;
}
if (demux->video_pad) {
gst_flow_combiner_remove_pad (demux->flowcombiner, demux->video_pad);
gst_element_remove_pad (GST_ELEMENT (demux), demux->video_pad);
gst_object_unref (demux->video_pad);
demux->video_pad = NULL;
}
if (demux->times) {
g_array_free (demux->times, TRUE);
demux->times = NULL;
}
if (demux->filepositions) {
g_array_free (demux->filepositions, TRUE);
demux->filepositions = NULL;
}
gst_flv_demux_clear_tags (demux);
}
/*
* Create and push a flushing seek event upstream
*/
static gboolean
flv_demux_seek_to_offset (GstFlvDemux * demux, guint64 offset)
{
GstEvent *event;
gboolean res = 0;
GST_DEBUG_OBJECT (demux, "Seeking to %" G_GUINT64_FORMAT, offset);
event =
gst_event_new_seek (1.0, GST_FORMAT_BYTES,
GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE, GST_SEEK_TYPE_SET, offset,
GST_SEEK_TYPE_NONE, -1);
res = gst_pad_push_event (demux->sinkpad, event);
if (res)
demux->offset = offset;
return res;
}
static GstFlowReturn
gst_flv_demux_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer)
{
GstFlowReturn ret = GST_FLOW_OK;
GstFlvDemux *demux = NULL;
demux = GST_FLV_DEMUX (parent);
GST_LOG_OBJECT (demux,
"received buffer of %" G_GSIZE_FORMAT " bytes at offset %"
G_GUINT64_FORMAT, gst_buffer_get_size (buffer),
GST_BUFFER_OFFSET (buffer));
if (G_UNLIKELY (GST_BUFFER_OFFSET (buffer) == 0)) {
GST_DEBUG_OBJECT (demux, "beginning of file, expect header");
demux->state = FLV_STATE_HEADER;
demux->offset = 0;
}
if (G_UNLIKELY (demux->offset == 0 && GST_BUFFER_OFFSET (buffer) != 0)) {
GST_DEBUG_OBJECT (demux, "offset was zero, synchronizing with buffer's");
demux->offset = GST_BUFFER_OFFSET (buffer);
}
if (GST_BUFFER_FLAG_IS_SET (buffer, GST_BUFFER_FLAG_DISCONT)) {
GST_DEBUG_OBJECT (demux, "Discontinuity");
gst_adapter_clear (demux->adapter);
}
gst_adapter_push (demux->adapter, buffer);
if (demux->seeking) {
demux->state = FLV_STATE_SEEK;
GST_OBJECT_LOCK (demux);
demux->seeking = FALSE;
GST_OBJECT_UNLOCK (demux);
}
parse:
if (G_UNLIKELY (ret != GST_FLOW_OK)) {
GST_DEBUG_OBJECT (demux, "got flow return %s", gst_flow_get_name (ret));
goto beach;
}
if (G_UNLIKELY (demux->flushing)) {
GST_DEBUG_OBJECT (demux, "we are now flushing, exiting parser loop");
ret = GST_FLOW_FLUSHING;
goto beach;
}
switch (demux->state) {
case FLV_STATE_HEADER:
{
if (gst_adapter_available (demux->adapter) >= FLV_HEADER_SIZE) {
GstBuffer *buffer;
buffer = gst_adapter_take_buffer (demux->adapter, FLV_HEADER_SIZE);
ret = gst_flv_demux_parse_header (demux, buffer);
gst_buffer_unref (buffer);
demux->offset += FLV_HEADER_SIZE;
demux->state = FLV_STATE_TAG_TYPE;
goto parse;
} else {
goto beach;
}
}
case FLV_STATE_TAG_TYPE:
{
if (gst_adapter_available (demux->adapter) >= FLV_TAG_TYPE_SIZE) {
GstBuffer *buffer;
/* Remember the tag offset in bytes */
demux->cur_tag_offset = demux->offset;
buffer = gst_adapter_take_buffer (demux->adapter, FLV_TAG_TYPE_SIZE);
ret = gst_flv_demux_parse_tag_type (demux, buffer);
gst_buffer_unref (buffer);
demux->offset += FLV_TAG_TYPE_SIZE;
/* last tag is not an index => no index/don't know where the index is
* seek back to the beginning */
if (demux->seek_event && demux->state != FLV_STATE_TAG_SCRIPT)
goto no_index;
goto parse;
} else {
goto beach;
}
}
case FLV_STATE_TAG_VIDEO:
{
if (gst_adapter_available (demux->adapter) >= demux->tag_size) {
GstBuffer *buffer;
buffer = gst_adapter_take_buffer (demux->adapter, demux->tag_size);
ret = gst_flv_demux_parse_tag_video (demux, buffer);
gst_buffer_unref (buffer);
demux->offset += demux->tag_size;
demux->state = FLV_STATE_TAG_TYPE;
goto parse;
} else {
goto beach;
}
}
case FLV_STATE_TAG_AUDIO:
{
if (gst_adapter_available (demux->adapter) >= demux->tag_size) {
GstBuffer *buffer;
buffer = gst_adapter_take_buffer (demux->adapter, demux->tag_size);
ret = gst_flv_demux_parse_tag_audio (demux, buffer);
gst_buffer_unref (buffer);
demux->offset += demux->tag_size;
demux->state = FLV_STATE_TAG_TYPE;
goto parse;
} else {
goto beach;
}
}
case FLV_STATE_TAG_SCRIPT:
{
if (gst_adapter_available (demux->adapter) >= demux->tag_size) {
GstBuffer *buffer;
buffer = gst_adapter_take_buffer (demux->adapter, demux->tag_size);
ret = gst_flv_demux_parse_tag_script (demux, buffer);
gst_buffer_unref (buffer);
demux->offset += demux->tag_size;
demux->state = FLV_STATE_TAG_TYPE;
/* if there's a seek event we're here for the index so if we don't have it
* we seek back to the beginning */
if (demux->seek_event) {
if (demux->indexed)
demux->state = FLV_STATE_SEEK;
else
goto no_index;
}
goto parse;
} else {
goto beach;
}
}
case FLV_STATE_SEEK:
{
GstEvent *event;
ret = GST_FLOW_OK;
if (!demux->indexed) {
if (demux->offset == demux->file_size - sizeof (guint32)) {
guint64 seek_offset;
guint8 *data;
data = gst_adapter_take (demux->adapter, 4);
if (!data)
goto no_index;
seek_offset = demux->file_size - sizeof (guint32) -
GST_READ_UINT32_BE (data);
g_free (data);
GST_INFO_OBJECT (demux,
"Seeking to beginning of last tag at %" G_GUINT64_FORMAT,
seek_offset);
demux->state = FLV_STATE_TAG_TYPE;
flv_demux_seek_to_offset (demux, seek_offset);
goto beach;
} else
goto no_index;
}
GST_OBJECT_LOCK (demux);
event = demux->seek_event;
demux->seek_event = NULL;
GST_OBJECT_UNLOCK (demux);
/* calculate and perform seek */
if (!flv_demux_handle_seek_push (demux, event))
goto seek_failed;
gst_event_unref (event);
demux->state = FLV_STATE_TAG_TYPE;
goto beach;
}
case FLV_STATE_SKIP:
/* Skip unknown tags (set in _parse_tag_type()) */
if (gst_adapter_available (demux->adapter) >= demux->tag_size) {
gst_adapter_flush (demux->adapter, demux->tag_size);
demux->offset += demux->tag_size;
demux->state = FLV_STATE_TAG_TYPE;
goto parse;
} else {
goto beach;
}
default:
GST_DEBUG_OBJECT (demux, "unexpected demuxer state");
}
beach:
return ret;
/* ERRORS */
no_index:
{
GST_OBJECT_LOCK (demux);
demux->seeking = FALSE;
gst_event_unref (demux->seek_event);
demux->seek_event = NULL;
GST_OBJECT_UNLOCK (demux);
GST_WARNING_OBJECT (demux,
"failed to find an index, seeking back to beginning");
flv_demux_seek_to_offset (demux, 0);
return GST_FLOW_OK;
}
seek_failed:
{
GST_ELEMENT_ERROR (demux, STREAM, DEMUX, (NULL), ("seek failed"));
return GST_FLOW_ERROR;
}
}
static GstFlowReturn
gst_flv_demux_pull_range (GstFlvDemux * demux, GstPad * pad, guint64 offset,
guint size, GstBuffer ** buffer)
{
GstFlowReturn ret;
ret = gst_pad_pull_range (pad, offset, size, buffer);
if (G_UNLIKELY (ret != GST_FLOW_OK)) {
GST_WARNING_OBJECT (demux,
"failed when pulling %d bytes from offset %" G_GUINT64_FORMAT ": %s",
size, offset, gst_flow_get_name (ret));
*buffer = NULL;
return ret;
}
if (G_UNLIKELY (*buffer && gst_buffer_get_size (*buffer) != size)) {
GST_WARNING_OBJECT (demux,
"partial pull got %" G_GSIZE_FORMAT " when expecting %d from offset %"
G_GUINT64_FORMAT, gst_buffer_get_size (*buffer), size, offset);
gst_buffer_unref (*buffer);
ret = GST_FLOW_EOS;
*buffer = NULL;
return ret;
}
return ret;
}
static GstFlowReturn
gst_flv_demux_pull_tag (GstPad * pad, GstFlvDemux * demux)
{
GstBuffer *buffer = NULL;
GstFlowReturn ret = GST_FLOW_OK;
/* Store tag offset */
demux->cur_tag_offset = demux->offset;
/* Get the first 4 bytes to identify tag type and size */
if (G_UNLIKELY ((ret = gst_flv_demux_pull_range (demux, pad, demux->offset,
FLV_TAG_TYPE_SIZE, &buffer)) != GST_FLOW_OK))
goto beach;
/* Identify tag type */
ret = gst_flv_demux_parse_tag_type (demux, buffer);
gst_buffer_unref (buffer);
if (G_UNLIKELY (ret != GST_FLOW_OK))
goto beach;
/* Jump over tag type + size */
demux->offset += FLV_TAG_TYPE_SIZE;
/* Pull the whole tag */
buffer = NULL;
if (G_UNLIKELY ((ret = gst_flv_demux_pull_range (demux, pad, demux->offset,
demux->tag_size, &buffer)) != GST_FLOW_OK))
goto beach;
switch (demux->state) {
case FLV_STATE_TAG_VIDEO:
ret = gst_flv_demux_parse_tag_video (demux, buffer);
break;
case FLV_STATE_TAG_AUDIO:
ret = gst_flv_demux_parse_tag_audio (demux, buffer);
break;
case FLV_STATE_TAG_SCRIPT:
ret = gst_flv_demux_parse_tag_script (demux, buffer);
break;
default:
GST_WARNING_OBJECT (demux, "unexpected state %d", demux->state);
}
gst_buffer_unref (buffer);
/* Jump over that part we've just parsed */
demux->offset += demux->tag_size;
/* Make sure we reinitialize the tag size */
demux->tag_size = 0;
/* Ready for the next tag */
demux->state = FLV_STATE_TAG_TYPE;
if (G_UNLIKELY (ret == GST_FLOW_NOT_LINKED)) {
GST_WARNING_OBJECT (demux, "parsing this tag returned not-linked and "
"neither video nor audio are linked");
}
beach:
return ret;
}
static GstFlowReturn
gst_flv_demux_pull_header (GstPad * pad, GstFlvDemux * demux)
{
GstBuffer *buffer = NULL;
GstFlowReturn ret = GST_FLOW_OK;
/* Get the first 9 bytes */
if (G_UNLIKELY ((ret = gst_flv_demux_pull_range (demux, pad, demux->offset,
FLV_HEADER_SIZE, &buffer)) != GST_FLOW_OK))
goto beach;
ret = gst_flv_demux_parse_header (demux, buffer);
gst_buffer_unref (buffer);
/* Jump over the header now */
demux->offset += FLV_HEADER_SIZE;
demux->state = FLV_STATE_TAG_TYPE;
beach:
return ret;
}
static void
gst_flv_demux_move_to_offset (GstFlvDemux * demux, gint64 offset,
gboolean reset)
{
demux->offset = offset;
/* Tell all the stream we moved to a different position (discont) */
demux->audio_need_discont = TRUE;
demux->video_need_discont = TRUE;
/* next section setup */
demux->from_offset = -1;
demux->audio_done = demux->video_done = FALSE;
demux->audio_first_ts = demux->video_first_ts = GST_CLOCK_TIME_NONE;
if (reset) {
demux->from_offset = -1;
demux->to_offset = G_MAXINT64;
}
/* If we seeked at the beginning of the file parse the header again */
if (G_UNLIKELY (!demux->offset)) {
demux->state = FLV_STATE_HEADER;
} else { /* or parse a tag */
demux->state = FLV_STATE_TAG_TYPE;
}
}
static GstFlowReturn
gst_flv_demux_seek_to_prev_keyframe (GstFlvDemux * demux)
{
GstFlowReturn ret = GST_FLOW_EOS;
GstIndex *index;
GstIndexEntry *entry = NULL;
GST_DEBUG_OBJECT (demux,
"terminated section started at offset %" G_GINT64_FORMAT,
demux->from_offset);
/* we are done if we got all audio and video */
if ((!GST_CLOCK_TIME_IS_VALID (demux->audio_first_ts) ||
demux->audio_first_ts < demux->segment.start) &&
(!GST_CLOCK_TIME_IS_VALID (demux->video_first_ts) ||
demux->video_first_ts < demux->segment.start))
goto done;
if (demux->from_offset <= 0)
goto done;
GST_DEBUG_OBJECT (demux, "locating previous position");
index = gst_flv_demux_get_index (GST_ELEMENT (demux));
/* locate index entry before previous start position */
if (index) {
entry = gst_index_get_assoc_entry (index, demux->index_id,
GST_INDEX_LOOKUP_BEFORE, GST_ASSOCIATION_FLAG_KEY_UNIT,
GST_FORMAT_BYTES, demux->from_offset - 1);
if (entry) {
gint64 bytes = 0, time = 0;
gst_index_entry_assoc_map (entry, GST_FORMAT_BYTES, &bytes);
gst_index_entry_assoc_map (entry, GST_FORMAT_TIME, &time);
GST_DEBUG_OBJECT (demux, "found index entry for %" G_GINT64_FORMAT
" at %" GST_TIME_FORMAT ", seeking to %" G_GINT64_FORMAT,
demux->offset - 1, GST_TIME_ARGS (time), bytes);
/* setup for next section */
demux->to_offset = demux->from_offset;
gst_flv_demux_move_to_offset (demux, bytes, FALSE);
ret = GST_FLOW_OK;
}
gst_object_unref (index);
}
done:
return ret;
}
static GstFlowReturn
gst_flv_demux_create_index (GstFlvDemux * demux, gint64 pos, GstClockTime ts)
{
gint64 size;
size_t tag_size;
guint64 old_offset;
GstBuffer *buffer;
GstClockTime tag_time;
GstFlowReturn ret = GST_FLOW_OK;
if (!gst_pad_peer_query_duration (demux->sinkpad, GST_FORMAT_BYTES, &size))
return GST_FLOW_OK;
GST_DEBUG_OBJECT (demux, "building index at %" G_GINT64_FORMAT
" looking for time %" GST_TIME_FORMAT, pos, GST_TIME_ARGS (ts));
old_offset = demux->offset;
demux->offset = pos;
buffer = NULL;
while ((ret = gst_flv_demux_pull_range (demux, demux->sinkpad, demux->offset,
12, &buffer)) == GST_FLOW_OK) {
tag_time =
gst_flv_demux_parse_tag_timestamp (demux, TRUE, buffer, &tag_size);
gst_buffer_unref (buffer);
buffer = NULL;
if (G_UNLIKELY (tag_time == GST_CLOCK_TIME_NONE || tag_time > ts))
goto exit;
demux->offset += tag_size;
}
if (ret == GST_FLOW_EOS) {
/* file ran out, so mark we have complete index */
demux->indexed = TRUE;
ret = GST_FLOW_OK;
}
exit:
demux->offset = old_offset;
return ret;
}
static gint64
gst_flv_demux_get_metadata (GstFlvDemux * demux)
{
gint64 ret = 0, offset;
size_t tag_size, size;
GstBuffer *buffer = NULL;
GstMapInfo map;
if (!gst_pad_peer_query_duration (demux->sinkpad, GST_FORMAT_BYTES, &offset))
goto exit;
ret = offset;
GST_DEBUG_OBJECT (demux, "upstream size: %" G_GINT64_FORMAT, offset);
if (G_UNLIKELY (offset < 4))
goto exit;
offset -= 4;
if (GST_FLOW_OK != gst_flv_demux_pull_range (demux, demux->sinkpad, offset,
4, &buffer))
goto exit;
gst_buffer_map (buffer, &map, GST_MAP_READ);
tag_size = GST_READ_UINT32_BE (map.data);
gst_buffer_unmap (buffer, &map);
GST_DEBUG_OBJECT (demux, "last tag size: %" G_GSIZE_FORMAT, tag_size);
gst_buffer_unref (buffer);
buffer = NULL;
if (G_UNLIKELY (offset < tag_size))
goto exit;
offset -= tag_size;
if (GST_FLOW_OK != gst_flv_demux_pull_range (demux, demux->sinkpad, offset,
12, &buffer))
goto exit;
/* a consistency check */
gst_buffer_map (buffer, &map, GST_MAP_READ);
size = GST_READ_UINT24_BE (map.data + 1);
if (size != tag_size - 11) {
gst_buffer_unmap (buffer, &map);
GST_DEBUG_OBJECT (demux,
"tag size %" G_GSIZE_FORMAT ", expected %" G_GSIZE_FORMAT
", corrupt or truncated file", size, tag_size - 11);
goto exit;
}
/* try to update duration with timestamp in any case */
gst_flv_demux_parse_tag_timestamp (demux, FALSE, buffer, &size);
/* maybe get some more metadata */
if (map.data[0] == 18) {
gst_buffer_unmap (buffer, &map);
gst_buffer_unref (buffer);
buffer = NULL;
GST_DEBUG_OBJECT (demux, "script tag, pulling it to parse");
offset += 4;
if (GST_FLOW_OK == gst_flv_demux_pull_range (demux, demux->sinkpad, offset,
tag_size, &buffer))
gst_flv_demux_parse_tag_script (demux, buffer);
} else {
gst_buffer_unmap (buffer, &map);
}
exit:
if (buffer)
gst_buffer_unref (buffer);
return ret;
}
static void
gst_flv_demux_loop (GstPad * pad)
{
GstFlvDemux *demux = NULL;
GstFlowReturn ret = GST_FLOW_OK;
demux = GST_FLV_DEMUX (gst_pad_get_parent (pad));
/* pull in data */
switch (demux->state) {
case FLV_STATE_TAG_TYPE:
if (demux->from_offset == -1)
demux->from_offset = demux->offset;
ret = gst_flv_demux_pull_tag (pad, demux);
/* if we have seen real data, we probably passed a possible metadata
* header located at start. So if we do not yet have an index,
* try to pick up metadata (index, duration) at the end */
if (G_UNLIKELY (!demux->file_size && !demux->indexed &&
(demux->has_video || demux->has_audio)))
demux->file_size = gst_flv_demux_get_metadata (demux);
break;
case FLV_STATE_DONE:
ret = GST_FLOW_EOS;
break;
case FLV_STATE_SEEK:
/* seek issued with insufficient index;
* scan for index in task thread from current maximum offset to
* desired time and then perform seek */
/* TODO maybe some buffering message or so to indicate scan progress */
ret = gst_flv_demux_create_index (demux, demux->index_max_pos,
demux->seek_time);
if (ret != GST_FLOW_OK)
goto pause;
/* position and state arranged by seek,
* also unrefs event */
gst_flv_demux_handle_seek_pull (demux, demux->seek_event, FALSE);
demux->seek_event = NULL;
break;
default:
ret = gst_flv_demux_pull_header (pad, demux);
/* index scans start after header */
demux->index_max_pos = demux->offset;
break;
}
if (demux->segment.rate < 0.0) {
/* check end of section */
if ((gint64) demux->offset >= demux->to_offset ||
demux->segment.position >= demux->segment.stop + 2 * GST_SECOND ||
(demux->audio_done && demux->video_done))
ret = gst_flv_demux_seek_to_prev_keyframe (demux);
} else {
/* check EOS condition */
if ((demux->segment.stop != -1) &&
(demux->segment.position >= demux->segment.stop)) {
ret = GST_FLOW_EOS;
}
}
/* pause if something went wrong or at end */
if (G_UNLIKELY (ret != GST_FLOW_OK))
goto pause;
gst_object_unref (demux);
return;
pause:
{
const gchar *reason = gst_flow_get_name (ret);
GST_LOG_OBJECT (demux, "pausing task, reason %s", reason);
gst_pad_pause_task (pad);
if (ret == GST_FLOW_EOS) {
/* handle end-of-stream/segment */
/* so align our position with the end of it, if there is one
* this ensures a subsequent will arrive at correct base/acc time */
if (demux->segment.rate > 0.0 &&
GST_CLOCK_TIME_IS_VALID (demux->segment.stop))
demux->segment.position = demux->segment.stop;
else if (demux->segment.rate < 0.0)
demux->segment.position = demux->segment.start;
/* perform EOS logic */
if (!demux->no_more_pads) {
gst_element_no_more_pads (GST_ELEMENT_CAST (demux));
demux->no_more_pads = TRUE;
}
if (demux->segment.flags & GST_SEGMENT_FLAG_SEGMENT) {
gint64 stop;
/* for segment playback we need to post when (in stream time)
* we stopped, this is either stop (when set) or the duration. */
if ((stop = demux->segment.stop) == -1)
stop = demux->segment.duration;
if (demux->segment.rate >= 0) {
GST_LOG_OBJECT (demux, "Sending segment done, at end of segment");
gst_element_post_message (GST_ELEMENT_CAST (demux),
gst_message_new_segment_done (GST_OBJECT_CAST (demux),
GST_FORMAT_TIME, stop));
gst_flv_demux_push_src_event (demux,
gst_event_new_segment_done (GST_FORMAT_TIME, stop));
} else { /* Reverse playback */
GST_LOG_OBJECT (demux, "Sending segment done, at beginning of "
"segment");
gst_element_post_message (GST_ELEMENT_CAST (demux),
gst_message_new_segment_done (GST_OBJECT_CAST (demux),
GST_FORMAT_TIME, demux->segment.start));
gst_flv_demux_push_src_event (demux,
gst_event_new_segment_done (GST_FORMAT_TIME,
demux->segment.start));
}
} else {
/* normal playback, send EOS to all linked pads */
if (!demux->no_more_pads) {
gst_element_no_more_pads (GST_ELEMENT (demux));
demux->no_more_pads = TRUE;
}
GST_LOG_OBJECT (demux, "Sending EOS, at end of stream");
if (!demux->audio_pad && !demux->video_pad)
GST_ELEMENT_ERROR (demux, STREAM, FAILED,
("Internal data stream error."), ("Got EOS before any data"));