/*! @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.Deprecate');
goog.require('shaka.log');
goog.require('shaka.util.ExpGolomb');
goog.require('shaka.util.Id3Utils');
goog.require('shaka.util.StringUtils');
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.<!Array.<Uint8Array>>} */
this.videoData_ = [];
/** @private {!Array.<shaka.extern.MPEG_PES>} */
this.videoPes_ = [];
/** @private {?number} */
this.audioPid_ = null;
/** @private {?string} */
this.audioCodec_ = null;
/** @private {!Array.<!Array.<Uint8Array>>} */
this.audioData_ = [];
/** @private {!Array.<shaka.extern.MPEG_PES>} */
this.audioPes_ = [];
/** @private {?number} */
this.id3Pid_ = null;
/** @private {!Array.<!Array.<Uint8Array>>} */
this.id3Data_ = [];
/** @private {?number} */
this.referencePts_ = null;
/** @private {?number} */
this.referenceDts_ = null;
/** @private {?shaka.util.TsParser.OpusMetadata} */
this.opusMetadata_ = null;
}
/**
* Clear previous data
*
* @export
*/
clearData() {
this.videoData_ = [];
this.videoPes_ = [];
this.audioData_ = [];
this.audioPes_ = [];
this.id3Data_ = [];
}
/**
* 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) {
this.videoData_.push([videoData]);
} else if (this.videoData_.length) {
const prevVideoData = this.videoData_[this.videoData_.length - 1];
if (prevVideoData) {
this.videoData_[this.videoData_.length - 1].push(videoData);
}
}
break;
}
case this.audioPid_: {
const audioData = data.subarray(offset, start + packetLength);
if (payloadUnitStartIndicator) {
this.audioData_.push([audioData]);
} else if (this.audioData_.length) {
const prevAudioData = this.audioData_[this.audioData_.length - 1];
if (prevAudioData) {
this.audioData_[this.audioData_.length - 1].push(audioData);
}
}
break;
}
case this.id3Pid_: {
const id3Data = data.subarray(offset, start + packetLength);
if (payloadUnitStartIndicator) {
this.id3Data_.push([id3Data]);
} else if (this.id3Data_.length) {
const prevId3Data = this.id3Data_[this.id3Data_.length - 1];
if (prevId3Data) {
this.id3Data_[this.id3Data_.length - 1].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 StringUtils = shaka.util.StringUtils;
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 registrationData =
data.subarray(parsePos + 2, parsePos + descriptorLen);
const registration =
StringUtils.fromCharCode(registrationData);
if (result.audio == -1 && registration === 'Opus') {
result.audio = pid;
result.audioCodec = 'opus';
} else if (result.video == -1 && registration === 'AV01') {
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;
}
/**
* 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;
}
/** @type {shaka.extern.MPEG_PES} */
const pes = {
data: new Uint8Array(0),
// get the packet length, this will be 0 for video
packetLength: ((data[4] << 8) | data[5]),
pts: null,
dts: null,
nalus: [],
};
// 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 (pes.packetLength && pes.packetLength > data.byteLength - 6) {
return 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;
}
pes.pts = this.handleRollover_(pts, this.referencePts_);
this.referencePts_ = pes.pts;
pes.dts = pes.pts;
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;
}
pes.dts = this.handleRollover_(dts, this.referenceDts_);
}
this.referenceDts_ = pes.dts;
}
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;
}
pes.data = data.subarray(payloadStartOffset);
return pes;
}
/**
* Parse AVC 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 {?shaka.extern.MPEG_PES=} nextPes
* @return {!Array.<shaka.extern.VideoNalu>}
* @export
*/
parseAvcNalus(pes, nextPes) {
shaka.Deprecate.deprecateFeature(5,
'TsParser.parseAvcNalus',
'Please use parseNalus function instead.');
return this.parseNalus(pes);
}
/**
* 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 {?shaka.extern.VideoNalu=} lastNalu
* @param {?number=} lastState
* @return {!Array.<shaka.extern.VideoNalu>}
* @export
*/
parseNalus(pes, lastNalu, lastState) {
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 = lastState || 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;
/** @type {?shaka.extern.VideoNalu} */
let infoOfLastNalu;
for (let i = 0; i < len; ++i) {
const value = data[i];
if (!value) {
numZeros++;
} else if (numZeros >= 2 && value == 1) {
if (lastNalu && !nalus.length && lastNaluStart == -1) {
// If we are scanning the next PES, we need append the data to the
// previous Nalu and don't scan for more nalus.
const startCodeSize = numZeros > 3 ? 3 : numZeros;
const lastByteToKeep = i - startCodeSize;
// Optimization
if (lastState && lastByteToKeep != 0) {
const prevData = data.subarray(0, lastByteToKeep);
lastNalu.data = shaka.util.Uint8ArrayUtils.concat(
lastNalu.data, prevData);
lastNalu.fullData = shaka.util.Uint8ArrayUtils.concat(
lastNalu.fullData, prevData);
}
}
// We just read a start code. Consume the NALU we passed, if any.
if (lastNaluStart >= 0) {
// Because the start position includes the header size.
const firstByteToKeep = lastNaluStart + naluHeaderSize;
// Compute the last byte to keep. The start code is at most 3 zeros.
// Any earlier zeros are not part of the start code.
const startCodeSize = (numZeros > 3 ? 3 : numZeros) + 1;
const lastByteToKeep = i - startCodeSize;
/** @type {shaka.extern.VideoNalu} */
const nalu = {
// subarray's end position is exclusive, so add one.
data: data.subarray(firstByteToKeep, lastByteToKeep + 1),
fullData: data.subarray(lastNaluStart, lastByteToKeep + 1),
type: lastNaluType,
time: time,
};
nalus.push(nalu);
} else if (lastNalu && !nalus.length) {
const overflow = i - numZeros;
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);
}
}
// We just read a start code, so there should be another byte here, at
// least, for the NALU type. Check just in case.
if (i >= len - naluHeaderSize) {
shaka.log.warning('Malformed TS, incomplete NALU, ignoring.');
return nalus;
}
// Advance and read the type of the next NALU.
i++;
lastNaluStart = i;
if (this.videoCodec_ == 'hvc') {
lastNaluType = (data[i] >> 1) & 0x3f;
} else {
lastNaluType = data[i] & 0x1f;
}
numZeros = 0;
} else {
numZeros = 0;
}
// If we have gone through all the data from the PES and we have an
// unfinished Nalu, we will try to use the next PES to complete the
// unfinished Nalu.
if (i >= (len - 1) && lastNaluStart >= 0 && numZeros >= 0) {
// The rest of the buffer was a NALU.
// Because the start position includes the header size.
const firstByteToKeep = lastNaluStart + naluHeaderSize;
infoOfLastNalu = {
data: data.subarray(firstByteToKeep, len),
fullData: data.subarray(lastNaluStart, len),
type: lastNaluType,
time: time,
};
}
}
if (infoOfLastNalu) {
nalus.push(infoOfLastNalu);
}
if (!nalus.length && lastNalu) {
lastNalu.data = shaka.util.Uint8ArrayUtils.concat(
lastNalu.data, data);
lastNalu.fullData = shaka.util.Uint8ArrayUtils.concat(
lastNalu.fullData, data);
}
lastState = Math.min(3, numZeros);
return nalus;
}
/**
* Return the ID3 metadata
*
* @return {!Array.<shaka.extern.ID3Metadata>}
* @export
*/
getMetadata() {
const timescale = shaka.util.TsParser.Timescale;
const metadata = [];
for (const id3DataArray of this.id3Data_) {
const id3Data = shaka.util.Uint8ArrayUtils.concat(...id3DataArray);
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.audioData_.length && !this.audioPes_.length) {
let sort = false;
for (const audioDataArray of this.audioData_) {
const audioData = shaka.util.Uint8ArrayUtils.concat(...audioDataArray);
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))) {
if (this.audioPes_.length && pes.dts < (previousPes.dts || 0)) {
sort = true;
}
this.audioPes_.push(pes);
} else if (this.audioPes_.length) {
const data = pes ? pes.data : audioData;
if (!data) {
continue;
}
previousPes = this.audioPes_.pop();
previousPes.data =
shaka.util.Uint8ArrayUtils.concat(previousPes.data, data);
this.audioPes_.push(previousPes);
}
}
if (sort) {
this.audioPes_ = this.audioPes_.sort((a, b) => {
const deltadts = (a.dts || 0) - (b.dts || 0);
const deltapts = (a.pts || 0) - (b.pts || 0);
return deltadts || deltapts;
});
}
}
return this.audioPes_;
}
/**
* Return the video data
*
* @param {boolean=} naluProcessing
* @return {!Array.<shaka.extern.MPEG_PES>}
* @export
*/
getVideoData(naluProcessing = true) {
if (this.videoData_.length && !this.videoPes_.length) {
let sort = false;
for (const videoDataArray of this.videoData_) {
const videoData = shaka.util.Uint8ArrayUtils.concat(...videoDataArray);
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))) {
if (this.videoPes_.length && pes.dts < (previousPes.dts || 0)) {
sort = true;
}
this.videoPes_.push(pes);
} else if (this.videoPes_.length) {
const data = pes ? pes.data : videoData;
if (!data) {
continue;
}
previousPes = this.videoPes_.pop();
previousPes.data =
shaka.util.Uint8ArrayUtils.concat(previousPes.data, data);
this.videoPes_.push(previousPes);
}
}
if (naluProcessing) {
let lastNalu;
let lastState;
const pesWithLength = [];
for (const pes of this.videoPes_) {
pes.nalus = this.parseNalus(pes, lastNalu, lastState);
if (pes.nalus.length) {
pesWithLength.push(pes);
lastNalu = pes.nalus[pes.nalus.length - 1];
}
}
this.videoPes_ = pesWithLength;
}
if (sort) {
this.videoPes_ = this.videoPes_.sort((a, b) => {
const deltadts = (a.dts || 0) - (b.dts || 0);
const deltapts = (a.pts || 0) - (b.pts || 0);
return deltadts || deltapts;
});
}
}
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 resolution
*
* @return {{height: ?string, width: ?string}}
* @export
*/
getVideoResolution() {
shaka.Deprecate.deprecateFeature(5,
'TsParser.getVideoResolution',
'Please use getVideoInfo function instead.');
const videoInfo = this.getVideoInfo();
return {
height: videoInfo.height,
width: videoInfo.width,
};
}
/**
* Return the video information
*
* @return {{height: ?string, width: ?string, codec: ?string,
* frameRate: ?string}}
* @export
*/
getVideoInfo() {
if (this.videoCodec_ == 'hvc') {
return this.getHvcInfo_();
}
return this.getAvcInfo_();
}
/**
* @return {?string}
* @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 String(1 / (secondPts - firstPts) * timescale);
}
}
return null;
}
/**
* Return the video information for AVC
*
* @return {{height: ?string, width: ?string, codec: ?string,
* frameRate: ?string}}
* @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: ?string}}
* @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_paramter_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 {
i++;
}
}
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;