2023-10-01 17:34:37 +02:00
|
|
|
import fscreen from "fscreen";
|
2023-10-14 16:32:54 +02:00
|
|
|
import Hls from "hls.js";
|
2023-10-01 17:34:37 +02:00
|
|
|
|
2023-09-30 20:57:00 +02:00
|
|
|
import {
|
|
|
|
DisplayInterface,
|
|
|
|
DisplayInterfaceEvents,
|
|
|
|
} from "@/components/player/display/displayInterface";
|
2023-10-01 21:08:26 +02:00
|
|
|
import { handleBuffered } from "@/components/player/utils/handleBuffered";
|
2023-10-14 16:06:25 +02:00
|
|
|
import { LoadableSource } from "@/stores/player/utils/qualities";
|
2023-10-01 17:34:37 +02:00
|
|
|
import {
|
2023-10-01 21:08:26 +02:00
|
|
|
canChangeVolume,
|
2023-10-01 17:34:37 +02:00
|
|
|
canFullscreen,
|
|
|
|
canFullscreenAnyElement,
|
|
|
|
canWebkitFullscreen,
|
|
|
|
} from "@/utils/detectFeatures";
|
2023-09-30 20:57:00 +02:00
|
|
|
import { makeEmitter } from "@/utils/events";
|
|
|
|
|
|
|
|
export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|
|
|
const { emit, on, off } = makeEmitter<DisplayInterfaceEvents>();
|
2023-10-14 16:06:25 +02:00
|
|
|
let source: LoadableSource | null = null;
|
2023-10-14 16:32:54 +02:00
|
|
|
let hls: Hls | null = null;
|
2023-09-30 20:57:00 +02:00
|
|
|
let videoElement: HTMLVideoElement | null = null;
|
2023-10-01 17:34:37 +02:00
|
|
|
let containerElement: HTMLElement | null = null;
|
|
|
|
let isFullscreen = false;
|
2023-10-01 21:08:26 +02:00
|
|
|
let isPausedBeforeSeeking = false;
|
2023-10-02 21:04:40 +02:00
|
|
|
let isSeeking = false;
|
2023-10-15 20:25:14 +02:00
|
|
|
let startAt = 0;
|
2023-09-30 20:57:00 +02:00
|
|
|
|
2023-10-14 16:32:54 +02:00
|
|
|
function setupSource(vid: HTMLVideoElement, src: LoadableSource) {
|
|
|
|
if (src.type === "hls") {
|
|
|
|
if (!Hls.isSupported()) throw new Error("HLS not supported");
|
|
|
|
|
2023-10-14 19:28:27 +02:00
|
|
|
if (!hls) {
|
|
|
|
hls = new Hls({ enableWorker: false });
|
|
|
|
hls.on(Hls.Events.ERROR, (event, data) => {
|
|
|
|
console.error("HLS error", data);
|
|
|
|
if (data.fatal) {
|
|
|
|
throw new Error(
|
|
|
|
`HLS ERROR:${data.error?.message ?? "Something went wrong"}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2023-10-14 16:32:54 +02:00
|
|
|
|
|
|
|
hls.attachMedia(vid);
|
|
|
|
hls.loadSource(src.url);
|
2023-10-15 20:25:14 +02:00
|
|
|
vid.currentTime = startAt;
|
2023-10-14 16:32:54 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
vid.src = src.url;
|
2023-10-15 20:25:14 +02:00
|
|
|
vid.currentTime = startAt;
|
2023-10-14 16:32:54 +02:00
|
|
|
}
|
|
|
|
|
2023-09-30 20:57:00 +02:00
|
|
|
function setSource() {
|
|
|
|
if (!videoElement || !source) return;
|
2023-10-14 16:32:54 +02:00
|
|
|
setupSource(videoElement, source);
|
2023-10-11 22:09:28 +02:00
|
|
|
|
2023-10-05 22:12:25 +02:00
|
|
|
videoElement.addEventListener("play", () => {
|
|
|
|
emit("play", undefined);
|
|
|
|
emit("loading", false);
|
|
|
|
});
|
|
|
|
videoElement.addEventListener("playing", () => emit("play", undefined));
|
2023-09-30 20:57:00 +02:00
|
|
|
videoElement.addEventListener("pause", () => emit("pause", undefined));
|
2023-10-05 22:12:25 +02:00
|
|
|
videoElement.addEventListener("canplay", () => emit("loading", false));
|
|
|
|
videoElement.addEventListener("waiting", () => emit("loading", true));
|
2023-10-01 21:08:26 +02:00
|
|
|
videoElement.addEventListener("volumechange", () =>
|
2023-10-11 22:09:28 +02:00
|
|
|
emit("volumechange", videoElement?.muted ? 0 : videoElement?.volume ?? 0)
|
2023-10-01 21:08:26 +02:00
|
|
|
);
|
|
|
|
videoElement.addEventListener("timeupdate", () =>
|
|
|
|
emit("time", videoElement?.currentTime ?? 0)
|
|
|
|
);
|
|
|
|
videoElement.addEventListener("loadedmetadata", () => {
|
|
|
|
emit("duration", videoElement?.duration ?? 0);
|
|
|
|
});
|
|
|
|
videoElement.addEventListener("progress", () => {
|
|
|
|
if (videoElement)
|
|
|
|
emit(
|
|
|
|
"buffered",
|
|
|
|
handleBuffered(videoElement.currentTime, videoElement.buffered)
|
|
|
|
);
|
|
|
|
});
|
2023-10-18 17:14:59 +02:00
|
|
|
videoElement.addEventListener("webkitendfullscreen", () => {
|
|
|
|
isFullscreen = false;
|
|
|
|
emit("fullscreen", isFullscreen);
|
|
|
|
if (!isFullscreen) emit("needstrack", false);
|
|
|
|
});
|
2023-09-30 20:57:00 +02:00
|
|
|
}
|
|
|
|
|
2023-10-14 19:28:27 +02:00
|
|
|
function unloadSource() {
|
|
|
|
if (videoElement) videoElement.removeAttribute("src");
|
|
|
|
if (hls) {
|
|
|
|
hls.destroy();
|
|
|
|
hls = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function destroyVideoElement() {
|
|
|
|
unloadSource();
|
|
|
|
if (videoElement) {
|
|
|
|
videoElement = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-01 17:34:37 +02:00
|
|
|
function fullscreenChange() {
|
|
|
|
isFullscreen =
|
|
|
|
!!document.fullscreenElement || // other browsers
|
|
|
|
!!(document as any).webkitFullscreenElement; // safari
|
2023-10-18 17:14:59 +02:00
|
|
|
emit("fullscreen", isFullscreen);
|
|
|
|
if (!isFullscreen) emit("needstrack", false);
|
2023-10-01 17:34:37 +02:00
|
|
|
}
|
|
|
|
fscreen.addEventListener("fullscreenchange", fullscreenChange);
|
|
|
|
|
2023-09-30 20:57:00 +02:00
|
|
|
return {
|
|
|
|
on,
|
|
|
|
off,
|
2023-10-01 17:34:37 +02:00
|
|
|
destroy: () => {
|
2023-10-14 19:28:27 +02:00
|
|
|
destroyVideoElement();
|
2023-10-01 17:34:37 +02:00
|
|
|
fscreen.removeEventListener("fullscreenchange", fullscreenChange);
|
|
|
|
},
|
2023-10-15 20:25:14 +02:00
|
|
|
load(newSource, startAtInput) {
|
2023-10-14 19:28:27 +02:00
|
|
|
if (!newSource) unloadSource();
|
2023-09-30 20:57:00 +02:00
|
|
|
source = newSource;
|
2023-10-05 22:12:25 +02:00
|
|
|
emit("loading", true);
|
2023-10-15 20:25:14 +02:00
|
|
|
startAt = startAtInput;
|
2023-09-30 20:57:00 +02:00
|
|
|
setSource();
|
|
|
|
},
|
|
|
|
|
|
|
|
processVideoElement(video) {
|
2023-10-14 19:28:27 +02:00
|
|
|
destroyVideoElement();
|
2023-09-30 20:57:00 +02:00
|
|
|
videoElement = video;
|
|
|
|
setSource();
|
|
|
|
},
|
2023-10-01 17:34:37 +02:00
|
|
|
processContainerElement(container) {
|
|
|
|
containerElement = container;
|
|
|
|
},
|
2023-09-30 20:57:00 +02:00
|
|
|
|
|
|
|
pause() {
|
|
|
|
videoElement?.pause();
|
|
|
|
},
|
|
|
|
play() {
|
|
|
|
videoElement?.play();
|
|
|
|
},
|
2023-10-01 21:08:26 +02:00
|
|
|
setSeeking(active) {
|
2023-10-02 21:04:40 +02:00
|
|
|
if (active === isSeeking) return;
|
|
|
|
isSeeking = active;
|
|
|
|
|
2023-10-01 21:08:26 +02:00
|
|
|
// if it was playing when starting to seek, play again
|
|
|
|
if (!active) {
|
|
|
|
if (!isPausedBeforeSeeking) this.play();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
isPausedBeforeSeeking = videoElement?.paused ?? true;
|
|
|
|
this.pause();
|
|
|
|
},
|
|
|
|
setTime(t) {
|
|
|
|
if (!videoElement) return;
|
|
|
|
// clamp time between 0 and max duration
|
|
|
|
let time = Math.min(t, videoElement.duration);
|
|
|
|
time = Math.max(0, time);
|
|
|
|
|
|
|
|
if (Number.isNaN(time)) return;
|
|
|
|
emit("time", time);
|
|
|
|
videoElement.currentTime = time;
|
|
|
|
},
|
|
|
|
async setVolume(v) {
|
|
|
|
if (!videoElement) return;
|
|
|
|
|
|
|
|
// clamp time between 0 and 1
|
|
|
|
let volume = Math.min(v, 1);
|
|
|
|
volume = Math.max(0, volume);
|
2023-10-11 22:09:28 +02:00
|
|
|
videoElement.muted = volume === 0; // Muted attribute is always supported
|
2023-10-01 21:08:26 +02:00
|
|
|
|
|
|
|
// update state
|
2023-10-11 22:09:28 +02:00
|
|
|
const isChangeable = await canChangeVolume();
|
|
|
|
if (isChangeable) {
|
|
|
|
videoElement.volume = volume;
|
|
|
|
} else {
|
|
|
|
// For browsers where it can't be changed
|
|
|
|
emit("volumechange", volume === 0 ? 0 : 1);
|
|
|
|
}
|
2023-10-01 21:08:26 +02:00
|
|
|
},
|
2023-10-01 17:34:37 +02:00
|
|
|
toggleFullscreen() {
|
|
|
|
if (isFullscreen) {
|
|
|
|
isFullscreen = false;
|
|
|
|
emit("fullscreen", isFullscreen);
|
2023-10-18 15:17:46 +02:00
|
|
|
emit("needstrack", false);
|
2023-10-01 17:34:37 +02:00
|
|
|
if (!fscreen.fullscreenElement) return;
|
|
|
|
fscreen.exitFullscreen();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// enter fullscreen
|
|
|
|
isFullscreen = true;
|
|
|
|
emit("fullscreen", isFullscreen);
|
|
|
|
if (!canFullscreen() || fscreen.fullscreenElement) return;
|
|
|
|
if (canFullscreenAnyElement()) {
|
|
|
|
if (containerElement) fscreen.requestFullscreen(containerElement);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (canWebkitFullscreen()) {
|
2023-10-18 15:17:46 +02:00
|
|
|
if (videoElement) {
|
|
|
|
emit("needstrack", true);
|
|
|
|
(videoElement as any).webkitEnterFullscreen();
|
|
|
|
}
|
2023-10-01 17:34:37 +02:00
|
|
|
}
|
|
|
|
},
|
2023-09-30 20:57:00 +02:00
|
|
|
};
|
|
|
|
}
|