gstreamer/subprojects/gst-plugins-good/gst/isomp4/qtdemux_tags.c
Guillaume Desmottes a56923d5e6 qtdemux: fix bug report URL
Using PACKAGE_BUGREPORT as in other modules.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/5762>
2023-12-05 09:25:22 +01:00

1139 lines
35 KiB
C

/* GStreamer
* Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
* Copyright (C) <2003> David A. Schleef <ds@schleef.org>
* Copyright (C) <2006> Wim Taymans <wim@fluendo.com>
* Copyright (C) <2007> Julien Moutte <julien@fluendo.com>
* Copyright (C) <2009> Tim-Philipp Müller <tim centricular net>
* Copyright (C) <2009> STEricsson <benjamin.gaignard@stericsson.com>
* Copyright (C) <2013> Sreerenj Balachandran <sreerenj.balachandran@intel.com>
* Copyright (C) <2013> Intel Corporation
* Copyright (C) <2014> Centricular Ltd
* Copyright (C) <2015> YouView TV Ltd.
* Copyright (C) <2016> British Broadcasting Corporation
*
* 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.
*/
/* Parsing functions for various MP4 standard extension atom groups */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdio.h>
#include <gst/base/gstbytereader.h>
#include <gst/tag/tag.h>
#include "qtdemux_tags.h"
#include "qtdemux_tree.h"
#include "qtdemux_types.h"
#include "qtdemux_debug.h"
#include "fourcc.h"
#define GST_CAT_DEFAULT qtdemux_debug
static GstBuffer *
_gst_buffer_new_wrapped (gpointer mem, gsize size, GFreeFunc free_func)
{
return gst_buffer_new_wrapped_full (free_func ? 0 : GST_MEMORY_FLAG_READONLY,
mem, size, 0, size, mem, free_func);
}
/* check if major or compatible brand is 3GP */
static inline gboolean
qtdemux_is_brand_3gp (GstQTDemux * qtdemux, gboolean major)
{
if (major) {
return ((qtdemux->major_brand & GST_MAKE_FOURCC (255, 255, 0, 0)) ==
FOURCC_3g__);
} else if (qtdemux->comp_brands != NULL) {
GstMapInfo map;
guint8 *data;
gsize size;
gboolean res = FALSE;
gst_buffer_map (qtdemux->comp_brands, &map, GST_MAP_READ);
data = map.data;
size = map.size;
while (size >= 4) {
res = res || ((QT_FOURCC (data) & GST_MAKE_FOURCC (255, 255, 0, 0)) ==
FOURCC_3g__);
data += 4;
size -= 4;
}
gst_buffer_unmap (qtdemux->comp_brands, &map);
return res;
} else {
return FALSE;
}
}
/* check if tag is a spec'ed 3GP tag keyword storing a string */
static inline gboolean
qtdemux_is_string_tag_3gp (GstQTDemux * qtdemux, guint32 fourcc)
{
return fourcc == FOURCC_cprt || fourcc == FOURCC_gnre || fourcc == FOURCC_titl
|| fourcc == FOURCC_dscp || fourcc == FOURCC_perf || fourcc == FOURCC_auth
|| fourcc == FOURCC_albm;
}
static void
qtdemux_tag_add_location (GstQTDemux * qtdemux, GstTagList * taglist,
const char *tag, const char *dummy, GNode * node)
{
const gchar *env_vars[] = { "GST_QT_TAG_ENCODING", "GST_TAG_ENCODING", NULL };
int offset;
char *name;
gchar *data;
gdouble longitude, latitude, altitude;
gint len;
len = QT_UINT32 (node->data);
if (len <= 14)
goto short_read;
data = node->data;
offset = 14;
/* TODO: language code skipped */
name = gst_tag_freeform_string_to_utf8 (data + offset, -1, env_vars);
if (!name) {
/* do not alarm in trivial case, but bail out otherwise */
if (*(data + offset) != 0) {
GST_DEBUG_OBJECT (qtdemux, "failed to convert %s tag to UTF-8, "
"giving up", tag);
}
} else {
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_GEO_LOCATION_NAME, name, NULL);
offset += strlen (name);
g_free (name);
}
if (len < offset + 2 + 4 + 4 + 4)
goto short_read;
/* +1 +1 = skip null-terminator and location role byte */
offset += 1 + 1;
/* table in spec says unsigned, semantics say negative has meaning ... */
longitude = QT_SFP32 (data + offset);
offset += 4;
latitude = QT_SFP32 (data + offset);
offset += 4;
altitude = QT_SFP32 (data + offset);
/* one invalid means all are invalid */
if (longitude >= -180.0 && longitude <= 180.0 &&
latitude >= -90.0 && latitude <= 90.0) {
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_GEO_LOCATION_LATITUDE, latitude,
GST_TAG_GEO_LOCATION_LONGITUDE, longitude,
GST_TAG_GEO_LOCATION_ELEVATION, altitude, NULL);
}
/* TODO: no GST_TAG_, so astronomical body and additional notes skipped */
return;
/* ERRORS */
short_read:
{
GST_DEBUG_OBJECT (qtdemux, "short read parsing 3GP location");
return;
}
}
static void
qtdemux_tag_add_year (GstQTDemux * qtdemux, GstTagList * taglist,
const char *tag, const char *dummy, GNode * node)
{
guint16 y;
GDate *date;
gint len;
len = QT_UINT32 (node->data);
if (len < 14)
return;
y = QT_UINT16 ((guint8 *) node->data + 12);
if (y == 0) {
GST_DEBUG_OBJECT (qtdemux, "year: %u is not a valid year", y);
return;
}
GST_DEBUG_OBJECT (qtdemux, "year: %u", y);
date = g_date_new_dmy (1, 1, y);
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, date, NULL);
g_date_free (date);
}
static void
qtdemux_tag_add_classification (GstQTDemux * qtdemux, GstTagList * taglist,
const char *tag, const char *dummy, GNode * node)
{
int offset;
char *tag_str = NULL;
guint8 *entity;
guint16 table;
gint len;
len = QT_UINT32 (node->data);
if (len <= 20)
goto short_read;
offset = 12;
entity = (guint8 *) node->data + offset;
if (entity[0] == 0 || entity[1] == 0 || entity[2] == 0 || entity[3] == 0) {
GST_DEBUG_OBJECT (qtdemux,
"classification info: %c%c%c%c invalid classification entity",
entity[0], entity[1], entity[2], entity[3]);
return;
}
offset += 4;
table = QT_UINT16 ((guint8 *) node->data + offset);
/* Language code skipped */
offset += 4;
/* Tag format: "XXXX://Y[YYYY]/classification info string"
* XXXX: classification entity, fixed length 4 chars.
* Y[YYYY]: classification table, max 5 chars.
*/
tag_str = g_strdup_printf ("----://%u/%s",
table, (char *) node->data + offset);
/* memcpy To be sure we're preserving byte order */
memcpy (tag_str, entity, 4);
GST_DEBUG_OBJECT (qtdemux, "classification info: %s", tag_str);
gst_tag_list_add (taglist, GST_TAG_MERGE_APPEND, tag, tag_str, NULL);
g_free (tag_str);
return;
/* ERRORS */
short_read:
{
GST_DEBUG_OBJECT (qtdemux, "short read parsing 3GP classification");
return;
}
}
static gboolean
qtdemux_tag_add_str_full (GstQTDemux * qtdemux, GstTagList * taglist,
const char *tag, const char *dummy, GNode * node)
{
const gchar *env_vars[] = { "GST_QT_TAG_ENCODING", "GST_TAG_ENCODING", NULL };
GNode *data;
char *s;
int len;
guint32 type;
int offset;
gboolean ret = TRUE;
const gchar *charset = NULL;
data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
if (data) {
len = QT_UINT32 (data->data);
type = QT_UINT32 ((guint8 *) data->data + 8);
if (type == 0x00000001 && len > 16) {
s = gst_tag_freeform_string_to_utf8 ((char *) data->data + 16, len - 16,
env_vars);
if (s) {
GST_DEBUG_OBJECT (qtdemux, "adding tag %s", GST_STR_NULL (s));
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, s, NULL);
g_free (s);
} else {
GST_DEBUG_OBJECT (qtdemux, "failed to convert %s tag to UTF-8", tag);
}
}
} else {
len = QT_UINT32 (node->data);
type = QT_UINT32 ((guint8 *) node->data + 4);
if ((type >> 24) == 0xa9 && len > 8 + 4) {
gint str_len;
gint lang_code;
/* Type starts with the (C) symbol, so the next data is a list
* of (string size(16), language code(16), string) */
str_len = QT_UINT16 ((guint8 *) node->data + 8);
lang_code = QT_UINT16 ((guint8 *) node->data + 10);
/* the string + fourcc + size + 2 16bit fields,
* means that there are more tags in this atom */
if (len > str_len + 8 + 4) {
/* TODO how to represent the same tag in different languages? */
GST_WARNING_OBJECT (qtdemux, "Ignoring metadata entry with multiple "
"text alternatives, reading only first one");
}
offset = 12;
len = MIN (len, str_len + 8 + 4); /* remove trailing strings that we don't use */
GST_DEBUG_OBJECT (qtdemux, "found international text tag");
if (lang_code < 0x800) { /* MAC encoded string */
charset = "mac";
}
} else if (len > 14 && qtdemux_is_string_tag_3gp (qtdemux,
QT_FOURCC ((guint8 *) node->data + 4))) {
guint32 type = QT_UINT32 ((guint8 *) node->data + 8);
/* we go for 3GP style encoding if major brands claims so,
* or if no hope for data be ok UTF-8, and compatible 3GP brand present */
if (qtdemux_is_brand_3gp (qtdemux, TRUE) ||
(qtdemux_is_brand_3gp (qtdemux, FALSE) &&
((type & 0x00FFFFFF) == 0x0) && (type >> 24 <= 0xF))) {
offset = 14;
/* 16-bit Language code is ignored here as well */
GST_DEBUG_OBJECT (qtdemux, "found 3gpp text tag");
} else {
goto normal;
}
} else {
normal:
offset = 8;
GST_DEBUG_OBJECT (qtdemux, "found normal text tag");
ret = FALSE; /* may have to fallback */
}
if (charset) {
GError *err = NULL;
s = g_convert ((gchar *) node->data + offset, len - offset, "utf8",
charset, NULL, NULL, &err);
if (err) {
GST_DEBUG_OBJECT (qtdemux, "Failed to convert string from charset %s:"
" %s(%d): %s", charset, g_quark_to_string (err->domain), err->code,
err->message);
g_error_free (err);
}
} else {
s = gst_tag_freeform_string_to_utf8 ((char *) node->data + offset,
len - offset, env_vars);
}
if (s) {
GST_DEBUG_OBJECT (qtdemux, "adding tag %s", GST_STR_NULL (s));
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, s, NULL);
g_free (s);
ret = TRUE;
} else {
GST_DEBUG_OBJECT (qtdemux, "failed to convert %s tag to UTF-8", tag);
}
}
return ret;
}
static void
qtdemux_tag_add_str (GstQTDemux * qtdemux, GstTagList * taglist,
const char *tag, const char *dummy, GNode * node)
{
qtdemux_tag_add_str_full (qtdemux, taglist, tag, dummy, node);
}
static void
qtdemux_tag_add_keywords (GstQTDemux * qtdemux, GstTagList * taglist,
const char *tag, const char *dummy, GNode * node)
{
const gchar *env_vars[] = { "GST_QT_TAG_ENCODING", "GST_TAG_ENCODING", NULL };
guint8 *data;
char *s, *t, *k = NULL;
int len;
int offset;
int count;
/* first try normal string tag if major brand not 3GP */
if (!qtdemux_is_brand_3gp (qtdemux, TRUE)) {
if (!qtdemux_tag_add_str_full (qtdemux, taglist, tag, dummy, node)) {
/* hm, that did not work, maybe 3gpp storage in non-3gpp major brand;
* let's try it 3gpp way after minor safety check */
data = node->data;
if (QT_UINT32 (data) < 15 || !qtdemux_is_brand_3gp (qtdemux, FALSE))
return;
} else
return;
}
GST_DEBUG_OBJECT (qtdemux, "found 3gpp keyword tag");
data = node->data;
len = QT_UINT32 (data);
if (len < 15)
goto short_read;
count = QT_UINT8 (data + 14);
offset = 15;
for (; count; count--) {
gint slen;
if (offset + 1 > len)
goto short_read;
slen = QT_UINT8 (data + offset);
offset += 1;
if (offset + slen > len)
goto short_read;
s = gst_tag_freeform_string_to_utf8 ((char *) node->data + offset,
slen, env_vars);
if (s) {
GST_DEBUG_OBJECT (qtdemux, "adding keyword %s", GST_STR_NULL (s));
if (k) {
t = g_strjoin (",", k, s, NULL);
g_free (s);
g_free (k);
k = t;
} else {
k = s;
}
} else {
GST_DEBUG_OBJECT (qtdemux, "failed to convert keyword to UTF-8");
}
offset += slen;
}
done:
if (k) {
GST_DEBUG_OBJECT (qtdemux, "adding tag %s", GST_STR_NULL (k));
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, k, NULL);
}
g_free (k);
return;
/* ERRORS */
short_read:
{
GST_DEBUG_OBJECT (qtdemux, "short read parsing 3GP keywords");
goto done;
}
}
static void
qtdemux_tag_add_num (GstQTDemux * qtdemux, GstTagList * taglist,
const char *tag1, const char *tag2, GNode * node)
{
GNode *data;
int len;
int type;
int n1, n2;
data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
if (data) {
len = QT_UINT32 (data->data);
type = QT_UINT32 ((guint8 *) data->data + 8);
if (type == 0x00000000 && len >= 22) {
n1 = QT_UINT16 ((guint8 *) data->data + 18);
n2 = QT_UINT16 ((guint8 *) data->data + 20);
if (n1 > 0) {
GST_DEBUG_OBJECT (qtdemux, "adding tag %s=%d", tag1, n1);
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag1, n1, NULL);
}
if (n2 > 0) {
GST_DEBUG_OBJECT (qtdemux, "adding tag %s=%d", tag2, n2);
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag2, n2, NULL);
}
}
}
}
static void
qtdemux_tag_add_tmpo (GstQTDemux * qtdemux, GstTagList * taglist,
const char *tag1, const char *dummy, GNode * node)
{
GNode *data;
int len;
int type;
int n1;
data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
if (data) {
len = QT_UINT32 (data->data);
type = QT_UINT32 ((guint8 *) data->data + 8);
GST_DEBUG_OBJECT (qtdemux, "have tempo tag, type=%d,len=%d", type, len);
/* some files wrongly have a type 0x0f=15, but it should be 0x15 */
if ((type == 0x00000015 || type == 0x0000000f) && len >= 18) {
n1 = QT_UINT16 ((guint8 *) data->data + 16);
if (n1) {
/* do not add bpm=0 */
GST_DEBUG_OBJECT (qtdemux, "adding tag %d", n1);
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag1, (gdouble) n1,
NULL);
}
}
}
}
static void
qtdemux_tag_add_uint32 (GstQTDemux * qtdemux, GstTagList * taglist,
const char *tag1, const char *dummy, GNode * node)
{
GNode *data;
int len;
int type;
guint32 num;
data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
if (data) {
len = QT_UINT32 (data->data);
type = QT_UINT32 ((guint8 *) data->data + 8);
GST_DEBUG_OBJECT (qtdemux, "have %s tag, type=%d,len=%d", tag1, type, len);
/* some files wrongly have a type 0x0f=15, but it should be 0x15 */
if ((type == 0x00000015 || type == 0x0000000f) && len >= 20) {
num = QT_UINT32 ((guint8 *) data->data + 16);
if (num) {
/* do not add num=0 */
GST_DEBUG_OBJECT (qtdemux, "adding tag %d", num);
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag1, num, NULL);
}
}
}
}
static void
qtdemux_tag_add_covr (GstQTDemux * qtdemux, GstTagList * taglist,
const char *tag1, const char *dummy, GNode * node)
{
GNode *data;
int len;
int type;
GstSample *sample;
data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
if (data) {
len = QT_UINT32 (data->data);
type = QT_UINT32 ((guint8 *) data->data + 8);
GST_DEBUG_OBJECT (qtdemux, "have covr tag, type=%d,len=%d", type, len);
if ((type == 0x0000000d || type == 0x0000000e) && len > 16) {
GstTagImageType image_type;
if (gst_tag_list_get_tag_size (taglist, GST_TAG_IMAGE) == 0)
image_type = GST_TAG_IMAGE_TYPE_FRONT_COVER;
else
image_type = GST_TAG_IMAGE_TYPE_NONE;
if ((sample =
gst_tag_image_data_to_image_sample ((guint8 *) data->data + 16,
len - 16, image_type))) {
GST_DEBUG_OBJECT (qtdemux, "adding tag size %d", len - 16);
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag1, sample, NULL);
gst_sample_unref (sample);
}
}
}
}
static void
qtdemux_tag_add_date (GstQTDemux * qtdemux, GstTagList * taglist,
const char *tag, const char *dummy, GNode * node)
{
GNode *data;
GstDateTime *datetime = NULL;
char *s;
int len;
int type;
data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
if (data) {
len = QT_UINT32 (data->data);
type = QT_UINT32 ((guint8 *) data->data + 8);
if (type == 0x00000001 && len > 16) {
guint y, m = 1, d = 1;
gint ret;
s = g_strndup ((char *) data->data + 16, len - 16);
GST_DEBUG_OBJECT (qtdemux, "adding date '%s'", s);
datetime = gst_date_time_new_from_iso8601_string (s);
if (datetime != NULL) {
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, GST_TAG_DATE_TIME,
datetime, NULL);
gst_date_time_unref (datetime);
}
ret = sscanf (s, "%u-%u-%u", &y, &m, &d);
if (ret >= 1 && y > 1500 && y < 3000) {
GDate *date;
date = g_date_new_dmy (d, m, y);
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, date, NULL);
g_date_free (date);
} else {
GST_DEBUG_OBJECT (qtdemux, "could not parse date string '%s'", s);
}
g_free (s);
}
}
}
static void
qtdemux_tag_add_gnre (GstQTDemux * qtdemux, GstTagList * taglist,
const char *tag, const char *dummy, GNode * node)
{
GNode *data;
data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
/* re-route to normal string tag if major brand says so
* or no data atom and compatible brand suggests so */
if (qtdemux_is_brand_3gp (qtdemux, TRUE) ||
(qtdemux_is_brand_3gp (qtdemux, FALSE) && !data)) {
qtdemux_tag_add_str (qtdemux, taglist, tag, dummy, node);
return;
}
if (data) {
guint len, type, n;
len = QT_UINT32 (data->data);
type = QT_UINT32 ((guint8 *) data->data + 8);
if (type == 0x00000000 && len >= 18) {
n = QT_UINT16 ((guint8 *) data->data + 16);
if (n > 0) {
const gchar *genre;
genre = gst_tag_id3_genre_get (n - 1);
if (genre != NULL) {
GST_DEBUG_OBJECT (qtdemux, "adding %d [%s]", n, genre);
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, genre, NULL);
}
}
}
}
}
static void
qtdemux_add_double_tag_from_str (GstQTDemux * demux, GstTagList * taglist,
const gchar * tag, guint8 * data, guint32 datasize)
{
gdouble value;
gchar *datacopy;
/* make a copy to have \0 at the end */
datacopy = g_strndup ((gchar *) data, datasize);
/* convert the str to double */
if (sscanf (datacopy, "%lf", &value) == 1) {
GST_DEBUG_OBJECT (demux, "adding tag: %s [%s]", tag, datacopy);
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, value, NULL);
} else {
GST_WARNING_OBJECT (demux, "Failed to parse double from string: %s",
datacopy);
}
g_free (datacopy);
}
static void
qtdemux_tag_add_revdns (GstQTDemux * demux, GstTagList * taglist,
const char *tag, const char *tag_bis, GNode * node)
{
GNode *mean;
GNode *name;
GNode *data;
guint32 meansize;
guint32 namesize;
guint32 datatype;
guint32 datasize;
const gchar *meanstr;
const gchar *namestr;
/* checking the whole ---- atom size for consistency */
if (QT_UINT32 (node->data) <= 4 + 12 + 12 + 16) {
GST_WARNING_OBJECT (demux, "Tag ---- atom is too small, ignoring");
return;
}
mean = qtdemux_tree_get_child_by_type (node, FOURCC_mean);
if (!mean) {
GST_WARNING_OBJECT (demux, "No 'mean' atom found");
return;
}
meansize = QT_UINT32 (mean->data);
if (meansize <= 12) {
GST_WARNING_OBJECT (demux, "Small mean atom, ignoring the whole tag");
return;
}
meanstr = ((gchar *) mean->data) + 12;
meansize -= 12;
name = qtdemux_tree_get_child_by_type (node, FOURCC_name);
if (!name) {
GST_WARNING_OBJECT (demux, "'name' atom not found, ignoring tag");
return;
}
namesize = QT_UINT32 (name->data);
if (namesize <= 12) {
GST_WARNING_OBJECT (demux, "'name' atom is too small, ignoring tag");
return;
}
namestr = ((gchar *) name->data) + 12;
namesize -= 12;
/*
* Data atom is:
* uint32 - size
* uint32 - name
* uint8 - version
* uint24 - data type
* uint32 - all 0
* rest - the data
*/
data = qtdemux_tree_get_child_by_type (node, FOURCC_data);
if (!data) {
GST_WARNING_OBJECT (demux, "No data atom in this tag");
return;
}
datasize = QT_UINT32 (data->data);
if (datasize <= 16) {
GST_WARNING_OBJECT (demux, "Data atom too small");
return;
}
datatype = QT_UINT32 (((gchar *) data->data) + 8) & 0xFFFFFF;
if ((strncmp (meanstr, "com.apple.iTunes", meansize) == 0) ||
(strncmp (meanstr, "org.hydrogenaudio.replaygain", meansize) == 0)) {
static const struct
{
const gchar name[28];
const gchar tag[28];
} tags[] = {
{
"replaygain_track_gain", GST_TAG_TRACK_GAIN}, {
"replaygain_track_peak", GST_TAG_TRACK_PEAK}, {
"replaygain_album_gain", GST_TAG_ALBUM_GAIN}, {
"replaygain_album_peak", GST_TAG_ALBUM_PEAK}, {
"MusicBrainz Track Id", GST_TAG_MUSICBRAINZ_TRACKID}, {
"MusicBrainz Artist Id", GST_TAG_MUSICBRAINZ_ARTISTID}, {
"MusicBrainz Album Id", GST_TAG_MUSICBRAINZ_ALBUMID}, {
"MusicBrainz Album Artist Id", GST_TAG_MUSICBRAINZ_ALBUMARTISTID}
};
int i;
for (i = 0; i < G_N_ELEMENTS (tags); ++i) {
if (!g_ascii_strncasecmp (tags[i].name, namestr, namesize)) {
switch (gst_tag_get_type (tags[i].tag)) {
case G_TYPE_DOUBLE:
qtdemux_add_double_tag_from_str (demux, taglist, tags[i].tag,
((guint8 *) data->data) + 16, datasize - 16);
break;
case G_TYPE_STRING:
qtdemux_tag_add_str (demux, taglist, tags[i].tag, NULL, node);
break;
default:
/* not reached */
break;
}
break;
}
}
/* Some tags might not actually be used for metadata about the media,
* but for other purposes. One such tag is iTunSMPB, which contains
* padding information for gapless playback. Scan these separately. */
if (i == G_N_ELEMENTS (tags)) {
if (!g_ascii_strncasecmp ("iTunSMPB", namestr, 8)) {
/* iTunSMPB tag format goes as follows:
*
* " 00000000 xxxxxxxx yyyyyyyy zzzzzzzzzzzzzzzz 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000"
*
* The data is actually an ASCII string containing these hex fields.
* The description above is _not_ a description of a binary format!
* These need to be parsed with g_ascii_strtoull() and base 16.
*
* (The quotes are not part of it; they just emphasize the
* whitespace at the beginning of the string).
*
* Only the fields marked with x/y/z are of interest here.
*
* The x field is the priming, in samples.
* These are the padding samples at the beginning of the stream.
*
* The y field is the remainder, in samples.
* These are the padding samples at the end of the stream.
*
* The z field is the number of valid PCM frames, excluding the
* priming and remainder. (In other words, the number of PCM
* frames that make up the actual audio, without the padding.)
*
* The data starts at offset 16. All access to it must therefore skip
* the first 16 bytes.
*/
const gsize start_offset = 16;
const gsize priming_offset = start_offset + 10;
const gsize remainder_offset = start_offset + 19;
const gsize num_valid_pcm_frames_offset = start_offset + 28;
const gsize total_length = 44;
const gchar *str;
guint64 priming;
guint64 remainder;
guint64 num_valid_pcm_frames;
/* Temporary buffer for g_ascii_strtoull() calls.
* Add extra +1 space for nullbyte. */
gchar tmp[16 + 1];
/* Use the iTunSMPB info if no other info has been found yet. */
if (demux->gapless_audio_info.type != GAPLESS_AUDIO_INFO_TYPE_NONE) {
GST_DEBUG_OBJECT (demux, "iTunSMPB information found, "
"but other gapless audio info was already read");
goto finish;
}
if (G_UNLIKELY (datasize < (start_offset + total_length))) {
GST_WARNING_OBJECT (demux,
"iTunSMPB tag data size too small - not parsing");
goto finish;
}
str = (gchar *) ((guint8 *) data->data);
#define PARSE_ITUNSMPB_FIELD(FIELD_NAME, NUM_DIGITS) \
G_STMT_START \
{ \
gint str_idx; \
\
for (str_idx = 0; str_idx < (NUM_DIGITS); ++str_idx) { \
gchar ch = str[FIELD_NAME ## _offset + str_idx]; \
if (!g_ascii_isxdigit (ch)) { \
GST_WARNING_OBJECT (demux, #FIELD_NAME " field in iTunSMPB " \
"tag data has invalid character '%c'", ch); \
goto finish; \
} \
tmp[str_idx] = ch; \
} \
tmp[NUM_DIGITS] = 0; \
\
FIELD_NAME = g_ascii_strtoull (tmp, NULL, 16); \
} \
G_STMT_END
PARSE_ITUNSMPB_FIELD (priming, 8);
PARSE_ITUNSMPB_FIELD (remainder, 8);
PARSE_ITUNSMPB_FIELD (num_valid_pcm_frames, 16);
#undef PARSE_ITUNSMPB_FIELD
GST_DEBUG_OBJECT (demux, "iTunSMPB information: priming %"
G_GUINT64_FORMAT " remainder %" G_GUINT64_FORMAT
" num valid PCM frames %" G_GUINT64_FORMAT, priming, remainder,
num_valid_pcm_frames);
demux->gapless_audio_info.type = GAPLESS_AUDIO_INFO_TYPE_ITUNES;
demux->gapless_audio_info.num_start_padding_pcm_frames = priming;
demux->gapless_audio_info.num_end_padding_pcm_frames = remainder;
demux->gapless_audio_info.num_valid_pcm_frames = num_valid_pcm_frames;
} else {
goto unknown_tag;
}
}
} else {
goto unknown_tag;
}
finish:
return;
/* errors */
unknown_tag:
#ifndef GST_DISABLE_GST_DEBUG
{
gchar *namestr_dbg;
gchar *meanstr_dbg;
meanstr_dbg = g_strndup (meanstr, meansize);
namestr_dbg = g_strndup (namestr, namesize);
GST_WARNING_OBJECT (demux, "This tag %s:%s type:%u is not mapped, "
"file a bug at %s", meanstr_dbg, namestr_dbg, datatype,
PACKAGE_BUGREPORT);
g_free (namestr_dbg);
g_free (meanstr_dbg);
}
#endif
return;
}
static void
qtdemux_tag_add_id32 (GstQTDemux * demux, GstTagList * taglist, const char *tag,
const char *tag_bis, GNode * node)
{
guint8 *data;
GstBuffer *buf;
guint len;
GstTagList *id32_taglist = NULL;
GST_LOG_OBJECT (demux, "parsing ID32");
data = node->data;
len = GST_READ_UINT32_BE (data);
/* need at least full box and language tag */
if (len < 12 + 2)
return;
buf = gst_buffer_new_allocate (NULL, len - 14, NULL);
gst_buffer_fill (buf, 0, data + 14, len - 14);
id32_taglist = gst_tag_list_from_id3v2_tag (buf);
if (id32_taglist) {
GST_LOG_OBJECT (demux, "parsing ok");
gst_tag_list_insert (taglist, id32_taglist, GST_TAG_MERGE_KEEP);
gst_tag_list_unref (id32_taglist);
} else {
GST_LOG_OBJECT (demux, "parsing failed");
}
gst_buffer_unref (buf);
}
typedef void (*GstQTDemuxAddTagFunc) (GstQTDemux * demux, GstTagList * taglist,
const char *tag, const char *tag_bis, GNode * node);
/* unmapped tags
FOURCC_pcst -> if media is a podcast -> bool
FOURCC_cpil -> if media is part of a compilation -> bool
FOURCC_pgap -> if media is part of a gapless context -> bool
FOURCC_tven -> the tv episode id e.g. S01E23 -> str
*/
static const struct
{
guint32 fourcc;
const gchar *gst_tag;
const gchar *gst_tag_bis;
const GstQTDemuxAddTagFunc func;
} add_funcs[] = {
{
FOURCC__nam, GST_TAG_TITLE, NULL, qtdemux_tag_add_str}, {
FOURCC_titl, GST_TAG_TITLE, NULL, qtdemux_tag_add_str}, {
FOURCC__grp, GST_TAG_GROUPING, NULL, qtdemux_tag_add_str}, {
FOURCC__wrt, GST_TAG_COMPOSER, NULL, qtdemux_tag_add_str}, {
FOURCC__ART, GST_TAG_ARTIST, NULL, qtdemux_tag_add_str}, {
FOURCC_aART, GST_TAG_ALBUM_ARTIST, NULL, qtdemux_tag_add_str}, {
FOURCC_perf, GST_TAG_ARTIST, NULL, qtdemux_tag_add_str}, {
FOURCC_auth, GST_TAG_COMPOSER, NULL, qtdemux_tag_add_str}, {
FOURCC__alb, GST_TAG_ALBUM, NULL, qtdemux_tag_add_str}, {
FOURCC_albm, GST_TAG_ALBUM, NULL, qtdemux_tag_add_str}, {
FOURCC_cprt, GST_TAG_COPYRIGHT, NULL, qtdemux_tag_add_str}, {
FOURCC__cpy, GST_TAG_COPYRIGHT, NULL, qtdemux_tag_add_str}, {
FOURCC__cmt, GST_TAG_COMMENT, NULL, qtdemux_tag_add_str}, {
FOURCC__des, GST_TAG_DESCRIPTION, NULL, qtdemux_tag_add_str}, {
FOURCC_desc, GST_TAG_DESCRIPTION, NULL, qtdemux_tag_add_str}, {
FOURCC_dscp, GST_TAG_DESCRIPTION, NULL, qtdemux_tag_add_str}, {
FOURCC__lyr, GST_TAG_LYRICS, NULL, qtdemux_tag_add_str}, {
FOURCC__day, GST_TAG_DATE, NULL, qtdemux_tag_add_date}, {
FOURCC_yrrc, GST_TAG_DATE, NULL, qtdemux_tag_add_year}, {
FOURCC__too, GST_TAG_ENCODER, NULL, qtdemux_tag_add_str}, {
FOURCC__inf, GST_TAG_COMMENT, NULL, qtdemux_tag_add_str}, {
FOURCC_trkn, GST_TAG_TRACK_NUMBER, GST_TAG_TRACK_COUNT,
qtdemux_tag_add_num}, {
FOURCC_disk, GST_TAG_ALBUM_VOLUME_NUMBER, GST_TAG_ALBUM_VOLUME_COUNT,
qtdemux_tag_add_num}, {
FOURCC_disc, GST_TAG_ALBUM_VOLUME_NUMBER, GST_TAG_ALBUM_VOLUME_COUNT,
qtdemux_tag_add_num}, {
FOURCC__gen, GST_TAG_GENRE, NULL, qtdemux_tag_add_str}, {
FOURCC_gnre, GST_TAG_GENRE, NULL, qtdemux_tag_add_gnre}, {
FOURCC_tmpo, GST_TAG_BEATS_PER_MINUTE, NULL, qtdemux_tag_add_tmpo}, {
FOURCC_covr, GST_TAG_IMAGE, NULL, qtdemux_tag_add_covr}, {
FOURCC_sonm, GST_TAG_TITLE_SORTNAME, NULL, qtdemux_tag_add_str}, {
FOURCC_soal, GST_TAG_ALBUM_SORTNAME, NULL, qtdemux_tag_add_str}, {
FOURCC_soar, GST_TAG_ARTIST_SORTNAME, NULL, qtdemux_tag_add_str}, {
FOURCC_soaa, GST_TAG_ALBUM_ARTIST_SORTNAME, NULL, qtdemux_tag_add_str}, {
FOURCC_soco, GST_TAG_COMPOSER_SORTNAME, NULL, qtdemux_tag_add_str}, {
FOURCC_sosn, GST_TAG_SHOW_SORTNAME, NULL, qtdemux_tag_add_str}, {
FOURCC_tvsh, GST_TAG_SHOW_NAME, NULL, qtdemux_tag_add_str}, {
FOURCC_tvsn, GST_TAG_SHOW_SEASON_NUMBER, NULL, qtdemux_tag_add_uint32}, {
FOURCC_tves, GST_TAG_SHOW_EPISODE_NUMBER, NULL, qtdemux_tag_add_uint32}, {
FOURCC_kywd, GST_TAG_KEYWORDS, NULL, qtdemux_tag_add_keywords}, {
FOURCC_keyw, GST_TAG_KEYWORDS, NULL, qtdemux_tag_add_str}, {
FOURCC__enc, GST_TAG_ENCODER, NULL, qtdemux_tag_add_str}, {
FOURCC_loci, GST_TAG_GEO_LOCATION_NAME, NULL, qtdemux_tag_add_location}, {
FOURCC_clsf, GST_QT_DEMUX_CLASSIFICATION_TAG, NULL,
qtdemux_tag_add_classification}, {
FOURCC__mak, GST_TAG_DEVICE_MANUFACTURER, NULL, qtdemux_tag_add_str}, {
FOURCC__mod, GST_TAG_DEVICE_MODEL, NULL, qtdemux_tag_add_str}, {
FOURCC__swr, GST_TAG_APPLICATION_NAME, NULL, qtdemux_tag_add_str}, {
/* This is a special case, some tags are stored in this
* 'reverse dns naming', according to:
* http://atomicparsley.sourceforge.net/mpeg-4files.html and
* bug #614471
*/
FOURCC_____, "", NULL, qtdemux_tag_add_revdns}, {
/* see http://www.mp4ra.org/specs.html for ID32 in meta box */
FOURCC_ID32, "", NULL, qtdemux_tag_add_id32}
};
struct _GstQtDemuxTagList
{
GstQTDemux *demux;
GstTagList *taglist;
};
typedef struct _GstQtDemuxTagList GstQtDemuxTagList;
static void
qtdemux_tag_add_blob (GNode * node, GstQtDemuxTagList * qtdemuxtaglist)
{
gint len;
guint8 *data;
GstBuffer *buf;
gchar *media_type;
const gchar *style;
GstSample *sample;
GstStructure *s;
guint i;
guint8 ndata[4];
GstQTDemux *demux = qtdemuxtaglist->demux;
GstTagList *taglist = qtdemuxtaglist->taglist;
data = node->data;
len = QT_UINT32 (data);
buf = gst_buffer_new_and_alloc (len);
gst_buffer_fill (buf, 0, data, len);
/* heuristic to determine style of tag */
if (QT_FOURCC (data + 4) == FOURCC_____ ||
(len > 8 + 12 && QT_FOURCC (data + 12) == FOURCC_data))
style = "itunes";
else if (demux->major_brand == FOURCC_qt__)
style = "quicktime";
/* fall back to assuming iso/3gp tag style */
else
style = "iso";
/* sanitize the name for the caps. */
for (i = 0; i < 4; i++) {
guint8 d = data[4 + i];
if (g_ascii_isalnum (d))
ndata[i] = g_ascii_tolower (d);
else
ndata[i] = '_';
}
media_type = g_strdup_printf ("application/x-gst-qt-%c%c%c%c-tag",
ndata[0], ndata[1], ndata[2], ndata[3]);
GST_DEBUG_OBJECT (demux, "media type %s", media_type);
s = gst_structure_new (media_type, "style", G_TYPE_STRING, style, NULL);
sample = gst_sample_new (buf, NULL, NULL, s);
gst_buffer_unref (buf);
g_free (media_type);
GST_DEBUG_OBJECT (demux, "adding private tag; size %d, info %" GST_PTR_FORMAT,
len, s);
gst_tag_list_add (taglist, GST_TAG_MERGE_APPEND,
GST_QT_DEMUX_PRIVATE_TAG, sample, NULL);
gst_sample_unref (sample);
}
void
qtdemux_parse_udta (GstQTDemux * qtdemux, GstTagList * taglist, GNode * udta)
{
GNode *meta;
GNode *ilst;
GNode *xmp_;
GNode *node;
gint i;
GstQtDemuxTagList demuxtaglist;
demuxtaglist.demux = qtdemux;
demuxtaglist.taglist = taglist;
meta = qtdemux_tree_get_child_by_type (udta, FOURCC_meta);
if (meta != NULL) {
ilst = qtdemux_tree_get_child_by_type (meta, FOURCC_ilst);
if (ilst == NULL) {
GST_LOG_OBJECT (qtdemux, "no ilst");
return;
}
} else {
ilst = udta;
GST_LOG_OBJECT (qtdemux, "no meta so using udta itself");
}
i = 0;
while (i < G_N_ELEMENTS (add_funcs)) {
node = qtdemux_tree_get_child_by_type (ilst, add_funcs[i].fourcc);
if (node) {
gint len;
len = QT_UINT32 (node->data);
if (len < 12) {
GST_DEBUG_OBJECT (qtdemux, "too small tag atom %" GST_FOURCC_FORMAT,
GST_FOURCC_ARGS (add_funcs[i].fourcc));
} else {
add_funcs[i].func (qtdemux, taglist, add_funcs[i].gst_tag,
add_funcs[i].gst_tag_bis, node);
}
g_node_destroy (node);
} else {
i++;
}
}
/* parsed nodes have been removed, pass along remainder as blob */
g_node_children_foreach (ilst, G_TRAVERSE_ALL,
(GNodeForeachFunc) qtdemux_tag_add_blob, &demuxtaglist);
/* parse up XMP_ node if existing */
xmp_ = qtdemux_tree_get_child_by_type (udta, FOURCC_XMP_);
if (xmp_ != NULL) {
GstBuffer *buf;
GstTagList *xmptaglist;
buf = _gst_buffer_new_wrapped (((guint8 *) xmp_->data) + 8,
QT_UINT32 ((guint8 *) xmp_->data) - 8, NULL);
xmptaglist = gst_tag_list_from_xmp_buffer (buf);
gst_buffer_unref (buf);
qtdemux_handle_xmp_taglist (qtdemux, taglist, xmptaglist);
} else {
GST_DEBUG_OBJECT (qtdemux, "No XMP_ node found");
}
}
void
qtdemux_handle_xmp_taglist (GstQTDemux * qtdemux, GstTagList * taglist,
GstTagList * xmptaglist)
{
/* Strip out bogus fields */
if (xmptaglist) {
if (gst_tag_list_get_scope (taglist) == GST_TAG_SCOPE_GLOBAL) {
gst_tag_list_remove_tag (xmptaglist, GST_TAG_VIDEO_CODEC);
gst_tag_list_remove_tag (xmptaglist, GST_TAG_AUDIO_CODEC);
} else {
gst_tag_list_remove_tag (xmptaglist, GST_TAG_CONTAINER_FORMAT);
}
GST_DEBUG_OBJECT (qtdemux, "Found XMP tags %" GST_PTR_FORMAT, xmptaglist);
/* prioritize native tags using _KEEP mode */
gst_tag_list_insert (taglist, xmptaglist, GST_TAG_MERGE_KEEP);
gst_tag_list_unref (xmptaglist);
}
}