| /* 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")); |
| else if (!gst_flv_demux_push_src_event (demux, gst_event_new_eos ())) |
| GST_WARNING_OBJECT (demux, "failed pushing EOS on streams"); |
| } |
| } else if (ret == GST_FLOW_NOT_LINKED || ret < GST_FLOW_EOS) { |
| GST_ELEMENT_FLOW_ERROR (demux, ret); |
| gst_flv_demux_push_src_event (demux, gst_event_new_eos ()); |
| } |
| gst_object_unref (demux); |
| return; |
| } |
| } |
| |
| static guint64 |
| gst_flv_demux_find_offset (GstFlvDemux * demux, GstSegment * segment, |
| GstSeekFlags seek_flags) |
| { |
| gint64 bytes = 0; |
| gint64 time = 0; |
| GstIndex *index; |
| GstIndexEntry *entry; |
| |
| g_return_val_if_fail (segment != NULL, 0); |
| |
| time = segment->position; |
| |
| index = gst_flv_demux_get_index (GST_ELEMENT (demux)); |
| |
| if (index) { |
| /* Let's check if we have an index entry for that seek time */ |
| entry = gst_index_get_assoc_entry (index, demux->index_id, |
| seek_flags & GST_SEEK_FLAG_SNAP_AFTER ? |
| GST_INDEX_LOOKUP_AFTER : GST_INDEX_LOOKUP_BEFORE, |
| GST_ASSOCIATION_FLAG_KEY_UNIT, GST_FORMAT_TIME, time); |
| |
| if (entry) { |
| 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 %" GST_TIME_FORMAT |
| " at %" GST_TIME_FORMAT ", seeking to %" G_GINT64_FORMAT, |
| GST_TIME_ARGS (segment->position), GST_TIME_ARGS (time), bytes); |
| |
| /* Key frame seeking */ |
| if (seek_flags & GST_SEEK_FLAG_KEY_UNIT) { |
| /* Adjust the segment so that the keyframe fits in */ |
| segment->start = segment->time = time; |
| segment->position = time; |
| } |
| } else { |
| GST_DEBUG_OBJECT (demux, "no index entry found for %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (segment->start)); |
| } |
| |
| gst_object_unref (index); |
| } |
| |
| return bytes; |
| } |
| |
| static gboolean |
| flv_demux_handle_seek_push (GstFlvDemux * demux, GstEvent * event) |
| { |
| GstFormat format; |
| GstSeekFlags flags; |
| GstSeekType start_type, stop_type; |
| gint64 start, stop; |
| gdouble rate; |
| gboolean update, flush, ret; |
| GstSegment seeksegment; |
| |
| gst_event_parse_seek (event, &rate, &format, &flags, |
| &start_type, &start, &stop_type, &stop); |
| |
| if (format != GST_FORMAT_TIME) |
| goto wrong_format; |
| |
| flush = ! !(flags & GST_SEEK_FLAG_FLUSH); |
| |
| /* Work on a copy until we are sure the seek succeeded. */ |
| memcpy (&seeksegment, &demux->segment, sizeof (GstSegment)); |
| |
| GST_DEBUG_OBJECT (demux, "segment before configure %" GST_SEGMENT_FORMAT, |
| &demux->segment); |
| |
| /* Apply the seek to our segment */ |
| gst_segment_do_seek (&seeksegment, rate, format, flags, |
| start_type, start, stop_type, stop, &update); |
| |
| GST_DEBUG_OBJECT (demux, "segment configured %" GST_SEGMENT_FORMAT, |
| &seeksegment); |
| |
| if (flush || seeksegment.position != demux->segment.position) { |
| /* Do the actual seeking */ |
| guint64 offset = gst_flv_demux_find_offset (demux, &seeksegment, flags); |
| |
| GST_DEBUG_OBJECT (demux, "generating an upstream seek at position %" |
| G_GUINT64_FORMAT, offset); |
| ret = gst_pad_push_event (demux->sinkpad, |
| gst_event_new_seek (seeksegment.rate, GST_FORMAT_BYTES, |
| flags | GST_SEEK_FLAG_ACCURATE, GST_SEEK_TYPE_SET, |
| offset, GST_SEEK_TYPE_NONE, 0)); |
| if (G_UNLIKELY (!ret)) { |
| GST_WARNING_OBJECT (demux, "upstream seek failed"); |
| } |
| |
| gst_flow_combiner_reset (demux->flowcombiner); |
| /* Tell all the stream we moved to a different position (discont) */ |
| demux->audio_need_discont = TRUE; |
| demux->video_need_discont = TRUE; |
| } else { |
| ret = TRUE; |
| } |
| |
| if (ret) { |
| /* Ok seek succeeded, take the newly configured segment */ |
| memcpy (&demux->segment, &seeksegment, sizeof (GstSegment)); |
| |
| /* Tell all the stream a new segment is needed */ |
| demux->audio_need_segment = TRUE; |
| demux->video_need_segment = TRUE; |
| /* Clean any potential newsegment event kept for the streams. The first |
| * stream needing a new segment will create a new one. */ |
| if (G_UNLIKELY (demux->new_seg_event)) { |
| gst_event_unref (demux->new_seg_event); |
| demux->new_seg_event = NULL; |
| } |
| GST_DEBUG_OBJECT (demux, "preparing newsegment from %" |
| GST_TIME_FORMAT " to %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (demux->segment.start), |
| GST_TIME_ARGS (demux->segment.stop)); |
| demux->new_seg_event = gst_event_new_segment (&demux->segment); |
| gst_event_unref (event); |
| } else { |
| ret = gst_pad_push_event (demux->sinkpad, event); |
| } |
| |
| return ret; |
| |
| /* ERRORS */ |
| wrong_format: |
| { |
| GST_WARNING_OBJECT (demux, "we only support seeking in TIME format"); |
| gst_event_unref (event); |
| return FALSE; |
| } |
| } |
| |
| static gboolean |
| gst_flv_demux_handle_seek_push (GstFlvDemux * demux, GstEvent * event) |
| { |
| GstFormat format; |
| |
| gst_event_parse_seek (event, NULL, &format, NULL, NULL, NULL, NULL, NULL); |
| |
| if (format != GST_FORMAT_TIME) { |
| GST_WARNING_OBJECT (demux, "we only support seeking in TIME format"); |
| gst_event_unref (event); |
| return FALSE; |
| } |
| |
| /* First try upstream */ |
| if (gst_pad_push_event (demux->sinkpad, gst_event_ref (event))) { |
| GST_DEBUG_OBJECT (demux, "Upstream successfully seeked"); |
| gst_event_unref (event); |
| return TRUE; |
| } |
| |
| if (!demux->indexed) { |
| guint64 seek_offset = 0; |
| gboolean building_index; |
| |
| GST_OBJECT_LOCK (demux); |
| /* handle the seek in the chain function */ |
| demux->seeking = TRUE; |
| demux->state = FLV_STATE_SEEK; |
| |
| /* copy the event */ |
| if (demux->seek_event) |
| gst_event_unref (demux->seek_event); |
| demux->seek_event = gst_event_ref (event); |
| |
| /* set the building_index flag so that only one thread can setup the |
| * structures for index seeking. */ |
| building_index = demux->building_index; |
| if (!building_index) { |
| demux->building_index = TRUE; |
| if (!demux->file_size |
| && !gst_pad_peer_query_duration (demux->sinkpad, GST_FORMAT_BYTES, |
| &demux->file_size)) { |
| GST_WARNING_OBJECT (demux, "Failed to query upstream file size"); |
| GST_OBJECT_UNLOCK (demux); |
| return FALSE; |
| } |
| |
| /* we hope the last tag is a scriptdataobject containing an index |
| * the size of the last tag is given in the last guint32 bits |
| * then we seek to the beginning of the tag, parse it and hopefully obtain an index */ |
| seek_offset = demux->file_size - sizeof (guint32); |
| GST_DEBUG_OBJECT (demux, |
| "File size obtained, seeking to %" G_GUINT64_FORMAT, seek_offset); |
| } |
| GST_OBJECT_UNLOCK (demux); |
| |
| if (!building_index) { |
| GST_INFO_OBJECT (demux, "Seeking to last 4 bytes at %" G_GUINT64_FORMAT, |
| seek_offset); |
| return flv_demux_seek_to_offset (demux, seek_offset); |
| } |
| |
| /* FIXME: we have to always return true so that we don't block the seek |
| * thread. |
| * Note: maybe it is OK to return true if we're still building the index */ |
| return TRUE; |
| } |
| |
| return flv_demux_handle_seek_push (demux, event); |
| } |
| |
| static gboolean |
| gst_flv_demux_handle_seek_pull (GstFlvDemux * demux, GstEvent * event, |
| gboolean seeking) |
| { |
| GstFormat format; |
| GstSeekFlags flags; |
| GstSeekType start_type, stop_type; |
| gint64 start, stop; |
| gdouble rate; |
| gboolean update, flush, ret = FALSE; |
| GstSegment seeksegment; |
| |
| gst_event_parse_seek (event, &rate, &format, &flags, |
| &start_type, &start, &stop_type, &stop); |
| |
| if (format != GST_FORMAT_TIME) |
| goto wrong_format; |
| |
| /* mark seeking thread entering flushing/pausing */ |
| GST_OBJECT_LOCK (demux); |
| if (seeking) |
| demux->seeking = seeking; |
| GST_OBJECT_UNLOCK (demux); |
| |
| flush = ! !(flags & GST_SEEK_FLAG_FLUSH); |
| |
| if (flush) { |
| /* Flush start up and downstream to make sure data flow and loops are |
| idle */ |
| gst_flv_demux_push_src_event (demux, gst_event_new_flush_start ()); |
| gst_pad_push_event (demux->sinkpad, gst_event_new_flush_start ()); |
| } else { |
| /* Pause the pulling task */ |
| gst_pad_pause_task (demux->sinkpad); |
| } |
| |
| /* Take the stream lock */ |
| GST_PAD_STREAM_LOCK (demux->sinkpad); |
| |
| if (flush) { |
| /* Stop flushing upstream we need to pull */ |
| gst_pad_push_event (demux->sinkpad, gst_event_new_flush_stop (TRUE)); |
| } |
| |
| /* Work on a copy until we are sure the seek succeeded. */ |
| memcpy (&seeksegment, &demux->segment, sizeof (GstSegment)); |
| |
| GST_DEBUG_OBJECT (demux, "segment before configure %" GST_SEGMENT_FORMAT, |
| &demux->segment); |
| |
| /* Apply the seek to our segment */ |
| gst_segment_do_seek (&seeksegment, rate, format, flags, |
| start_type, start, stop_type, stop, &update); |
| |
| GST_DEBUG_OBJECT (demux, "segment configured %" GST_SEGMENT_FORMAT, |
| &seeksegment); |
| |
| if (flush || seeksegment.position != demux->segment.position) { |
| /* Do the actual seeking */ |
| /* index is reliable if it is complete or we do not go to far ahead */ |
| if (seeking && !demux->indexed && |
| seeksegment.position > demux->index_max_time + 10 * GST_SECOND) { |
| GST_DEBUG_OBJECT (demux, "delaying seek to post-scan; " |
| " index only up to %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (demux->index_max_time)); |
| /* stop flushing for now */ |
| if (flush) |
| gst_flv_demux_push_src_event (demux, gst_event_new_flush_stop (TRUE)); |
| /* delegate scanning and index building to task thread to avoid |
| * occupying main (UI) loop */ |
| if (demux->seek_event) |
| gst_event_unref (demux->seek_event); |
| demux->seek_event = gst_event_ref (event); |
| demux->seek_time = seeksegment.position; |
| demux->state = FLV_STATE_SEEK; |
| /* do not know about succes yet, but we did care and handled it */ |
| ret = TRUE; |
| goto exit; |
| } |
| |
| /* now index should be as reliable as it can be for current purpose */ |
| gst_flv_demux_move_to_offset (demux, |
| gst_flv_demux_find_offset (demux, &seeksegment, flags), TRUE); |
| ret = TRUE; |
| } else { |
| ret = TRUE; |
| } |
| |
| if (flush) { |
| /* Stop flushing, the sinks are at time 0 now */ |
| gst_flv_demux_push_src_event (demux, gst_event_new_flush_stop (TRUE)); |
| } |
|