UNPKG

dashjs

Version:

A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.

811 lines (674 loc) 33.7 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 MssParser * @ignore * @param {Object} config object */ function MssParser(config) { config = config || {}; const BASE64 = config.BASE64; const debug = config.debug; const constants = config.constants; const manifestModel = config.manifestModel; const DEFAULT_TIME_SCALE = 10000000.0; const SUPPORTED_CODECS = ['AAC', 'AACL', 'AVC1', 'H264', 'TTML', 'DFXP']; // MPEG-DASH Role and accessibility mapping for text tracks according to ETSI TS 103 285 v1.1.1 (section 7.1.2) const ROLE = { 'CAPT': 'main', 'SUBT': 'alternate', 'DESC': 'main' }; const ACCESSIBILITY = { 'DESC': '2' }; 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 }; const mimeTypeMap = { 'video': 'video/mp4', 'audio': 'audio/mp4', 'text': 'application/mp4' }; let instance, logger, mediaPlayerModel; function setup() { logger = debug.getLogger(instance); mediaPlayerModel = config.mediaPlayerModel; } function mapPeriod(smoothStreamingMedia, timescale) { const period = {}; let streams, adaptation; // For each StreamIndex node, create an AdaptationSet element period.AdaptationSet_asArray = []; streams = smoothStreamingMedia.getElementsByTagName('StreamIndex'); for (let i = 0; i < streams.length; i++) { adaptation = mapAdaptationSet(streams[i], timescale); if (adaptation !== null) { period.AdaptationSet_asArray.push(adaptation); } } if (period.AdaptationSet_asArray.length > 0) { period.AdaptationSet = (period.AdaptationSet_asArray.length > 1) ? period.AdaptationSet_asArray : period.AdaptationSet_asArray[0]; } return period; } function mapAdaptationSet(streamIndex, timescale) { const adaptationSet = {}; const representations = []; let segmentTemplate; let qualityLevels, representation, segments, i; adaptationSet.id = streamIndex.getAttribute('Name') ? streamIndex.getAttribute('Name') : streamIndex.getAttribute('Type'); adaptationSet.contentType = streamIndex.getAttribute('Type'); adaptationSet.lang = streamIndex.getAttribute('Language') || 'und'; adaptationSet.mimeType = mimeTypeMap[adaptationSet.contentType]; adaptationSet.subType = streamIndex.getAttribute('Subtype'); adaptationSet.maxWidth = streamIndex.getAttribute('MaxWidth'); adaptationSet.maxHeight = streamIndex.getAttribute('MaxHeight'); // Map text tracks subTypes to MPEG-DASH AdaptationSet role and accessibility (see ETSI TS 103 285 v1.1.1, section 7.1.2) if (adaptationSet.subType) { if (ROLE[adaptationSet.subType]) { let role = { schemeIdUri: 'urn:mpeg:dash:role:2011', value: ROLE[adaptationSet.subType] }; adaptationSet.Role = role; adaptationSet.Role_asArray = [role]; } if (ACCESSIBILITY[adaptationSet.subType]) { let accessibility = { schemeIdUri: 'urn:tva:metadata:cs:AudioPurposeCS:2007', value: ACCESSIBILITY[adaptationSet.subType] }; adaptationSet.Accessibility = accessibility; adaptationSet.Accessibility_asArray = [accessibility]; } } // Create a SegmentTemplate with a SegmentTimeline segmentTemplate = mapSegmentTemplate(streamIndex, timescale); qualityLevels = streamIndex.getElementsByTagName('QualityLevel'); // For each QualityLevel node, create a Representation element for (i = 0; i < qualityLevels.length; i++) { // Propagate BaseURL and mimeType qualityLevels[i].BaseURL = adaptationSet.BaseURL; qualityLevels[i].mimeType = adaptationSet.mimeType; // Set quality level id qualityLevels[i].Id = adaptationSet.id + '_' + qualityLevels[i].getAttribute('Index'); // Map Representation to QualityLevel representation = mapRepresentation(qualityLevels[i], streamIndex); if (representation !== null) { // Copy SegmentTemplate into Representation representation.SegmentTemplate = segmentTemplate; representations.push(representation); } } if (representations.length === 0) { return null; } adaptationSet.Representation = (representations.length > 1) ? representations : representations[0]; adaptationSet.Representation_asArray = representations; // Set SegmentTemplate adaptationSet.SegmentTemplate = segmentTemplate; segments = segmentTemplate.SegmentTimeline.S_asArray; return adaptationSet; } function mapRepresentation(qualityLevel, streamIndex) { const representation = {}; const type = streamIndex.getAttribute('Type'); let fourCCValue = null; representation.id = qualityLevel.Id; representation.bandwidth = parseInt(qualityLevel.getAttribute('Bitrate'), 10); representation.mimeType = qualityLevel.mimeType; representation.width = parseInt(qualityLevel.getAttribute('MaxWidth'), 10); representation.height = parseInt(qualityLevel.getAttribute('MaxHeight'), 10); fourCCValue = qualityLevel.getAttribute('FourCC'); // If FourCC not defined at QualityLevel level, then get it from StreamIndex level if (fourCCValue === null || fourCCValue === '') { fourCCValue = streamIndex.getAttribute('FourCC'); } // If still not defined (optionnal for audio stream, see https://msdn.microsoft.com/en-us/library/ff728116%28v=vs.95%29.aspx), // then we consider the stream is an audio AAC stream if (fourCCValue === null || fourCCValue === '') { if (type === 'audio') { fourCCValue = 'AAC'; } else if (type === 'video') { logger.debug('FourCC is not defined whereas it is required for a QualityLevel element for a StreamIndex of type "video"'); return null; } } // Check if codec is supported if (SUPPORTED_CODECS.indexOf(fourCCValue.toUpperCase()) === -1) { // Do not send warning logger.warn('Codec not supported: ' + fourCCValue); return null; } // Get codecs value according to FourCC field if (fourCCValue === 'H264' || fourCCValue === 'AVC1') { representation.codecs = getH264Codec(qualityLevel); } else if (fourCCValue.indexOf('AAC') >= 0) { representation.codecs = getAACCodec(qualityLevel, fourCCValue); representation.audioSamplingRate = parseInt(qualityLevel.getAttribute('SamplingRate'), 10); representation.audioChannels = parseInt(qualityLevel.getAttribute('Channels'), 10); } else if (fourCCValue.indexOf('TTML') || fourCCValue.indexOf('DFXP')) { representation.codecs = constants.STPP; } representation.codecPrivateData = '' + qualityLevel.getAttribute('CodecPrivateData'); representation.BaseURL = qualityLevel.BaseURL; return representation; } function getH264Codec(qualityLevel) { let codecPrivateData = qualityLevel.getAttribute('CodecPrivateData').toString(); let nalHeader, avcoti; // Extract from the CodecPrivateData field the hexadecimal representation of the following // three bytes in the sequence parameter set NAL unit. // => Find the SPS nal header nalHeader = /00000001[0-9]7/.exec(codecPrivateData); // => Find the 6 characters after the SPS nalHeader (if it exists) avcoti = nalHeader && nalHeader[0] ? (codecPrivateData.substr(codecPrivateData.indexOf(nalHeader[0]) + 10, 6)) : undefined; return 'avc1.' + avcoti; } function getAACCodec(qualityLevel, fourCCValue) { const samplingRate = parseInt(qualityLevel.getAttribute('SamplingRate'), 10); let codecPrivateData = qualityLevel.getAttribute('CodecPrivateData').toString(); let objectType = 0; let codecPrivateDataHex, arr16, indexFreq, extensionSamplingFrequencyIndex; //chrome problem, in implicit AAC HE definition, so when AACH is detected in FourCC //set objectType to 5 => strange, it should be 2 if (fourCCValue === 'AACH') { objectType = 0x05; } //if codecPrivateData is empty, build it : if (codecPrivateData === undefined || codecPrivateData === '') { objectType = 0x02; //AAC Main Low Complexity => object Type = 2 indexFreq = samplingFrequencyIndex[samplingRate]; if (fourCCValue === 'AACH') { // 4 bytes : XXXXX XXXX XXXX XXXX XXXXX XXX XXXXXXX // ' ObjectType' 'Freq Index' 'Channels value' 'Extens Sampl Freq' 'ObjectType' 'GAS' 'alignment = 0' objectType = 0x05; // High Efficiency AAC Profile = object Type = 5 SBR codecPrivateData = new Uint8Array(4); extensionSamplingFrequencyIndex = samplingFrequencyIndex[samplingRate * 2]; // in HE AAC Extension Sampling frequence // equals to SamplingRate*2 //Freq Index is present for 3 bits in the first byte, last bit is in the second codecPrivateData[0] = (objectType << 3) | (indexFreq >> 1); codecPrivateData[1] = (indexFreq << 7) | (qualityLevel.Channels << 3) | (extensionSamplingFrequencyIndex >> 1); codecPrivateData[2] = (extensionSamplingFrequencyIndex << 7) | (0x02 << 2); // origin object type equals to 2 => AAC Main Low Complexity codecPrivateData[3] = 0x0; //alignment bits arr16 = new Uint16Array(2); arr16[0] = (codecPrivateData[0] << 8) + codecPrivateData[1]; arr16[1] = (codecPrivateData[2] << 8) + codecPrivateData[3]; //convert decimal to hex value codecPrivateDataHex = arr16[0].toString(16); codecPrivateDataHex = arr16[0].toString(16) + arr16[1].toString(16); } else { // 2 bytes : XXXXX XXXX XXXX XXX // ' ObjectType' 'Freq Index' 'Channels value' 'GAS = 000' codecPrivateData = new Uint8Array(2); //Freq Index is present for 3 bits in the first byte, last bit is in the second codecPrivateData[0] = (objectType << 3) | (indexFreq >> 1); codecPrivateData[1] = (indexFreq << 7) | (parseInt(qualityLevel.getAttribute('Channels'), 10) << 3); // put the 2 bytes in an 16 bits array arr16 = new Uint16Array(1); arr16[0] = (codecPrivateData[0] << 8) + codecPrivateData[1]; //convert decimal to hex value codecPrivateDataHex = arr16[0].toString(16); } codecPrivateData = '' + codecPrivateDataHex; codecPrivateData = codecPrivateData.toUpperCase(); qualityLevel.setAttribute('CodecPrivateData', codecPrivateData); } else if (objectType === 0) { objectType = (parseInt(codecPrivateData.substr(0, 2), 16) & 0xF8) >> 3; } return 'mp4a.40.' + objectType; } function mapSegmentTemplate(streamIndex, timescale) { const segmentTemplate = {}; let mediaUrl, streamIndexTimeScale, url; url = streamIndex.getAttribute('Url'); mediaUrl = url ? url.replace('{bitrate}', '$Bandwidth$') : null; mediaUrl = mediaUrl ? mediaUrl.replace('{start time}', '$Time$') : null; streamIndexTimeScale = streamIndex.getAttribute('TimeScale'); streamIndexTimeScale = streamIndexTimeScale ? parseFloat(streamIndexTimeScale) : timescale; segmentTemplate.media = mediaUrl; segmentTemplate.timescale = streamIndexTimeScale; segmentTemplate.SegmentTimeline = mapSegmentTimeline(streamIndex, segmentTemplate.timescale); return segmentTemplate; } function mapSegmentTimeline(streamIndex, timescale) { const segmentTimeline = {}; const chunks = streamIndex.getElementsByTagName('c'); const segments = []; let segment, prevSegment, tManifest, i,j,r; let duration = 0; for (i = 0; i < chunks.length; i++) { segment = {}; // Get time 't' attribute value tManifest = chunks[i].getAttribute('t'); // => segment.tManifest = original timestamp value as a string (for constructing the fragment request url, see DashHandler) // => segment.t = number value of timestamp (maybe rounded value, but only for 0.1 microsecond) segment.tManifest = parseFloat(tManifest); segment.t = parseFloat(tManifest); // Get duration 'd' attribute value segment.d = parseFloat(chunks[i].getAttribute('d')); // If 't' not defined for first segment then t=0 if ((i === 0) && !segment.t) { segment.t = 0; } if (i > 0) { prevSegment = segments[segments.length - 1]; // Update previous segment duration if not defined if (!prevSegment.d) { if (prevSegment.tManifest) { prevSegment.d = parseFloat(tManifest) - parseFloat(prevSegment.tManifest); } else { prevSegment.d = segment.t - prevSegment.t; } duration += prevSegment.d; } // Set segment absolute timestamp if not set in manifest if (!segment.t) { if (prevSegment.tManifest) { segment.tManifest = parseFloat(prevSegment.tManifest) + prevSegment.d; segment.t = parseFloat(segment.tManifest); } else { segment.t = prevSegment.t + prevSegment.d; } } } if (segment.d) { duration += segment.d; } // Create new segment segments.push(segment); // Support for 'r' attribute (i.e. "repeat" as in MPEG-DASH) r = parseFloat(chunks[i].getAttribute('r')); if (r) { for (j = 0; j < (r - 1); j++) { prevSegment = segments[segments.length - 1]; segment = {}; segment.t = prevSegment.t + prevSegment.d; segment.d = prevSegment.d; if (prevSegment.tManifest) { segment.tManifest = parseFloat(prevSegment.tManifest) + prevSegment.d; } duration += segment.d; segments.push(segment); } } } segmentTimeline.S = segments; segmentTimeline.S_asArray = segments; segmentTimeline.duration = duration / timescale; return segmentTimeline; } function getKIDFromProtectionHeader(protectionHeader) { let prHeader, wrmHeader, xmlReader, KID; // Get PlayReady header as byte array (base64 decoded) prHeader = BASE64.decodeArray(protectionHeader.firstChild.data); // Get Right Management header (WRMHEADER) from PlayReady header wrmHeader = getWRMHeaderFromPRHeader(prHeader); if (wrmHeader) { // Convert from multi-byte to unicode wrmHeader = new Uint16Array(wrmHeader.buffer); // Convert to string wrmHeader = String.fromCharCode.apply(null, wrmHeader); // Parse <WRMHeader> to get KID field value xmlReader = (new DOMParser()).parseFromString(wrmHeader, 'application/xml'); KID = xmlReader.querySelector('KID').textContent; // Get KID (base64 decoded) as byte array KID = BASE64.decodeArray(KID); // Convert UUID from little-endian to big-endian convertUuidEndianness(KID); } return KID; } function getWRMHeaderFromPRHeader(prHeader) { let length, recordCount, recordType, recordLength, recordValue; let i = 0; // Parse PlayReady header // Length - 32 bits (LE format) length = (prHeader[i + 3] << 24) + (prHeader[i + 2] << 16) + (prHeader[i + 1] << 8) + prHeader[i]; i += 4; // Record count - 16 bits (LE format) recordCount = (prHeader[i + 1] << 8) + prHeader[i]; i += 2; // Parse records while (i < prHeader.length) { // Record type - 16 bits (LE format) recordType = (prHeader[i + 1] << 8) + prHeader[i]; i += 2; // Check if Rights Management header (record type = 0x01) if (recordType === 0x01) { // Record length - 16 bits (LE format) recordLength = (prHeader[i + 1] << 8) + prHeader[i]; i += 2; // Record value => contains <WRMHEADER> recordValue = new Uint8Array(recordLength); recordValue.set(prHeader.subarray(i, i + recordLength)); return recordValue; } } return null; } function convertUuidEndianness(uuid) { swapBytes(uuid, 0, 3); swapBytes(uuid, 1, 2); swapBytes(uuid, 4, 5); swapBytes(uuid, 6, 7); } function swapBytes(bytes, pos1, pos2) { const temp = bytes[pos1]; bytes[pos1] = bytes[pos2]; bytes[pos2] = temp; } function createPRContentProtection(protectionHeader) { let pro = { __text: protectionHeader.firstChild.data, __prefix: 'mspr' }; return { schemeIdUri: 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95', value: 'com.microsoft.playready', pro: pro, pro_asArray: pro }; } function createWidevineContentProtection(KID) { let widevineCP = { schemeIdUri: 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', value: 'com.widevine.alpha' }; if (!KID) return widevineCP; // Create Widevine CENC header (Protocol Buffer) with KID value const wvCencHeader = new Uint8Array(2 + KID.length); wvCencHeader[0] = 0x12; wvCencHeader[1] = 0x10; wvCencHeader.set(KID, 2); // Create a pssh box const length = 12 /* box length, type, version and flags */ + 16 /* SystemID */ + 4 /* data length */ + wvCencHeader.length; let pssh = new Uint8Array(length); let i = 0; // Set box length value pssh[i++] = (length & 0xFF000000) >> 24; pssh[i++] = (length & 0x00FF0000) >> 16; pssh[i++] = (length & 0x0000FF00) >> 8; pssh[i++] = (length & 0x000000FF); // Set type ('pssh'), version (0) and flags (0) pssh.set([0x70, 0x73, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00], i); i += 8; // Set SystemID ('edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') pssh.set([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed], i); i += 16; // Set data length value pssh[i++] = (wvCencHeader.length & 0xFF000000) >> 24; pssh[i++] = (wvCencHeader.length & 0x00FF0000) >> 16; pssh[i++] = (wvCencHeader.length & 0x0000FF00) >> 8; pssh[i++] = (wvCencHeader.length & 0x000000FF); // Copy Widevine CENC header pssh.set(wvCencHeader, i); // Convert to BASE64 string pssh = String.fromCharCode.apply(null, pssh); pssh = BASE64.encodeASCII(pssh); widevineCP.pssh = { __text: pssh }; return widevineCP; } function processManifest(xmlDoc, manifestLoadedTime) { const manifest = {}; const contentProtections = []; const smoothStreamingMedia = xmlDoc.getElementsByTagName('SmoothStreamingMedia')[0]; const protection = xmlDoc.getElementsByTagName('Protection')[0]; let protectionHeader = null; let period, adaptations, contentProtection, KID, timestampOffset, startTime, segments, timescale, i, j; // Set manifest node properties manifest.protocol = 'MSS'; manifest.profiles = 'urn:mpeg:dash:profile:isoff-live:2011'; manifest.type = smoothStreamingMedia.getAttribute('IsLive') === 'TRUE' ? 'dynamic' : 'static'; timescale = smoothStreamingMedia.getAttribute('TimeScale'); manifest.timescale = timescale ? parseFloat(timescale) : DEFAULT_TIME_SCALE; let dvrWindowLength = parseFloat(smoothStreamingMedia.getAttribute('DVRWindowLength')); if (dvrWindowLength === 0 && smoothStreamingMedia.getAttribute('CanSeek') === 'TRUE') { dvrWindowLength = Infinity; } if (dvrWindowLength > 0) { manifest.timeShiftBufferDepth = dvrWindowLength / manifest.timescale; } let duration = parseFloat(smoothStreamingMedia.getAttribute('Duration')); manifest.mediaPresentationDuration = (duration === 0) ? Infinity : duration / manifest.timescale; // By default, set minBufferTime to 2 sec. (but set below according to video segment duration) manifest.minBufferTime = 2; manifest.ttmlTimeIsRelative = true; // Live manifest with Duration = start-over if (manifest.type === 'dynamic' && duration > 0) { manifest.type = 'static'; // We set timeShiftBufferDepth to initial duration, to be used by MssFragmentController to update segment timeline manifest.timeShiftBufferDepth = duration / manifest.timescale; // Duration will be set according to current segment timeline duration (see below) } if (manifest.type === 'dynamic' && manifest.timeShiftBufferDepth < Infinity) { manifest.refreshManifestOnSwitchTrack = true; // Refresh manifest when switching tracks manifest.doNotUpdateDVRWindowOnBufferUpdated = true; // DVRWindow is update by MssFragmentMoofPocessor based on tfrf boxes manifest.ignorePostponeTimePeriod = true; // Never update manifest } // Map period node to manifest root node manifest.Period = mapPeriod(smoothStreamingMedia, manifest.timescale); manifest.Period_asArray = [manifest.Period]; // Initialize period start time period = manifest.Period; period.start = 0; // Uncomment to test live to static manifests // if (manifest.type !== 'static') { // manifest.type = 'static'; // manifest.mediaPresentationDuration = manifest.timeShiftBufferDepth; // manifest.timeShiftBufferDepth = null; // } // ContentProtection node if (protection !== undefined) { protectionHeader = xmlDoc.getElementsByTagName('ProtectionHeader')[0]; // Some packagers put newlines into the ProtectionHeader base64 string, which is not good // because this cannot be correctly parsed. Let's just filter out any newlines found in there. protectionHeader.firstChild.data = protectionHeader.firstChild.data.replace(/\n|\r/g, ''); // Get KID (in CENC format) from protection header KID = getKIDFromProtectionHeader(protectionHeader); // Create ContentProtection for PlayReady contentProtection = createPRContentProtection(protectionHeader); contentProtection['cenc:default_KID'] = KID; contentProtections.push(contentProtection); // Create ContentProtection for Widevine (as a CENC protection) contentProtection = createWidevineContentProtection(KID); contentProtection['cenc:default_KID'] = KID; contentProtections.push(contentProtection); manifest.ContentProtection = contentProtections; manifest.ContentProtection_asArray = contentProtections; } adaptations = period.AdaptationSet_asArray; for (i = 0; i < adaptations.length; i += 1) { adaptations[i].SegmentTemplate.initialization = '$Bandwidth$'; // Propagate content protection information into each adaptation if (manifest.ContentProtection !== undefined) { adaptations[i].ContentProtection = manifest.ContentProtection; adaptations[i].ContentProtection_asArray = manifest.ContentProtection_asArray; } if (adaptations[i].contentType === 'video') { // Set minBufferTime manifest.minBufferTime = adaptations[i].SegmentTemplate.SegmentTimeline.S_asArray[0].d / adaptations[i].SegmentTemplate.timescale * 2; if (manifest.type === 'dynamic' ) { // Set availabilityStartTime segments = adaptations[i].SegmentTemplate.SegmentTimeline.S_asArray; let endTime = (segments[segments.length - 1].t + segments[segments.length - 1].d) / adaptations[i].SegmentTemplate.timescale * 1000; manifest.availabilityStartTime = new Date(manifestLoadedTime.getTime() - endTime); // Match timeShiftBufferDepth to video segment timeline duration if (manifest.timeShiftBufferDepth > 0 && manifest.timeShiftBufferDepth !== Infinity && manifest.timeShiftBufferDepth > adaptations[i].SegmentTemplate.SegmentTimeline.duration) { manifest.timeShiftBufferDepth = adaptations[i].SegmentTemplate.SegmentTimeline.duration; } } } } if (manifest.timeShiftBufferDepth < manifest.minBufferTime) { manifest.minBufferTime = manifest.timeShiftBufferDepth; } // Delete Content Protection under root manifest node delete manifest.ContentProtection; delete manifest.ContentProtection_asArray; // In case of VOD streams, check if start time is greater than 0 // Then determine timestamp offset according to higher audio/video start time // (use case = live stream delinearization) if (manifest.type === 'static') { // In case of start-over stream and manifest reloading (due to track switch) // we consider previous timestampOffset to keep timelines synchronized var prevManifest = manifestModel.getValue(); if (prevManifest && prevManifest.timestampOffset) { timestampOffset = prevManifest.timestampOffset; } else { for (i = 0; i < adaptations.length; i++) { if (adaptations[i].contentType === 'audio' || adaptations[i].contentType === 'video') { segments = adaptations[i].SegmentTemplate.SegmentTimeline.S_asArray; startTime = segments[0].t / adaptations[i].SegmentTemplate.timescale; if (timestampOffset === undefined) { timestampOffset = startTime; } timestampOffset = Math.min(timestampOffset, startTime); // Correct content duration according to minimum adaptation's segment timeline duration // in order to force <video> element sending 'ended' event manifest.mediaPresentationDuration = Math.min(manifest.mediaPresentationDuration, adaptations[i].SegmentTemplate.SegmentTimeline.duration); } } } // Patch segment templates timestamps and determine period start time (since audio/video should not be aligned to 0) if (timestampOffset > 0) { manifest.timestampOffset = timestampOffset; for (i = 0; i < adaptations.length; i++) { segments = adaptations[i].SegmentTemplate.SegmentTimeline.S_asArray; for (j = 0; j < segments.length; j++) { if (!segments[j].tManifest) { segments[j].tManifest = segments[j].t; } segments[j].t -= (timestampOffset * adaptations[i].SegmentTemplate.timescale); } if (adaptations[i].contentType === 'audio' || adaptations[i].contentType === 'video') { period.start = Math.max(segments[0].t, period.start); adaptations[i].SegmentTemplate.presentationTimeOffset = period.start; } } period.start /= manifest.timescale; } } // Floor the duration to get around precision differences between segments timestamps and MSE buffer timestamps // and then avoid 'ended' event not being raised manifest.mediaPresentationDuration = Math.floor(manifest.mediaPresentationDuration * 1000) / 1000; period.duration = manifest.mediaPresentationDuration; return manifest; } function parseDOM(data) { let xmlDoc = null; if (window.DOMParser) { const parser = new window.DOMParser(); xmlDoc = parser.parseFromString(data, 'text/xml'); if (xmlDoc.getElementsByTagName('parsererror').length > 0) { throw new Error('parsing the manifest failed'); } } return xmlDoc; } function getMatchers() { return null; } function getIron() { return null; } function internalParse(data) { let xmlDoc = null; let manifest = null; const startTime = window.performance.now(); // Parse the MSS XML manifest xmlDoc = parseDOM(data); const xmlParseTime = window.performance.now(); if (xmlDoc === null) { return null; } // Convert MSS manifest into DASH manifest manifest = processManifest(xmlDoc, new Date()); const mss2dashTime = window.performance.now(); logger.info('Parsing complete: (xmlParsing: ' + (xmlParseTime - startTime).toPrecision(3) + 'ms, mss2dash: ' + (mss2dashTime - xmlParseTime).toPrecision(3) + 'ms, total: ' + ((mss2dashTime - startTime) / 1000).toPrecision(3) + 's)'); return manifest; } instance = { parse: internalParse, getMatchers: getMatchers, getIron: getIron }; setup(); return instance; } MssParser.__dashjs_factory_name = 'MssParser'; export default dashjs.FactoryMaker.getClassFactory(MssParser); /* jshint ignore:line */