UNPKG

rx-player

Version:
292 lines (291 loc) 14.2 kB
/** * Copyright 2015 CANAL+ Group * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import config from "../../../../config"; import log from "../../../../log"; import arrayFind from "../../../../utils/array_find"; import isNullOrUndefined from "../../../../utils/is_null_or_undefined"; import getMonotonicTimeStamp from "../../../../utils/monotonic_timestamp"; import { getFilenameIndexInUrl } from "../../../../utils/url-utils"; import ContentProtectionParser from "./content_protection_parser"; import getClockOffset from "./get_clock_offset"; import getHTTPUTCTimingURL from "./get_http_utc-timing_url"; import getMinimumAndMaximumPositions from "./get_minimum_and_maximum_positions"; import ManifestBoundsCalculator from "./manifest_bounds_calculator"; import parseAvailabilityStartTime from "./parse_availability_start_time"; import parsePeriods from "./parse_periods"; import resolveBaseURLs from "./resolve_base_urls"; /** * Checks if xlinks needs to be loaded before actually parsing the manifest. * @param {Object} mpdIR * @param {Object} args * @param {boolean} hasLoadedClock * @param {Array.<Object>} warnings * @returns {Object} */ export default function parseMpdIr(mpdIR, args, warnings, hasLoadedClock, xlinkInfos = new WeakMap()) { const { children: rootChildren, attributes: rootAttributes } = mpdIR; if (isNullOrUndefined(args.externalClockOffset)) { const isDynamic = rootAttributes.type === "dynamic"; const directTiming = arrayFind(rootChildren.utcTimings, (utcTiming) => { return (utcTiming.schemeIdUri === "urn:mpeg:dash:utc:direct:2014" && !isNullOrUndefined(utcTiming.value)); }); const clockOffsetFromDirectUTCTiming = !isNullOrUndefined(directTiming) && !isNullOrUndefined(directTiming.value) ? getClockOffset(directTiming.value) : undefined; const clockOffset = !isNullOrUndefined(clockOffsetFromDirectUTCTiming) && !isNaN(clockOffsetFromDirectUTCTiming) ? clockOffsetFromDirectUTCTiming : undefined; if (!isNullOrUndefined(clockOffset) && hasLoadedClock !== true) { args.externalClockOffset = clockOffset; } else if (isDynamic && hasLoadedClock !== true) { const UTCTimingHTTPURL = getHTTPUTCTimingURL(mpdIR); if (!isNullOrUndefined(UTCTimingHTTPURL) && UTCTimingHTTPURL.length > 0) { // TODO fetch UTCTiming and XLinks at the same time return { type: "needs-clock", value: { url: UTCTimingHTTPURL, continue: function continueParsingMPD(responseDataClock) { if (!responseDataClock.success) { warnings.push(responseDataClock.error); log.warn("dash", "Error on fetching the clock ressource", responseDataClock.error); return parseMpdIr(mpdIR, args, warnings, true); } args.externalClockOffset = getClockOffset(responseDataClock.data); return parseMpdIr(mpdIR, args, warnings, true); }, }, }; } } } const xlinksToLoad = []; for (let i = 0; i < rootChildren.periods.length; i++) { const { xlinkHref, xlinkActuate } = rootChildren.periods[i].attributes; if (!isNullOrUndefined(xlinkHref) && xlinkActuate === "onLoad") { xlinksToLoad.push({ index: i, ressource: xlinkHref }); } } if (xlinksToLoad.length === 0) { return parseCompleteIntermediateRepresentation(mpdIR, args, warnings, xlinkInfos); } return { type: "needs-xlinks", value: { xlinksUrls: xlinksToLoad.map(({ ressource }) => ressource), continue: function continueParsingMPD(loadedRessources) { if (loadedRessources.length !== xlinksToLoad.length) { throw new Error("DASH parser: wrong number of loaded ressources."); } // Note: It is important to go from the last index to the first index in // the resulting array, as we will potentially add elements to the array for (let i = loadedRessources.length - 1; i >= 0; i--) { const index = xlinksToLoad[i].index; const { parsed: periodsIR, warnings: parsingWarnings, receivedTime, sendingTime, url, } = loadedRessources[i]; if (parsingWarnings.length > 0) { warnings.push(...parsingWarnings); } for (const periodIR of periodsIR) { xlinkInfos.set(periodIR, { receivedTime, sendingTime, url }); } // replace original "xlinked" periods by the real deal rootChildren.periods.splice(index, 1, ...periodsIR); } return parseMpdIr(mpdIR, args, warnings, hasLoadedClock, xlinkInfos); }, }, }; } /** * Parse the MPD intermediate representation into a regular Manifest. * @param {Object} mpdIR * @param {Object} args * @param {Array.<Object>} warnings * @param {Object} xlinkInfos * @returns {Object} */ function parseCompleteIntermediateRepresentation(mpdIR, args, warnings, xlinkInfos) { var _a, _b, _c, _d, _e; const { children: rootChildren, attributes: rootAttributes } = mpdIR; const isDynamic = rootAttributes.type === "dynamic"; const initialBaseUrl = args.url !== undefined ? [{ url: args.url.substring(0, getFilenameIndexInUrl(args.url)) }] : []; const mpdBaseUrls = resolveBaseURLs(initialBaseUrl, rootChildren.baseURLs); const availabilityStartTime = parseAvailabilityStartTime(rootAttributes, args.referenceDateTime); const timeShiftBufferDepth = rootAttributes.timeShiftBufferDepth; const maxSegmentDuration = rootAttributes.maxSegmentDuration; const { externalClockOffset: clockOffset, unsafelyBaseOnPreviousManifest } = args; const { externalClockOffset } = args; const manifestBoundsCalculator = new ManifestBoundsCalculator({ availabilityStartTime, isDynamic, timeShiftBufferDepth, serverTimestampOffset: externalClockOffset, }); const contentProtectionParser = new ContentProtectionParser(); contentProtectionParser.addReferences((_a = rootChildren.contentProtections) !== null && _a !== void 0 ? _a : []); const manifestInfos = { availabilityStartTime, baseURLs: mpdBaseUrls, clockOffset, contentProtectionParser, duration: rootAttributes.duration, isDynamic, manifestBoundsCalculator, manifestProfiles: mpdIR.attributes.profiles, receivedTime: args.manifestReceivedTime, unsafelyBaseOnPreviousManifest, xlinkInfos, xmlNamespaces: mpdIR.attributes.namespaces, }; const parsedPeriods = parsePeriods(rootChildren.periods, manifestInfos); contentProtectionParser.finalize(); const mediaPresentationDuration = rootAttributes.duration; let lifetime; let minimumTime; let timeshiftDepth = null; let maximumTimeData; if (rootAttributes.minimumUpdatePeriod !== undefined && rootAttributes.minimumUpdatePeriod >= 0) { lifetime = rootAttributes.minimumUpdatePeriod === 0 ? config.getCurrent().DASH_FALLBACK_LIFETIME_WHEN_MINIMUM_UPDATE_PERIOD_EQUAL_0 : rootAttributes.minimumUpdatePeriod; } const { minimumSafePosition, maximumSafePosition, maximumUnsafePosition } = getMinimumAndMaximumPositions(parsedPeriods); const now = getMonotonicTimeStamp(); if (!isDynamic) { minimumTime = minimumSafePosition; if (minimumTime === undefined) { minimumTime = (_c = (_b = parsedPeriods[0]) === null || _b === void 0 ? void 0 : _b.start) !== null && _c !== void 0 ? _c : 0; } let finalMaximumSafePosition = mediaPresentationDuration !== null && mediaPresentationDuration !== void 0 ? mediaPresentationDuration : Infinity; if (parsedPeriods[parsedPeriods.length - 1] !== undefined) { const lastPeriod = parsedPeriods[parsedPeriods.length - 1]; const lastPeriodEnd = (_d = lastPeriod.end) !== null && _d !== void 0 ? _d : (lastPeriod.duration !== undefined ? lastPeriod.start + lastPeriod.duration : undefined); if (lastPeriodEnd !== undefined && lastPeriodEnd < finalMaximumSafePosition) { finalMaximumSafePosition = lastPeriodEnd; } } if (maximumSafePosition !== undefined && maximumSafePosition < finalMaximumSafePosition) { finalMaximumSafePosition = maximumSafePosition; } maximumTimeData = { isLinear: false, maximumSafePosition: finalMaximumSafePosition, livePosition: undefined, time: now, }; } else { // Determine the maximum seekable position let finalMaximumSafePosition; if (maximumSafePosition !== undefined) { finalMaximumSafePosition = maximumSafePosition; } else { if (externalClockOffset === undefined) { log.warn("dash", "use system clock to define maximum position"); finalMaximumSafePosition = Date.now() / 1000 - availabilityStartTime; } else { const serverTime = getMonotonicTimeStamp() + externalClockOffset; finalMaximumSafePosition = serverTime / 1000 - availabilityStartTime; } } // Determine live edge (what position corresponds to live content, can be // inferior or superior to the maximum anounced position in some specific // scenarios). However, the `timeShiftBufferDepth` should be based on it. let livePosition = manifestBoundsCalculator.getEstimatedLiveEdge(); if (livePosition === undefined) { if (maximumUnsafePosition !== undefined) { livePosition = maximumUnsafePosition; } else { livePosition = finalMaximumSafePosition; } // manifestBoundsCalculator.forceLiveEdge(livePosition); } maximumTimeData = { isLinear: true, maximumSafePosition: finalMaximumSafePosition, livePosition, time: now, }; // if the minimum calculated time is even below the buffer depth, perhaps we // can go even lower in terms of depth minimumTime = minimumSafePosition; timeshiftDepth = timeShiftBufferDepth !== null && timeShiftBufferDepth !== void 0 ? timeShiftBufferDepth : null; if (timeshiftDepth !== null) { // The DASH spec implies that a segment is still available after a given // `timeShiftBufferDepth` for a time equal to its duration // (What I interpret from "ISO/IEC 23009-1 fifth edition 2022-08 // A.3.4 Media Segment list restrictions). // // This `timeshiftDepth` property is global for the whole Manifest (and // not per segment), thus we cannot do exactly that, but we can take the // anounced `maxSegmentDuration` by default instead. This may be a little // too optimistic, but would in reality not lead to a lot of issues as // this `timeshiftDepth` property is not the one that should be relied on // to know which segment can or cannot be requested anymore. timeshiftDepth += maxSegmentDuration !== null && maxSegmentDuration !== void 0 ? maxSegmentDuration : 0; } if (timeshiftDepth !== null && minimumTime !== undefined && livePosition - minimumTime > timeshiftDepth) { timeshiftDepth = livePosition - minimumTime; } } // `isLastPeriodKnown` should be `true` in two cases for DASH contents: // 1. When the content is static, because we know that no supplementary // Period will be added. // 2. If the content is dynamic, only when both the duration is known and // the `minimumUpdatePeriod` is not set. This corresponds to the case // explained in "4.6.4. Transition Phase between Live and On-Demand" of // the DASH-IF IOP v4.3 for live contents transitionning to on-demand. const isLastPeriodKnown = !isDynamic || (mpdIR.attributes.minimumUpdatePeriod === undefined && (((_e = parsedPeriods[parsedPeriods.length - 1]) === null || _e === void 0 ? void 0 : _e.end) !== undefined || mpdIR.attributes.duration !== undefined)); const parsedMPD = { availabilityStartTime, clockOffset: args.externalClockOffset, isDynamic, isLive: isDynamic, isLastPeriodKnown, periods: parsedPeriods, publishTime: rootAttributes.publishTime, suggestedPresentationDelay: rootAttributes.suggestedPresentationDelay, transportType: "dash", timeBounds: { minimumSafePosition: minimumTime, timeshiftDepth, maximumTimeData, }, lifetime, uris: isNullOrUndefined(args.url) ? rootChildren.locations : [args.url, ...rootChildren.locations], }; return { type: "done", value: { parsed: parsedMPD, warnings } }; }