UNPKG

rx-player

Version:
927 lines (926 loc) 45.6 kB
import config from "../../config"; import { MediaError, OtherError } from "../../errors"; import features from "../../features"; import log from "../../log"; import Manifest, { Adaptation, Period, Representation } from "../../manifest/classes"; import { ObservationPosition } from "../../playback_observer"; import CorePlaybackObserver from "../../playback_observer/core_playback_observer"; import arrayFind from "../../utils/array_find"; import assert, { assertUnreachable } from "../../utils/assert"; import isNullOrUndefined from "../../utils/is_null_or_undefined"; import { scaleTimestamp } from "../../utils/monotonic_timestamp"; import objectAssign from "../../utils/object_assign"; import SharedReference from "../../utils/reference"; import TaskCanceller from "../../utils/task_canceller"; import StreamOrchestrator from "../stream"; import ContentPreparer from "./content_preparer"; import createContentTimeBoundariesObserver from "./create_content_time_boundaries_observer"; import getBufferedDataPerMediaBuffer from "./get_buffered_data_per_media_buffer"; import getThumbnailData from "./get_thumbnail_data"; import { formatErrorForSender, synchronizeSegmentSinksOnObservation } from "./utils"; /** * Initialize a `CoreEntry`, which is the part of the RxPlayer acting as an * entry point to all its "core" code. * * Its role is to receive and react to messages coming from "main thead", which * may include loading and playing a content, and to send back messages for the main * thread. * @param {Function} setMessageReceiver - Declares the function that will * receive messages coming from the "main thread" part of the RxPlayer logic. * @param {Function} sendMessage - Function allowing to send messages to the * "main thread" part of the RxPlayer logic. * @param {Object} corePlugins - Callbacks that may have been registered by * the application if it loaded the core independantly as a worker. */ export default function initializeCoreEntry(setMessageReceiver, sendMessage, corePlugins) { const { DEFAULT_WANTED_BUFFER_AHEAD, DEFAULT_MAX_VIDEO_BUFFER_SIZE, DEFAULT_MAX_BUFFER_AHEAD, DEFAULT_MAX_BUFFER_BEHIND, } = config.getCurrent(); const refs = { wantedBufferAhead: new SharedReference(DEFAULT_WANTED_BUFFER_AHEAD), maxVideoBufferSize: new SharedReference(DEFAULT_MAX_VIDEO_BUFFER_SIZE), maxBufferAhead: new SharedReference(DEFAULT_MAX_BUFFER_AHEAD), maxBufferBehind: new SharedReference(DEFAULT_MAX_BUFFER_BEHIND), limitVideoResolution: new SharedReference({ height: undefined, width: undefined, pixelRatio: 1, }), throttleVideoBitrate: new SharedReference(Infinity), }; /** * `true` once the CoreEntry has been initialized. * Allow to enforce the fact that it is only initialized once. */ let isInitialized = false; /** * Abstraction allowing to prepare contents (fetching its manifest as * well as creating and reloading its MediaSource) for playback. * * Creating a default one which may change on initialization. */ let contentPreparer = new ContentPreparer({ hasVideo: true }); /** * Object allowing to control the lifecycle of the current content (stop/reload etc.). * `null` if there's no content loaded currently. */ let currentContentHandle = null; /** * When set, emit playback observation made on the main thread. */ let playbackObservationRef = null; setMessageReceiver((e) => { var _a, _b; log.debug("Core", "received message", { name: e.data.type }); const msg = e.data; switch (msg.type) { case "init" /* MainThreadMessageType.Init */: { assert(!isInitialized); isInitialized = true; scaleTimestamp(msg.value); updateLoggerLevel(msg.value.logLevel, msg.value.logFormat, msg.value.sendBackLogs); if (!isNullOrUndefined(msg.value.dashWasmUrl)) { const dashWasmParser = features.dashParsers.wasm; if (dashWasmParser === null) { log.error("Core", "Could not initialize DASH_WASM parser: DASH_WASM feature not added"); } else if (!dashWasmParser.isCompatible()) { log.warn("Core", "Could not initialize DASH_WASM parser: Browser not compatible"); } else { dashWasmParser .initialize({ wasmUrl: msg.value.dashWasmUrl }) .catch((err) => { const error = err instanceof Error ? err.toString() : "Unknown Error"; log.error("Core", "Could not initialize DASH_WASM parser", error); }); } } if (!msg.value.hasVideo) { contentPreparer.disposeCurrentContent("Received Init msg"); contentPreparer = new ContentPreparer({ hasVideo: msg.value.hasVideo }); } sendMessage({ type: "init-success" /* CoreMessageType.InitSuccess */, value: null }); } break; case "log-level-update" /* MainThreadMessageType.LogLevelUpdate */: updateLoggerLevel(msg.value.logLevel, msg.value.logFormat, msg.value.sendBackLogs); break; case "prepare" /* MainThreadMessageType.PrepareContent */: prepareNewContent(sendMessage, contentPreparer, msg.value, refs, corePlugins); break; case "start" /* MainThreadMessageType.StartPreparedContent */: { const preparedContent = contentPreparer.getCurrentContent(); if (msg.contentId !== (preparedContent === null || preparedContent === void 0 ? void 0 : preparedContent.contentId)) { return; } currentContentHandle === null || currentContentHandle === void 0 ? void 0 : currentContentHandle.stop(); playbackObservationRef === null || playbackObservationRef === void 0 ? void 0 : playbackObservationRef.finish(); const currentContentObservationRef = new SharedReference(objectAssign(msg.value.initialObservation, { position: new ObservationPosition(...msg.value.initialObservation.position), })); playbackObservationRef = currentContentObservationRef; currentContentHandle = loadPreparedContent(sendMessage, msg.value, contentPreparer, currentContentObservationRef, refs); break; } case "observation" /* MainThreadMessageType.PlaybackObservation */: { const currentContent = contentPreparer.getCurrentContent(); if (msg.contentId !== (currentContent === null || currentContent === void 0 ? void 0 : currentContent.contentId)) { return; } const observation = msg.value; const { buffered } = observation; const newBuffered = getBufferedDataPerMediaBuffer(currentContent.mediaSource, null); if (newBuffered.audio !== null) { buffered.audio = newBuffered.audio; } if (newBuffered.video !== null) { buffered.video = newBuffered.video; } playbackObservationRef === null || playbackObservationRef === void 0 ? void 0 : playbackObservationRef.setValue(objectAssign(observation, { position: new ObservationPosition(...msg.value.position), })); break; } case "ref-update" /* MainThreadMessageType.ReferenceUpdate */: updateCoreReference(msg, refs); break; case "stop" /* MainThreadMessageType.StopContent */: if (msg.contentId !== ((_a = contentPreparer.getCurrentContent()) === null || _a === void 0 ? void 0 : _a.contentId)) { return; } contentPreparer.disposeCurrentContent("StopContent message"); currentContentHandle === null || currentContentHandle === void 0 ? void 0 : currentContentHandle.stop(); currentContentHandle = null; playbackObservationRef === null || playbackObservationRef === void 0 ? void 0 : playbackObservationRef.finish(); playbackObservationRef = null; break; case "ms-reload" /* MainThreadMessageType.MediaSourceReload */: { const preparedContent = contentPreparer.getCurrentContent(); if (msg.mediaSourceId !== (preparedContent === null || preparedContent === void 0 ? void 0 : preparedContent.mediaSource.id)) { return; } currentContentHandle === null || currentContentHandle === void 0 ? void 0 : currentContentHandle.signalMediaSourceReload(); } break; case "sb-success" /* MainThreadMessageType.SourceBufferSuccess */: { const preparedContent = contentPreparer.getCurrentContent(); if (msg.mediaSourceId !== (preparedContent === null || preparedContent === void 0 ? void 0 : preparedContent.mediaSource.id)) { return; } const { sourceBuffers } = preparedContent.mediaSource; const sourceBuffer = arrayFind(sourceBuffers, (s) => s.type === msg.sourceBufferType); if (sourceBuffer === undefined) { log.info("Core", "Success for an unknown SourceBuffer", { sourceBufferType: msg.sourceBufferType, }); return; } if (sourceBuffer.onOperationSuccess === undefined) { log.warn("Core", "A SourceBufferInterface with MSE performed a cross-thread operation", { sourceBufferType: msg.sourceBufferType }); return; } sourceBuffer.onOperationSuccess(msg.operationId, msg.value.buffered); break; } case "sb-error" /* MainThreadMessageType.SourceBufferError */: { const preparedContent = contentPreparer.getCurrentContent(); if (msg.mediaSourceId !== (preparedContent === null || preparedContent === void 0 ? void 0 : preparedContent.mediaSource.id)) { return; } const { sourceBuffers } = preparedContent.mediaSource; const sourceBuffer = arrayFind(sourceBuffers, (s) => s.type === msg.sourceBufferType); if (sourceBuffer === undefined) { log.info("Core", "Error for an unknown SourceBuffer", { sourceBufferType: msg.sourceBufferType, }); return; } if (sourceBuffer.onOperationFailure === undefined) { log.warn("Core", "A SourceBufferInterface with MSE performed a cross-thread operation", { sourceBufferType: msg.sourceBufferType, }); return; } sourceBuffer.onOperationFailure(msg.operationId, msg.value); break; } case "media-source-ready-state-change" /* MainThreadMessageType.MediaSourceReadyStateChange */: { const preparedContent = contentPreparer.getCurrentContent(); if (msg.mediaSourceId !== (preparedContent === null || preparedContent === void 0 ? void 0 : preparedContent.mediaSource.id)) { return; } if (preparedContent.mediaSource.onMediaSourceReadyStateChanged === undefined) { log.warn("Core", "A MediaSourceInterface with MSE performed a cross-thread operation"); return; } preparedContent.mediaSource.onMediaSourceReadyStateChanged(msg.value); break; } case "decipherability-update" /* MainThreadMessageType.DecipherabilityStatusUpdate */: { if (msg.contentId !== ((_b = contentPreparer.getCurrentContent()) === null || _b === void 0 ? void 0 : _b.contentId)) { return; } const currentContent = contentPreparer.getCurrentContent(); if (currentContent === null || currentContent.manifest === null) { return; } const updates = msg.value; currentContent.manifest.updateRepresentationsDeciperability((content) => { for (const update of updates) { if (content.representation.uniqueId === update.representationUniqueId) { return update.decipherable; } } return content.representation.decipherable; }); break; } case "codec-support-update" /* MainThreadMessageType.CodecSupportUpdate */: { const preparedContent = contentPreparer.getCurrentContent(); if (preparedContent === null || preparedContent.manifest === null) { return; } const newEvaluatedCodecs = msg.value; try { const warning = preparedContent.manifest.updateCodecSupport(newEvaluatedCodecs); if (warning !== null) { sendMessage({ type: "warning" /* CoreMessageType.Warning */, contentId: preparedContent.contentId, value: formatErrorForSender(warning), }); } } catch (err) { sendMessage({ type: "error" /* CoreMessageType.Error */, contentId: preparedContent.contentId, value: formatErrorForSender(err), }); } break; } case "urls-update" /* MainThreadMessageType.ContentUrlsUpdate */: { const preparedContent = contentPreparer.getCurrentContent(); if (preparedContent === null || preparedContent.contentId !== msg.contentId) { return; } preparedContent.manifestFetcher.updateContentUrls(msg.value.urls, msg.value.refreshNow); break; } case "track-update" /* MainThreadMessageType.TrackUpdate */: { const preparedContent = contentPreparer.getCurrentContent(); if (preparedContent === null || preparedContent.contentId !== msg.contentId) { return; } preparedContent.trackChoiceSetter.setTrack(msg.value.periodId, msg.value.bufferType, msg.value.choice); break; } case "rep-update" /* MainThreadMessageType.RepresentationUpdate */: { const preparedContent = contentPreparer.getCurrentContent(); if (preparedContent === null || preparedContent.contentId !== msg.contentId) { return; } preparedContent.trackChoiceSetter.updateRepresentations(msg.value.periodId, msg.value.adaptationId, msg.value.bufferType, msg.value.choice); break; } case "add-text-success" /* MainThreadMessageType.PushTextDataSuccess */: { const preparedContent = contentPreparer.getCurrentContent(); if (preparedContent === null || preparedContent.contentId !== msg.contentId) { return; } if (preparedContent.coreTextSender === null) { log.error("Core", "Added text track but text track aren't enabled"); return; } preparedContent.coreTextSender.onPushedTrackSuccess(msg.value.ranges); break; } case "push-text-error" /* MainThreadMessageType.PushTextDataError */: { const preparedContent = contentPreparer.getCurrentContent(); if (preparedContent === null || preparedContent.contentId !== msg.contentId) { return; } if (preparedContent.coreTextSender === null) { log.error("Core", "Added text track but text track aren't enabled"); return; } preparedContent.coreTextSender.onPushedTrackError(new Error(msg.value.message)); break; } case "remove-text-success" /* MainThreadMessageType.RemoveTextDataSuccess */: { const preparedContent = contentPreparer.getCurrentContent(); if (preparedContent === null || preparedContent.contentId !== msg.contentId) { return; } if (preparedContent.coreTextSender === null) { log.error("Core", "Removed text track but text track aren't enabled"); return; } preparedContent.coreTextSender.onRemoveSuccess(msg.value.ranges); break; } case "remove-text-error" /* MainThreadMessageType.RemoveTextDataError */: { const preparedContent = contentPreparer.getCurrentContent(); if (preparedContent === null || preparedContent.contentId !== msg.contentId) { return; } if (preparedContent.coreTextSender === null) { log.error("Core", "Removed text track but text track aren't enabled"); return; } preparedContent.coreTextSender.onRemoveError(new Error(msg.value.message)); break; } case "pull-segment-sink-store-infos" /* MainThreadMessageType.PullSegmentSinkStoreInfos */: { sendSegmentSinksStoreInfos(sendMessage, contentPreparer, msg.value.requestId); break; } case "thumbnail-request" /* MainThreadMessageType.ThumbnailDataRequest */: { sendThumbnailData(sendMessage, contentPreparer, msg); break; } case "config-update" /* MainThreadMessageType.ConfigUpdate */: { config.update(msg.value); break; } case "app-defined" /* MainThreadMessageType.AppDefined */: { break; } default: assertUnreachable(msg); } }); } /** * Performs steps needed to prepare a future content to be played: * - Load its Manifest file * - Create MSE `MediaSource` for that content. * - Initialize all modules that will follow that content * - etc. * @param {Function} sendMessage - Function allowing to send messages to the * "main thread" part of the RxPlayer logic. * @param {ContentPreparer} contentPreparer - Interface allowing to setup and * prepare a content. * @param {Object} contentInitData - Configuration wanted for the content to * load. * @param {Object} refs - Collection of so-called "references": values * configuring playback that may be updated at any time and that the * CoreEntry should react on. * @param {Object} corePlugins - Callbacks that may have been registered by * the application if it loaded the core independantly as a worker. */ function prepareNewContent(sendMessage, contentPreparer, contentInitData, refs, corePlugins) { contentPreparer .initializeNewContent(sendMessage, contentInitData, { limitResolution: { video: refs.limitVideoResolution }, throttleBitrate: { video: refs.throttleVideoBitrate }, }, corePlugins) .then((manifest) => { sendMessage({ type: "manifest-ready" /* CoreMessageType.ManifestReady */, contentId: contentInitData.contentId, value: { manifest }, }); }, (err) => { sendMessage({ type: "error" /* CoreMessageType.Error */, contentId: contentInitData.contentId, value: formatErrorForSender(err), }); }); } function updateCoreReference(msg, refs) { switch (msg.value.name) { case "wantedBufferAhead": refs.wantedBufferAhead.setValueIfChanged(msg.value.newVal); break; case "maxVideoBufferSize": refs.maxVideoBufferSize.setValueIfChanged(msg.value.newVal); break; case "maxBufferBehind": refs.maxBufferBehind.setValueIfChanged(msg.value.newVal); break; case "maxBufferAhead": refs.maxBufferAhead.setValueIfChanged(msg.value.newVal); break; case "limitVideoResolution": refs.limitVideoResolution.setValueIfChanged(msg.value.newVal); break; case "throttleVideoBitrate": refs.throttleVideoBitrate.setValueIfChanged(msg.value.newVal); break; default: assertUnreachable(msg.value); } } function loadPreparedContent(sendMessage, val, contentPreparer, playbackObservationRef, refs) { log.debug("Core", "Loading pepared content."); const contentCanceller = new TaskCanceller("Start Content Worker"); let currentLoadCanceller = null; startLoadingAt(val.initialTime); return { signalMediaSourceReload: () => { return onMediaSourceReload(); }, stop: () => { contentCanceller.cancel("ContentHandle stop"); }, }; function startLoadingAt(startTime) { var _a; currentLoadCanceller === null || currentLoadCanceller === void 0 ? void 0 : currentLoadCanceller.cancel("Reloading content worker"); currentLoadCanceller = new TaskCanceller("(Re)Loading Content Worker"); currentLoadCanceller.linkToSignal(contentCanceller.signal); /** * Stores last discontinuity update sent to the Core for each Period and type * combinations, at least until the corresponding `PeriodStreamCleared` * message. * * This is an optimization to avoid sending too much discontinuity messages to * the main thread when it is not needed because nothing changed. */ const lastSentDiscontinuitiesStore = new Map(); const preparedContent = contentPreparer.getCurrentContent(); if (preparedContent === null || preparedContent.manifest === null) { const error = new OtherError("NONE", "Loading content when none is prepared"); sendMessage({ type: "error" /* CoreMessageType.Error */, contentId: undefined, value: formatErrorForSender(error), }); throw error; } const { contentId, cmcdDataBuilder, enableRepresentationAvoidance, manifest, mediaSource, representationEstimator, segmentSinksStore, segmentQueueCreator, } = preparedContent; const { drmSystemId, enableFastSwitching, onCodecSwitch } = val; playbackObservationRef.onUpdate((observation) => { synchronizeSegmentSinksOnObservation(observation, segmentSinksStore); const freezeResolution = preparedContent.freezeResolver.onNewObservation(observation); if (freezeResolution !== null) { handleFreezeResolution(sendMessage, freezeResolution, { contentId, manifest, handleMediaSourceReload: performMediaSourceReload, enableRepresentationAvoidance, }); } }, { clearSignal: currentLoadCanceller.signal }); const initialPeriod = (_a = manifest.getPeriodForTime(startTime)) !== null && _a !== void 0 ? _a : manifest.getNextPeriod(startTime); if (initialPeriod === undefined) { const error = new MediaError("MEDIA_STARTING_TIME_NOT_FOUND", "Wanted starting time not found in the Manifest."); sendMessage({ type: "error" /* CoreMessageType.Error */, contentId, value: formatErrorForSender(error), }); throw error; } const playbackObserver = new CorePlaybackObserver(playbackObservationRef, contentId, sendMessage, currentLoadCanceller.signal); cmcdDataBuilder === null || cmcdDataBuilder === void 0 ? void 0 : cmcdDataBuilder.startMonitoringPlayback(playbackObserver); currentLoadCanceller.signal.register(() => { cmcdDataBuilder === null || cmcdDataBuilder === void 0 ? void 0 : cmcdDataBuilder.stopMonitoringPlayback(); }); const contentTimeBoundariesObserver = createContentTimeBoundariesObserver(manifest, mediaSource, playbackObserver, segmentSinksStore, { onWarning: (err) => sendMessage({ type: "warning" /* CoreMessageType.Warning */, contentId, value: formatErrorForSender(err), }), onPeriodChanged: (period) => { sendMessage({ type: "active-period-changed" /* CoreMessageType.ActivePeriodChanged */, contentId, value: { periodId: period.id }, }); }, }, currentLoadCanceller.signal); StreamOrchestrator({ initialPeriod, manifest }, playbackObserver, representationEstimator, segmentSinksStore, segmentQueueCreator, { wantedBufferAhead: refs.wantedBufferAhead, maxVideoBufferSize: refs.maxVideoBufferSize, maxBufferAhead: refs.maxBufferAhead, maxBufferBehind: refs.maxBufferBehind, drmSystemId, enableFastSwitching, onCodecSwitch, }, handleStreamOrchestratorCallbacks(), currentLoadCanceller.signal); /** * Returns Object handling the callbacks from a `StreamOrchestrator`, which * are basically how it communicates about events. * @returns {Object} */ function handleStreamOrchestratorCallbacks() { return { needsBufferFlush(payload) { sendMessage({ type: "needs-buffer-flush" /* CoreMessageType.NeedsBufferFlush */, contentId, value: payload, }); }, streamStatusUpdate(value) { sendDiscontinuityUpdateIfNeeded(value); // If the status for the last Period indicates that segments are all loaded // or on the contrary that the loading resumed, announce it to the // ContentTimeBoundariesObserver. if (manifest.isLastPeriodKnown && value.period.id === manifest.periods[manifest.periods.length - 1].id) { const hasFinishedLoadingLastPeriod = value.hasFinishedLoading || value.isEmptyStream; if (hasFinishedLoadingLastPeriod) { contentTimeBoundariesObserver.onLastSegmentFinishedLoading(value.bufferType); } else { contentTimeBoundariesObserver.onLastSegmentLoadingResume(value.bufferType); } } }, needsManifestRefresh() { contentPreparer.scheduleManifestRefresh({ enablePartialRefresh: true, canUseUnsafeMode: true, }); }, manifestMightBeOufOfSync() { const { OUT_OF_SYNC_MANIFEST_REFRESH_DELAY } = config.getCurrent(); contentPreparer.scheduleManifestRefresh({ enablePartialRefresh: false, canUseUnsafeMode: false, delay: OUT_OF_SYNC_MANIFEST_REFRESH_DELAY, }); }, lockedStream(payload) { sendMessage({ type: "locked-stream" /* CoreMessageType.LockedStream */, contentId, value: { periodId: payload.period.id, bufferType: payload.bufferType, }, }); }, adaptationChange(value) { var _a, _b; sendMessage({ type: "adaptation-changed" /* CoreMessageType.AdaptationChanged */, contentId, value: { adaptationId: (_b = (_a = value.adaptation) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null, periodId: value.period.id, type: value.type, }, }); contentTimeBoundariesObserver.onAdaptationChange(value.type, value.period, value.adaptation); }, representationChange(value) { var _a, _b; contentTimeBoundariesObserver.onRepresentationChange(value.type, value.period); if (currentLoadCanceller === null || currentLoadCanceller.signal.isCancelled()) { return; } sendMessage({ type: "representation-changed" /* CoreMessageType.RepresentationChanged */, contentId, value: { adaptationId: value.adaptation.id, representationId: (_b = (_a = value.representation) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null, periodId: value.period.id, type: value.type, }, }); }, inbandEvent(value) { sendMessage({ type: "inband-event" /* CoreMessageType.InbandEvent */, contentId, value, }); }, warning(value) { sendMessage({ type: "warning" /* CoreMessageType.Warning */, contentId, value: formatErrorForSender(value), }); }, periodStreamReady(value) { if (preparedContent === null) { return; } preparedContent.trackChoiceSetter.addTrackSetter(value.period.id, value.type, value.adaptationRef); sendMessage({ type: "period-stream-ready" /* CoreMessageType.PeriodStreamReady */, contentId, value: { periodId: value.period.id, bufferType: value.type }, }); }, periodStreamCleared(value) { if (preparedContent === null) { return; } const periodDiscontinuitiesStore = lastSentDiscontinuitiesStore.get(value.period); if (periodDiscontinuitiesStore !== undefined) { periodDiscontinuitiesStore.delete(value.type); if (periodDiscontinuitiesStore.size === 0) { lastSentDiscontinuitiesStore.delete(value.period); } } contentTimeBoundariesObserver.onPeriodCleared(value.type, value.period); preparedContent.trackChoiceSetter.removeTrackSetter(value.period.id, value.type); sendMessage({ type: "period-stream-cleared" /* CoreMessageType.PeriodStreamCleared */, contentId, value: { periodId: value.period.id, bufferType: value.type }, }); }, bitrateEstimateChange(payload) { var _a; if (preparedContent !== null) { (_a = preparedContent.cmcdDataBuilder) === null || _a === void 0 ? void 0 : _a.updateThroughput(payload.type, payload.bitrate); } // TODO for low-latency contents it is __VERY__ frequent. // Considering this is only for an unimportant undocumented API, we may // throttle such messages. (e.g. max one per 2 seconds for each type?). sendMessage({ type: "bitrate-estimate-change" /* CoreMessageType.BitrateEstimateChange */, contentId, value: { bitrate: payload.bitrate, bufferType: payload.type, }, }); }, needsMediaSourceReload(payload) { performMediaSourceReload(payload); }, needsDecipherabilityFlush() { sendMessage({ type: "needs-decipherability-flush" /* CoreMessageType.NeedsDecipherabilityFlush */, contentId, value: null, }); }, encryptionDataEncountered(values) { for (const value of values) { const originalContent = value.content; const content = Object.assign({}, originalContent); if (content.manifest instanceof Manifest) { content.manifest = content.manifest.getMetadataSnapshot(); } if (content.period instanceof Period) { content.period = content.period.getMetadataSnapshot(); } if (content.adaptation instanceof Adaptation) { content.adaptation = content.adaptation.getMetadataSnapshot(); } if (content.representation instanceof Representation) { content.representation = content.representation.getMetadataSnapshot(); } sendMessage({ type: "encryption-data-encountered" /* CoreMessageType.EncryptionDataEncountered */, contentId, value: { keyIds: value.keyIds, values: value.values, content, type: value.type, }, }); } }, error(error) { sendMessage({ type: "error" /* CoreMessageType.Error */, contentId, value: formatErrorForSender(error), }); }, }; } function sendDiscontinuityUpdateIfNeeded(value) { const { imminentDiscontinuity } = value; let periodMap = lastSentDiscontinuitiesStore.get(value.period); const sentObjInfo = periodMap === null || periodMap === void 0 ? void 0 : periodMap.get(value.bufferType); if (sentObjInfo !== undefined) { if (sentObjInfo.discontinuity === null) { if (imminentDiscontinuity === null) { return; } } else if (imminentDiscontinuity !== null && sentObjInfo.discontinuity.start === imminentDiscontinuity.start && sentObjInfo.discontinuity.end === imminentDiscontinuity.end) { return; } } if (periodMap === undefined) { periodMap = new Map(); lastSentDiscontinuitiesStore.set(value.period, periodMap); } const msgObj = { periodId: value.period.id, bufferType: value.bufferType, discontinuity: value.imminentDiscontinuity, position: value.position, }; periodMap.set(value.bufferType, msgObj); sendMessage({ type: "discontinuity-update" /* CoreMessageType.DiscontinuityUpdate */, contentId, value: msgObj, }); } } function performMediaSourceReload(payload) { var _a; if (currentLoadCanceller !== null) { currentLoadCanceller.cancel("WorkerMain MediaSource reload"); currentLoadCanceller = null; } const mediaSourceId = (_a = contentPreparer.getCurrentContent()) === null || _a === void 0 ? void 0 : _a.mediaSource.id; if (mediaSourceId === undefined) { log.warn("Core", "Cannot reload MediaSource: no MediaSource currently."); return; } log.debug("Core", "Reloading MediaSource", { timeOffset: payload.timeOffset, minimumPosition: payload.minimumPosition, maximumPosition: payload.maximumPosition, }); sendMessage({ type: "reloading-media-source" /* CoreMessageType.ReloadingMediaSource */, mediaSourceId, value: payload, }, []); onMediaSourceReload(); } function onMediaSourceReload() { var _a; // TODO more precize one day? const lastObservation = playbackObservationRef.getValue(); const newInitialTime = lastObservation.position.getWanted(); if (currentLoadCanceller !== null) { currentLoadCanceller.cancel("MediaSource reload"); currentLoadCanceller = null; } const contentId = (_a = contentPreparer.getCurrentContent()) === null || _a === void 0 ? void 0 : _a.contentId; contentPreparer.reloadMediaSource(sendMessage).then(() => { log.info("Core", "MediaSource Reloaded, loading content again", { newInitialTime, }); startLoadingAt(newInitialTime); }, (err) => { if (TaskCanceller.isCancellationError(err)) { log.info("Core", "A reloading operation was cancelled"); return; } sendMessage({ type: "error" /* CoreMessageType.Error */, contentId, value: formatErrorForSender(err), }); }); } } function updateLoggerLevel(logLevel, logFormat, sendBackLogs) { if (!sendBackLogs) { log.setLevel(logLevel, logFormat); } else { // Here we force the log format to "standard" as the full formatting will be // performed on main thread. log.setLevel(logLevel, "standard", (levelStr, namespace, logs) => { const sentLogs = logs.map((e) => { if (e instanceof Error) { return formatErrorForSender(e); } return e; }); // Not relying on `sendMessage` as it also logs postMessage({ type: "log" /* CoreMessageType.LogMessage */, value: { namespace, logLevel: levelStr, logs: sentLogs, }, }); }); } } /** * Send a message `SegmentSinkStoreUpdate` to the main thread with * a serialized object that represents the segmentSinksStore state. * @param {Function} sendMessage - Function allowing to send messages to the * "main thread" part of the RxPlayer logic. * @param {ContentPreparer} contentPreparer */ function sendSegmentSinksStoreInfos(sendMessage, contentPreparer, requestId) { const currentContent = contentPreparer.getCurrentContent(); if (currentContent === null) { return; } const segmentSinksMetrics = currentContent.segmentSinksStore.getSegmentSinksMetrics(); sendMessage({ type: "segment-sink-store-update" /* CoreMessageType.SegmentSinkStoreUpdate */, contentId: currentContent.contentId, value: { segmentSinkMetrics: segmentSinksMetrics, requestId }, }); } /** * Handle accordingly an `IFreezeResolution` object. * @param {Function} sendMessage - Function allowing to send messages to the * "main thread" part of the RxPlayer logic. * @param {Object|null} freezeResolution - The `IFreezeResolution` suggested. * @param {Object} param - Parameters that might be needed to implement the * resolution. * @param {string} param.contentId - `contentId` for the current content, used * e.g. for message exchanges between threads. * @param {Object} param.manifest - The current content's Manifest object. * @param {Function} param.handleMediaSourceReload - Function to call if we need * to ask for a "MediaSource reload". * @param {Boolean} param.enableRepresentationAvoidance - If `true`, this * function is authorized to mark `Representation` as "to avoid" if the * `IFreezeResolution` object suggest it. */ function handleFreezeResolution(sendMessage, freezeResolution, { contentId, manifest, handleMediaSourceReload, enableRepresentationAvoidance, }) { switch (freezeResolution.type) { case "reload": { log.info("Core", "Planning reload due to freeze"); handleMediaSourceReload({ timeOffset: 0, minimumPosition: 0, maximumPosition: Infinity, }); break; } case "flush": { log.info("Core", "Flushing buffer due to freeze"); sendMessage({ type: "needs-buffer-flush" /* CoreMessageType.NeedsBufferFlush */, contentId, value: { relativeResumingPosition: freezeResolution.value.relativeSeek, relativePosHasBeenDefaulted: false, }, }); break; } case "avoid-representations": { log.info("Core", "Planning Representation avoidance due to freeze"); const content = freezeResolution.value; if (enableRepresentationAvoidance) { manifest.addRepresentationsToAvoid(content); } handleMediaSourceReload({ timeOffset: 0, minimumPosition: 0, maximumPosition: Infinity, }); break; } default: assertUnreachable(freezeResolution); } } /** * Handles thumbnail requests and send back the result to the main thread. * @param {ContentPreparer} contentPreparer * @returns {void} */ function sendThumbnailData(sendMessage, contentPreparer, msg) { const preparedContent = contentPreparer.getCurrentContent(); const respondWithError = (err) => { sendMessage({ type: "thumbnail-response" /* CoreMessageType.ThumbnailDataResponse */, contentId: msg.contentId, value: { status: "error", requestId: msg.value.requestId, error: formatErrorForSender(err), }, }); }; if (preparedContent === null || preparedContent.manifest === null || preparedContent.contentId !== msg.contentId) { return respondWithError(new Error("Content changed")); } getThumbnailData(preparedContent.fetchThumbnailData, preparedContent.manifest, msg.value.periodId, msg.value.thumbnailTrackId, msg.value.time).then((result) => { // Multiple thumbnail requests can share the same fetched payload. // Transfer a copy here so replying to one request does not detach the // ArrayBuffer that still has to be sent to another requester. const data = result.data.slice(0); sendMessage({ type: "thumbnail-response" /* CoreMessageType.ThumbnailDataResponse */, contentId: msg.contentId, value: { status: "success", requestId: msg.value.requestId, data: Object.assign(Object.assign({}, result), { data }), }, }, [data]); }, (err) => { return respondWithError(err); }); }