/*****************************************************************************
* meta.c: mp4 meta handling
*****************************************************************************
* Copyright (C) 2001-2004, 2010, 2014 VLC authors and VideoLAN
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "libmp4.h"
#include "../../meta_engine/ID3Genres.h" /* for ATOM_gnre */
#include "languages.h"
#include <vlc_meta.h>
#include <vlc_charset.h>
#include "meta.h"
#include "../../meta_engine/ID3Tag.h"
#include "../../meta_engine/ID3Meta.h"
#include <assert.h>
static const struct
{
const uint32_t xa9_type;
const vlc_meta_type_t meta_type;
} xa9typetometa[] = {
{ ATOM_0x40PRM, vlc_meta_EncodedBy }, /* Adobe Premiere */
{ ATOM_0x40PRQ, vlc_meta_EncodedBy }, /* Adobe Qt */
{ ATOM_0xa9nam, vlc_meta_Title }, /* Full name */
{ ATOM_0xa9aut, vlc_meta_Artist },
{ ATOM_0xa9ART, vlc_meta_Artist },
{ ATOM_0xa9cpy, vlc_meta_Copyright },
{ ATOM_0xa9day, vlc_meta_Date }, /* Creation Date */
{ ATOM_0xa9des, vlc_meta_Description }, /* Description */
{ ATOM_0xa9gen, vlc_meta_Genre }, /* Genre */
{ ATOM_0xa9alb, vlc_meta_Album }, /* Album */
{ ATOM_0xa9trk, vlc_meta_TrackNumber }, /* Track */
{ ATOM_0xa9cmt, vlc_meta_Description }, /* Comment */
{ ATOM_0xa9url, vlc_meta_URL }, /* URL */
{ ATOM_0xa9too, vlc_meta_EncodedBy }, /* Encoder Tool */
{ ATOM_0xa9enc, vlc_meta_EncodedBy }, /* Encoded By */
{ ATOM_0xa9pub, vlc_meta_Publisher },
{ ATOM_0xa9dir, vlc_meta_Director },
{ ATOM_desc, vlc_meta_Description },
{ ATOM_MCPS, vlc_meta_EncodedBy }, /* Cleaner Pro */
{ ATOM_aART, vlc_meta_AlbumArtist },
};
static const struct
{
const uint32_t xa9_type;
const char metadata[26];
} xa9typetoextrameta[] = {
{ ATOM_0xa9wrt, N_("Writer") },
{ ATOM_0xa9com, N_("Composer") },
{ ATOM_0xa9prd, N_("Producer") },
{ ATOM_0xa9inf, N_("Information") },
{ ATOM_0xa9dis, N_("Disclaimer") },
{ ATOM_0xa9req, N_("Requirements") },
{ ATOM_0xa9fmt, N_("Original Format") },
{ ATOM_0xa9dsa, N_("Display Source As") },
{ ATOM_0xa9hst, N_("Host Computer") },
{ ATOM_0xa9prf, N_("Performers") },
{ ATOM_0xa9ope, N_("Original Performer") },
{ ATOM_0xa9src, N_("Providers Source Content") },
{ ATOM_0xa9wrn, N_("Warning") },
{ ATOM_0xa9swr, N_("Software") },
{ ATOM_0xa9lyr, N_("Lyrics") },
{ ATOM_0xa9mak, N_("Record Company") },
{ ATOM_0xa9mod, N_("Model") },
{ ATOM_0xa9PRD, N_("Product") },
{ ATOM_0xa9grp, N_("Grouping") },
{ ATOM_0xa9gen, N_("Genre") },
{ ATOM_0xa9st3, N_("Sub-Title") },
{ ATOM_0xa9arg, N_("Arranger") },
{ ATOM_0xa9ard, N_("Art Director") },
{ ATOM_0xa9cak, N_("Copyright Acknowledgement") },
{ ATOM_0xa9con, N_("Conductor") },
{ ATOM_0xa9des, N_("Song Description") },
{ ATOM_0xa9lnt, N_("Liner Notes") },
{ ATOM_0xa9phg, N_("Phonogram Rights") },
{ ATOM_0xa9pub, N_("Publisher") },
{ ATOM_0xa9sne, N_("Sound Engineer") },
{ ATOM_0xa9sol, N_("Soloist") },
{ ATOM_0xa9thx, N_("Thanks") },
{ ATOM_0xa9xpd, N_("Executive Producer") },
{ ATOM_aART, N_("Album Artist") },
{ ATOM_flvr, N_("Encoding Params") },
{ ATOM_vndr, N_("Vendor") },
{ ATOM_xid_, N_("Catalog Number") },
{ ATOM_gshh, "YouTube Host" },
{ ATOM_gspm, "YouTube Ping Message" },
{ ATOM_gspu, "YouTube Ping Url" },
{ ATOM_gssd, "YouTube Source Data" },
{ ATOM_gsst, "YouTube Start Time" },
{ ATOM_gstd, "YouTube Track Duration" },
{ ATOM_purl, "Podcast URL" },
};
static const struct
{
const char *psz_naming;
const vlc_meta_type_t meta_type;
} com_apple_quicktime_tometa[] = {
{ "displayname", vlc_meta_NowPlaying },
{ "software", vlc_meta_EncodedBy },
{ "Encoded_With", vlc_meta_EncodedBy },
{ "album", vlc_meta_Album },
{ "artist", vlc_meta_Artist },
{ "comment", vlc_meta_Description },
{ "description", vlc_meta_Description },
{ "copyright", vlc_meta_Copyright },
{ "creationdate", vlc_meta_Date },
{ "director", vlc_meta_Director },
{ "genre", vlc_meta_Genre },
{ "publisher", vlc_meta_Publisher },
};
static const struct
{
const char *psz_naming;
const char *psz_metadata;
} com_apple_quicktime_toextrameta[] = {
{ "information", N_("Information") },
{ "keywords", N_("Keywords") },
{ "make", N_("Vendor") },
};
inline static char * StringConvert( const MP4_Box_data_data_t *p_data )
{
if ( !p_data || !p_data->i_blob )
return NULL;
switch( p_data->e_wellknowntype )
{
case DATA_WKT_UTF8:
case DATA_WKT_UTF8_SORT:
return FromCharset( "UTF-8", p_data->p_blob, p_data->i_blob );
case DATA_WKT_UTF16:
case DATA_WKT_UTF16_SORT:
return FromCharset( "UTF-16BE", p_data->p_blob, p_data->i_blob );
case DATA_WKT_SJIS:
return FromCharset( "SHIFT-JIS", p_data->p_blob, p_data->i_blob );
default:
return NULL;
}
}
static char * ExtractString( const MP4_Box_t *p_box )
{
if ( p_box->i_type == ATOM_data )
return StringConvert( p_box->data.p_data );
const MP4_Box_t *p_data = MP4_BoxGet( p_box, "data" );
if ( p_data )
return StringConvert( BOXDATA(p_data) );
else if ( p_box->data.p_binary && p_box->data.p_binary->p_blob )
{
char *psz_utf = strndup( p_box->data.p_binary->p_blob,
p_box->data.p_binary->i_blob );
if (likely( psz_utf ))
EnsureUTF8( psz_utf );
return psz_utf;
}
else
return NULL;
}
static bool AppleNameToMeta( char const* name,
vlc_meta_type_t const** meta_type, char const** meta_key )
{
*meta_type = NULL;
*meta_key = NULL;
for( size_t i = 0; *meta_type == NULL &&
i < ARRAY_SIZE( com_apple_quicktime_tometa ); ++i )
{
if( !strcmp( name, com_apple_quicktime_tometa[i].psz_naming ) )
*meta_type = &com_apple_quicktime_tometa[i].meta_type;
}
for( size_t i = 0; *meta_key == NULL &&
i < ARRAY_SIZE( com_apple_quicktime_toextrameta ); ++i )
{
if( !strcmp( name, com_apple_quicktime_toextrameta[i].psz_naming ) )
*meta_key = com_apple_quicktime_toextrameta[i].psz_metadata;
}
return *meta_type || *meta_key;
}
static bool AtomXA9ToMeta( uint32_t i_type,
vlc_meta_type_t const** meta_type, char const** meta_key )
{
*meta_type = NULL;
*meta_key = NULL;
for( size_t i = 0; !*meta_type && i < ARRAY_SIZE( xa9typetometa ); ++i )
if( xa9typetometa[i].xa9_type == i_type )
*meta_type = &xa9typetometa[i].meta_type;
for( size_t i = 0; !*meta_key && i < ARRAY_SIZE( xa9typetoextrameta ); ++i )
if( xa9typetoextrameta[i].xa9_type == i_type )
*meta_key = xa9typetoextrameta[i].metadata;
return *meta_type || *meta_key;
}
static bool SetMeta( vlc_meta_t* p_meta, int i_type, char const* name, const MP4_Box_t* p_box )
{
vlc_meta_type_t const* type;
char const* key;
if( name != NULL)
{
if ( !AppleNameToMeta( name, &type, &key ) )
return false;
}
else
{
if ( !AtomXA9ToMeta( i_type, &type, &key ) )
return false;
}
char* psz_utf = ExtractString( p_box );
if( psz_utf )
{
if( type ) vlc_meta_Set( p_meta, *type, psz_utf );
else vlc_meta_SetExtra( p_meta, key, psz_utf );
free( psz_utf );
}
return true;
}
static int ExtractIntlStrings( vlc_meta_t *p_meta, const MP4_Box_t *p_box )
{
if( MP4_BoxGet( p_box, "data" ) )
return false;
vlc_meta_type_t const* meta_type;
char const* meta_key;
if( AtomXA9ToMeta( p_box->i_type, &meta_type, &meta_key ) == false )
return false;
if( p_box->p_father == NULL ||
p_box->p_father->i_type != ATOM_udta ||
p_box->data.p_binary == NULL ||
p_box->data.p_binary->p_blob == NULL )
{
return false;
}
vlc_meta_t* p_meta_intl = vlc_meta_New();
if( unlikely( !p_meta_intl ) )
return false;
char const* p_peek = p_box->data.p_binary->p_blob;
uint64_t i_read = p_box->data.p_binary->i_blob;
while( i_read >= 4 )
{
uint16_t i_len = GetWBE( p_peek );
uint16_t i_lang = GetWBE( p_peek + 2 );
p_peek += 4;
i_read -= 4;
if( i_len > i_read )
break;
char charset[15] = "MACINTOSH//";
decodeQtLanguageCode( i_lang, charset+11, &(bool){0} );
if( i_lang >= 0x400 && i_lang != 0x7fff )
{
strcpy( charset, i_len < 2 || memcmp( p_peek, "\xFE\xFF", 2 )
? "UTF-8" : "UTF-16BE" );
}
char* data = FromCharset( charset, p_peek, i_len );
if( data )
{
if( meta_type )
{
vlc_meta_Set( p_meta_intl, *meta_type, data );
meta_key = vlc_meta_TypeToLocalizedString( *meta_type );
meta_type = NULL;
}
else
{
char* key;
if( asprintf( &key, "%s (%s)", meta_key, charset+11 ) != -1 )
{
vlc_meta_SetExtra( p_meta_intl, key, data );
free( key );
}
}
free( data );
}
p_peek += i_len;
i_read -= i_len;
}
if( i_read == 0 )
vlc_meta_Merge( p_meta, p_meta_intl );
vlc_meta_Delete( p_meta_intl );
return i_read == 0;
}
static void ParseTriplet( const MP4_Box_t *p_box,
const char *psz_ns,
const char *psz_key,
void (*pf_callback)(const char *psz_key,
const MP4_Box_data_data_t *, void *),
void *p_priv )
{
size_t i_ns = psz_ns ? strlen(psz_ns) : 0;
size_t i_key = psz_key ? strlen(psz_key) : 0;
const MP4_Box_t *p_mean = MP4_BoxGet( p_box, "mean" );
const MP4_Box_t *p_name = MP4_BoxGet( p_box, "name" );
const MP4_Box_t *p_data = MP4_BoxGet( p_box, "data" );
if( !p_mean || p_mean->data.p_binary->i_blob < 4 + i_ns ||
!p_name || p_name->data.p_binary->i_blob < 4 + i_key ||
!p_data || !BOXDATA(p_data) )
return;
if( p_name->data.p_binary->i_blob - 4 != i_ns &&
strncmp( &((const char*)p_mean->data.p_binary->p_blob)[4], psz_ns, i_ns ) )
return;
if( psz_key != NULL &&
p_name->data.p_binary->i_blob - 4 != i_key &&
strncmp( &((const char*)p_name->data.p_binary->p_blob)[4], psz_key, i_key ) )
return;
char *psz_name = strndup( &((const char*)p_name->data.p_binary->p_blob)[4],
p_name->data.p_binary->i_blob - 4 );
if( psz_name )
pf_callback( psz_name, p_data->data.p_data, p_priv );
free( psz_name );
}
struct itunprivcallbackctx
{
vlc_meta_t *p_meta;
qt_itunes_callback ituncb;
void *priv;
};
static void iTUNTripletCallback( const char *psz_key,
const MP4_Box_data_data_t *p_data, void *priv )
{
struct itunprivcallbackctx *ctx = priv;
if( !strcmp(psz_key, "iTunSMPB") )
{
struct qt_itunes_triplet_data data = {.type = iTunSMPB };
char *psz_val = StringConvert( p_data );
if( psz_val && sscanf(psz_val, "%*"PRIX32" %"PRIX32" %"PRIX32" %"PRIX64,
&data.SMPB.delay, &data.SMPB.padding,
&data.SMPB.original_samplescount) == 3 )
ctx->ituncb( &data, ctx->priv );
free( psz_val );
}
else if( !strcmp(psz_key, "iTunNORM") )
{
struct qt_itunes_triplet_data data = {.type = iTunNORM };
char *psz_val = StringConvert( p_data );
uint32_t values[4];
if( psz_val && sscanf(psz_val, "%"PRIX32" %"PRIX32" %*"PRIX32" %*"PRIX32
"%*"PRIX32" %*"PRIX32" %"PRIX32" %"PRIX32,
&values[0], &values[1], &values[2], &values[3] ) == 4 )
{
values[0] = __MAX(values[0], values[1]); /* left / right volume */
if( values[0] )
data.NORM.volume_adjust = -10.0 * log10( values[0] / 1000.0 );
values[2] = __MAX(values[2], values[3]); /* left / right peak */
if( values[2] )
data.NORM.peak = values[2] / 32768.0;
if( values[0] && values[2] )
ctx->ituncb( &data, ctx->priv );
}
free( psz_val );
}
else if( !strcmp(psz_key, "Encoding Params"))
{
struct qt_itunes_triplet_data data = {.type = iTunEncodingParams,
.EncodingParams = { 0 } };
const uint8_t *p_blob = p_data->p_blob;
size_t i_blob = p_data->i_blob;
while( i_blob >= 8 )
{
const char *id = (const char*) &p_blob[0];
const uint8_t *val = &p_blob[4];
if( !strncmp(id, "brat", 4) )
data.EncodingParams.target_bitrate = GetDWBE(val);
else if( !strncmp(id, "vbrq", 4) )
data.EncodingParams.target_quality = GetDWBE(val);
p_blob += 8; i_blob -= 8;
}
ctx->ituncb( &data, ctx->priv );
}
else if( !strncasecmp(psz_key, "REPLAYGAIN_", 11) )
{
char *psz_val = StringConvert( p_data );
if( psz_val )
vlc_meta_SetExtra( ctx->p_meta, psz_key, psz_val );
free( psz_val );
}
}
static void SetupmdirMeta( vlc_meta_t *p_meta, const MP4_Box_t *p_box, void *priv )
{
const MP4_Box_t *p_data = MP4_BoxGet( p_box, "data" );
if( p_data == NULL || !BOXDATA(p_data) )
{
if( ExtractIntlStrings( p_meta, p_box ) )
return;
SetMeta( p_meta, p_box->i_type, NULL, p_box );
return;
}
/* XXX Becarefull p_udta can have box that are not 0xa9xx */
switch( p_box->i_type )
{
case ATOM_atID:
{
if ( BOXDATA(p_data)->i_blob >= 4 &&
BOXDATA(p_data)->e_wellknowntype == DATA_WKT_BE_SIGNED )
{
char psz_utf[11];
snprintf( psz_utf, sizeof( psz_utf ), "%"PRId32,
GetDWBE(BOXDATA(p_data)->p_blob) );
vlc_meta_SetExtra( p_meta, "iTunes Account ID", psz_utf );
}
break;
}
case ATOM_cnID:
{
if ( BOXDATA(p_data)->i_blob >= 4 &&
BOXDATA(p_data)->e_wellknowntype == DATA_WKT_BE_SIGNED )
{
char psz_utf[11];
snprintf( psz_utf, sizeof( psz_utf ), "%"PRId32,
GetDWBE(BOXDATA(p_data)->p_blob) );
vlc_meta_SetExtra( p_meta, "iTunes Catalog ID", psz_utf );
}
break;
}
case ATOM_cpil:
{
if ( BOXDATA(p_data)->i_blob >= 1 &&
BOXDATA(p_data)->e_wellknowntype == DATA_WKT_BE_SIGNED )
{
vlc_meta_SetCompilation( p_meta,
*BOXDATA(p_data)->p_blob ? "1" : "0" );
}
break;
}
case ATOM_disk:
{
if ( BOXDATA(p_data)->i_blob >= 6 &&
BOXDATA(p_data)->e_wellknowntype == DATA_WKT_RESERVED )
{
char psz_number[6];
snprintf( psz_number, sizeof( psz_number ), "%"PRIu16, GetWBE(&BOXDATA(p_data)->p_blob[2]) );
vlc_meta_Set( p_meta, vlc_meta_DiscNumber, psz_number );
snprintf( psz_number, sizeof( psz_number ), "%"PRIu16, GetWBE(&BOXDATA(p_data)->p_blob[4]) );
vlc_meta_Set( p_meta, vlc_meta_DiscTotal, psz_number );
}
break;
}
case ATOM_gnre:
{
if ( BOXDATA(p_data)->i_blob >= 2 &&
BOXDATA(p_data)->e_wellknowntype == DATA_WKT_RESERVED )
{
const uint16_t i_genre = GetWBE(BOXDATA(p_data)->p_blob);
if( i_genre && i_genre <= ID3_GENRES_COUNT )
vlc_meta_SetGenre( p_meta, ID3_ppsz_genres[i_genre - 1] );
}
break;
}
case ATOM_rtng:
{
if ( BOXDATA(p_data)->i_blob >= 1 )
{
const char *psz_rating;
switch( *BOXDATA(p_data)->p_blob )
{
case 0x4:
psz_rating = N_("Explicit");
break;
case 0x2:
psz_rating = N_("Clean");
break;
default:
case 0x0:
psz_rating = N_("None");
break;
}
vlc_meta_SetExtra( p_meta, N_("Rating"), psz_rating );
}
break;
}
case ATOM_trkn:
{
if ( BOXDATA(p_data)->i_blob >= 4 &&
BOXDATA(p_data)->e_wellknowntype == DATA_WKT_RESERVED )
{
char psz_trck[6];
snprintf( psz_trck, sizeof( psz_trck ), "%"PRIu16, GetWBE(&BOXDATA(p_data)->p_blob[2]) );
vlc_meta_SetTrackNum( p_meta, psz_trck );
if( BOXDATA(p_data)->i_blob >= 8 && GetWBE(&BOXDATA(p_data)->p_blob[4]) )
{
snprintf( psz_trck, sizeof( psz_trck ), "%"PRIu16, GetWBE(&BOXDATA(p_data)->p_blob[4]) );
vlc_meta_Set( p_meta, vlc_meta_TrackTotal, psz_trck );
}
}
break;
}
case ATOM_ITUN:
ParseTriplet( p_box, "com.apple.iTunes", NULL, iTUNTripletCallback, priv );
break;
default:
SetMeta( p_meta, p_box->i_type, NULL, p_box );
break;
}
}
static void SetupmdtaMeta( vlc_meta_t *p_meta, const MP4_Box_t *p_box, const MP4_Box_t *p_keys )
{
if ( !p_keys || !BOXDATA(p_keys) || BOXDATA(p_keys)->i_entry_count == 0 )
return;
if ( !p_box->i_index || p_box->i_index > BOXDATA(p_keys)->i_entry_count )
return;
const char *psz_naming = BOXDATA(p_keys)->p_entries[p_box->i_index - 1].psz_value;
const uint32_t i_namespace = BOXDATA(p_keys)->p_entries[p_box->i_index - 1].i_namespace;
if( i_namespace == HANDLER_mdta )
{
if ( !strncmp( "com.apple.quicktime.", psz_naming, 20 ) )
SetMeta( p_meta, 0, psz_naming + 20, p_box );
}
else if ( i_namespace == ATOM_udta )
{
/* Regular atom inside... could that be even more complex ??? */
char *psz_utf = ExtractString( p_box );
if ( psz_utf )
{
if ( strlen(psz_utf) == 4 )
{
SetMeta( p_meta,
VLC_FOURCC(psz_utf[0],psz_utf[1],psz_utf[2],psz_utf[3]),
NULL, p_box );
}
free( psz_utf );
}
}
}
static int ID3TAG_Parse_Handler( uint32_t i_tag, const uint8_t *p_payload,
size_t i_payload, void *p_priv )
{
vlc_meta_t *p_meta = (vlc_meta_t *) p_priv;
(void) ID3HandleTag( p_payload, i_payload, i_tag, p_meta, NULL );
return VLC_SUCCESS;
}
static void SetupID3v2Meta( vlc_meta_t *p_meta, const MP4_Box_t *p_box )
{
const MP4_Box_t *p_binary = MP4_BoxGet( p_box, "ID32" );
if( p_binary == NULL || !BOXDATA(p_binary) || BOXDATA(p_binary)->i_blob < 6 + 20 + 1 )
return;
/* ID3v2 in 3GPP / ETSI TS 126 244 8.3, Header size 4 + 2 */
ID3TAG_Parse( &((uint8_t *)BOXDATA(p_binary)->p_blob)[6], BOXDATA(p_binary)->i_blob - 6,
ID3TAG_Parse_Handler, p_meta );
}
void SetupMeta( vlc_meta_t *p_meta, const MP4_Box_t *p_udta,
qt_itunes_callback ituncb, void *priv )
{
uint32_t i_handler = 0;
if ( p_udta->p_father )
i_handler = p_udta->i_handler;
for( const MP4_Box_t *p_box = p_udta->p_first; p_box; p_box = p_box->p_next )
{
switch( i_handler )
{
case HANDLER_mdta:
{
const MP4_Box_t *p_keys = MP4_BoxGet( p_udta->p_father, "keys" );
SetupmdtaMeta( p_meta, p_box, p_keys );
break;
}
case HANDLER_ID32:
SetupID3v2Meta( p_meta, p_box );
break;
case HANDLER_mdir:
default:
{
struct itunprivcallbackctx ctx = { .priv = priv, .p_meta = p_meta, .ituncb = ituncb };
SetupmdirMeta( p_meta, p_box, &ctx );
break;
}
}
}
}
¤ Dauer der Verarbeitung: 0.3 Sekunden
(vorverarbeitet am 2026-11-04)
¤
*© Formatika GbR, Deutschland
|
|