UNPKG

dashjs

Version:

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

340 lines (291 loc) 14 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. */ import DashJSError from '../streaming/vo/DashJSError'; import MssErrors from './errors/MssErrors'; import Events from '../streaming/MediaPlayerEvents'; /** * @module MssFragmentMoofProcessor * @ignore * @param {Object} config object */ function MssFragmentMoofProcessor(config) { config = config || {}; let instance, type, logger; const dashMetrics = config.dashMetrics; const playbackController = config.playbackController; const errorHandler = config.errHandler; const eventBus = config.eventBus; const ISOBoxer = config.ISOBoxer; const debug = config.debug; function setup() { logger = debug.getLogger(instance); type = ''; } function processTfrf(request, tfrf, tfdt, streamProcessor) { const representationController = streamProcessor.getRepresentationController(); const representation = representationController.getCurrentRepresentation(); const indexHandler = streamProcessor.getIndexHandler(); const manifest = representation.adaptation.period.mpd.manifest; const adaptation = manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index]; const timescale = adaptation.SegmentTemplate.timescale; type = streamProcessor.getType(); if (manifest.type !== 'dynamic' && !manifest.timeShiftBufferDepth) { return; } if (!tfrf) { errorHandler.error(new DashJSError(MssErrors.MSS_NO_TFRF_CODE, MssErrors.MSS_NO_TFRF_MESSAGE)); return; } // Get adaptation's segment timeline (always a SegmentTimeline in Smooth Streaming use case) const segments = adaptation.SegmentTemplate.SegmentTimeline.S; const entries = tfrf.entry; let entry, segmentTime, range; let segment = null; let t = 0; let availabilityStartTime = null; if (entries.length === 0) { return; } // Consider only first tfrf entry (to avoid pre-condition failure on fragment info requests) entry = entries[0]; // In case of start-over streams, check if we have reached end of original manifest duration (set in timeShiftBufferDepth) // => then do not update anymore timeline if (manifest.type === 'static') { // Get first segment time segmentTime = segments[0].tManifest ? parseFloat(segments[0].tManifest) : segments[0].t; if (entry.fragment_absolute_time > (segmentTime + (manifest.timeShiftBufferDepth * timescale))) { return; } } logger.debug('entry - t = ', (entry.fragment_absolute_time / timescale)); // Get last segment time segmentTime = segments[segments.length - 1].tManifest ? parseFloat(segments[segments.length - 1].tManifest) : segments[segments.length - 1].t; logger.debug('Last segment - t = ', (segmentTime / timescale)); // Check if we have to append new segment to timeline if (entry.fragment_absolute_time <= segmentTime) { // Update DVR window range // => set range end to end time of current segment range = { start: segments[0].t / timescale, end: (tfdt.baseMediaDecodeTime / timescale) + request.duration }; updateDVR(request.mediaType, range, streamProcessor.getStreamInfo().manifestInfo); return; } logger.debug('Add new segment - t = ', (entry.fragment_absolute_time / timescale)); segment = {}; segment.t = entry.fragment_absolute_time; segment.d = entry.fragment_duration; // If timestamps starts at 0 relative to 1st segment (dynamic to static) then update segment time if (segments[0].tManifest) { segment.t -= parseFloat(segments[0].tManifest) - segments[0].t; segment.tManifest = entry.fragment_absolute_time; } segments.push(segment); // In case of static start-over streams, update content duration if (manifest.type === 'static') { if (type === 'video') { segment = segments[segments.length - 1]; var end = (segment.t + segment.d) / timescale; if (end > representation.adaptation.period.duration) { eventBus.trigger(Events.MANIFEST_VALIDITY_CHANGED, { sender: this, newDuration: end }); } } return; } // In case of live streams, update segment timeline according to DVR window else if (manifest.timeShiftBufferDepth && manifest.timeShiftBufferDepth > 0) { // Get timestamp of the last segment segment = segments[segments.length - 1]; t = segment.t; // Determine the segments' availability start time availabilityStartTime = Math.round((t - (manifest.timeShiftBufferDepth * timescale)) / timescale); // Remove segments prior to availability start time segment = segments[0]; while (Math.round(segment.t / timescale) < availabilityStartTime) { logger.debug('Remove segment - t = ' + (segment.t / timescale)); segments.splice(0, 1); segment = segments[0]; } // Update DVR window range => set range end to end time of current segment range = { start: segments[0].t / timescale, end: (tfdt.baseMediaDecodeTime / timescale) + request.duration }; updateDVR(type, range, streamProcessor.getStreamInfo().manifestInfo); } indexHandler.updateRepresentation(representation, true); } function updateDVR(type, range, manifestInfo) { const dvrInfos = dashMetrics.getCurrentDVRInfo(type); if (!dvrInfos || (range.end > dvrInfos.range.end)) { logger.debug('Update DVR Infos [' + range.start + ' - ' + range.end + ']'); dashMetrics.addDVRInfo(type, playbackController.getTime(), manifestInfo, range); } } // This function returns the offset of the 1st byte of a child box within a container box function getBoxOffset(parent, type) { let offset = 8; let i = 0; for (i = 0; i < parent.boxes.length; i++) { if (parent.boxes[i].type === type) { return offset; } offset += parent.boxes[i].size; } return offset; } function convertFragment(e, sp) { let i; // e.request contains request description object // e.response contains fragment bytes const isoFile = ISOBoxer.parseBuffer(e.response); // Update track_Id in tfhd box const tfhd = isoFile.fetch('tfhd'); tfhd.track_ID = e.request.mediaInfo.index + 1; // Add tfdt box let tfdt = isoFile.fetch('tfdt'); const traf = isoFile.fetch('traf'); if (tfdt === null) { tfdt = ISOBoxer.createFullBox('tfdt', traf, tfhd); tfdt.version = 1; tfdt.flags = 0; tfdt.baseMediaDecodeTime = Math.floor(e.request.startTime * e.request.timescale); } const trun = isoFile.fetch('trun'); // Process tfxd boxes // This box provide absolute timestamp but we take the segment start time for tfdt let tfxd = isoFile.fetch('tfxd'); if (tfxd) { tfxd._parent.boxes.splice(tfxd._parent.boxes.indexOf(tfxd), 1); tfxd = null; } let tfrf = isoFile.fetch('tfrf'); processTfrf(e.request, tfrf, tfdt, sp); if (tfrf) { tfrf._parent.boxes.splice(tfrf._parent.boxes.indexOf(tfrf), 1); tfrf = null; } // If protected content in PIFF1.1 format (sepiff box = Sample Encryption PIFF) // => convert sepiff box it into a senc box // => create saio and saiz boxes (if not already present) const sepiff = isoFile.fetch('sepiff'); if (sepiff !== null) { sepiff.type = 'senc'; sepiff.usertype = undefined; let saio = isoFile.fetch('saio'); if (saio === null) { // Create Sample Auxiliary Information Offsets Box box (saio) saio = ISOBoxer.createFullBox('saio', traf); saio.version = 0; saio.flags = 0; saio.entry_count = 1; saio.offset = [0]; const saiz = ISOBoxer.createFullBox('saiz', traf); saiz.version = 0; saiz.flags = 0; saiz.sample_count = sepiff.sample_count; saiz.default_sample_info_size = 0; saiz.sample_info_size = []; if (sepiff.flags & 0x02) { // Sub-sample encryption => set sample_info_size for each sample for (i = 0; i < sepiff.sample_count; i += 1) { // 10 = 8 (InitializationVector field size) + 2 (subsample_count field size) // 6 = 2 (BytesOfClearData field size) + 4 (BytesOfEncryptedData field size) saiz.sample_info_size[i] = 10 + (6 * sepiff.entry[i].NumberOfEntries); } } else { // No sub-sample encryption => set default sample_info_size = InitializationVector field size (8) saiz.default_sample_info_size = 8; } } } tfhd.flags &= 0xFFFFFE; // set tfhd.base-data-offset-present to false tfhd.flags |= 0x020000; // set tfhd.default-base-is-moof to true trun.flags |= 0x000001; // set trun.data-offset-present to true // Update trun.data_offset field that corresponds to first data byte (inside mdat box) const moof = isoFile.fetch('moof'); let length = moof.getLength(); trun.data_offset = length + 8; // Update saio box offset field according to new senc box offset let saio = isoFile.fetch('saio'); if (saio !== null) { let trafPosInMoof = getBoxOffset(moof, 'traf'); let sencPosInTraf = getBoxOffset(traf, 'senc'); // Set offset from begin fragment to the first IV field in senc box saio.offset[0] = trafPosInMoof + sencPosInTraf + 16; // 16 = box header (12) + sample_count field size (4) } // Write transformed/processed fragment into request reponse data e.response = isoFile.write(); } function updateSegmentList(e, sp) { // e.request contains request description object // e.response contains fragment bytes if (!e.response) { throw new Error('e.response parameter is missing'); } const isoFile = ISOBoxer.parseBuffer(e.response); // Update track_Id in tfhd box const tfhd = isoFile.fetch('tfhd'); tfhd.track_ID = e.request.mediaInfo.index + 1; // Add tfdt box let tfdt = isoFile.fetch('tfdt'); let traf = isoFile.fetch('traf'); if (tfdt === null) { tfdt = ISOBoxer.createFullBox('tfdt', traf, tfhd); tfdt.version = 1; tfdt.flags = 0; tfdt.baseMediaDecodeTime = Math.floor(e.request.startTime * e.request.timescale); } let tfrf = isoFile.fetch('tfrf'); processTfrf(e.request, tfrf, tfdt, sp); if (tfrf) { tfrf._parent.boxes.splice(tfrf._parent.boxes.indexOf(tfrf), 1); tfrf = null; } } function getType() { return type; } instance = { convertFragment: convertFragment, updateSegmentList: updateSegmentList, getType: getType }; setup(); return instance; } MssFragmentMoofProcessor.__dashjs_factory_name = 'MssFragmentMoofProcessor'; export default dashjs.FactoryMaker.getClassFactory(MssFragmentMoofProcessor); /* jshint ignore:line */