UNPKG

shaka-player

Version:
524 lines (449 loc) • 15.6 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.msf.packaging.M2ts'); goog.require('shaka.log'); goog.require('shaka.media.SegmentUtils'); goog.require('shaka.msf.PackagingRegistry'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.TsParser'); /** * Packaging for MPEG-2 Transport Stream tracks, where a MoQT object is a run * of whole transport packets. * * Unlike the other packagings, an object is not a segment. A transport packet * carries no timing of its own, a PES packet spans many of them, and an object * boundary falls wherever the publisher chose to cut, so a single object is * neither self-describing nor independently decodable. What the draft does * guarantee is that a Group begins at a random access point, so the Group is * the smallest unit that can be appended, and objects are accumulated until * the Group ends. * * That costs roughly one Group of latency, which is the price of the format: * there is no smaller boundary that can be cut safely. * * @see https://datatracker.ietf.org/doc/draft-gregoire-moq-msfts/ * * @implements {shaka.extern.MsfPackaging} * @final */ shaka.msf.packaging.M2ts = class { constructor() { /** @private {number} */ this.packetSize_ = 0; /** * The catalog's initialization packets, normalized to 188-octet transport * packets and prepended to every group. * @private {!Uint8Array} */ this.psi_ = new Uint8Array(0); /** @private {string} */ this.timingContentType_ = ''; /** @private {number} */ this.frameDuration_ = 0; /** @private {?number} */ this.pcrPid_ = null; } /** * @override */ describeTrack(track, initData) { const M2ts = shaka.msf.packaging.M2ts; const ContentType = shaka.util.ManifestParserUtils.ContentType; const packetSize = track.m2tsPacketSize; if (packetSize != M2ts.TS_PACKET_SIZE && packetSize != M2ts.M2TS_PACKET_SIZE) { shaka.log.alwaysWarn(`m2ts track "${track.name}" declares an ` + `unsupported m2tsPacketSize ${packetSize}; skipping. Only 188 ` + 'and 192 are supported.'); return null; } // The draft's Track object leaves the codec to the base MSF catalog, and // the player needs it before playback starts: the codec decides which // source buffers MediaSource opens, and that happens before the first // group has arrived to be probed. if (!track.codec) { shaka.log.alwaysWarn(`m2ts track "${track.name}" has no codec; ` + 'skipping. The catalog must declare it.'); return null; } const codecs = track.codec.split(',') .map((codec) => codec.trim()) .filter((codec) => codec.length); const hasVideo = codecs.some((codec) => M2ts.isVideoCodec_(codec)); const hasAudio = codecs.some((codec) => M2ts.isAudioCodec_(codec)); if (!hasVideo && !hasAudio) { shaka.log.alwaysWarn(`m2ts track "${track.name}" declares no ` + `recognized codec ("${track.codec}"); skipping.`); return null; } if (track.m2tsRandomAccess === false) { shaka.log.warning(`m2ts track "${track.name}" declares ` + 'm2tsRandomAccess false, so a group may not begin at a random ' + 'access point and the start of a group may not decode.'); } this.packetSize_ = packetSize; this.psi_ = M2ts.normalizePackets_(initData, packetSize); // Timestamps are read from whichever elementary stream the track is // presented as. A muxed program is presented as video. this.timingContentType_ = hasVideo ? ContentType.VIDEO : ContentType.AUDIO; this.frameDuration_ = track.framerate ? 1 / track.framerate : 0; this.pcrPid_ = track.m2tsPcrPid != undefined ? track.m2tsPcrPid : null; // A muxed program keeps both codecs on the one stream. MediaSourceEngine // recognizes that combination and opens a source buffer for each, feeding // both from this stream's segments. const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromMimeType( `video/mp2t; codecs="${codecs.join(',')}"`); // Transport streams have no initialization segment. The PAT/PMT that plays // its part is prepended to each group's payload instead, because it is // part of the transport stream rather than something appended before it. return {basicInfo, initSegmentReference: null}; } /** * @override */ createSegmenter() { return new shaka.msf.packaging.M2tsSegmenter( this.packetSize_, this.psi_, this.timingContentType_, this.frameDuration_, this.pcrPid_); } /** * Copies out the 188-octet transport packets of a buffer of source packets, * dropping the 4-octet timestamp prefix of 192-octet M2TS source packets. * * The prefix records when a packet arrived at a contribution encoder, which * says nothing about presentation, so nothing downstream wants it. Removing * it here means the rest of the player only ever sees plain transport * packets. * * @param {!Uint8Array} data * @param {number} packetSize * @return {!Uint8Array} * @private */ static normalizePackets_(data, packetSize) { const M2ts = shaka.msf.packaging.M2ts; if (packetSize == M2ts.TS_PACKET_SIZE) { return data; } const count = Math.floor(data.byteLength / packetSize); const out = new Uint8Array(count * M2ts.TS_PACKET_SIZE); for (let i = 0; i < count; i++) { const start = i * packetSize + M2ts.M2TS_TIMESTAMP_PREFIX_SIZE; out.set(data.subarray(start, start + M2ts.TS_PACKET_SIZE), i * M2ts.TS_PACKET_SIZE); } return out; } /** * @param {string} codec * @return {boolean} * @private */ static isVideoCodec_(codec) { return shaka.util.ManifestParserUtils.VIDEO_CODEC_REGEXPS.some( (regexp) => regexp.test(codec)); } /** * @param {string} codec * @return {boolean} * @private */ static isAudioCodec_(codec) { return shaka.util.ManifestParserUtils.AUDIO_CODEC_REGEXPS.some( (regexp) => regexp.test(codec)); } }; /** * The size of an MPEG-2 transport packet. * * @const {number} */ shaka.msf.packaging.M2ts.TS_PACKET_SIZE = 188; /** * The size of an M2TS source packet: a transport packet behind a 4-octet * arrival timestamp. * * @const {number} */ shaka.msf.packaging.M2ts.M2TS_PACKET_SIZE = 192; /** * @const {number} */ shaka.msf.packaging.M2ts.M2TS_TIMESTAMP_PREFIX_SIZE = 4; /** * Accumulates a Group's objects and emits it as one segment once the Group * ends, which is when an object from the next Group arrives. * * @implements {shaka.extern.MsfSegmenter} * @final */ shaka.msf.packaging.M2tsSegmenter = class { /** * @param {number} packetSize * @param {!Uint8Array} psi * @param {string} timingContentType * @param {number} frameDuration Seconds per frame from the catalog, or 0 * when the catalog does not say. * @param {?number} pcrPid */ constructor(packetSize, psi, timingContentType, frameDuration, pcrPid) { /** @private {number} */ this.packetSize_ = packetSize; /** @private {!Uint8Array} */ this.psi_ = psi; /** @private {string} */ this.timingContentType_ = timingContentType; /** @private {number} */ this.frameDuration_ = frameDuration; /** @private {?number} */ this.pcrPid_ = pcrPid; /** * Kept across groups so that its 33-bit timestamp rollover reference * survives, which is what lets a group that straddles the wrap be timed * correctly. * @private {!shaka.util.TsParser} */ this.tsParser_ = new shaka.util.TsParser(); /** @private {?bigint} */ this.group_ = null; /** @private {!Array<!Uint8Array>} */ this.chunks_ = []; /** @private {number} */ this.packetCount_ = 0; /** @private {boolean} */ this.discontinuityInGroup_ = false; /** @private {number} */ this.discontinuitySequence_ = 0; /** * Maps media time onto presentation time. Non-zero only once a * discontinuity has moved the media's own clock. * @private {number} */ this.timelineOffset_ = 0; /** @private {?number} */ this.previousEndTime_ = null; } /** * @override */ push(obj) { /** @type {!Array<!shaka.extern.MsfSegment>} */ const segments = []; if (this.group_ !== null && obj.location.group !== this.group_) { const segment = this.flush_(); if (segment) { segments.push(segment); } } this.group_ = obj.location.group; if (obj.data.byteLength) { this.append_(obj.data); } return segments; } /** * Validates an object's source packets and holds onto them until the group * ends. * * @param {!Uint8Array} data * @private */ append_(data) { const M2ts = shaka.msf.packaging.M2ts; const size = this.packetSize_; if (data.byteLength % size) { shaka.log.warning('Dropping m2ts object whose payload is not a whole ' + `number of ${size}-octet source packets.`); return; } // In a 192-octet source packet the transport packet starts after the // timestamp prefix. const tsOffset = size - M2ts.TS_PACKET_SIZE; let discontinuity = false; for (let i = 0; i + size <= data.byteLength; i += size) { const packet = i + tsOffset; if (data[packet] != 0x47) { shaka.log.warning('Dropping m2ts object with a source packet that ' + 'does not start with the sync byte 0x47.'); return; } if (!discontinuity && this.isDiscontinuity_(data, packet)) { discontinuity = true; } } this.chunks_.push(data); this.packetCount_ += data.byteLength / size; if (discontinuity) { this.discontinuityInGroup_ = true; } } /** * Reads the adaptation field's discontinuity_indicator, which is how the * draft requires a publisher to signal that it moved the clock between two * groups. * * When the catalog names the PCR PID we only trust that PID, because on * other PIDs the same bit signals a continuity counter discontinuity, which * is a different thing and does not move the clock. * * @param {!Uint8Array} data * @param {number} packet Offset of the transport packet. * @return {boolean} * @private */ isDiscontinuity_(data, packet) { const adaptationFieldControl = (data[packet + 3] & 0x30) >> 4; // 0 and 1 mean there is no adaptation field to read the flag out of. if (adaptationFieldControl <= 1) { return false; } // An adaptation field of length zero is a single stuffing octet. if (!data[packet + 4]) { return false; } if (!(data[packet + 5] & 0x80)) { return false; } if (this.pcrPid_ == null) { return true; } const pid = ((data[packet + 1] & 0x1f) << 8) | data[packet + 2]; return pid == this.pcrPid_; } /** * Turns the accumulated group into a segment. * * @return {?shaka.extern.MsfSegment} * @private */ flush_() { if (!this.packetCount_) { this.resetGroup_(); return null; } const discontinuity = this.discontinuityInGroup_; if (discontinuity) { this.discontinuitySequence_++; } const data = this.buildGroupData_(); this.resetGroup_(); this.tsParser_.clearData(); this.tsParser_.setDiscontinuitySequence(this.discontinuitySequence_); this.tsParser_.parse(data); const timing = this.getTiming_(); if (!timing) { shaka.log.warning('Dropping m2ts group with no usable timestamps.'); return null; } if (discontinuity && this.previousEndTime_ != null) { // The media's clock moved. Re-anchor it so that presentation time keeps // running forward, which the rest of the player requires, and shift the // media by the same amount at append time so the two stay in step. this.timelineOffset_ = this.previousEndTime_ - timing.startTime; } const startTime = timing.startTime + this.timelineOffset_; this.previousEndTime_ = startTime + timing.duration; return { startTime, duration: timing.duration, data, timestampOffset: this.timelineOffset_, discontinuitySequence: this.discontinuitySequence_, }; } /** * Copies the group's transport packets into one buffer, behind the * initialization packets. * * The 192-to-188 normalization happens as part of this copy rather than as a * pass of its own, so supporting M2TS source packets costs no extra * allocation and no extra traffic over the plain transport stream path. * * The PAT/PMT is prepended because the draft only requires a publisher to * repeat the program information periodically, so a group may open without * it, and a transport stream cannot be interpreted without it. * * @return {!Uint8Array} * @private */ buildGroupData_() { const M2ts = shaka.msf.packaging.M2ts; const packetSize = M2ts.TS_PACKET_SIZE; const out = new Uint8Array( this.psi_.byteLength + this.packetCount_ * packetSize); out.set(this.psi_, 0); let offset = this.psi_.byteLength; for (const chunk of this.chunks_) { if (this.packetSize_ == packetSize) { out.set(chunk, offset); offset += chunk.byteLength; } else { for (let i = 0; i + this.packetSize_ <= chunk.byteLength; i += this.packetSize_) { const start = i + M2ts.M2TS_TIMESTAMP_PREFIX_SIZE; out.set(chunk.subarray(start, start + packetSize), offset); offset += packetSize; } } } return out; } /** * Derives the group's presentation timing from the timestamps of the * elementary stream the track is presented as. * * @return {?{startTime: number, duration: number}} * @private */ getTiming_() { const ContentType = shaka.util.ManifestParserUtils.ContentType; const timescale = shaka.util.TsParser.Timescale; const pesList = this.timingContentType_ == ContentType.VIDEO ? this.tsParser_.getVideoData(/* naluProcessing= */ false) : this.tsParser_.getAudioData(); let earliest = Infinity; let latestPts = -Infinity; let count = 0; for (const pes of pesList) { if (pes.pts == null) { continue; } // A frame is decoded at its DTS and presented at its PTS, so the group // starts at the earlier of the two and ends at the last PTS. const start = pes.dts != null ? Math.min(pes.pts, pes.dts) : pes.pts; earliest = Math.min(earliest, start); latestPts = Math.max(latestPts, pes.pts); count++; } if (!count) { return null; } const startTime = earliest / timescale; const span = (latestPts - earliest) / timescale; // The last frame's own duration is not in the timestamps. Use the // catalog's framerate when it has one, and otherwise the average spacing // of this group's frames. let lastFrameDuration = this.frameDuration_; if (!lastFrameDuration && count > 1) { lastFrameDuration = span / (count - 1); } const duration = span + lastFrameDuration; if (!(duration > 0)) { return null; } return {startTime, duration}; } /** * @private */ resetGroup_() { this.chunks_ = []; this.packetCount_ = 0; this.discontinuityInGroup_ = false; } }; shaka.msf.PackagingRegistry.registerPackaging( 'm2ts', () => new shaka.msf.packaging.M2ts());