/*! @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());