From patchwork Tue Dec 8 19:35:16 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: zsugabubus X-Patchwork-Id: 24437 Return-Path: X-Original-To: patchwork@ffaux-bg.ffmpeg.org Delivered-To: patchwork@ffaux-bg.ffmpeg.org Received: from ffbox0-bg.mplayerhq.hu (ffbox0-bg.ffmpeg.org [79.124.17.100]) by ffaux.localdomain (Postfix) with ESMTP id 3760844A999 for ; Tue, 8 Dec 2020 21:35:39 +0200 (EET) Received: from [127.0.1.1] (localhost [127.0.0.1]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTP id 07F18687FA3; Tue, 8 Dec 2020 21:35:39 +0200 (EET) X-Original-To: ffmpeg-devel@ffmpeg.org Delivered-To: ffmpeg-devel@ffmpeg.org Received: from mail.cock.li (mail.cock.li [37.120.193.124]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTPS id A199A680795 for ; Tue, 8 Dec 2020 21:35:31 +0200 (EET) Date: Tue, 8 Dec 2020 20:35:16 +0100 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=national.shitposting.agency; s=mail; t=1607456122; bh=N7jwbhuMDsfaGhGgv1Ich5TSSFEL3M2jxHYcQkL5Dk4=; h=Date:From:To:Subject:From; b=DxddSjSI+KblsFIOo6BMvMnqO+J5JuOIWvHcAZS7BoFUnnY1r+EEvbK60FE6217BT xikYK9tRqPxq99fzhsizkDMGLNQlxwDfdd+pRn1NFjp8cdH7kF2sDcseNG0Z7d9vlu cD4r69YgipNwTnKNkndAFJb/RWEAdoj9DcLbEMBtnOZSq2P1513RMrRh1/sIV9C+HA W1KSPbInSwDUsXcN//yqAK9/xPi4+UoAVnmSul0WByDO18+6Any6vbI40AfZzMTHCM 0ZmtSm6CMIxmCenbN7BnmRv4ENtGRCUYPJExjr1vjOxujAJ0vx0gWBCIJw1q6vq6MZ tlTiAcBK+tUjg== From: zsugabubus@national.shitposting.agency To: ffmpeg-devel@ffmpeg.org Message-ID: <20201208193516.2m4cnuhigjwowonh@localhost> MIME-Version: 1.0 Content-Disposition: inline Subject: [FFmpeg-devel] [PATCH] avformat/matroska: Handle TargetType and nested SimpleTags in metadata X-BeenThere: ffmpeg-devel@ffmpeg.org X-Mailman-Version: 2.1.20 Precedence: list List-Id: FFmpeg development discussions and patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: FFmpeg development discussions and patches Errors-To: ffmpeg-devel-bounces@ffmpeg.org Sender: "ffmpeg-devel" Metadata names now recognized in the following format: [ [TargetTypeValue][TargetType] "/" ] TagName [ "/" TagName ]... [ "@" [ "-" ] [TagLanguage] ] Language designators separated with "@" instead of "-" to avoid future ambiguity about IETF language tags, like "en-US" and similar. "-" designates TagDefault := 0. Signed-off-by: zsugabubus Signed-off-by: zsugabubus --- libavformat/matroska.c | 56 ++++++- libavformat/matroska.h | 9 ++ libavformat/matroskadec.c | 286 +++++++++++++++++++++-------------- libavformat/matroskaenc.c | 308 ++++++++++++++++++++++++++------------ 4 files changed, 449 insertions(+), 210 deletions(-) diff --git a/libavformat/matroska.c b/libavformat/matroska.c index 7c56aba..4fcade2 100644 --- a/libavformat/matroska.c +++ b/libavformat/matroska.c @@ -120,11 +120,63 @@ const CodecTags ff_webm_codec_tags[] = { }; const AVMetadataConv ff_mkv_metadata_conv[] = { - { "LEAD_PERFORMER", "performer" }, - { "PART_NUMBER" , "track" }, + { "TRACK/ARTIST", "artist" }, + { "ALBUM/ARTIST", "album_artist" }, + { "ALBUM/TITLE", "album" }, + { "ALBUM/DATE_RELEASED", "date" }, + { "TRACK/TITLE", "title" }, + { "ALBUM/PART_NUMBER", "track" }, + { "ALBUM/TOTAL_PARTS", "track_total" }, + { "VOLUME/PART_NUMBER", "disc" }, + { "VOLUME/TOTAL_PARTS", "disc_total" }, + { "TRACK/GENRE", "genre" }, + + { "50/COMMENT", "comment" }, + { "ALBUM/COMMENT", "comment" }, + { "EPISODE/COMMENT", "comment" }, { 0 } }; +#define A (1 << AVMEDIA_TYPE_AUDIO) +#define V (1 << AVMEDIA_TYPE_VIDEO) +#define AV A | V + +/* Note: When converting from typevalues -> strings first matching entry is used. */ +const TypeValueConv ff_mkv_typevalue_conv[] = { + { "COLLECTION", 70, AV }, + + { "SEASON", 60, V }, + { "VOLUME", 60, AV }, + { "EDITION", 60, A }, + { "ISSUE", 60, A }, + { "OPUS", 60, A }, + { "SEQUEL", 60, V }, + + { "ALBUM", 50, A }, + { "EPISODE", 50, V }, + { "OPERA", 50, A }, + { "MOVIE", 50, V }, + { "CONCERT", 50, AV }, + + { "PART", 40, AV }, + { "SESSION", 40, AV }, + + { "TRACK", 30, A }, + { "CHAPTER", 30, V }, + { "SONG", 30, A }, + + { "SUBTRACK", 20, A }, + { "MOVEMENT", 20, A }, + { "SCENE", 20, V }, + + { "SHOT", 10, V }, + { "", 0, -1 } +}; + +#undef A +#undef V +#undef AV + const char * const ff_matroska_video_stereo_mode[MATROSKA_VIDEO_STEREOMODE_TYPE_NB] = { "mono", "left_right", diff --git a/libavformat/matroska.h b/libavformat/matroska.h index 6f198f0..6dc9582 100644 --- a/libavformat/matroska.h +++ b/libavformat/matroska.h @@ -355,14 +355,23 @@ typedef struct CodecTags{ enum AVCodecID id; }CodecTags; +typedef struct { + char str[sizeof "COLLECTION"]; + uint8_t typevalue; + uint8_t codec_types; /* (1 << AVMEDIA_TYPE_*)... */ +} TypeValueConv; + /* max. depth in the EBML tree structure */ #define EBML_MAX_DEPTH 16 #define MATROSKA_VIDEO_STEREO_PLANE_COUNT 3 +enum { MATROSKA_DEFAULT_TAGTARGETS_TYPEVALUE = 50 }; + extern const CodecTags ff_mkv_codec_tags[]; extern const CodecTags ff_webm_codec_tags[]; extern const AVMetadataConv ff_mkv_metadata_conv[]; +extern const TypeValueConv ff_mkv_typevalue_conv[]; extern const char * const ff_matroska_video_stereo_mode[MATROSKA_VIDEO_STEREOMODE_TYPE_NB]; extern const char * const ff_matroska_video_stereo_plane[MATROSKA_VIDEO_STEREO_PLANE_COUNT]; diff --git a/libavformat/matroskadec.c b/libavformat/matroskadec.c index 5455594..1c0478d 100644 --- a/libavformat/matroskadec.c +++ b/libavformat/matroskadec.c @@ -98,8 +98,8 @@ typedef enum { typedef const struct EbmlSyntax { uint32_t id; EbmlType type; - size_t list_elem_size; - size_t data_offset; + unsigned list_elem_size; + unsigned data_offset; union { int64_t i; uint64_t u; @@ -228,8 +228,8 @@ typedef struct MatroskaTrackOperation { } MatroskaTrackOperation; typedef struct MatroskaTrack { + uint64_t uid; /* Must be the first field. */ uint64_t num; - uint64_t uid; uint64_t type; char *name; char *codec_id; @@ -258,7 +258,7 @@ typedef struct MatroskaTrack { } MatroskaTrack; typedef struct MatroskaAttachment { - uint64_t uid; + uint64_t uid; /* Must be the first field. */ char *filename; char *description; char *mime; @@ -268,9 +268,9 @@ typedef struct MatroskaAttachment { } MatroskaAttachment; typedef struct MatroskaChapter { + uint64_t uid; /* Must be the first field. */ uint64_t start; uint64_t end; - uint64_t uid; char *title; AVChapter *chapter; @@ -286,26 +286,26 @@ typedef struct MatroskaIndex { EbmlList pos; } MatroskaIndex; -typedef struct MatroskaTag { +typedef struct MatroskaSimpleTag { char *name; char *string; char *lang; uint64_t def; EbmlList sub; -} MatroskaTag; +} MatroskaSimpleTag; typedef struct MatroskaTagTarget { char *type; uint64_t typevalue; - uint64_t trackuid; - uint64_t chapteruid; - uint64_t attachuid; + EbmlList trackuids; + EbmlList chapteruids; + EbmlList attachuids; } MatroskaTagTarget; -typedef struct MatroskaTags { +typedef struct MatroskaTag { MatroskaTagTarget target; - EbmlList tag; -} MatroskaTags; + EbmlList simpletags; +} MatroskaTag; typedef struct MatroskaSeekhead { uint64_t id; @@ -652,32 +652,32 @@ static EbmlSyntax matroska_index[] = { }; static EbmlSyntax matroska_simpletag[] = { - { MATROSKA_ID_TAGNAME, EBML_UTF8, 0, offsetof(MatroskaTag, name) }, - { MATROSKA_ID_TAGSTRING, EBML_UTF8, 0, offsetof(MatroskaTag, string) }, - { MATROSKA_ID_TAGLANG, EBML_STR, 0, offsetof(MatroskaTag, lang), { .s = "und" } }, - { MATROSKA_ID_TAGDEFAULT, EBML_UINT, 0, offsetof(MatroskaTag, def) }, - { MATROSKA_ID_TAGDEFAULT_BUG, EBML_UINT, 0, offsetof(MatroskaTag, def) }, - { MATROSKA_ID_SIMPLETAG, EBML_NEST, sizeof(MatroskaTag), offsetof(MatroskaTag, sub), { .n = matroska_simpletag } }, + { MATROSKA_ID_TAGNAME, EBML_UTF8, 0, offsetof(MatroskaSimpleTag, name) }, + { MATROSKA_ID_TAGSTRING, EBML_UTF8, 0, offsetof(MatroskaSimpleTag, string) }, + { MATROSKA_ID_TAGLANG, EBML_STR, 0, offsetof(MatroskaSimpleTag, lang), { .s = "und" } }, + { MATROSKA_ID_TAGDEFAULT, EBML_UINT, 0, offsetof(MatroskaSimpleTag, def), { .u = 1 } }, + { MATROSKA_ID_TAGDEFAULT_BUG, EBML_UINT, 0, offsetof(MatroskaSimpleTag, def), { .u = 1 } }, + { MATROSKA_ID_SIMPLETAG, EBML_NEST, sizeof(MatroskaSimpleTag), offsetof(MatroskaSimpleTag, sub), { .n = matroska_simpletag } }, CHILD_OF(matroska_tag) }; static EbmlSyntax matroska_tagtargets[] = { - { MATROSKA_ID_TAGTARGETS_TYPE, EBML_STR, 0, offsetof(MatroskaTagTarget, type) }, - { MATROSKA_ID_TAGTARGETS_TYPEVALUE, EBML_UINT, 0, offsetof(MatroskaTagTarget, typevalue), { .u = 50 } }, - { MATROSKA_ID_TAGTARGETS_TRACKUID, EBML_UINT, 0, offsetof(MatroskaTagTarget, trackuid) }, - { MATROSKA_ID_TAGTARGETS_CHAPTERUID, EBML_UINT, 0, offsetof(MatroskaTagTarget, chapteruid) }, - { MATROSKA_ID_TAGTARGETS_ATTACHUID, EBML_UINT, 0, offsetof(MatroskaTagTarget, attachuid) }, + { MATROSKA_ID_TAGTARGETS_TYPE, EBML_STR, 0, offsetof(MatroskaTagTarget, type) }, + { MATROSKA_ID_TAGTARGETS_TYPEVALUE, EBML_UINT, 0, offsetof(MatroskaTagTarget, typevalue), { .u = MATROSKA_DEFAULT_TAGTARGETS_TYPEVALUE } }, + { MATROSKA_ID_TAGTARGETS_TRACKUID, EBML_UINT, sizeof(uint64_t), offsetof(MatroskaTagTarget, trackuids) }, + { MATROSKA_ID_TAGTARGETS_CHAPTERUID, EBML_UINT, sizeof(uint64_t), offsetof(MatroskaTagTarget, chapteruids) }, + { MATROSKA_ID_TAGTARGETS_ATTACHUID, EBML_UINT, sizeof(uint64_t), offsetof(MatroskaTagTarget, attachuids) }, CHILD_OF(matroska_tag) }; static EbmlSyntax matroska_tag[] = { - { MATROSKA_ID_SIMPLETAG, EBML_NEST, sizeof(MatroskaTag), offsetof(MatroskaTags, tag), { .n = matroska_simpletag } }, - { MATROSKA_ID_TAGTARGETS, EBML_NEST, 0, offsetof(MatroskaTags, target), { .n = matroska_tagtargets } }, + { MATROSKA_ID_SIMPLETAG, EBML_NEST, sizeof(MatroskaSimpleTag), offsetof(MatroskaTag, simpletags), { .n = matroska_simpletag } }, + { MATROSKA_ID_TAGTARGETS, EBML_NEST, 0, offsetof(MatroskaTag, target), { .n = matroska_tagtargets } }, CHILD_OF(matroska_tags) }; static EbmlSyntax matroska_tags[] = { - { MATROSKA_ID_TAG, EBML_NEST, sizeof(MatroskaTags), offsetof(MatroskaDemuxContext, tags), { .n = matroska_tag } }, + { MATROSKA_ID_TAG, EBML_NEST, sizeof(MatroskaTag), offsetof(MatroskaDemuxContext, tags), { .n = matroska_tag } }, CHILD_OF(matroska_segment) }; @@ -1719,103 +1719,169 @@ failed: return result; } -static void matroska_convert_tag(AVFormatContext *s, EbmlList *list, - AVDictionary **metadata, char *prefix) +static void matroska_get_targettype_string(char *key, unsigned *key_ptr, unsigned key_size, + MatroskaTag const *tag, enum AVMediaType codec_type) { - MatroskaTag *tags = list->elem; - char key[1024]; - int i; + for (const TypeValueConv *conv = ff_mkv_typevalue_conv; + tag->target.typevalue <= (uint64_t)conv->typevalue; + ++conv) + if (tag->target.typevalue == (uint64_t)conv->typevalue && + (codec_type == AVMEDIA_TYPE_UNKNOWN || (conv->codec_types & (1 << codec_type))) && + (!tag->target.type || !av_strcasecmp(tag->target.type, conv->str))) + { + *key_ptr = strlen(conv->str); + memcpy(key, conv->str, sizeof conv->str); + return; + } - for (i = 0; i < list->nb_elem; i++) { - const char *lang = tags[i].lang && - strcmp(tags[i].lang, "und") ? tags[i].lang : NULL; + *key_ptr = snprintf(key, key_size, "%"PRIu64"%s", + tag->target.typevalue, tag->target.type); + if (key_size < *key_ptr) + *key_ptr = key_size; +} - if (!tags[i].name) { - av_log(s, AV_LOG_WARNING, "Skipping invalid tag with no TagName.\n"); +static void matroska_convert_simpletags(AVFormatContext *s, const EbmlList *simpletag_list, + AVDictionary **metadata, + char *key, unsigned base_key_ptr, unsigned key_size) +{ + MatroskaSimpleTag *simpletags = simpletag_list->elem; + + for (int i = 0; i < simpletag_list->nb_elem; i++) { + MatroskaSimpleTag *simpletag = &simpletags[i]; + const char *lang = simpletag->lang && + strcmp(simpletag->lang, "und") ? simpletag->lang : NULL; + unsigned key_ptr; + + if (!simpletag->name) { + av_log(s, AV_LOG_WARNING, + "Skipping invalid Tag with no TagName.\n"); continue; } - if (prefix) - snprintf(key, sizeof(key), "%s/%s", prefix, tags[i].name); - else - av_strlcpy(key, tags[i].name, sizeof(key)); - if (tags[i].def || !lang) { - av_dict_set(metadata, key, tags[i].string, 0); - if (tags[i].sub.nb_elem) - matroska_convert_tag(s, &tags[i].sub, metadata, key); - } - if (lang) { - av_strlcat(key, "-", sizeof(key)); - av_strlcat(key, lang, sizeof(key)); - av_dict_set(metadata, key, tags[i].string, 0); - if (tags[i].sub.nb_elem) - matroska_convert_tag(s, &tags[i].sub, metadata, key); + + key_ptr = base_key_ptr + + snprintf(key + base_key_ptr, key_size - base_key_ptr, "%s%s", + base_key_ptr ? "/" : "", simpletag->name); + if (key_size <= key_ptr) { + av_log(s, AV_LOG_WARNING, + "Skipping SimpleTag with path larger than %u bytes.\n", + key_size); + return; } + + if (lang) + snprintf(key + key_ptr, key_size - key_ptr, "@%s%s", + !simpletag->def ? "-" : "", lang); + av_dict_set(metadata, key, simpletag->string, AV_DICT_MULTIKEY); + + matroska_convert_simpletags(s, &simpletag->sub, metadata, + key, key_ptr, key_size); } +} + +static void matroska_convert_tag(AVFormatContext *s, const MatroskaTag *tag, + AVDictionary **metadata, enum AVMediaType codec_type) +{ + char key[1024]; + unsigned key_ptr; + + matroska_get_targettype_string(key, &key_ptr, sizeof(key), + tag, codec_type); + matroska_convert_simpletags(s, &tag->simpletags, metadata, + key, key_ptr, sizeof(key)); + ff_metadata_conv(metadata, NULL, ff_mkv_metadata_conv); } -static void matroska_convert_tags(AVFormatContext *s) +static void matroska_convert_tags_for_attachment_cb(void *data, + AVDictionary ***metadata, + enum AVMediaType *codec_type) { - MatroskaDemuxContext *matroska = s->priv_data; - MatroskaTags *tags = matroska->tags.elem; - int i, j; + MatroskaAttachment *attachment = data; + if (attachment->stream) { + *metadata = &attachment->stream->metadata; + *codec_type = attachment->stream->codecpar->codec_type; + } +} - for (i = 0; i < matroska->tags.nb_elem; i++) { - if (tags[i].target.attachuid) { - MatroskaAttachment *attachment = matroska->attachments.elem; - int found = 0; - for (j = 0; j < matroska->attachments.nb_elem; j++) { - if (attachment[j].uid == tags[i].target.attachuid && - attachment[j].stream) { - matroska_convert_tag(s, &tags[i].tag, - &attachment[j].stream->metadata, NULL); - found = 1; - } - } - if (!found) { - av_log(s, AV_LOG_WARNING, - "The tags at index %d refer to a " - "non-existent attachment %"PRId64".\n", - i, tags[i].target.attachuid); - } - } else if (tags[i].target.chapteruid) { - MatroskaChapter *chapter = matroska->chapters.elem; - int found = 0; - for (j = 0; j < matroska->chapters.nb_elem; j++) { - if (chapter[j].uid == tags[i].target.chapteruid && - chapter[j].chapter) { - matroska_convert_tag(s, &tags[i].tag, - &chapter[j].chapter->metadata, NULL); - found = 1; - } - } - if (!found) { - av_log(s, AV_LOG_WARNING, - "The tags at index %d refer to a non-existent chapter " - "%"PRId64".\n", - i, tags[i].target.chapteruid); - } - } else if (tags[i].target.trackuid) { - MatroskaTrack *track = matroska->tracks.elem; - int found = 0; - for (j = 0; j < matroska->tracks.nb_elem; j++) { - if (track[j].uid == tags[i].target.trackuid && - track[j].stream) { - matroska_convert_tag(s, &tags[i].tag, - &track[j].stream->metadata, NULL); - found = 1; - } - } - if (!found) { - av_log(s, AV_LOG_WARNING, - "The tags at index %d refer to a non-existent track " - "%"PRId64".\n", - i, tags[i].target.trackuid); +static void matroska_convert_tags_for_chapter_cb(void *data, + AVDictionary ***metadata, + enum AVMediaType *codec_type) +{ + MatroskaChapter *chapter = data; + if (chapter->chapter) + *metadata = &chapter->chapter->metadata; +} + +static void matroska_convert_tags_for_track_cb(void *data, + AVDictionary ***metadata, + enum AVMediaType *codec_type) +{ + MatroskaTrack *track = data; + if (track->stream) { + *metadata = &track->stream->metadata; + *codec_type = track->stream->codecpar->codec_type; + } +} + +/* First field of elements in elem_list is expected to contain the uid. */ +static int matroska_convert_tag_for_uids(AVFormatContext *s, const MatroskaTag *tag, + EbmlList *elem_list, unsigned list_elem_size, + EbmlList *uid_list, const char *elem_name, + void(*elem_cb)(void *data, AVDictionary ***, enum AVMediaType *)) +{ + uint64_t *uids = uid_list->elem; + + for (int i = 0; i < uid_list->nb_elem; ++i) { + uint64_t uid = uids[i]; + int found = 0; + + void *data = elem_list->elem; + for (int j = 0; j < elem_list->nb_elem; ++j, data = (char *)data + list_elem_size) { + uint64_t elem_uid = *(uint64_t *)data; + + if (uid == 0 /* Matches all. */ || uid == elem_uid) { + AVDictionary **metadata = NULL; + enum AVMediaType codec_type = AVMEDIA_TYPE_UNKNOWN; + + found = 1; + elem_cb(data, &metadata, &codec_type); + + if (metadata) + matroska_convert_tag(s, tag, metadata, codec_type); } - } else { - matroska_convert_tag(s, &tags[i].tag, &s->metadata, - tags[i].target.type); } + + if (!found) + av_log(s, AV_LOG_WARNING, + "Tags element refer to a non-existent %s %"PRIu64".\n", + elem_name, uid); + } + + return uid_list->nb_elem; +} + +static void matroska_convert_tags(AVFormatContext *s) +{ + MatroskaDemuxContext *matroska = s->priv_data; + MatroskaTag *tags = matroska->tags.elem; + + for (int i = 0; i < matroska->tags.nb_elem; i++) { + MatroskaTag *tag = &tags[i]; + int any = 0; + any |= matroska_convert_tag_for_uids(s, tag, + &matroska->attachments, sizeof(MatroskaAttachment), + &tag->target.attachuids, "attachment", + matroska_convert_tags_for_attachment_cb); + any |= matroska_convert_tag_for_uids(s, tag, + &matroska->chapters, sizeof(MatroskaChapter), + &tag->target.chapteruids, "chapter", + matroska_convert_tags_for_chapter_cb); + any |= matroska_convert_tag_for_uids(s, tag, + &matroska->tracks, sizeof(MatroskaTrack), + &tag->target.trackuids, "track", + matroska_convert_tags_for_track_cb); + if (!any) + matroska_convert_tag(s, tag, &s->metadata, AVMEDIA_TYPE_UNKNOWN); } } diff --git a/libavformat/matroskaenc.c b/libavformat/matroskaenc.c index 233c472..2a098c1 100644 --- a/libavformat/matroskaenc.c +++ b/libavformat/matroskaenc.c @@ -61,6 +61,10 @@ #define IS_SEEKABLE(pb, mkv) (((pb)->seekable & AVIO_SEEKABLE_NORMAL) && \ !(mkv)->is_live) +#define TAG_DURATION_PLACEHOLDER "00:00:00.000000000" + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) + enum { DEFAULT_MODE_INFER, DEFAULT_MODE_INFER_NO_SUBS, @@ -1433,58 +1437,185 @@ static int mkv_write_tracks(AVFormatContext *s) MATROSKA_ID_TRACKS); } -static int mkv_write_simpletag(AVIOContext *pb, const AVDictionaryEntry *t) +typedef struct { + uint64_t typevalue; + uint64_t uid; + uint32_t uid_elementid; + const char *lang; + unsigned def; + char *data; + /* (TargetValue, Tag), [ (TagName, SimpleTag)... ] */ + unsigned depth; + struct { + char *name; + ebml_master tag; + } levels[EBML_MAX_DEPTH - 1 /* Tags */ - 1 /* Segment */]; +} TagWriterContext; + +static int mkv_start_tag(MatroskaMuxContext *mkv, AVIOContext **pb, + TagWriterContext *tw) +{ + ebml_master targets; + int ret; + const char *type = tw->levels[0].name; + + if (!*pb) { + ret = start_ebml_master_crc32(pb, mkv); + if (ret < 0) + return ret; + } + + tw->levels[0].tag = start_ebml_master(*pb, MATROSKA_ID_TAG, 0); + targets = start_ebml_master(*pb, MATROSKA_ID_TAGTARGETS, 0); + + if (*type || tw->typevalue != MATROSKA_DEFAULT_TAGTARGETS_TYPEVALUE) + put_ebml_uint(*pb, MATROSKA_ID_TAGTARGETS_TYPEVALUE, tw->typevalue); + if (*type) + put_ebml_string(*pb, MATROSKA_ID_TAGTARGETS_TYPE, type); + if (tw->uid_elementid) + put_ebml_uid(*pb, tw->uid_elementid, tw->uid); + + end_ebml_master(*pb, targets); + return 0; +} + +static uint64_t mkv_type2typevalue(const char *type) { + char type_buf[sizeof(((TypeValueConv *)0)->str)]; + + strncpy(type_buf, type, sizeof type_buf); + + for (const TypeValueConv *conv = ff_mkv_typevalue_conv; + conv->typevalue; + ++conv) + if (!memcmp(type_buf, conv->str, sizeof(type_buf))) + return conv->typevalue; + + return MATROSKA_DEFAULT_TAGTARGETS_TYPEVALUE; +} + +static int mkv_tagwriter_init(TagWriterContext *tw, const AVDictionaryEntry *t) +{ + enum { MAX_DEPTH = ARRAY_SIZE(tw->levels) }; + uint8_t *key = av_strdup(t->key); - uint8_t *p = key; - const uint8_t *lang = NULL; - ebml_master tag; + char *p = key; + unsigned depth; if (!key) return AVERROR(ENOMEM); - if ((p = strrchr(p, '-')) && - (lang = ff_convert_lang_to(p + 1, AV_LANG_ISO639_2_BIBL))) - *p = 0; + /* TAG@-lang */ + if ((p = strrchr(p, '@'))) { + tw->def = p[1] != '-'; + tw->lang = ff_convert_lang_to(p + 1 + !tw->def, AV_LANG_ISO639_2_BIBL); + *p = '\0'; + } else { + tw->def = 1; + tw->lang = NULL; + } - p = key; - while (*p) { + /* "hello world" -> "HELLO_WORLD" */ + for (p = key; *p; ++p) if (*p == ' ') *p = '_'; - else if (*p >= 'a' && *p <= 'z') + else if ('a' <= *p && *p <= 'z') *p -= 'a' - 'A'; - p++; - } - tag = start_ebml_master(pb, MATROSKA_ID_SIMPLETAG, 0); - put_ebml_string(pb, MATROSKA_ID_TAGNAME, key); - if (lang) - put_ebml_string(pb, MATROSKA_ID_TAGLANG, lang); - put_ebml_string(pb, MATROSKA_ID_TAGSTRING, t->value); - end_ebml_master(pb, tag); + tw->data = key; + + depth = 0; + p = key; + for (;;) { + tw->levels[depth++].name = p; + if (!(p = strchr(p, '/')) || MAX_DEPTH <= depth) + break; + *p++ = '\0'; + } + /* Use default TagTarget for simple tag names (no '/'). */ + if (1 == depth) { + tw->levels[1].name = tw->levels[0].name; + tw->levels[0].name = (char *)""; + depth++; + tw->typevalue = MATROSKA_DEFAULT_TAGTARGETS_TYPEVALUE; + } else { + /* Try parse "123TARGETTYPE". */ + p = tw->levels[0].name; + if ('0' <= *p && *p <= '9') { + tw->typevalue = 0; + do + tw->typevalue = tw->typevalue * 10 + (*p++ - '0'); + while ('0' <= *p && *p <= '9'); + tw->levels[0].name = *p ? p : (char *)""; + } else { + tw->typevalue = mkv_type2typevalue(p); + } + } + tw->depth = depth; - av_freep(&key); return 0; } -static int mkv_write_tag_targets(MatroskaMuxContext *mkv, AVIOContext **pb, - ebml_master *tag, uint32_t elementid, uint64_t uid) +static int mkv_write_simpletag(MatroskaMuxContext *mkv, AVIOContext **pb, + TagWriterContext *ptw, const AVDictionaryEntry *t, + uint32_t uid_elementid, uint64_t uid) { - ebml_master targets; + TagWriterContext tw; + unsigned i = 0; int ret; - if (!*pb) { - ret = start_ebml_master_crc32(pb, mkv); + if (t) { + ret = mkv_tagwriter_init(&tw, t); if (ret < 0) - return ret; + goto out; + + tw.uid = uid; + tw.uid_elementid = uid_elementid; + } + + /* 1. Find common levels. */ + if (ptw) { + if (t && + ptw->uid == tw.uid && + ptw->uid_elementid == tw.uid_elementid) + for (; + i < tw.depth && i < ptw->depth && + !strcmp(tw.levels[i].name, ptw->levels[i].name); + ++i) + tw.levels[i].tag = ptw->levels[i].tag; + + /* 2. Close unneded tags. */ + for (unsigned j = ptw->depth; i < j;) + end_ebml_master(*pb, ptw->levels[--j].tag); + } + + /* 3. Open new tags. */ + if (t) { + for (; i < tw.depth; ++i) { + if (!i) { + ret = mkv_start_tag(mkv, pb, &tw); + if (ret < 0) + return ret; + } else { + tw.levels[i].tag = start_ebml_master(*pb, MATROSKA_ID_SIMPLETAG, 0); + put_ebml_string(*pb, MATROSKA_ID_TAGNAME, tw.levels[i].name); + } + } + + /* 4. Write contents to the last one (intermediate tags will have name only). */ + if (tw.lang) + put_ebml_string(*pb, MATROSKA_ID_TAGLANG, tw.lang); + if (!tw.def) + put_ebml_uint(*pb, MATROSKA_ID_TAGDEFAULT, tw.def); + put_ebml_string(*pb, MATROSKA_ID_TAGSTRING, t->value); } - *tag = start_ebml_master(*pb, MATROSKA_ID_TAG, 0); - targets = start_ebml_master(*pb, MATROSKA_ID_TAGTARGETS, 4 + 1 + 8); - if (elementid) - put_ebml_uid(*pb, elementid, uid); - end_ebml_master(*pb, targets); - return 0; +out: + if (ptw) + av_free(ptw->data); + /* Save new state. */ + memcpy(ptw, &tw, sizeof(tw)); + return ret; } static int mkv_check_tag_name(const char *name, uint32_t elementid) @@ -1501,58 +1632,36 @@ static int mkv_check_tag_name(const char *name, uint32_t elementid) av_strcasecmp(name, "mimetype"))); } -static int mkv_write_tag(MatroskaMuxContext *mkv, const AVDictionary *m, - AVIOContext **pb, ebml_master *tag, +static int mkv_write_tag(MatroskaMuxContext *mkv, const AVDictionary *metadata, + AVIOContext **pb, TagWriterContext *tw, uint32_t elementid, uint64_t uid) { const AVDictionaryEntry *t = NULL; - ebml_master tag2; int ret; - ret = mkv_write_tag_targets(mkv, pb, tag ? tag : &tag2, elementid, uid); - if (ret < 0) - return ret; - - while ((t = av_dict_get(m, "", t, AV_DICT_IGNORE_SUFFIX))) { + while ((t = av_dict_get(metadata, "", t, AV_DICT_IGNORE_SUFFIX))) { if (mkv_check_tag_name(t->key, elementid)) { - ret = mkv_write_simpletag(*pb, t); + ret = mkv_write_simpletag(mkv, pb, tw, t, elementid, uid); if (ret < 0) return ret; } } - - if (!tag) - end_ebml_master(*pb, tag2); - return 0; } -static int mkv_check_tag(const AVDictionary *m, uint32_t elementid) -{ - const AVDictionaryEntry *t = NULL; - - while ((t = av_dict_get(m, "", t, AV_DICT_IGNORE_SUFFIX))) - if (mkv_check_tag_name(t->key, elementid)) - return 1; - - return 0; -} - -static int mkv_write_tags(AVFormatContext *s) +static int mkv_write_tags(AVFormatContext *s, TagWriterContext *tw) { MatroskaMuxContext *mkv = s->priv_data; - ebml_master tag, *tagp = IS_SEEKABLE(s->pb, mkv) ? &tag : NULL; int i, ret; + AVIOContext **pb = &mkv->tags.bc; mkv->wrote_tags = 1; ff_metadata_conv_ctx(s, ff_mkv_metadata_conv, NULL); - if (mkv_check_tag(s->metadata, 0)) { - ret = mkv_write_tag(mkv, s->metadata, &mkv->tags.bc, NULL, 0, 0); - if (ret < 0) - return ret; - } + ret = mkv_write_tag(mkv, s->metadata, pb, tw, 0, 0); + if (ret < 0) + return ret; for (i = 0; i < s->nb_streams; i++) { const AVStream *st = s->streams[i]; @@ -1561,28 +1670,24 @@ static int mkv_write_tags(AVFormatContext *s) if (st->codecpar->codec_type == AVMEDIA_TYPE_ATTACHMENT) continue; - if (!tagp && !mkv_check_tag(st->metadata, MATROSKA_ID_TAGTARGETS_TRACKUID)) - continue; - - ret = mkv_write_tag(mkv, st->metadata, &mkv->tags.bc, tagp, + ret = mkv_write_tag(mkv, st->metadata, pb, tw, MATROSKA_ID_TAGTARGETS_TRACKUID, track->uid); if (ret < 0) return ret; - if (tagp) { - AVIOContext *pb = mkv->tags.bc; - ebml_master simpletag; + if (IS_SEEKABLE(s->pb, mkv)) { + static const AVDictionaryEntry duration_tag = { + .key = (char *)"DURATION", + .value = (char *)TAG_DURATION_PLACEHOLDER, + }; - simpletag = start_ebml_master(pb, MATROSKA_ID_SIMPLETAG, - 2 + 1 + 8 + 23); - put_ebml_string(pb, MATROSKA_ID_TAGNAME, "DURATION"); - track->duration_offset = avio_tell(pb); + ret = mkv_write_simpletag(mkv, pb, tw, &duration_tag, + MATROSKA_ID_TAGTARGETS_TRACKUID, track->uid); + if (ret < 0) + return ret; - // Reserve space to write duration as a 20-byte string. - // 2 (ebml id) + 1 (data size) + 20 (data) - put_ebml_void(pb, 23); - end_ebml_master(pb, simpletag); - end_ebml_master(pb, tag); + /* Save position at the start of tag value. */ + track->duration_offset = avio_tell(*pb) - (sizeof(TAG_DURATION_PLACEHOLDER) - 1); } } @@ -1594,24 +1699,24 @@ static int mkv_write_tags(AVFormatContext *s) if (st->codecpar->codec_type != AVMEDIA_TYPE_ATTACHMENT) continue; - if (!mkv_check_tag(st->metadata, MATROSKA_ID_TAGTARGETS_ATTACHUID)) - continue; - - ret = mkv_write_tag(mkv, st->metadata, &mkv->tags.bc, NULL, + ret = mkv_write_tag(mkv, st->metadata, pb, tw, MATROSKA_ID_TAGTARGETS_ATTACHUID, track->uid); if (ret < 0) return ret; } } - if (mkv->tags.bc) { + ret = mkv_write_simpletag(mkv, pb, tw, NULL, 0, 0); + if (ret < 0) + return ret; + + if (mkv->tags.bc) return end_ebml_master_crc32_tentatively(s->pb, &mkv->tags, mkv, MATROSKA_ID_TAGS); - } return 0; } -static int mkv_write_chapters(AVFormatContext *s) +static int mkv_write_chapters(AVFormatContext *s, TagWriterContext *tw) { MatroskaMuxContext *mkv = s->priv_data; AVIOContext *dyn_cp = NULL, *dyn_tags = NULL, **tags, *pb = s->pb; @@ -1669,23 +1774,29 @@ static int mkv_write_chapters(AVFormatContext *s) } end_ebml_master(dyn_cp, chapteratom); - if (tags && mkv_check_tag(c->metadata, MATROSKA_ID_TAGTARGETS_CHAPTERUID)) { - ret = mkv_write_tag(mkv, c->metadata, tags, NULL, + if (tags) { + ret = mkv_write_tag(mkv, c->metadata, tags, tw, MATROSKA_ID_TAGTARGETS_CHAPTERUID, (uint32_t)c->id + chapter_id_offset); if (ret < 0) goto fail; } } + end_ebml_master(dyn_cp, editionentry); mkv->wrote_chapters = 1; ret = end_ebml_master_crc32(pb, &dyn_cp, mkv, MATROSKA_ID_CHAPTERS, 0, 0, 1); if (ret < 0) goto fail; - if (dyn_tags) + if (dyn_tags) { + ret = mkv_write_simpletag(mkv, &dyn_tags, tw, NULL, 0, 0); + if (ret < 0) + return ret; + return end_ebml_master_crc32(pb, &dyn_tags, mkv, MATROSKA_ID_TAGS, 0, 0, 1); + } return 0; fail: @@ -1791,6 +1902,7 @@ static int mkv_write_header(AVFormatContext *s) const AVDictionaryEntry *tag; int ret, i, version = 2; int64_t creation_time; + TagWriterContext tw = { 0 }; if (mkv->mode != MODE_WEBM || av_dict_get(s->metadata, "stereo_mode", NULL, 0) || @@ -1881,7 +1993,7 @@ static int mkv_write_header(AVFormatContext *s) if (ret < 0) return ret; - ret = mkv_write_chapters(s); + ret = mkv_write_chapters(s, &tw); if (ret < 0) return ret; @@ -1893,7 +2005,7 @@ static int mkv_write_header(AVFormatContext *s) /* Must come after mkv_write_chapters() to write chapter tags * into the same Tags element as the other tags. */ - ret = mkv_write_tags(s); + ret = mkv_write_tags(s, &tw); if (ret < 0) return ret; @@ -2458,6 +2570,7 @@ static int mkv_write_trailer(AVFormatContext *s) AVIOContext *pb = s->pb; int64_t endpos, ret64; int ret, ret2 = 0; + TagWriterContext tw = { 0 }; // check if we have an audio packet cached if (mkv->cur_audio_pkt.size > 0) { @@ -2476,7 +2589,7 @@ static int mkv_write_trailer(AVFormatContext *s) return ret; } - ret = mkv_write_chapters(s); + ret = mkv_write_chapters(s, &tw); if (ret < 0) return ret; @@ -2579,20 +2692,19 @@ after_cues: const AVStream *st = s->streams[i]; const mkv_track *track = &mkv->tracks[i]; - if (track->duration_offset > 0) { + if (track->duration_offset) { double duration_sec = track->duration * av_q2d(st->time_base); - char duration_string[20] = ""; + char duration_string[sizeof TAG_DURATION_PLACEHOLDER]; av_log(s, AV_LOG_DEBUG, "stream %d end duration = %" PRIu64 "\n", i, track->duration); - avio_seek(mkv->tags.bc, track->duration_offset, SEEK_SET); - - snprintf(duration_string, 20, "%02d:%02d:%012.9f", - (int) duration_sec / 3600, ((int) duration_sec / 60) % 60, + snprintf(duration_string, sizeof duration_string, "%02d:%02d:%012.9f", + (int)duration_sec / 3600, ((int)duration_sec / 60) % 60, fmod(duration_sec, 60)); - put_ebml_binary(mkv->tags.bc, MATROSKA_ID_TAGSTRING, duration_string, 20); + avio_seek(mkv->tags.bc, track->duration_offset, SEEK_SET); + avio_write(mkv->tags.bc, duration_string, sizeof TAG_DURATION_PLACEHOLDER - 1); } }