/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.msf.packaging.Mpeg2tsProgram');
goog.require('shaka.log');
goog.require('shaka.util.TsParser');
/**
* Follows the program specific information of one program in a transport
* stream, packet by packet.
*
* It learns which PIDs make up the program from its PAT and PMT, remembers the
* latest PAT and PMT so they can be put back in front of a Group that opens
* without them.
*
* When asked to filter, it also cuts a multi-program transport stream down to
* the one program: every packet of another program is dropped and the PAT is
* rewritten to list only the program kept. The transmuxer's own parser follows
* whichever program the PAT lists first, so a stream that still listed the
* others would play the wrong one.
*
* @final
*/
shaka.msf.packaging.Mpeg2tsProgram = class {
/**
* @param {?number} programNumber The program to follow, or null for the
* first one the PAT lists.
* @param {boolean} filter Whether to drop the packets of every other program
* and rewrite the PAT to list only this one.
*/
constructor(programNumber, filter) {
/** @private {?number} */
this.programNumber_ = programNumber;
/** @private {boolean} */
this.filter_ = filter;
/**
* The program the PAT selected, which is the requested one when there is
* one.
* @private {?number}
*/
this.selectedProgram_ = null;
/** @private {?number} */
this.pmtPid_ = null;
/** @private {?number} */
this.pcrPid_ = null;
/**
* The stream_type of each elementary stream the PMT lists, by PID.
* @private {!Map<number, number>}
*/
this.streams_ = new Map();
/** @private {!Array<!Uint8Array>} */
this.patPackets_ = [];
/** @private {!Array<!Uint8Array>} */
this.pmtPackets_ = [];
/**
* Sections still being reassembled, by PID.
* @private {!Map<number, shaka.msf.packaging.Mpeg2tsProgram.Section>}
*/
this.sections_ = new Map();
/**
* The rewritten PAT produced by the packet being processed, if any.
* @private {?Uint8Array}
*/
this.rewrittenPat_ = null;
/** @private {number} */
this.patContinuityCounter_ = 0;
}
/**
* Reads one 188-octet transport packet and returns what should be kept of
* it: the packet itself, a rewritten PAT in its place, or null when it does
* not belong to the program.
*
* @param {!Uint8Array} packet
* @return {?Uint8Array}
*/
process(packet) {
const Mpeg2tsProgram = shaka.msf.packaging.Mpeg2tsProgram;
const pid = Mpeg2tsProgram.getPid(packet);
if (pid == Mpeg2tsProgram.PAT_PID_) {
this.rewrittenPat_ = null;
this.readSections_(pid, packet);
return this.filter_ ? this.rewrittenPat_ : packet;
}
if (pid == this.pmtPid_) {
this.readSections_(pid, packet);
return packet;
}
if (!this.filter_ || this.streams_.has(pid) || pid == this.pcrPid_) {
return packet;
}
return null;
}
/**
* The latest PAT and PMT of the program as transport packets, or an empty
* array until both have been seen.
*
* @return {!Uint8Array}
*/
getPsi() {
if (!this.patPackets_.length || !this.pmtPackets_.length) {
return new Uint8Array(0);
}
const packets = this.patPackets_.concat(this.pmtPackets_);
const out = new Uint8Array(
packets.length * shaka.msf.packaging.Mpeg2tsProgram.PACKET_SIZE);
for (let i = 0; i < packets.length; i++) {
out.set(packets[i], i * shaka.msf.packaging.Mpeg2tsProgram.PACKET_SIZE);
}
return out;
}
/**
* Whether a PID carries the program's PAT or PMT.
*
* @param {number} pid
* @return {boolean}
*/
isPsiPid(pid) {
return pid == shaka.msf.packaging.Mpeg2tsProgram.PAT_PID_ ||
pid == this.pmtPid_;
}
/**
* The PID the PMT says carries the program clock, or null until the PMT has
* been seen or when the program has none.
*
* @return {?number}
*/
getPcrPid() {
const Mpeg2tsProgram = shaka.msf.packaging.Mpeg2tsProgram;
return this.pcrPid_ == Mpeg2tsProgram.NULL_PID_ ? null : this.pcrPid_;
}
/**
* The program's video elementary stream, or null when the PMT lists none
* or has not been seen.
*
* @return {?{pid: number, streamType: number}}
*/
getVideoStream() {
for (const [pid, streamType] of this.streams_) {
if (shaka.msf.packaging.Mpeg2tsProgram.VIDEO_STREAM_TYPES_.has(
streamType)) {
return {pid, streamType};
}
}
return null;
}
/**
* Reassembles the PSI sections a packet carries, which may finish one that
* started in an earlier packet, start several, or both.
*
* @param {number} pid
* @param {!Uint8Array} packet
* @private
*/
readSections_(pid, packet) {
const Mpeg2tsProgram = shaka.msf.packaging.Mpeg2tsProgram;
const size = Mpeg2tsProgram.PACKET_SIZE;
let offset = Mpeg2tsProgram.getPayloadOffset(packet);
if (offset >= size) {
return;
}
const pending = this.sections_.get(pid);
if (!(packet[1] & 0x40)) {
if (pending) {
this.continueSection_(pid, pending, packet, packet.subarray(offset));
}
return;
}
// The pointer field counts the octets that finish the previous section.
const pointer = packet[offset];
offset++;
if (pending) {
this.continueSection_(pid, pending, packet,
packet.subarray(offset, Math.min(offset + pointer, size)));
}
this.sections_.delete(pid);
offset += pointer;
// Stuffing octets end the sections in a packet.
while (offset + 3 <= size && packet[offset] != 0xff) {
const length =
3 + (((packet[offset + 1] & 0x0f) << 8) | packet[offset + 2]);
if (length > Mpeg2tsProgram.MAX_SECTION_LENGTH_) {
return;
}
/** @type {shaka.msf.packaging.Mpeg2tsProgram.Section} */
const section = {
data: new Uint8Array(length),
received: 0,
packets: [],
};
const chunk = packet.subarray(offset, Math.min(offset + length, size));
offset += chunk.byteLength;
if (!this.continueSection_(pid, section, packet, chunk)) {
this.sections_.set(pid, section);
return;
}
}
}
/**
* @param {number} pid
* @param {shaka.msf.packaging.Mpeg2tsProgram.Section} section
* @param {!Uint8Array} packet
* @param {!Uint8Array} chunk
* @return {boolean} Whether the section is complete.
* @private
*/
continueSection_(pid, section, packet, chunk) {
const take = Math.min(
chunk.byteLength, section.data.byteLength - section.received);
section.data.set(chunk.subarray(0, take), section.received);
section.received += take;
section.packets.push(packet);
if (section.received < section.data.byteLength) {
return false;
}
this.sections_.delete(pid);
this.onSection_(pid, section.data, section.packets);
return true;
}
/**
* @param {number} pid
* @param {!Uint8Array} section
* @param {!Array<!Uint8Array>} packets The packets the section came in.
* @private
*/
onSection_(pid, section, packets) {
const Mpeg2tsProgram = shaka.msf.packaging.Mpeg2tsProgram;
if (shaka.util.TsParser.crc32(section)) {
shaka.log.v1('Ignoring a transport stream section with a bad CRC');
return;
}
const tableId = section[0];
if (pid == Mpeg2tsProgram.PAT_PID_ && tableId == 0x00) {
this.onPat_(section, packets);
} else if (pid == this.pmtPid_ && tableId == 0x02) {
this.onPmt_(section, packets);
}
}
/**
* @param {!Uint8Array} section
* @param {!Array<!Uint8Array>} packets
* @private
*/
onPat_(section, packets) {
const Mpeg2tsProgram = shaka.msf.packaging.Mpeg2tsProgram;
let programNumber = null;
let pmtPid = null;
const end = section.byteLength - 4;
for (let i = 8; i + 4 <= end; i += 4) {
const number = (section[i] << 8) | section[i + 1];
// Program 0 points at the network information table, not at a PMT.
if (!number) {
continue;
}
if (this.programNumber_ == null || number == this.programNumber_) {
programNumber = number;
pmtPid = ((section[i + 2] & 0x1f) << 8) | section[i + 3];
break;
}
}
if (programNumber == null || pmtPid == null) {
shaka.log.warning(this.programNumber_ == null ?
'The transport stream PAT lists no program' :
`The transport stream PAT does not list program ` +
`${this.programNumber_}`);
return;
}
if (pmtPid != this.pmtPid_ || programNumber != this.selectedProgram_) {
this.pmtPid_ = pmtPid;
this.selectedProgram_ = programNumber;
this.pcrPid_ = null;
this.streams_.clear();
this.pmtPackets_ = [];
this.sections_.delete(pmtPid);
}
if (this.filter_) {
this.rewrittenPat_ = this.buildPat_(section, programNumber, pmtPid);
this.patPackets_ = [this.rewrittenPat_];
} else {
this.patPackets_ = Mpeg2tsProgram.copyPackets_(packets);
}
}
/**
* @param {!Uint8Array} section
* @param {!Array<!Uint8Array>} packets
* @private
*/
onPmt_(section, packets) {
// Several programs may share a PMT PID.
const programNumber = (section[3] << 8) | section[4];
if (programNumber != this.selectedProgram_) {
return;
}
this.pcrPid_ = ((section[8] & 0x1f) << 8) | section[9];
this.streams_.clear();
const infoLength = ((section[10] & 0x0f) << 8) | section[11];
const end = section.byteLength - 4;
for (let i = 12 + infoLength; i + 5 <= end;) {
const streamType = section[i];
const pid = ((section[i + 1] & 0x1f) << 8) | section[i + 2];
const esInfoLength = ((section[i + 3] & 0x0f) << 8) | section[i + 4];
this.streams_.set(pid, streamType);
i += 5 + esInfoLength;
}
this.pmtPackets_ =
shaka.msf.packaging.Mpeg2tsProgram.copyPackets_(packets);
}
/**
* Builds a PAT that lists only the given program.
*
* @param {!Uint8Array} source The PAT section it replaces.
* @param {number} programNumber
* @param {number} pmtPid
* @return {!Uint8Array}
* @private
*/
buildPat_(source, programNumber, pmtPid) {
const Mpeg2tsProgram = shaka.msf.packaging.Mpeg2tsProgram;
const packet = new Uint8Array(Mpeg2tsProgram.PACKET_SIZE).fill(0xff);
packet[0] = 0x47;
// payload_unit_start_indicator, PID 0.
packet[1] = 0x40;
packet[2] = 0x00;
// Payload only.
packet[3] = 0x10 | this.patContinuityCounter_;
this.patContinuityCounter_ = (this.patContinuityCounter_ + 1) & 0x0f;
// pointer_field
packet[4] = 0;
const section = packet.subarray(5, 5 + 16);
section[0] = 0x00;
// section_syntax_indicator and a section_length of 13.
section[1] = 0xb0;
section[2] = 13;
// transport_stream_id, version_number and current_next_indicator are the
// source's, so a change to the source PAT still reads as one.
section[3] = source[3];
section[4] = source[4];
section[5] = source[5];
// section_number and last_section_number.
section[6] = 0;
section[7] = 0;
section[8] = programNumber >> 8;
section[9] = programNumber & 0xff;
section[10] = 0xe0 | (pmtPid >> 8);
section[11] = pmtPid & 0xff;
const crc = shaka.util.TsParser.crc32(section.subarray(0, 12));
section[12] = crc >>> 24;
section[13] = (crc >>> 16) & 0xff;
section[14] = (crc >>> 8) & 0xff;
section[15] = crc & 0xff;
return packet;
}
/**
* @param {!Array<!Uint8Array>} packets
* @return {!Array<!Uint8Array>}
* @private
*/
static copyPackets_(packets) {
// The packets are views of a whole MoQT object, which a reference would
// keep alive for as long as the PSI is.
return packets.map((packet) => packet.slice());
}
/**
* @param {!Uint8Array} packet
* @return {number}
*/
static getPid(packet) {
return ((packet[1] & 0x1f) << 8) | packet[2];
}
/**
* The offset of a transport packet's payload, which is the packet size when
* it has none.
*
* @param {!Uint8Array} packet
* @return {number}
*/
static getPayloadOffset(packet) {
const adaptationFieldControl = (packet[3] & 0x30) >> 4;
if (!(adaptationFieldControl & 0x01)) {
return shaka.msf.packaging.Mpeg2tsProgram.PACKET_SIZE;
}
if (adaptationFieldControl == 0x03) {
return 5 + packet[4];
}
return 4;
}
};
/**
* A PSI section being reassembled from the packets it spans.
*
* @typedef {{
* data: !Uint8Array,
* received: number,
* packets: !Array<!Uint8Array>,
* }}
*/
shaka.msf.packaging.Mpeg2tsProgram.Section;
/**
* @const {number}
*/
shaka.msf.packaging.Mpeg2tsProgram.PACKET_SIZE = 188;
/**
* @private @const {number}
*/
shaka.msf.packaging.Mpeg2tsProgram.PAT_PID_ = 0x0000;
/**
* @private @const {number}
*/
shaka.msf.packaging.Mpeg2tsProgram.NULL_PID_ = 0x1fff;
/**
* The longest a PAT or PMT section may be.
*
* @private @const {number}
*/
shaka.msf.packaging.Mpeg2tsProgram.MAX_SECTION_LENGTH_ = 1024;
/**
* The video stream_types whose access units can be told apart as random
* access points: H.264 and H.265.
*
* @private @const {!Set<number>}
*/
shaka.msf.packaging.Mpeg2tsProgram.VIDEO_STREAM_TYPES_ = new Set([0x1b, 0x24]);