mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-13 15:46:46 +03:00
87a7d86daf
Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
1398 lines
42 KiB
JavaScript
1398 lines
42 KiB
JavaScript
/*! @license
|
||
* Shaka Player
|
||
* Copyright 2016 Google LLC
|
||
* SPDX-License-Identifier: Apache-2.0
|
||
*/
|
||
|
||
goog.provide('shaka.util.TsParser');
|
||
|
||
goog.require('goog.asserts');
|
||
goog.require('shaka.log');
|
||
goog.require('shaka.metadata.Id3Utils');
|
||
goog.require('shaka.util.ExpGolomb');
|
||
goog.require('shaka.util.Uint8ArrayUtils');
|
||
|
||
|
||
/**
|
||
* @see https://en.wikipedia.org/wiki/MPEG_transport_stream
|
||
* @export
|
||
*/
|
||
shaka.util.TsParser = class {
|
||
constructor() {
|
||
/** @private {?number} */
|
||
this.pmtId_ = null;
|
||
|
||
/** @private {boolean} */
|
||
this.pmtParsed_ = false;
|
||
|
||
/** @private {?number} */
|
||
this.videoPid_ = null;
|
||
|
||
/** @private {?string} */
|
||
this.videoCodec_ = null;
|
||
|
||
/** @private {!Array<!Uint8Array>} Flat list of all video TS packets. */
|
||
this.videoData_ = [];
|
||
|
||
/**
|
||
* Index into videoData_ where each video PES unit starts.
|
||
* @private {!Array<number>}
|
||
*/
|
||
this.videoDataPesIndices_ = [];
|
||
|
||
/** @private {!Array<shaka.extern.MPEG_PES>} */
|
||
this.videoPes_ = [];
|
||
|
||
/** @private {?number} */
|
||
this.audioPid_ = null;
|
||
|
||
/** @private {?string} */
|
||
this.audioCodec_ = null;
|
||
|
||
/** @private {!Array<!Uint8Array>} Flat list of all audio TS packets. */
|
||
this.audioData_ = [];
|
||
|
||
/**
|
||
* Index into audioData_ where each audio PES unit starts.
|
||
* @private {!Array<number>}
|
||
*/
|
||
this.audioDataPesIndices_ = [];
|
||
|
||
/** @private {!Array<shaka.extern.MPEG_PES>} */
|
||
this.audioPes_ = [];
|
||
|
||
/** @private {?number} */
|
||
this.id3Pid_ = null;
|
||
|
||
/** @private {!Array<!Uint8Array>} Flat list of all ID3 TS packets. */
|
||
this.id3Data_ = [];
|
||
|
||
/**
|
||
* Index into id3Data_ where each ID3 PES unit starts.
|
||
* @private {!Array<number>}
|
||
*/
|
||
this.id3DataPesIndices_ = [];
|
||
|
||
/** @private {?number} */
|
||
this.referencePts_ = null;
|
||
|
||
/** @private {?number} */
|
||
this.referenceDts_ = null;
|
||
|
||
/** @private {?shaka.util.TsParser.OpusMetadata} */
|
||
this.opusMetadata_ = null;
|
||
|
||
/** @private {?number} */
|
||
this.discontinuitySequence_ = null;
|
||
}
|
||
|
||
/**
|
||
* Clear previous data
|
||
*
|
||
* @export
|
||
*/
|
||
clearData() {
|
||
this.videoData_ = [];
|
||
this.videoDataPesIndices_ = [];
|
||
this.videoPes_ = [];
|
||
this.audioData_ = [];
|
||
this.audioDataPesIndices_ = [];
|
||
this.audioPes_ = [];
|
||
this.id3Data_ = [];
|
||
this.id3DataPesIndices_ = [];
|
||
}
|
||
|
||
/**
|
||
* Set the current discontinuity sequence number.
|
||
*
|
||
* @param {number} discontinuitySequence
|
||
* @export
|
||
*/
|
||
setDiscontinuitySequence(discontinuitySequence) {
|
||
if (this.discontinuitySequence_ != null &&
|
||
this.discontinuitySequence_ != discontinuitySequence) {
|
||
this.referencePts_ = null;
|
||
this.referenceDts_ = null;
|
||
}
|
||
this.discontinuitySequence_ = discontinuitySequence;
|
||
}
|
||
|
||
/**
|
||
* Parse the given data
|
||
*
|
||
* @param {Uint8Array} data
|
||
* @return {!shaka.util.TsParser}
|
||
* @export
|
||
*/
|
||
parse(data) {
|
||
const packetLength = shaka.util.TsParser.PacketLength_;
|
||
|
||
// A TS fragment should contain at least 3 TS packets, a PAT, a PMT, and
|
||
// one PID.
|
||
if (data.length < 3 * packetLength) {
|
||
return this;
|
||
}
|
||
const syncOffset = Math.max(0, shaka.util.TsParser.syncOffset(data));
|
||
|
||
const length = data.length - (data.length + syncOffset) % packetLength;
|
||
|
||
let unknownPIDs = false;
|
||
|
||
// loop through TS packets
|
||
for (let start = syncOffset; start < length; start += packetLength) {
|
||
if (data[start] == 0x47) {
|
||
const payloadUnitStartIndicator = !!(data[start + 1] & 0x40);
|
||
// pid is a 13-bit field starting at the last 5 bits of TS[1]
|
||
const pid = ((data[start + 1] & 0x1f) << 8) + data[start + 2];
|
||
const adaptationFieldControl = (data[start + 3] & 0x30) >> 4;
|
||
|
||
// if an adaption field is present, its length is specified by the
|
||
// fifth byte of the TS packet header.
|
||
let offset;
|
||
if (adaptationFieldControl > 1) {
|
||
offset = start + 5 + data[start + 4];
|
||
// continue if there is only adaptation field
|
||
if (offset == start + packetLength) {
|
||
continue;
|
||
}
|
||
} else {
|
||
offset = start + 4;
|
||
}
|
||
switch (pid) {
|
||
case 0:
|
||
if (payloadUnitStartIndicator) {
|
||
offset += data[offset] + 1;
|
||
}
|
||
|
||
this.pmtId_ = this.getPmtId_(data, offset);
|
||
break;
|
||
case 17:
|
||
case 0x1fff:
|
||
break;
|
||
case this.pmtId_: {
|
||
if (payloadUnitStartIndicator) {
|
||
offset += data[offset] + 1;
|
||
}
|
||
|
||
const parsedPIDs = this.parsePMT_(data, offset);
|
||
|
||
// only update track id if track PID found while parsing PMT
|
||
// this is to avoid resetting the PID to -1 in case
|
||
// track PID transiently disappears from the stream
|
||
// this could happen in case of transient missing audio samples
|
||
// for example
|
||
// NOTE this is only the PID of the track as found in TS,
|
||
// but we are not using this for MP4 track IDs.
|
||
if (parsedPIDs.video != -1) {
|
||
this.videoPid_ = parsedPIDs.video;
|
||
this.videoCodec_ = parsedPIDs.videoCodec;
|
||
}
|
||
if (parsedPIDs.audio != -1) {
|
||
this.audioPid_ = parsedPIDs.audio;
|
||
this.audioCodec_ = parsedPIDs.audioCodec;
|
||
}
|
||
if (parsedPIDs.id3 != -1) {
|
||
this.id3Pid_ = parsedPIDs.id3;
|
||
}
|
||
|
||
if (unknownPIDs && !this.pmtParsed_) {
|
||
shaka.log.debug('reparse from beginning');
|
||
unknownPIDs = false;
|
||
// we set it to -188, the += 188 in the for loop will reset
|
||
// start to 0
|
||
start = syncOffset - packetLength;
|
||
}
|
||
this.pmtParsed_ = true;
|
||
break;
|
||
}
|
||
case this.videoPid_: {
|
||
const videoData = data.subarray(offset, start + packetLength);
|
||
if (payloadUnitStartIndicator) {
|
||
// Record where this new PES unit starts in the flat packet array.
|
||
this.videoDataPesIndices_.push(this.videoData_.length);
|
||
this.videoData_.push(videoData);
|
||
} else if (this.videoDataPesIndices_.length) {
|
||
this.videoData_.push(videoData);
|
||
}
|
||
break;
|
||
}
|
||
case this.audioPid_: {
|
||
const audioData = data.subarray(offset, start + packetLength);
|
||
if (payloadUnitStartIndicator) {
|
||
// Record where this new PES unit starts in the flat packet array.
|
||
this.audioDataPesIndices_.push(this.audioData_.length);
|
||
this.audioData_.push(audioData);
|
||
} else if (this.audioDataPesIndices_.length) {
|
||
this.audioData_.push(audioData);
|
||
}
|
||
break;
|
||
}
|
||
case this.id3Pid_: {
|
||
const id3Data = data.subarray(offset, start + packetLength);
|
||
if (payloadUnitStartIndicator) {
|
||
// Record where this new PES unit starts in the flat packet array.
|
||
this.id3DataPesIndices_.push(this.id3Data_.length);
|
||
this.id3Data_.push(id3Data);
|
||
} else if (this.id3DataPesIndices_.length) {
|
||
this.id3Data_.push(id3Data);
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
unknownPIDs = true;
|
||
break;
|
||
}
|
||
} else {
|
||
shaka.log.warning('Found TS packet that do not start with 0x47');
|
||
}
|
||
}
|
||
return this;
|
||
}
|
||
|
||
/**
|
||
* Get the PMT ID from the PAT
|
||
*
|
||
* @param {Uint8Array} data
|
||
* @param {number} offset
|
||
* @return {number}
|
||
* @private
|
||
*/
|
||
getPmtId_(data, offset) {
|
||
// skip the PSI header and parse the first PMT entry
|
||
return ((data[offset + 10] & 0x1f) << 8) | data[offset + 11];
|
||
}
|
||
|
||
/**
|
||
* Parse PMT
|
||
*
|
||
* @param {Uint8Array} data
|
||
* @param {number} offset
|
||
* @return {!shaka.util.TsParser.PMT}
|
||
* @private
|
||
*/
|
||
parsePMT_(data, offset) {
|
||
const result = {
|
||
audio: -1,
|
||
video: -1,
|
||
id3: -1,
|
||
audioCodec: '',
|
||
videoCodec: '',
|
||
};
|
||
const sectionLength = ((data[offset + 1] & 0x0f) << 8) | data[offset + 2];
|
||
const tableEnd = offset + 3 + sectionLength - 4;
|
||
// to determine where the table is, we have to figure out how
|
||
// long the program info descriptors are
|
||
const programInfoLength =
|
||
((data[offset + 10] & 0x0f) << 8) | data[offset + 11];
|
||
// advance the offset to the first entry in the mapping table
|
||
offset += 12 + programInfoLength;
|
||
while (offset < tableEnd) {
|
||
const pid = ((data[offset + 1] & 0x1f) << 8) | data[offset + 2];
|
||
const esInfoLength = ((data[offset + 3] & 0x0f) << 8) | data[offset + 4];
|
||
switch (data[offset]) {
|
||
case 0x06:
|
||
// stream_type 6 can mean a lot of different things in case of DVB.
|
||
// We need to look at the descriptors. Right now, we're only
|
||
// interested in a few audio and video formats,.
|
||
if (esInfoLength > 0) {
|
||
let parsePos = offset + 5;
|
||
let remaining = esInfoLength;
|
||
// Descriptor info: https://www.streamguru.de/mpeg-analyzer/supported-descriptor-list/
|
||
while (remaining > 2) {
|
||
const descriptorId = data[parsePos];
|
||
const descriptorLen = data[parsePos + 1] + 2;
|
||
switch (descriptorId) {
|
||
// Registration descriptor
|
||
case 0x05: {
|
||
const base = parsePos + 2;
|
||
const registration = (data[base] << 24) |
|
||
(data[base+1] << 16) |
|
||
(data[base+2] << 8) |
|
||
data[base+3];
|
||
|
||
// "Opus" in ASCII
|
||
const OPUS_FOURCC = 0x4F707573;
|
||
|
||
// "AV01" in ASCII
|
||
const AV01_FOURCC = 0x41563031;
|
||
|
||
if (result.audio == -1 &&
|
||
registration === OPUS_FOURCC) {
|
||
result.audio = pid;
|
||
result.audioCodec = 'opus';
|
||
} else if (result.video == -1 &&
|
||
registration === AV01_FOURCC) {
|
||
result.video = pid;
|
||
result.videoCodec = 'av1';
|
||
}
|
||
break;
|
||
}
|
||
// DVB Descriptor for AC-3
|
||
case 0x6a:
|
||
if (result.audio == -1) {
|
||
result.audio = pid;
|
||
result.audioCodec = 'ac3';
|
||
}
|
||
break;
|
||
// DVB Descriptor for EC-3
|
||
case 0x7a:
|
||
if (result.audio == -1) {
|
||
result.audio = pid;
|
||
result.audioCodec = 'ec3';
|
||
}
|
||
break;
|
||
// DVB Descriptor for AAC
|
||
case 0x7c:
|
||
if (result.audio == -1) {
|
||
result.audio = pid;
|
||
result.audioCodec = 'aac';
|
||
}
|
||
break;
|
||
// DVB extension descriptor
|
||
case 0x7f:
|
||
if (result.audioCodec == 'opus') {
|
||
const extensionDescriptorId = data[parsePos + 2];
|
||
let channelConfigCode = null;
|
||
// User defined (provisional Opus)
|
||
if (extensionDescriptorId === 0x80) {
|
||
channelConfigCode = data[parsePos + 3];
|
||
}
|
||
|
||
if (channelConfigCode == null) {
|
||
// Not Supported Opus channel count.
|
||
break;
|
||
}
|
||
const channelCount = (channelConfigCode & 0x0F) === 0 ?
|
||
2 : (channelConfigCode & 0x0F);
|
||
this.opusMetadata_ = {
|
||
channelCount,
|
||
channelConfigCode,
|
||
sampleRate: 48000,
|
||
};
|
||
}
|
||
break;
|
||
}
|
||
parsePos += descriptorLen;
|
||
remaining -= descriptorLen;
|
||
}
|
||
}
|
||
break;
|
||
// SAMPLE-AES AAC
|
||
case 0xcf:
|
||
break;
|
||
// ISO/IEC 13818-7 ADTS AAC (MPEG-2 lower bit-rate audio)
|
||
case 0x0f:
|
||
if (result.audio == -1) {
|
||
result.audio = pid;
|
||
result.audioCodec = 'aac';
|
||
}
|
||
break;
|
||
// LOAS/LATM AAC
|
||
case 0x11:
|
||
if (result.audio == -1) {
|
||
result.audio = pid;
|
||
result.audioCodec = 'aac-loas';
|
||
}
|
||
break;
|
||
// Packetized metadata (ID3)
|
||
case 0x15:
|
||
if (result.id3 == -1) {
|
||
result.id3 = pid;
|
||
}
|
||
break;
|
||
// SAMPLE-AES AVC
|
||
case 0xdb:
|
||
break;
|
||
// ITU-T Rec. H.264 and ISO/IEC 14496-10 (lower bit-rate video)
|
||
case 0x1b:
|
||
if (result.video == -1) {
|
||
result.video = pid;
|
||
result.videoCodec = 'avc';
|
||
}
|
||
break;
|
||
// ISO/IEC 11172-3 (MPEG-1 audio)
|
||
// or ISO/IEC 13818-3 (MPEG-2 halved sample rate audio)
|
||
case 0x03:
|
||
case 0x04:
|
||
if (result.audio == -1) {
|
||
result.audio = pid;
|
||
result.audioCodec = 'mp3';
|
||
}
|
||
break;
|
||
// HEVC
|
||
case 0x24:
|
||
if (result.video == -1) {
|
||
result.video = pid;
|
||
result.videoCodec = 'hvc';
|
||
}
|
||
break;
|
||
// AC-3
|
||
case 0x81:
|
||
if (result.audio == -1) {
|
||
result.audio = pid;
|
||
result.audioCodec = 'ac3';
|
||
}
|
||
break;
|
||
// EC-3
|
||
case 0x84:
|
||
case 0x87:
|
||
if (result.audio == -1) {
|
||
result.audio = pid;
|
||
result.audioCodec = 'ec3';
|
||
}
|
||
break;
|
||
default:
|
||
// shaka.log.warning('Unknown stream type:', data[offset]);
|
||
break;
|
||
}
|
||
// move to the next table entry
|
||
// skip past the elementary stream descriptors, if present
|
||
offset += esInfoLength + 5;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Concatenate all TS packets that belong to a single PES unit.
|
||
*
|
||
* @param {!Array<!Uint8Array>} packets Flat list of all TS packets for a PID.
|
||
* @param {!Array<number>} pesIndices Start index in `packets` for each PES.
|
||
* @param {number} pesIdx Index into `pesIndices` for the desired PES.
|
||
* @return {!Uint8Array}
|
||
* @private
|
||
*/
|
||
concatPesPackets_(packets, pesIndices, pesIdx) {
|
||
const first = pesIndices[pesIdx]; // inclusive
|
||
const end = pesIdx + 1 < pesIndices.length ? // exclusive
|
||
pesIndices[pesIdx + 1] : packets.length;
|
||
return shaka.util.Uint8ArrayUtils.concatRange(packets, first, end);
|
||
}
|
||
|
||
/**
|
||
* Parse PES
|
||
*
|
||
* @param {Uint8Array} data
|
||
* @return {?shaka.extern.MPEG_PES}
|
||
* @private
|
||
*/
|
||
parsePES_(data) {
|
||
const startPrefix = (data[0] << 16) | (data[1] << 8) | data[2];
|
||
// In certain live streams, the start of a TS fragment has ts packets
|
||
// that are frame data that is continuing from the previous fragment. This
|
||
// is to check that the pes data is the start of a new pes data
|
||
if (startPrefix !== 1) {
|
||
return null;
|
||
}
|
||
|
||
// get the packet length, this will be 0 for video
|
||
const packetLength = (data[4] << 8) | data[5];
|
||
|
||
// if PES parsed length is not zero and greater than total received length,
|
||
// stop parsing. PES might be truncated. minus 6 : PES header size
|
||
if (packetLength && packetLength > data.byteLength - 6) {
|
||
return null;
|
||
}
|
||
|
||
let pesPts = null;
|
||
let pesDts = null;
|
||
|
||
// PES packets may be annotated with a PTS value, or a PTS value
|
||
// and a DTS value. Determine what combination of values is
|
||
// available to work with.
|
||
const ptsDtsFlags = data[7];
|
||
|
||
// PTS and DTS are normally stored as a 33-bit number. Javascript
|
||
// performs all bitwise operations on 32-bit integers but javascript
|
||
// supports a much greater range (52-bits) of integer using standard
|
||
// mathematical operations.
|
||
// We construct a 31-bit value using bitwise operators over the 31
|
||
// most significant bits and then multiply by 4 (equal to a left-shift
|
||
// of 2) before we add the final 2 least significant bits of the
|
||
// timestamp (equal to an OR.)
|
||
if (ptsDtsFlags & 0xC0) {
|
||
// the PTS and DTS are not written out directly. For information
|
||
// on how they are encoded, see
|
||
// http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
|
||
const pts =
|
||
(data[9] & 0x0e) * 536870912 + // 1 << 29
|
||
(data[10] & 0xff) * 4194304 + // 1 << 22
|
||
(data[11] & 0xfe) * 16384 + // 1 << 14
|
||
(data[12] & 0xff) * 128 + // 1 << 7
|
||
(data[13] & 0xfe) / 2;
|
||
|
||
if (this.referencePts_ == null) {
|
||
this.referencePts_ = pts;
|
||
}
|
||
|
||
pesPts = this.handleRollover_(pts, this.referencePts_);
|
||
this.referencePts_ = pesPts;
|
||
|
||
pesDts = pesPts;
|
||
if (ptsDtsFlags & 0x40) {
|
||
const dts =
|
||
(data[14] & 0x0e) * 536870912 + // 1 << 29
|
||
(data[15] & 0xff) * 4194304 + // 1 << 22
|
||
(data[16] & 0xfe) * 16384 + // 1 << 14
|
||
(data[17] & 0xff) * 128 + // 1 << 7
|
||
(data[18] & 0xfe) / 2;
|
||
|
||
if (this.referenceDts_ == null) {
|
||
this.referenceDts_ = dts;
|
||
}
|
||
|
||
if (pesPts != pts) {
|
||
pesDts = this.handleRollover_(dts, this.referenceDts_);
|
||
} else {
|
||
pesDts = dts;
|
||
}
|
||
}
|
||
this.referenceDts_ = pesDts;
|
||
}
|
||
|
||
const pesHdrLen = data[8];
|
||
// 9 bytes : 6 bytes for PES header + 3 bytes for PES extension
|
||
const payloadStartOffset = pesHdrLen + 9;
|
||
if (data.byteLength <= payloadStartOffset) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
data: data.subarray(payloadStartOffset),
|
||
packetLength: packetLength,
|
||
pts: pesPts,
|
||
dts: pesDts,
|
||
nalus: [],
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Parse AVC and HVC Nalus
|
||
*
|
||
* The code is based on hls.js
|
||
* Credit to https://github.com/video-dev/hls.js/blob/master/src/demux/tsdemuxer.ts
|
||
*
|
||
* @param {shaka.extern.MPEG_PES} pes
|
||
* @param {{nalu: ?shaka.extern.VideoNalu, state: ?number}} lastInfo
|
||
* @return {!Array<shaka.extern.VideoNalu>}
|
||
* @export
|
||
*/
|
||
parseNalus(pes, lastInfo) {
|
||
const timescale = shaka.util.TsParser.Timescale;
|
||
const time = pes.pts ? pes.pts / timescale : null;
|
||
const data = pes.data;
|
||
const len = data.byteLength;
|
||
|
||
let naluHeaderSize = 1;
|
||
if (this.videoCodec_ == 'hvc') {
|
||
naluHeaderSize = 2;
|
||
}
|
||
|
||
// A NALU does not contain is its size.
|
||
// The Annex B specification solves this by requiring ‘Start Codes’ to
|
||
// precede each NALU. A start code is 2 or 3 0x00 bytes followed with a
|
||
// 0x01 byte. e.g. 0x000001 or 0x00000001.
|
||
// More info in: https://stackoverflow.com/questions/24884827/possible-locations-for-sequence-picture-parameter-sets-for-h-264-stream/24890903#24890903
|
||
let numZeros = lastInfo.state || 0;
|
||
|
||
const initialNumZeros = numZeros;
|
||
|
||
/** @type {number} */
|
||
let i = 0;
|
||
|
||
/** @type {!Array<shaka.extern.VideoNalu>} */
|
||
const nalus = [];
|
||
|
||
// Start position includes the first byte where we read the type.
|
||
// The data we extract begins at the next byte.
|
||
let lastNaluStart = -1;
|
||
// Extracted from the first byte.
|
||
let lastNaluType = 0;
|
||
|
||
const isHvc = this.videoCodec_ === 'hvc';
|
||
|
||
const getNaluType = (offset) => {
|
||
return isHvc ? ((data[offset] >> 1) & 0x3f) : (data[offset] & 0x1f);
|
||
};
|
||
|
||
const getLastNalu = () => {
|
||
if (nalus.length) {
|
||
return nalus[nalus.length - 1];
|
||
}
|
||
return lastInfo.nalu;
|
||
};
|
||
|
||
if (numZeros == -1) {
|
||
// special use case where we found 3 or 4-byte start codes exactly at the
|
||
// end of previous PES packet
|
||
lastNaluStart = 0;
|
||
// NALu type is value read from offset 0
|
||
lastNaluType = getNaluType(0);
|
||
numZeros = 0;
|
||
i = 1;
|
||
}
|
||
|
||
while (i < len) {
|
||
const value = data[i++];
|
||
// Optimization. numZeros 0 and 1 are the predominant case.
|
||
if (!numZeros) {
|
||
numZeros = value ? 0 : 1;
|
||
continue;
|
||
}
|
||
if (numZeros === 1) {
|
||
numZeros = value ? 0 : 2;
|
||
continue;
|
||
}
|
||
if (!value) {
|
||
numZeros = 3;
|
||
} else if (value == 1) {
|
||
const overflow = i - numZeros - 1;
|
||
if (lastNaluStart >= 0) {
|
||
/** @type {shaka.extern.VideoNalu} */
|
||
const nalu = {
|
||
data: data.subarray(lastNaluStart + naluHeaderSize, overflow),
|
||
fullData: data.subarray(lastNaluStart, overflow),
|
||
type: lastNaluType,
|
||
time: time,
|
||
state: null,
|
||
};
|
||
nalus.push(nalu);
|
||
} else {
|
||
const lastNalu = getLastNalu();
|
||
if (lastNalu) {
|
||
if (initialNumZeros && i <= 4 - initialNumZeros) {
|
||
// Start delimiter overlapping between PES packets
|
||
// strip start delimiter bytes from the end of last NAL unit
|
||
// check if lastNalu had a state different from zero
|
||
if (lastNalu.state) {
|
||
// strip last bytes
|
||
lastNalu.data = lastNalu.data.subarray(
|
||
0, lastNalu.data.byteLength - initialNumZeros);
|
||
lastNalu.fullData = lastNalu.fullData.subarray(
|
||
0, lastNalu.fullData.byteLength - initialNumZeros);
|
||
}
|
||
}
|
||
// If NAL units are not starting right at the beginning of the PES
|
||
// packet, push preceding data into previous NAL unit.
|
||
if (overflow > 0) {
|
||
const prevData = data.subarray(0, overflow);
|
||
lastNalu.data = shaka.util.Uint8ArrayUtils.concat(
|
||
lastNalu.data, prevData);
|
||
lastNalu.fullData = shaka.util.Uint8ArrayUtils.concat(
|
||
lastNalu.fullData, prevData);
|
||
lastNalu.state = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Check if we can read unit type
|
||
if (i < len) {
|
||
lastNaluType = getNaluType(i);
|
||
lastNaluStart = i;
|
||
numZeros = 0;
|
||
} else {
|
||
// Not enough byte to read unit type.
|
||
// Let's read it on next PES parsing.
|
||
numZeros = -1;
|
||
}
|
||
} else {
|
||
numZeros = 0;
|
||
}
|
||
}
|
||
|
||
if (lastNaluStart >= 0 && numZeros >= 0) {
|
||
const nalu = {
|
||
data: data.subarray(lastNaluStart + naluHeaderSize, len),
|
||
fullData: data.subarray(lastNaluStart, len),
|
||
type: lastNaluType,
|
||
time: time,
|
||
state: numZeros,
|
||
};
|
||
nalus.push(nalu);
|
||
}
|
||
if (!nalus.length && lastInfo.nalu) {
|
||
const lastNalu = getLastNalu();
|
||
if (lastNalu) {
|
||
lastNalu.data = shaka.util.Uint8ArrayUtils.concat(
|
||
lastNalu.data, data);
|
||
lastNalu.fullData = shaka.util.Uint8ArrayUtils.concat(
|
||
lastNalu.fullData, data);
|
||
}
|
||
}
|
||
lastInfo.state = numZeros;
|
||
return nalus;
|
||
}
|
||
|
||
/**
|
||
* Return the ID3 metadata
|
||
*
|
||
* @return {!Array<shaka.extern.ID3Metadata>}
|
||
* @export
|
||
*/
|
||
getMetadata() {
|
||
const timescale = shaka.util.TsParser.Timescale;
|
||
const metadata = [];
|
||
for (let pesIdx = 0; pesIdx < this.id3DataPesIndices_.length; pesIdx++) {
|
||
const id3Data = this.concatPesPackets_(
|
||
this.id3Data_, this.id3DataPesIndices_, pesIdx);
|
||
const pes = this.parsePES_(id3Data);
|
||
if (pes) {
|
||
metadata.push({
|
||
cueTime: pes.pts ? pes.pts / timescale : null,
|
||
data: pes.data,
|
||
frames: shaka.metadata.Id3Utils.getID3Frames(pes.data),
|
||
dts: pes.dts,
|
||
pts: pes.pts,
|
||
});
|
||
}
|
||
}
|
||
return metadata;
|
||
}
|
||
|
||
/**
|
||
* Return the audio data
|
||
*
|
||
* @return {!Array<shaka.extern.MPEG_PES>}
|
||
* @export
|
||
*/
|
||
getAudioData() {
|
||
if (this.audioDataPesIndices_.length && !this.audioPes_.length) {
|
||
for (let pesIdx = 0;
|
||
pesIdx < this.audioDataPesIndices_.length;
|
||
pesIdx++) {
|
||
const audioData = this.concatPesPackets_(
|
||
this.audioData_, this.audioDataPesIndices_, pesIdx);
|
||
const pes = this.parsePES_(audioData);
|
||
let previousPes = this.audioPes_.length ?
|
||
this.audioPes_[this.audioPes_.length - 1] : null;
|
||
if (pes && pes.pts != null && pes.dts != null && (!previousPes ||
|
||
(previousPes.pts != pes.pts && previousPes.dts != pes.dts))) {
|
||
this.audioPes_.push(pes);
|
||
} else if (this.audioPes_.length) {
|
||
const data = pes ? pes.data : audioData;
|
||
if (!data) {
|
||
continue;
|
||
}
|
||
previousPes = this.audioPes_[this.audioPes_.length - 1];
|
||
previousPes.data =
|
||
shaka.util.Uint8ArrayUtils.concat(previousPes.data, data);
|
||
}
|
||
}
|
||
}
|
||
return this.audioPes_;
|
||
}
|
||
|
||
/**
|
||
* Return the video data
|
||
*
|
||
* @param {boolean=} naluProcessing
|
||
* @return {!Array<shaka.extern.MPEG_PES>}
|
||
* @export
|
||
*/
|
||
getVideoData(naluProcessing = true) {
|
||
if (this.videoDataPesIndices_.length && !this.videoPes_.length) {
|
||
for (let pesIdx = 0;
|
||
pesIdx < this.videoDataPesIndices_.length;
|
||
pesIdx++) {
|
||
const videoData = this.concatPesPackets_(
|
||
this.videoData_, this.videoDataPesIndices_, pesIdx);
|
||
const pes = this.parsePES_(videoData);
|
||
let previousPes = this.videoPes_.length ?
|
||
this.videoPes_[this.videoPes_.length - 1] : null;
|
||
if (pes && pes.pts != null && pes.dts != null && (!previousPes ||
|
||
(previousPes.pts != pes.pts && previousPes.dts != pes.dts))) {
|
||
this.videoPes_.push(pes);
|
||
} else if (this.videoPes_.length) {
|
||
const data = pes ? pes.data : videoData;
|
||
if (!data) {
|
||
continue;
|
||
}
|
||
previousPes = this.videoPes_[this.videoPes_.length - 1];
|
||
previousPes.data =
|
||
shaka.util.Uint8ArrayUtils.concat(previousPes.data, data);
|
||
}
|
||
}
|
||
if (naluProcessing) {
|
||
const lastInfo = {
|
||
nalu: null,
|
||
state: null,
|
||
};
|
||
const pesWithLength = [];
|
||
for (const pes of this.videoPes_) {
|
||
pes.nalus = this.parseNalus(pes, lastInfo);
|
||
if (pes.nalus.length) {
|
||
pesWithLength.push(pes);
|
||
lastInfo.nalu = pes.nalus[pes.nalus.length - 1];
|
||
}
|
||
}
|
||
this.videoPes_ = pesWithLength;
|
||
}
|
||
}
|
||
if (!naluProcessing) {
|
||
const prevVideoPes = this.videoPes_;
|
||
this.videoPes_ = [];
|
||
return prevVideoPes;
|
||
}
|
||
return this.videoPes_;
|
||
}
|
||
|
||
/**
|
||
* Return the start time for the audio and video
|
||
*
|
||
* @param {string} contentType
|
||
* @return {?number}
|
||
* @export
|
||
*/
|
||
getStartTime(contentType) {
|
||
const timescale = shaka.util.TsParser.Timescale;
|
||
if (contentType == 'audio') {
|
||
let audioStartTime = null;
|
||
const audioData = this.getAudioData();
|
||
if (audioData.length) {
|
||
const pes = audioData[0];
|
||
audioStartTime = Math.min(pes.dts, pes.pts) / timescale;
|
||
}
|
||
return audioStartTime;
|
||
} else if (contentType == 'video') {
|
||
let videoStartTime = null;
|
||
const videoData = this.getVideoData(/* naluProcessing= */ false);
|
||
if (videoData.length) {
|
||
const pes = videoData[0];
|
||
videoStartTime = Math.min(pes.dts, pes.pts) / timescale;
|
||
}
|
||
return videoStartTime;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Return the audio and video codecs
|
||
*
|
||
* @return {{audio: ?string, video: ?string}}
|
||
* @export
|
||
*/
|
||
getCodecs() {
|
||
return {
|
||
audio: this.audioCodec_,
|
||
video: this.videoCodec_,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Return the video data
|
||
*
|
||
* @return {!Array<shaka.extern.VideoNalu>}
|
||
* @export
|
||
*/
|
||
getVideoNalus() {
|
||
const nalus = [];
|
||
for (const pes of this.getVideoData()) {
|
||
nalus.push(...pes.nalus);
|
||
}
|
||
return nalus;
|
||
}
|
||
|
||
/**
|
||
* Return the video information
|
||
*
|
||
* @return {{
|
||
* height: ?string,
|
||
* width: ?string,
|
||
* codec: ?string,
|
||
* frameRate: ?number,
|
||
* }}
|
||
* @export
|
||
*/
|
||
getVideoInfo() {
|
||
if (this.videoCodec_ == 'hvc') {
|
||
return this.getHvcInfo_();
|
||
}
|
||
return this.getAvcInfo_();
|
||
}
|
||
|
||
/**
|
||
* @return {?number}
|
||
* @private
|
||
*/
|
||
getFrameRate_() {
|
||
const timescale = shaka.util.TsParser.Timescale;
|
||
const videoData = this.getVideoData();
|
||
if (videoData.length > 1) {
|
||
const firstPts = videoData[0].pts;
|
||
goog.asserts.assert(typeof(firstPts) == 'number',
|
||
'Should be an number!');
|
||
const secondPts = videoData[1].pts;
|
||
goog.asserts.assert(typeof(secondPts) == 'number',
|
||
'Should be an number!');
|
||
if (!isNaN(secondPts - firstPts)) {
|
||
return Math.abs(1 / (secondPts - firstPts) * timescale);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Return the video information for AVC
|
||
*
|
||
* @return {{
|
||
* height: ?string,
|
||
* width: ?string,
|
||
* codec: ?string,
|
||
* frameRate: ?number,
|
||
* }}
|
||
* @private
|
||
*/
|
||
getAvcInfo_() {
|
||
const TsParser = shaka.util.TsParser;
|
||
const videoInfo = {
|
||
height: null,
|
||
width: null,
|
||
codec: null,
|
||
frameRate: null,
|
||
};
|
||
const videoNalus = this.getVideoNalus();
|
||
if (!videoNalus.length) {
|
||
return videoInfo;
|
||
}
|
||
const spsNalu = videoNalus.find((nalu) => {
|
||
return nalu.type == TsParser.H264_NALU_TYPE_SPS_;
|
||
});
|
||
if (!spsNalu) {
|
||
return videoInfo;
|
||
}
|
||
|
||
const expGolombDecoder = new shaka.util.ExpGolomb(spsNalu.data);
|
||
// profile_idc
|
||
const profileIdc = expGolombDecoder.readUnsignedByte();
|
||
// constraint_set[0-5]_flag
|
||
const profileCompatibility = expGolombDecoder.readUnsignedByte();
|
||
// level_idc u(8)
|
||
const levelIdc = expGolombDecoder.readUnsignedByte();
|
||
// seq_parameter_set_id
|
||
expGolombDecoder.skipExpGolomb();
|
||
|
||
// some profiles have more optional data we don't need
|
||
if (TsParser.PROFILES_WITH_OPTIONAL_SPS_DATA_.includes(profileIdc)) {
|
||
const chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb();
|
||
if (chromaFormatIdc === 3) {
|
||
// separate_colour_plane_flag
|
||
expGolombDecoder.skipBits(1);
|
||
}
|
||
// bit_depth_luma_minus8
|
||
expGolombDecoder.skipExpGolomb();
|
||
// bit_depth_chroma_minus8
|
||
expGolombDecoder.skipExpGolomb();
|
||
// qpprime_y_zero_transform_bypass_flag
|
||
expGolombDecoder.skipBits(1);
|
||
// seq_scaling_matrix_present_flag
|
||
if (expGolombDecoder.readBoolean()) {
|
||
const scalingListCount = (chromaFormatIdc !== 3) ? 8 : 12;
|
||
for (let i = 0; i < scalingListCount; i++) {
|
||
// seq_scaling_list_present_flag[ i ]
|
||
if (expGolombDecoder.readBoolean()) {
|
||
if (i < 6) {
|
||
expGolombDecoder.skipScalingList(16);
|
||
} else {
|
||
expGolombDecoder.skipScalingList(64);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// log2_max_frame_num_minus4
|
||
expGolombDecoder.skipExpGolomb();
|
||
const picOrderCntType = expGolombDecoder.readUnsignedExpGolomb();
|
||
|
||
if (picOrderCntType === 0) {
|
||
// log2_max_pic_order_cnt_lsb_minus4
|
||
expGolombDecoder.readUnsignedExpGolomb();
|
||
} else if (picOrderCntType === 1) {
|
||
// delta_pic_order_always_zero_flag
|
||
expGolombDecoder.skipBits(1);
|
||
// offset_for_non_ref_pic
|
||
expGolombDecoder.skipExpGolomb();
|
||
// offset_for_top_to_bottom_field
|
||
expGolombDecoder.skipExpGolomb();
|
||
const numRefFramesInPicOrderCntCycle =
|
||
expGolombDecoder.readUnsignedExpGolomb();
|
||
for (let i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
|
||
// offset_for_ref_frame[ i ]
|
||
expGolombDecoder.skipExpGolomb();
|
||
}
|
||
}
|
||
|
||
// max_num_ref_frames
|
||
expGolombDecoder.skipExpGolomb();
|
||
// gaps_in_frame_num_value_allowed_flag
|
||
expGolombDecoder.skipBits(1);
|
||
|
||
const picWidthInMbsMinus1 =
|
||
expGolombDecoder.readUnsignedExpGolomb();
|
||
const picHeightInMapUnitsMinus1 =
|
||
expGolombDecoder.readUnsignedExpGolomb();
|
||
|
||
const frameMbsOnlyFlag = expGolombDecoder.readBits(1);
|
||
if (frameMbsOnlyFlag === 0) {
|
||
// mb_adaptive_frame_field_flag
|
||
expGolombDecoder.skipBits(1);
|
||
}
|
||
// direct_8x8_inference_flag
|
||
expGolombDecoder.skipBits(1);
|
||
|
||
let frameCropLeftOffset = 0;
|
||
let frameCropRightOffset = 0;
|
||
let frameCropTopOffset = 0;
|
||
let frameCropBottomOffset = 0;
|
||
|
||
// frame_cropping_flag
|
||
if (expGolombDecoder.readBoolean()) {
|
||
frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb();
|
||
frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb();
|
||
frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb();
|
||
frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb();
|
||
}
|
||
|
||
videoInfo.height = String(((2 - frameMbsOnlyFlag) *
|
||
(picHeightInMapUnitsMinus1 + 1) * 16) - (frameCropTopOffset * 2) -
|
||
(frameCropBottomOffset * 2));
|
||
videoInfo.width = String(((picWidthInMbsMinus1 + 1) * 16) -
|
||
frameCropLeftOffset * 2 - frameCropRightOffset * 2);
|
||
videoInfo.codec = 'avc1.' + this.byteToHex_(profileIdc) +
|
||
this.byteToHex_(profileCompatibility) + this.byteToHex_(levelIdc);
|
||
videoInfo.frameRate = this.getFrameRate_();
|
||
|
||
return videoInfo;
|
||
}
|
||
|
||
/**
|
||
* Return the video information for HVC
|
||
*
|
||
* @return {{
|
||
* height: ?string,
|
||
* width: ?string,
|
||
* codec: ?string,
|
||
* frameRate: ?number,
|
||
* }}
|
||
* @private
|
||
*/
|
||
getHvcInfo_() {
|
||
const TsParser = shaka.util.TsParser;
|
||
const videoInfo = {
|
||
height: null,
|
||
width: null,
|
||
codec: null,
|
||
frameRate: null,
|
||
};
|
||
const videoNalus = this.getVideoNalus();
|
||
if (!videoNalus.length) {
|
||
return videoInfo;
|
||
}
|
||
const spsNalu = videoNalus.find((nalu) => {
|
||
return nalu.type == TsParser.H265_NALU_TYPE_SPS_;
|
||
});
|
||
if (!spsNalu) {
|
||
return videoInfo;
|
||
}
|
||
|
||
const gb = new shaka.util.ExpGolomb(
|
||
spsNalu.fullData, /* convertEbsp2rbsp= */ true);
|
||
|
||
// remove NALu Header
|
||
gb.readUnsignedByte();
|
||
gb.readUnsignedByte();
|
||
|
||
// SPS
|
||
gb.readBits(4); // video_parameter_set_id
|
||
const maxSubLayersMinus1 = gb.readBits(3);
|
||
gb.readBoolean(); // temporal_id_nesting_flag
|
||
|
||
// profile_tier_level begin
|
||
const generalProfileSpace = gb.readBits(2);
|
||
const generalTierFlag = gb.readBits(1);
|
||
const generalProfileIdc = gb.readBits(5);
|
||
const generalProfileCompatibilityFlags = gb.readBits(32);
|
||
const generalConstraintIndicatorFlags1 = gb.readUnsignedByte();
|
||
const generalConstraintIndicatorFlags2 = gb.readUnsignedByte();
|
||
const generalConstraintIndicatorFlags3 = gb.readUnsignedByte();
|
||
const generalConstraintIndicatorFlags4 = gb.readUnsignedByte();
|
||
const generalConstraintIndicatorFlags5 = gb.readUnsignedByte();
|
||
const generalConstraintIndicatorFlags6 = gb.readUnsignedByte();
|
||
const generalLevelIdc = gb.readUnsignedByte();
|
||
const subLayerProfilePresentFlag = [];
|
||
const subLayerLevelPresentFlag = [];
|
||
for (let i = 0; i < maxSubLayersMinus1; i++) {
|
||
subLayerProfilePresentFlag.push(gb.readBoolean());
|
||
subLayerLevelPresentFlag.push(gb.readBoolean());
|
||
}
|
||
if (maxSubLayersMinus1 > 0) {
|
||
for (let i = maxSubLayersMinus1; i < 8; i++) {
|
||
gb.readBits(2);
|
||
}
|
||
}
|
||
for (let i = 0; i < maxSubLayersMinus1; i++) {
|
||
if (subLayerProfilePresentFlag[i]) {
|
||
gb.readBits(88);
|
||
}
|
||
if (subLayerLevelPresentFlag[i]) {
|
||
gb.readUnsignedByte();
|
||
}
|
||
}
|
||
// profile_tier_level end
|
||
|
||
gb.readUnsignedExpGolomb(); // seq_parameter_set_id
|
||
const chromaFormatIdc = gb.readUnsignedExpGolomb();
|
||
if (chromaFormatIdc == 3) {
|
||
gb.readBits(1); // separate_colour_plane_flag
|
||
}
|
||
const picWidthInLumaSamples = gb.readUnsignedExpGolomb();
|
||
const picHeightInLumaSamples = gb.readUnsignedExpGolomb();
|
||
let leftOffset = 0;
|
||
let rightOffset = 0;
|
||
let topOffset = 0;
|
||
let bottomOffset = 0;
|
||
const conformanceWindowFlag = gb.readBoolean();
|
||
if (conformanceWindowFlag) {
|
||
leftOffset += gb.readUnsignedExpGolomb();
|
||
rightOffset += gb.readUnsignedExpGolomb();
|
||
topOffset += gb.readUnsignedExpGolomb();
|
||
bottomOffset += gb.readUnsignedExpGolomb();
|
||
}
|
||
|
||
const subWc = chromaFormatIdc === 1 || chromaFormatIdc === 2 ? 2 : 1;
|
||
const subHc = chromaFormatIdc === 1 ? 2 : 1;
|
||
videoInfo.width =
|
||
String(picWidthInLumaSamples - (leftOffset + rightOffset) * subWc);
|
||
videoInfo.height =
|
||
String(picHeightInLumaSamples - (topOffset + bottomOffset) * subHc);
|
||
|
||
const reverseBits = (integer) => {
|
||
let result = 0;
|
||
for (let i = 0; i < 32; i++) {
|
||
result |= ((integer >> i) & 1) << (31 - i);
|
||
}
|
||
return result >>> 0;
|
||
};
|
||
|
||
const profileSpace = ['', 'A', 'B', 'C'][generalProfileSpace];
|
||
const profileCompatibility = reverseBits(generalProfileCompatibilityFlags);
|
||
const tierFlag = generalTierFlag == 1 ? 'H' : 'L';
|
||
|
||
let codec = 'hvc1';
|
||
codec += '.' + profileSpace + generalProfileIdc;
|
||
codec += '.' + profileCompatibility.toString(16).toUpperCase();
|
||
codec += '.' + tierFlag + generalLevelIdc;
|
||
if (generalConstraintIndicatorFlags6) {
|
||
codec += '.' +
|
||
generalConstraintIndicatorFlags6.toString(16).toUpperCase();
|
||
}
|
||
if (generalConstraintIndicatorFlags5) {
|
||
codec += '.' +
|
||
generalConstraintIndicatorFlags5.toString(16).toUpperCase();
|
||
}
|
||
if (generalConstraintIndicatorFlags4) {
|
||
codec += '.' +
|
||
generalConstraintIndicatorFlags4.toString(16).toUpperCase();
|
||
}
|
||
if (generalConstraintIndicatorFlags3) {
|
||
codec += '.' +
|
||
generalConstraintIndicatorFlags3.toString(16).toUpperCase();
|
||
}
|
||
if (generalConstraintIndicatorFlags2) {
|
||
codec += '.' +
|
||
generalConstraintIndicatorFlags2.toString(16).toUpperCase();
|
||
}
|
||
if (generalConstraintIndicatorFlags1) {
|
||
codec += '.' +
|
||
generalConstraintIndicatorFlags1.toString(16).toUpperCase();
|
||
}
|
||
videoInfo.codec = codec;
|
||
videoInfo.frameRate = this.getFrameRate_();
|
||
|
||
return videoInfo;
|
||
}
|
||
|
||
/**
|
||
* Return the Opus metadata
|
||
*
|
||
* @return {?shaka.util.TsParser.OpusMetadata}
|
||
*/
|
||
getOpusMetadata() {
|
||
return this.opusMetadata_;
|
||
}
|
||
|
||
/**
|
||
* Convert a byte to 2 digits of hex. (Only handles values 0-255.)
|
||
*
|
||
* @param {number} x
|
||
* @return {string}
|
||
* @private
|
||
*/
|
||
byteToHex_(x) {
|
||
return ('0' + x.toString(16).toUpperCase()).slice(-2);
|
||
}
|
||
|
||
/**
|
||
* @param {number} value
|
||
* @param {number} reference
|
||
* @return {number}
|
||
* @private
|
||
*/
|
||
handleRollover_(value, reference) {
|
||
const MAX_TS = 8589934592;
|
||
const RO_THRESH = 4294967296;
|
||
|
||
let direction = 1;
|
||
|
||
if (value > reference) {
|
||
// If the current timestamp value is greater than our reference timestamp
|
||
// and we detect a timestamp rollover, this means the roll over is
|
||
// happening in the opposite direction.
|
||
// Example scenario: Enter a long stream/video just after a rollover
|
||
// occurred. The reference point will be set to a small number, e.g. 1.
|
||
// The user then seeks backwards over the rollover point. In loading this
|
||
// segment, the timestamp values will be very large, e.g. 2^33 - 1. Since
|
||
// this comes before the data we loaded previously, we want to adjust the
|
||
// time stamp to be `value - 2^33`.
|
||
direction = -1;
|
||
}
|
||
|
||
// Note: A seek forwards or back that is greater than the RO_THRESH
|
||
// (2^32, ~13 hours) will cause an incorrect adjustment.
|
||
while (Math.abs(reference - value) > RO_THRESH) {
|
||
value += (direction * MAX_TS);
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
/**
|
||
* Check if the passed data corresponds to an MPEG2-TS
|
||
*
|
||
* @param {Uint8Array} data
|
||
* @return {boolean}
|
||
* @export
|
||
*/
|
||
static probe(data) {
|
||
const syncOffset = shaka.util.TsParser.syncOffset(data);
|
||
if (syncOffset < 0) {
|
||
return false;
|
||
} else {
|
||
if (syncOffset > 0) {
|
||
shaka.log.warning('MPEG2-TS detected but first sync word found @ ' +
|
||
'offset ' + syncOffset + ', junk ahead ?');
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns the synchronization offset
|
||
*
|
||
* @param {Uint8Array} data
|
||
* @return {number}
|
||
* @export
|
||
*/
|
||
static syncOffset(data) {
|
||
const packetLength = shaka.util.TsParser.PacketLength_;
|
||
// scan 1000 first bytes
|
||
const scanWindow = Math.min(1000, data.length - 3 * packetLength);
|
||
let i = 0;
|
||
while (i < scanWindow) {
|
||
// a TS fragment should contain at least 3 TS packets, a PAT, a PMT, and
|
||
// one PID, each starting with 0x47
|
||
if (data[i] == 0x47 &&
|
||
data[i + packetLength] == 0x47 &&
|
||
data[i + 2 * packetLength] == 0x47) {
|
||
return i;
|
||
} else {
|
||
const next = data.indexOf(0x47, i + 1);
|
||
i = (next >= 0 && next < scanWindow) ? next : i + 1;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* @const {number}
|
||
* @export
|
||
*/
|
||
shaka.util.TsParser.Timescale = 90000;
|
||
|
||
|
||
/**
|
||
* @const {number}
|
||
* @private
|
||
*/
|
||
shaka.util.TsParser.PacketLength_ = 188;
|
||
|
||
|
||
/**
|
||
* NALU type for Sequence Parameter Set (SPS) for H.264.
|
||
* @const {number}
|
||
* @private
|
||
*/
|
||
shaka.util.TsParser.H264_NALU_TYPE_SPS_ = 0x07;
|
||
|
||
|
||
/**
|
||
* NALU type for Sequence Parameter Set (SPS) for H.265.
|
||
* @const {number}
|
||
* @private
|
||
*/
|
||
shaka.util.TsParser.H265_NALU_TYPE_SPS_ = 0x21;
|
||
|
||
|
||
/**
|
||
* Values of profile_idc that indicate additional fields are included in the
|
||
* SPS.
|
||
* see Recommendation ITU-T H.264 (4/2013)
|
||
* 7.3.2.1.1 Sequence parameter set data syntax
|
||
*
|
||
* @const {!Array<number>}
|
||
* @private
|
||
*/
|
||
shaka.util.TsParser.PROFILES_WITH_OPTIONAL_SPS_DATA_ =
|
||
[100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134];
|
||
|
||
|
||
/**
|
||
* @typedef {{
|
||
* audio: number,
|
||
* video: number,
|
||
* id3: number,
|
||
* audioCodec: string,
|
||
* videoCodec: string,
|
||
* }}
|
||
*
|
||
* @summary PMT.
|
||
* @property {number} audio
|
||
* Audio PID
|
||
* @property {number} video
|
||
* Video PID
|
||
* @property {number} id3
|
||
* ID3 PID
|
||
* @property {string} audioCodec
|
||
* Audio codec
|
||
* @property {string} videoCodec
|
||
* Video codec
|
||
*/
|
||
shaka.util.TsParser.PMT;
|
||
|
||
|
||
/**
|
||
* @typedef {{
|
||
* channelCount: number,
|
||
* channelConfigCode: number,
|
||
* sampleRate: number,
|
||
* }}
|
||
*
|
||
* @summary PMT.
|
||
* @property {number} channelCount
|
||
* @property {number} channelConfigCode
|
||
* @property {number} sampleRate
|
||
*/
|
||
shaka.util.TsParser.OpusMetadata;
|
||
|