UNPKG

@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
/** * 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 */