mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
de30b7003a
In `H264.getVideoSamples()` `lastVideoSample.data` is initialized with an empty `Uint8Array` which is always replaced before the sample is pushed into the `videoSamples` array, so to avoid creating a new empty placeholder `Uint8Array` for each sample, the empty `Uint8Array` can be created once and then use to initialize all `lastVideoSample` objects in that segment. `TSParser.parsePES_` had a similar placeholder empty `Uint8Array` issue, which I was able to resolve by creating the PES object in the return statement instead of upfront, that way it can be initialized with the final data, avoiding the placeholder. While yes empty Uint8Arrays definitely need less memory than larger ones, it is still better to not create 100+ unnecessary objects in rapid succession which then need to be cleaned up by the garbage collector later on.
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.util.ExpGolomb');
|
||
goog.require('shaka.util.Id3Utils');
|
||
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.util.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;
|
||
|