@l5i/dashjs
Version:
A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.
654 lines (521 loc) • 24.2 kB
JavaScript
/**
* The copyright in this software is being made available under the BSD License,
* included below. This software may be subject to other third party and contributor
* rights, including patent rights, and no such rights are granted under this license.
*
* Copyright (c) 2013, Dash Industry Forum.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
* * Neither the name of Dash Industry Forum nor the names of its
* contributors may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @module MssFragmentMoovProcessor
* @param {Object} config object
*/
function MssFragmentMoovProcessor(config) {
config = config || {};
const NALUTYPE_SPS = 7;
const NALUTYPE_PPS = 8;
const constants = config.constants;
const ISOBoxer = config.ISOBoxer;
let protectionController = config.protectionController;
let instance,
period,
adaptationSet,
representation,
contentProtection,
timescale,
trackId;
function createFtypBox(isoFile) {
let ftyp = ISOBoxer.createBox('ftyp', isoFile);
ftyp.major_brand = 'iso6';
ftyp.minor_version = 1; // is an informative integer for the minor version of the major brand
ftyp.compatible_brands = []; //is a list, to the end of the box, of brands isom, iso6 and msdh
ftyp.compatible_brands[0] = 'isom'; // => decimal ASCII value for isom
ftyp.compatible_brands[1] = 'iso6'; // => decimal ASCII value for iso6
ftyp.compatible_brands[2] = 'msdh'; // => decimal ASCII value for msdh
return ftyp;
}
function createMoovBox(isoFile) {
// moov box
let moov = ISOBoxer.createBox('moov', isoFile);
// moov/mvhd
createMvhdBox(moov);
// moov/trak
let trak = ISOBoxer.createBox('trak', moov);
// moov/trak/tkhd
createTkhdBox(trak);
// moov/trak/mdia
let mdia = ISOBoxer.createBox('mdia', trak);
// moov/trak/mdia/mdhd
createMdhdBox(mdia);
// moov/trak/mdia/hdlr
createHdlrBox(mdia);
// moov/trak/mdia/minf
let minf = ISOBoxer.createBox('minf', mdia);
switch (adaptationSet.type) {
case constants.VIDEO:
// moov/trak/mdia/minf/vmhd
createVmhdBox(minf);
break;
case constants.AUDIO:
// moov/trak/mdia/minf/smhd
createSmhdBox(minf);
break;
default:
break;
}
// moov/trak/mdia/minf/dinf
let dinf = ISOBoxer.createBox('dinf', minf);
// moov/trak/mdia/minf/dinf/dref
createDrefBox(dinf);
// moov/trak/mdia/minf/stbl
let stbl = ISOBoxer.createBox('stbl', minf);
// Create empty stts, stsc, stco and stsz boxes
// Use data field as for codem-isoboxer unknown boxes for setting fields value
// moov/trak/mdia/minf/stbl/stts
let stts = ISOBoxer.createFullBox('stts', stbl);
stts._data = [0, 0, 0, 0, 0, 0, 0, 0]; // version = 0, flags = 0, entry_count = 0
// moov/trak/mdia/minf/stbl/stsc
let stsc = ISOBoxer.createFullBox('stsc', stbl);
stsc._data = [0, 0, 0, 0, 0, 0, 0, 0]; // version = 0, flags = 0, entry_count = 0
// moov/trak/mdia/minf/stbl/stco
let stco = ISOBoxer.createFullBox('stco', stbl);
stco._data = [0, 0, 0, 0, 0, 0, 0, 0]; // version = 0, flags = 0, entry_count = 0
// moov/trak/mdia/minf/stbl/stsz
let stsz = ISOBoxer.createFullBox('stsz', stbl);
stsz._data = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // version = 0, flags = 0, sample_size = 0, sample_count = 0
// moov/trak/mdia/minf/stbl/stsd
createStsdBox(stbl);
// moov/mvex
let mvex = ISOBoxer.createBox('mvex', moov);
// moov/mvex/trex
createTrexBox(mvex);
if (contentProtection && protectionController) {
let supportedKS = protectionController.getSupportedKeySystemsFromContentProtection(contentProtection);
createProtectionSystemSpecificHeaderBox(moov, supportedKS);
}
}
function createMvhdBox(moov) {
let mvhd = ISOBoxer.createFullBox('mvhd', moov);
mvhd.version = 1; // version = 1 in order to have 64bits duration value
mvhd.creation_time = 0; // the creation time of the presentation => ignore (set to 0)
mvhd.modification_time = 0; // the most recent time the presentation was modified => ignore (set to 0)
mvhd.timescale = timescale; // the time-scale for the entire presentation => 10000000 for MSS
mvhd.duration = Math.round(period.duration * timescale); // the length of the presentation (in the indicated timescale) => take duration of period
mvhd.rate = 1.0; // 16.16 number, '1.0' = normal playback
mvhd.volume = 1.0; // 8.8 number, '1.0' = full volume
mvhd.reserved1 = 0;
mvhd.reserved2 = [0x0, 0x0];
mvhd.matrix = [
1, 0, 0, // provides a transformation matrix for the video;
0, 1, 0, // (u,v,w) are restricted here to (0,0,1)
0, 0, 16384
];
mvhd.pre_defined = [0, 0, 0, 0, 0, 0];
mvhd.next_track_ID = trackId + 1; // indicates a value to use for the track ID of the next track to be added to this presentation
return mvhd;
}
function createTkhdBox(trak) {
let tkhd = ISOBoxer.createFullBox('tkhd', trak);
tkhd.version = 1; // version = 1 in order to have 64bits duration value
tkhd.flags = 0x1 | // Track_enabled (0x000001): Indicates that the track is enabled
0x2 | // Track_in_movie (0x000002): Indicates that the track is used in the presentation
0x4; // Track_in_preview (0x000004): Indicates that the track is used when previewing the presentation
tkhd.creation_time = 0; // the creation time of the presentation => ignore (set to 0)
tkhd.modification_time = 0; // the most recent time the presentation was modified => ignore (set to 0)
tkhd.track_ID = trackId; // uniquely identifies this track over the entire life-time of this presentation
tkhd.reserved1 = 0;
tkhd.duration = Math.round(period.duration * timescale); // the duration of this track (in the timescale indicated in the Movie Header Box) => take duration of period
tkhd.reserved2 = [0x0, 0x0];
tkhd.layer = 0; // specifies the front-to-back ordering of video tracks; tracks with lower numbers are closer to the viewer => 0 since only one video track
tkhd.alternate_group = 0; // specifies a group or collection of tracks => ignore
tkhd.volume = 1.0; // '1.0' = full volume
tkhd.reserved3 = 0;
tkhd.matrix = [
1, 0, 0, // provides a transformation matrix for the video;
0, 1, 0, // (u,v,w) are restricted here to (0,0,1)
0, 0, 16384
];
tkhd.width = representation.width; // visual presentation width
tkhd.height = representation.height; // visual presentation height
return tkhd;
}
function createMdhdBox(mdia) {
let mdhd = ISOBoxer.createFullBox('mdhd', mdia);
mdhd.version = 1; // version = 1 in order to have 64bits duration value
mdhd.creation_time = 0; // the creation time of the presentation => ignore (set to 0)
mdhd.modification_time = 0; // the most recent time the presentation was modified => ignore (set to 0)
mdhd.timescale = timescale; // the time-scale for the entire presentation
mdhd.duration = Math.round(period.duration * timescale); // the duration of this media (in the scale of the timescale). If the duration cannot be determined then duration is set to all 1s.
mdhd.language = adaptationSet.lang || 'und'; // declares the language code for this media (see getLanguageCode())
mdhd.pre_defined = 0;
return mdhd;
}
function createHdlrBox(mdia) {
let hdlr = ISOBoxer.createFullBox('hdlr', mdia);
hdlr.pre_defined = 0;
switch (adaptationSet.type) {
case constants.VIDEO:
hdlr.handler_type = 'vide';
break;
case constants.AUDIO:
hdlr.handler_type = 'soun';
break;
default:
hdlr.handler_type = 'meta';
break;
}
hdlr.name = representation.id;
hdlr.reserved = [0, 0, 0];
return hdlr;
}
function createVmhdBox(minf) {
let vmhd = ISOBoxer.createFullBox('vmhd', minf);
vmhd.flags = 1;
vmhd.graphicsmode = 0; // specifies a composition mode for this video track, from the following enumerated set, which may be extended by derived specifications: copy = 0 copy over the existing image
vmhd.opcolor = [0, 0, 0]; // is a set of 3 colour values (red, green, blue) available for use by graphics modes
return vmhd;
}
function createSmhdBox(minf) {
let smhd = ISOBoxer.createFullBox('smhd', minf);
smhd.flags = 1;
smhd.balance = 0; // is a fixed-point 8.8 number that places mono audio tracks in a stereo space; 0 is centre (the normal value); full left is -1.0 and full right is 1.0.
smhd.reserved = 0;
return smhd;
}
function createDrefBox(dinf) {
let dref = ISOBoxer.createFullBox('dref', dinf);
dref.entry_count = 1;
dref.entries = [];
let url = ISOBoxer.createFullBox('url ', dref, false);
url.location = '';
url.flags = 1;
dref.entries.push(url);
return dref;
}
function createStsdBox(stbl) {
let stsd = ISOBoxer.createFullBox('stsd', stbl);
stsd.entries = [];
switch (adaptationSet.type) {
case constants.VIDEO:
case constants.AUDIO:
stsd.entries.push(createSampleEntry(stsd));
break;
default:
break;
}
stsd.entry_count = stsd.entries.length; // is an integer that counts the actual entries
return stsd;
}
function createSampleEntry(stsd) {
let codec = representation.codecs.substring(0, representation.codecs.indexOf('.'));
switch (codec) {
case 'avc1':
return createAVCVisualSampleEntry(stsd, codec);
case 'mp4a':
return createMP4AudioSampleEntry(stsd, codec);
default:
throw {
name: 'Unsupported codec',
message: 'Unsupported codec',
data: {
codec: codec
}
};
}
}
function createAVCVisualSampleEntry(stsd, codec) {
let avc1;
if (contentProtection) {
avc1 = ISOBoxer.createBox('encv', stsd, false);
} else {
avc1 = ISOBoxer.createBox('avc1', stsd, false);
}
// SampleEntry fields
avc1.reserved1 = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0];
avc1.data_reference_index = 1;
// VisualSampleEntry fields
avc1.pre_defined1 = 0;
avc1.reserved2 = 0;
avc1.pre_defined2 = [0, 0, 0];
avc1.height = representation.height;
avc1.width = representation.width;
avc1.horizresolution = 72; // 72 dpi
avc1.vertresolution = 72; // 72 dpi
avc1.reserved3 = 0;
avc1.frame_count = 1; // 1 compressed video frame per sample
avc1.compressorname = [
0x0A, 0x41, 0x56, 0x43, 0x20, 0x43, 0x6F, 0x64, // = 'AVC Coding';
0x69, 0x6E, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
];
avc1.depth = 0x0018; // 0x0018 – images are in colour with no alpha.
avc1.pre_defined3 = 65535;
avc1.config = createAVC1ConfigurationRecord();
if (contentProtection) {
// Create and add Protection Scheme Info Box
let sinf = ISOBoxer.createBox('sinf', avc1);
// Create and add Original Format Box => indicate codec type of the encrypted content
createOriginalFormatBox(sinf, codec);
// Create and add Scheme Type box
createSchemeTypeBox(sinf);
// Create and add Scheme Information Box
createSchemeInformationBox(sinf);
}
return avc1;
}
function createAVC1ConfigurationRecord() {
let avcC = null;
let avcCLength = 15; // length = 15 by default (0 SPS and 0 PPS)
// First get all SPS and PPS from codecPrivateData
let sps = [];
let pps = [];
let AVCProfileIndication = 0;
let AVCLevelIndication = 0;
let profile_compatibility = 0;
let nalus = representation.codecPrivateData.split('00000001').slice(1);
let naluBytes, naluType;
for (let i = 0; i < nalus.length; i++) {
naluBytes = hexStringtoBuffer(nalus[i]);
naluType = naluBytes[0] & 0x1F;
switch (naluType) {
case NALUTYPE_SPS:
sps.push(naluBytes);
avcCLength += naluBytes.length + 2; // 2 = sequenceParameterSetLength field length
break;
case NALUTYPE_PPS:
pps.push(naluBytes);
avcCLength += naluBytes.length + 2; // 2 = pictureParameterSetLength field length
break;
default:
break;
}
}
// Get profile and level from SPS
if (sps.length > 0) {
AVCProfileIndication = sps[0][1];
profile_compatibility = sps[0][2];
AVCLevelIndication = sps[0][3];
}
// Generate avcC buffer
avcC = new Uint8Array(avcCLength);
let i = 0;
// length
avcC[i++] = (avcCLength & 0xFF000000) >> 24;
avcC[i++] = (avcCLength & 0x00FF0000) >> 16;
avcC[i++] = (avcCLength & 0x0000FF00) >> 8;
avcC[i++] = (avcCLength & 0x000000FF);
avcC.set([0x61, 0x76, 0x63, 0x43], i); // type = 'avcC'
i += 4;
avcC[i++] = 1; // configurationVersion = 1
avcC[i++] = AVCProfileIndication;
avcC[i++] = profile_compatibility;
avcC[i++] = AVCLevelIndication;
avcC[i++] = 0xFF; // '11111' + lengthSizeMinusOne = 3
avcC[i++] = 0xE0 | sps.length; // '111' + numOfSequenceParameterSets
for (let n = 0; n < sps.length; n++) {
avcC[i++] = (sps[n].length & 0xFF00) >> 8;
avcC[i++] = (sps[n].length & 0x00FF);
avcC.set(sps[n], i);
i += sps[n].length;
}
avcC[i++] = pps.length; // numOfPictureParameterSets
for (let n = 0; n < pps.length; n++) {
avcC[i++] = (pps[n].length & 0xFF00) >> 8;
avcC[i++] = (pps[n].length & 0x00FF);
avcC.set(pps[n], i);
i += pps[n].length;
}
return avcC;
}
function createMP4AudioSampleEntry(stsd, codec) {
let mp4a;
if (contentProtection) {
mp4a = ISOBoxer.createBox('enca', stsd, false);
} else {
mp4a = ISOBoxer.createBox('mp4a', stsd, false);
}
// SampleEntry fields
mp4a.reserved1 = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0];
mp4a.data_reference_index = 1;
// AudioSampleEntry fields
mp4a.reserved2 = [0x0, 0x0];
mp4a.channelcount = representation.audioChannels;
mp4a.samplesize = 16;
mp4a.pre_defined = 0;
mp4a.reserved_3 = 0;
mp4a.samplerate = representation.audioSamplingRate << 16;
mp4a.esds = createMPEG4AACESDescriptor();
if (contentProtection) {
// Create and add Protection Scheme Info Box
let sinf = ISOBoxer.createBox('sinf', mp4a);
// Create and add Original Format Box => indicate codec type of the encrypted content
createOriginalFormatBox(sinf, codec);
// Create and add Scheme Type box
createSchemeTypeBox(sinf);
// Create and add Scheme Information Box
createSchemeInformationBox(sinf);
}
return mp4a;
}
function createMPEG4AACESDescriptor() {
// AudioSpecificConfig (see ISO/IEC 14496-3, subpart 1) => corresponds to hex bytes contained in 'codecPrivateData' field
let audioSpecificConfig = hexStringtoBuffer(representation.codecPrivateData);
// ESDS length = esds box header length (= 12) +
// ES_Descriptor header length (= 5) +
// DecoderConfigDescriptor header length (= 15) +
// decoderSpecificInfo header length (= 2) +
// AudioSpecificConfig length (= codecPrivateData length)
let esdsLength = 34 + audioSpecificConfig.length;
let esds = new Uint8Array(esdsLength);
let i = 0;
// esds box
esds[i++] = (esdsLength & 0xFF000000) >> 24; // esds box length
esds[i++] = (esdsLength & 0x00FF0000) >> 16; // ''
esds[i++] = (esdsLength & 0x0000FF00) >> 8; // ''
esds[i++] = (esdsLength & 0x000000FF); // ''
esds.set([0x65, 0x73, 0x64, 0x73], i); // type = 'esds'
i += 4;
esds.set([0, 0, 0, 0], i); // version = 0, flags = 0
i += 4;
// ES_Descriptor (see ISO/IEC 14496-1 (Systems))
esds[i++] = 0x03; // tag = 0x03 (ES_DescrTag)
esds[i++] = 20 + audioSpecificConfig.length; // size
esds[i++] = (trackId & 0xFF00) >> 8; // ES_ID = track_id
esds[i++] = (trackId & 0x00FF); // ''
esds[i++] = 0; // flags and streamPriority
// DecoderConfigDescriptor (see ISO/IEC 14496-1 (Systems))
esds[i++] = 0x04; // tag = 0x04 (DecoderConfigDescrTag)
esds[i++] = 15 + audioSpecificConfig.length; // size
esds[i++] = 0x40; // objectTypeIndication = 0x40 (MPEG-4 AAC)
esds[i] = 0x05 << 2; // streamType = 0x05 (Audiostream)
esds[i] |= 0 << 1; // upStream = 0
esds[i++] |= 1; // reserved = 1
esds[i++] = 0xFF; // buffersizeDB = undefined
esds[i++] = 0xFF; // ''
esds[i++] = 0xFF; // ''
esds[i++] = (representation.bandwidth & 0xFF000000) >> 24; // maxBitrate
esds[i++] = (representation.bandwidth & 0x00FF0000) >> 16; // ''
esds[i++] = (representation.bandwidth & 0x0000FF00) >> 8; // ''
esds[i++] = (representation.bandwidth & 0x000000FF); // ''
esds[i++] = (representation.bandwidth & 0xFF000000) >> 24; // avgbitrate
esds[i++] = (representation.bandwidth & 0x00FF0000) >> 16; // ''
esds[i++] = (representation.bandwidth & 0x0000FF00) >> 8; // ''
esds[i++] = (representation.bandwidth & 0x000000FF); // ''
// DecoderSpecificInfo (see ISO/IEC 14496-1 (Systems))
esds[i++] = 0x05; // tag = 0x05 (DecSpecificInfoTag)
esds[i++] = audioSpecificConfig.length; // size
esds.set(audioSpecificConfig, i); // AudioSpecificConfig bytes
return esds;
}
function createOriginalFormatBox(sinf, codec) {
let frma = ISOBoxer.createBox('frma', sinf);
frma.data_format = stringToCharCode(codec);
}
function createSchemeTypeBox(sinf) {
let schm = ISOBoxer.createFullBox('schm', sinf);
schm.flags = 0;
schm.version = 0;
schm.scheme_type = 0x63656E63; // 'cenc' => common encryption
schm.scheme_version = 0x00010000; // version set to 0x00010000 (Major version 1, Minor version 0)
}
function createSchemeInformationBox(sinf) {
let schi = ISOBoxer.createBox('schi', sinf);
// Create and add Track Encryption Box
createTrackEncryptionBox(schi);
}
function createProtectionSystemSpecificHeaderBox(moov, keySystems) {
let pssh_bytes;
let pssh;
let i;
let parsedBuffer;
for (i = 0; i < keySystems.length; i += 1) {
pssh_bytes = keySystems[i].initData;
parsedBuffer = ISOBoxer.parseBuffer(pssh_bytes);
pssh = parsedBuffer.fetch('pssh');
if (pssh) {
ISOBoxer.Utils.appendBox(moov, pssh);
}
}
}
function createTrackEncryptionBox(schi) {
let tenc = ISOBoxer.createFullBox('tenc', schi);
tenc.flags = 0;
tenc.version = 0;
tenc.default_IsEncrypted = 0x1;
tenc.default_IV_size = 8;
tenc.default_KID = (contentProtection && (contentProtection.length) > 0 && contentProtection[0]['cenc:default_KID']) ?
contentProtection[0]['cenc:default_KID'] : [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0];
}
function createTrexBox(moov) {
let trex = ISOBoxer.createFullBox('trex', moov);
trex.track_ID = trackId;
trex.default_sample_description_index = 1;
trex.default_sample_duration = 0;
trex.default_sample_size = 0;
trex.default_sample_flags = 0;
return trex;
}
function hexStringtoBuffer(str) {
let buf = new Uint8Array(str.length / 2);
let i;
for (i = 0; i < str.length / 2; i += 1) {
buf[i] = parseInt('' + str[i * 2] + str[i * 2 + 1], 16);
}
return buf;
}
function stringToCharCode(str) {
let code = 0;
let i;
for (i = 0; i < str.length; i += 1) {
code |= str.charCodeAt(i) << ((str.length - i - 1) * 8);
}
return code;
}
function generateMoov(rep) {
if (!rep || !rep.adaptation) {
return;
}
let isoFile,
arrayBuffer;
representation = rep;
adaptationSet = representation.adaptation;
period = adaptationSet.period;
trackId = adaptationSet.index + 1;
contentProtection = period.mpd.manifest.Period_asArray[period.index].AdaptationSet_asArray[adaptationSet.index].ContentProtection;
timescale = period.mpd.manifest.Period_asArray[period.index].AdaptationSet_asArray[adaptationSet.index].SegmentTemplate.timescale;
isoFile = ISOBoxer.createFile();
createFtypBox(isoFile);
createMoovBox(isoFile);
arrayBuffer = isoFile.write();
return arrayBuffer;
}
instance = {
generateMoov: generateMoov
};
return instance;
}
MssFragmentMoovProcessor.__dashjs_factory_name = 'MssFragmentMoovProcessor';
export default dashjs.FactoryMaker.getClassFactory(MssFragmentMoovProcessor); /* jshint ignore:line */