movie-web/src/video/state/providers/videoStateProvider.ts
2023-03-03 19:33:30 +03:00

346 lines
11 KiB
TypeScript

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 { revokeCaptionBlob } from "@/backend/helpers/captions";
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) {
revokeCaptionBlob(state.source.caption?.url);
state.source.caption = {
id,
url,
};
updateSource(descriptor, state);
}
},
clearCaption() {
if (state.source) {
revokeCaptionBlob(state.source.caption?.url);
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
);
},
};
},
};
}