UNPKG

shaka-player

Version:
234 lines (210 loc) • 7.8 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.msf.packaging.Locmaf'); goog.require('goog.asserts'); goog.require('shaka.log'); goog.require('shaka.media.InitSegmentReference'); goog.require('shaka.media.SegmentUtils'); goog.require('shaka.msf.LOCMAFParser'); goog.require('shaka.msf.PackagingRegistry'); goog.require('shaka.msf.draft18.Codec'); goog.require('shaka.util.Mp4BoxParsers'); goog.require('shaka.util.Mp4Parser'); /** * Packaging for LOCMAF tracks, where every MoQT object is one CMAF chunk with * its `moof` reduced to a handful of tagged fields. * * The media is the same as a `cmaf` track's -- the same initialization * segment, the same sample data, the same DRM signaling -- so everything * about how the track presents itself is shared with that packaging. What * differs is that an object is not appendable as it stands: the `moof` has to * be rebuilt from the fields ahead of the sample data, and a chunk that * carries only what changed since the previous one cannot be read without it. * All of that lives in shaka.msf.LOCMAFParser; this class supplies it the * three things the initialization segment contributes and nothing else. * * A catalog may offer one rendition under `locmaf` and `cmaf` at the same * time, sharing a single initialization-data entry: they are alternative * encodings of one source rather than distinct media, and nothing in the * catalog marks either as the one to use. Registering this packaging * therefore makes both visible, doubling the variant list for such a catalog. * Which encoding to keep is the application's call, made by dropping the * others in `manifest.msf.catalogPreprocessor`; the demo's * `shakaAssets.preferLocmafTracks` is a worked example. * * @see https://datatracker.ietf.org/doc/draft-einarsson-moq-locmaf/ * * @implements {shaka.extern.MsfPackaging} * @final */ shaka.msf.packaging.Locmaf = class { constructor() { /** @private {?shaka.msf.LOCMAFParser.TrackParams} */ this.params_ = null; } /** * @override */ describeTrack(track, initData) { // A version this parser does not know reinterprets wire syntax it would // otherwise decode into plausible nonsense, which the bytes themselves // give no way to detect. The catalog offers the same source under `cmaf`, // so skipping the track is the graceful outcome. if (track.locmafVersion !== shaka.msf.packaging.Locmaf.VERSION) { shaka.log.warning(`Skipping LOCMAF track "${track.name}" with ` + `unsupported locmafVersion "${track.locmafVersion}"; this build ` + `supports "${shaka.msf.packaging.Locmaf.VERSION}".`); return null; } if (!initData.byteLength) { shaka.log.warning(`Skipping LOCMAF track "${track.name}" with no ` + 'initialization data; a LOCMAF chunk cannot be reconstructed ' + 'without the trex defaults its fields fall back to.'); return null; } const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromMp4( initData, initData, /* disableText= */ false); if (!basicInfo) { return null; } const timescale = basicInfo.timescale || track.timescale; if (!timescale) { shaka.log.info( 'Skipping incompatible track due missing timescale', track); return null; } const params = shaka.msf.packaging.Locmaf.readTrackParams_(initData, timescale); if (!params) { shaka.log.warning(`Skipping LOCMAF track "${track.name}" whose ` + 'initialization segment carries no trex defaults.'); return null; } this.params_ = params; return { basicInfo, initSegmentReference: new shaka.media.InitSegmentReference( () => [], /* startBytes= */ 0, /* endBytes= */ null, /* mediaQuality= */ null, timescale, initData), }; } /** * @override */ createSegmenter(codec) { goog.asserts.assert(this.params_, 'describeTrack() must run first'); // A caller with no session -- a test, or a packaging driven directly -- // gets the encoding every supported draft uses. return new shaka.msf.packaging.LocmafSegmenter( new shaka.msf.LOCMAFParser( codec || new shaka.msf.draft18.Codec(), /** @type {!shaka.msf.LOCMAFParser.TrackParams} */ ( this.params_))); } /** * Reads what the CMAF Header contributes to reconstruction: the track's ID, * the `trex` defaults every omitted field falls back to, and the `tenc` * per-sample IV size protected tracks need. * * shaka.util.Mp4BoxParsers.parseTREX is not used here because it reports * only two of the five values -- it exists for the transmuxer, which needs * no more -- and widening it would change what every other caller reads. * * @param {!Uint8Array} initData * @param {number} timescale * @return {?shaka.msf.LOCMAFParser.TrackParams} * @private */ static readTrackParams_(initData, timescale) { const Mp4Parser = shaka.util.Mp4Parser; /** @type {?shaka.msf.LOCMAFParser.TrackParams} */ let params = null; let isProtected = false; let defaultPerSampleIvSize = 0; new Mp4Parser() .box('moov', Mp4Parser.children) .box('mvex', Mp4Parser.children) .fullBox('trex', (box) => { const reader = box.reader; params = { trackId: reader.readUint32(), timescale, trexSampleDescriptionIndex: reader.readUint32(), trexSampleDuration: reader.readUint32(), trexSampleSize: reader.readUint32(), trexSampleFlags: reader.readUint32(), isProtected: false, defaultPerSampleIvSize: 0, }; }) .boxes([...Mp4Parser.SAMPLE_TABLE_PATH, 'sinf', 'schi'], Mp4Parser.children) .fullBox('stsd', Mp4Parser.sampleDescription) .box('encv', Mp4Parser.visualSampleEntry) .box('enca', Mp4Parser.audioSampleEntry) .fullBox('tenc', (box) => { goog.asserts.assert( box.version != null, 'TENC is a full box and should have a valid version.'); const parsed = shaka.util.Mp4BoxParsers.parseTENC(box.reader, box.version); isProtected = parsed.defaultIsProtected === 1; defaultPerSampleIvSize = parsed.defaultPerSampleIVSize; }) .parse(initData, /* partialOkay= */ true); if (!params) { return null; } params.isProtected = isProtected; params.defaultPerSampleIvSize = defaultPerSampleIvSize; return params; } }; /** * The LOCMAF packaging version this build reads, as carried in the catalog's * `locmafVersion` field. * * @const {string} */ shaka.msf.packaging.Locmaf.VERSION = '0.3'; /** * @implements {shaka.extern.MsfSegmenter} * @final */ shaka.msf.packaging.LocmafSegmenter = class { /** * @param {!shaka.msf.LOCMAFParser} parser */ constructor(parser) { /** @private {!shaka.msf.LOCMAFParser} */ this.parser_ = parser; } /** * @override */ push(obj) { // An object with an empty payload carries a status rather than media, but // it still occupies an object ID, so the parser is told about it: a // status object between two chunks is not a gap in the delta chain. const chunk = this.parser_.parse(obj); if (!chunk) { return []; } return [{ startTime: chunk.startTime, duration: chunk.duration, data: chunk.data, timestampOffset: 0, discontinuitySequence: -1, }]; } }; shaka.msf.PackagingRegistry.registerPackaging( 'locmaf', () => new shaka.msf.packaging.Locmaf());