| /* GStreamer |
| * Copyright (C) 2004 Wim Taymans <wim@fluendo.com> |
| * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net> |
| * Copyright (C) 2008 Sebastian Dröge <sebastian.droege@collabora.co.uk> |
| * Copyright (C) <2011-2012> Vincent Penquerc'h <vincent.penquerch@collabora.co.uk> |
| * |
| * 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-opusparse |
| * @see_also: opusenc, opusdec |
| * |
| * This element parses OPUS packets. |
| * |
| * <refsect2> |
| * <title>Example pipelines</title> |
| * |[ |
| * gst-launch-1.0 -v filesrc location=opusdata ! opusparse ! opusdec ! audioconvert ! audioresample ! alsasink |
| * ]| Decode and plays an unmuxed Opus file. |
| * </refsect2> |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| # include "config.h" |
| #endif |
| |
| #include <string.h> |
| #include <opus.h> |
| #include "gstopusheader.h" |
| #include "gstopusparse.h" |
| |
| #include <gst/audio/audio.h> |
| #include <gst/pbutils/pbutils.h> |
| |
| GST_DEBUG_CATEGORY_STATIC (opusparse_debug); |
| #define GST_CAT_DEFAULT opusparse_debug |
| |
| #define MAX_PAYLOAD_BYTES 1500 |
| |
| static GstStaticPadTemplate opus_parse_src_factory = |
| GST_STATIC_PAD_TEMPLATE ("src", |
| GST_PAD_SRC, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS ("audio/x-opus, framed = (boolean) true") |
| ); |
| |
| static GstStaticPadTemplate opus_parse_sink_factory = |
| GST_STATIC_PAD_TEMPLATE ("sink", |
| GST_PAD_SINK, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS ("audio/x-opus") |
| ); |
| |
| G_DEFINE_TYPE (GstOpusParse, gst_opus_parse, GST_TYPE_BASE_PARSE); |
| |
| static gboolean gst_opus_parse_start (GstBaseParse * parse); |
| static gboolean gst_opus_parse_stop (GstBaseParse * parse); |
| static GstFlowReturn gst_opus_parse_handle_frame (GstBaseParse * base, |
| GstBaseParseFrame * frame, gint * skip); |
| static GstFlowReturn gst_opus_parse_parse_frame (GstBaseParse * base, |
| GstBaseParseFrame * frame); |
| |
| static void |
| gst_opus_parse_class_init (GstOpusParseClass * klass) |
| { |
| GstBaseParseClass *bpclass; |
| GstElementClass *element_class; |
| |
| bpclass = (GstBaseParseClass *) klass; |
| element_class = (GstElementClass *) klass; |
| |
| bpclass->start = GST_DEBUG_FUNCPTR (gst_opus_parse_start); |
| bpclass->stop = GST_DEBUG_FUNCPTR (gst_opus_parse_stop); |
| bpclass->handle_frame = GST_DEBUG_FUNCPTR (gst_opus_parse_handle_frame); |
| |
| gst_element_class_add_static_pad_template (element_class, |
| &opus_parse_src_factory); |
| gst_element_class_add_static_pad_template (element_class, |
| &opus_parse_sink_factory); |
| gst_element_class_set_static_metadata (element_class, "Opus audio parser", |
| "Codec/Parser/Audio", "parses opus audio streams", |
| "Vincent Penquerc'h <vincent.penquerch@collabora.co.uk>"); |
| |
| GST_DEBUG_CATEGORY_INIT (opusparse_debug, "opusparse", 0, |
| "opus parsing element"); |
| } |
| |
| static void |
| gst_opus_parse_init (GstOpusParse * parse) |
| { |
| parse->header_sent = FALSE; |
| parse->got_headers = FALSE; |
| parse->pre_skip = 0; |
| } |
| |
| static gboolean |
| gst_opus_parse_start (GstBaseParse * base) |
| { |
| GstOpusParse *parse = GST_OPUS_PARSE (base); |
| |
| parse->header_sent = FALSE; |
| parse->got_headers = FALSE; |
| parse->pre_skip = 0; |
| parse->next_ts = 0; |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| gst_opus_parse_stop (GstBaseParse * base) |
| { |
| GstOpusParse *parse = GST_OPUS_PARSE (base); |
| |
| parse->header_sent = FALSE; |
| parse->got_headers = FALSE; |
| parse->pre_skip = 0; |
| |
| return TRUE; |
| } |
| |
| static GstFlowReturn |
| gst_opus_parse_handle_frame (GstBaseParse * base, |
| GstBaseParseFrame * frame, gint * skip) |
| { |
| GstOpusParse *parse; |
| guint8 *data; |
| gsize size; |
| guint32 packet_size; |
| int ret = FALSE; |
| const unsigned char *frames[48]; |
| unsigned char toc; |
| short frame_sizes[48]; |
| int payload_offset; |
| int packet_offset = 0; |
| gboolean is_header, is_idheader, is_commentheader; |
| GstMapInfo map; |
| |
| parse = GST_OPUS_PARSE (base); |
| |
| *skip = -1; |
| |
| gst_buffer_map (frame->buffer, &map, GST_MAP_READ); |
| data = map.data; |
| size = map.size; |
| GST_DEBUG_OBJECT (parse, |
| "Checking for frame, %" G_GSIZE_FORMAT " bytes in buffer", size); |
| |
| /* check for headers */ |
| is_idheader = gst_opus_header_is_id_header (frame->buffer); |
| is_commentheader = gst_opus_header_is_comment_header (frame->buffer); |
| is_header = is_idheader || is_commentheader; |
| |
| if (!is_header) { |
| int nframes; |
| |
| /* Next, check if there's an Opus packet there */ |
| nframes = |
| opus_packet_parse (data, size, &toc, frames, frame_sizes, |
| &payload_offset); |
| |
| if (nframes < 0) { |
| /* Then, check for the test vector framing */ |
| GST_DEBUG_OBJECT (parse, |
| "No Opus packet found, trying test vector framing"); |
| if (size < 4) { |
| GST_DEBUG_OBJECT (parse, "Too small"); |
| goto beach; |
| } |
| packet_size = GST_READ_UINT32_BE (data); |
| GST_DEBUG_OBJECT (parse, "Packet size: %u bytes", packet_size); |
| if (packet_size > MAX_PAYLOAD_BYTES) { |
| GST_DEBUG_OBJECT (parse, "Too large"); |
| goto beach; |
| } |
| if (packet_size > size - 4) { |
| GST_DEBUG_OBJECT (parse, "Truncated"); |
| goto beach; |
| } |
| nframes = |
| opus_packet_parse (data + 8, packet_size, &toc, frames, frame_sizes, |
| &payload_offset); |
| if (nframes < 0) { |
| GST_DEBUG_OBJECT (parse, "No test vector framing either"); |
| goto beach; |
| } |
| |
| packet_offset = 8; |
| |
| /* for ad hoc framing, heed the framing, so we eat any padding */ |
| payload_offset = packet_size; |
| } else { |
| /* Add up all the frame sizes found */ |
| int f; |
| for (f = 0; f < nframes; ++f) |
| payload_offset += frame_sizes[f]; |
| } |
| } |
| |
| if (is_header) { |
| *skip = 0; |
| } else { |
| *skip = packet_offset; |
| size = payload_offset; |
| } |
| |
| GST_DEBUG_OBJECT (parse, |
| "Got Opus packet at offset %d, %" G_GSIZE_FORMAT " bytes", *skip, size); |
| ret = TRUE; |
| |
| beach: |
| gst_buffer_unmap (frame->buffer, &map); |
| |
| /* convert old style result to new one */ |
| if (!ret) { |
| if (*skip < 0) |
| *skip = 1; |
| return GST_FLOW_OK; |
| } |
| |
| /* always skip first if needed */ |
| if (*skip > 0) |
| return GST_FLOW_OK; |
| |
| /* normalize again */ |
| if (*skip < 0) |
| *skip = 0; |
| |
| /* not enough */ |
| if (size > map.size) |
| return GST_FLOW_OK; |
| |
| /* FIXME some day ... should not mess with buffer itself */ |
| if (!parse->got_headers) { |
| gst_buffer_replace (&frame->buffer, |
| gst_buffer_copy_region (frame->buffer, GST_BUFFER_COPY_ALL, 0, size)); |
| gst_buffer_unref (frame->buffer); |
| } |
| |
| ret = gst_opus_parse_parse_frame (base, frame); |
| |
| if (ret == GST_BASE_PARSE_FLOW_DROPPED) { |
| frame->flags |= GST_BASE_PARSE_FRAME_FLAG_DROP; |
| ret = GST_FLOW_OK; |
| } |
| if (ret == GST_FLOW_OK) |
| ret = gst_base_parse_finish_frame (base, frame, size); |
| |
| return ret; |
| } |
| |
| /* Adapted copy of the one in gstoggstream.c... */ |
| static guint64 |
| packet_duration_opus (const guint8 * data, size_t len) |
| { |
| static const guint64 durations[32] = { |
| 10000, 20000, 40000, 60000, /* Silk NB */ |
| 10000, 20000, 40000, 60000, /* Silk MB */ |
| 10000, 20000, 40000, 60000, /* Silk WB */ |
| 10000, 20000, /* Hybrid SWB */ |
| 10000, 20000, /* Hybrid FB */ |
| 2500, 5000, 10000, 20000, /* CELT NB */ |
| 2500, 5000, 10000, 20000, /* CELT NB */ |
| 2500, 5000, 10000, 20000, /* CELT NB */ |
| 2500, 5000, 10000, 20000, /* CELT NB */ |
| }; |
| |
| gint64 duration; |
| gint64 frame_duration; |
| gint nframes; |
| guint8 toc; |
| |
| if (len < 1) |
| return 0; |
| |
| toc = data[0]; |
| |
| frame_duration = durations[toc >> 3] * 1000; |
| switch (toc & 3) { |
| case 0: |
| nframes = 1; |
| break; |
| case 1: |
| nframes = 2; |
| break; |
| case 2: |
| nframes = 2; |
| break; |
| case 3: |
| if (len < 2) { |
| GST_WARNING ("Code 3 Opus packet has less than 2 bytes"); |
| return 0; |
| } |
| nframes = data[1] & 63; |
| break; |
| } |
| |
| duration = nframes * frame_duration; |
| if (duration > 120 * GST_MSECOND) { |
| GST_WARNING ("Opus packet duration > 120 ms, invalid"); |
| return 0; |
| } |
| GST_LOG ("Opus packet: frame size %.1f ms, %d frames, duration %.1f ms", |
| frame_duration / 1000000.f, nframes, duration / 1000000.f); |
| return duration; |
| } |
| |
| static GstFlowReturn |
| gst_opus_parse_parse_frame (GstBaseParse * base, GstBaseParseFrame * frame) |
| { |
| guint64 duration; |
| GstOpusParse *parse; |
| gboolean is_idheader, is_commentheader; |
| GstMapInfo map; |
| GstAudioClippingMeta *cmeta = |
| gst_buffer_get_audio_clipping_meta (frame->buffer); |
| |
| parse = GST_OPUS_PARSE (base); |
| |
| g_assert (!cmeta || cmeta->format == GST_FORMAT_DEFAULT); |
| |
| is_idheader = gst_opus_header_is_id_header (frame->buffer); |
| is_commentheader = gst_opus_header_is_comment_header (frame->buffer); |
| |
| if (!parse->got_headers || !parse->header_sent) { |
| GstCaps *caps; |
| |
| /* Opus streams can decode to 1 or 2 channels, so use the header |
| value if we have one, or 2 otherwise */ |
| if (is_idheader) { |
| gst_buffer_replace (&parse->id_header, frame->buffer); |
| GST_DEBUG_OBJECT (parse, "Found ID header, keeping"); |
| return GST_BASE_PARSE_FLOW_DROPPED; |
| } else if (is_commentheader) { |
| gst_buffer_replace (&parse->comment_header, frame->buffer); |
| GST_DEBUG_OBJECT (parse, "Found comment header, keeping"); |
| return GST_BASE_PARSE_FLOW_DROPPED; |
| } |
| |
| parse->got_headers = TRUE; |
| |
| if (cmeta && cmeta->start) { |
| parse->pre_skip += cmeta->start; |
| |
| gst_buffer_map (frame->buffer, &map, GST_MAP_READ); |
| duration = packet_duration_opus (map.data, map.size); |
| gst_buffer_unmap (frame->buffer, &map); |
| |
| /* Queue frame for later once we know all initial padding */ |
| if (duration == cmeta->start) { |
| frame->flags |= GST_BASE_PARSE_FRAME_FLAG_QUEUE; |
| } |
| } |
| |
| if (!(frame->flags & GST_BASE_PARSE_FRAME_FLAG_QUEUE)) { |
| if (FALSE && parse->id_header && parse->comment_header) { |
| guint16 pre_skip; |
| |
| gst_buffer_map (parse->id_header, &map, GST_MAP_READWRITE); |
| pre_skip = GST_READ_UINT16_LE (map.data + 10); |
| if (pre_skip != parse->pre_skip) { |
| GST_DEBUG_OBJECT (parse, |
| "Fixing up pre-skip %u -> %" G_GUINT64_FORMAT, pre_skip, |
| parse->pre_skip); |
| GST_WRITE_UINT16_LE (map.data + 10, parse->pre_skip); |
| } |
| gst_buffer_unmap (parse->id_header, &map); |
| |
| caps = |
| gst_codec_utils_opus_create_caps_from_header (parse->id_header, |
| parse->comment_header); |
| } else { |
| GstCaps *sink_caps; |
| guint32 sample_rate = 48000; |
| guint8 n_channels, n_streams, n_stereo_streams, channel_mapping_family; |
| guint8 channel_mapping[256]; |
| GstBuffer *id_header; |
| |
| sink_caps = gst_pad_get_current_caps (GST_BASE_PARSE_SINK_PAD (parse)); |
| if (!sink_caps |
| || !gst_codec_utils_opus_parse_caps (sink_caps, &sample_rate, |
| &n_channels, &channel_mapping_family, &n_streams, |
| &n_stereo_streams, channel_mapping)) { |
| GST_INFO_OBJECT (parse, |
| "No headers and no caps, blindly setting up canonical stereo"); |
| n_channels = 2; |
| n_streams = 1; |
| n_stereo_streams = 1; |
| channel_mapping_family = 0; |
| channel_mapping[0] = 0; |
| channel_mapping[1] = 1; |
| } |
| if (sink_caps) |
| gst_caps_unref (sink_caps); |
| |
| id_header = |
| gst_codec_utils_opus_create_header (sample_rate, n_channels, |
| channel_mapping_family, n_streams, n_stereo_streams, |
| channel_mapping, parse->pre_skip, 0); |
| caps = gst_codec_utils_opus_create_caps_from_header (id_header, NULL); |
| gst_buffer_unref (id_header); |
| } |
| |
| gst_buffer_replace (&parse->id_header, NULL); |
| gst_buffer_replace (&parse->comment_header, NULL); |
| |
| gst_pad_set_caps (GST_BASE_PARSE_SRC_PAD (parse), caps); |
| gst_caps_unref (caps); |
| parse->header_sent = TRUE; |
| } |
| } |
| |
| GST_BUFFER_TIMESTAMP (frame->buffer) = parse->next_ts; |
| |
| gst_buffer_map (frame->buffer, &map, GST_MAP_READ); |
| duration = packet_duration_opus (map.data, map.size); |
| gst_buffer_unmap (frame->buffer, &map); |
| parse->next_ts += duration; |
| |
| GST_BUFFER_DURATION (frame->buffer) = duration; |
| GST_BUFFER_OFFSET_END (frame->buffer) = |
| gst_util_uint64_scale (parse->next_ts, 48000, GST_SECOND); |
| GST_BUFFER_OFFSET (frame->buffer) = parse->next_ts; |
| |
| return GST_FLOW_OK; |
| } |