gstreamer/ext/ogg/gstoggstream.c

2323 lines
58 KiB
C
Raw Normal View History

/* GStreamer Ogg Granulepos Mapping Utility Functions
* Copyright (C) 2006 Tim-Philipp Müller <tim centricular net>
* Copyright (C) 2009 David Schleef <ds@schleef.org>
*
* 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., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstoggstream.h"
#include "dirac_parse.h"
#include "vorbis_parse.h"
#include <gst/riff/riff-media.h>
#include <stdlib.h>
#include <string.h>
GST_DEBUG_CATEGORY_EXTERN (gst_ogg_demux_debug);
GST_DEBUG_CATEGORY_EXTERN (gst_ogg_demux_setup_debug);
#define GST_CAT_DEFAULT gst_ogg_demux_debug
typedef struct _GstOggMap GstOggMap;
typedef gboolean (*GstOggMapSetupFunc) (GstOggStream * pad,
ogg_packet * packet);
typedef GstClockTime (*GstOggMapToTimeFunc) (GstOggStream * pad,
gint64 granulepos);
typedef gint64 (*GstOggMapToGranuleFunc) (GstOggStream * pad,
gint64 granulepos);
typedef gint64 (*GstOggMapToGranuleposFunc) (GstOggStream * pad,
gint64 granule, gint64 keyframe_granule);
/* returns TRUE if the granulepos denotes a key frame */
typedef gboolean (*GstOggMapIsGranuleposKeyFrameFunc) (GstOggStream * pad,
gint64 granulepos);
/* returns TRUE if the packet is a key frame */
typedef gboolean (*GstOggMapIsPacketKeyFrameFunc) (GstOggStream * pad,
ogg_packet * packet);
/* returns TRUE if the given packet is a stream header packet */
typedef gboolean (*GstOggMapIsHeaderPacketFunc) (GstOggStream * pad,
ogg_packet * packet);
typedef gint64 (*GstOggMapPacketDurationFunc) (GstOggStream * pad,
ogg_packet * packet);
typedef void (*GstOggMapExtractTagsFunc) (GstOggStream * pad,
ogg_packet * packet);
typedef gint64 (*GstOggMapGranuleposToKeyGranuleFunc) (GstOggStream * pad,
gint64 granulepos);
#define SKELETON_FISBONE_MIN_SIZE 52
#define SKELETON_FISHEAD_3_3_MIN_SIZE 112
#define SKELETON_FISHEAD_4_0_MIN_SIZE 80
struct _GstOggMap
{
const gchar *id;
int id_length;
int min_packet_size;
const gchar *media_type;
GstOggMapSetupFunc setup_func;
GstOggMapToGranuleFunc granulepos_to_granule_func;
GstOggMapToGranuleposFunc granule_to_granulepos_func;
GstOggMapIsGranuleposKeyFrameFunc is_granulepos_key_frame_func;
GstOggMapIsPacketKeyFrameFunc is_packet_key_frame_func;
GstOggMapIsHeaderPacketFunc is_header_func;
GstOggMapPacketDurationFunc packet_duration_func;
GstOggMapGranuleposToKeyGranuleFunc granulepos_to_key_granule_func;
GstOggMapExtractTagsFunc extract_tags_func;
};
extern const GstOggMap mappers[];
GstClockTime
gst_ogg_stream_get_packet_start_time (GstOggStream * pad, ogg_packet * packet)
{
int duration;
if (packet->granulepos == -1) {
return GST_CLOCK_TIME_NONE;
}
duration = gst_ogg_stream_get_packet_duration (pad, packet);
if (duration == -1) {
return GST_CLOCK_TIME_NONE;
}
return gst_ogg_stream_granule_to_time (pad,
gst_ogg_stream_granulepos_to_granule (pad,
packet->granulepos) - duration);
}
GstClockTime
gst_ogg_stream_get_start_time_for_granulepos (GstOggStream * pad,
gint64 granulepos)
{
if (pad->frame_size == 0)
return GST_CLOCK_TIME_NONE;
return gst_ogg_stream_granule_to_time (pad,
gst_ogg_stream_granulepos_to_granule (pad, granulepos));
}
GstClockTime
gst_ogg_stream_get_end_time_for_granulepos (GstOggStream * pad,
gint64 granulepos)
{
return gst_ogg_stream_granule_to_time (pad,
gst_ogg_stream_granulepos_to_granule (pad, granulepos));
}
GstClockTime
gst_ogg_stream_granule_to_time (GstOggStream * pad, gint64 granule)
{
if (granule == 0 || pad->granulerate_n == 0 || pad->granulerate_d == 0)
return 0;
granule += pad->granule_offset;
if (granule < 0)
return 0;
return gst_util_uint64_scale (granule, GST_SECOND * pad->granulerate_d,
pad->granulerate_n);
}
gint64
gst_ogg_stream_granulepos_to_granule (GstOggStream * pad, gint64 granulepos)
{
if (granulepos == -1 || granulepos == 0) {
return granulepos;
}
if (mappers[pad->map].granulepos_to_granule_func == NULL) {
GST_WARNING ("Failed to convert %s granulepos to granule",
gst_ogg_stream_get_media_type (pad));
return -1;
}
return mappers[pad->map].granulepos_to_granule_func (pad, granulepos);
}
gint64
gst_ogg_stream_granulepos_to_key_granule (GstOggStream * pad, gint64 granulepos)
{
if (mappers[pad->map].granulepos_to_key_granule_func)
return mappers[pad->map].granulepos_to_key_granule_func (pad, granulepos);
if (granulepos == -1 || granulepos == 0) {
return granulepos;
}
return granulepos >> pad->granuleshift;
}
gint64
gst_ogg_stream_granule_to_granulepos (GstOggStream * pad, gint64 granule,
gint64 keyframe_granule)
{
if (granule == -1 || granule == 0) {
return granule;
}
if (mappers[pad->map].granule_to_granulepos_func == NULL) {
GST_WARNING ("Failed to convert %s granule to granulepos",
gst_ogg_stream_get_media_type (pad));
return -1;
}
return mappers[pad->map].granule_to_granulepos_func (pad, granule,
keyframe_granule);
}
gboolean
2010-11-21 03:02:50 +00:00
gst_ogg_stream_granulepos_is_key_frame (GstOggStream * pad, gint64 granulepos)
{
if (granulepos == -1) {
return FALSE;
}
if (mappers[pad->map].is_granulepos_key_frame_func == NULL) {
GST_WARNING ("Failed to determine keyframeness for %s granulepos",
gst_ogg_stream_get_media_type (pad));
return FALSE;
}
return mappers[pad->map].is_granulepos_key_frame_func (pad, granulepos);
}
gboolean
gst_ogg_stream_packet_is_key_frame (GstOggStream * pad, ogg_packet * packet)
{
if (mappers[pad->map].is_packet_key_frame_func == NULL) {
GST_WARNING ("Failed to determine keyframeness of %s packet",
gst_ogg_stream_get_media_type (pad));
return FALSE;
}
return mappers[pad->map].is_packet_key_frame_func (pad, packet);
}
gboolean
gst_ogg_stream_packet_is_header (GstOggStream * pad, ogg_packet * packet)
{
if (mappers[pad->map].is_header_func == NULL) {
GST_WARNING ("Failed to determine headerness of %s packet",
gst_ogg_stream_get_media_type (pad));
return FALSE;
}
return mappers[pad->map].is_header_func (pad, packet);
}
gint64
gst_ogg_stream_get_packet_duration (GstOggStream * pad, ogg_packet * packet)
{
if (mappers[pad->map].packet_duration_func == NULL) {
GST_WARNING ("Failed to determine %s packet duration",
gst_ogg_stream_get_media_type (pad));
return -1;
}
return mappers[pad->map].packet_duration_func (pad, packet);
}
void
gst_ogg_stream_extract_tags (GstOggStream * pad, ogg_packet * packet)
{
if (mappers[pad->map].extract_tags_func == NULL) {
GST_DEBUG ("No tag extraction");
return;
}
mappers[pad->map].extract_tags_func (pad, packet);
}
const char *
gst_ogg_stream_get_media_type (GstOggStream * pad)
{
const GstCaps *caps = pad->caps;
const GstStructure *structure;
if (!caps)
return NULL;
structure = gst_caps_get_structure (caps, 0);
if (!structure)
return NULL;
return gst_structure_get_name (structure);
}
/* some generic functions */
static gboolean
is_granulepos_keyframe_true (GstOggStream * pad, gint64 granulepos)
{
return TRUE;
}
static gboolean
is_packet_keyframe_true (GstOggStream * pad, ogg_packet * packet)
{
return TRUE;
}
static gint64
granulepos_to_granule_default (GstOggStream * pad, gint64 granulepos)
{
gint64 keyindex, keyoffset;
if (pad->granuleshift != 0) {
keyindex = granulepos >> pad->granuleshift;
keyoffset = granulepos - (keyindex << pad->granuleshift);
return keyindex + keyoffset;
} else {
return granulepos;
}
}
static gint64
granule_to_granulepos_default (GstOggStream * pad, gint64 granule,
gint64 keyframe_granule)
{
gint64 keyoffset;
if (pad->granuleshift != 0) {
/* If we don't know where the previous keyframe is yet, assume it is
at 0 or 1, depending on bitstream version. If nothing else, this
avoids getting negative granpos back. */
if (keyframe_granule < 0)
keyframe_granule = pad->theora_has_zero_keyoffset ? 0 : 1;
keyoffset = granule - keyframe_granule;
return (keyframe_granule << pad->granuleshift) | keyoffset;
} else {
return granule;
}
}
#ifdef unused
static gboolean
is_header_unknown (GstOggStream * pad, ogg_packet * packet)
{
GST_WARNING ("don't know how to detect header");
return FALSE;
}
#endif
static gboolean
is_header_true (GstOggStream * pad, ogg_packet * packet)
{
return TRUE;
}
static gboolean
is_header_count (GstOggStream * pad, ogg_packet * packet)
{
if (pad->n_header_packets_seen < pad->n_header_packets) {
return TRUE;
}
return FALSE;
}
static gint64
packet_duration_constant (GstOggStream * pad, ogg_packet * packet)
{
return pad->frame_size;
}
/* helper: extracts tags from vorbis comment ogg packet.
* Returns result in *tags after free'ing existing *tags (if any) */
static gboolean
tag_list_from_vorbiscomment_packet (ogg_packet * packet,
const guint8 * id_data, const guint id_data_length, GstTagList ** tags)
{
gchar *encoder = NULL;
GstTagList *list;
gboolean ret = TRUE;
g_return_val_if_fail (tags != NULL, FALSE);
list = gst_tag_list_from_vorbiscomment (packet->packet, packet->bytes,
id_data, id_data_length, &encoder);
if (!list) {
GST_WARNING ("failed to decode vorbis comments");
ret = FALSE;
goto exit;
}
if (encoder) {
if (encoder[0])
gst_tag_list_add (list, GST_TAG_MERGE_REPLACE, GST_TAG_ENCODER, encoder,
NULL);
g_free (encoder);
}
exit:
if (*tags)
gst_tag_list_free (*tags);
*tags = list;
return ret;
}
/* theora */
static gboolean
setup_theora_mapper (GstOggStream * pad, ogg_packet * packet)
{
guint8 *data = packet->packet;
guint w, h, par_d, par_n;
guint8 vmaj, vmin, vrev;
vmaj = data[7];
vmin = data[8];
vrev = data[9];
2010-12-22 23:29:56 +00:00
w = GST_READ_UINT24_BE (data + 14) & 0xFFFFFF;
h = GST_READ_UINT24_BE (data + 17) & 0xFFFFFF;
pad->granulerate_n = GST_READ_UINT32_BE (data + 22);
pad->granulerate_d = GST_READ_UINT32_BE (data + 26);
par_n = GST_READ_UINT24_BE (data + 30);
par_d = GST_READ_UINT24_BE (data + 33);
GST_LOG ("fps = %d/%d, PAR = %u/%u, width = %u, height = %u",
pad->granulerate_n, pad->granulerate_d, par_n, par_d, w, h);
/* 2 bits + 3 bits = 5 bits KFGSHIFT */
pad->granuleshift = ((GST_READ_UINT8 (data + 40) & 0x03) << 3) +
(GST_READ_UINT8 (data + 41) >> 5);
GST_LOG ("granshift: %d", pad->granuleshift);
2010-11-21 03:02:50 +00:00
pad->is_video = TRUE;
pad->n_header_packets = 3;
pad->frame_size = 1;
pad->bitrate = GST_READ_UINT24_BE (data + 37);
GST_LOG ("bit rate: %d", pad->bitrate);
2009-12-04 01:02:11 +00:00
if (pad->granulerate_n == 0 || pad->granulerate_d == 0) {
GST_WARNING ("frame rate %d/%d", pad->granulerate_n, pad->granulerate_d);
return FALSE;
2009-12-04 01:02:11 +00:00
}
/* The interpretation of the granule position has changed with 3.2.1.
The granule is now made from the number of frames encoded, rather than
2010-12-29 11:51:42 +00:00
the index of the frame being encoded - so there is a difference of 1. */
pad->theora_has_zero_keyoffset =
((vmaj << 16) | (vmin << 8) | vrev) < 0x030201;
2011-10-27 15:26:58 +00:00
pad->caps = gst_caps_new_empty_simple ("video/x-theora");
if (w > 0 && h > 0) {
gst_caps_set_simple (pad->caps, "width", G_TYPE_INT, w, "height",
G_TYPE_INT, h, NULL);
}
/* PAR of 0:N, N:0 and 0:0 is allowed and maps to 1:1 */
if (par_n == 0 || par_d == 0)
par_n = par_d = 1;
/* only add framerate now so caps look prettier, with width/height first */
gst_caps_set_simple (pad->caps, "framerate", GST_TYPE_FRACTION,
pad->granulerate_n, pad->granulerate_d, "pixel-aspect-ratio",
GST_TYPE_FRACTION, par_n, par_d, NULL);
return TRUE;
}
static gint64
granulepos_to_granule_theora (GstOggStream * pad, gint64 granulepos)
{
gint64 keyindex, keyoffset;
if (pad->granuleshift != 0) {
keyindex = granulepos >> pad->granuleshift;
keyoffset = granulepos - (keyindex << pad->granuleshift);
if (pad->theora_has_zero_keyoffset) {
keyoffset++;
}
return keyindex + keyoffset;
} else {
return granulepos;
}
}
static gboolean
is_granulepos_keyframe_theora (GstOggStream * pad, gint64 granulepos)
{
gint64 frame_mask;
if (granulepos == (gint64) - 1)
return FALSE;
2010-11-21 03:02:50 +00:00
frame_mask = (1 << pad->granuleshift) - 1;
return ((granulepos & frame_mask) == 0);
}
static gboolean
is_packet_keyframe_theora (GstOggStream * pad, ogg_packet * packet)
{
if (packet->bytes == 0)
return FALSE;
return (packet->packet[0] & 0xc0) == 0x00;
}
static gboolean
is_header_theora (GstOggStream * pad, ogg_packet * packet)
{
return (packet->bytes > 0 && (packet->packet[0] & 0x80) == 0x80);
}
static void
extract_tags_theora (GstOggStream * pad, ogg_packet * packet)
{
if (packet->bytes > 0 && packet->packet[0] == 0x81) {
tag_list_from_vorbiscomment_packet (packet,
(const guint8 *) "\201theora", 7, &pad->taglist);
if (!pad->taglist)
pad->taglist = gst_tag_list_new_empty ();
if (pad->bitrate)
gst_tag_list_add (pad->taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_BITRATE, (guint) pad->bitrate, NULL);
}
}
/* dirac */
static gboolean
setup_dirac_mapper (GstOggStream * pad, ogg_packet * packet)
{
int ret;
DiracSequenceHeader header;
ret = dirac_sequence_header_parse (&header, packet->packet + 13,
packet->bytes - 13);
if (ret == 0) {
GST_DEBUG ("Failed to parse Dirac sequence header");
return FALSE;
}
2010-11-21 03:02:50 +00:00
pad->is_video = TRUE;
pad->always_flush_page = TRUE;
pad->granulerate_n = header.frame_rate_numerator * 2;
pad->granulerate_d = header.frame_rate_denominator;
pad->granuleshift = 22;
pad->n_header_packets = 1;
pad->frame_size = 2;
if (header.interlaced_coding != 0) {
GST_DEBUG ("non-progressive Dirac coding not implemented");
return FALSE;
}
pad->caps = gst_caps_new_simple ("video/x-dirac",
"width", G_TYPE_INT, header.width,
"height", G_TYPE_INT, header.height,
2012-06-26 15:09:25 +00:00
"interlace-mode", G_TYPE_STRING,
(header.interlaced ? "mixed" : "progressive"), "pixel-aspect-ratio",
GST_TYPE_FRACTION, header.aspect_ratio_numerator,
header.aspect_ratio_denominator, "framerate", GST_TYPE_FRACTION,
header.frame_rate_numerator, header.frame_rate_denominator, NULL);
return TRUE;
}
#define OGG_DIRAC_GRANULE_LOW_MASK ((1<<22) - 1)
static gboolean
is_keyframe_dirac (GstOggStream * pad, gint64 granulepos)
{
int dist_h;
int dist_l;
int dist;
if (granulepos == -1)
return -1;
dist_h = (granulepos >> 22) & 0xff;
dist_l = granulepos & 0xff;
dist = (dist_h << 8) | dist_l;
return (dist == 0);
}
static gint64
granulepos_to_granule_dirac (GstOggStream * pad, gint64 gp)
{
gint64 pt;
int delay;
gint64 dt;
pt = ((gp >> 22) + (gp & OGG_DIRAC_GRANULE_LOW_MASK)) >> 9;
delay = (gp >> 9) & 0x1fff;
dt = pt - delay;
GST_DEBUG ("pt %" G_GINT64_FORMAT " delay %d", pt, delay);
return dt + 4;
}
static gint64
granule_to_granulepos_dirac (GstOggStream * pad, gint64 granule,
gint64 keyframe_granule)
{
/* This conversion requires knowing more details about the Dirac
* stream. */
return -1;
}
static gint64
granulepos_to_key_granule_dirac (GstOggStream * pad, gint64 gp)
{
gint64 pt;
int dist_h;
int dist_l;
int dist;
int delay;
gint64 dt;
if (gp == -1 || gp == 0)
return gp;
pt = ((gp >> 22) + (gp & OGG_DIRAC_GRANULE_LOW_MASK)) >> 9;
dist_h = (gp >> 22) & 0xff;
dist_l = gp & 0xff;
dist = (dist_h << 8) | dist_l;
delay = (gp >> 9) & 0x1fff;
dt = pt - delay;
return dt - 2 * dist + 4;
}
2010-05-05 11:59:57 +00:00
/* VP8 */
static gboolean
setup_vp8_mapper (GstOggStream * pad, ogg_packet * packet)
{
gint width, height, par_n, par_d, fps_n, fps_d;
if (packet->bytes < 26) {
2010-05-05 11:59:57 +00:00
GST_DEBUG ("Failed to parse VP8 BOS page");
return FALSE;
}
width = GST_READ_UINT16_BE (packet->packet + 8);
height = GST_READ_UINT16_BE (packet->packet + 10);
par_n = GST_READ_UINT24_BE (packet->packet + 12);
par_d = GST_READ_UINT24_BE (packet->packet + 15);
fps_n = GST_READ_UINT32_BE (packet->packet + 18);
fps_d = GST_READ_UINT32_BE (packet->packet + 22);
2010-05-05 11:59:57 +00:00
2010-11-21 03:02:50 +00:00
pad->is_video = TRUE;
2010-05-05 11:59:57 +00:00
pad->is_vp8 = TRUE;
pad->granulerate_n = fps_n;
pad->granulerate_d = fps_d;
pad->n_header_packets = 2;
pad->frame_size = 1;
pad->caps = gst_caps_new_simple ("video/x-vp8",
"width", G_TYPE_INT, width,
"height", G_TYPE_INT, height,
"pixel-aspect-ratio", GST_TYPE_FRACTION,
par_n, par_d, "framerate", GST_TYPE_FRACTION, fps_n, fps_d, NULL);
return TRUE;
}
static gboolean
is_keyframe_vp8 (GstOggStream * pad, gint64 granulepos)
{
guint64 gpos = granulepos;
if (granulepos == -1)
return FALSE;
/* Get rid of flags */
gpos >>= 3;
return ((gpos & 0x07ffffff) == 0);
}
static gint64
granulepos_to_granule_vp8 (GstOggStream * pad, gint64 gpos)
{
guint64 gp = (guint64) gpos;
guint32 pt;
guint32 dist;
pt = (gp >> 32);
dist = (gp >> 3) & 0x07ffffff;
GST_DEBUG ("pt %u, dist %u", pt, dist);
return pt;
}
static gint64
granule_to_granulepos_vp8 (GstOggStream * pad, gint64 granule,
gint64 keyframe_granule)
{
/* FIXME: This requires to look into the content of the packets
* because the simple granule counter doesn't know about invisible
* frames...
*/
return -1;
}
/* Check if this packet contains an invisible frame or not */
static gint64
packet_duration_vp8 (GstOggStream * pad, ogg_packet * packet)
{
guint32 hdr;
if (packet->bytes < 3)
return 0;
hdr = GST_READ_UINT24_LE (packet->packet);
return (((hdr >> 4) & 1) != 0) ? 1 : 0;
}
static gint64
granulepos_to_key_granule_vp8 (GstOggStream * pad, gint64 granulepos)
{
guint64 gp = granulepos;
guint64 pts = (gp >> 32);
guint32 dist = (gp >> 3) & 0x07ffffff;
if (granulepos == -1 || granulepos == 0)
return granulepos;
if (dist > pts)
return 0;
return pts - dist;
}
2010-05-19 19:35:19 +00:00
static gboolean
is_header_vp8 (GstOggStream * pad, ogg_packet * packet)
{
2010-05-20 11:58:14 +00:00
if (packet->bytes >= 5 && packet->packet[0] == 0x4F &&
2010-05-19 19:35:19 +00:00
packet->packet[1] == 0x56 && packet->packet[2] == 0x50 &&
packet->packet[3] == 0x38 && packet->packet[4] == 0x30)
2010-05-19 19:35:19 +00:00
return TRUE;
return FALSE;
}
static void
extract_tags_vp8 (GstOggStream * pad, ogg_packet * packet)
{
if (packet->bytes >= 7 && memcmp (packet->packet, "OVP80\2 ", 7) == 0) {
tag_list_from_vorbiscomment_packet (packet,
(const guint8 *) "OVP80\2 ", 7, &pad->taglist);
}
}
/* vorbis */
static gboolean
setup_vorbis_mapper (GstOggStream * pad, ogg_packet * packet)
{
guint8 *data = packet->packet;
guint chans;
data += 1 + 6;
pad->version = GST_READ_UINT32_LE (data);
data += 4;
chans = GST_READ_UINT8 (data);
data += 1;
pad->granulerate_n = GST_READ_UINT32_LE (data);
pad->granulerate_d = 1;
pad->granuleshift = 0;
pad->preroll = 2;
pad->last_size = 0;
GST_LOG ("sample rate: %d", pad->granulerate_n);
data += 4;
pad->bitrate_upper = GST_READ_UINT32_LE (data);
data += 4;
pad->bitrate_nominal = GST_READ_UINT32_LE (data);
data += 4;
pad->bitrate_lower = GST_READ_UINT32_LE (data);
if (pad->bitrate_nominal > 0)
pad->bitrate = pad->bitrate_nominal;
if (pad->bitrate_upper > 0 && !pad->bitrate)
pad->bitrate = pad->bitrate_upper;
if (pad->bitrate_lower > 0 && !pad->bitrate)
pad->bitrate = pad->bitrate_lower;
GST_LOG ("bit rate: %d", pad->bitrate);
pad->n_header_packets = 3;
if (pad->granulerate_n == 0)
return FALSE;
parse_vorbis_header_packet (pad, packet);
pad->caps = gst_caps_new_simple ("audio/x-vorbis",
"rate", G_TYPE_INT, pad->granulerate_n, "channels", G_TYPE_INT, chans,
NULL);
return TRUE;
}
static gboolean
is_header_vorbis (GstOggStream * pad, ogg_packet * packet)
{
if (packet->bytes > 0 && (packet->packet[0] & 0x01) == 0)
return FALSE;
if (packet->packet[0] == 5) {
parse_vorbis_setup_packet (pad, packet);
}
return TRUE;
}
static void
extract_tags_vorbis (GstOggStream * pad, ogg_packet * packet)
{
if (packet->bytes == 0 || (packet->packet[0] & 0x01) == 0)
return;
if (((guint8 *) (packet->packet))[0] == 0x03) {
tag_list_from_vorbiscomment_packet (packet,
(const guint8 *) "\003vorbis", 7, &pad->taglist);
if (!pad->taglist)
pad->taglist = gst_tag_list_new_empty ();
gst_tag_list_add (pad->taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_ENCODER_VERSION, pad->version, NULL);
if (pad->bitrate_nominal > 0)
gst_tag_list_add (pad->taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_NOMINAL_BITRATE, (guint) pad->bitrate_nominal, NULL);
if (pad->bitrate_upper > 0)
gst_tag_list_add (pad->taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_MAXIMUM_BITRATE, (guint) pad->bitrate_upper, NULL);
if (pad->bitrate_lower > 0)
gst_tag_list_add (pad->taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_MINIMUM_BITRATE, (guint) pad->bitrate_lower, NULL);
if (pad->bitrate)
gst_tag_list_add (pad->taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_BITRATE, (guint) pad->bitrate, NULL);
}
}
static gint64
packet_duration_vorbis (GstOggStream * pad, ogg_packet * packet)
{
int mode;
int size;
int duration;
if (packet->bytes == 0 || packet->packet[0] & 1)
return 0;
mode = (packet->packet[0] >> 1) & ((1 << pad->vorbis_log2_num_modes) - 1);
size = pad->vorbis_mode_sizes[mode] ? pad->long_size : pad->short_size;
if (pad->last_size == 0) {
duration = 0;
} else {
duration = pad->last_size / 4 + size / 4;
}
pad->last_size = size;
GST_DEBUG ("duration %d", (int) duration);
return duration;
}
/* speex */
static gboolean
setup_speex_mapper (GstOggStream * pad, ogg_packet * packet)
{
guint8 *data = packet->packet;
guint chans;
data += 8 + 20 + 4 + 4;
pad->granulerate_n = GST_READ_UINT32_LE (data);
pad->granulerate_d = 1;
pad->granuleshift = 0;
data += 4 + 4 + 4;
chans = GST_READ_UINT32_LE (data);
data += 4;
pad->bitrate = GST_READ_UINT32_LE (data);
GST_LOG ("sample rate: %d, channels: %u", pad->granulerate_n, chans);
GST_LOG ("bit rate: %d", pad->bitrate);
pad->n_header_packets = GST_READ_UINT32_LE (packet->packet + 68) + 2;
pad->frame_size = GST_READ_UINT32_LE (packet->packet + 64) *
GST_READ_UINT32_LE (packet->packet + 56);
if (pad->granulerate_n == 0)
return FALSE;
pad->caps = gst_caps_new_simple ("audio/x-speex", "rate", G_TYPE_INT,
pad->granulerate_n, "channels", G_TYPE_INT, chans, NULL);
return TRUE;
}
static void
extract_tags_count (GstOggStream * pad, ogg_packet * packet)
{
/* packet 2 must be comment packet */
if (packet->bytes > 0 && pad->n_header_packets_seen == 1) {
tag_list_from_vorbiscomment_packet (packet, NULL, 0, &pad->taglist);
if (!pad->taglist)
pad->taglist = gst_tag_list_new_empty ();
if (pad->bitrate)
gst_tag_list_add (pad->taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_BITRATE, (guint) pad->bitrate, NULL);
}
}
/* flac */
static gboolean
setup_fLaC_mapper (GstOggStream * pad, ogg_packet * packet)
{
2009-12-11 06:47:53 +00:00
pad->granulerate_n = 0;
pad->granulerate_d = 1;
pad->granuleshift = 0;
pad->n_header_packets = 3;
2011-10-27 15:26:58 +00:00
pad->caps = gst_caps_new_empty_simple ("audio/x-flac");
2009-12-11 06:47:53 +00:00
return TRUE;
}
static gboolean
is_header_fLaC (GstOggStream * pad, ogg_packet * packet)
{
if (pad->n_header_packets_seen == 1) {
pad->granulerate_n = (packet->packet[14] << 12) |
(packet->packet[15] << 4) | ((packet->packet[16] >> 4) & 0xf);
}
if (pad->n_header_packets_seen < pad->n_header_packets) {
return TRUE;
}
return FALSE;
}
static gboolean
setup_flac_mapper (GstOggStream * pad, ogg_packet * packet)
{
guint8 *data = packet->packet;
guint chans;
/* see http://flac.sourceforge.net/ogg_mapping.html */
pad->granulerate_n = (GST_READ_UINT32_BE (data + 27) & 0xFFFFF000) >> 12;
pad->granulerate_d = 1;
pad->granuleshift = 0;
chans = ((GST_READ_UINT32_BE (data + 27) & 0x00000E00) >> 9) + 1;
GST_DEBUG ("sample rate: %d, channels: %u", pad->granulerate_n, chans);
pad->n_header_packets = GST_READ_UINT16_BE (packet->packet + 7);
if (pad->granulerate_n == 0)
return FALSE;
pad->caps = gst_caps_new_simple ("audio/x-flac", "rate", G_TYPE_INT,
pad->granulerate_n, "channels", G_TYPE_INT, chans, NULL);
return TRUE;
}
static gboolean
is_header_flac (GstOggStream * pad, ogg_packet * packet)
{
return (packet->bytes > 0 && (packet->packet[0] != 0xff));
}
static gint64
packet_duration_flac (GstOggStream * pad, ogg_packet * packet)
{
int block_size_index;
if (packet->bytes < 4)
return -1;
block_size_index = packet->packet[2] >> 4;
if (block_size_index == 1)
return 192;
if (block_size_index >= 2 && block_size_index <= 5) {
return 576 << (block_size_index - 2);
}
if (block_size_index >= 8) {
return 256 << (block_size_index - 8);
}
if (block_size_index == 6 || block_size_index == 7) {
guint len, bytes = (block_size_index - 6) + 1;
guint8 tmp;
if (packet->bytes < 4 + 1 + bytes)
return -1;
tmp = packet->packet[4];
/* utf-8 prefix */
len = 0;
while (tmp & 0x80) {
len++;
tmp <<= 1;
}
if (len == 2)
return -1;
if (len == 0)
len++;
if (packet->bytes < 4 + len + bytes)
return -1;
if (bytes == 1) {
return packet->packet[4 + len] + 1;
} else {
return GST_READ_UINT16_BE (packet->packet + 4 + len) + 1;
}
}
return -1;
}
static void
extract_tags_flac (GstOggStream * pad, ogg_packet * packet)
{
if (packet->bytes > 4 && ((packet->packet[0] & 0x7F) == 0x4)) {
tag_list_from_vorbiscomment_packet (packet,
packet->packet, 4, &pad->taglist);
}
}
/* fishead */
static gboolean
setup_fishead_mapper (GstOggStream * pad, ogg_packet * packet)
{
guint8 *data;
gint64 prestime_n, prestime_d;
gint64 basetime_n, basetime_d;
data = packet->packet;
data += 8; /* header */
pad->skeleton_major = GST_READ_UINT16_LE (data);
data += 2;
pad->skeleton_minor = GST_READ_UINT16_LE (data);
data += 2;
prestime_n = (gint64) GST_READ_UINT64_LE (data);
data += 8;
prestime_d = (gint64) GST_READ_UINT64_LE (data);
data += 8;
basetime_n = (gint64) GST_READ_UINT64_LE (data);
data += 8;
basetime_d = (gint64) GST_READ_UINT64_LE (data);
data += 8;
/* FIXME: we don't use basetime anywhere in the demuxer! */
if (basetime_d != 0)
pad->basetime = gst_util_uint64_scale (GST_SECOND, basetime_n, basetime_d);
else
pad->basetime = -1;
if (prestime_d != 0)
pad->prestime = gst_util_uint64_scale (GST_SECOND, prestime_n, prestime_d);
else
pad->prestime = -1;
/* Ogg Skeleton 3.3+ streams provide additional information in the header */
if (packet->bytes >= SKELETON_FISHEAD_3_3_MIN_SIZE && pad->skeleton_major == 3
&& pad->skeleton_minor > 0) {
gint64 firstsampletime_n, firstsampletime_d;
gint64 lastsampletime_n, lastsampletime_d;
gint64 firstsampletime, lastsampletime;
guint64 segment_length, content_offset;
firstsampletime_n = GST_READ_UINT64_LE (data + 64);
firstsampletime_d = GST_READ_UINT64_LE (data + 72);
lastsampletime_n = GST_READ_UINT64_LE (data + 80);
lastsampletime_d = GST_READ_UINT64_LE (data + 88);
segment_length = GST_READ_UINT64_LE (data + 96);
content_offset = GST_READ_UINT64_LE (data + 104);
GST_INFO ("firstsampletime %" G_GUINT64_FORMAT "/%" G_GUINT64_FORMAT,
firstsampletime_n, firstsampletime_d);
GST_INFO ("lastsampletime %" G_GUINT64_FORMAT "/%" G_GUINT64_FORMAT,
lastsampletime_n, lastsampletime_d);
GST_INFO ("segment length %" G_GUINT64_FORMAT, segment_length);
GST_INFO ("content offset %" G_GUINT64_FORMAT, content_offset);
if (firstsampletime_d > 0)
firstsampletime = gst_util_uint64_scale (GST_SECOND,
firstsampletime_n, firstsampletime_d);
else
firstsampletime = 0;
if (lastsampletime_d > 0)
lastsampletime = gst_util_uint64_scale (GST_SECOND,
lastsampletime_n, lastsampletime_d);
else
lastsampletime = 0;
if (lastsampletime > firstsampletime)
pad->total_time = lastsampletime - firstsampletime;
else
pad->total_time = -1;
GST_INFO ("skeleton fishead parsed total: %" GST_TIME_FORMAT,
GST_TIME_ARGS (pad->total_time));
} else if (packet->bytes >= SKELETON_FISHEAD_4_0_MIN_SIZE
&& pad->skeleton_major == 4) {
guint64 segment_length, content_offset;
segment_length = GST_READ_UINT64_LE (data + 64);
content_offset = GST_READ_UINT64_LE (data + 72);
GST_INFO ("segment length %" G_GUINT64_FORMAT, segment_length);
GST_INFO ("content offset %" G_GUINT64_FORMAT, content_offset);
} else {
pad->total_time = -1;
}
GST_INFO ("skeleton fishead %u.%u parsed (basetime: %" GST_TIME_FORMAT
", prestime: %" GST_TIME_FORMAT ")", pad->skeleton_major,
pad->skeleton_minor, GST_TIME_ARGS (pad->basetime),
GST_TIME_ARGS (pad->prestime));
pad->is_skeleton = TRUE;
pad->is_sparse = TRUE;
2011-10-27 15:26:58 +00:00
pad->caps = gst_caps_new_empty_simple ("application/x-ogg-skeleton");
return TRUE;
}
gboolean
gst_ogg_map_parse_fisbone (GstOggStream * pad, const guint8 * data, guint size,
2010-03-02 10:16:39 +00:00
guint32 * serialno, GstOggSkeleton * type)
{
2010-03-02 10:16:39 +00:00
GstOggSkeleton stype;
guint serial_offset;
if (size != 0 && size < SKELETON_FISBONE_MIN_SIZE) {
GST_WARNING ("small fisbone packet of size %d, ignoring", size);
return FALSE;
}
if (size == 0) {
/* Skeleton EOS packet is zero bytes */
return FALSE;
} else if (memcmp (data, "fisbone\0", 8) == 0) {
2010-03-02 10:16:39 +00:00
GST_INFO ("got fisbone packet");
stype = GST_OGG_SKELETON_FISBONE;
serial_offset = 12;
} else if (memcmp (data, "index\0", 6) == 0) {
GST_INFO ("got index packet");
stype = GST_OGG_SKELETON_INDEX;
serial_offset = 6;
} else if (memcmp (data, "fishead\0", 8) == 0) {
return FALSE;
2010-03-02 10:16:39 +00:00
} else {
GST_WARNING ("unknown skeleton packet \"%10.10s\"", data);
return FALSE;
}
if (serialno)
2010-03-02 10:16:39 +00:00
*serialno = GST_READ_UINT32_LE (data + serial_offset);
if (type)
*type = stype;
return TRUE;
}
gboolean
gst_ogg_map_add_fisbone (GstOggStream * pad, GstOggStream * skel_pad,
const guint8 * data, guint size, GstClockTime * p_start_time)
{
GstClockTime start_time;
gint64 start_granule;
2010-03-02 10:16:39 +00:00
if (pad->have_fisbone) {
GST_DEBUG ("already have fisbone, ignoring second one");
return FALSE;
}
/* skip "fisbone\0" + headers offset + serialno + num headers */
data += 8 + 4 + 4 + 4;
pad->have_fisbone = TRUE;
/* We don't overwrite whatever was set before by the format-specific
setup: skeleton contains wrong information sometimes, and the codec
headers are authoritative.
So we only gather information that was not already filled out by
the mapper setup. This should hopefully allow handling unknown
streams a bit better, while not trashing correct setup from bad
skeleton data. */
if (pad->granulerate_n == 0 || pad->granulerate_d == 0) {
pad->granulerate_n = GST_READ_UINT64_LE (data);
pad->granulerate_d = GST_READ_UINT64_LE (data + 8);
}
if (pad->granuleshift == G_MAXUINT32) {
pad->granuleshift = GST_READ_UINT8 (data + 28);
}
start_granule = GST_READ_UINT64_LE (data + 16);
pad->preroll = GST_READ_UINT32_LE (data + 24);
start_time = granulepos_to_granule_default (pad, start_granule);
GST_INFO ("skeleton fisbone parsed "
"(start time: %" GST_TIME_FORMAT
" granulerate_n: %d granulerate_d: %d "
" preroll: %" G_GUINT32_FORMAT " granuleshift: %d)",
GST_TIME_ARGS (start_time),
pad->granulerate_n, pad->granulerate_d, pad->preroll, pad->granuleshift);
if (p_start_time)
*p_start_time = start_time;
return TRUE;
}
static gboolean
read_vlc (const guint8 ** data, guint * size, guint64 * result)
2010-03-02 10:16:39 +00:00
{
gint shift = 0;
guint8 byte;
2010-03-02 10:16:39 +00:00
*result = 0;
2010-03-02 10:16:39 +00:00
do {
if (G_UNLIKELY (*size < 1))
return FALSE;
2010-03-02 10:16:39 +00:00
byte = **data;
*result |= ((byte & 0x7f) << shift);
2010-03-02 10:16:39 +00:00
shift += 7;
2010-03-02 10:16:39 +00:00
(*data)++;
(*size)--;
2010-03-02 10:16:39 +00:00
} while ((byte & 0x80) != 0x80);
return TRUE;
2010-03-02 10:16:39 +00:00
}
gboolean
gst_ogg_map_add_index (GstOggStream * pad, GstOggStream * skel_pad,
const guint8 * data, guint size)
2010-03-02 10:16:39 +00:00
{
guint64 i, n_keypoints, isize;
2010-03-02 10:16:39 +00:00
guint64 offset, timestamp;
guint64 offset_d, timestamp_d;
2010-03-02 10:16:39 +00:00
if (pad->index) {
GST_DEBUG ("already have index, ignoring second one");
return TRUE;
}
if ((skel_pad->skeleton_major == 3 && size < 26) ||
(skel_pad->skeleton_major == 4 && size < 62)) {
GST_WARNING ("small index packet of size %u, ignoring", size);
return FALSE;
}
/* skip "index\0" + serialno */
data += 6 + 4;
size -= 6 + 4;
2010-03-02 10:16:39 +00:00
n_keypoints = GST_READ_UINT64_LE (data);
2010-05-04 11:51:07 +00:00
data += 8;
size -= 8;
pad->kp_denom = GST_READ_UINT64_LE (data);
if (pad->kp_denom == 0)
pad->kp_denom = 1;
2010-03-02 10:16:39 +00:00
data += 8;
size -= 8;
if (skel_pad->skeleton_major == 4) {
gint64 firstsampletime_n;
gint64 lastsampletime_n;
gint64 firstsampletime, lastsampletime;
firstsampletime_n = GST_READ_UINT64_LE (data + 0);
lastsampletime_n = GST_READ_UINT64_LE (data + 8);
GST_INFO ("firstsampletime %" G_GUINT64_FORMAT "/%" G_GUINT64_FORMAT,
firstsampletime_n, pad->kp_denom);
GST_INFO ("lastsampletime %" G_GUINT64_FORMAT "/%" G_GUINT64_FORMAT,
lastsampletime_n, pad->kp_denom);
firstsampletime = gst_util_uint64_scale (GST_SECOND,
firstsampletime_n, pad->kp_denom);
lastsampletime = gst_util_uint64_scale (GST_SECOND,
lastsampletime_n, pad->kp_denom);
if (lastsampletime > firstsampletime)
pad->total_time = lastsampletime - firstsampletime;
else
pad->total_time = -1;
GST_INFO ("skeleton index parsed total: %" GST_TIME_FORMAT,
GST_TIME_ARGS (pad->total_time));
data += 16;
size -= 16;
}
2010-03-02 10:16:39 +00:00
GST_INFO ("skeleton index has %" G_GUINT64_FORMAT " keypoints, denom: %"
G_GINT64_FORMAT, n_keypoints, pad->kp_denom);
pad->index = g_try_new (GstOggIndex, n_keypoints);
if (!pad->index)
return FALSE;
isize = 0;
2010-03-02 10:16:39 +00:00
offset = 0;
timestamp = 0;
for (i = 0; i < n_keypoints; i++) {
/* read deltas */
if (!read_vlc (&data, &size, &offset_d))
break;
if (!read_vlc (&data, &size, &timestamp_d))
break;
offset += offset_d;
timestamp += timestamp_d;
2010-03-02 10:16:39 +00:00
pad->index[i].offset = offset;
pad->index[i].timestamp = timestamp;
isize++;
2010-03-02 10:16:39 +00:00
GST_INFO ("offset %" G_GUINT64_FORMAT " time %" G_GUINT64_FORMAT, offset,
timestamp);
}
if (isize != n_keypoints) {
GST_WARNING ("truncated index, expected %" G_GUINT64_FORMAT ", found %"
G_GUINT64_FORMAT, n_keypoints, isize);
}
pad->n_index = isize;
/* try to use the index to estimate the bitrate */
if (isize > 2) {
guint64 so, eo, st, et, b, t;
/* get start and end offset and timestamps */
so = pad->index[0].offset;
st = pad->index[0].timestamp;
eo = pad->index[isize - 1].offset;
et = pad->index[isize - 1].timestamp;
b = eo - so;
t = et - st;
GST_DEBUG ("bytes/time %" G_GUINT64_FORMAT "/%" G_GUINT64_FORMAT, b, t);
/* this is the total stream bitrate according to this index */
pad->idx_bitrate = gst_util_uint64_scale (8 * b, pad->kp_denom, t);
GST_DEBUG ("bitrate %" G_GUINT64_FORMAT, pad->idx_bitrate);
}
2010-03-02 10:16:39 +00:00
return TRUE;
}
static gint
gst_ogg_index_compare (const GstOggIndex * index, const guint64 * ts,
gpointer user_data)
{
if (index->timestamp < *ts)
return -1;
else if (index->timestamp > *ts)
return 1;
else
return 0;
}
2010-03-02 10:16:39 +00:00
gboolean
gst_ogg_map_search_index (GstOggStream * pad, gboolean before,
guint64 * timestamp, guint64 * offset)
{
guint64 n_index;
guint64 ts;
GstOggIndex *best;
2010-03-02 10:16:39 +00:00
n_index = pad->n_index;
if (n_index == 0 || pad->index == NULL)
return FALSE;
ts = gst_util_uint64_scale (*timestamp, pad->kp_denom, GST_SECOND);
GST_INFO ("timestamp %" G_GUINT64_FORMAT, ts);
best =
gst_util_array_binary_search (pad->index, n_index, sizeof (GstOggIndex),
(GCompareDataFunc) gst_ogg_index_compare, GST_SEARCH_MODE_BEFORE, &ts,
NULL);
if (best == NULL)
2010-03-02 10:16:39 +00:00
return FALSE;
GST_INFO ("found at index %u", (guint) (best - pad->index));
2010-03-02 10:16:39 +00:00
if (offset)
*offset = best->offset;
2010-03-02 10:16:39 +00:00
if (timestamp)
*timestamp =
gst_util_uint64_scale (best->timestamp, GST_SECOND, pad->kp_denom);
2010-03-02 10:16:39 +00:00
return TRUE;
}
/* Do we need these for something?
* ogm->hdr.size = GST_READ_UINT32_LE (&data[13]);
* ogm->hdr.time_unit = GST_READ_UINT64_LE (&data[17]);
* ogm->hdr.samples_per_unit = GST_READ_UINT64_LE (&data[25]);
* ogm->hdr.default_len = GST_READ_UINT32_LE (&data[33]);
* ogm->hdr.buffersize = GST_READ_UINT32_LE (&data[37]);
* ogm->hdr.bits_per_sample = GST_READ_UINT32_LE (&data[41]);
*/
static gboolean
is_header_ogm (GstOggStream * pad, ogg_packet * packet)
{
if (packet->bytes >= 1 && (packet->packet[0] & 0x01))
return TRUE;
return FALSE;
}
static void
extract_tags_ogm (GstOggStream * pad, ogg_packet * packet)
{
if (!(packet->packet[0] & 1) && (packet->packet[0] & 3 && pad->is_ogm_text)) {
tag_list_from_vorbiscomment_packet (packet,
(const guint8 *) "\003vorbis", 7, &pad->taglist);
}
}
static gint64
packet_duration_ogm (GstOggStream * pad, ogg_packet * packet)
{
const guint8 *data;
int samples;
int offset;
int n;
data = packet->packet;
offset = 1 + (((data[0] & 0xc0) >> 6) | ((data[0] & 0x02) << 1));
if (offset > packet->bytes) {
GST_ERROR ("buffer too small");
return -1;
}
samples = 0;
for (n = offset - 1; n > 0; n--) {
samples = (samples << 8) | data[n];
}
return samples;
}
static gboolean
setup_ogmaudio_mapper (GstOggStream * pad, ogg_packet * packet)
{
guint8 *data = packet->packet;
guint32 fourcc;
gchar *fstr;
pad->granulerate_n = GST_READ_UINT64_LE (data + 25);
pad->granulerate_d = 1;
fourcc = GST_READ_UINT32_LE (data + 9);
fstr = g_strdup_printf ("%" GST_FOURCC_FORMAT, GST_FOURCC_ARGS (fourcc));
GST_DEBUG ("fourcc: %s", fstr);
/* FIXME: Need to do something with the reorder map */
pad->caps =
gst_riff_create_audio_caps (fourcc, NULL, NULL, NULL, NULL, NULL, NULL);
GST_LOG ("sample rate: %d", pad->granulerate_n);
if (pad->granulerate_n == 0)
return FALSE;
if (pad->caps) {
gst_caps_set_simple (pad->caps,
"rate", G_TYPE_INT, pad->granulerate_n, NULL);
} else {
pad->caps = gst_caps_new_simple ("audio/x-ogm-unknown",
"fourcc", G_TYPE_STRING, fstr,
"rate", G_TYPE_INT, pad->granulerate_n, NULL);
}
g_free (fstr);
pad->n_header_packets = 1;
pad->is_ogm = TRUE;
return TRUE;
}
static gboolean
setup_ogmvideo_mapper (GstOggStream * pad, ogg_packet * packet)
{
guint8 *data = packet->packet;
guint32 fourcc;
int width, height;
gint64 time_unit;
gchar *fstr;
GST_DEBUG ("time unit %d", GST_READ_UINT32_LE (data + 16));
GST_DEBUG ("samples per unit %d", GST_READ_UINT32_LE (data + 24));
2010-11-21 03:02:50 +00:00
pad->is_video = TRUE;
pad->granulerate_n = 10000000;
time_unit = GST_READ_UINT64_LE (data + 17);
if (time_unit > G_MAXINT || time_unit < G_MININT) {
GST_WARNING ("timeunit is out of range");
}
pad->granulerate_d = (gint) CLAMP (time_unit, G_MININT, G_MAXINT);
GST_LOG ("fps = %d/%d = %.3f",
pad->granulerate_n, pad->granulerate_d,
(double) pad->granulerate_n / pad->granulerate_d);
fourcc = GST_READ_UINT32_LE (data + 9);
width = GST_READ_UINT32_LE (data + 45);
height = GST_READ_UINT32_LE (data + 49);
fstr = g_strdup_printf ("%" GST_FOURCC_FORMAT, GST_FOURCC_ARGS (fourcc));
GST_DEBUG ("fourcc: %s", fstr);
pad->caps = gst_riff_create_video_caps (fourcc, NULL, NULL, NULL, NULL, NULL);
if (pad->caps == NULL) {
pad->caps = gst_caps_new_simple ("video/x-ogm-unknown",
"fourcc", G_TYPE_STRING, fstr,
"framerate", GST_TYPE_FRACTION, pad->granulerate_n,
pad->granulerate_d, NULL);
} else {
gst_caps_set_simple (pad->caps,
"framerate", GST_TYPE_FRACTION, pad->granulerate_n,
pad->granulerate_d,
"width", G_TYPE_INT, width, "height", G_TYPE_INT, height, NULL);
}
GST_DEBUG ("caps: %" GST_PTR_FORMAT, pad->caps);
g_free (fstr);
pad->n_header_packets = 1;
pad->frame_size = 1;
pad->is_ogm = TRUE;
return TRUE;
}
static gboolean
setup_ogmtext_mapper (GstOggStream * pad, ogg_packet * packet)
{
guint8 *data = packet->packet;
gint64 time_unit;
pad->granulerate_n = 10000000;
time_unit = GST_READ_UINT64_LE (data + 17);
if (time_unit > G_MAXINT || time_unit < G_MININT) {
GST_WARNING ("timeunit is out of range");
}
pad->granulerate_d = (gint) CLAMP (time_unit, G_MININT, G_MAXINT);
GST_LOG ("fps = %d/%d = %.3f",
pad->granulerate_n, pad->granulerate_d,
(double) pad->granulerate_n / pad->granulerate_d);
if (pad->granulerate_d <= 0)
return FALSE;
2011-10-27 15:26:58 +00:00
pad->caps = gst_caps_new_empty_simple ("text/plain");
pad->n_header_packets = 1;
pad->is_ogm = TRUE;
pad->is_ogm_text = TRUE;
pad->is_sparse = TRUE;
return TRUE;
}
/* PCM */
#define OGGPCM_FMT_S8 0x00000000 /* Signed integer 8 bit */
#define OGGPCM_FMT_U8 0x00000001 /* Unsigned integer 8 bit */
#define OGGPCM_FMT_S16_LE 0x00000002 /* Signed integer 16 bit little endian */
#define OGGPCM_FMT_S16_BE 0x00000003 /* Signed integer 16 bit big endian */
#define OGGPCM_FMT_S24_LE 0x00000004 /* Signed integer 24 bit little endian */
#define OGGPCM_FMT_S24_BE 0x00000005 /* Signed integer 24 bit big endian */
#define OGGPCM_FMT_S32_LE 0x00000006 /* Signed integer 32 bit little endian */
#define OGGPCM_FMT_S32_BE 0x00000007 /* Signed integer 32 bit big endian */
#define OGGPCM_FMT_ULAW 0x00000010 /* G.711 u-law encoding (8 bit) */
#define OGGPCM_FMT_ALAW 0x00000011 /* G.711 A-law encoding (8 bit) */
#define OGGPCM_FMT_FLT32_LE 0x00000020 /* IEEE Float [-1,1] 32 bit little endian */
#define OGGPCM_FMT_FLT32_BE 0x00000021 /* IEEE Float [-1,1] 32 bit big endian */
#define OGGPCM_FMT_FLT64_LE 0x00000022 /* IEEE Float [-1,1] 64 bit little endian */
#define OGGPCM_FMT_FLT64_BE 0x00000023 /* IEEE Float [-1,1] 64 bit big endian */
static gboolean
setup_pcm_mapper (GstOggStream * pad, ogg_packet * packet)
{
guint8 *data = packet->packet;
int format;
int channels;
GstCaps *caps;
pad->granulerate_n = GST_READ_UINT32_LE (data + 16);
pad->granulerate_d = 1;
GST_LOG ("sample rate: %d", pad->granulerate_n);
format = GST_READ_UINT32_LE (data + 12);
channels = GST_READ_UINT8 (data + 21);
pad->n_header_packets = 2 + GST_READ_UINT32_LE (data + 24);
if (pad->granulerate_n == 0)
return FALSE;
switch (format) {
case OGGPCM_FMT_S8:
2011-08-19 15:41:22 +00:00
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, "S8", NULL);
break;
case OGGPCM_FMT_U8:
2011-08-19 15:41:22 +00:00
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, "U8", NULL);
break;
case OGGPCM_FMT_S16_LE:
2011-08-19 15:41:22 +00:00
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, "S16LE", NULL);
break;
case OGGPCM_FMT_S16_BE:
2011-08-19 15:41:22 +00:00
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, "S16BE", NULL);
break;
case OGGPCM_FMT_S24_LE:
2011-08-19 15:41:22 +00:00
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, "S24LE", NULL);
break;
case OGGPCM_FMT_S24_BE:
2011-08-19 15:41:22 +00:00
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, "S24BE", NULL);
break;
case OGGPCM_FMT_S32_LE:
2011-08-19 15:41:22 +00:00
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, "S32LE", NULL);
break;
case OGGPCM_FMT_S32_BE:
2011-08-19 15:41:22 +00:00
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, "S32BE", NULL);
break;
case OGGPCM_FMT_ULAW:
2011-10-27 15:26:58 +00:00
caps = gst_caps_new_empty_simple ("audio/x-mulaw");
break;
case OGGPCM_FMT_ALAW:
2011-10-27 15:26:58 +00:00
caps = gst_caps_new_empty_simple ("audio/x-alaw");
break;
case OGGPCM_FMT_FLT32_LE:
2011-08-19 15:41:22 +00:00
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, "F32LE", NULL);
break;
case OGGPCM_FMT_FLT32_BE:
2011-08-19 15:41:22 +00:00
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, "F32BE", NULL);
break;
case OGGPCM_FMT_FLT64_LE:
2011-08-19 15:41:22 +00:00
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, "F64LE", NULL);
break;
case OGGPCM_FMT_FLT64_BE:
2011-08-19 15:41:22 +00:00
caps = gst_caps_new_simple ("audio/x-raw",
"format", G_TYPE_STRING, "F64BE", NULL);
break;
default:
return FALSE;
}
2011-08-19 15:41:22 +00:00
gst_caps_set_simple (caps,
"layout", G_TYPE_STRING, "interleaved",
"rate", G_TYPE_INT, pad->granulerate_n,
"channels", G_TYPE_INT, channels, NULL);
pad->caps = caps;
return TRUE;
}
/* cmml */
static gboolean
setup_cmml_mapper (GstOggStream * pad, ogg_packet * packet)
{
guint8 *data = packet->packet;
pad->granulerate_n = GST_READ_UINT64_LE (data + 12);
pad->granulerate_d = GST_READ_UINT64_LE (data + 20);
pad->granuleshift = data[28];
GST_LOG ("sample rate: %d", pad->granulerate_n);
pad->n_header_packets = 3;
if (pad->granulerate_n == 0)
return FALSE;
data += 4 + (4 + 4 + 4);
GST_DEBUG ("blocksize0: %u", 1 << (data[0] >> 4));
GST_DEBUG ("blocksize1: %u", 1 << (data[0] & 0x0F));
2011-10-27 15:26:58 +00:00
pad->caps = gst_caps_new_empty_simple ("text/x-cmml");
pad->always_flush_page = TRUE;
pad->is_sparse = TRUE;
pad->is_cmml = TRUE;
return TRUE;
}
/* celt */
static gboolean
setup_celt_mapper (GstOggStream * pad, ogg_packet * packet)
{
guint8 *data = packet->packet;
pad->granulerate_n = GST_READ_UINT32_LE (data + 36);
pad->granulerate_d = 1;
pad->granuleshift = 0;
GST_LOG ("sample rate: %d", pad->granulerate_n);
pad->frame_size = GST_READ_UINT32_LE (packet->packet + 44);
pad->n_header_packets = GST_READ_UINT32_LE (packet->packet + 56) + 2;
if (pad->granulerate_n == 0)
return FALSE;
pad->caps = gst_caps_new_simple ("audio/x-celt",
"rate", G_TYPE_INT, pad->granulerate_n, NULL);
return TRUE;
}
/* kate */
static gboolean
setup_kate_mapper (GstOggStream * pad, ogg_packet * packet)
{
guint8 *data = packet->packet;
const char *category;
if (packet->bytes < 64)
return FALSE;
pad->granulerate_n = GST_READ_UINT32_LE (data + 24);
pad->granulerate_d = GST_READ_UINT32_LE (data + 28);
pad->granuleshift = GST_READ_UINT8 (data + 15);
GST_LOG ("sample rate: %d", pad->granulerate_n);
pad->n_header_packets = GST_READ_UINT8 (data + 11);
2010-10-29 10:47:53 +00:00
GST_LOG ("kate header packets: %d", pad->n_header_packets);
if (pad->granulerate_n == 0)
return FALSE;
category = (const char *) data + 48;
if (strcmp (category, "subtitles") == 0 || strcmp (category, "SUB") == 0 ||
strcmp (category, "spu-subtitles") == 0 ||
strcmp (category, "K-SPU") == 0) {
2011-10-27 15:26:58 +00:00
pad->caps = gst_caps_new_empty_simple ("subtitle/x-kate");
} else {
2011-10-27 15:26:58 +00:00
pad->caps = gst_caps_new_empty_simple ("application/x-kate");
}
pad->is_sparse = TRUE;
pad->always_flush_page = TRUE;
return TRUE;
}
static gint64
packet_duration_kate (GstOggStream * pad, ogg_packet * packet)
{
gint64 duration;
if (packet->bytes < 1)
return 0;
switch (packet->packet[0]) {
case 0x00: /* text data */
if (packet->bytes < 1 + 8 * 2) {
duration = 0;
} else {
duration = GST_READ_UINT64_LE (packet->packet + 1 + 8);
if (duration < 0)
duration = 0;
}
break;
default:
duration = GST_CLOCK_TIME_NONE;
break;
}
return duration;
}
static void
extract_tags_kate (GstOggStream * pad, ogg_packet * packet)
{
GstTagList *list = NULL;
if (packet->bytes <= 0)
return;
switch (packet->packet[0]) {
case 0x80:{
const gchar *canonical;
char language[16];
if (packet->bytes < 64) {
GST_WARNING ("Kate ID header packet is less than 64 bytes, ignored");
break;
}
/* the language tag is 16 bytes at offset 32, ensure NUL terminator */
memcpy (language, packet->packet + 32, 16);
language[15] = 0;
/* language is an ISO 639-1 code or RFC 3066 language code, we
* truncate to ISO 639-1 */
g_strdelimit (language, NULL, '\0');
canonical = gst_tag_get_language_code_iso_639_1 (language);
if (canonical) {
list = gst_tag_list_new (GST_TAG_LANGUAGE_CODE, canonical, NULL);
} else {
GST_WARNING ("Unknown or invalid language code %s, ignored", language);
}
break;
}
case 0x81:
tag_list_from_vorbiscomment_packet (packet,
(const guint8 *) "\201kate\0\0\0\0", 9, &list);
break;
default:
break;
}
if (list) {
if (pad->taglist) {
/* ensure the comment packet cannot override the category/language
from the identification header */
gst_tag_list_insert (pad->taglist, list, GST_TAG_MERGE_KEEP_ALL);
2012-01-10 16:57:04 +00:00
gst_tag_list_free (list);
} else
pad->taglist = list;
}
}
2011-11-19 16:06:09 +00:00
/* opus */
static gboolean
setup_opus_mapper (GstOggStream * pad, ogg_packet * packet)
{
if (packet->bytes < 19)
return FALSE;
pad->granulerate_n = 48000;
pad->granulerate_d = 1;
pad->granuleshift = 0;
pad->n_header_packets = 2;
pad->first_granpos = -1;
pad->forbid_start_clamping = TRUE;
2011-11-19 16:06:09 +00:00
/* pre-skip is in samples at 48000 Hz, which matches granule one for one */
pad->granule_offset = -GST_READ_UINT16_LE (packet->packet + 10);
GST_INFO ("Opus has a pre-skip of %" G_GINT64_FORMAT " samples",
-pad->granule_offset);
2011-11-23 10:10:31 +00:00
pad->caps = gst_caps_new_empty_simple ("audio/x-opus");
2011-11-19 16:06:09 +00:00
return TRUE;
}
static gboolean
is_header_opus (GstOggStream * pad, ogg_packet * packet)
{
return packet->bytes >= 8 && !memcmp (packet->packet, "Opus", 4);
}
static gint64
granulepos_to_granule_opus (GstOggStream * pad, gint64 granulepos)
{
if (granulepos == -1)
return -1;
/* We must reject some particular cases for the first granpos */
if (pad->first_granpos < 0 || granulepos < pad->first_granpos)
pad->first_granpos = granulepos;
if (pad->first_granpos == granulepos) {
if (granulepos < -pad->granule_offset) {
GST_ERROR ("Invalid Opus stream: first granulepos (%" G_GINT64_FORMAT
") less than preskip (%" G_GINT64_FORMAT ")", granulepos,
-pad->granule_offset);
return -1;
}
}
return granulepos;
}
2011-11-19 16:06:09 +00:00
static gint64
packet_duration_opus (GstOggStream * pad, ogg_packet * packet)
{
static const guint64 durations[32] = {
480, 960, 1920, 2880, /* Silk NB */
480, 960, 1920, 2880, /* Silk MB */
480, 960, 1920, 2880, /* Silk WB */
480, 960, /* Hybrid SWB */
480, 960, /* Hybrid FB */
120, 240, 480, 960, /* CELT NB */
120, 240, 480, 960, /* CELT NB */
120, 240, 480, 960, /* CELT NB */
120, 240, 480, 960, /* CELT NB */
2011-11-19 16:06:09 +00:00
};
gint64 duration;
gint64 frame_duration;
gint nframes = 0;
2011-11-19 16:06:09 +00:00
guint8 toc;
if (packet->bytes < 1)
return 0;
/* headers */
if (is_header_opus (pad, packet))
return 0;
2011-11-19 16:06:09 +00:00
toc = packet->packet[0];
frame_duration = durations[toc >> 3];
2011-11-19 16:06:09 +00:00
switch (toc & 3) {
case 0:
nframes = 1;
break;
case 1:
nframes = 2;
break;
case 2:
nframes = 2;
break;
case 3:
if (packet->bytes < 2) {
GST_WARNING ("Code 3 Opus packet has less than 2 bytes");
return 0;
}
nframes = packet->packet[1] & 63;
break;
}
duration = nframes * frame_duration;
if (duration > 5760) {
2011-11-19 16:06:09 +00:00
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 / 48.f, nframes, duration / 48.f);
return duration;
2011-11-19 16:06:09 +00:00
}
static void
extract_tags_opus (GstOggStream * pad, ogg_packet * packet)
{
if (packet->bytes >= 8 && memcmp (packet->packet, "OpusTags", 8) == 0) {
tag_list_from_vorbiscomment_packet (packet,
(const guint8 *) "OpusTags", 8, &pad->taglist);
}
}
/* *INDENT-OFF* */
/* indent hates our freedoms */
const GstOggMap mappers[] = {
{
"\200theora", 7, 42,
"video/x-theora",
setup_theora_mapper,
granulepos_to_granule_theora,
granule_to_granulepos_default,
is_granulepos_keyframe_theora,
is_packet_keyframe_theora,
is_header_theora,
packet_duration_constant,
NULL,
extract_tags_theora
},
{
"\001vorbis", 7, 22,
"audio/x-vorbis",
setup_vorbis_mapper,
granulepos_to_granule_default,
granule_to_granulepos_default,
is_granulepos_keyframe_true,
is_packet_keyframe_true,
is_header_vorbis,
packet_duration_vorbis,
NULL,
extract_tags_vorbis
},
{
"Speex", 5, 80,
"audio/x-speex",
setup_speex_mapper,
granulepos_to_granule_default,
granule_to_granulepos_default,
is_granulepos_keyframe_true,
is_packet_keyframe_true,
is_header_count,
packet_duration_constant,
NULL,
extract_tags_count
},
{
"PCM ", 8, 0,
2011-08-19 15:41:22 +00:00
"audio/x-raw",
setup_pcm_mapper,
NULL,
NULL,
NULL,
NULL,
is_header_count,
NULL,
NULL,
NULL
},
{
"CMML\0\0\0\0", 8, 0,
"text/x-cmml",
setup_cmml_mapper,
NULL,
NULL,
NULL,
NULL,
is_header_count,
NULL,
NULL,
NULL
},
{
"Annodex", 7, 0,
"application/x-annodex",
setup_fishead_mapper,
granulepos_to_granule_default,
granule_to_granulepos_default,
NULL,
NULL,
is_header_count,
NULL,
NULL,
NULL
},
{
"fishead", 7, 64,
"application/octet-stream",
setup_fishead_mapper,
NULL,
NULL,
NULL,
NULL,
is_header_true,
NULL,
NULL,
NULL
},
{
"fLaC", 4, 0,
"audio/x-flac",
setup_fLaC_mapper,
granulepos_to_granule_default,
granule_to_granulepos_default,
is_granulepos_keyframe_true,
is_packet_keyframe_true,
2009-12-11 06:47:53 +00:00
is_header_fLaC,
packet_duration_flac,
NULL,
NULL
},
{
"\177FLAC", 5, 36,
"audio/x-flac",
setup_flac_mapper,
granulepos_to_granule_default,
granule_to_granulepos_default,
is_granulepos_keyframe_true,
is_packet_keyframe_true,
is_header_flac,
packet_duration_flac,
NULL,
extract_tags_flac
},
{
"AnxData", 7, 0,
"application/octet-stream",
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
2010-05-05 11:59:57 +00:00
NULL
},
{
"CELT ", 8, 0,
"audio/x-celt",
setup_celt_mapper,
granulepos_to_granule_default,
granule_to_granulepos_default,
NULL,
NULL,
is_header_count,
packet_duration_constant,
NULL,
extract_tags_count
},
{
"\200kate\0\0\0", 8, 0,
"text/x-kate",
setup_kate_mapper,
granulepos_to_granule_default,
granule_to_granulepos_default,
NULL,
NULL,
is_header_count,
packet_duration_kate,
NULL,
extract_tags_kate
},
{
"BBCD\0", 5, 13,
"video/x-dirac",
setup_dirac_mapper,
granulepos_to_granule_dirac,
granule_to_granulepos_dirac,
is_keyframe_dirac,
NULL,
is_header_count,
packet_duration_constant,
granulepos_to_key_granule_dirac,
NULL
},
2010-05-05 11:59:57 +00:00
{
"OVP80\1\1", 7, 4,
2010-05-05 11:59:57 +00:00
"video/x-vp8",
setup_vp8_mapper,
granulepos_to_granule_vp8,
granule_to_granulepos_vp8,
is_keyframe_vp8,
NULL,
2010-05-19 19:35:19 +00:00
is_header_vp8,
packet_duration_vp8,
granulepos_to_key_granule_vp8,
extract_tags_vp8
2010-05-05 11:59:57 +00:00
},
2011-11-19 16:06:09 +00:00
{
"OpusHead", 8, 0,
"audio/x-opus",
setup_opus_mapper,
granulepos_to_granule_opus,
2011-11-19 16:06:09 +00:00
granule_to_granulepos_default,
NULL,
NULL,
is_header_opus,
2011-11-19 16:06:09 +00:00
packet_duration_opus,
NULL,
extract_tags_opus
2011-11-19 16:06:09 +00:00
},
{
"\001audio\0\0\0", 9, 53,
"application/x-ogm-audio",
setup_ogmaudio_mapper,
granulepos_to_granule_default,
granule_to_granulepos_default,
is_granulepos_keyframe_true,
is_packet_keyframe_true,
is_header_ogm,
packet_duration_ogm,
NULL,
NULL
},
{
"\001video\0\0\0", 9, 53,
"application/x-ogm-video",
setup_ogmvideo_mapper,
granulepos_to_granule_default,
granule_to_granulepos_default,
NULL,
NULL,
is_header_ogm,
packet_duration_constant,
NULL,
NULL
},
{
"\001text\0\0\0", 9, 9,
"application/x-ogm-text",
setup_ogmtext_mapper,
granulepos_to_granule_default,
granule_to_granulepos_default,
is_granulepos_keyframe_true,
is_packet_keyframe_true,
is_header_ogm,
packet_duration_ogm,
NULL,
extract_tags_ogm
}
};
/* *INDENT-ON* */
gboolean
gst_ogg_stream_setup_map (GstOggStream * pad, ogg_packet * packet)
{
int i;
gboolean ret;
for (i = 0; i < G_N_ELEMENTS (mappers); i++) {
if (packet->bytes >= mappers[i].min_packet_size &&
packet->bytes >= mappers[i].id_length &&
memcmp (packet->packet, mappers[i].id, mappers[i].id_length) == 0) {
GST_DEBUG ("found mapper for '%s'", mappers[i].id);
if (mappers[i].setup_func)
ret = mappers[i].setup_func (pad, packet);
else
continue;
if (ret) {
GST_DEBUG ("got stream type %" GST_PTR_FORMAT, pad->caps);
pad->map = i;
return TRUE;
} else {
GST_WARNING ("mapper '%s' did not accept setup header",
mappers[i].media_type);
}
}
}
return FALSE;
}
gboolean
gst_ogg_stream_setup_map_from_caps_headers (GstOggStream * pad,
const GstCaps * caps)
{
GstBuffer *buf;
const GstStructure *structure;
const GValue *streamheader;
const GValue *first_element;
ogg_packet packet;
2012-01-20 15:11:54 +00:00
GstMapInfo map;
gboolean ret;
GST_INFO ("Checking streamheader on caps %" GST_PTR_FORMAT, caps);
if (caps == NULL)
return FALSE;
structure = gst_caps_get_structure (caps, 0);
streamheader = gst_structure_get_value (structure, "streamheader");
if (streamheader == NULL) {
GST_LOG ("no streamheader field in caps %" GST_PTR_FORMAT, caps);
return FALSE;
}
if (!GST_VALUE_HOLDS_ARRAY (streamheader)) {
GST_ERROR ("streamheader field not an array, caps: %" GST_PTR_FORMAT, caps);
return FALSE;
}
if (gst_value_array_get_size (streamheader) == 0) {
GST_ERROR ("empty streamheader field in caps %" GST_PTR_FORMAT, caps);
return FALSE;
}
first_element = gst_value_array_get_value (streamheader, 0);
if (!GST_VALUE_HOLDS_BUFFER (first_element)) {
GST_ERROR ("first streamheader not a buffer, caps: %" GST_PTR_FORMAT, caps);
return FALSE;
}
buf = gst_value_get_buffer (first_element);
if (buf == NULL) {
GST_ERROR ("no first streamheader buffer");
return FALSE;
}
2012-01-20 15:11:54 +00:00
if (!gst_buffer_map (buf, &map, GST_MAP_READ) || map.size == 0) {
GST_ERROR ("invalid first streamheader buffer");
return FALSE;
}
2012-01-20 15:11:54 +00:00
GST_MEMDUMP ("streamheader", map.data, map.size);
2012-01-20 15:11:54 +00:00
packet.packet = map.data;
packet.bytes = map.size;
GST_INFO ("Found headers on caps, using those to determine type");
ret = gst_ogg_stream_setup_map (pad, &packet);
2012-01-20 15:11:54 +00:00
gst_buffer_unmap (buf, &map);
return ret;
}