/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.msf.packaging.Mpeg2ts');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.SegmentUtils');
goog.require('shaka.msf.PackagingRegistry');
goog.require('shaka.msf.packaging.Mpeg2tsProgram');
goog.require('shaka.util.Error');
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 source 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. 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.
*
* Of the draft's carriage modes, the ones that carry a whole program are
* supported: "unmodified-program", "per-program" and "unmodified-multiplex",
* which is cut down to its reference program. "es-packets" splits a program
* into one track per PID, which would have to be put back together before it
* could be played, and is not supported yet.
*
* @see https://datatracker.ietf.org/doc/draft-gregoire-moq-msfts/
*
* @implements {shaka.extern.MsfPackaging}
* @final
*/
shaka.msf.packaging.Mpeg2ts = class {
constructor() {
/** @private {?shaka.msf.packaging.Mpeg2ts.Config} */
this.config_ = null;
}
/**
* @override
*/
describeTrack(track, initData) {
const Mpeg2ts = shaka.msf.packaging.Mpeg2ts;
const Mode = Mpeg2ts.Mode_;
const ContentType = shaka.util.ManifestParserUtils.ContentType;
const name = track.name;
const mode = track.mpeg2tsMode;
if (mode == Mode.ES_PACKETS) {
shaka.log.alwaysWarn(`mpeg2ts track "${name}" uses the "es-packets" ` +
'mode, which is not supported yet; skipping.');
return null;
}
if (mode != Mode.UNMODIFIED_PROGRAM && mode != Mode.PER_PROGRAM &&
mode != Mode.UNMODIFIED_MULTIPLEX) {
shaka.log.alwaysWarn(`mpeg2ts track "${name}" declares an unknown ` +
`mpeg2tsMode "${mode}"; skipping.`);
return null;
}
const packetSize = track.mpeg2tsPacketSize;
if (packetSize != Mpeg2ts.TS_PACKET_SIZE &&
packetSize != Mpeg2ts.M2TS_PACKET_SIZE) {
shaka.log.alwaysWarn(`mpeg2ts track "${name}" declares an ` +
`unsupported mpeg2tsPacketSize ${packetSize}; skipping. Only 188 ` +
'and 192 are supported.');
return null;
}
if (track.mpeg2tsEsPid != undefined) {
shaka.log.alwaysWarn(`mpeg2ts track "${name}" declares mpeg2tsEsPid ` +
'outside the "es-packets" mode; skipping.');
return null;
}
// The catalog is untrusted input, and these values decide which packets
// are kept.
const ranges = [
['mpeg2tsProgramNumber', track.mpeg2tsProgramNumber, 1, 0xffff],
['mpeg2tsPcrPid', track.mpeg2tsPcrPid, 0, Mpeg2ts.MAX_PID_],
];
for (const [field, value, min, max] of ranges) {
if (value != undefined &&
!(Number.isInteger(value) && value >= min && value <= max)) {
shaka.log.alwaysWarn(`mpeg2ts track "${name}" declares an invalid ` +
`${field} ${value}; skipping.`);
return null;
}
}
const isMultiplex = mode == Mode.UNMODIFIED_MULTIPLEX;
if (isMultiplex) {
// Only one program of a multiplex can be played, and without a
// reference program nothing says which.
if (track.mpeg2tsProgramNumber == undefined) {
shaka.log.alwaysWarn(`mpeg2ts track "${name}" carries a multiplex ` +
'without naming a reference program in mpeg2tsProgramNumber; ' +
'skipping.');
return null;
}
if (track.altGroup != undefined) {
shaka.log.alwaysWarn(`mpeg2ts track "${name}" carries a multiplex ` +
'in an altGroup, which the draft forbids; skipping.');
return null;
}
}
if (track.mpeg2tsTimestampMode != undefined &&
packetSize == Mpeg2ts.TS_PACKET_SIZE) {
shaka.log.warning(`mpeg2ts track "${name}" declares ` +
'mpeg2tsTimestampMode for 188-octet packets, which have no ' +
'timestamp prefix; ignoring it.');
}
const randomAccess = track.mpeg2tsRandomAccess === true;
if (!randomAccess) {
shaka.log.info(`mpeg2ts track "${name}" does not guarantee that a ` +
'group begins at a random access point, so the first group is ' +
'played from its first one.');
}
let rawPsi = new Uint8Array(0);
if (initData.byteLength) {
if (Mpeg2ts.isWholePackets_(initData, packetSize)) {
rawPsi = Mpeg2ts.normalizePackets_(initData, packetSize);
} else {
shaka.log.warning(`mpeg2ts track "${name}" has initialization data ` +
'that is not a run of whole source packets; ignoring it.');
}
}
// Outside a multiplex the program number is advisory, and the PAT, which
// takes precedence, lists only the one program anyway.
const programNumber = isMultiplex ?
/** @type {number} */ (track.mpeg2tsProgramNumber) : null;
const program =
new shaka.msf.packaging.Mpeg2tsProgram(programNumber, isMultiplex);
Mpeg2ts.feed_(program, rawPsi);
// A multiplex's initialization data may still list every program, so
// only what was learnt of the reference program is kept of it.
const psi = isMultiplex ? program.getPsi() : rawPsi;
// The codec decides which source buffers MediaSource opens, which happens
// before the first group has arrived. The draft leaves it to the base MSF
// catalog, and failing that it can be read from the PMT.
let codecs = [];
if (track.codec) {
codecs = track.codec.split(',')
.map((codec) => codec.trim())
.filter((codec) => codec.length);
} else {
codecs = Mpeg2ts.codecsFromPsi_(program.getPsi());
if (!codecs.length) {
shaka.log.alwaysWarn(`mpeg2ts track "${name}" declares no codec, ` +
'and none can be read from initialization data; skipping.');
return null;
}
}
const hasVideo = codecs.some((codec) => Mpeg2ts.isVideoCodec_(codec));
const hasAudio = codecs.some((codec) => Mpeg2ts.isAudioCodec_(codec));
if (!hasVideo && !hasAudio) {
shaka.log.alwaysWarn(`mpeg2ts track "${name}" declares no ` +
`recognized codec ("${codecs.join(',')}"); skipping.`);
return null;
}
this.config_ = {
packetSize,
psi,
programNumber,
filter: isMultiplex,
// Timestamps are read from whichever elementary stream the track is
// presented as. A muxed program is presented as video.
timingContentType: hasVideo ? ContentType.VIDEO : ContentType.AUDIO,
frameDuration: track.framerate ? 1 / track.framerate : 0,
pcrPid: track.mpeg2tsPcrPid != undefined ? track.mpeg2tsPcrPid : null,
randomAccess,
};
// 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 a group that lacks it instead, because it is
// part of the transport stream rather than something appended before it.
return {basicInfo, initSegmentReference: null};
}
/**
* @override
*/
createSegmenter() {
goog.asserts.assert(this.config_, 'The track must be described first');
return new shaka.msf.packaging.Mpeg2tsSegmenter(
/** @type {shaka.msf.packaging.Mpeg2ts.Config} */ (this.config_));
}
/**
* @param {!shaka.msf.packaging.Mpeg2tsProgram} program
* @param {!Uint8Array} data 188-octet transport packets.
* @private
*/
static feed_(program, data) {
const size = shaka.msf.packaging.Mpeg2ts.TS_PACKET_SIZE;
for (let i = 0; i + size <= data.byteLength; i += size) {
program.process(data.subarray(i, i + size));
}
}
/**
* @param {!Uint8Array} data
* @param {number} packetSize
* @return {boolean}
* @private
*/
static isWholePackets_(data, packetSize) {
if (data.byteLength % packetSize) {
return false;
}
const tsOffset = packetSize - shaka.msf.packaging.Mpeg2ts.TS_PACKET_SIZE;
for (let i = tsOffset; i < data.byteLength; i += packetSize) {
if (data[i] != 0x47) {
return false;
}
}
return true;
}
/**
* Copies out the 188-octet transport packets of a buffer of source packets,
* dropping the 4-octet prefix of 192-octet M2TS source packets.
*
* Whatever the prefix carries -- an arrival time, or something the draft
* leaves opaque -- 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 Mpeg2ts = shaka.msf.packaging.Mpeg2ts;
if (packetSize == Mpeg2ts.TS_PACKET_SIZE) {
return data;
}
const count = Math.floor(data.byteLength / packetSize);
const out = new Uint8Array(count * Mpeg2ts.TS_PACKET_SIZE);
for (let i = 0; i < count; i++) {
const start = i * packetSize + Mpeg2ts.M2TS_PREFIX_SIZE;
out.set(data.subarray(start, start + Mpeg2ts.TS_PACKET_SIZE),
i * Mpeg2ts.TS_PACKET_SIZE);
}
return out;
}
/**
* Reads the codecs of a program from its PMT.
*
* A PMT only says which kind of codec each stream uses, not its profile or
* level, so these are the same defaults used when a transport stream is
* probed. The transmuxer writes the real ones into the initialization
* segment it produces.
*
* @param {!Uint8Array} psi The program's PAT and PMT.
* @return {!Array<string>}
* @private
*/
static codecsFromPsi_(psi) {
const Mpeg2ts = shaka.msf.packaging.Mpeg2ts;
if (!psi.byteLength) {
return [];
}
// TsParser ignores anything shorter than three packets, so a null packet
// pads out a lone PAT and PMT.
const data = new Uint8Array(psi.byteLength + Mpeg2ts.TS_PACKET_SIZE);
data.set(psi);
data.set([0x47, 0x1f, 0xff, 0x10], psi.byteLength);
const tsCodecs = new shaka.util.TsParser().parse(data).getCodecs();
const codecs = [];
const video = Mpeg2ts.DEFAULT_VIDEO_CODECS_.get(tsCodecs.video || '');
if (video) {
codecs.push(video);
}
const audio = Mpeg2ts.DEFAULT_AUDIO_CODECS_.get(tsCodecs.audio || '');
if (audio) {
codecs.push(audio);
}
return codecs;
}
/**
* @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));
}
};
/**
* What a segmenter needs to know about its track.
*
* @typedef {{
* packetSize: number,
* psi: !Uint8Array,
* programNumber: ?number,
* filter: boolean,
* timingContentType: string,
* frameDuration: number,
* pcrPid: ?number,
* randomAccess: boolean,
* }}
*
* @property {number} packetSize
* The source packet size, 188 or 192.
* @property {!Uint8Array} psi
* The catalog's initialization packets, normalized to 188 octets.
* @property {?number} programNumber
* The program to follow, or null for the first one.
* @property {boolean} filter
* Whether the track is a multiplex to cut down to that program.
* @property {string} timingContentType
* The elementary stream the track's timing is read from.
* @property {number} frameDuration
* Seconds per frame from the catalog, or 0 when it does not say.
* @property {?number} pcrPid
* The catalog's PCR PID, used until the PMT has been seen.
* @property {boolean} randomAccess
* Whether every group is guaranteed to begin at a random access point.
*/
shaka.msf.packaging.Mpeg2ts.Config;
/**
* The values of mpeg2tsMode.
*
* @enum {string}
* @private
*/
shaka.msf.packaging.Mpeg2ts.Mode_ = {
UNMODIFIED_PROGRAM: 'unmodified-program',
UNMODIFIED_MULTIPLEX: 'unmodified-multiplex',
PER_PROGRAM: 'per-program',
ES_PACKETS: 'es-packets',
};
/**
* The size of an MPEG-2 transport packet.
*
* @const {number}
*/
shaka.msf.packaging.Mpeg2ts.TS_PACKET_SIZE = 188;
/**
* The size of an M2TS source packet: a transport packet behind a 4-octet
* prefix.
*
* @const {number}
*/
shaka.msf.packaging.Mpeg2ts.M2TS_PACKET_SIZE = 192;
/**
* @const {number}
*/
shaka.msf.packaging.Mpeg2ts.M2TS_PREFIX_SIZE = 4;
/**
* @private @const {number}
*/
shaka.msf.packaging.Mpeg2ts.MAX_PID_ = 0x1fff;
/**
* @private @const {!Map<string, string>}
*/
shaka.msf.packaging.Mpeg2ts.DEFAULT_VIDEO_CODECS_ = new Map([
['avc', 'avc1.42E01E'],
['hvc', 'hvc1.1.6.L93.90'],
['av1', 'av01.0.01M.08'],
]);
/**
* @private @const {!Map<string, string>}
*/
shaka.msf.packaging.Mpeg2ts.DEFAULT_AUDIO_CODECS_ = new Map([
['aac', 'mp4a.40.2'],
['aac-loas', 'mp4a.40.2'],
['mp3', 'mp4a.40.34'],
['ac3', 'ac-3'],
['ec3', 'ec-3'],
['opus', 'opus'],
]);
/**
* 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.Mpeg2tsSegmenter = class {
/**
* @param {shaka.msf.packaging.Mpeg2ts.Config} config
*/
constructor(config) {
/** @private {shaka.msf.packaging.Mpeg2ts.Config} */
this.config_ = config;
/** @private {!shaka.msf.packaging.Mpeg2tsProgram} */
this.program_ = new shaka.msf.packaging.Mpeg2tsProgram(
config.programNumber, config.filter);
shaka.msf.packaging.Mpeg2ts.feed_(this.program_, config.psi);
/**
* 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;
/**
* The 188-octet transport packets kept of the current part of the group.
* @private {!Array<!Uint8Array>}
*/
this.packets_ = [];
/**
* The PAT and PMT known when the current part began, which is what to put
* in front of it if it does not open with its own.
* @private {!Uint8Array}
*/
this.psi_ = new Uint8Array(0);
/** @private {boolean} */
this.startsWithPsi_ = false;
/**
* Whether an elementary stream has started a PES packet in the current
* part, which is what makes a discontinuity worth a part of its own.
* @private {boolean}
*/
this.hasMedia_ = false;
/** @private {boolean} */
this.discontinuity_ = 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;
/**
* Whether the first random access point is still to come. A subscription
* joins a track at the start of a Group, and when the catalog does not
* guarantee that a Group starts at a random access point, what comes
* before the first one cannot be decoded.
* @private {boolean}
*/
this.waitingForRandomAccess_ = !config.randomAccess;
}
/**
* @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) {
segments.push(...this.append_(obj.data));
}
return segments;
}
/**
* Validates an object's source packets and holds onto what is kept of them
* until the group ends.
*
* @param {!Uint8Array} data
* @return {!Array<!shaka.extern.MsfSegment>} A part of the group cut short
* by a discontinuity, if there was one.
* @private
*/
append_(data) {
const Mpeg2ts = shaka.msf.packaging.Mpeg2ts;
const Mpeg2tsProgram = shaka.msf.packaging.Mpeg2tsProgram;
const size = this.config_.packetSize;
if (!Mpeg2ts.isWholePackets_(data, size)) {
shaka.log.warning('Dropping mpeg2ts object whose payload is not a ' +
`run of whole ${size}-octet source packets.`);
return [];
}
/** @type {!Array<!shaka.extern.MsfSegment>} */
const segments = [];
// In a 192-octet source packet the transport packet starts after the
// prefix.
const tsOffset = size - Mpeg2ts.TS_PACKET_SIZE;
for (let i = 0; i < data.byteLength; i += size) {
const packet = this.program_.process(data.subarray(
i + tsOffset, i + tsOffset + Mpeg2ts.TS_PACKET_SIZE));
if (!packet) {
continue;
}
const pid = Mpeg2tsProgram.getPid(packet);
const isPsi = this.program_.isPsiPid(pid);
if (!isPsi && (packet[3] & 0xc0)) {
shaka.log.alwaysWarn('The mpeg2ts track is scrambled at the ' +
'transport stream level, which browsers cannot descramble.');
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER);
}
if (!isPsi && this.isDiscontinuity_(packet, pid)) {
// In the unmodified modes a discontinuity in the source reaches us
// wherever it happened, so it can fall in the middle of a Group. The
// media on either side of it is on a different clock and cannot be
// timed as one segment.
if (this.hasMedia_) {
const segment = this.flush_();
if (segment) {
segments.push(segment);
}
}
this.discontinuity_ = true;
}
if (!this.packets_.length) {
this.startsWithPsi_ = isPsi;
this.psi_ = this.program_.getPsi();
if (!this.psi_.byteLength) {
this.psi_ = this.config_.psi;
}
}
if (!isPsi && (packet[1] & 0x40)) {
this.hasMedia_ = true;
}
this.packets_.push(packet);
}
return segments;
}
/**
* 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.
*
* Only the PCR PID is trusted, because on other PIDs the same bit signals a
* continuity counter discontinuity, which is a different thing and does not
* move the clock. The PMT says which PID that is; the catalog's value is
* advisory and only used until the PMT has been seen.
*
* @param {!Uint8Array} packet
* @param {number} pid
* @return {boolean}
* @private
*/
isDiscontinuity_(packet, pid) {
const adaptationFieldControl = (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 (!packet[4]) {
return false;
}
if (!(packet[5] & 0x80)) {
return false;
}
let pcrPid = this.program_.getPcrPid();
if (pcrPid == null) {
pcrPid = this.config_.pcrPid;
}
return pcrPid == null || pid == pcrPid;
}
/**
* Turns the accumulated part of the group into a segment.
*
* @return {?shaka.extern.MsfSegment}
* @private
*/
flush_() {
const Mpeg2tsProgram = shaka.msf.packaging.Mpeg2tsProgram;
let packets = this.packets_;
const psi = this.startsWithPsi_ ? new Uint8Array(0) : this.psi_;
const discontinuity = this.discontinuity_;
this.resetGroup_();
if (!packets.length) {
return null;
}
if (this.waitingForRandomAccess_) {
const start = this.findRandomAccess_(packets);
if (start < 0) {
shaka.log.info('Dropping an mpeg2ts group with no random access ' +
'point to start playback from.');
return null;
}
if (start > 0) {
// The program information ahead of it is still needed to read it.
packets = packets.filter((packet, i) => i >= start ||
this.program_.isPsiPid(Mpeg2tsProgram.getPid(packet)));
}
this.waitingForRandomAccess_ = false;
}
if (discontinuity) {
this.discontinuitySequence_++;
}
const data = this.buildData_(psi, packets);
this.tsParser_.clearData();
this.tsParser_.setDiscontinuitySequence(this.discontinuitySequence_);
this.tsParser_.parse(data);
const timing = this.getTiming_();
if (!timing) {
shaka.log.warning('Dropping mpeg2ts 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_,
independent: this.config_.randomAccess ||
this.tsParser_.startsWithVideoKeyframe() !== false,
group: this.group_ === null ? undefined : this.group_,
};
}
/**
* Copies the part's transport packets into one buffer, behind the program
* information it needs.
*
* 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. The latest
* one seen in the track is used rather than the catalog's, because the
* program information in the media takes precedence when the two differ.
*
* @param {!Uint8Array} psi
* @param {!Array<!Uint8Array>} packets
* @return {!Uint8Array}
* @private
*/
buildData_(psi, packets) {
const size = shaka.msf.packaging.Mpeg2ts.TS_PACKET_SIZE;
const out = new Uint8Array(psi.byteLength + packets.length * size);
out.set(psi, 0);
let offset = psi.byteLength;
for (const packet of packets) {
out.set(packet, offset);
offset += size;
}
return out;
}
/**
* The index of the packet that starts the first random access point among
* the given packets, or -1 when there is none.
*
* A random access point is a PES packet of the video stream that the
* publisher flagged with random_access_indicator, or whose first picture is
* an IDR. A track without H.264 or H.265 video can be decoded from
* anywhere, as far as can be told here.
*
* @param {!Array<!Uint8Array>} packets
* @return {number}
* @private
*/
findRandomAccess_(packets) {
const Mpeg2tsProgram = shaka.msf.packaging.Mpeg2tsProgram;
const video = this.program_.getVideoStream();
if (!video) {
return 0;
}
for (let i = 0; i < packets.length; i++) {
const packet = packets[i];
if (Mpeg2tsProgram.getPid(packet) != video.pid ||
!(packet[1] & 0x40)) {
continue;
}
const adaptationFieldControl = (packet[3] & 0x30) >> 4;
if (adaptationFieldControl > 1 && packet[4] && (packet[5] & 0x40)) {
return i;
}
if (this.startsWithIdr_(packets, i, video.pid, video.streamType)) {
return i;
}
}
return -1;
}
/**
* Whether the first picture of the PES packet that starts at the given
* packet is an IDR.
*
* @param {!Array<!Uint8Array>} packets
* @param {number} index
* @param {number} pid
* @param {number} streamType
* @return {boolean}
* @private
*/
startsWithIdr_(packets, index, pid, streamType) {
const Mpeg2tsProgram = shaka.msf.packaging.Mpeg2tsProgram;
const Mpeg2tsSegmenter = shaka.msf.packaging.Mpeg2tsSegmenter;
const isHevc = streamType == 0x24;
// The first picture follows the parameter sets and SEI, which fit in a
// few packets; looking further would only cost time.
const payloads = [];
let length = 0;
for (let i = index; i < packets.length &&
payloads.length < Mpeg2tsSegmenter.MAX_IDR_SCAN_PACKETS_; i++) {
const packet = packets[i];
if (Mpeg2tsProgram.getPid(packet) != pid) {
continue;
}
if (i > index && (packet[1] & 0x40)) {
break;
}
const payload =
packet.subarray(Mpeg2tsProgram.getPayloadOffset(packet));
payloads.push(payload);
length += payload.byteLength;
}
const data = new Uint8Array(length);
let offset = 0;
for (const payload of payloads) {
data.set(payload, offset);
offset += payload.byteLength;
}
// Skip the PES header to reach the elementary stream.
if (data.byteLength < 9 || data[0] || data[1] || data[2] != 1) {
return false;
}
for (let i = 9 + data[8]; i + 3 < data.byteLength; i++) {
if (data[i] || data[i + 1] || data[i + 2] != 1) {
continue;
}
i += 3;
if (isHevc) {
const type = (data[i] >> 1) & 0x3f;
// 16 to 21 are the random access pictures, below them the rest.
if (type <= 21) {
return type >= 16;
}
} else {
const type = data[i] & 0x1f;
// 5 is an IDR slice, 1 to 4 the rest.
if (type >= 1 && type <= 5) {
return type == 5;
}
}
}
return false;
}
/**
* Derives the part'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.config_.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 part
// 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 part's frames.
let lastFrameDuration = this.config_.frameDuration;
if (!lastFrameDuration && count > 1) {
lastFrameDuration = span / (count - 1);
}
const duration = span + lastFrameDuration;
if (!(duration > 0)) {
return null;
}
return {startTime, duration};
}
/**
* @private
*/
resetGroup_() {
this.packets_ = [];
this.psi_ = new Uint8Array(0);
this.startsWithPsi_ = false;
this.hasMedia_ = false;
this.discontinuity_ = false;
}
};
/**
* @private @const {number}
*/
shaka.msf.packaging.Mpeg2tsSegmenter.MAX_IDR_SCAN_PACKETS_ = 64;
shaka.msf.PackagingRegistry.registerPackaging(
'mpeg2ts', () => new shaka.msf.packaging.Mpeg2ts());