| @@ -535,6 +535,42 @@ static void get_aac_sample_rates(AVFormatContext *s, AVCodecContext *codec, | |||||
| *output_sample_rate = mp4ac.ext_sample_rate; | *output_sample_rate = mp4ac.ext_sample_rate; | ||||
| } | } | ||||
| static int mkv_write_native_codecprivate(AVFormatContext *s, | |||||
| AVCodecContext *codec, | |||||
| AVIOContext *dyn_cp) | |||||
| { | |||||
| switch (codec->codec_id) { | |||||
| case AV_CODEC_ID_VORBIS: | |||||
| case AV_CODEC_ID_THEORA: | |||||
| return put_xiph_codecpriv(s, dyn_cp, codec); | |||||
| case AV_CODEC_ID_FLAC: | |||||
| return put_flac_codecpriv(s, dyn_cp, codec); | |||||
| case AV_CODEC_ID_WAVPACK: | |||||
| return put_wv_codecpriv(dyn_cp, codec); | |||||
| case AV_CODEC_ID_H264: | |||||
| return ff_isom_write_avcc(dyn_cp, codec->extradata, | |||||
| codec->extradata_size); | |||||
| case AV_CODEC_ID_HEVC: | |||||
| return ff_isom_write_hvcc(dyn_cp, codec->extradata, | |||||
| codec->extradata_size, 0); | |||||
| case AV_CODEC_ID_ALAC: | |||||
| if (codec->extradata_size < 36) { | |||||
| av_log(s, AV_LOG_ERROR, | |||||
| "Invalid extradata found, ALAC expects a 36-byte " | |||||
| "QuickTime atom."); | |||||
| return AVERROR_INVALIDDATA; | |||||
| } else | |||||
| avio_write(dyn_cp, codec->extradata + 12, | |||||
| codec->extradata_size - 12); | |||||
| break; | |||||
| default: | |||||
| if (codec->extradata_size) | |||||
| avio_write(dyn_cp, codec->extradata, codec->extradata_size); | |||||
| } | |||||
| return 0; | |||||
| } | |||||
| static int mkv_write_codecprivate(AVFormatContext *s, AVIOContext *pb, | static int mkv_write_codecprivate(AVFormatContext *s, AVIOContext *pb, | ||||
| AVCodecContext *codec, int native_id, | AVCodecContext *codec, int native_id, | ||||
| int qt_id) | int qt_id) | ||||
| @@ -548,30 +584,7 @@ static int mkv_write_codecprivate(AVFormatContext *s, AVIOContext *pb, | |||||
| return ret; | return ret; | ||||
| if (native_id) { | if (native_id) { | ||||
| if (codec->codec_id == AV_CODEC_ID_VORBIS || | |||||
| codec->codec_id == AV_CODEC_ID_THEORA) | |||||
| ret = put_xiph_codecpriv(s, dyn_cp, codec); | |||||
| else if (codec->codec_id == AV_CODEC_ID_FLAC) | |||||
| ret = put_flac_codecpriv(s, dyn_cp, codec); | |||||
| else if (codec->codec_id == AV_CODEC_ID_WAVPACK) | |||||
| ret = put_wv_codecpriv(dyn_cp, codec); | |||||
| else if (codec->codec_id == AV_CODEC_ID_H264) | |||||
| ret = ff_isom_write_avcc(dyn_cp, codec->extradata, | |||||
| codec->extradata_size); | |||||
| else if (codec->codec_id == AV_CODEC_ID_HEVC) | |||||
| ret = ff_isom_write_hvcc(dyn_cp, codec->extradata, | |||||
| codec->extradata_size, 0); | |||||
| else if (codec->codec_id == AV_CODEC_ID_ALAC) { | |||||
| if (codec->extradata_size < 36) { | |||||
| av_log(s, AV_LOG_ERROR, | |||||
| "Invalid extradata found, ALAC expects a 36-byte " | |||||
| "QuickTime atom."); | |||||
| ret = AVERROR_INVALIDDATA; | |||||
| } else | |||||
| avio_write(dyn_cp, codec->extradata + 12, | |||||
| codec->extradata_size - 12); | |||||
| } else if (codec->extradata_size) | |||||
| avio_write(dyn_cp, codec->extradata, codec->extradata_size); | |||||
| ret = mkv_write_native_codecprivate(s, codec, dyn_cp); | |||||
| } else if (codec->codec_type == AVMEDIA_TYPE_VIDEO) { | } else if (codec->codec_type == AVMEDIA_TYPE_VIDEO) { | ||||
| if (qt_id) { | if (qt_id) { | ||||
| if (!codec->codec_tag) | if (!codec->codec_tag) | ||||
| @@ -611,174 +624,185 @@ static int mkv_write_codecprivate(AVFormatContext *s, AVIOContext *pb, | |||||
| return ret; | return ret; | ||||
| } | } | ||||
| static int mkv_write_tracks(AVFormatContext *s) | |||||
| static int mkv_write_track(AVFormatContext *s, MatroskaMuxContext *mkv, | |||||
| int i, AVIOContext *pb) | |||||
| { | { | ||||
| MatroskaMuxContext *mkv = s->priv_data; | |||||
| AVIOContext *pb = s->pb; | |||||
| ebml_master tracks; | |||||
| int i, j, ret; | |||||
| AVStream *st = s->streams[i]; | |||||
| AVCodecContext *codec = st->codec; | |||||
| ebml_master subinfo, track; | |||||
| int native_id = 0; | |||||
| int qt_id = 0; | |||||
| int bit_depth = av_get_bits_per_sample(codec->codec_id); | |||||
| int sample_rate = codec->sample_rate; | |||||
| int output_sample_rate = 0; | |||||
| int j, ret; | |||||
| AVDictionaryEntry *tag; | |||||
| ret = mkv_add_seekhead_entry(mkv->main_seekhead, MATROSKA_ID_TRACKS, avio_tell(pb)); | |||||
| if (ret < 0) | |||||
| return ret; | |||||
| // ms precision is the de-facto standard timescale for mkv files | |||||
| avpriv_set_pts_info(st, 64, 1, 1000); | |||||
| tracks = start_ebml_master(pb, MATROSKA_ID_TRACKS, 0); | |||||
| for (i = 0; i < s->nb_streams; i++) { | |||||
| AVStream *st = s->streams[i]; | |||||
| AVCodecContext *codec = st->codec; | |||||
| ebml_master subinfo, track; | |||||
| int native_id = 0; | |||||
| int qt_id = 0; | |||||
| int bit_depth = av_get_bits_per_sample(codec->codec_id); | |||||
| int sample_rate = codec->sample_rate; | |||||
| int output_sample_rate = 0; | |||||
| AVDictionaryEntry *tag; | |||||
| // ms precision is the de-facto standard timescale for mkv files | |||||
| avpriv_set_pts_info(st, 64, 1, 1000); | |||||
| if (codec->codec_type == AVMEDIA_TYPE_ATTACHMENT) { | |||||
| mkv->have_attachments = 1; | |||||
| continue; | |||||
| } | |||||
| if (codec->codec_type == AVMEDIA_TYPE_ATTACHMENT) { | |||||
| mkv->have_attachments = 1; | |||||
| return 0; | |||||
| } | |||||
| if (!bit_depth) | |||||
| bit_depth = av_get_bytes_per_sample(codec->sample_fmt) << 3; | |||||
| if (!bit_depth) | |||||
| bit_depth = av_get_bytes_per_sample(codec->sample_fmt) << 3; | |||||
| if (codec->codec_id == AV_CODEC_ID_AAC) | |||||
| get_aac_sample_rates(s, codec, &sample_rate, &output_sample_rate); | |||||
| if (codec->codec_id == AV_CODEC_ID_AAC) | |||||
| get_aac_sample_rates(s, codec, &sample_rate, &output_sample_rate); | |||||
| track = start_ebml_master(pb, MATROSKA_ID_TRACKENTRY, 0); | |||||
| put_ebml_uint (pb, MATROSKA_ID_TRACKNUMBER , i + 1); | |||||
| put_ebml_uint (pb, MATROSKA_ID_TRACKUID , i + 1); | |||||
| put_ebml_uint (pb, MATROSKA_ID_TRACKFLAGLACING , 0); // no lacing (yet) | |||||
| track = start_ebml_master(pb, MATROSKA_ID_TRACKENTRY, 0); | |||||
| put_ebml_uint (pb, MATROSKA_ID_TRACKNUMBER , i + 1); | |||||
| put_ebml_uint (pb, MATROSKA_ID_TRACKUID , i + 1); | |||||
| put_ebml_uint (pb, MATROSKA_ID_TRACKFLAGLACING , 0); // no lacing (yet) | |||||
| if ((tag = av_dict_get(st->metadata, "title", NULL, 0))) | |||||
| put_ebml_string(pb, MATROSKA_ID_TRACKNAME, tag->value); | |||||
| tag = av_dict_get(st->metadata, "language", NULL, 0); | |||||
| put_ebml_string(pb, MATROSKA_ID_TRACKLANGUAGE, tag ? tag->value:"und"); | |||||
| if ((tag = av_dict_get(st->metadata, "title", NULL, 0))) | |||||
| put_ebml_string(pb, MATROSKA_ID_TRACKNAME, tag->value); | |||||
| tag = av_dict_get(st->metadata, "language", NULL, 0); | |||||
| put_ebml_string(pb, MATROSKA_ID_TRACKLANGUAGE, tag ? tag->value:"und"); | |||||
| // The default value for TRACKFLAGDEFAULT is 1, so add element | |||||
| // if we need to clear it. | |||||
| if (!(st->disposition & AV_DISPOSITION_DEFAULT)) | |||||
| put_ebml_uint(pb, MATROSKA_ID_TRACKFLAGDEFAULT, !!(st->disposition & AV_DISPOSITION_DEFAULT)); | |||||
| // The default value for TRACKFLAGDEFAULT is 1, so add element | |||||
| // if we need to clear it. | |||||
| if (!(st->disposition & AV_DISPOSITION_DEFAULT)) | |||||
| put_ebml_uint(pb, MATROSKA_ID_TRACKFLAGDEFAULT, !!(st->disposition & AV_DISPOSITION_DEFAULT)); | |||||
| if (codec->codec_type == AVMEDIA_TYPE_AUDIO && codec->delay) { | |||||
| mkv->tracks[i].ts_offset = av_rescale_q(codec->delay, | |||||
| (AVRational){ 1, codec->sample_rate }, | |||||
| st->time_base); | |||||
| if (codec->codec_type == AVMEDIA_TYPE_AUDIO && codec->delay) { | |||||
| mkv->tracks[i].ts_offset = av_rescale_q(codec->delay, | |||||
| (AVRational){ 1, codec->sample_rate }, | |||||
| st->time_base); | |||||
| put_ebml_uint(pb, MATROSKA_ID_CODECDELAY, | |||||
| av_rescale_q(codec->delay, (AVRational){ 1, codec->sample_rate }, | |||||
| (AVRational){ 1, 1000000000 })); | |||||
| } | |||||
| put_ebml_uint(pb, MATROSKA_ID_CODECDELAY, | |||||
| av_rescale_q(codec->delay, (AVRational){ 1, codec->sample_rate }, | |||||
| (AVRational){ 1, 1000000000 })); | |||||
| // look for a codec ID string specific to mkv to use, | |||||
| // if none are found, use AVI codes | |||||
| for (j = 0; ff_mkv_codec_tags[j].id != AV_CODEC_ID_NONE; j++) { | |||||
| if (ff_mkv_codec_tags[j].id == codec->codec_id) { | |||||
| put_ebml_string(pb, MATROSKA_ID_CODECID, ff_mkv_codec_tags[j].str); | |||||
| native_id = 1; | |||||
| break; | |||||
| } | } | ||||
| } | |||||
| if (mkv->mode == MODE_WEBM && !(codec->codec_id == AV_CODEC_ID_VP8 || | |||||
| codec->codec_id == AV_CODEC_ID_VP9 || | |||||
| codec->codec_id == AV_CODEC_ID_OPUS || | |||||
| codec->codec_id == AV_CODEC_ID_VORBIS)) { | |||||
| av_log(s, AV_LOG_ERROR, | |||||
| "Only VP8 or VP9 video and Vorbis or Opus audio are supported for WebM.\n"); | |||||
| return AVERROR(EINVAL); | |||||
| } | |||||
| // look for a codec ID string specific to mkv to use, | |||||
| // if none are found, use AVI codes | |||||
| for (j = 0; ff_mkv_codec_tags[j].id != AV_CODEC_ID_NONE; j++) { | |||||
| if (ff_mkv_codec_tags[j].id == codec->codec_id) { | |||||
| put_ebml_string(pb, MATROSKA_ID_CODECID, ff_mkv_codec_tags[j].str); | |||||
| native_id = 1; | |||||
| switch (codec->codec_type) { | |||||
| case AVMEDIA_TYPE_VIDEO: | |||||
| put_ebml_uint(pb, MATROSKA_ID_TRACKTYPE, MATROSKA_TRACK_TYPE_VIDEO); | |||||
| if (st->avg_frame_rate.num > 0 && st->avg_frame_rate.den > 0) | |||||
| put_ebml_uint(pb, MATROSKA_ID_TRACKDEFAULTDURATION, 1E9 / av_q2d(st->avg_frame_rate)); | |||||
| if (!native_id && | |||||
| ff_codec_get_tag(ff_codec_movvideo_tags, codec->codec_id) && | |||||
| (!ff_codec_get_tag(ff_codec_bmp_tags, codec->codec_id) || | |||||
| codec->codec_id == AV_CODEC_ID_SVQ1 || | |||||
| codec->codec_id == AV_CODEC_ID_SVQ3 || | |||||
| codec->codec_id == AV_CODEC_ID_CINEPAK)) | |||||
| qt_id = 1; | |||||
| if (qt_id) | |||||
| put_ebml_string(pb, MATROSKA_ID_CODECID, "V_QUICKTIME"); | |||||
| else if (!native_id) { | |||||
| // if there is no mkv-specific codec ID, use VFW mode | |||||
| put_ebml_string(pb, MATROSKA_ID_CODECID, "V_MS/VFW/FOURCC"); | |||||
| mkv->tracks[i].write_dts = 1; | |||||
| } | |||||
| subinfo = start_ebml_master(pb, MATROSKA_ID_TRACKVIDEO, 0); | |||||
| // XXX: interlace flag? | |||||
| put_ebml_uint (pb, MATROSKA_ID_VIDEOPIXELWIDTH , codec->width); | |||||
| put_ebml_uint (pb, MATROSKA_ID_VIDEOPIXELHEIGHT, codec->height); | |||||
| if ((tag = av_dict_get(s->metadata, "stereo_mode", NULL, 0))) { | |||||
| uint8_t stereo_fmt = atoi(tag->value); | |||||
| int valid_fmt = 0; | |||||
| switch (mkv->mode) { | |||||
| case MODE_WEBM: | |||||
| if (stereo_fmt <= MATROSKA_VIDEO_STEREOMODE_TYPE_TOP_BOTTOM || | |||||
| stereo_fmt == MATROSKA_VIDEO_STEREOMODE_TYPE_RIGHT_LEFT) | |||||
| valid_fmt = 1; | |||||
| break; | |||||
| case MODE_MATROSKAv2: | |||||
| if (stereo_fmt <= MATROSKA_VIDEO_STEREOMODE_TYPE_BOTH_EYES_BLOCK_RL) | |||||
| valid_fmt = 1; | |||||
| break; | break; | ||||
| } | } | ||||
| } | |||||
| if (mkv->mode == MODE_WEBM && !(codec->codec_id == AV_CODEC_ID_VP8 || | |||||
| codec->codec_id == AV_CODEC_ID_VP9 || | |||||
| codec->codec_id == AV_CODEC_ID_OPUS || | |||||
| codec->codec_id == AV_CODEC_ID_VORBIS)) { | |||||
| av_log(s, AV_LOG_ERROR, | |||||
| "Only VP8 or VP9 video and Vorbis or Opus audio are supported for WebM.\n"); | |||||
| return AVERROR(EINVAL); | |||||
| if (valid_fmt) | |||||
| put_ebml_uint (pb, MATROSKA_ID_VIDEOSTEREOMODE, stereo_fmt); | |||||
| } | |||||
| if (st->sample_aspect_ratio.num) { | |||||
| int d_width = codec->width*av_q2d(st->sample_aspect_ratio); | |||||
| put_ebml_uint(pb, MATROSKA_ID_VIDEODISPLAYWIDTH , d_width); | |||||
| put_ebml_uint(pb, MATROSKA_ID_VIDEODISPLAYHEIGHT, codec->height); | |||||
| put_ebml_uint(pb, MATROSKA_ID_VIDEODISPLAYUNIT, 3); | |||||
| } | } | ||||
| end_ebml_master(pb, subinfo); | |||||
| break; | |||||
| case AVMEDIA_TYPE_AUDIO: | |||||
| put_ebml_uint(pb, MATROSKA_ID_TRACKTYPE, MATROSKA_TRACK_TYPE_AUDIO); | |||||
| if (!native_id) | |||||
| // no mkv-specific ID, use ACM mode | |||||
| put_ebml_string(pb, MATROSKA_ID_CODECID, "A_MS/ACM"); | |||||
| subinfo = start_ebml_master(pb, MATROSKA_ID_TRACKAUDIO, 0); | |||||
| put_ebml_uint (pb, MATROSKA_ID_AUDIOCHANNELS , codec->channels); | |||||
| put_ebml_float (pb, MATROSKA_ID_AUDIOSAMPLINGFREQ, sample_rate); | |||||
| if (output_sample_rate) | |||||
| put_ebml_float(pb, MATROSKA_ID_AUDIOOUTSAMPLINGFREQ, output_sample_rate); | |||||
| if (bit_depth) | |||||
| put_ebml_uint(pb, MATROSKA_ID_AUDIOBITDEPTH, bit_depth); | |||||
| end_ebml_master(pb, subinfo); | |||||
| break; | |||||
| case AVMEDIA_TYPE_SUBTITLE: | |||||
| put_ebml_uint(pb, MATROSKA_ID_TRACKTYPE, MATROSKA_TRACK_TYPE_SUBTITLE); | |||||
| if (!native_id) { | |||||
| av_log(s, AV_LOG_ERROR, "Subtitle codec %d is not supported.\n", codec->codec_id); | |||||
| return AVERROR(ENOSYS); | |||||
| } | |||||
| break; | |||||
| default: | |||||
| av_log(s, AV_LOG_ERROR, "Only audio, video, and subtitles are supported for Matroska.\n"); | |||||
| break; | |||||
| } | |||||
| ret = mkv_write_codecprivate(s, pb, codec, native_id, qt_id); | |||||
| if (ret < 0) | |||||
| return ret; | |||||
| switch (codec->codec_type) { | |||||
| case AVMEDIA_TYPE_VIDEO: | |||||
| put_ebml_uint(pb, MATROSKA_ID_TRACKTYPE, MATROSKA_TRACK_TYPE_VIDEO); | |||||
| if (st->avg_frame_rate.num > 0 && st->avg_frame_rate.den > 0) | |||||
| put_ebml_uint(pb, MATROSKA_ID_TRACKDEFAULTDURATION, 1E9 / av_q2d(st->avg_frame_rate)); | |||||
| if (!native_id && | |||||
| ff_codec_get_tag(ff_codec_movvideo_tags, codec->codec_id) && | |||||
| (!ff_codec_get_tag(ff_codec_bmp_tags, codec->codec_id) || | |||||
| codec->codec_id == AV_CODEC_ID_SVQ1 || | |||||
| codec->codec_id == AV_CODEC_ID_SVQ3 || | |||||
| codec->codec_id == AV_CODEC_ID_CINEPAK)) | |||||
| qt_id = 1; | |||||
| if (qt_id) | |||||
| put_ebml_string(pb, MATROSKA_ID_CODECID, "V_QUICKTIME"); | |||||
| else if (!native_id) { | |||||
| // if there is no mkv-specific codec ID, use VFW mode | |||||
| put_ebml_string(pb, MATROSKA_ID_CODECID, "V_MS/VFW/FOURCC"); | |||||
| mkv->tracks[i].write_dts = 1; | |||||
| } | |||||
| end_ebml_master(pb, track); | |||||
| subinfo = start_ebml_master(pb, MATROSKA_ID_TRACKVIDEO, 0); | |||||
| // XXX: interlace flag? | |||||
| put_ebml_uint (pb, MATROSKA_ID_VIDEOPIXELWIDTH , codec->width); | |||||
| put_ebml_uint (pb, MATROSKA_ID_VIDEOPIXELHEIGHT, codec->height); | |||||
| if ((tag = av_dict_get(s->metadata, "stereo_mode", NULL, 0))) { | |||||
| uint8_t stereo_fmt = atoi(tag->value); | |||||
| int valid_fmt = 0; | |||||
| switch (mkv->mode) { | |||||
| case MODE_WEBM: | |||||
| if (stereo_fmt <= MATROSKA_VIDEO_STEREOMODE_TYPE_TOP_BOTTOM || | |||||
| stereo_fmt == MATROSKA_VIDEO_STEREOMODE_TYPE_RIGHT_LEFT) | |||||
| valid_fmt = 1; | |||||
| break; | |||||
| case MODE_MATROSKAv2: | |||||
| if (stereo_fmt <= MATROSKA_VIDEO_STEREOMODE_TYPE_BOTH_EYES_BLOCK_RL) | |||||
| valid_fmt = 1; | |||||
| break; | |||||
| } | |||||
| return 0; | |||||
| } | |||||
| if (valid_fmt) | |||||
| put_ebml_uint (pb, MATROSKA_ID_VIDEOSTEREOMODE, stereo_fmt); | |||||
| } | |||||
| if (st->sample_aspect_ratio.num) { | |||||
| int d_width = codec->width*av_q2d(st->sample_aspect_ratio); | |||||
| put_ebml_uint(pb, MATROSKA_ID_VIDEODISPLAYWIDTH , d_width); | |||||
| put_ebml_uint(pb, MATROSKA_ID_VIDEODISPLAYHEIGHT, codec->height); | |||||
| put_ebml_uint(pb, MATROSKA_ID_VIDEODISPLAYUNIT, 3); | |||||
| } | |||||
| end_ebml_master(pb, subinfo); | |||||
| break; | |||||
| static int mkv_write_tracks(AVFormatContext *s) | |||||
| { | |||||
| MatroskaMuxContext *mkv = s->priv_data; | |||||
| AVIOContext *pb = s->pb; | |||||
| ebml_master tracks; | |||||
| int i, ret; | |||||
| case AVMEDIA_TYPE_AUDIO: | |||||
| put_ebml_uint(pb, MATROSKA_ID_TRACKTYPE, MATROSKA_TRACK_TYPE_AUDIO); | |||||
| if (!native_id) | |||||
| // no mkv-specific ID, use ACM mode | |||||
| put_ebml_string(pb, MATROSKA_ID_CODECID, "A_MS/ACM"); | |||||
| subinfo = start_ebml_master(pb, MATROSKA_ID_TRACKAUDIO, 0); | |||||
| put_ebml_uint (pb, MATROSKA_ID_AUDIOCHANNELS , codec->channels); | |||||
| put_ebml_float (pb, MATROSKA_ID_AUDIOSAMPLINGFREQ, sample_rate); | |||||
| if (output_sample_rate) | |||||
| put_ebml_float(pb, MATROSKA_ID_AUDIOOUTSAMPLINGFREQ, output_sample_rate); | |||||
| if (bit_depth) | |||||
| put_ebml_uint(pb, MATROSKA_ID_AUDIOBITDEPTH, bit_depth); | |||||
| end_ebml_master(pb, subinfo); | |||||
| break; | |||||
| ret = mkv_add_seekhead_entry(mkv->main_seekhead, MATROSKA_ID_TRACKS, avio_tell(pb)); | |||||
| if (ret < 0) | |||||
| return ret; | |||||
| case AVMEDIA_TYPE_SUBTITLE: | |||||
| put_ebml_uint(pb, MATROSKA_ID_TRACKTYPE, MATROSKA_TRACK_TYPE_SUBTITLE); | |||||
| if (!native_id) { | |||||
| av_log(s, AV_LOG_ERROR, "Subtitle codec %d is not supported.\n", codec->codec_id); | |||||
| return AVERROR(ENOSYS); | |||||
| } | |||||
| break; | |||||
| default: | |||||
| av_log(s, AV_LOG_ERROR, "Only audio, video, and subtitles are supported for Matroska.\n"); | |||||
| break; | |||||
| } | |||||
| ret = mkv_write_codecprivate(s, pb, codec, native_id, qt_id); | |||||
| tracks = start_ebml_master(pb, MATROSKA_ID_TRACKS, 0); | |||||
| for (i = 0; i < s->nb_streams; i++) { | |||||
| ret = mkv_write_track(s, mkv, i, pb); | |||||
| if (ret < 0) | if (ret < 0) | ||||
| return ret; | return ret; | ||||
| end_ebml_master(pb, track); | |||||
| } | } | ||||
| end_ebml_master(pb, tracks); | end_ebml_master(pb, tracks); | ||||
| return 0; | return 0; | ||||