UNPKG

dashjs

Version:

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

325 lines (278 loc) 12.4 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 RequestModifier from '../streaming/utils/RequestModifier'; import Segment from './vo/Segment'; import DashJSError from '../streaming/vo/DashJSError'; import Events from '../core/events/Events'; import EventBus from '../core/EventBus'; import BoxParser from '../streaming/utils/BoxParser'; import FactoryMaker from '../core/FactoryMaker'; import Debug from '../core/Debug'; import { HTTPRequest } from '../streaming/vo/metrics/HTTPRequest'; import FragmentRequest from '../streaming/vo/FragmentRequest'; import HTTPLoader from '../streaming/net/HTTPLoader'; import Errors from '../core/errors/Errors'; function SegmentBaseLoader() { const context = this.context; const eventBus = EventBus(context).getInstance(); let instance, logger, errHandler, boxParser, requestModifier, dashMetrics, mediaPlayerModel, httpLoader, baseURLController; function setup() { logger = Debug(context).getInstance().getLogger(instance); } function initialize() { boxParser = BoxParser(context).getInstance(); requestModifier = RequestModifier(context).getInstance(); httpLoader = HTTPLoader(context).create({ errHandler: errHandler, dashMetrics: dashMetrics, mediaPlayerModel: mediaPlayerModel, requestModifier: requestModifier }); } function setConfig(config) { if (config.baseURLController) { baseURLController = config.baseURLController; } if (config.dashMetrics) { dashMetrics = config.dashMetrics; } if (config.mediaPlayerModel) { mediaPlayerModel = config.mediaPlayerModel; } if (config.errHandler) { errHandler = config.errHandler; } } function checkConfig() { if (!baseURLController || !baseURLController.hasOwnProperty('resolve')) { throw new Error('setConfig function has to be called previously'); } } function loadInitialization(representation, loadingInfo) { checkConfig(); let initRange = null; const baseUrl = representation ? baseURLController.resolve(representation.path) : null; const info = loadingInfo || { init: true, url: baseUrl ? baseUrl.url : undefined, range: { start: 0, end: 1500 }, searching: false, bytesLoaded: 0, bytesToLoad: 1500, mediaType: representation && representation.adaptation ? representation.adaptation.type : null }; logger.debug('Start searching for initialization.'); const request = getFragmentRequest(info); const onload = function (response) { info.bytesLoaded = info.range.end; initRange = boxParser.findInitRange(response); if (initRange) { representation.range = initRange; // note that we don't explicitly set rep.initialization as this // will be computed when all BaseURLs are resolved later eventBus.trigger(Events.INITIALIZATION_LOADED, {representation: representation}); } else { info.range.end = info.bytesLoaded + info.bytesToLoad; loadInitialization(representation, info); } }; const onerror = function () { eventBus.trigger(Events.INITIALIZATION_LOADED, {representation: representation}); }; httpLoader.load({request: request, success: onload, error: onerror}); logger.debug('Perform init search: ' + info.url); } function loadSegments(representation, type, range, loadingInfo, callback) { checkConfig(); if (range && (range.start === undefined || range.end === undefined)) { const parts = range ? range.toString().split('-') : null; range = parts ? {start: parseFloat(parts[0]), end: parseFloat(parts[1])} : null; } callback = !callback ? onLoaded : callback; let isoFile = null; let sidx = null; const hasRange = !!range; const baseUrl = representation ? baseURLController.resolve(representation.path) : null; const info = { init: false, url: baseUrl ? baseUrl.url : undefined, range: hasRange ? range : { start: 0, end: 1500 }, searching: !hasRange, bytesLoaded: loadingInfo ? loadingInfo.bytesLoaded : 0, bytesToLoad: 1500, mediaType: representation && representation.adaptation ? representation.adaptation.type : null }; const request = getFragmentRequest(info); const onload = function (response) { const extraBytes = info.bytesToLoad; const loadedLength = response.byteLength; info.bytesLoaded = info.range.end - info.range.start; isoFile = boxParser.parse(response); sidx = isoFile.getBox('sidx'); if (!sidx || !sidx.isComplete) { if (sidx) { info.range.start = sidx.offset || info.range.start; info.range.end = info.range.start + (sidx.size || extraBytes); } else if (loadedLength < info.bytesLoaded) { // if we have reached a search limit or if we have reached the end of the file we have to stop trying to find sidx callback(null, representation, type); return; } else { const lastBox = isoFile.getLastBox(); if (lastBox && lastBox.size) { info.range.start = lastBox.offset + lastBox.size; info.range.end = info.range.start + extraBytes; } else { info.range.end += extraBytes; } } loadSegments(representation, type, info.range, info, callback); } else { const ref = sidx.references; let loadMultiSidx, segments; if (ref !== null && ref !== undefined && ref.length > 0) { loadMultiSidx = (ref[0].reference_type === 1); } if (loadMultiSidx) { logger.debug('Initiate multiple SIDX load.'); info.range.end = info.range.start + sidx.size; let j, len, ss, se, r; let segs = []; let count = 0; let offset = (sidx.offset || info.range.start) + sidx.size; const tmpCallback = function (result) { if (result) { segs = segs.concat(result); count++; if (count >= len) { callback(segs, representation, type); } } else { callback(null, representation, type); } }; for (j = 0, len = ref.length; j < len; j++) { ss = offset; se = offset + ref[j].referenced_size - 1; offset = offset + ref[j].referenced_size; r = {start: ss, end: se}; loadSegments(representation, null, r, info, tmpCallback); } } else { logger.debug('Parsing segments from SIDX.'); segments = getSegmentsForSidx(sidx, info); callback(segments, representation, type); } } }; const onerror = function () { callback(null, representation, type); }; httpLoader.load({request: request, success: onload, error: onerror}); logger.debug('Perform SIDX load: ' + info.url); } function reset() { httpLoader.abort(); httpLoader = null; errHandler = null; boxParser = null; requestModifier = null; } function getSegmentsForSidx(sidx, info) { const refs = sidx.references; const len = refs.length; const timescale = sidx.timescale; let time = sidx.earliest_presentation_time; let start = info.range.start + sidx.offset + sidx.first_offset + sidx.size; const segments = []; let segment, end, duration, size; for (let i = 0; i < len; i++) { duration = refs[i].subsegment_duration; size = refs[i].referenced_size; segment = new Segment(); // note that we don't explicitly set segment.media as this will be // computed when all BaseURLs are resolved later segment.duration = duration; segment.startTime = time; segment.timescale = timescale; end = start + size - 1; segment.mediaRange = start + '-' + end; segments.push(segment); time += duration; start += size; } return segments; } function getFragmentRequest(info) { if (!info.url) { return; } const request = new FragmentRequest(); request.type = info.init ? HTTPRequest.INIT_SEGMENT_TYPE : HTTPRequest.MEDIA_SEGMENT_TYPE; request.url = info.url; request.range = info.range.start + '-' + info.range.end; request.mediaType = info.mediaType; return request; } function onLoaded(segments, representation, type) { if (segments) { eventBus.trigger(Events.SEGMENTS_LOADED, {segments: segments, representation: representation, mediaType: type}); } else { eventBus.trigger(Events.SEGMENTS_LOADED, {segments: null, representation: representation, mediaType: type, error: new DashJSError(Errors.SEGMENT_BASE_LOADER_ERROR_CODE, Errors.SEGMENT_BASE_LOADER_ERROR_MESSAGE)}); } } instance = { setConfig: setConfig, initialize: initialize, loadInitialization: loadInitialization, loadSegments: loadSegments, reset: reset }; setup(); return instance; } SegmentBaseLoader.__dashjs_factory_name = 'SegmentBaseLoader'; export default FactoryMaker.getSingletonFactory(SegmentBaseLoader);