/* * libmedia matroska decoder * * 版权所有 (C) 2024 赵高兴 * Copyright (C) 2024 Gaoxing Zhao * * 此文件是 libmedia 的一部分 * This file is part of libmedia. * * libmedia 是自由软件;您可以根据 GNU Lesser General Public License(GNU LGPL)3.1 * 或任何其更新的版本条款重新分发或修改它 * libmedia 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 3.1 of the License, or (at your option) any later version. * * libmedia 希望能够为您提供帮助,但不提供任何明示或暗示的担保,包括但不限于适销性或特定用途的保证 * 您应自行承担使用 libmedia 的风险,并且需要遵守 GNU Lesser General Public License 中的条款和条件。 * libmedia 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. * */ import type { AVIFormatContext } from '../AVFormatContext' import IFormat from './IFormat' import { EBMLId, MATROSKABlockAddIdType, MATROSKALacingMode, MATROSKATrackEncodingComp, MATROSKATrackType, MkvImageMime2CodecId, MkvTag2CodecId, WebmTag2CodecId } from './matroska/matroska' import type { Additions, ClusterIndex, MatroskaContext, TrackEntry } from './matroska/type' import { EbmlSyntaxAttachments, EbmlSyntaxBlockGroup, EbmlSyntaxChapters, EbmlSyntaxCluster, EbmlSyntaxCues, EbmlSyntaxHeadSeek, EbmlSyntaxHeader, EbmlSyntaxInfo, EbmlSyntaxTags, EbmlSyntaxTracks, parseEbmlSyntax, readEbmlId, readVInt, readVInt64, readVSint } from './matroska/imatroska' import findStreamByTrackUid from './matroska/function/findStreamByTrackUid' import findStreamByTrackNumber from './matroska/function/findStreamByTrackNumber' import * as riff from './riff/riff' import * as isomTags from './isom/tags' import { memcpyFromUint8Array, mapUint8Array } from '@libmedia/cheap' import { AVDisposition, AVFormat, AVSeekFlags, IOFlags, AVMediaType, AVCodecID, type AVPacket, type AVStream, avMalloc, avFree, createAVPacket, getAVPacketData, addAVPacketData, addAVPacketSideData, AVPacketFlags, AVStreamMetadataKey, AVCodecParameterFlags, NOPTS_VALUE_BIGINT, AVPacketSideDataType, avRescaleQ, errorType, nalu as naluUtil, intwrite } from '@libmedia/avutil' import { AV_MILLI_TIME_BASE_Q, AV_TIME_BASE, AV_TIME_BASE_Q, mp3, h264, hevc, vvc, aac, av1, vp9, vp8, flac, opus } from '@libmedia/avutil/internal' import { array, concatTypeArray, logger, is, object, text, isDef, string } from '@libmedia/common' import { IOError, BufferReader } from '@libmedia/common/io' /** * 毫秒时间戳转 hh:mm:ss.mill * * @param time */ function int642HHColonDDColonSSDotMill(time: int64) { if (time < 0) { time = 0n } const ms = Math.round(static_cast(time % 1000n) / 10) const secs = static_cast(time / 1000n % 60n) const mins = static_cast(time / 1000n / 60n % 60n) const hours = static_cast(time / 1000n / 3600n) return string.format('%s:%02d:%02d.%02d', hours, mins, secs, ms) } export default class IMatroskaFormat extends IFormat { public type: AVFormat = AVFormat.MATROSKA private context: MatroskaContext private blockReader: BufferReader constructor() { super() } public init(formatContext: AVIFormatContext): void { if (formatContext.ioReader) { formatContext.ioReader.setEndian(true) } const context: MatroskaContext = { isLive: true, firstCluster: -1n, segmentStart: -1n, header: { version: 0, readVersion: 0, maxIdLength: 4, maxSizeLength: 8, docType: '', docTypeVersion: 0, docTypeReadVersion: 0 }, seekHead: null, info: null, tracks: null, attachments: null, chapters: null, cues: null, tags: null, maxPts: NOPTS_VALUE_BIGINT, currentCluster: { timeCode: 0n, pos: -1n, block: { pos: -1n, size: -1n }, blockGroup: { block: null } }, clusterIndexes: [], clusterIndexesPosMap: new Map() } formatContext.privateData = this.context = context } private analyzeStreams(formatContext: AVIFormatContext) { let duration = NOPTS_VALUE_BIGINT const tag2CodecId = this.context.header.docType === 'webm' ? WebmTag2CodecId : MkvTag2CodecId if (this.context.tracks) { array.each(this.context.tracks.entry, (track) => { const stream = formatContext.createStream() stream.privData = track stream.codecpar.codecId = tag2CodecId[track.codecId] || AVCodecID.AV_CODEC_ID_NONE switch (track.type) { case MATROSKATrackType.AUDIO: stream.codecpar.codecType = AVMediaType.AVMEDIA_TYPE_AUDIO break case MATROSKATrackType.VIDEO: stream.codecpar.codecType = AVMediaType.AVMEDIA_TYPE_VIDEO stream.codecpar.flags |= AVCodecParameterFlags.AV_CODECPAR_FLAG_NO_DTS break case MATROSKATrackType.SUBTITLE: stream.codecpar.codecType = AVMediaType.AVMEDIA_TYPE_SUBTITLE break default: stream.codecpar.codecType = AVMediaType.AVMEDIA_TYPE_UNKNOWN } if (track.language) { stream.metadata[AVStreamMetadataKey.LANGUAGE] = track.language } if (track.name) { stream.metadata[AVStreamMetadataKey.TITLE] = track.name } if (track.audio) { if (track.codecName === 'A_PCM/FLOAT/IEEE') { if (track.audio.bitDepth === 64) { stream.codecpar.codecId = AVCodecID.AV_CODEC_ID_PCM_F64LE } else if (track.audio.bitDepth === 32) { stream.codecpar.codecId = AVCodecID.AV_CODEC_ID_PCM_F32LE } } else if (track.codecName === 'A_PCM/INT/BIG') { if (track.audio.bitDepth === 16) { stream.codecpar.codecId = AVCodecID.AV_CODEC_ID_PCM_S16BE } else if (track.audio.bitDepth === 24) { stream.codecpar.codecId = AVCodecID.AV_CODEC_ID_PCM_S24BE } else if (track.audio.bitDepth === 32) { stream.codecpar.codecId = AVCodecID.AV_CODEC_ID_PCM_S32BE } } else if (track.codecName === 'A_PCM/INT/LIT') { if (track.audio.bitDepth === 8) { stream.codecpar.codecId = AVCodecID.AV_CODEC_ID_PCM_U8 } else if (track.audio.bitDepth === 16) { stream.codecpar.codecId = AVCodecID.AV_CODEC_ID_PCM_S16LE } else if (track.audio.bitDepth === 24) { stream.codecpar.codecId = AVCodecID.AV_CODEC_ID_PCM_S24LE } else if (track.audio.bitDepth === 32) { stream.codecpar.codecId = AVCodecID.AV_CODEC_ID_PCM_S32LE } } stream.codecpar.chLayout.nbChannels = track.audio.channels stream.codecpar.sampleRate = static_cast(track.audio.sampleRate) stream.codecpar.bitsPerCodedSample = track.audio.bitDepth } else if (track.video) { stream.codecpar.width = track.video.pixelWidth stream.codecpar.height = track.video.pixelHeight if (track.video.color) { if (is.number(track.video.color.primaries)) { stream.codecpar.colorPrimaries = track.video.color.primaries } if (is.number(track.video.color.transferCharacteristics)) { stream.codecpar.colorTrc = track.video.color.transferCharacteristics } if (is.number(track.video.color.range)) { stream.codecpar.colorRange = track.video.color.range } if (is.number(track.video.color.matrixCoefficients)) { stream.codecpar.colorSpace = track.video.color.matrixCoefficients } } } let extradataOffset = 0 if (track.codecId === 'V_MS/VFW/FOURCC' && track.codecPrivate?.size >= 40) { stream.codecpar.bitsPerCodedSample = (track.codecPrivate.data[15] << 8) | track.codecPrivate.data[14] stream.codecpar.codecTag = (track.codecPrivate.data[19] << 24) | (track.codecPrivate.data[18] << 16) | (track.codecPrivate.data[17] << 8) | track.codecPrivate.data[16] stream.codecpar.codecId = riff.codecBmpTags[stream.codecpar.codecTag] || AVCodecID.AV_CODEC_ID_NONE if (stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_NONE) { stream.codecpar.codecId === isomTags.codecIsobmffVideoTags[stream.codecpar.codecTag] || AVCodecID.AV_CODEC_ID_NONE } extradataOffset = 40 } else if (track.codecId === 'V_QUICKTIME' && track.codecPrivate?.size >= 21) { const tags = stream.codecpar.codecType === AVMediaType.AVMEDIA_TYPE_AUDIO ? isomTags.codecIsobmffAudioTags : isomTags.codecIsobmffVideoTags stream.codecpar.codecTag = (track.codecPrivate.data[3] << 24) | (track.codecPrivate.data[2] << 16) | (track.codecPrivate.data[1] << 8) | track.codecPrivate.data[0] let codecId = tags[stream.codecpar.codecTag] if (codecId) { const data = new Uint8Array(4) const size = static_cast(track.codecPrivate.size) data[0] = (size >>> 24) & 0xff data[1] = (size >>> 16) & 0xff data[2] = (size >>> 8) & 0xff data[3] = size & 0xff track.codecPrivate.size += 4n track.codecPrivate.data = concatTypeArray(Uint8Array, [data, track.codecPrivate.data]) } else { stream.codecpar.codecTag = (track.codecPrivate.data[7] << 24) | (track.codecPrivate.data[6] << 16) | (track.codecPrivate.data[5] << 8) | track.codecPrivate.data[4] codecId = tags[stream.codecpar.codecTag] } stream.codecpar.codecId = codecId || AVCodecID.AV_CODEC_ID_NONE } if (track.codecPrivate?.data && (track.codecPrivate.size - static_cast(extradataOffset)) > 0) { let codecPrivateData = track.codecPrivate.data.subarray(extradataOffset) if (stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_SSA || stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_ASS ) { track.ass = { hasLayer: false } const header = text.decode(codecPrivateData) let lines = header.split(/\r?\n/) let format: string for (let i = 0; i < lines.length; i++) { if (lines[i].trim() === '[Events]') { if (lines[i + 1] && /^Format:/.test(lines[i + 1])) { format = lines[i + 1] } lines = lines.slice(0, i) break } } if (format) { format = format.replace(/^Format:/, '') const list = format.split(',').map((v) => v.trim()).filter((v) => v !== 'Start' && v !== 'End') track.ass.hasLayer = list[0] === 'Layer' if (track.ass.hasLayer) { list.splice(1, 0, 'Start', 'End') } else { list.unshift('End') list.unshift('Start') } format = `Format: ${list.join(', ')}` } // add the default Events Format lines.push('[Events]') lines.push(format || 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text') if (!format) { track.ass.hasLayer = true } codecPrivateData = text.encode(lines.join('\n')) } stream.codecpar.extradataSize = codecPrivateData.length stream.codecpar.extradata = avMalloc(codecPrivateData.length) memcpyFromUint8Array(stream.codecpar.extradata, codecPrivateData.length, codecPrivateData) if (stream.codecpar.extradata) { switch (stream.codecpar.codecId) { case AVCodecID.AV_CODEC_ID_H264: { const extradata = mapUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize)) h264.parseAVCodecParameters(stream, extradata) if (naluUtil.isAnnexb(extradata)) { stream.codecpar.flags |= AVCodecParameterFlags.AV_CODECPAR_FLAG_H26X_ANNEXB } break } case AVCodecID.AV_CODEC_ID_HEVC: { const extradata = mapUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize)) hevc.parseAVCodecParameters(stream, extradata) if (naluUtil.isAnnexb(extradata)) { stream.codecpar.flags |= AVCodecParameterFlags.AV_CODECPAR_FLAG_H26X_ANNEXB } break } case AVCodecID.AV_CODEC_ID_VVC: { const extradata = mapUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize)) vvc.parseAVCodecParameters(stream, extradata) if (naluUtil.isAnnexb(extradata)) { stream.codecpar.flags |= AVCodecParameterFlags.AV_CODECPAR_FLAG_H26X_ANNEXB } break } case AVCodecID.AV_CODEC_ID_AV1: av1.parseAVCodecParameters(stream, mapUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize))) break case AVCodecID.AV_CODEC_ID_VP8: vp8.parseAVCodecParameters(stream, mapUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize))) break case AVCodecID.AV_CODEC_ID_VP9: vp9.parseAVCodecParameters(stream, mapUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize))) break case AVCodecID.AV_CODEC_ID_AAC: aac.parseAVCodecParameters(stream, mapUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize))) break case AVCodecID.AV_CODEC_ID_MP3: mp3.parseAVCodecParameters(stream, mapUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize))) break case AVCodecID.AV_CODEC_ID_OPUS: opus.parseAVCodecParameters(stream, mapUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize))) break case AVCodecID.AV_CODEC_ID_FLAC: { if (stream.codecpar.extradataSize > flac.FLAC_STREAMINFO_SIZE) { const bufferReader = new BufferReader(mapUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize))) if (bufferReader.readString(4) === 'fLaC') { while (bufferReader.remainingSize()) { const blockHeader = bufferReader.readUint8() const blockLen = bufferReader.readUint24() const blockType = blockHeader & (~0x80) if (blockType === flac.FlacMetadataType.FLAC_METADATA_TYPE_STREAMINFO) { const extradata = bufferReader.readBuffer(blockLen) avFree(stream.codecpar.extradata) stream.codecpar.extradata = avMalloc(extradata.length) stream.codecpar.extradataSize = extradata.length memcpyFromUint8Array(stream.codecpar.extradata, extradata.length, extradata) break } bufferReader.skip(blockType) } } } if (stream.codecpar.extradataSize === flac.FLAC_STREAMINFO_SIZE) { flac.parseAVCodecParameters(stream, mapUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize))) } } } } else { let extradata: Uint8Array switch (stream.codecpar.codecId) { case AVCodecID.AV_CODEC_ID_AAC: extradata = aac.avCodecParameters2Extradata(stream.codecpar) break } if (extradata) { stream.codecpar.extradataSize = extradata.length stream.codecpar.extradata = avMalloc(reinterpret_cast(stream.codecpar.extradataSize)) memcpyFromUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize), extradata) } } } if (this.context.info) { stream.timeBase.num = 1 if (this.context.info.timestampScale) { stream.timeBase.den = static_cast((AV_TIME_BASE * 1000) / this.context.info.timestampScale) } else { stream.timeBase.den = 1000 this.context.info.timestampScale = AV_TIME_BASE } if (this.context.info.duration) { stream.duration = static_cast(this.context.info.duration) } } if (track.default == null || track.default) { stream.disposition |= AVDisposition.DEFAULT } if (stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_WEBVTT) { if (track.codecId === 'D_WEBVTT/CAPTIONS') { stream.disposition |= AVDisposition.CAPTIONS } else if (track.codecId === 'D_WEBVTT/DESCRIPTIONS') { stream.disposition != AVDisposition.DESCRIPTIONS } else if (track.codecId === 'D_WEBVTT/METADATA') { stream.disposition != AVDisposition.METADATA } } if (track.encodings) { array.each(track.encodings.entry, (entry) => { if (entry.compression && isDef(entry.compression.algo)) { track.needDecompression = true } if (entry.encryption && isDef(entry.encryption.algo)) { track.needDecryption = true } }) } }) } if (this.context.attachments) { array.each(this.context.attachments.entry, (attachment) => { const stream = formatContext.createStream() stream.codecpar.codecType = AVMediaType.AVMEDIA_TYPE_ATTACHMENT stream.privData = attachment if (attachment.name) { stream.metadata[AVStreamMetadataKey.TITLE] = attachment.name } if (attachment.mime) { stream.metadata[AVStreamMetadataKey.MIME] = attachment.mime } if (attachment.description) { stream.metadata[AVStreamMetadataKey.DESCRIPTION] = attachment.description } if (attachment.data) { if (MkvImageMime2CodecId[attachment.mime]) { stream.codecpar.codecId = MkvImageMime2CodecId[attachment.mime] stream.codecpar.codecType = AVMediaType.AVMEDIA_TYPE_VIDEO stream.disposition |= AVDisposition.ATTACHED_PIC stream.attachedPic = createAVPacket() stream.attachedPic.streamIndex = stream.index const data: pointer = avMalloc(static_cast(attachment.data.size)) memcpyFromUint8Array(data, static_cast(attachment.data.size), attachment.data.data) addAVPacketData(stream.attachedPic, data, static_cast(attachment.data.size)) stream.attachedPic.flags |= AVPacketFlags.AV_PKT_FLAG_KEY } else { stream.codecpar.extradataSize = static_cast(attachment.data.size) stream.codecpar.extradata = avMalloc(reinterpret_cast(stream.codecpar.extradataSize)) memcpyFromUint8Array(stream.codecpar.extradata, reinterpret_cast(stream.codecpar.extradataSize), attachment.data.data) } } }) } if (this.context.tags) { array.each(this.context.tags.entry, (tag) => { if (tag.tag) { tag.tag.forEach((t) => { const key = t.name let value: any = t.string if (tag.target) { if (key === 'DURATION') { let time = t.string.replaceAll('\x00', '').split('.') let f = time[0].split(':') value = BigInt(+f[0]) * BigInt(1000000 * 60 * 60) + BigInt(+f[1]) * BigInt(1000000 * 60) + BigInt(+f[2]) * 1000000n + (BigInt(+time[1]) / 1000n) } if (tag.target.chapterUid) { for (let i = 0; i < formatContext.chapters.length; i++) { const chapter = formatContext.chapters[i] if (chapter.id === tag.target.chapterUid) { chapter.metadata[key] = value } } } else if (tag.target.trackUid || tag.target.attachUid) { const stream = findStreamByTrackUid(formatContext.streams, tag.target.trackUid || tag.target.attachUid) if (stream) { if (key === 'DURATION') { stream.duration = avRescaleQ(value, AV_TIME_BASE_Q, stream.timeBase) if (value > duration) { duration = value } } else { stream.metadata[key] = value } } } else { formatContext.metadata[key] = value } } else { formatContext.metadata[key] = value } }) } }) } if (this.context.info) { if (this.context.info.muxingApp) { formatContext.metadata[AVStreamMetadataKey.ENCODER] = this.context.info.muxingApp } if (this.context.info.title) { formatContext.metadata[AVStreamMetadataKey.TITLE] = this.context.info.title } if (this.context.info.dateUTC?.data?.byteLength === 8) { const view = new DataView(this.context.info.dateUTC.data.buffer) // Convert to seconds and adjust by number of seconds between 2001-01-01 and Epoch const ts = view.getBigUint64(0) / 1000000n + 978307200000n formatContext.metadata[AVStreamMetadataKey.CREATION_TIME] = (new Date(Number(ts))).toISOString() } } if (this.context.chapters) { array.each(this.context.chapters.entry, (chapter) => { const atom = chapter.atom if (atom) { array.each(atom, (item) => { formatContext.chapters.push({ id: item.uid, start: item.start ?? NOPTS_VALUE_BIGINT, end: item.end ?? NOPTS_VALUE_BIGINT, timeBase: { num: 1, den: 1000000000 }, metadata: { [AVStreamMetadataKey.TITLE]: item.display?.title || '', [AVStreamMetadataKey.LANGUAGE]: item.display?.language || '' } }) }) } }) if (formatContext.chapters.length) { for (let i = 0; i < formatContext.chapters.length - 1; i++) { if (formatContext.chapters[i].end === NOPTS_VALUE_BIGINT) { formatContext.chapters[i].end = formatContext.chapters[i + 1].start } } if (formatContext.chapters[formatContext.chapters.length - 1].end === NOPTS_VALUE_BIGINT) { formatContext.chapters[formatContext.chapters.length - 1].end = avRescaleQ( duration, AV_TIME_BASE_Q, formatContext.chapters[formatContext.chapters.length - 1].timeBase ) } } } } public async readHeader(formatContext: AVIFormatContext): Promise { const magic = await readEbmlId(formatContext, this.context.header.maxIdLength) if (magic !== EBMLId.HEADER) { logger.error('not matroska format') return errorType.DATA_INVALID } const headerSize = await readVInt64(formatContext.ioReader, this.context.header.maxSizeLength) this.context.header = object.extend(this.context.header, await parseEbmlSyntax(formatContext, headerSize, EbmlSyntaxHeader)) const segmentId = await readEbmlId(formatContext, this.context.header.maxIdLength) if (segmentId !== EBMLId.SEGMENT) { logger.error('not matroska format') return errorType.DATA_INVALID } const segmentSize = await readVInt64(formatContext.ioReader, this.context.header.maxSizeLength) this.context.segmentStart = formatContext.ioReader.getPos() const segmentEndPos = formatContext.ioReader.getPos() + segmentSize const readTopLevelEbml: Set = new Set() let hasTracks = false let hasCluster = false while (formatContext.ioReader.getPos() < segmentEndPos || !hasTracks || !hasCluster) { const currentElementPos = formatContext.ioReader.getPos() const id = await readEbmlId(formatContext, this.context.header.maxIdLength) if (id === errorType.DATA_INVALID) { await this.syncTopLevelElement(formatContext, 2) continue } const length = await readVInt64(formatContext.ioReader, this.context.header.maxSizeLength) if (length === static_cast(errorType.DATA_INVALID as int32)) { await this.syncTopLevelElement(formatContext, 2) continue } if (id === EBMLId.SEEK_HEAD) { this.context.isLive = false this.context.seekHead = await parseEbmlSyntax(formatContext, length, EbmlSyntaxHeadSeek) } else if (id === EBMLId.CUES) { this.context.isLive = false this.context.cues = await parseEbmlSyntax(formatContext, length, EbmlSyntaxCues) } else if (id === EBMLId.CHAPTERS) { this.context.isLive = false this.context.chapters = await parseEbmlSyntax(formatContext, length, EbmlSyntaxChapters) } else if (id === EBMLId.INFO) { this.context.info = await parseEbmlSyntax(formatContext, length, EbmlSyntaxInfo) } else if (id === EBMLId.TAGS) { this.context.tags = await parseEbmlSyntax(formatContext, length, EbmlSyntaxTags) } else if (id === EBMLId.ATTACHMENTS) { this.context.attachments = await parseEbmlSyntax(formatContext, length, EbmlSyntaxAttachments) } else if (id === EBMLId.TRACKS) { hasTracks = true this.context.tracks = await parseEbmlSyntax(formatContext, length, EbmlSyntaxTracks) } else if (id === EBMLId.CLUSTER) { hasCluster = true this.context.firstCluster = currentElementPos if (this.context.isLive || !this.context.seekHead || !(formatContext.ioReader.flags & IOFlags.SEEKABLE)) { break } const entry = this.context.seekHead.entry let i = 0 for (; i < entry.length; i++) { if (!readTopLevelEbml.has(entry[i].id)) { await formatContext.ioReader.seek(entry[i].pos + this.context.segmentStart) break } } if (i === entry.length) { break } } else { await formatContext.ioReader.skip(static_cast(length)) } readTopLevelEbml.add(id) } if (this.context.firstCluster > 0) { await formatContext.ioReader.seek(this.context.firstCluster) } this.analyzeStreams(formatContext) return 0 } private parseAdditions(avpacket: pointer, additions: Additions, stream: AVStream) { for (let i = 0; i < additions.entry.length; i++) { const addition = additions.entry[i] if (addition.additional?.size) { if (addition.additionalId === MATROSKABlockAddIdType.ITU_T_T35) { // TODO handle ITU_T_T35 logger.warn('ITU_T_T35 not support now') } if (stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_WEBVTT) { const content = text.decode(addition.additional.data) const list = content.split('\n') if (list.length >= 2) { if (list[0]) { const buffer = text.encode(list[0]) const data = avMalloc(buffer.length) memcpyFromUint8Array(data, buffer.length, buffer) addAVPacketSideData(avpacket, AVPacketSideDataType.AV_PKT_DATA_WEBVTT_SETTINGS, data, buffer.length) } if (list[1]) { const buffer = text.encode(list[1]) const data = avMalloc(buffer.length) memcpyFromUint8Array(data, buffer.length, buffer) addAVPacketSideData(avpacket, AVPacketSideDataType.AV_PKT_DATA_WEBVTT_IDENTIFIER, data, buffer.length) } } } else { const data = avMalloc(addition.additional.data.length + 8) intwrite.wb64(data, static_cast(addition.additionalId ?? MATROSKABlockAddIdType.OPAQUE)) memcpyFromUint8Array(data + 8, addition.additional.data.length, addition.additional.data) addAVPacketSideData(avpacket, AVPacketSideDataType.AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL, data, addition.additional.data.length + 8) if (addition.additionalId === 1 && (stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_VP8 || stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_VP9 || stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_AV1 ) ) { stream.codecpar.flags |= AVCodecParameterFlags.AV_CODECPAR_FLAG_ALPHA } } } } } private processAss(avpacket: pointer, stream: AVStream) { const track = stream.privData as TrackEntry const context = text.decode(getAVPacketData(avpacket)) const list = context.split(',').map((v) => v.trim()) // 去掉 readOrder list.shift() const start = int642HHColonDDColonSSDotMill(avRescaleQ(avpacket.pts, avpacket.timeBase, AV_MILLI_TIME_BASE_Q)) const end = int642HHColonDDColonSSDotMill(avRescaleQ(avpacket.pts + avpacket.duration, avpacket.timeBase, AV_MILLI_TIME_BASE_Q)) if (track.ass.hasLayer) { list.splice(1, 0, start, end) } else { list.unshift(end) list.unshift(start) } const buffer = text.encode(`Dialogue: ${list.join(',')}`) const data: pointer = avMalloc(buffer.length) memcpyFromUint8Array(data, buffer.length, buffer) addAVPacketData(avpacket, data, buffer.length) } private async parseBlock(formatContext: AVIFormatContext, packet: pointer) { const buffer = this.context.currentCluster.block?.data || this.context.currentCluster.blockGroup.block.data let basePos = this.context.currentCluster.block?.pos if (basePos < 0) { basePos = this.context.currentCluster.blockGroup.block.pos } let isKey = -1 let additions: Additions let duration = 0n if (this.context.currentCluster.blockGroup?.block) { additions = this.context.currentCluster.blockGroup.additions if (this.context.currentCluster.blockGroup.reference) { isKey = this.context.currentCluster.blockGroup.reference.length === 0 ? 1 : 0 } else { isKey = 1 } if (this.context.currentCluster.blockGroup.duration) { duration = this.context.currentCluster.blockGroup.duration } } if (!this.blockReader) { this.blockReader = new BufferReader(buffer) } else { this.blockReader.resetBuffer(buffer) } const now = this.blockReader.getPos() const trackNumber = static_cast(await readVInt64(this.blockReader, 8)) const stream = findStreamByTrackNumber(formatContext.streams, trackNumber) if (!stream) { logger.error(`invalid track number ${trackNumber}`) return errorType.EAGAIN } const timestamp = this.blockReader.readInt16() const flags = this.blockReader.readUint8() if (isKey === -1) { isKey = (flags & 0x80) ? 1 : 0 } const lacing = (flags >>> 1) & 0x03 let frameCount = 0 let frameSize: number[] = [] switch (lacing) { case MATROSKALacingMode.XIPH: { frameCount = this.blockReader.readUint8() + 1 let sum = 0 for (let i = 0; i < frameCount - 1; i++) { let size = 0 while (true) { const next = this.blockReader.readUint8() if (next === 0) { break } if (next !== 0xff) { size += next break } size += 0xff } sum += size frameSize.push(size) } // the last frame frameSize.push(buffer.length - static_cast(this.blockReader.getPos() - now) - sum) break } case MATROSKALacingMode.EBML: { frameCount = this.blockReader.readUint8() + 1 frameSize.push(await readVInt(this.blockReader, 4)) let sum = frameSize[0] for (let i = 1; i < frameCount - 1; i++) { const next = await readVSint(this.blockReader, 4) let size = next + frameSize[i - 1] sum += size frameSize.push(size) } // the last frame frameSize.push(buffer.length - static_cast(this.blockReader.getPos() - now) - sum) break } case MATROSKALacingMode.FIXED_SIZE: frameCount = this.blockReader.readUint8() + 1 const size = (buffer.length - static_cast(this.blockReader.getPos() - now)) / frameCount assert(size === (size >>> 0)) for (let i = 0; i < frameCount; i++) { frameSize.push(size) } break case MATROSKALacingMode.NO_LACING: frameCount = 1 frameSize.push(buffer.length - static_cast(this.blockReader.getPos() - now)) break } const track = stream.privData as TrackEntry const trackTimestampScale = track.timeScale || 1 if (track.needDecryption) { throw new Error('not support encryption stream') } // 纳秒时间戳 let pts = (this.context.currentCluster.timeCode + static_cast((timestamp * trackTimestampScale) as float)) * static_cast(this.context.info.timestampScale) if (track.codecDelay) { pts -= track.codecDelay } // 微秒时间戳 pts /= 1000n pts = avRescaleQ(pts, AV_TIME_BASE_Q, stream.timeBase) duration = static_cast((static_cast(duration) * trackTimestampScale) as float) * static_cast(this.context.info.timestampScale) duration /= 1000n duration = avRescaleQ(duration, AV_TIME_BASE_Q, stream.timeBase) for (let i = 0; i < frameCount; i++) { const avpacket = i !== 0 ? createAVPacket() : packet let size = frameSize[i] let offset = 0 let header: Uint8Array if (track.needDecompression) { const compression = track.encodings.entry.find((entry) => { return !!entry.compression }) switch (compression.compression.algo) { case MATROSKATrackEncodingComp.HEADER_STRIP: header = compression.compression.settings.data size += header.length offset = header.length break default: logger.fatal(`not support compression stream, algo: ${compression.compression.algo}`) } } avpacket.pos = basePos + this.blockReader.getPos() avpacket.streamIndex = stream.index avpacket.timeBase = stream.timeBase avpacket.pts = pts avpacket.size = size avpacket.duration = duration const data: pointer = avMalloc(size) if (header) { memcpyFromUint8Array(data, offset, header) } memcpyFromUint8Array(data + offset, frameSize[i], this.blockReader.readBuffer(frameSize[i])) addAVPacketData(avpacket, data, size) if (stream.codecpar.codecType !== AVMediaType.AVMEDIA_TYPE_VIDEO) { avpacket.flags |= AVPacketFlags.AV_PKT_FLAG_KEY avpacket.dts = pts } else { if (isKey) { avpacket.flags |= AVPacketFlags.AV_PKT_FLAG_KEY } if (stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_H264 || stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_HEVC || stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_VVC ) { if (stream.codecpar.flags & AVCodecParameterFlags.AV_CODECPAR_FLAG_H26X_ANNEXB) { avpacket.flags |= AVPacketFlags.AV_PKT_FLAG_H26X_ANNEXB } else { avpacket.flags &= ~AVPacketFlags.AV_PKT_FLAG_H26X_ANNEXB } } } if (additions) { this.parseAdditions(avpacket, additions, stream) } if (stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_SSA || stream.codecpar.codecId === AVCodecID.AV_CODEC_ID_ASS ) { this.processAss(avpacket, stream) } if (i !== 0) { formatContext.interval.packetBuffer.push(avpacket) } if (this.context.maxPts < pts) { this.context.maxPts = pts } } this.context.currentCluster.block = { pos: -1n, size: -1n } this.context.currentCluster.blockGroup = { block: null } return 0 } private addClusterIndex(clusterIndex: ClusterIndex) { if (this.context.clusterIndexesPosMap.has(clusterIndex.pos)) { return } const index = array.binarySearch(this.context.clusterIndexes, (sample) => { if (sample.time < clusterIndex.time) { return 1 } else { return -1 } }) if (index > -1) { this.context.clusterIndexesPosMap.set(clusterIndex.pos, index) this.context.clusterIndexes.splice(index, 0, clusterIndex) } else { this.context.clusterIndexesPosMap.set(clusterIndex.pos, this.context.clusterIndexes.length) this.context.clusterIndexes.push(clusterIndex) } } private async readAVPacket_(formatContext: AVIFormatContext, avpacket: pointer): Promise { const now = formatContext.ioReader.getPos() const id = await readEbmlId(formatContext, this.context.header.maxIdLength) if (id === errorType.DATA_INVALID) { await this.syncTopLevelElement(formatContext) return this.readAVPacket_(formatContext, avpacket) } const length = await readVInt64(formatContext.ioReader, this.context.header.maxSizeLength) if (length === static_cast(errorType.DATA_INVALID)) { await this.syncTopLevelElement(formatContext) return this.readAVPacket_(formatContext, avpacket) } if (id === EBMLId.CLUSTER) { this.context.currentCluster.pos = now this.context.currentCluster.timeCode = 0n await parseEbmlSyntax( formatContext, length, EbmlSyntaxCluster, this.context.currentCluster, [EBMLId.SIMPLE_BLOCK, EBMLId.BLOCK_GROUP] ) if (!this.context.isLive) { this.addClusterIndex({ time: this.context.currentCluster.timeCode, pos: now }) } let ret = await this.parseBlock(formatContext, avpacket) if (ret === errorType.EAGAIN) { return this.readAVPacket_(formatContext, avpacket) } else if (ret < 0) { return ret } } else if (id === EBMLId.SIMPLE_BLOCK) { this.context.currentCluster.block = { pos: formatContext.ioReader.getPos(), size: length, data: await formatContext.ioReader.readBuffer(static_cast(length)) } let ret = await this.parseBlock(formatContext, avpacket) if (ret === errorType.EAGAIN) { return this.readAVPacket_(formatContext, avpacket) } else if (ret < 0) { return ret } } else if (id === EBMLId.BLOCK_GROUP) { await parseEbmlSyntax( formatContext, length, EbmlSyntaxBlockGroup, this.context.currentCluster.blockGroup ) let ret = await this.parseBlock(formatContext, avpacket) if (ret === errorType.EAGAIN) { return this.readAVPacket_(formatContext, avpacket) } else if (ret < 0) { return ret } } else if (id === EBMLId.CUES || id === EBMLId.TAGS || id === EBMLId.ATTACHMENTS || id === EBMLId.CHAPTERS || id === EBMLId.INFO || id === EBMLId.TRACKS || id === EBMLId.SEEK_HEAD ) { await formatContext.ioReader.skip(static_cast(length)) return this.readAVPacket_(formatContext, avpacket) } else { await this.syncTopLevelElement(formatContext) return this.readAVPacket_(formatContext, avpacket) } return 0 } public async readAVPacket(formatContext: AVIFormatContext, avpacket: pointer): Promise { try { return await this.readAVPacket_(formatContext, avpacket) } catch (error) { if (formatContext.ioReader.error !== IOError.END && formatContext.ioReader.error !== IOError.ABORT ) { logger.error(`read packet error, ${error}`) return errorType.DATA_INVALID } return formatContext.ioReader.error } } private async syncTopLevelElement(formatContext: AVIFormatContext, analyzeCount: int32 = 3) { let pos: int64 = NOPTS_VALUE_BIGINT while (true) { try { if (formatContext.ioReader.flags & IOFlags.ABORT) { break } const now = formatContext.ioReader.getPos() const id = await formatContext.ioReader.peekUint32() if (id === EBMLId.CLUSTER || id === EBMLId.CUES || id === EBMLId.TAGS || id === EBMLId.ATTACHMENTS || id === EBMLId.CHAPTERS || id === EBMLId.INFO || id === EBMLId.TRACKS || id === EBMLId.SEEK_HEAD ) { pos = now let count = 0 await formatContext.ioReader.skip(4) const length = await readVInt64(formatContext.ioReader, this.context.header.maxSizeLength) if (length === static_cast(errorType.DATA_INVALID)) { await formatContext.ioReader.seek(now + 1n) continue } await formatContext.ioReader.skip(static_cast(length)) while (count <= analyzeCount) { const id = await readEbmlId(formatContext, this.context.header.maxIdLength) const length = await readVInt64(formatContext.ioReader, this.context.header.maxSizeLength) if (id === EBMLId.CLUSTER || id === EBMLId.CUES || id === EBMLId.TAGS || id === EBMLId.ATTACHMENTS || id === EBMLId.CHAPTERS || id === EBMLId.INFO || id === EBMLId.TRACKS || id === EBMLId.SEEK_HEAD ) { count++ await formatContext.ioReader.skip(static_cast(length)) } else { break } } if (count < analyzeCount) { await formatContext.ioReader.seek(pos + 1n) pos = NOPTS_VALUE_BIGINT } else { break } } else { await formatContext.ioReader.skip(1) pos = NOPTS_VALUE_BIGINT } } catch (error) { break } } if (pos !== NOPTS_VALUE_BIGINT) { await formatContext.ioReader.seek(pos) } } public async seek(formatContext: AVIFormatContext, stream: AVStream, timestamp: int64, flags: int32): Promise { const now = formatContext.ioReader.getPos() if (flags & AVSeekFlags.BYTE) { await formatContext.ioReader.seek(timestamp) return now } const pts = avRescaleQ(timestamp, stream.timeBase, AV_TIME_BASE_Q) let pos: int64 = NOPTS_VALUE_BIGINT if (this.context.cues?.entry.length) { const track = stream.privData as TrackEntry for (let i = 0; i < this.context.cues.entry.length; i++) { const cue = this.context.cues.entry[i] const time = (cue.time || 0n) * static_cast(this.context.info.timestampScale) / 1000n if (time > pts) { const poses = this.context.cues.entry[Math.max(i - 1, 0)].pos if (poses.length) { const matchPos = poses.find((p) => p.track === track.number) if (matchPos) { pos = matchPos.pos + this.context.segmentStart break } } } } } if (pos === NOPTS_VALUE_BIGINT && this.context.clusterIndexes.length) { for (let i = 0; i < this.context.clusterIndexes.length; i++) { const time = this.context.clusterIndexes[i].time * static_cast(this.context.info.timestampScale) / 1000n if (time > pts) { pos = this.context.clusterIndexes[Math.max(i - 1, 0)].pos break } } if (pos === NOPTS_VALUE_BIGINT) { pos = this.context.clusterIndexes[this.context.clusterIndexes.length - 1].pos } } if (pos !== NOPTS_VALUE_BIGINT) { await formatContext.ioReader.seek(pos) return now } return static_cast(errorType.FORMAT_NOT_SUPPORT) } public getAnalyzeStreamsCount(): number { // mkv 在 readheader 时分析了 track element,不需要在进行流分析 return 0 } }