UNPKG

shaka-player

Version:
746 lines (696 loc) • 26.5 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.transmuxer.MatroskaTransmuxer'); goog.require('shaka.codec.AAC'); goog.require('shaka.codec.Ac3'); goog.require('shaka.codec.DolbyVision'); goog.require('shaka.codec.Ec3'); goog.require('shaka.codec.Flac'); goog.require('shaka.codec.MpegAudio'); goog.require('shaka.codec.Opus'); goog.require('shaka.codec.Vorbis'); goog.require('shaka.media.Capabilities'); goog.require('shaka.mkv.CodecId'); goog.require('shaka.mkv.MatroskaClusterParser'); goog.require('shaka.mkv.MatroskaCodecs'); goog.require('shaka.mkv.MatroskaConstants'); goog.require('shaka.mkv.MatroskaIndexParser'); goog.require('shaka.transmuxer.BaseTransmuxer'); goog.require('shaka.transmuxer.TransmuxerEngine'); goog.require('shaka.transmuxer.TransmuxerUtils'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MimeUtils'); goog.require('shaka.util.Mp4Generator'); goog.require('shaka.util.Uint8ArrayUtils'); goog.require('shaka.util.WebmGenerator'); goog.requireType('shaka.media.SegmentReference'); /** * Repackages the frames of one Matroska track into what MSE plays: fragmented * MP4, or WebM for the codecs that browsers only take there (VP8, Vorbis). The * encoded frames are copied as they are; nothing is decoded or transcoded. * * The initialization segment of a track (the beginning of the file, up to the * end of the track's TrackEntry) has no media to convert, so it only tells the * transmuxer what the track is. The initialization segment that MSE gets is * produced together with the first media segment. * * @extends {shaka.transmuxer.BaseTransmuxer} * @implements {shaka.extern.Transmuxer} * @export */ shaka.transmuxer.MatroskaTransmuxer = class extends shaka.transmuxer.BaseTransmuxer { /** * @param {string} mimeType */ constructor(mimeType) { super(mimeType); /** * The tracks and the time scale described by the last initialization * segment. * @private {?shaka.mkv.MatroskaIndexParser.Header} */ this.header_ = null; /** * The track that is being converted. * @private {?shaka.mkv.MatroskaIndexParser.Track} */ this.track_ = null; /** * The decode time at which the next segment is expected to start, in ticks * of the timescale, or null before the first segment. * @private {?number} */ this.nextDecodeTime_ = null; /** * What the first audio segment showed about the audio stream. * @private {?shaka.transmuxer.MatroskaTransmuxer.AudioInfo} */ this.audioInfo_ = null; } /** * @override * @export */ destroy() { super.destroy(); this.header_ = null; this.track_ = null; this.audioInfo_ = null; } /** * @param {string} mimeType * @param {string=} contentType * @return {boolean} * @override * @export */ isSupported(mimeType, contentType) { if (this.getContainer_(mimeType) != this.getOriginalMimeType()) { return false; } const codecs = shaka.util.MimeUtils.getCodecs(mimeType); const MatroskaTransmuxer = shaka.transmuxer.MatroskaTransmuxer; if (!codecs || !MatroskaTransmuxer.isSupportedCodec_(codecs)) { return false; } const ContentType = shaka.util.ManifestParserUtils.ContentType; const type = contentType || (mimeType.startsWith('audio/') ? ContentType.AUDIO : ContentType.VIDEO); return shaka.media.Capabilities.isTypeSupported( this.convertCodecs(type, mimeType)); } /** * @override * @export */ convertCodecs(contentType, mimeType) { if (this.getContainer_(mimeType) != this.getOriginalMimeType()) { return mimeType; } const ContentType = shaka.util.ManifestParserUtils.ContentType; const MimeUtils = shaka.util.MimeUtils; const isAudio = contentType == ContentType.AUDIO; let codecs = MimeUtils.getCodecs(mimeType); if (isAudio && MimeUtils.getNormalizedCodec(codecs) == 'mp3' && shaka.mkv.MatroskaCodecs.usesRawMpegAudio(shaka.mkv.CodecId.MP3)) { return 'audio/mpeg'; } if (isAudio && MimeUtils.getNormalizedCodec(codecs) == 'mp3') { codecs = shaka.mkv.MatroskaCodecs.getMp3Mp4Codec() || codecs; } const isWebm = shaka.transmuxer.MatroskaTransmuxer.WEBM_CODECS_.some( (prefix) => codecs.toLowerCase().startsWith(prefix)); const container = (isAudio ? 'audio' : 'video') + (isWebm ? '/webm' : '/mp4'); if (isAudio) { codecs = MimeUtils.getCorrectAudioCodecs(codecs, container); } return MimeUtils.getFullType(container, codecs); } /** * @override * @export */ transmux(data, stream, reference, duration, contentType) { const ContentType = shaka.util.ManifestParserUtils.ContentType; try { const bytes = shaka.util.BufferUtils.toUint8(data); if (!reference) { this.parseInit_(bytes, stream); // Nothing to append yet: see the description of the class. return Promise.resolve(new Uint8Array(0)); } const trackNumber = shaka.mkv.MatroskaCodecs.getTrackNumber(stream.id); if (!this.header_ || !this.track_ || this.track_.number != trackNumber) { throw new Error('The initialization segment of track ' + trackNumber + ' has not been received'); } const output = contentType == ContentType.AUDIO ? this.transmuxAudio_(bytes, stream, reference, duration) : this.transmuxVideo_(bytes, stream, reference, duration); return Promise.resolve(output); } catch (error) { if (error instanceof shaka.util.Error) { return Promise.reject(error); } return Promise.reject(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MEDIA, shaka.util.Error.Code.TRANSMUXING_FAILED, error && error.message ? error.message : String(error))); } } /** * Learns what the track is from its initialization segment. * * @param {!Uint8Array} bytes * @param {shaka.extern.Stream} stream * @private */ parseInit_(bytes, stream) { const header = shaka.mkv.MatroskaIndexParser.parseHeader(bytes); const trackNumber = shaka.mkv.MatroskaCodecs.getTrackNumber(stream.id); const track = header.tracks.find((entry) => entry.number == trackNumber); if (!track) { throw new Error('Track ' + trackNumber + ' is not in the file'); } this.header_ = header; this.track_ = track; // Everything derived from the previous track no longer applies. this.nextDecodeTime_ = null; this.audioInfo_ = null; } /** * @param {!Uint8Array} bytes * @param {shaka.extern.Stream} stream * @param {?shaka.media.SegmentReference} reference * @param {number} duration * @return {shaka.extern.TransmuxerOutput} * @private */ transmuxVideo_(bytes, stream, reference, duration) { const MatroskaTransmuxer = shaka.transmuxer.MatroskaTransmuxer; const track = /** @type {!shaka.mkv.MatroskaIndexParser.Track} */( this.track_); const header = /** @type {!shaka.mkv.MatroskaIndexParser.Header} */( this.header_); const timescale = MatroskaTransmuxer.VIDEO_TIMESCALE_; if (shaka.mkv.MatroskaCodecs.usesWebm(track.codecId)) { return this.packageWebm_(bytes, stream, reference); } const frames = shaka.mkv.MatroskaClusterParser.parseFrames( bytes, track, header.timecodeScale); if (!frames.length) { return {data: new Uint8Array(0), init: null}; } // Matroska stores only presentation times, and the frames come in decode // order. With B-frames the two orders differ, and a frame must never be // decoded after the moment it is to be presented. The frames decode in // the order of their presentation times, so those, sorted, are the decode // times. The presentation times are moved later by the shift, which is // more than any encoder reorders frames. As the decode times come from the // file alone, they continue from one segment to the next without any need // to know how the frames are reordered. const shift = Math.round( shaka.mkv.MatroskaConstants.MEDIA_TIME_SHIFT * timescale); const times = frames.map((frame) => Math.round(frame.time * timescale)); const presentationTimes = times.map((time) => time + shift); const decodeTimes = times.slice().sort((a, b) => a - b); const offset = this.getContinuityOffset_( decodeTimes[0], header, timescale); const lastDuration = MatroskaTransmuxer.getLastDuration_( track, decodeTimes, timescale, reference); /** @type {!Array<shaka.util.Mp4Generator.Mp4Sample>} */ const samples = []; for (let i = 0; i < frames.length; i++) { const decodeTime = decodeTimes[i] + offset; const nextDecodeTime = i + 1 < frames.length ? decodeTimes[i + 1] + offset : decodeTime + lastDuration; samples.push({ data: frames[i].data, size: frames[i].data.byteLength, duration: nextDecodeTime - decodeTime, cts: presentationTimes[i] - decodeTime, flags: frames[i].keyframe ? shaka.transmuxer.TransmuxerUtils.VIDEO_KEYFRAME_FLAGS : shaka.transmuxer.TransmuxerUtils.VIDEO_NON_KEYFRAME_FLAGS, }); } const baseMediaDecodeTime = decodeTimes[0] + offset; this.nextDecodeTime_ = baseMediaDecodeTime + samples.reduce((sum, sample) => sum + sample.duration, 0); // The stream is Dolby Vision if its codec string says so. const dolbyVisionEntry = track.dolbyVisionConfig ? shaka.codec.DolbyVision.getEntryName(stream.codecs) : null; const codecs = dolbyVisionEntry || shaka.mkv.MatroskaCodecs.getMp4VideoCodec(track.codecId); const pixelAspectRatio = shaka.mkv.MatroskaCodecs.getPixelAspectRatio(track); /** @type {shaka.util.Mp4Generator.StreamInfo} */ const streamInfo = { id: stream.id, type: shaka.util.ManifestParserUtils.ContentType.VIDEO, codecs, timescale, duration, mediaConfig: shaka.mkv.MatroskaCodecs.getVideoConfig( track, stream.codecs), hSpacing: pixelAspectRatio ? pixelAspectRatio.horizontal : undefined, vSpacing: pixelAspectRatio ? pixelAspectRatio.vertical : undefined, dolbyVisionConfig: dolbyVisionEntry ? /** @type {!Uint8Array} */(track.dolbyVisionConfig) : undefined, data: { sequenceNumber: this.frameIndex, baseMediaDecodeTime, samples, }, stream, }; return this.packageSegment( new shaka.util.Mp4Generator([streamInfo]), stream, reference); } /** * @param {!Uint8Array} bytes * @param {shaka.extern.Stream} stream * @param {?shaka.media.SegmentReference} reference * @param {number} duration * @return {shaka.extern.TransmuxerOutput} * @private */ transmuxAudio_(bytes, stream, reference, duration) { const track = /** @type {!shaka.mkv.MatroskaIndexParser.Track} */( this.track_); const header = /** @type {!shaka.mkv.MatroskaIndexParser.Header} */( this.header_); if (shaka.mkv.MatroskaCodecs.usesWebm(track.codecId)) { return this.packageWebm_(bytes, stream, reference); } const frames = shaka.mkv.MatroskaClusterParser.parseFrames( bytes, track, header.timecodeScale); if (!frames.length) { return {data: new Uint8Array(0), init: null}; } if (!this.audioInfo_) { this.audioInfo_ = shaka.transmuxer.MatroskaTransmuxer.getAudioInfo_( track, frames[0].data); } const info = this.audioInfo_; // What the decoder is going to see decides what the stream is, not what // the file announced. stream.audioSamplingRate = info.sampleRate; stream.channelsCount = info.channelCount; if (shaka.mkv.MatroskaCodecs.usesRawMpegAudio(track.codecId)) { return { data: shaka.util.Uint8ArrayUtils.concatRange( frames.map((frame) => frame.data)), init: null, }; } /** @type {!Array<shaka.util.Mp4Generator.Mp4Sample>} */ const samples = frames.map((frame) => { return { data: frame.data, size: frame.data.byteLength, duration: info.getDuration(frame.data), cts: 0, flags: shaka.transmuxer.TransmuxerUtils.AUDIO_SAMPLE_FLAGS, }; }); let baseMediaDecodeTime = Math.round( (frames[0].time + shaka.mkv.MatroskaConstants.MEDIA_TIME_SHIFT) * info.timescale); baseMediaDecodeTime += this.getContinuityOffset_( baseMediaDecodeTime, header, info.timescale); this.nextDecodeTime_ = baseMediaDecodeTime + samples.reduce((sum, sample) => sum + sample.duration, 0); /** @type {shaka.util.Mp4Generator.StreamInfo} */ const streamInfo = { id: stream.id, type: shaka.util.ManifestParserUtils.ContentType.AUDIO, codecs: info.codecs, timescale: info.timescale, duration, mediaConfig: info.config, data: { sequenceNumber: this.frameIndex, baseMediaDecodeTime, samples, }, stream, }; return this.packageSegment( new shaka.util.Mp4Generator([streamInfo]), stream, reference); } /** * Writes the frames of a track as WebM. WebM has presentation times only, * and the codecs it is used for here (VP8, Vorbis) do not reorder frames, so * they are what the file says, moved by the shift. * * @param {!Uint8Array} bytes * @param {shaka.extern.Stream} stream * @param {?shaka.media.SegmentReference} reference * @return {shaka.extern.TransmuxerOutput} * @private */ packageWebm_(bytes, stream, reference) { const track = /** @type {!shaka.mkv.MatroskaIndexParser.Track} */( this.track_); const header = /** @type {!shaka.mkv.MatroskaIndexParser.Header} */( this.header_); const {NANOSECONDS_PER_SECOND, MEDIA_TIME_SHIFT} = shaka.mkv.MatroskaConstants; const ticksPerSecond = NANOSECONDS_PER_SECOND / header.timecodeScale; const frames = shaka.mkv.MatroskaClusterParser.parseFrames( bytes, track, header.timecodeScale); if (!frames.length) { return {data: new Uint8Array(0), init: null}; } const times = this.getWebmTimes_(frames, track); /** @type {shaka.util.WebmGenerator.StreamInfo} */ const streamInfo = { trackNumber: track.number, type: track.type, codecId: track.codecId, codecPrivate: track.codecPrivate, timecodeScale: header.timecodeScale, width: track.width || stream.width || 0, height: track.height || stream.height || 0, channelCount: track.channels || stream.channelsCount || 1, sampleRate: track.sampleRate || stream.audioSamplingRate || 0, frames: frames.map((frame, i) => { return { timecode: Math.round((times[i] + MEDIA_TIME_SHIFT) * ticksPerSecond), data: frame.data, keyframe: frame.keyframe, }; }), }; return this.packageSegment( new shaka.util.WebmGenerator(streamInfo), stream, reference); } /** * Gets the time of each frame. Every block has one, and blocks written by * muxers often lace several frames of audio: MSE does not take lacing, so * each of those frames has to get a time of its own. What is known of a * frame laced with others is the time of its block, which is the time of the * first, and (for the codecs that say it) how much audio each frame holds. * * @param {!Array<shaka.mkv.MatroskaClusterParser.Frame>} frames * @param {!shaka.mkv.MatroskaIndexParser.Track} track * @return {!Array<number>} The times in seconds. * @private */ getWebmTimes_(frames, track) { const Vorbis = shaka.codec.Vorbis; const vorbisInfo = track.codecId == shaka.mkv.CodecId.VORBIS && track.codecPrivate ? Vorbis.parseHeaders(track.codecPrivate) : null; const sampleRate = track.sampleRate || 0; const times = []; let previousWindow = 0; for (const frame of frames) { if (!vorbisInfo || !sampleRate || frame.laceIndex == 0) { // The time that the block writes, or that it spreads over its frames // if it says how long it lasts. times.push(frame.time); previousWindow = vorbisInfo ? Vorbis.getWindowSize(vorbisInfo, frame.data) : 0; continue; } // The next frame of a block of Vorbis starts when the audio of the one // before it is over. const window = Vorbis.getWindowSize(vorbisInfo, frame.data); times.push(times[times.length - 1] + Vorbis.getAdvance(previousWindow, window) / sampleRate); previousWindow = window; } return times; } /** * Segments that follow each other should have decode times that follow each * other too. Times taken from Matroska have the resolution of its timecode * scale (one millisecond by default), so a segment's start can differ from * the end of the previous one by that much, which MSE would see as a tiny * gap or overlap. This tells how much to move the start of a segment to * make it continue the previous one, when it is close enough to be the * next segment. * * @param {number} decodeTime The decode time the segment starts at. * @param {shaka.mkv.MatroskaIndexParser.Header} header * @param {number} timescale * @return {number} How much to add to the decode times, in ticks. * @private */ getContinuityOffset_(decodeTime, header, timescale) { if (this.nextDecodeTime_ == null) { return 0; } const nanosecondsPerSecond = shaka.mkv.MatroskaConstants.NANOSECONDS_PER_SECOND; const ticks = shaka.transmuxer.MatroskaTransmuxer.CONTINUITY_TICKS_; // The rounding error of a time is up to half a tick of the timecode scale. // Allow two, for the ends of both segments. const tolerance = Math.ceil( ticks * header.timecodeScale / nanosecondsPerSecond * timescale); const offset = this.nextDecodeTime_ - decodeTime; return Math.abs(offset) <= tolerance ? offset : 0; } /** * Gets the duration of the last video sample of a segment, which has no * next frame to measure it against. * * @param {!shaka.mkv.MatroskaIndexParser.Track} track * @param {!Array<number>} decodeTimes * @param {number} timescale * @param {?shaka.media.SegmentReference} reference * @return {number} * @private */ static getLastDuration_(track, decodeTimes, timescale, reference) { if (track.defaultDuration) { return Math.round(track.defaultDuration * timescale); } const count = decodeTimes.length; if (count > 1) { return decodeTimes[count - 1] - decodeTimes[count - 2]; } // A lone frame: it can only last as long as its segment. return reference ? Math.round((reference.endTime - reference.startTime) * timescale) : 0; } /** * Works out how to describe an audio track in MP4. * * @param {!shaka.mkv.MatroskaIndexParser.Track} track * @param {!Uint8Array} firstFrame The first frame of the track. * @return {!shaka.transmuxer.MatroskaTransmuxer.AudioInfo} * @private */ static getAudioInfo_(track, firstFrame) { const CodecId = shaka.mkv.CodecId; const MatroskaCodecs = shaka.mkv.MatroskaCodecs; const MatroskaTransmuxer = shaka.transmuxer.MatroskaTransmuxer; const sampleRate = track.sampleRate || 0; const channelCount = track.channels || 0; switch (track.codecId) { case CodecId.AAC: { if (!sampleRate || !track.codecPrivate) { throw new Error('The AAC track has no sample rate or configuration'); } return { codecs: MatroskaCodecs.getCodecs(track) || '', timescale: sampleRate, sampleRate, channelCount, config: shaka.codec.AAC.buildEsds(track.codecPrivate), getDuration: () => MatroskaTransmuxer.AAC_SAMPLES_PER_FRAME_, }; } case CodecId.AC3: case CodecId.EAC3: { const isEc3 = track.codecId == CodecId.EAC3; // The configuration boxes (dac3, dec3) describe the first frame. /** @type {!shaka.codec.Ac3.Ac3Frame} */ const frame = { sampleRate: 0, channelCount: 0, audioConfig: new Uint8Array(0), frameLength: 0, }; const parsed = isEc3 ? shaka.codec.Ec3.parseFrame(firstFrame, 0, frame) : shaka.codec.Ac3.parseFrame(firstFrame, 0, frame); if (!parsed || !frame.sampleRate) { throw new Error('The first Dolby Digital frame is invalid'); } const samplesPerFrame = isEc3 ? shaka.codec.Ec3.EC3_SAMPLES_PER_FRAME : shaka.codec.Ac3.AC3_SAMPLES_PER_FRAME; return { codecs: isEc3 ? 'ec-3' : 'ac-3', timescale: frame.sampleRate, sampleRate: frame.sampleRate, channelCount: frame.channelCount, config: frame.audioConfig, getDuration: () => samplesPerFrame, }; } case CodecId.OPUS: return { codecs: 'opus', timescale: shaka.codec.Opus.SAMPLE_RATE, sampleRate: shaka.codec.Opus.SAMPLE_RATE, channelCount, config: MatroskaCodecs.getOpusConfig(track), // A packet holds a variable amount of audio, given by its TOC. getDuration: (frame) => shaka.codec.Opus.getPacketSampleCount(frame), }; case CodecId.FLAC: { const parsedInfo = track.codecPrivate && shaka.codec.Flac.parseStreamInfo(track.codecPrivate); if (!parsedInfo || !track.codecPrivate) { throw new Error('The FLAC track has no STREAMINFO'); } const flacInfo = /** @type {shaka.codec.Flac.StreamInfo} */( parsedInfo); return { codecs: 'flac', timescale: flacInfo.sampleRate, sampleRate: flacInfo.sampleRate, channelCount: flacInfo.channelCount, config: shaka.codec.Flac.buildDfLa(track.codecPrivate), // Each frame says how many samples it has. getDuration: (frame) => { return shaka.codec.Flac.getBlockSize(frame, flacInfo); }, }; } case CodecId.MP3: { const parsed = shaka.codec.MpegAudio.parseHeader(firstFrame, 0); if (!parsed) { throw new Error('The first MP3 frame is invalid'); } // MP4 takes MP3 as "mp3" (a sample entry without configuration) or, // on the platforms that need it, as MPEG audio with its esds. const mp4Codec = shaka.mkv.MatroskaCodecs.getMp3Mp4Codec(); const usesEsds = mp4Codec != null && mp4Codec != 'mp3'; return { codecs: usesEsds ? shaka.codec.MpegAudio.getMp4Codec(parsed.sampleRate) : 'mp3', timescale: parsed.sampleRate, sampleRate: parsed.sampleRate, channelCount: parsed.channelCount, config: usesEsds ? shaka.codec.MpegAudio.buildEsds( parsed.sampleRate) : new Uint8Array(0), getDuration: () => parsed.samplesPerFrame, }; } } throw new Error('Unsupported audio codec ' + track.codecId); } /** * @param {string} mimeType * @return {string} The MIME type without the parameters, in lower case. * @private */ getContainer_(mimeType) { return mimeType.toLowerCase().split(';')[0]; } /** * @param {string} codecs * @return {boolean} * @private */ static isSupportedCodec_(codecs) { const prefixes = shaka.transmuxer.MatroskaTransmuxer.SUPPORTED_CODECS_; return prefixes.some((prefix) => codecs.toLowerCase().startsWith(prefix)); } }; /** * @typedef {{ * codecs: string, * timescale: number, * sampleRate: number, * channelCount: number, * config: !Uint8Array, * getDuration: function(!Uint8Array):number, * }} * * @property {string} codecs * The codec, as Mp4Generator names it. * @property {number} timescale * The ticks per second the media times are written in. * @property {number} sampleRate * @property {number} channelCount * @property {!Uint8Array} config * The payload of the codec configuration box. * @property {function(!Uint8Array):number} getDuration * Returns the duration, in ticks, of a frame. */ shaka.transmuxer.MatroskaTransmuxer.AudioInfo; /** * The clock of MPEG video, 90 kHz. Video frame durations (1/24, 1/25, 1/30, * 1/60 s and their 1001/1000 variants) are whole numbers of ticks at this rate, * and it leaves plenty of room in the 64-bit decode times. * * @private @const {number} */ shaka.transmuxer.MatroskaTransmuxer.VIDEO_TIMESCALE_ = 90000; /** * The number of samples an AAC frame codes (AAC-LC and HE-AAC alike, counted * at the rate of the AAC core). * * @private @const {number} */ shaka.transmuxer.MatroskaTransmuxer.AAC_SAMPLES_PER_FRAME_ = 1024; /** * How many ticks of the timecode scale apart two adjoining segments may be for * the transmuxer to consider them continuous: half a tick of rounding error at * the end of one and at the start of the other. * * @private @const {number} */ shaka.transmuxer.MatroskaTransmuxer.CONTINUITY_TICKS_ = 2; /** * The prefixes of the codec strings that can be repackaged. They are the * codecs MP4 can carry and that MSE plays; see shaka.mkv.MatroskaCodecs. * * @private @const {!Array<string>} */ shaka.transmuxer.MatroskaTransmuxer.SUPPORTED_CODECS_ = [ 'avc1.', 'hvc1.', 'av01.', 'vp09.', 'dvh1.', 'dva1.', 'dav1.', 'mp4a.40.', 'ac-3', 'ec-3', 'opus', 'mp3', 'flac', 'vp8', 'vorbis', ]; /** * The prefixes of the codec strings that are written as WebM; the others are * written as MP4. * * @private @const {!Array<string>} */ shaka.transmuxer.MatroskaTransmuxer.WEBM_CODECS_ = ['vp8', 'vorbis']; shaka.transmuxer.TransmuxerEngine.registerTransmuxer( 'video/x-matroska', () => new shaka.transmuxer.MatroskaTransmuxer('video/x-matroska'), shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK); shaka.transmuxer.TransmuxerEngine.registerTransmuxer( 'audio/x-matroska', () => new shaka.transmuxer.MatroskaTransmuxer('audio/x-matroska'), shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);