import Hls from "hls.js"; import fscreen from "fscreen"; import { canChangeVolume, canFullscreen, canFullscreenAnyElement, canWebkitFullscreen, } from "@/utils/detectFeatures"; import { MWStreamType } from "@/backend/helpers/streams"; import { updateInterface } from "@/video/state/logic/interface"; import { updateSource } from "@/video/state/logic/source"; import { getStoredVolume, setStoredVolume, } from "@/video/components/hooks/volumeStore"; import { updateError } from "@/video/state/logic/error"; import { updateMisc } from "@/video/state/logic/misc"; import { resetStateForSource } from "@/video/state/providers/helpers"; import { getPlayerState } from "../cache"; import { updateMediaPlaying } from "../logic/mediaplaying"; import { VideoPlayerStateProvider } from "./providerTypes"; import { updateProgress } from "../logic/progress"; import { handleBuffered } from "./utils"; function errorMessage(err: MediaError) { switch (err.code) { case MediaError.MEDIA_ERR_ABORTED: return { code: "ABORTED", description: "Video was aborted", }; case MediaError.MEDIA_ERR_NETWORK: return { code: "NETWORK_ERROR", description: "A network error occured, the video failed to stream", }; case MediaError.MEDIA_ERR_DECODE: return { code: "DECODE_ERROR", description: "Video stream could not be decoded", }; case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: return { code: "SRC_NOT_SUPPORTED", description: "The video type is not supported by your browser", }; default: return { code: "UNKNOWN_ERROR", description: "Unknown media error occured", }; } } export function createVideoStateProvider( descriptor: string, playerEl: HTMLVideoElement ): VideoPlayerStateProvider { const player = playerEl; const state = getPlayerState(descriptor); return { getId() { return "video"; }, play() { player.play(); }, pause() { player.pause(); }, exitFullscreen() { if (!fscreen.fullscreenElement) return; fscreen.exitFullscreen(); }, enterFullscreen() { if (!canFullscreen() || fscreen.fullscreenElement) return; if (canFullscreenAnyElement()) { if (state.wrapperElement) fscreen.requestFullscreen(state.wrapperElement); return; } if (canWebkitFullscreen()) { (player as any).webkitEnterFullscreen(); } }, startAirplay() { const videoPlayer = player as any; if (videoPlayer.webkitShowPlaybackTargetPicker) videoPlayer.webkitShowPlaybackTargetPicker(); }, setTime(t) { // clamp time between 0 and max duration let time = Math.min(t, player.duration); time = Math.max(0, time); if (Number.isNaN(time)) return; // update state player.currentTime = time; state.progress.time = time; updateProgress(descriptor, state); }, setSeeking(active) { state.mediaPlaying.isSeeking = active; state.mediaPlaying.isDragSeeking = active; updateMediaPlaying(descriptor, state); // if it was playing when starting to seek, play again if (!active) { if (!state.pausedWhenSeeking) this.play(); return; } // when seeking we pause the video // this variables isnt reactive, just used so the state can be remembered next unseek state.pausedWhenSeeking = state.mediaPlaying.isPaused; this.pause(); }, async setVolume(v) { // clamp time between 0 and 1 let volume = Math.min(v, 1); volume = Math.max(0, volume); // update state if (await canChangeVolume()) player.volume = volume; state.mediaPlaying.volume = volume; updateMediaPlaying(descriptor, state); // update localstorage setStoredVolume(volume); }, setSource(source) { if (!source) { resetStateForSource(descriptor, state); player.removeAttribute("src"); player.load(); state.source = null; updateSource(descriptor, state); return; } // reset before assign new one so the old HLS instance gets destroyed resetStateForSource(descriptor, state); if (source?.type === MWStreamType.HLS) { if (player.canPlayType("application/vnd.apple.mpegurl")) { // HLS supported natively by browser player.src = source.source; } else { // HLS through HLS.js if (!Hls.isSupported()) { state.error = { name: `Not supported`, description: "Your browser does not support HLS video", }; updateError(descriptor, state); return; } const hls = new Hls({ enableWorker: false }); state.hlsInstance = hls; hls.on(Hls.Events.ERROR, (event, data) => { if (data.fatal) { state.error = { name: `error ${data.details}`, description: data.error?.message ?? "Something went wrong", }; updateError(descriptor, state); } console.error("HLS error", data); }); hls.attachMedia(player); hls.loadSource(source.source); } } else if (source.type === MWStreamType.MP4) { // standard MP4 stream player.src = source.source; } // update state state.source = { quality: source.quality, type: source.type, url: source.source, caption: null, }; updateSource(descriptor, state); }, setCaption(id, url) { if (state.source) { state.source.caption = { id, url, }; updateSource(descriptor, state); } }, clearCaption() { if (state.source) { state.source.caption = null; updateSource(descriptor, state); } }, providerStart() { this.setVolume(getStoredVolume()); const pause = () => { state.mediaPlaying.isPaused = true; state.mediaPlaying.isPlaying = false; updateMediaPlaying(descriptor, state); }; const playing = () => { state.mediaPlaying.isPaused = false; state.mediaPlaying.isPlaying = true; state.mediaPlaying.isLoading = false; state.mediaPlaying.hasPlayedOnce = true; updateMediaPlaying(descriptor, state); }; const waiting = () => { state.mediaPlaying.isLoading = true; updateMediaPlaying(descriptor, state); }; const seeking = () => { state.mediaPlaying.isSeeking = true; updateMediaPlaying(descriptor, state); }; const seeked = () => { state.mediaPlaying.isSeeking = false; updateMediaPlaying(descriptor, state); }; const loadedmetadata = () => { state.progress.duration = player.duration; updateProgress(descriptor, state); }; const timeupdate = () => { state.progress.duration = player.duration; state.progress.time = player.currentTime; updateProgress(descriptor, state); }; const progress = () => { state.progress.buffered = handleBuffered( player.currentTime, player.buffered ); updateProgress(descriptor, state); }; const canplay = () => { state.mediaPlaying.isFirstLoading = false; state.mediaPlaying.isLoading = false; updateMediaPlaying(descriptor, state); }; const fullscreenchange = () => { state.interface.isFullscreen = !!document.fullscreenElement; updateInterface(descriptor, state); }; const volumechange = async () => { if (await canChangeVolume()) { state.mediaPlaying.volume = player.volume; updateMediaPlaying(descriptor, state); } }; const isFocused = (evt: any) => { state.interface.isFocused = evt.type !== "mouseleave"; updateInterface(descriptor, state); }; const canAirplay = (e: any) => { if (e.availability === "available") { state.canAirplay = true; updateMisc(descriptor, state); } }; const error = () => { if (player.error) { const err = errorMessage(player.error); console.error("Native video player threw error", player.error); state.error = { description: err.description, name: `Error ${err.code}`, }; this.pause(); // stop video from playing } else { state.error = null; } updateError(descriptor, state); }; state.wrapperElement?.addEventListener("click", isFocused); state.wrapperElement?.addEventListener("mouseenter", isFocused); state.wrapperElement?.addEventListener("mouseleave", isFocused); player.addEventListener("volumechange", volumechange); player.addEventListener("pause", pause); player.addEventListener("playing", playing); player.addEventListener("seeking", seeking); player.addEventListener("seeked", seeked); player.addEventListener("progress", progress); player.addEventListener("waiting", waiting); player.addEventListener("timeupdate", timeupdate); player.addEventListener("loadedmetadata", loadedmetadata); player.addEventListener("canplay", canplay); fscreen.addEventListener("fullscreenchange", fullscreenchange); player.addEventListener("error", error); player.addEventListener( "webkitplaybacktargetavailabilitychanged", canAirplay ); if (state.source) this.setSource({ quality: state.source.quality, source: state.source.url, type: state.source.type, }); return { destroy: () => { player.removeEventListener("pause", pause); player.removeEventListener("playing", playing); player.removeEventListener("seeking", seeking); player.removeEventListener("volumechange", volumechange); player.removeEventListener("seeked", seeked); player.removeEventListener("timeupdate", timeupdate); player.removeEventListener("loadedmetadata", loadedmetadata); player.removeEventListener("progress", progress); player.removeEventListener("waiting", waiting); player.removeEventListener("error", error); player.removeEventListener("canplay", canplay); fscreen.removeEventListener("fullscreenchange", fullscreenchange); state.wrapperElement?.removeEventListener("click", isFocused); state.wrapperElement?.removeEventListener("mouseenter", isFocused); state.wrapperElement?.removeEventListener("mouseleave", isFocused); player.removeEventListener( "webkitplaybacktargetavailabilitychanged", canAirplay ); }, }; }, }; }