UNPKG

paella-core

Version:
445 lines (377 loc) 12.1 kB
import PlayerResource from 'paella-core/js/core/PlayerResource'; import { getVideoPlugin } from 'paella-core/js/core/VideoPlugin'; import { loadCanvasPlugins, getCanvasPlugin, unloadCanvasPlugins } from 'paella-core/js/core/CanvasPlugin'; import Events, { triggerIfReady } from 'paella-core/js/core/Events'; export function checkManifestIntegrity(manifest) { const check = (field, error) => { if (!field) { throw new Error(`Invalid video manifest: ${error}`); } } check(manifest.streams, "missing 'streams' object."); check(manifest.streams.length>0, "the 'streams' array is empty."); check(manifest.metadata?.preview, "the 'metadata.preview' field is required."); } export default class SteramProvider extends PlayerResource { constructor(player, videoContainer) { super(player, videoContainer); this._videoContainer = videoContainer; this._streamData = null; this._streams = null; this._players = []; this._mainAudioPlayer = null; this._streamSyncTimer = null; this._trimming = { enabled: false, start: 100, end: 200 } } async load(streamData) { this._streamData = streamData; this._streams = {}; let mainAudioContent = this.player.config.defaultAudioStream || "presenter"; if (this._streamData.length === 1) { mainAudioContent = this._streamData[0].content; } streamData.some(s => { if (s.role === "mainAudio") { mainAudioContent = s.content; return true; } }); this.player.log.debug("Finding compatible video plugins"); await loadCanvasPlugins(this.player); // Find video plugins for each stream this._streamData.forEach(stream => { const canvasPlugin = getCanvasPlugin(this.player, stream); if (!canvasPlugin) { throw Error(`Canvas plugin not found: ${ stream.canvas }`); } const isMainAudio = stream.content === mainAudioContent; const videoPlugin = getVideoPlugin(this.player, stream); if (!videoPlugin) { throw Error(`Incompatible stream type: ${ stream.content }`); } this._streams[stream.content] = { stream, isMainAudio, videoPlugin, canvasPlugin } }) let videoEndedEventTimer = null; for (const content in this._streams) { const s = this._streams[content]; s.canvas = await s.canvasPlugin.getCanvasInstance(this._videoContainer); s.player = await s.videoPlugin.getVideoInstance(s.canvas.element, s.isMainAudio); if (mainAudioContent===content) { this._mainAudioPlayer = s.player; s.player.initVolume(1); } else { s.player.initVolume(0); } await s.player.load(s.stream, this); await s.canvas.loadCanvas(s.player); s.player.onVideoEnded(async () => { // Pause all streams, to prevent other vídeos from playing, when not all the // streams have the same duration. this.executeAction("pause"); // Set current time to 0 to put the video in the initial state this.executeAction("setCurrentTime", 0); // Trigger the ended event triggerIfReady(this.player, Events.ENDED); }) this._players.push(s.player); } if (this.mainAudioPlayer === null) { this.player.log.error("The video stream containing the audio track could not be identified. The `role` attribute must be specified in the main video stream, or the `defaultAudioStream` attribute must be set correctly in the player configuration."); throw new Error("The video stream containing the audio track could not be identified."); } } async unload() { this.stopStreamSync(); await unloadCanvasPlugins(this.player); } get players() { return this._players; } // This is the raw streamData loaded from the video manifest get streamData() { return this._streamData; } // This property stores the available streams, indexed by the content identifier, and contains the // stream data, the video plugin and the player, for each content identifier. get streams() { return this._streams; } get mainAudioPlayer() { return this._mainAudioPlayer; } get isTrimEnabled() { return this._trimming?.enabled && this._trimming?.end > this._trimming?.start; } get trimStart() { return this._trimming?.start; } get trimEnd() { return this._trimming?.end; } async setTrimming({ enabled, start, end }) { if (start>=end) { throw Error(`Error setting trimming: start time (${ start }) must be lower than end time ${ end }`); } this._trimming = { enabled, start, end }; const currentTime = await this.currentTime() triggerIfReady(this.player, Events.TIMEUPDATE, { currentTime: enabled ? start + currentTime : currentTime }); } startStreamSync() { const maxSync = this.player.config.videoContainer?.multiStreamMaxDesyncTime ?? 0.2; this.player.log.debug(`Max video desynchronization: ${ maxSync }`); this._timeSync = true; const setupSyncTimer = async () => { if (!this._players.length) { this.player.log.warn("Player not yet loaded. Waiting for video sync."); return; } let currentTime = this.mainAudioPlayer.currentTimeSync; if (this.players.length>1) { for (let i = 0; i<this.players.length; ++i) { const secPlayer = this.players[i]; if (secPlayer !== this.mainAudioPlayer) { const playerTime = secPlayer.currentTimeSync; if (Math.abs(currentTime - playerTime) > maxSync) { this.player.log.debug("Video synchronization triggered"); secPlayer.setCurrentTime(currentTime); } } } } // Check trimming if (this.isTrimEnabled) { let trimmedCurrentTime = currentTime - this.trimStart; if (this.trimEnd<=currentTime) { await this.executeAction("pause"); await this.setCurrentTime(0); this.stopStreamSync(); currentTime = 0; triggerIfReady(this.player, Events.ENDED, {}); return; } else if (currentTime<this.trimStart) { await this.setCurrentTime(0); currentTime = this.trimStart; trimmedCurrentTime = 0; } triggerIfReady(this.player, Events.TIMEUPDATE, { currentTime: trimmedCurrentTime }); this._timeupdateTimer = setTimeout(() => { if (this._timeSync) { setupSyncTimer(); } }, 250); } else if (this._timeSync) { triggerIfReady(this.player, Events.TIMEUPDATE, { currentTime }); this._timeupdateTimer = setTimeout(() => { setupSyncTimer(); }, 250); } } setupSyncTimer(); } stopStreamSync() { this._timeSync = false; if (this._timeupdateTimer) { clearTimeout(this._timeupdateTimer); } } executeAction(fnName, params = []) { // Important: this implementation must be done using promises instead of async/await, due to // a bug in babel that causes that the resulting array may not be available when the async function // is completed. if (!Array.isArray(params)) { params = [params]; } return new Promise((resolve) => { let res = []; let p = []; this.players.forEach(player => { p.push(new Promise(innerResolve => { player[fnName](...params).then(r => { res.push(r); innerResolve(); }) })); }) Promise.allSettled(p).then(() => resolve(res)); }) } get isLiveStream() { return this._streamData.some(sd => Array.from(Object.keys(sd.sources)).indexOf("hlsLive") !== -1); } async play() { this.startStreamSync(); const result = await this.executeAction("play"); return result; } async pause() { this.stopStreamSync(); const result = await this.executeAction("pause"); return result; } async stop() { this.stopStreamSync() await this.executeAction("pause"); await this.executeAction("setCurrentTime", 0); } async paused() { return (await this.executeAction("paused"))[0]; } async setCurrentTime(t) { const duration = await this.duration(); if (t < 0) { t = 0; } else if (t > duration) { t = duration; } const prevTime = (await this.executeAction("currentTime"))[0]; let returnValue = null; if (this.isTrimEnabled) { t = t + this.trimStart; t = t >= this.trimEnd ? this.trimEnd : t; const result = (await this.executeAction("setCurrentTime", [t]))[0]; const newTime = (await this.executeAction("currentTime"))[0]; returnValue = { result, prevTime: prevTime - this.trimStart, newTime: newTime - this.trimStart } } else { const result = (await this.executeAction("setCurrentTime", [t]))[0]; const newTime = (await this.executeAction("currentTime"))[0]; returnValue = { result, prevTime, newTime }; } const currentTime = await this.currentTime(); triggerIfReady(this.player, Events.TIMEUPDATE, { currentTime: currentTime }); return returnValue; } async currentTime() { const currentTime = await this.mainAudioPlayer.currentTime(); if (this.isTrimEnabled) { return currentTime - this.trimStart; } else { return currentTime; } } async currentTimeIgnoringTrimming() { const currentTime = await this.mainAudioPlayer.currentTime(); return currentTime; } async volume() { if (this.mainAudioPlayer) { return await this.mainAudioPlayer.volume(); } else { return (await this.executeAction("volume"))[0]; } } async setVolume(v) { if (this.mainAudioPlayer) { return await this.mainAudioPlayer.setVolume(v); } else { return (await this.executeAction("setVolume",[v]))[0]; } } async duration() { if (this.isTrimEnabled) { return this.trimEnd - this.trimStart; } else { return await this.durationIgnoringTrimming(); } } async durationIgnoringTrimming() { const result = (await this.executeAction("duration")).reduce((acc, val) => Math.min(acc, val), Number.MAX_VALUE); return result; } async playbackRate() { return (await this.executeAction("playbackRate"))[0]; } async setPlaybackRate(rate) { return (await this.executeAction("setPlaybackRate",[rate]))[0]; } async getQualityReferencePlayer() { let player = null; let referenceQualities = []; if (Object.keys(this.streams).length>0) { for (const content in this.streams) { const stream = this.streams[content]; const q = (await stream.player.getQualities()) || []; if (!player && q.length > referenceQualities.length) { referenceQualities = q; player = stream.player; } } } return player || this.mainAudioPlayer; } async getCurrentQuality() { return (await this.getQualityReferencePlayer()).currentQuality; } async getQualities() { const player = await this.getQualityReferencePlayer(); return await player.getQualities(); } async setQuality(quality) { const player = await this.getQualityReferencePlayer(); // The video is paused to stop the synchronization timer, and then it is resumed // if it was playing after the quality change. const isPaused = await this.paused(); if (!isPaused) { this.player.log.debug("Quality change started. Pausing video."); await this.pause(); } const qualities = await player.getQualities(); const total = qualities.length; const index = qualities.findIndex(q => quality.index === q.index); if (index>=0) { const qualityFactor = index / total; for (const content in this.streams) { const stream = this.streams[content]; const streamQualities = (await stream.player.getQualities()) || []; this.player.log.debug(streamQualities); if (streamQualities.length>1) { const qualityIndex = Math.round(streamQualities.length * qualityFactor); const selectedQuality = streamQualities[qualityIndex]; await stream.player.setQuality(selectedQuality); } } } if (!isPaused) { this.player.log.debug("Quality change finished. Resuming video."); await this.play(); } } async supportsMultiaudio() { return this.mainAudioPlayer.supportsMultiaudio(); } async getAudioTracks() { return this.mainAudioPlayer.getAudioTracks(); } async setCurrentAudioTrack(track) { return this.mainAudioPlayer.setCurrentAudioTrack(track); } get currentAudioTrack() { return this.mainAudioPlayer.currentAudioTrack; } }