UNPKG

dashjs

Version:

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

439 lines (376 loc) 14.5 kB
import Events from '../core/events/Events'; import EventBus from '../core/EventBus'; import EBMLParser from '../streaming/utils/EBMLParser'; import Constants from '../streaming/constants/Constants'; import FactoryMaker from '../core/FactoryMaker'; import Debug from '../core/Debug'; import RequestModifier from '../streaming/utils/RequestModifier'; import Segment from './vo/Segment'; import { HTTPRequest } from '../streaming/vo/metrics/HTTPRequest'; import FragmentRequest from '../streaming/vo/FragmentRequest'; import HTTPLoader from '../streaming/net/HTTPLoader'; import DashJSError from '../streaming/vo/DashJSError'; import Errors from '../core/errors/Errors'; function WebmSegmentBaseLoader() { const context = this.context; const eventBus = EventBus(context).getInstance(); let instance, logger, WebM, errHandler, requestModifier, dashMetrics, mediaPlayerModel, httpLoader, baseURLController; function setup() { logger = Debug(context).getInstance().getLogger(instance); WebM = { EBML: { tag: 0x1A45DFA3, required: true }, Segment: { tag: 0x18538067, required: true, SeekHead: { tag: 0x114D9B74, required: true }, Info: { tag: 0x1549A966, required: true, TimecodeScale: { tag: 0x2AD7B1, required: true, parse: 'getMatroskaUint' }, Duration: { tag: 0x4489, required: true, parse: 'getMatroskaFloat' } }, Tracks: { tag: 0x1654AE6B, required: true }, Cues: { tag: 0x1C53BB6B, required: true, CuePoint: { tag: 0xBB, required: true, CueTime: { tag: 0xB3, required: true, parse: 'getMatroskaUint' }, CueTrackPositions: { tag: 0xB7, required: true, CueTrack: { tag: 0xF7, required: true, parse: 'getMatroskaUint' }, CueClusterPosition: { tag: 0xF1, required: true, parse: 'getMatroskaUint' } } } } }, Void: { tag: 0xEC, required: true } }; } function initialize() { requestModifier = RequestModifier(context).getInstance(); httpLoader = HTTPLoader(context).create({ errHandler: errHandler, dashMetrics: dashMetrics, mediaPlayerModel: mediaPlayerModel, requestModifier: requestModifier }); } function setConfig(config) { if (!config.baseURLController || !config.dashMetrics || !config.mediaPlayerModel || !config.errHandler) { throw new Error(Constants.MISSING_CONFIG_ERROR); } baseURLController = config.baseURLController; dashMetrics = config.dashMetrics; mediaPlayerModel = config.mediaPlayerModel; errHandler = config.errHandler; } function parseCues(ab) { let cues = []; let ebmlParser = EBMLParser(context).create({ data: ab }); let cue, cueTrack; ebmlParser.consumeTagAndSize(WebM.Segment.Cues); while (ebmlParser.moreData() && ebmlParser.consumeTagAndSize(WebM.Segment.Cues.CuePoint, true)) { cue = {}; cue.CueTime = ebmlParser.parseTag(WebM.Segment.Cues.CuePoint.CueTime); cue.CueTracks = []; while (ebmlParser.moreData() && ebmlParser.consumeTag(WebM.Segment.Cues.CuePoint.CueTrackPositions, true)) { const cueTrackPositionSize = ebmlParser.getMatroskaCodedNum(); const startPos = ebmlParser.getPos(); cueTrack = {}; cueTrack.Track = ebmlParser.parseTag(WebM.Segment.Cues.CuePoint.CueTrackPositions.CueTrack); if (cueTrack.Track === 0) { throw new Error('Cue track cannot be 0'); } cueTrack.ClusterPosition = ebmlParser.parseTag(WebM.Segment.Cues.CuePoint.CueTrackPositions.CueClusterPosition); cue.CueTracks.push(cueTrack); // we're not interested any other elements - skip remaining bytes ebmlParser.setPos(startPos + cueTrackPositionSize); } if (cue.CueTracks.length === 0) { throw new Error('Mandatory cuetrack not found'); } cues.push(cue); } if (cues.length === 0) { throw new Error('mandatory cuepoint not found'); } return cues; } function parseSegments(data, segmentStart, segmentEnd, segmentDuration) { let duration, parsed, segments, segment, i, len, start, end; parsed = parseCues(data); segments = []; // we are assuming one cue track per cue point // both duration and media range require the i + 1 segment // the final segment has to use global segment parameters for (i = 0, len = parsed.length; i < len; i += 1) { segment = new Segment(); duration = 0; if (i < parsed.length - 1) { duration = parsed[i + 1].CueTime - parsed[i].CueTime; } else { duration = segmentDuration - parsed[i].CueTime; } // 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 = parsed[i].CueTime; segment.timescale = 1000; // hardcoded for ms start = parsed[i].CueTracks[0].ClusterPosition + segmentStart; if (i < parsed.length - 1) { end = parsed[i + 1].CueTracks[0].ClusterPosition + segmentStart - 1; } else { end = segmentEnd - 1; } segment.mediaRange = start + '-' + end; segments.push(segment); } logger.debug('Parsed cues: ' + segments.length + ' cues.'); return segments; } function parseEbmlHeader(data, media, theRange, callback) { if (!data || data.byteLength === 0) { callback(null); return; } let ebmlParser = EBMLParser(context).create({ data: data }); let duration, segments, segmentEnd, segmentStart; let parts = theRange ? theRange.split('-') : null; let request = null; let info = { url: media, range: { start: parts ? parseFloat(parts[0]) : null, end: parts ? parseFloat(parts[1]) : null }, request: request }; logger.debug('Parse EBML header: ' + info.url); // skip over the header itself ebmlParser.skipOverElement(WebM.EBML); ebmlParser.consumeTag(WebM.Segment); // segments start here segmentEnd = ebmlParser.getMatroskaCodedNum(); segmentEnd += ebmlParser.getPos(); segmentStart = ebmlParser.getPos(); // skip over any top level elements to get to the segment info while (ebmlParser.moreData() && !ebmlParser.consumeTagAndSize(WebM.Segment.Info, true)) { if (!(ebmlParser.skipOverElement(WebM.Segment.SeekHead, true) || ebmlParser.skipOverElement(WebM.Segment.Tracks, true) || ebmlParser.skipOverElement(WebM.Segment.Cues, true) || ebmlParser.skipOverElement(WebM.Void, true))) { throw new Error('no valid top level element found'); } } // we only need one thing in segment info, duration while (duration === undefined) { let infoTag = ebmlParser.getMatroskaCodedNum(true); let infoElementSize = ebmlParser.getMatroskaCodedNum(); switch (infoTag) { case WebM.Segment.Info.Duration.tag: duration = ebmlParser[WebM.Segment.Info.Duration.parse](infoElementSize); break; default: ebmlParser.setPos(ebmlParser.getPos() + infoElementSize); break; } } // once we have what we need from segment info, we jump right to the // cues request = getFragmentRequest(info); const onload = function (response) { segments = parseSegments(response, segmentStart, segmentEnd, duration); callback(segments); }; const onloadend = function () { logger.error('Download Error: Cues ' + info.url); callback(null); }; httpLoader.load({ request: request, success: onload, error: onloadend }); logger.debug('Perform cues load: ' + info.url + ' bytes=' + info.range.start + '-' + info.range.end); } function checkConfig() { if (!baseURLController || !baseURLController.hasOwnProperty('resolve')) { throw new Error('setConfig function has to be called previously'); } } function loadInitialization(representation, loadingInfo) { checkConfig(); let request = null; let baseUrl = representation ? baseURLController.resolve(representation.path) : null; let media = baseUrl ? baseUrl.url : undefined; let initRange = representation ? representation.range.split('-') : null; let info = loadingInfo || { range: { start: initRange ? parseFloat(initRange[0]) : null, end: initRange ? parseFloat(initRange[1]) : null }, request: request, url: media, init: true }; logger.info('Start loading initialization.'); request = getFragmentRequest(info); const onload = function () { // 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 }); }; const onloadend = function () { eventBus.trigger(Events.INITIALIZATION_LOADED, { representation: representation }); }; httpLoader.load({ request: request, success: onload, error: onloadend }); logger.debug('Perform init load: ' + info.url); } function loadSegments(representation, type, theRange, callback) { checkConfig(); let request = null; let baseUrl = representation ? baseURLController.resolve(representation.path) : null; let media = baseUrl ? baseUrl.url : undefined; let bytesToLoad = 8192; let info = { bytesLoaded: 0, bytesToLoad: bytesToLoad, range: { start: 0, end: bytesToLoad }, request: request, url: media, init: false }; callback = !callback ? onLoaded : callback; request = getFragmentRequest(info); // first load the header, but preserve the manifest range so we can // load the cues after parsing the header // NOTE: we expect segment info to appear in the first 8192 bytes logger.debug('Parsing ebml header'); const onload = function (response) { parseEbmlHeader(response, media, theRange, function (segments) { callback(segments, representation, type); }); }; const onloadend = function () { callback(null, representation, type); }; httpLoader.load({ request: request, success: onload, error: onloadend }); } 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) }); } } function getFragmentRequest(info) { let 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; return request; } function reset() { errHandler = null; requestModifier = null; } instance = { setConfig: setConfig, initialize: initialize, loadInitialization: loadInitialization, loadSegments: loadSegments, reset: reset }; setup(); return instance; } WebmSegmentBaseLoader.__dashjs_factory_name = 'WebmSegmentBaseLoader'; export default FactoryMaker.getSingletonFactory(WebmSegmentBaseLoader);