UNPKG

shaka-player

Version:
887 lines (829 loc) • 30.4 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.mkv.MatroskaParser'); goog.require('shaka.codec.VP9'); goog.require('shaka.log'); goog.require('shaka.media.InitSegmentReference'); goog.require('shaka.media.ManifestParser'); goog.require('shaka.media.PresentationTimeline'); goog.require('shaka.media.SegmentIndex'); goog.require('shaka.media.SegmentReference'); goog.require('shaka.mkv.CodecId'); goog.require('shaka.mkv.ElementId'); goog.require('shaka.mkv.MatroskaClusterParser'); goog.require('shaka.mkv.MatroskaCodecs'); goog.require('shaka.mkv.MatroskaConstants'); goog.require('shaka.mkv.MatroskaIndexParser'); goog.require('shaka.text.MatroskaTextParser'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.net.NetworkingUtils'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.StreamUtils'); /** * Parses a Matroska (.mkv, .mka) file into a manifest. Every track becomes a * stream, and the Cues of the file give the segments: byte ranges of the file, * each starting at a Cluster with a keyframe. Nothing is downloaded but the * header, the Cues and the Chapters until the streams are played; the media is * repackaged by shaka.transmuxer.MatroskaTransmuxer, and the * subtitles are read by shaka.text.MatroskaTextParser. * * @implements {shaka.extern.ManifestParser} * @export */ shaka.mkv.MatroskaParser = class { constructor() { /** @private {?shaka.extern.ManifestConfiguration} */ this.config_ = null; /** @private {?shaka.extern.ManifestParser.PlayerInterface} */ this.playerInterface_ = null; /** @private {!Set<!shaka.extern.IAbortableOperation>} */ this.operations_ = new Set(); /** @private {boolean} */ this.stopped_ = false; } /** * @override * @export */ configure(config) { this.config_ = config; } /** * @override * @export */ async start(uri, playerInterface) { this.playerInterface_ = playerInterface; try { return await this.load_(uri); } catch (error) { throw this.addUri_(error, uri); } } /** * @override * @export */ async stop() { this.stopped_ = true; await Promise.all(Array.from(this.operations_).map((operation) => { return operation.abort(); })); this.operations_.clear(); this.playerInterface_ = null; } /** @override */ update() {} /** @override */ onExpirationUpdated(sessionId, expiration) {} /** @override */ onInitialVariantChosen(variant) {} /** @override */ banLocation(uri) {} /** @override */ setMediaElement(mediaElement) {} /** * @param {string} uri * @return {!Promise<shaka.extern.Manifest>} * @private */ async load_(uri) { const {header, totalSize} = await this.readHeader_(uri); const candidates = this.getCandidates_(header); if (!candidates.length) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.MKV_NO_SUPPORTED_TRACKS, uri); } // The ranges the streams are made of come from the Cues of one track: a // video track when there is one, as only video is cut at keyframes. const indexTrack = (candidates.find((candidate) => { return candidate.contentType == shaka.util.ManifestParserUtils.ContentType.VIDEO; }) || candidates[0]).track; const cues = await this.readCues_(uri, header); const announcedDuration = header.duration != null ? header.duration : shaka.mkv.MatroskaParser.estimateDuration_(cues, indexTrack.number); if (!announcedDuration || !Number.isFinite(announcedDuration) || announcedDuration <= 0) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.MKV_MISSING_INDEX, uri); } const duration = announcedDuration; // The streams that are wanted. const ContentType = shaka.util.ManifestParserUtils.ContentType; const wanted = candidates.filter((candidate) => { if (!this.config_) { return true; } switch (candidate.contentType) { case ContentType.VIDEO: return !this.config_.disableVideo; case ContentType.AUDIO: return !this.config_.disableAudio; default: return !this.config_.disableText; } }); await this.probeVp9_(uri, header, cues, indexTrack, wanted); const bandwidth = totalSize ? Math.round(totalSize * 8 / duration) : 0; const makeReferences = (initReference, timestampOffset) => { return shaka.mkv.MatroskaParser.makeReferences_(cues, header, indexTrack.number, duration, uri, initReference, timestampOffset); }; const videoStreams = []; const audioStreams = []; const textStreams = []; for (const candidate of wanted) { // The two streams of a track are told apart by the initialization // segment, which the StreamingEngine appends again when it is another // range than the last one. So the second one takes a byte more. The // byte is the start of the element that follows, and an element takes two // at least, so it does not add anything to what the segment says. const initEnd = shaka.mkv.MatroskaIndexParser.getInitEndOffset( header, candidate.track) + (candidate.isAlternate ? 1 : 0); const initReference = new shaka.media.InitSegmentReference( () => [uri], 0, initEnd); // The transmuxer writes the audio and the video later than they are in // the file; see MEDIA_TIME_SHIFT. Subtitles are not moved. const references = makeReferences(initReference, candidate.contentType == ContentType.TEXT ? 0 : -shaka.mkv.MatroskaConstants.MEDIA_TIME_SHIFT); if (shaka.mkv.MatroskaCodecs.usesRawMpegAudio(candidate.track.codecId)) { // Raw MPEG audio has no timestamps. Anchor each append to its segment, // including after a seek, instead of applying the MP4/WebM shift. for (const reference of references) { reference.timestampOffset = reference.startTime; } } if (!references.length) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.MKV_MISSING_INDEX, uri); } const stream = this.createStream_(candidate, references, bandwidth); switch (candidate.contentType) { case ContentType.VIDEO: videoStreams.push(stream); break; case ContentType.AUDIO: audioStreams.push(stream); break; default: textStreams.push(stream); break; } } const variants = shaka.mkv.MatroskaParser.makeVariants_( videoStreams, audioStreams, bandwidth); if (!variants.length) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.MKV_NO_SUPPORTED_TRACKS, uri); } const timeline = new shaka.media.PresentationTimeline(null, 0); timeline.setStatic(true); timeline.setDuration(duration); const chapters = await this.readChapters_(uri, header); const firstChapterId = Math.max(0, ...header.tracks.map((track) => track.number)) + 1; return { presentationTimeline: timeline, variants, textStreams, imageStreams: [], chapterStreams: shaka.mkv.MatroskaParser.makeChapterStreams_( chapters, duration, firstChapterId), offlineSessionIds: [], sequenceMode: false, ignoreManifestTimestampsInSegmentsMode: false, type: shaka.media.ManifestParser.MKV, serviceDescription: null, nextUrl: null, periodCount: 1, gapCount: 0, isLowLatency: false, startTime: null, }; } /** * Reads the header of the file: everything before the first Cluster. Its * size is not known beforehand, so the range that is requested grows until * the first Cluster is in it. * * @param {string} uri * @return {!Promise<{header: shaka.mkv.MatroskaIndexParser.Header, * totalSize: ?number}>} * @private */ async readHeader_(uri) { let size = shaka.mkv.MatroskaParser.INITIAL_HEADER_READ_SIZE_; let totalSize = null; while (true) { // The size of the header is unknown until it has been read, so the // requests cannot be made at the same time. // eslint-disable-next-line no-await-in-loop const response = await this.requestRange_(uri, 0, size - 1); if (totalSize == null) { totalSize = shaka.mkv.MatroskaParser.getTotalSize_(response); } const header = shaka.mkv.MatroskaIndexParser.parseHeader(response.data); if (header.firstClusterOffset != null) { return {header, totalSize}; } // Either the file is over (it has no media) or the header is still to // come. const received = shaka.util.BufferUtils.toUint8(response.data).length; const maxSize = shaka.mkv.MatroskaParser.MAX_HEADER_READ_SIZE_; if (received < size || size >= maxSize) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.MKV_INVALID_FILE, uri); } size *= 2; } } /** * Lists the tracks that become streams, with what is known of them. * * @param {shaka.mkv.MatroskaIndexParser.Header} header * @return {!Array<shaka.mkv.MatroskaParser.Candidate>} * @private */ getCandidates_(header) { const MatroskaCodecs = shaka.mkv.MatroskaCodecs; const numbers = new Set(); const candidates = []; for (const track of header.tracks) { const contentType = MatroskaCodecs.getContentType(track); if (!contentType || !track.enabled) { continue; } if (!track.number || numbers.has(track.number)) { // The number is what the blocks refer to, so it has to be unique. throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.MKV_INVALID_FILE); } numbers.add(track.number); const codecs = MatroskaCodecs.getCodecs(track); if (!codecs) { shaka.log.warning('Ignoring the Matroska track', track.number, 'with the unsupported codec', track.codecId); continue; } const supplementalCodecs = this.config_ && this.config_.ignoreSupplementalCodecs ? '' : shaka.mkv.MatroskaCodecs.getSupplementalCodecs(track); candidates.push({ track, contentType, codecs, supplementalCodecs, streamId: track.number, isAlternate: false, }); if (supplementalCodecs) { // A second stream for the platforms that decode Dolby Vision, next to // the one for those that do not. The player keeps the ones it plays. candidates.push({ track, contentType, codecs: supplementalCodecs, supplementalCodecs, streamId: track.number + shaka.mkv.MatroskaConstants.ALTERNATE_STREAM_ID_OFFSET, isAlternate: true, }); } } return candidates; } /** * A VP9 track normally has no CodecPrivate, so the profile and the bit depth * that the codec string needs are only in the first key frame. Reads it. * * @param {string} uri * @param {shaka.mkv.MatroskaIndexParser.Header} header * @param {!Array<shaka.mkv.MatroskaIndexParser.CuePoint>} cues * @param {!shaka.mkv.MatroskaIndexParser.Track} indexTrack * @param {!Array<shaka.mkv.MatroskaParser.Candidate>} candidates * @return {!Promise} * @private */ async probeVp9_(uri, header, cues, indexTrack, candidates) { const toProbe = candidates.filter((candidate) => { return candidate.track.codecId == shaka.mkv.CodecId.VP9 && !candidate.track.codecPrivate; }); if (!toProbe.length) { return; } const first = shaka.mkv.MatroskaParser.makeReferences_(cues, header, indexTrack.number, Number.MAX_VALUE, uri, null)[0]; if (!first) { return; } const start = first.getStartByte(); const end = Math.min(first.getEndByte() ?? Infinity, start + shaka.mkv.MatroskaParser.VP9_PROBE_SIZE_ - 1); const response = await this.requestRange_(uri, start, end); for (const candidate of toProbe) { const frames = shaka.mkv.MatroskaClusterParser.parseFrames( response.data, candidate.track, header.timecodeScale, /* allowPartial= */ true); const keyframe = frames.find((frame) => frame.keyframe); const info = keyframe && shaka.codec.VP9.parseKeyFrame(keyframe.data); if (info) { candidate.codecs = shaka.codec.VP9.getCodecs(info, candidate.track.width || info.width, candidate.track.height || info.height, candidate.track.defaultDuration); } } } /** * @param {string} uri * @param {shaka.mkv.MatroskaIndexParser.Header} header * @return {!Promise<!Array<shaka.mkv.MatroskaIndexParser.CuePoint>>} * @private */ async readCues_(uri, header) { const cuesOffset = header.cuesOffset; if (cuesOffset == null) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.MKV_MISSING_INDEX, uri); } const data = await this.requestElement_( uri, cuesOffset, shaka.mkv.ElementId.CUES); const cues = shaka.mkv.MatroskaIndexParser.parseCues( data, header.segmentOffset, header.timecodeScale); if (!cues.length) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.MKV_MISSING_INDEX, uri); } return cues; } /** * @param {string} uri * @param {shaka.mkv.MatroskaIndexParser.Header} header * @return {!Promise<!Array<shaka.mkv.MatroskaIndexParser.Chapter>>} * @private */ async readChapters_(uri, header) { if (header.chaptersOffset == null || (this.config_ && this.config_.disableChapters)) { return []; } const data = await this.requestElement_( uri, header.chaptersOffset, shaka.mkv.ElementId.CHAPTERS); return shaka.mkv.MatroskaIndexParser.parseChapters(data); } /** * Downloads a top-level element whose size is not known: its header first, * then exactly the element. * * @param {string} uri * @param {number} offset * @param {shaka.mkv.ElementId} id * @return {!Promise<BufferSource>} * @private */ async requestElement_(uri, offset, id) { const head = await this.requestRange_(uri, offset, offset + shaka.mkv.MatroskaParser.ELEMENT_HEADER_READ_SIZE_ - 1); const length = shaka.mkv.MatroskaIndexParser.getElementLength( head.data, id); if (length > shaka.mkv.MatroskaParser.MAX_ELEMENT_SIZE_) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.MKV_INVALID_FILE, uri); } const body = await this.requestRange_(uri, offset, offset + length - 1); return body.data; } /** * @param {string} uri * @param {number} start * @param {number} end * @return {!Promise<shaka.extern.Response>} * @private */ async requestRange_(uri, start, end) { if (this.stopped_ || !this.playerInterface_) { throw this.makeAbortedError_(); } const request = shaka.net.NetworkingUtils.createSegmentRequest( [uri], start, end, this.playerInterface_.getStreamingRetryParameters()); const operation = this.playerInterface_.networkingEngine.request( shaka.net.NetworkingEngine.RequestType.SEGMENT, request); this.operations_.add(operation); try { const response = await operation.promise; if (this.stopped_) { throw this.makeAbortedError_(); } // A server that ignores the Range header answers with the whole file. if (shaka.util.BufferUtils.toUint8(response.data).byteLength > end - start + 1) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.MKV_INVALID_FILE, uri); } return response; } finally { this.operations_.delete(operation); } } /** * @param {!shaka.mkv.MatroskaParser.Candidate} candidate * @param {!Array<!shaka.media.SegmentReference>} references * @param {number} bandwidth * @return {!shaka.extern.Stream} * @private */ createStream_(candidate, references, bandwidth) { const {track, contentType, codecs} = candidate; const ContentType = shaka.util.ManifestParserUtils.ContentType; const MatroskaCodecs = shaka.mkv.MatroskaCodecs; const AccessibilityPurpose = shaka.media.ManifestParser.AccessibilityPurpose; const isVideo = contentType == ContentType.VIDEO; const isText = contentType == ContentType.TEXT; /** @type {!Object<string, *>} */ const overrides = { id: candidate.streamId, originalId: String(track.number), type: contentType, mimeType: isText ? shaka.text.MatroskaTextParser.MIME_TYPE : contentType + '/x-matroska', codecs, supplementalCodecs: candidate.supplementalCodecs, language: shaka.util.LanguageUtils.normalize(track.language), originalLanguage: track.language, label: track.name || null, primary: track.isDefault, forced: track.forced, }; if (isVideo) { const pixelAspectRatio = MatroskaCodecs.getPixelAspectRatio(track); Object.assign(overrides, { width: track.width, height: track.height, frameRate: MatroskaCodecs.getFrameRate(track), pixelAspectRatio: pixelAspectRatio ? pixelAspectRatio.horizontal + ':' + pixelAspectRatio.vertical : undefined, hdr: MatroskaCodecs.getHdr(track), // Every stream is read from the same bytes, so the file's bitrate is // what any of them costs. bandwidth, }); } else if (!isText) { Object.assign(overrides, { channelsCount: track.channels, audioSamplingRate: track.outputSampleRate || track.sampleRate, }); } if (track.hearingImpaired) { overrides['accessibilityPurpose'] = AccessibilityPurpose.HARD_OF_HEARING; overrides['roles'] = ['caption']; } else if (track.visualImpaired) { overrides['accessibilityPurpose'] = AccessibilityPurpose.VISUALLY_IMPAIRED; overrides['roles'] = ['description']; } else if (track.commentary) { overrides['roles'] = ['commentary']; } if (isText) { overrides['kind'] = track.hearingImpaired ? 'caption' : 'subtitle'; // The text engine waits for the timestamp offset that MediaSource // reports for the media, which this container never has: the cues have // absolute times. External streams are the ones that do not wait. overrides['external'] = true; } const stream = shaka.util.StreamUtils.createStream(overrides); stream.createSegmentIndex = () => { // The index may be released and created again, so it gets a copy. stream.segmentIndex = new shaka.media.SegmentIndex(references.slice()); return Promise.resolve(); }; stream.closeSegmentIndex = () => { if (stream.segmentIndex) { stream.segmentIndex.release(); stream.segmentIndex = null; } }; return stream; } /** * Adds the URI to the errors of the file, which are created without it. * * @param {*} error * @param {string} uri * @return {*} * @private */ addUri_(error, uri) { const Code = shaka.util.Error.Code; if (error instanceof shaka.util.Error && !error.data.length && [Code.MKV_INVALID_FILE, Code.MKV_MISSING_INDEX, Code.MKV_NO_SUPPORTED_TRACKS].includes(error.code)) { return new shaka.util.Error( error.severity, error.category, error.code, uri); } return error; } /** @return {!shaka.util.Error} @private */ makeAbortedError_() { return new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.PLAYER, shaka.util.Error.Code.OPERATION_ABORTED); } /** * Makes references to consecutive cluster ranges. A range may contain more * than one cluster when not every cluster has a CuePoint. Each reference * therefore starts on a random access point indicated by Cues. * * @param {!Array<shaka.mkv.MatroskaIndexParser.CuePoint>} cues * @param {shaka.mkv.MatroskaIndexParser.Header} header * @param {number} trackNumber The track whose CuePoints define the ranges. * Cues of other tracks (e.g. subtitles) do not fall on keyframes. * @param {number} duration * @param {string} uri * @param {shaka.media.InitSegmentReference} initSegmentReference * @param {number=} timestampOffset * @return {!Array<!shaka.media.SegmentReference>} * @private */ static makeReferences_(cues, header, trackNumber, duration, uri, initSegmentReference, timestampOffset = 0) { const firstClusterOffset = header.firstClusterOffset; if (firstClusterOffset == null) { return []; } const pointsByOffset = new Map(); pointsByOffset.set(firstClusterOffset, 0); for (const cue of cues) { if (cue.track == trackNumber && cue.offset >= firstClusterOffset && cue.time < duration) { const oldTime = pointsByOffset.get(cue.offset); pointsByOffset.set(cue.offset, oldTime == null ? cue.time : Math.min(oldTime, cue.time)); } } const points = Array.from(pointsByOffset).sort((a, b) => a[0] - b[0]); const references = []; for (let i = 0; i < points.length; i++) { const [startByte, startTime] = points[i]; const next = points[i + 1]; const endTime = next ? next[1] : duration; if (endTime <= startTime) { continue; } // The last range stops before the Cues (or any other element that // follows the Clusters) so it does not download them. const endByte = next ? next[0] - 1 : (header.mediaEndOffset == null ? null : header.mediaEndOffset - 1); references.push(new shaka.media.SegmentReference( startTime, endTime, () => [uri], startByte, endByte, initSegmentReference, timestampOffset, /* appendWindowStart= */ 0, /* appendWindowEnd= */ duration)); } return references; } /** * @param {!shaka.extern.Response} response * @return {?number} The size of the whole file, from a Content-Range header. * @private */ static getTotalSize_(response) { const contentRange = response.headers['content-range']; const match = contentRange && /\/(\d+)\s*$/.exec(contentRange); return match ? parseInt(match[1], 10) : null; } /** * Guesses the duration of a file that does not write it: the time of the * last keyframe, and one more interval like the last one. * * @param {!Array<shaka.mkv.MatroskaIndexParser.CuePoint>} cues * @param {number} trackNumber * @return {?number} * @private */ static estimateDuration_(cues, trackNumber) { const times = cues.filter((cue) => cue.track == trackNumber) .map((cue) => cue.time).sort((a, b) => a - b); if (times.length < 2) { return null; } const last = times[times.length - 1]; return last + (last - times[times.length - 2]); } /** * Combines every video with every audio: they are all in the same file, so * any pair can be played. * * @param {!Array<!shaka.extern.Stream>} videoStreams * @param {!Array<!shaka.extern.Stream>} audioStreams * @param {number} bandwidth * @return {!Array<!shaka.extern.Variant>} * @private */ static makeVariants_(videoStreams, audioStreams, bandwidth) { /** @type {!Array<!shaka.extern.Variant>} */ const variants = []; for (const video of videoStreams.length ? videoStreams : [null]) { for (const audio of audioStreams.length ? audioStreams : [null]) { if (!video && !audio) { continue; } variants.push({ id: variants.length, language: audio ? audio.language : 'und', disabledUntilTime: 0, primary: false, audio, video, bandwidth, allowedByApplication: true, allowedByKeySystem: true, decodingInfos: [], }); } } // The player is to start with what the file marks as default. const defaultVariant = variants.find((variant) => { return (!variant.video || variant.video.primary) && (!variant.audio || variant.audio.primary); }) || variants[0]; if (defaultVariant) { defaultVariant.primary = true; } return variants; } /** * @param {!Array<shaka.mkv.MatroskaIndexParser.Chapter>} chapters * @param {number} duration * @param {number} firstId * @return {!Array<!shaka.extern.Stream>} * @private */ static makeChapterStreams_(chapters, duration, firstId) { /** @type {!Map<string, !Array<shaka.mkv.MatroskaIndexParser.Chapter>>} */ const byLanguage = new Map(); for (const chapter of chapters) { if (chapter.start < duration) { if (!byLanguage.has(chapter.language)) { byLanguage.set(chapter.language, []); } byLanguage.get(chapter.language).push(chapter); } } const result = []; for (const language of byLanguage.keys()) { const entries = byLanguage.get(language); entries.sort((a, b) => a.start - b.start); const references = []; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; // A chapter that has no end lasts until the next one starts. const nextStart = i + 1 < entries.length ? entries[i + 1].start : duration; const end = Math.min(duration, entry.end ?? nextStart); if (end <= entry.start) { continue; } const reference = new shaka.media.SegmentReference( entry.start, end, () => [], 0, null, null, 0, 0, Infinity); reference.setMetadata({title: entry.title, images: []}); references.push(reference); } const stream = shaka.util.StreamUtils.createStream({ id: firstId++, type: shaka.util.ManifestParserUtils.ContentType.CHAPTER, mimeType: 'text/plain', language: shaka.util.LanguageUtils.normalize(language), originalLanguage: language, external: true, createSegmentIndex: () => { stream.segmentIndex = new shaka.media.SegmentIndex( references.slice()); return Promise.resolve(); }, }); result.push(stream); } return result; } }; /** * @typedef {{ * track: !shaka.mkv.MatroskaIndexParser.Track, * contentType: string, * codecs: string, * supplementalCodecs: string, * streamId: number, * isAlternate: boolean, * }} * * @property {!shaka.mkv.MatroskaIndexParser.Track} track * @property {string} contentType * @property {string} codecs * @property {string} supplementalCodecs * The codecs of the Dolby Vision version of the track, if it has one. * @property {number} streamId * @property {boolean} isAlternate * True for the second stream of a track: the one with the supplemental * codecs. */ shaka.mkv.MatroskaParser.Candidate; /** * How much of the file is read first. The EBML header, the SeekHead, the Info * and the Tracks add up to a few kilobytes, so this is enough for almost any * file in one request. * * @private @const {number} */ shaka.mkv.MatroskaParser.INITIAL_HEADER_READ_SIZE_ = 64 * 1024; /** * The most that is read to find the first Cluster. Files that put large * elements (cover art, big Void areas) before their media need more than the * first read, but there is a limit to what can be a header. * * @private @const {number} */ shaka.mkv.MatroskaParser.MAX_HEADER_READ_SIZE_ = 4 * 1024 * 1024; /** * The most an element that is downloaded whole (Cues, Chapters) may be. Cues * take about 20 bytes per keyframe, so this holds those of days of video; it * keeps a corrupt size from asking for the whole file. * * @private @const {number} */ shaka.mkv.MatroskaParser.MAX_ELEMENT_SIZE_ = 16 * 1024 * 1024; /** * The size of an element header: an ID (at most 4 bytes) and a size (at most * 8 bytes), rounded up. * * @private @const {number} */ shaka.mkv.MatroskaParser.ELEMENT_HEADER_READ_SIZE_ = 16; /** * How many bytes of the first Cluster are read to look for a VP9 key frame. * Frames of the track come early in the Cluster, and a key frame of a VP9 track * at 4K is a few hundred kilobytes. * * @private @const {number} */ shaka.mkv.MatroskaParser.VP9_PROBE_SIZE_ = 2 * 1024 * 1024; /** * The MIME types of Matroska: the ones IANA registered and the ones that are * really sent. * * @private @const {!Array<string>} */ shaka.mkv.MatroskaParser.MIME_TYPES_ = [ 'video/x-matroska', 'video/matroska', 'audio/x-matroska', 'audio/matroska', ]; for (const mimeType of shaka.mkv.MatroskaParser.MIME_TYPES_) { shaka.media.ManifestParser.registerParserByMime( mimeType, () => new shaka.mkv.MatroskaParser()); } // Other builds keep the MIME types these extensions have always had. shaka.net.NetworkingUtils.registerExtension('mkv', 'video/x-matroska'); shaka.net.NetworkingUtils.registerExtension('mk3d', 'video/x-matroska'); shaka.net.NetworkingUtils.registerExtension('mka', 'audio/x-matroska');