mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-15 16:06:41 +03:00
970d7756ea
The goal is to simplify and abstract feature logic detection. Currently lots of places depend on various calls to `shaka.util.Platform` and mainteinance of this is hard & not easy to read. By introducing device API ideally rest of the player logic would look into device features instead of directly checking platform. Additionally we can more easily cache needed values, so we won't have to parse user agent several times anymore. --------- Co-authored-by: Álvaro Velad Galván <ladvan91@hotmail.com>
1468 lines
41 KiB
JavaScript
1468 lines
41 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
goog.provide('shaka.util.Mp4Generator');
|
|
|
|
goog.require('goog.asserts');
|
|
goog.require('shaka.device.DeviceFactory');
|
|
goog.require('shaka.util.ManifestParserUtils');
|
|
goog.require('shaka.util.Uint8ArrayUtils');
|
|
|
|
|
|
shaka.util.Mp4Generator = class {
|
|
/**
|
|
* @param {!Array<shaka.util.Mp4Generator.StreamInfo>} streamInfos
|
|
*/
|
|
constructor(streamInfos) {
|
|
shaka.util.Mp4Generator.initStaticProperties_();
|
|
|
|
/** @private {!Array<shaka.util.Mp4Generator.StreamInfo>} */
|
|
this.streamInfos_ = streamInfos;
|
|
}
|
|
|
|
/**
|
|
* Generate a Init Segment (MP4).
|
|
*
|
|
* @return {!Uint8Array}
|
|
*/
|
|
initSegment() {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const movie = this.moov_();
|
|
const length = Mp4Generator.FTYP_.byteLength + movie.byteLength;
|
|
const result = new Uint8Array(length);
|
|
result.set(Mp4Generator.FTYP_);
|
|
result.set(movie, Mp4Generator.FTYP_.byteLength);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Generate a MOOV box
|
|
*
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
moov_() {
|
|
goog.asserts.assert(this.streamInfos_.length > 0,
|
|
'StreamInfos must have elements');
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const trakArrays = [];
|
|
for (const streamInfo of this.streamInfos_) {
|
|
trakArrays.push(this.trak_(streamInfo));
|
|
}
|
|
const traks = shaka.util.Uint8ArrayUtils.concat(...trakArrays);
|
|
const firstStreamInfo = this.streamInfos_[0];
|
|
return Mp4Generator.box('moov',
|
|
this.mvhd_(firstStreamInfo),
|
|
traks,
|
|
this.mvex_(),
|
|
this.pssh_(firstStreamInfo));
|
|
}
|
|
|
|
/**
|
|
* Generate a MVHD box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
mvhd_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const duration = streamInfo.duration * streamInfo.timescale;
|
|
const upperWordDuration =
|
|
Math.floor(duration / (Mp4Generator.UINT32_MAX_ + 1));
|
|
const lowerWordDuration =
|
|
Math.floor(duration % (Mp4Generator.UINT32_MAX_ + 1));
|
|
const bytes = new Uint8Array([
|
|
0x01, // version 1
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x02, // creation_time
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x03, // modification_time
|
|
...this.breakNumberIntoBytes_(streamInfo.timescale, 4), // timescale
|
|
...this.breakNumberIntoBytes_(upperWordDuration, 4),
|
|
...this.breakNumberIntoBytes_(lowerWordDuration, 4), // duration
|
|
0x00, 0x01, 0x00, 0x00, // 1.0 rate
|
|
0x01, 0x00, // 1.0 volume
|
|
0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x01, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x01, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x40, 0x00, 0x00, 0x00, // transformation: unity matrix
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined
|
|
0xff, 0xff, 0xff, 0xff, // next_track_ID
|
|
]);
|
|
return Mp4Generator.box('mvhd', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a TRAK box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
trak_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
return Mp4Generator.box('trak',
|
|
this.tkhd_(streamInfo), this.mdia_(streamInfo));
|
|
}
|
|
|
|
/**
|
|
* Generate a TKHD box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
tkhd_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
const id = streamInfo.id + 1;
|
|
let width = streamInfo.stream.width || 0;
|
|
let height = streamInfo.stream.height || 0;
|
|
if (streamInfo.type == ContentType.AUDIO) {
|
|
width = 0;
|
|
height = 0;
|
|
}
|
|
const duration = streamInfo.duration * streamInfo.timescale;
|
|
const upperWordDuration =
|
|
Math.floor(duration / (Mp4Generator.UINT32_MAX_ + 1));
|
|
const lowerWordDuration =
|
|
Math.floor(duration % (Mp4Generator.UINT32_MAX_ + 1));
|
|
const bytes = new Uint8Array([
|
|
0x01, // version 1
|
|
0x00, 0x00, 0x07, // flags
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x02, // creation_time
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x03, // modification_time
|
|
...this.breakNumberIntoBytes_(id, 4), // track_ID
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
...this.breakNumberIntoBytes_(upperWordDuration, 4),
|
|
...this.breakNumberIntoBytes_(lowerWordDuration, 4), // duration
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x00, // layer
|
|
0x00, 0x00, // alternate_group
|
|
0x00, 0x00, // non-audio track volume
|
|
0x00, 0x00, // reserved
|
|
0x00, 0x01, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x01, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x40, 0x00, 0x00, 0x00, // transformation: unity matrix
|
|
...this.breakNumberIntoBytes_(width, 2),
|
|
0x00, 0x00, // width
|
|
...this.breakNumberIntoBytes_(height, 2),
|
|
0x00, 0x00, // height
|
|
]);
|
|
return Mp4Generator.box('tkhd', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a MDIA box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
mdia_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
return Mp4Generator.box('mdia', this.mdhd_(streamInfo),
|
|
this.hdlr_(streamInfo), this.minf_(streamInfo));
|
|
}
|
|
|
|
/**
|
|
* Generate a MDHD box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
mdhd_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const duration = streamInfo.duration * streamInfo.timescale;
|
|
const upperWordDuration =
|
|
Math.floor(duration / (Mp4Generator.UINT32_MAX_ + 1));
|
|
const lowerWordDuration =
|
|
Math.floor(duration % (Mp4Generator.UINT32_MAX_ + 1));
|
|
const language = streamInfo.stream.language;
|
|
const languageNumber = ((language.charCodeAt(0) - 0x60) << 10) |
|
|
((language.charCodeAt(1) - 0x60) << 5) |
|
|
((language.charCodeAt(2) - 0x60));
|
|
const bytes = new Uint8Array([
|
|
0x01, // version 1
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x02, // creation_time
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x03, // modification_time
|
|
...this.breakNumberIntoBytes_(streamInfo.timescale, 4), // timescale
|
|
...this.breakNumberIntoBytes_(upperWordDuration, 4),
|
|
...this.breakNumberIntoBytes_(lowerWordDuration, 4), // duration
|
|
...this.breakNumberIntoBytes_(languageNumber, 2), // language
|
|
0x00, 0x00,
|
|
]);
|
|
return Mp4Generator.box('mdhd', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a HDLR box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
hdlr_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
let bytes = new Uint8Array([]);
|
|
switch (streamInfo.type) {
|
|
case ContentType.VIDEO:
|
|
bytes = Mp4Generator.HDLR_TYPES_.video;
|
|
break;
|
|
case ContentType.AUDIO:
|
|
bytes = Mp4Generator.HDLR_TYPES_.audio;
|
|
break;
|
|
}
|
|
return Mp4Generator.box('hdlr', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a MINF box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
minf_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
switch (streamInfo.type) {
|
|
case ContentType.VIDEO:
|
|
return Mp4Generator.box(
|
|
'minf', Mp4Generator.box('vmhd', Mp4Generator.VMHD_),
|
|
Mp4Generator.DINF_, this.stbl_(streamInfo));
|
|
case ContentType.AUDIO:
|
|
return Mp4Generator.box(
|
|
'minf', Mp4Generator.box('smhd', Mp4Generator.SMHD_),
|
|
Mp4Generator.DINF_, this.stbl_(streamInfo));
|
|
}
|
|
return new Uint8Array([]);
|
|
}
|
|
|
|
/**
|
|
* Generate a STBL box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
stbl_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
return Mp4Generator.box(
|
|
'stbl',
|
|
this.stsd_(streamInfo),
|
|
Mp4Generator.box('stts', Mp4Generator.STTS_),
|
|
Mp4Generator.box('stsc', Mp4Generator.STSC_),
|
|
Mp4Generator.box('stsz', Mp4Generator.STSZ_),
|
|
Mp4Generator.box('stco', Mp4Generator.STCO_));
|
|
}
|
|
|
|
/**
|
|
* Generate a STSD box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
stsd_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
let audioCodec = 'aac';
|
|
if (streamInfo.codecs.includes('mp3')) {
|
|
audioCodec = 'mp3';
|
|
} else if (streamInfo.codecs.includes('ac-3')) {
|
|
const device = shaka.device.DeviceFactory.getDevice();
|
|
if (device.requiresEC3InitSegments()) {
|
|
audioCodec = 'ec-3';
|
|
} else {
|
|
audioCodec = 'ac-3';
|
|
}
|
|
} else if (streamInfo.codecs.includes('ec-3')) {
|
|
audioCodec = 'ec-3';
|
|
} else if (streamInfo.codecs.includes('opus')) {
|
|
audioCodec = 'opus';
|
|
}
|
|
let bytes = new Uint8Array([]);
|
|
switch (streamInfo.type) {
|
|
case ContentType.VIDEO:
|
|
if (streamInfo.codecs.includes('avc1')) {
|
|
bytes = this.avc1_(streamInfo);
|
|
} else if (streamInfo.codecs.includes('hvc1')) {
|
|
bytes = this.hvc1_(streamInfo);
|
|
}
|
|
break;
|
|
case ContentType.AUDIO:
|
|
if (audioCodec == 'mp3') {
|
|
bytes = this.mp3_(streamInfo);
|
|
} else if (audioCodec == 'ac-3') {
|
|
bytes = this.ac3_(streamInfo);
|
|
} else if (audioCodec == 'ec-3') {
|
|
bytes = this.ec3_(streamInfo);
|
|
} else if (audioCodec == 'opus') {
|
|
bytes = this.opus_(streamInfo);
|
|
} else {
|
|
bytes = this.mp4a_(streamInfo);
|
|
}
|
|
break;
|
|
}
|
|
return Mp4Generator.box('stsd', Mp4Generator.STSD_, bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a AVC1 box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
avc1_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
|
|
const width = streamInfo.stream.width || 0;
|
|
const height = streamInfo.stream.height || 0;
|
|
|
|
let avcCBox;
|
|
if (streamInfo.videoConfig.byteLength > 0) {
|
|
avcCBox = Mp4Generator.box('avcC', streamInfo.videoConfig);
|
|
} else {
|
|
avcCBox = Mp4Generator.box('avcC', this.avcC_(streamInfo));
|
|
}
|
|
|
|
const avc1Bytes = new Uint8Array([
|
|
0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x01, // data_reference_index
|
|
0x00, 0x00, // pre_defined
|
|
0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined
|
|
...this.breakNumberIntoBytes_(width, 2), // width
|
|
...this.breakNumberIntoBytes_(height, 2), // height
|
|
0x00, 0x48, 0x00, 0x00, // horizontal resolution
|
|
0x00, 0x48, 0x00, 0x00, // vertical resolution
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x01, // frame_count
|
|
0x13,
|
|
0x76, 0x69, 0x64, 0x65,
|
|
0x6f, 0x6a, 0x73, 0x2d,
|
|
0x63, 0x6f, 0x6e, 0x74,
|
|
0x72, 0x69, 0x62, 0x2d,
|
|
0x68, 0x6c, 0x73, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, // compressor name
|
|
0x00, 0x18, // depth = 24
|
|
0x11, 0x11, // pre_defined = -1
|
|
]);
|
|
|
|
let boxName = 'avc1';
|
|
const paspBox = this.pasp_(streamInfo);
|
|
let sinfBox = new Uint8Array([]);
|
|
if (streamInfo.encrypted) {
|
|
sinfBox = this.sinf(streamInfo.stream, streamInfo.codecs);
|
|
boxName = 'encv';
|
|
}
|
|
return Mp4Generator.box(boxName, avc1Bytes, avcCBox, paspBox, sinfBox);
|
|
}
|
|
|
|
/**
|
|
* Generate a AVCC box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
avcC_(streamInfo) {
|
|
const NALU_TYPE_SPS = 7;
|
|
const NALU_TYPE_PPS = 8;
|
|
|
|
// length = 7 by default (0 SPS and 0 PPS)
|
|
let avcCLength = 7;
|
|
|
|
// First get all SPS and PPS from nalus
|
|
const sps = [];
|
|
const pps = [];
|
|
let AVCProfileIndication = 0;
|
|
let AVCLevelIndication = 0;
|
|
let profileCompatibility = 0;
|
|
for (let i = 0; i < streamInfo.videoNalus.length; i++) {
|
|
const naluBytes = this.hexStringToBuffer_(streamInfo.videoNalus[i]);
|
|
const naluType = naluBytes[0] & 0x1F;
|
|
switch (naluType) {
|
|
case NALU_TYPE_SPS:
|
|
sps.push(naluBytes);
|
|
// 2 = sequenceParameterSetLength field length
|
|
avcCLength += naluBytes.length + 2;
|
|
break;
|
|
case NALU_TYPE_PPS:
|
|
pps.push(naluBytes);
|
|
// 2 = pictureParameterSetLength field length
|
|
avcCLength += naluBytes.length + 2;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
// Get profile and level from SPS
|
|
if (sps.length > 0) {
|
|
AVCProfileIndication = sps[0][1];
|
|
profileCompatibility = sps[0][2];
|
|
AVCLevelIndication = sps[0][3];
|
|
}
|
|
|
|
// Generate avcC buffer
|
|
const avcCBytes = new Uint8Array(avcCLength);
|
|
let i = 0;
|
|
// configurationVersion = 1
|
|
avcCBytes[i++] = 1;
|
|
avcCBytes[i++] = AVCProfileIndication;
|
|
avcCBytes[i++] = profileCompatibility;
|
|
avcCBytes[i++] = AVCLevelIndication;
|
|
// '11111' + lengthSizeMinusOne = 3
|
|
avcCBytes[i++] = 0xFF;
|
|
// '111' + numOfSequenceParameterSets
|
|
avcCBytes[i++] = 0xE0 | sps.length;
|
|
for (let n = 0; n < sps.length; n++) {
|
|
avcCBytes[i++] = (sps[n].length & 0xFF00) >> 8;
|
|
avcCBytes[i++] = (sps[n].length & 0x00FF);
|
|
avcCBytes.set(sps[n], i);
|
|
i += sps[n].length;
|
|
}
|
|
// numOfPictureParameterSets
|
|
avcCBytes[i++] = pps.length;
|
|
for (let n = 0; n < pps.length; n++) {
|
|
avcCBytes[i++] = (pps[n].length & 0xFF00) >> 8;
|
|
avcCBytes[i++] = (pps[n].length & 0x00FF);
|
|
avcCBytes.set(pps[n], i);
|
|
i += pps[n].length;
|
|
}
|
|
return avcCBytes;
|
|
}
|
|
|
|
/**
|
|
* Generate a HVC1 box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
hvc1_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
|
|
const width = streamInfo.stream.width || 0;
|
|
const height = streamInfo.stream.height || 0;
|
|
|
|
let hvcCBox = new Uint8Array([]);
|
|
if (streamInfo.videoConfig.byteLength > 0) {
|
|
hvcCBox = Mp4Generator.box('hvcC', streamInfo.videoConfig);
|
|
}
|
|
|
|
const hvc1Bytes = new Uint8Array([
|
|
0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x01, // data_reference_index
|
|
0x00, 0x00, // pre_defined
|
|
0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined
|
|
...this.breakNumberIntoBytes_(width, 2), // width
|
|
...this.breakNumberIntoBytes_(height, 2), // height
|
|
0x00, 0x48, 0x00, 0x00, // horizontal resolution
|
|
0x00, 0x48, 0x00, 0x00, // vertical resolution
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x01, // frame_count
|
|
0x13,
|
|
0x76, 0x69, 0x64, 0x65,
|
|
0x6f, 0x6a, 0x73, 0x2d,
|
|
0x63, 0x6f, 0x6e, 0x74,
|
|
0x72, 0x69, 0x62, 0x2d,
|
|
0x68, 0x6c, 0x73, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, // compressor name
|
|
0x00, 0x18, // depth = 24
|
|
0x11, 0x11, // pre_defined = -1
|
|
]);
|
|
|
|
let boxName = 'hvc1';
|
|
const paspBox = this.pasp_(streamInfo);
|
|
let sinfBox = new Uint8Array([]);
|
|
if (streamInfo.encrypted) {
|
|
sinfBox = this.sinf(streamInfo.stream, streamInfo.codecs);
|
|
boxName = 'encv';
|
|
}
|
|
return Mp4Generator.box(boxName, hvc1Bytes, hvcCBox, paspBox, sinfBox);
|
|
}
|
|
|
|
/**
|
|
* Generate a PASP box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
pasp_(streamInfo) {
|
|
if (!streamInfo.hSpacing && !streamInfo.vSpacing) {
|
|
return new Uint8Array([]);
|
|
}
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const hSpacing = streamInfo.hSpacing;
|
|
const vSpacing = streamInfo.vSpacing;
|
|
const bytes = new Uint8Array([
|
|
...this.breakNumberIntoBytes_(hSpacing, 4), // hSpacing
|
|
...this.breakNumberIntoBytes_(vSpacing, 4), // vSpacing
|
|
]);
|
|
return Mp4Generator.box('pasp', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate STSD bytes
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
audioStsd_(streamInfo) {
|
|
const channelsCount = streamInfo.stream.channelsCount || 2;
|
|
const audioSamplingRate = streamInfo.stream.audioSamplingRate || 44100;
|
|
const bytes = new Uint8Array([
|
|
0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x01, // data_reference_index
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00,
|
|
channelsCount, // channel count
|
|
0x00, 0x10, // sampleSize:16bits
|
|
0x00, 0x00, 0x00, 0x00, // reserved2
|
|
...this.breakNumberIntoBytes_(audioSamplingRate, 2), // Sample Rate
|
|
0x00, 0x00,
|
|
]);
|
|
return bytes;
|
|
}
|
|
|
|
/**
|
|
* Generate a .MP3 box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
mp3_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
return Mp4Generator.box('.mp3', this.audioStsd_(streamInfo));
|
|
}
|
|
|
|
/**
|
|
* Generate a AC-3 box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
ac3_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const dac3Box = Mp4Generator.box('dac3', streamInfo.audioConfig);
|
|
|
|
let boxName = 'ac-3';
|
|
let sinfBox = new Uint8Array([]);
|
|
if (streamInfo.encrypted) {
|
|
sinfBox = this.sinf(streamInfo.stream, streamInfo.codecs);
|
|
boxName = 'enca';
|
|
}
|
|
return Mp4Generator.box(boxName,
|
|
this.audioStsd_(streamInfo), dac3Box, sinfBox);
|
|
}
|
|
|
|
/**
|
|
* Generate a EC-3 box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
ec3_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const dec3Box = Mp4Generator.box('dec3', streamInfo.audioConfig);
|
|
|
|
let boxName = 'ec-3';
|
|
let sinfBox = new Uint8Array([]);
|
|
if (streamInfo.encrypted) {
|
|
sinfBox = this.sinf(streamInfo.stream, streamInfo.codecs);
|
|
boxName = 'enca';
|
|
}
|
|
return Mp4Generator.box(boxName,
|
|
this.audioStsd_(streamInfo), dec3Box, sinfBox);
|
|
}
|
|
|
|
/**
|
|
* Generate a Opus box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
opus_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const dopsBox = Mp4Generator.box('dOps', streamInfo.audioConfig);
|
|
|
|
let boxName = 'Opus';
|
|
let sinfBox = new Uint8Array([]);
|
|
if (streamInfo.encrypted) {
|
|
sinfBox = this.sinf(streamInfo.stream, streamInfo.codecs);
|
|
boxName = 'enca';
|
|
}
|
|
return Mp4Generator.box(boxName,
|
|
this.audioStsd_(streamInfo), dopsBox, sinfBox);
|
|
}
|
|
|
|
/**
|
|
* Generate a MP4A box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
mp4a_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
let esdsBox;
|
|
if (streamInfo.audioConfig.byteLength > 0) {
|
|
esdsBox = Mp4Generator.box('esds', streamInfo.audioConfig);
|
|
} else {
|
|
esdsBox = Mp4Generator.box('esds', this.esds_(streamInfo));
|
|
}
|
|
|
|
let boxName = 'mp4a';
|
|
let sinfBox = new Uint8Array([]);
|
|
if (streamInfo.encrypted) {
|
|
sinfBox = this.sinf(streamInfo.stream, streamInfo.codecs);
|
|
boxName = 'enca';
|
|
}
|
|
return Mp4Generator.box(boxName,
|
|
this.audioStsd_(streamInfo), esdsBox, sinfBox);
|
|
}
|
|
|
|
/**
|
|
* Generate a ESDS box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
esds_(streamInfo) {
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
|
|
const id = streamInfo.id + 1;
|
|
const channelsCount = streamInfo.stream.channelsCount || 2;
|
|
const audioSamplingRate = streamInfo.stream.audioSamplingRate || 44100;
|
|
|
|
const audioCodec = shaka.util.ManifestParserUtils.guessCodecs(
|
|
ContentType.AUDIO, streamInfo.codecs.split(','));
|
|
|
|
const samplingFrequencyIndex = {
|
|
96000: 0x0,
|
|
88200: 0x1,
|
|
64000: 0x2,
|
|
48000: 0x3,
|
|
44100: 0x4,
|
|
32000: 0x5,
|
|
24000: 0x6,
|
|
22050: 0x7,
|
|
16000: 0x8,
|
|
12000: 0x9,
|
|
11025: 0xA,
|
|
8000: 0xB,
|
|
7350: 0xC,
|
|
};
|
|
|
|
let indexFreq = samplingFrequencyIndex[audioSamplingRate];
|
|
// In HE AAC Sampling frequence equals to SamplingRate * 2
|
|
if (audioCodec === 'mp4a.40.5' || audioCodec === 'mp4a.40.29') {
|
|
indexFreq = samplingFrequencyIndex[audioSamplingRate * 2];
|
|
}
|
|
|
|
const audioObjectType = parseInt(audioCodec.split('.').pop(), 10);
|
|
|
|
return new Uint8Array([
|
|
0x00, // version
|
|
0x00, 0x00, 0x00, // flags
|
|
|
|
// ES_Descriptor
|
|
0x03, // tag, ES_DescriptionTag
|
|
0x19, // length
|
|
...this.breakNumberIntoBytes_(id, 2), // ES_ID
|
|
0x00, // streamDependenceFlag, URL_flag, reserved, streamPriority
|
|
|
|
// DecoderConfigDescriptor
|
|
0x04, // tag, DecoderConfigDescriptionTag
|
|
0x11, // length
|
|
0x40, // object type
|
|
0x15, // streamType
|
|
0x00, 0x00, 0x00, // bufferSizeDB
|
|
0x00, 0x00, 0x00, 0x00, // maxBitrate
|
|
0x00, 0x00, 0x00, 0x00, // avgBitrate
|
|
|
|
// DecoderSpecificInfo
|
|
0x05, // tag, DecoderSpecificInfoTag
|
|
0x02, // length
|
|
// ISO/IEC 14496-3, AudioSpecificConfig
|
|
// for samplingFrequencyIndex see
|
|
// ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35
|
|
(audioObjectType << 3) | (indexFreq >>> 1),
|
|
(indexFreq << 7) | (channelsCount << 3),
|
|
0x06, 0x01, 0x02, // GASpecificConfig
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Generate a MVEX box
|
|
*
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
mvex_() {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const trexArrays = [];
|
|
for (const streamInfo of this.streamInfos_) {
|
|
trexArrays.push(this.trex_(streamInfo));
|
|
}
|
|
const trexs = shaka.util.Uint8ArrayUtils.concat(...trexArrays);
|
|
return Mp4Generator.box('mvex', trexs);
|
|
}
|
|
|
|
/**
|
|
* Generate a TREX box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
trex_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const id = streamInfo.id + 1;
|
|
const bytes = new Uint8Array([
|
|
0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
...this.breakNumberIntoBytes_(id, 4), // track_ID
|
|
0x00, 0x00, 0x00, 0x01, // default_sample_description_index
|
|
0x00, 0x00, 0x00, 0x00, // default_sample_duration
|
|
0x00, 0x00, 0x00, 0x00, // default_sample_size
|
|
0x00, 0x00, 0x00, 0x00, // default_sample_flags
|
|
]);
|
|
return Mp4Generator.box('trex', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a PSSH box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
pssh_(streamInfo) {
|
|
const initDatas = [];
|
|
if (!streamInfo.encrypted) {
|
|
return new Uint8Array([]);
|
|
}
|
|
|
|
for (const drmInfo of streamInfo.stream.drmInfos) {
|
|
if (!drmInfo.initData) {
|
|
continue;
|
|
}
|
|
for (const initData of drmInfo.initData) {
|
|
initDatas.push(initData.initData);
|
|
}
|
|
}
|
|
const boxes = shaka.util.Uint8ArrayUtils.concat(...initDatas);
|
|
return boxes;
|
|
}
|
|
|
|
/**
|
|
* Generate a SINF box
|
|
*
|
|
* @param {!shaka.extern.Stream} stream
|
|
* @param {string} codecs
|
|
* @return {!Uint8Array}
|
|
*/
|
|
sinf(stream, codecs) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
return Mp4Generator.box('sinf',
|
|
this.frma_(codecs),
|
|
this.schm_(stream),
|
|
this.schi_(stream));
|
|
}
|
|
|
|
/**
|
|
* Generate a FRMA box
|
|
*
|
|
* @param {string} codecs
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
frma_(codecs) {
|
|
const codec = codecs.split('.')[0];
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const codecNumber = this.stringToCharCode_(codec);
|
|
const bytes = new Uint8Array([
|
|
...this.breakNumberIntoBytes_(codecNumber, 4),
|
|
]);
|
|
return Mp4Generator.box('frma', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a SCHM box
|
|
*
|
|
* @param {!shaka.extern.Stream} stream
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
schm_(stream) {
|
|
let encryptionScheme = 'cenc';
|
|
const drmInfo = stream.drmInfos[0];
|
|
if (drmInfo && drmInfo.encryptionScheme) {
|
|
encryptionScheme = drmInfo.encryptionScheme;
|
|
}
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const schemeNumber = this.stringToCharCode_(encryptionScheme);
|
|
const bytes = new Uint8Array([
|
|
0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
...this.breakNumberIntoBytes_(schemeNumber, 4), // Scheme
|
|
0x00, 0x01, 0x00, 0x00, // Scheme version: 1.0
|
|
]);
|
|
return Mp4Generator.box('schm', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a SCHI box
|
|
*
|
|
* @param {!shaka.extern.Stream} stream
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
schi_(stream) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
return Mp4Generator.box('schi', this.tenc_(stream));
|
|
}
|
|
|
|
/**
|
|
* Generate a TENC box
|
|
*
|
|
* @param {!shaka.extern.Stream} stream
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
tenc_(stream) {
|
|
// Default key ID: all zeros (dummy)
|
|
let defaultKeyId = new Uint8Array([
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
]);
|
|
for (const drmInfo of stream.drmInfos) {
|
|
if (drmInfo && drmInfo.keyIds && drmInfo.keyIds.size) {
|
|
for (const keyId of drmInfo.keyIds) {
|
|
defaultKeyId = this.hexStringToBuffer_(keyId);
|
|
}
|
|
}
|
|
}
|
|
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const bytes = new Uint8Array([
|
|
0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, // Reserved fields
|
|
0x01, // Default protected: true
|
|
0x08, // Default per-sample IV size: 8
|
|
]);
|
|
goog.asserts.assert(defaultKeyId, 'Default KID should be non-null');
|
|
return Mp4Generator.box('tenc', bytes, defaultKeyId);
|
|
}
|
|
|
|
/**
|
|
* Generate a Segment Data (MP4).
|
|
*
|
|
* @return {!Uint8Array}
|
|
*/
|
|
segmentData() {
|
|
const segmentDataArray = [];
|
|
for (const streamInfo of this.streamInfos_) {
|
|
segmentDataArray.push(
|
|
...[this.moof_(streamInfo), this.mdat_(streamInfo)]);
|
|
}
|
|
const result = shaka.util.Uint8ArrayUtils.concat(...segmentDataArray);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Generate a MOOF box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
moof_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
return Mp4Generator.box('moof',
|
|
this.mfhd_(streamInfo), this.traf_(streamInfo));
|
|
}
|
|
|
|
/**
|
|
* Generate a MOOF box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
mfhd_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const sequenceNumber =
|
|
streamInfo.data ? streamInfo.data.sequenceNumber : 0;
|
|
const bytes = new Uint8Array([
|
|
0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
...this.breakNumberIntoBytes_(sequenceNumber, 4),
|
|
]);
|
|
return Mp4Generator.box('mfhd', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a TRAF box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
traf_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const sampleDependencyTable = this.sdtp_(streamInfo);
|
|
const offset = sampleDependencyTable.length +
|
|
32 + // tfhd
|
|
20 + // tfdt
|
|
8 + // traf header
|
|
16 + // mfhd
|
|
8 + // moof header
|
|
8; // mdat header;
|
|
return Mp4Generator.box('traf',
|
|
this.tfhd_(streamInfo),
|
|
this.tfdt_(streamInfo),
|
|
this.trun_(streamInfo, offset),
|
|
sampleDependencyTable);
|
|
}
|
|
|
|
/**
|
|
* Generate a SDTP box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
sdtp_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const samples = streamInfo.data ? streamInfo.data.samples : [];
|
|
const bytes = new Uint8Array(4 + samples.length);
|
|
// leave the full box header (4 bytes) all zero
|
|
// write the sample table
|
|
for (let i = 0; i < samples.length; i++) {
|
|
const flags = samples[i].flags;
|
|
bytes[i + 4] = (flags.dependsOn << 4) |
|
|
(flags.isDependedOn << 2) |
|
|
flags.hasRedundancy;
|
|
}
|
|
return Mp4Generator.box('sdtp', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a TFHD box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
tfhd_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const id = streamInfo.id + 1;
|
|
const bytes = new Uint8Array([
|
|
0x00, // version 0
|
|
0x00, 0x00, 0x3a, // flags
|
|
...this.breakNumberIntoBytes_(id, 4), // track_ID
|
|
0x00, 0x00, 0x00, 0x01, // sample_description_index
|
|
0x00, 0x00, 0x00, 0x00, // default_sample_duration
|
|
0x00, 0x00, 0x00, 0x00, // default_sample_size
|
|
0x00, 0x00, 0x00, 0x00, // default_sample_flags
|
|
]);
|
|
return Mp4Generator.box('tfhd', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a TFDT box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
tfdt_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const baseMediaDecodeTime =
|
|
streamInfo.data ? streamInfo.data.baseMediaDecodeTime : 0;
|
|
const upperWordBaseMediaDecodeTime =
|
|
Math.floor(baseMediaDecodeTime / (Mp4Generator.UINT32_MAX_ + 1));
|
|
const lowerWordBaseMediaDecodeTime =
|
|
Math.floor(baseMediaDecodeTime % (Mp4Generator.UINT32_MAX_ + 1));
|
|
const bytes = new Uint8Array([
|
|
0x01, // version 1
|
|
0x00, 0x00, 0x00, // flags
|
|
...this.breakNumberIntoBytes_(upperWordBaseMediaDecodeTime, 4),
|
|
...this.breakNumberIntoBytes_(lowerWordBaseMediaDecodeTime, 4),
|
|
]);
|
|
return Mp4Generator.box('tfdt', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a TRUN box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @param {number} offset
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
trun_(streamInfo, offset) {
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
|
|
const samples = streamInfo.data ? streamInfo.data.samples : [];
|
|
const samplesLength = samples.length;
|
|
const bytesLen = 12 + 16 * samplesLength;
|
|
const bytes = new Uint8Array(bytesLen);
|
|
offset += 8 + bytesLen;
|
|
const isVideo = streamInfo.type === ContentType.VIDEO;
|
|
bytes.set(
|
|
[
|
|
// version 1 for video with signed-int sample_composition_time_offset
|
|
isVideo ? 0x01 : 0x00,
|
|
0x00, 0x0f, 0x01, // flags
|
|
...this.breakNumberIntoBytes_(samplesLength, 4), // sample_count
|
|
...this.breakNumberIntoBytes_(offset, 4), // data_offset
|
|
],
|
|
0,
|
|
);
|
|
for (let i = 0; i < samplesLength; i++) {
|
|
const sample = samples[i];
|
|
const duration = this.breakNumberIntoBytes_(sample.duration, 4);
|
|
const size = this.breakNumberIntoBytes_(sample.size, 4);
|
|
const flags = sample.flags;
|
|
const cts = this.breakNumberIntoBytes_(sample.cts, 4);
|
|
bytes.set(
|
|
[
|
|
...duration, // sample_duration
|
|
...size, // sample_size
|
|
(flags.isLeading << 2) | flags.dependsOn,
|
|
(flags.isDependedOn << 6) | (flags.hasRedundancy << 4) |
|
|
flags.isNonSync,
|
|
flags.degradPrio & (0xf0 << 8),
|
|
flags.degradPrio & 0x0f, // sample_flags
|
|
...cts, // sample_composition_time_offset
|
|
],
|
|
12 + 16 * i,
|
|
);
|
|
}
|
|
return Mp4Generator.box('trun', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a MDAT box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
mdat_(streamInfo) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const samples = streamInfo.data ? streamInfo.data.samples : [];
|
|
const allData = samples.map((sample) => sample.data);
|
|
const bytes = shaka.util.Uint8ArrayUtils.concat(...allData);
|
|
return Mp4Generator.box('mdat', bytes);
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {number} number
|
|
* @param {number} numBytes
|
|
* @return {!Array<number>}
|
|
* @private
|
|
*/
|
|
breakNumberIntoBytes_(number, numBytes) {
|
|
const bytes = [];
|
|
for (let byte = numBytes - 1; byte >= 0; byte--) {
|
|
bytes.push((number >> (8 * byte)) & 0xff);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
/**
|
|
* Convert a hex string to buffer.
|
|
*
|
|
* @param {string} str
|
|
* @return {Uint8Array}
|
|
* @private
|
|
*/
|
|
hexStringToBuffer_(str) {
|
|
const buf = new Uint8Array(str.length / 2);
|
|
for (let i = 0; i < str.length / 2; i += 1) {
|
|
buf[i] = parseInt(String(str[i * 2] + str[i * 2 + 1]), 16);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
/**
|
|
* Convert a string to char code.
|
|
*
|
|
* @param {string} str
|
|
* @return {number}
|
|
* @private
|
|
*/
|
|
stringToCharCode_(str) {
|
|
let code = 0;
|
|
for (let i = 0; i < str.length; i += 1) {
|
|
code |= str.charCodeAt(i) << ((str.length - i - 1) * 8);
|
|
}
|
|
return code;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
static initStaticProperties_() {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
if (Mp4Generator.initialized_) {
|
|
return;
|
|
}
|
|
|
|
Mp4Generator.initialized_ = true;
|
|
|
|
const majorBrand = new Uint8Array([105, 115, 111, 109]); // isom
|
|
const avc1Brand = new Uint8Array([97, 118, 99, 49]); // avc1
|
|
const minorVersion = new Uint8Array([0, 0, 0, 1]);
|
|
|
|
Mp4Generator.FTYP_ = Mp4Generator.box(
|
|
'ftyp', majorBrand, minorVersion, majorBrand, avc1Brand);
|
|
const drefBox = Mp4Generator.box('dref', Mp4Generator.DREF_);
|
|
Mp4Generator.DINF_ = Mp4Generator.box('dinf', drefBox);
|
|
}
|
|
|
|
/**
|
|
* Generate a box
|
|
*
|
|
* @param {string} boxName
|
|
* @param {...!Uint8Array} payload
|
|
* @return {!Uint8Array}
|
|
*/
|
|
static box(boxName, ...payload) {
|
|
let type = shaka.util.Mp4Generator.BOX_TYPES_.get(boxName);
|
|
if (!type) {
|
|
type = [
|
|
boxName.charCodeAt(0),
|
|
boxName.charCodeAt(1),
|
|
boxName.charCodeAt(2),
|
|
boxName.charCodeAt(3),
|
|
];
|
|
shaka.util.Mp4Generator.BOX_TYPES_.set(boxName, type);
|
|
}
|
|
// make the header for the box
|
|
let size = 8;
|
|
// calculate the total size we need to allocate
|
|
for (let i = payload.length - 1; i >= 0; i--) {
|
|
size += payload[i].byteLength;
|
|
}
|
|
const result = new Uint8Array(size);
|
|
result[0] = (size >> 24) & 0xff;
|
|
result[1] = (size >> 16) & 0xff;
|
|
result[2] = (size >> 8) & 0xff;
|
|
result[3] = size & 0xff;
|
|
result.set(type, 4);
|
|
|
|
// copy the payload into the result
|
|
for (let i = 0, pointer = 8; i < payload.length; i++) {
|
|
// copy payload[i] array @ offset pointer
|
|
result.set(payload[i], pointer);
|
|
pointer += payload[i].byteLength;
|
|
}
|
|
return result;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private {boolean}
|
|
*/
|
|
shaka.util.Mp4Generator.initialized_ = false;
|
|
|
|
|
|
/**
|
|
* @private {number}
|
|
*/
|
|
shaka.util.Mp4Generator.UINT32_MAX_ = Math.pow(2, 32) - 1;
|
|
|
|
/**
|
|
* @private {!Map<string, !Array<number>>}
|
|
*/
|
|
shaka.util.Mp4Generator.BOX_TYPES_ = new Map();
|
|
|
|
/**
|
|
* @private {{video: !Uint8Array, audio: !Uint8Array}}
|
|
*/
|
|
shaka.util.Mp4Generator.HDLR_TYPES_ = {
|
|
video: new Uint8Array([
|
|
0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined
|
|
0x76, 0x69, 0x64, 0x65, // handler_type: 'vide'
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x56, 0x69, 0x64, 0x65,
|
|
0x6f, 0x48, 0x61, 0x6e,
|
|
0x64, 0x6c, 0x65, 0x72, 0x00, // name: 'VideoHandler'
|
|
]),
|
|
audio: new Uint8Array([
|
|
0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined
|
|
0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun'
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x53, 0x6f, 0x75, 0x6e,
|
|
0x64, 0x48, 0x61, 0x6e,
|
|
0x64, 0x6c, 0x65, 0x72, 0x00, // name: 'SoundHandler'
|
|
]),
|
|
};
|
|
|
|
/**
|
|
* @private {!Uint8Array}
|
|
*/
|
|
shaka.util.Mp4Generator.STTS_ = new Uint8Array([
|
|
0x00, // version
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x00, // entry_count
|
|
]);
|
|
|
|
/**
|
|
* @private {!Uint8Array}
|
|
*/
|
|
shaka.util.Mp4Generator.STSC_ = new Uint8Array([
|
|
0x00, // version
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x00, // entry_count
|
|
]);
|
|
|
|
/**
|
|
* @private {!Uint8Array}
|
|
*/
|
|
shaka.util.Mp4Generator.STCO_ = new Uint8Array([
|
|
0x00, // version
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x00, // entry_count
|
|
]);
|
|
|
|
/**
|
|
* @private {!Uint8Array}
|
|
*/
|
|
shaka.util.Mp4Generator.STSZ_ = new Uint8Array([
|
|
0x00, // version
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x00, // sample_size
|
|
0x00, 0x00, 0x00, 0x00, // sample_count
|
|
]);
|
|
|
|
/**
|
|
* @private {!Uint8Array}
|
|
*/
|
|
shaka.util.Mp4Generator.VMHD_ = new Uint8Array([
|
|
0x00, // version
|
|
0x00, 0x00, 0x01, // flags
|
|
0x00, 0x00, // graphics mode
|
|
0x00, 0x00,
|
|
0x00, 0x00,
|
|
0x00, 0x00, // op color
|
|
]);
|
|
|
|
/**
|
|
* @private {!Uint8Array}
|
|
*/
|
|
shaka.util.Mp4Generator.SMHD_ = new Uint8Array([
|
|
0x00, // version
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, // balance, 0 means centered
|
|
0x00, 0x00, // reserved
|
|
]);
|
|
|
|
/**
|
|
* @private {!Uint8Array}
|
|
*/
|
|
shaka.util.Mp4Generator.STSD_ = new Uint8Array([
|
|
0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x01, // entry_count
|
|
]);
|
|
|
|
/**
|
|
* @private {!Uint8Array}
|
|
*/
|
|
shaka.util.Mp4Generator.FTYP_ = new Uint8Array([]);
|
|
|
|
/**
|
|
* @private {!Uint8Array}
|
|
*/
|
|
shaka.util.Mp4Generator.DREF_ = new Uint8Array([
|
|
0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x01, // entry_count
|
|
0x00, 0x00, 0x00, 0x0c, // entry_size
|
|
0x75, 0x72, 0x6c, 0x20, // 'url' type
|
|
0x00, // version 0
|
|
0x00, 0x00, 0x01, // entry_flags
|
|
]);
|
|
|
|
/**
|
|
* @private {!Uint8Array}
|
|
*/
|
|
shaka.util.Mp4Generator.DINF_ = new Uint8Array([]);
|
|
|
|
/**
|
|
* @typedef {{
|
|
* id: number,
|
|
* type: string,
|
|
* codecs: string,
|
|
* encrypted: boolean,
|
|
* timescale: number,
|
|
* duration: number,
|
|
* videoNalus: !Array<string>,
|
|
* audioConfig: !Uint8Array,
|
|
* videoConfig: !Uint8Array,
|
|
* hSpacing: number,
|
|
* vSpacing: number,
|
|
* data: ?shaka.util.Mp4Generator.Data,
|
|
* stream: !shaka.extern.Stream
|
|
* }}
|
|
*
|
|
* @property {number} id
|
|
* A unique ID
|
|
* @property {string} type
|
|
* Indicate the content type: 'video' or 'audio'.
|
|
* @property {string} codecs
|
|
* <i>Defaults to '' (i.e., unknown / not needed).</i> <br>
|
|
* The Stream's codecs, e.g., 'avc1.4d4015' or 'vp9'<br>
|
|
* See {@link https://tools.ietf.org/html/rfc6381}
|
|
* @property {boolean} encrypted
|
|
* Indicate if the stream is encrypted.
|
|
* @property {number} timescale
|
|
* The Stream's timescale.
|
|
* @property {number} duration
|
|
* The Stream's duration.
|
|
* @property {!Array<string>} videoNalus
|
|
* The stream's video nalus.
|
|
* @property {!Uint8Array} audioConfig
|
|
* The stream's audio config.
|
|
* @property {!Uint8Array} videoConfig
|
|
* The stream's video config.
|
|
* @property {number} hSpacing
|
|
* The stream's video horizontal spacing of pixels.
|
|
* @property {number} vSpacing
|
|
* The stream's video vertical spacing of pixels.
|
|
* @property {?shaka.util.Mp4Generator.Data} data
|
|
* The stream's data.
|
|
* @property {!shaka.extern.Stream} stream
|
|
* The Stream.
|
|
*/
|
|
shaka.util.Mp4Generator.StreamInfo;
|
|
|
|
/**
|
|
* @typedef {{
|
|
* sequenceNumber: number,
|
|
* baseMediaDecodeTime: number,
|
|
* samples: !Array<shaka.util.Mp4Generator.Mp4Sample>
|
|
* }}
|
|
*
|
|
* @property {number} sequenceNumber
|
|
* The sequence number.
|
|
* @property {number} baseMediaDecodeTime
|
|
* The base media decode time.
|
|
* @property {!Array<shaka.util.Mp4Generator.Mp4Sample>} samples
|
|
* The data samples.
|
|
*/
|
|
shaka.util.Mp4Generator.Data;
|
|
|
|
/**
|
|
* @typedef {{
|
|
* data: !Uint8Array,
|
|
* size: number,
|
|
* duration: number,
|
|
* cts: number,
|
|
* flags: !shaka.util.Mp4Generator.Mp4SampleFlags
|
|
* }}
|
|
*
|
|
* @property {!Uint8Array} data
|
|
* The sample data.
|
|
* @property {number} size
|
|
* The sample size.
|
|
* @property {number} duration
|
|
* The sample duration.
|
|
* @property {number} cts
|
|
* The sample composition time.
|
|
* @property {!shaka.util.Mp4Generator.Mp4SampleFlags} flags
|
|
* The sample flags.
|
|
*/
|
|
shaka.util.Mp4Generator.Mp4Sample;
|
|
|
|
/**
|
|
* @typedef {{
|
|
* isLeading: number,
|
|
* isDependedOn: number,
|
|
* hasRedundancy: number,
|
|
* degradPrio: number,
|
|
* dependsOn: number,
|
|
* isNonSync: number
|
|
* }}
|
|
*
|
|
* @property {number} isLeading
|
|
* @property {number} isDependedOn
|
|
* @property {number} hasRedundancy
|
|
* @property {number} degradPrio
|
|
* @property {number} dependsOn
|
|
* @property {number} isNonSync
|
|
*/
|
|
shaka.util.Mp4Generator.Mp4SampleFlags;
|