mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
5492f31b1a
This PR reduces allocation pressure in MP4 segment generation by writing mdat data directly into the final segment buffer - this avoids one full media-payload copy per segment
1483 lines
43 KiB
JavaScript
1483 lines
43 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.BufferUtils');
|
|
goog.require('shaka.util.Lazy');
|
|
goog.require('shaka.util.ManifestParserUtils');
|
|
goog.require('shaka.util.Uint8ArrayUtils');
|
|
|
|
|
|
shaka.util.Mp4Generator = class {
|
|
/**
|
|
* @param {!Array<shaka.util.Mp4Generator.StreamInfo>=} streamInfos
|
|
*/
|
|
constructor(streamInfos = []) {
|
|
/** @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 ftyp = Mp4Generator.FTYP_.value();
|
|
const length = ftyp.byteLength + movie.byteLength;
|
|
const result = new Uint8Array(length);
|
|
result.set(ftyp);
|
|
result.set(movie, ftyp.byteLength);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Generate a MOOV box
|
|
*
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
moov_() {
|
|
if (this.streamInfos_.length === 0) {
|
|
throw new Error('StreamInfos must have elements');
|
|
}
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const trakArrays = [];
|
|
for (const streamInfo of this.streamInfos_) {
|
|
trakArrays.push(this.trak_(streamInfo));
|
|
}
|
|
const firstStreamInfo = this.streamInfos_[0];
|
|
return Mp4Generator.box('moov',
|
|
this.mvhd_(firstStreamInfo),
|
|
...trakArrays,
|
|
this.mvex_(),
|
|
this.psshs(firstStreamInfo.stream));
|
|
}
|
|
|
|
/**
|
|
* 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 = Mp4Generator.MVHD_TEMPLATE_.value();
|
|
this.writeUint32_(bytes, streamInfo.timescale, 20);
|
|
this.writeUint32_(bytes, upperWordDuration, 24);
|
|
this.writeUint32_(bytes, lowerWordDuration, 28);
|
|
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 = Mp4Generator.TKHD_TEMPLATE_.value();
|
|
this.writeUint32_(bytes, id, 20);
|
|
this.writeUint32_(bytes, upperWordDuration, 28);
|
|
this.writeUint32_(bytes, lowerWordDuration, 32);
|
|
this.writeUint16_(bytes, width, 88);
|
|
this.writeUint16_(bytes, height, 92);
|
|
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 = Mp4Generator.MDHD_TEMPLATE_.value();
|
|
this.writeUint32_(bytes, streamInfo.timescale, 20);
|
|
this.writeUint32_(bytes, upperWordDuration, 24);
|
|
this.writeUint32_(bytes, lowerWordDuration, 28);
|
|
this.writeUint16_(bytes, languageNumber, 32);
|
|
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.value();
|
|
break;
|
|
case ContentType.AUDIO:
|
|
bytes = Mp4Generator.HDLR_TYPES_.audio.value();
|
|
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_.value()),
|
|
Mp4Generator.DINF_.value(), this.stbl_(streamInfo));
|
|
case ContentType.AUDIO:
|
|
return Mp4Generator.box(
|
|
'minf', Mp4Generator.box('smhd', Mp4Generator.SMHD_.value()),
|
|
Mp4Generator.DINF_.value(), 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.STTS_BOX_.value(),
|
|
Mp4Generator.STSC_BOX_.value(),
|
|
Mp4Generator.STSZ_BOX_.value(),
|
|
Mp4Generator.STCO_BOX_.value());
|
|
}
|
|
|
|
/**
|
|
* 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_.value(), bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a video sample entry box (shared implementation for AVC1 / HVC1)
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @param {string} boxName 'avc1' | 'hvc1'
|
|
* @param {string} configBoxName 'avcC' | 'hvcC'
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
videoSampleEntry_(streamInfo, boxName, configBoxName) {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
|
|
const width = streamInfo.stream.width || 0;
|
|
const height = streamInfo.stream.height || 0;
|
|
|
|
let configBox = new Uint8Array([]);
|
|
if (streamInfo.mediaConfig.byteLength > 0) {
|
|
configBox = Mp4Generator.box(configBoxName, streamInfo.mediaConfig);
|
|
}
|
|
|
|
const entryBytes = Mp4Generator.VIDEO_SAMPLE_ENTRY_TEMPLATE_.value();
|
|
this.writeUint16_(entryBytes, width, 24);
|
|
this.writeUint16_(entryBytes, height, 26);
|
|
|
|
const paspBox = this.pasp_(streamInfo);
|
|
let sinfBox = new Uint8Array([]);
|
|
let entryBoxName = boxName;
|
|
if (Mp4Generator.isStreamEncrypted_(streamInfo.stream)) {
|
|
sinfBox = this.sinf(streamInfo.stream, streamInfo.codecs);
|
|
entryBoxName = 'encv';
|
|
}
|
|
return Mp4Generator.box(
|
|
entryBoxName, entryBytes, configBox, paspBox, sinfBox);
|
|
}
|
|
|
|
/**
|
|
* Generate a AVC1 box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
avc1_(streamInfo) {
|
|
return this.videoSampleEntry_(streamInfo, 'avc1', 'avcC');
|
|
}
|
|
|
|
/**
|
|
* Generate a HVC1 box
|
|
*
|
|
* @param {shaka.util.Mp4Generator.StreamInfo} streamInfo
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
hvc1_(streamInfo) {
|
|
return this.videoSampleEntry_(streamInfo, 'hvc1', 'hvcC');
|
|
}
|
|
|
|
/**
|
|
* 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 || 0;
|
|
const vSpacing = streamInfo.vSpacing || 0;
|
|
const box = Mp4Generator.allocBox_('pasp', 8);
|
|
this.writeUint32_(box, hSpacing, Mp4Generator.BOX_HEADER_SIZE_);
|
|
this.writeUint32_(box, vSpacing, Mp4Generator.BOX_HEADER_SIZE_ + 4);
|
|
return box;
|
|
}
|
|
|
|
/**
|
|
* 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 = shaka.util.Mp4Generator.AUDIO_STSD_TEMPLATE_.value();
|
|
bytes[17] = channelsCount;
|
|
this.writeUint16_(bytes, audioSamplingRate, 24);
|
|
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.mediaConfig);
|
|
|
|
let boxName = 'ac-3';
|
|
let sinfBox = new Uint8Array([]);
|
|
if (Mp4Generator.isStreamEncrypted_(streamInfo.stream)) {
|
|
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.mediaConfig);
|
|
|
|
let boxName = 'ec-3';
|
|
let sinfBox = new Uint8Array([]);
|
|
if (Mp4Generator.isStreamEncrypted_(streamInfo.stream)) {
|
|
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.mediaConfig);
|
|
|
|
let boxName = 'Opus';
|
|
let sinfBox = new Uint8Array([]);
|
|
if (Mp4Generator.isStreamEncrypted_(streamInfo.stream)) {
|
|
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.mediaConfig.byteLength > 0) {
|
|
esdsBox = Mp4Generator.box('esds', streamInfo.mediaConfig);
|
|
} else {
|
|
esdsBox = Mp4Generator.box('esds', this.esds_(streamInfo));
|
|
}
|
|
|
|
let boxName = 'mp4a';
|
|
let sinfBox = new Uint8Array([]);
|
|
if (Mp4Generator.isStreamEncrypted_(streamInfo.stream)) {
|
|
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);
|
|
|
|
const bytes = shaka.util.Mp4Generator.ESDS_TEMPLATE_.value();
|
|
// ES_ID
|
|
this.writeUint16_(bytes, id, 6);
|
|
// ASC byte 1
|
|
bytes[26] = (audioObjectType << 3) | (indexFreq >>> 1);
|
|
// ASC byte 2
|
|
bytes[27] = (indexFreq << 7) | (channelsCount << 3);
|
|
return bytes;
|
|
}
|
|
|
|
/**
|
|
* 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));
|
|
}
|
|
return Mp4Generator.box('mvex', ...trexArrays);
|
|
}
|
|
|
|
/**
|
|
* 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 = Mp4Generator.TREX_TEMPLATE_.value();
|
|
this.writeUint32_(bytes, id, 4);
|
|
return Mp4Generator.box('trex', bytes);
|
|
}
|
|
|
|
/**
|
|
* Generate a PSSH box
|
|
*
|
|
* @param {!shaka.extern.Stream} stream
|
|
* @return {!Uint8Array}
|
|
*/
|
|
psshs(stream) {
|
|
const initDatas = [];
|
|
if (!shaka.util.Mp4Generator.isStreamEncrypted_(stream)) {
|
|
return new Uint8Array([]);
|
|
}
|
|
|
|
for (const drmInfo of 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 BOX_HEADER_SIZE = Mp4Generator.BOX_HEADER_SIZE_;
|
|
const box = Mp4Generator.allocBox_('frma', 4);
|
|
this.writeUint32_(box, this.stringToCharCode_(codec), BOX_HEADER_SIZE);
|
|
return box;
|
|
}
|
|
|
|
/**
|
|
* 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 bytes = Mp4Generator.SCHM_TEMPLATE_.value();
|
|
this.writeUint32_(bytes, this.stringToCharCode_(encryptionScheme), 4);
|
|
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;
|
|
goog.asserts.assert(defaultKeyId, 'Default KID should be non-null');
|
|
return Mp4Generator.box(
|
|
'tenc', Mp4Generator.TENC_HEADER_.value(), defaultKeyId);
|
|
}
|
|
|
|
/**
|
|
* Generate a Segment Data (MP4).
|
|
*
|
|
* @return {!Uint8Array}
|
|
*/
|
|
segmentData() {
|
|
const Mp4Generator = shaka.util.Mp4Generator;
|
|
const boxHeaderSize = Mp4Generator.BOX_HEADER_SIZE_;
|
|
// Assemble whole segment into one allocation
|
|
const streams = [];
|
|
// Running sum of bytes needed for the final segment buffer.
|
|
let total = 0;
|
|
for (const streamInfo of this.streamInfos_) {
|
|
const moof = this.moof_(streamInfo);
|
|
const samples = streamInfo.data ? streamInfo.data.samples : [];
|
|
// Sum of sample byte lengths for this stream's mdat payload.
|
|
let mdatPayload = 0;
|
|
for (const sample of samples) {
|
|
mdatPayload += sample.data.byteLength;
|
|
}
|
|
const mdatSize = boxHeaderSize + mdatPayload;
|
|
streams.push({moof, samples, mdatSize});
|
|
total += moof.byteLength + mdatSize;
|
|
}
|
|
const result = shaka.util.BufferUtils.toUint8(new ArrayBuffer(total));
|
|
// Current byte position in the output buffer.
|
|
let offset = 0;
|
|
for (const {moof, samples, mdatSize} of streams) {
|
|
result.set(moof, offset);
|
|
offset += moof.byteLength;
|
|
this.writeUint32_(result, mdatSize, offset);
|
|
result.set(Mp4Generator.MDAT_TYPE_.value(), offset + 4);
|
|
offset += boxHeaderSize;
|
|
for (const sample of samples) {
|
|
result.set(sample.data, offset);
|
|
offset += sample.data.byteLength;
|
|
}
|
|
}
|
|
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 MFHD 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 box = Mp4Generator.allocBox_('mfhd', 8);
|
|
// box[BOX_HEADER_SIZE_ + 0] = version 0
|
|
// box[BOX_HEADER_SIZE_ + 1-3] = flags 0
|
|
this.writeUint32_(box, sequenceNumber, Mp4Generator.BOX_HEADER_SIZE_ + 4);
|
|
return box;
|
|
}
|
|
|
|
/**
|
|
* 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 box = Mp4Generator.allocBox_('sdtp', 4 + samples.length);
|
|
// leave the full box header (4 bytes) all zero
|
|
const offset = Mp4Generator.BOX_HEADER_SIZE_ + 4;
|
|
// write the sample table
|
|
for (let i = 0; i < samples.length; i++) {
|
|
const flags = samples[i].flags;
|
|
box[i + offset] = (flags.dependsOn << 4) |
|
|
(flags.isDependedOn << 2) |
|
|
flags.hasRedundancy;
|
|
}
|
|
return box;
|
|
}
|
|
|
|
/**
|
|
* 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 = Mp4Generator.TFHD_TEMPLATE_.value();
|
|
this.writeUint32_(bytes, id, 4);
|
|
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 BOX_HEADER_SIZE = Mp4Generator.BOX_HEADER_SIZE_;
|
|
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 box = Mp4Generator.allocBox_('tfdt', 12);
|
|
box[BOX_HEADER_SIZE] = 0x01; // version 1
|
|
// bytes[BOX_HEADER_SIZE + 1-3] = flags 0
|
|
this.writeUint32_(box, upperWordBaseMediaDecodeTime, BOX_HEADER_SIZE + 4);
|
|
this.writeUint32_(box, lowerWordBaseMediaDecodeTime, BOX_HEADER_SIZE + 8);
|
|
return box;
|
|
}
|
|
|
|
/**
|
|
* 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 box = Mp4Generator.allocBox_('trun', bytesLen);
|
|
offset += Mp4Generator.BOX_HEADER_SIZE_ + bytesLen;
|
|
const isVideo = streamInfo.type === ContentType.VIDEO;
|
|
|
|
// Header del TRUN
|
|
// version
|
|
box[Mp4Generator.BOX_HEADER_SIZE_] = isVideo ? 0x01 : 0x00;
|
|
// flags
|
|
box[Mp4Generator.BOX_HEADER_SIZE_ + 1] = 0x00;
|
|
box[Mp4Generator.BOX_HEADER_SIZE_ + 2] = 0x0f;
|
|
box[Mp4Generator.BOX_HEADER_SIZE_ + 3] = 0x01;
|
|
// sample_count
|
|
this.writeUint32_(box, samplesLength, Mp4Generator.BOX_HEADER_SIZE_ + 4);
|
|
// data_offset
|
|
this.writeUint32_(box, offset, Mp4Generator.BOX_HEADER_SIZE_ + 8);
|
|
|
|
for (let i = 0; i < samplesLength; i++) {
|
|
const sample = samples[i];
|
|
const flags = sample.flags;
|
|
const base = Mp4Generator.BOX_HEADER_SIZE_ + 12 + 16 * i;
|
|
// sample_duration
|
|
this.writeUint32_(box, sample.duration, base);
|
|
// sample_size
|
|
this.writeUint32_(box, sample.size, base + 4);
|
|
// sample_flags
|
|
box[base + 8] = (flags.isLeading << 2) | flags.dependsOn;
|
|
box[base + 9] = (flags.isDependedOn << 6) |
|
|
(flags.hasRedundancy << 4) | flags.isNonSync;
|
|
box[base + 10] = flags.degradPrio & (0xf0 << 8);
|
|
box[base + 11] = flags.degradPrio & 0x0f;
|
|
// sample_composition_time_offset
|
|
this.writeUint32_(box, sample.cts, base + 12);
|
|
}
|
|
return box;
|
|
}
|
|
|
|
/**
|
|
* @param {!Uint8Array} bytes
|
|
* @param {number} value
|
|
* @param {number} offset
|
|
* @private
|
|
*/
|
|
writeUint32_(bytes, value, offset) {
|
|
bytes[offset] = (value >>> 24) & 0xff;
|
|
bytes[offset + 1] = (value >>> 16) & 0xff;
|
|
bytes[offset + 2] = (value >>> 8) & 0xff;
|
|
bytes[offset + 3] = value & 0xff;
|
|
}
|
|
|
|
/**
|
|
* @param {!Uint8Array} bytes
|
|
* @param {number} value
|
|
* @param {number} offset
|
|
* @private
|
|
*/
|
|
writeUint16_(bytes, value, offset) {
|
|
bytes[offset] = (value >>> 8) & 0xff;
|
|
bytes[offset + 1] = value & 0xff;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Generate a box
|
|
*
|
|
* @param {string} boxName
|
|
* @param {...!Uint8Array} payload
|
|
* @return {!Uint8Array}
|
|
*/
|
|
static box(boxName, ...payload) {
|
|
let size = 0;
|
|
// calculate the total size of the payload
|
|
for (const chunk of payload) {
|
|
size += chunk.byteLength;
|
|
}
|
|
|
|
const result = shaka.util.Mp4Generator.allocBox_(boxName, size);
|
|
|
|
// copy the payload into the result
|
|
let pointer = shaka.util.Mp4Generator.BOX_HEADER_SIZE_;
|
|
|
|
for (let i = 0; i < payload.length; i++) {
|
|
// copy payload[i] array @ offset pointer
|
|
result.set(payload[i], pointer);
|
|
pointer += payload[i].byteLength;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Generate a box that can hold a payload of a given size
|
|
*
|
|
* @param {string} boxName
|
|
* @param {number} payloadSize
|
|
* @return {!Uint8Array}
|
|
* @private
|
|
*/
|
|
static allocBox_(boxName, payloadSize) {
|
|
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
|
|
// calculate the total size we need to allocate
|
|
const size = shaka.util.Mp4Generator.BOX_HEADER_SIZE_ + payloadSize;
|
|
|
|
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);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.extern.Stream} stream
|
|
* @return {boolean}
|
|
* @private
|
|
*/
|
|
static isStreamEncrypted_(stream) {
|
|
return stream.encrypted && stream.drmInfos.length > 0;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private {number}
|
|
*/
|
|
shaka.util.Mp4Generator.UINT32_MAX_ = Math.pow(2, 32) - 1;
|
|
|
|
/**
|
|
* @private {!Map<string, !Array<number>>}
|
|
*/
|
|
shaka.util.Mp4Generator.BOX_TYPES_ = new Map();
|
|
|
|
/**
|
|
* @const {number}
|
|
*/
|
|
shaka.util.Mp4Generator.BOX_HEADER_SIZE_ = 8;
|
|
|
|
/**
|
|
* 'mdat' box-type bytes (ASCII).
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.MDAT_TYPE_ =
|
|
new shaka.util.Lazy(() => new Uint8Array([0x6d, 0x64, 0x61, 0x74]));
|
|
|
|
/**
|
|
* Template MVHD (version 1, 112 bytes).
|
|
* Dynamic fields: timescale [20], upperDur [24], lowerDur [28].
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.MVHD_TEMPLATE_ =
|
|
new shaka.util.Lazy(() => new Uint8Array([
|
|
0x01, 0x00, 0x00, 0x00, // version 1, flags
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x02, // creation_time
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x03, // modification_time
|
|
0x00, 0x00, 0x00, 0x00, // timescale [20]
|
|
0x00, 0x00, 0x00, 0x00, // upperWordDuration [24]
|
|
0x00, 0x00, 0x00, 0x00, // lowerWordDuration [28]
|
|
0x00, 0x01, 0x00, 0x00, // rate 1.0
|
|
0x01, 0x00, // volume 1.0
|
|
0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
// unity matrix
|
|
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,
|
|
// pre_defined (24 bytes, todos cero)
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0xff, 0xff, 0xff, 0xff, // next_track_ID
|
|
]));
|
|
|
|
/**
|
|
* Template TKHD (version 1, 96 bytes).
|
|
* Dynamic fields: track_ID [20], upperDur [28], lowerDur [32],
|
|
* width [88] (uint16), height [92] (uint16).
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.TKHD_TEMPLATE_ =
|
|
new shaka.util.Lazy(() => new Uint8Array([
|
|
0x01, 0x00, 0x00, 0x07, // version 1, flags
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x02, // creation_time
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x03, // modification_time
|
|
0x00, 0x00, 0x00, 0x00, // track_ID [20]
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x00, 0x00, 0x00, // upperWordDuration [28]
|
|
0x00, 0x00, 0x00, 0x00, // lowerWordDuration [32]
|
|
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
|
|
// unity matrix
|
|
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,
|
|
0x00, 0x00, 0x00, 0x00, // width [88]
|
|
0x00, 0x00, 0x00, 0x00, // height [92]
|
|
]));
|
|
|
|
/**
|
|
* Template MDHD (version 1, 36 bytes).
|
|
* Dynamic fields: timescale [20], upperDur [24], lowerDur [28],
|
|
* language [32] (uint16).
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.MDHD_TEMPLATE_ =
|
|
new shaka.util.Lazy(() => new Uint8Array([
|
|
0x01, 0x00, 0x00, 0x00, // version 1, flags
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x02, // creation_time
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x03, // modification_time
|
|
0x00, 0x00, 0x00, 0x00, // timescale [20]
|
|
0x00, 0x00, 0x00, 0x00, // upperWordDuration [24]
|
|
0x00, 0x00, 0x00, 0x00, // lowerWordDuration [28]
|
|
0x00, 0x00, // language [32]
|
|
0x00, 0x00, // pre_defined
|
|
]));
|
|
|
|
/**
|
|
* Template TREX (24 bytes).
|
|
* Dynamic field: track_ID [4].
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.TREX_TEMPLATE_ =
|
|
new shaka.util.Lazy(() => new Uint8Array([
|
|
0x00, 0x00, 0x00, 0x00, // version 0, flags
|
|
0x00, 0x00, 0x00, 0x00, // track_ID [4]
|
|
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
|
|
]));
|
|
|
|
/**
|
|
* Template TFHD (24 bytes).
|
|
* Dynamic field: track_ID [4].
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.TFHD_TEMPLATE_ =
|
|
new shaka.util.Lazy(() => new Uint8Array([
|
|
0x00, 0x00, 0x00, 0x3a, // version 0, flags
|
|
0x00, 0x00, 0x00, 0x00, // track_ID [4]
|
|
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
|
|
]));
|
|
|
|
/**
|
|
* Template ESDS (31 bytes).
|
|
* Dynamic fields: ES_ID [6] (uint16), ASC byte1 [26], ASC byte2 [27].
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.ESDS_TEMPLATE_ =
|
|
new shaka.util.Lazy(() => new Uint8Array([
|
|
0x00, 0x00, 0x00, 0x00, // version 0, flags
|
|
0x03, // tag: ES_DescriptionTag
|
|
0x19, // length: 25
|
|
0x00, 0x00, // ES_ID [6]
|
|
0x00, // streamDependenceFlag / URL_flag / priority
|
|
0x04, // tag: DecoderConfigDescriptionTag
|
|
0x11, // length: 17
|
|
0x40, // object type: Audio ISO/IEC 14496-3
|
|
0x15, // streamType: AudioStream
|
|
0x00, 0x00, 0x00, // bufferSizeDB
|
|
0x00, 0x00, 0x00, 0x00, // maxBitrate
|
|
0x00, 0x00, 0x00, 0x00, // avgBitrate
|
|
0x05, // tag: DecoderSpecificInfoTag
|
|
0x02, // length: 2
|
|
0x00, // AudioSpecificConfig byte 1 [26]
|
|
0x00, // AudioSpecificConfig byte 2 [27]
|
|
0x06, 0x01, 0x02, // GASpecificConfig
|
|
]));
|
|
|
|
/**
|
|
* Template for mp3_, ac3_, ec3_, opus_, mp4a_ (28 bytes).
|
|
* Dynamic fields: channelsCount [17] (uint8),
|
|
* audioSamplingRate [24] (uint16).
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.AUDIO_STSD_TEMPLATE_ =
|
|
new shaka.util.Lazy(() => 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, // padding
|
|
0x00, // channelsCount [17]
|
|
0x00, 0x10, // sampleSize: 16 bits
|
|
0x00, 0x00, 0x00, 0x00, // reserved2
|
|
0x00, 0x00, // audioSamplingRate [24]
|
|
0x00, 0x00,
|
|
]));
|
|
|
|
/**
|
|
* Template for avc1_ and hvc1_ (78 bytes).
|
|
* Dynamic fields: width [24] (uint16), height [26] (uint16).
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.VIDEO_SAMPLE_ENTRY_TEMPLATE_ =
|
|
new shaka.util.Lazy(() => 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
|
|
0x00, 0x00, // width [24]
|
|
0x00, 0x00, // height [26]
|
|
0x00, 0x48, 0x00, 0x00, // horizontal resolution 72 dpi
|
|
0x00, 0x48, 0x00, 0x00, // vertical resolution 72 dpi
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
0x00, 0x01, // frame_count
|
|
// compressor name (32 bytes)
|
|
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,
|
|
0x00, 0x18, // depth = 24
|
|
0x11, 0x11, // pre_defined = -1
|
|
]));
|
|
|
|
/**
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.TENC_HEADER_ =
|
|
new shaka.util.Lazy(() => new Uint8Array([
|
|
0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, // reserved fields
|
|
0x01, // default_isProtected: true
|
|
0x08, // default_Per_Sample_IV_Size: 8
|
|
]));
|
|
|
|
/**
|
|
* Template SCHM (12 bytes).
|
|
* Dynamic field: schemeNumber [4] (uint32).
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.SCHM_TEMPLATE_ =
|
|
new shaka.util.Lazy(() => new Uint8Array([
|
|
0x00, 0x00, 0x00, 0x00, // version 0, flags
|
|
0x00, 0x00, 0x00, 0x00, // scheme type [4]
|
|
0x00, 0x01, 0x00, 0x00, // scheme version: 1.0
|
|
]));
|
|
|
|
/**
|
|
* @private {{
|
|
* video: !shaka.util.Lazy<!Uint8Array>,
|
|
* audio: !shaka.util.Lazy<!Uint8Array>,
|
|
* }}
|
|
*/
|
|
shaka.util.Mp4Generator.HDLR_TYPES_ = {
|
|
video: new shaka.util.Lazy(() => 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 shaka.util.Lazy(() => 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 {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.VMHD_ = new shaka.util.Lazy(() => new Uint8Array([
|
|
0x00, // version
|
|
0x00, 0x00, 0x01, // flags
|
|
0x00, 0x00, // graphics mode
|
|
0x00, 0x00,
|
|
0x00, 0x00,
|
|
0x00, 0x00, // op color
|
|
]));
|
|
|
|
/**
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.SMHD_ = new shaka.util.Lazy(() => new Uint8Array([
|
|
0x00, // version
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, // balance, 0 means centered
|
|
0x00, 0x00, // reserved
|
|
]));
|
|
|
|
/**
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.STSD_ = new shaka.util.Lazy(() => new Uint8Array([
|
|
0x00, // version 0
|
|
0x00, 0x00, 0x00, // flags
|
|
0x00, 0x00, 0x00, 0x01, // entry_count
|
|
]));
|
|
|
|
/**
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.FTYP_ = new shaka.util.Lazy(() => {
|
|
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]);
|
|
return shaka.util.Mp4Generator.box(
|
|
'ftyp', majorBrand, minorVersion, majorBrand, avc1Brand);
|
|
});
|
|
|
|
/**
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.DINF_ = new shaka.util.Lazy(() => {
|
|
const 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
|
|
]);
|
|
const drefBox = shaka.util.Mp4Generator.box('dref', dref);
|
|
return shaka.util.Mp4Generator.box('dinf', drefBox);
|
|
});
|
|
|
|
/**
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.STTS_BOX_ = new shaka.util.Lazy(
|
|
// [0] version
|
|
// [1-3] flags
|
|
// [4-7] entry_count
|
|
() => shaka.util.Mp4Generator.allocBox_('stts', 8));
|
|
|
|
/**
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.STSC_BOX_ = new shaka.util.Lazy(
|
|
// [0] version
|
|
// [1-3] flags
|
|
// [4-7] entry_count
|
|
() => shaka.util.Mp4Generator.allocBox_('stsc', 8));
|
|
|
|
/**
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.STSZ_BOX_ = new shaka.util.Lazy(
|
|
// [0] version
|
|
// [1-3] flags
|
|
// [4-7] entry_count
|
|
// [8-11] sample_count
|
|
() => shaka.util.Mp4Generator.allocBox_('stsz', 12));
|
|
|
|
/**
|
|
* @private {!shaka.util.Lazy<!Uint8Array>}
|
|
*/
|
|
shaka.util.Mp4Generator.STCO_BOX_ = new shaka.util.Lazy(
|
|
// [0] // version
|
|
// [1-3] // flags
|
|
// [4-7] // entry_count
|
|
() => shaka.util.Mp4Generator.allocBox_('stco', 8));
|
|
|
|
/**
|
|
* @typedef {{
|
|
* id: number,
|
|
* type: string,
|
|
* codecs: string,
|
|
* timescale: number,
|
|
* duration: number,
|
|
* mediaConfig: !Uint8Array,
|
|
* hSpacing: (number|undefined),
|
|
* vSpacing: (number|undefined),
|
|
* 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 {number} timescale
|
|
* The Stream's timescale.
|
|
* @property {number} duration
|
|
* The Stream's duration.
|
|
* @property {!Uint8Array} mediaConfig
|
|
* The stream's media config.
|
|
* @property {number|undefined} hSpacing
|
|
* The stream's video horizontal spacing of pixels.
|
|
* @property {number|undefined} 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;
|