mirror of
https://github.com/movie-web/movie-web.git
synced 2024-06-03 01:38:46 +02:00
291 lines
7.8 KiB
TypeScript
291 lines
7.8 KiB
TypeScript
import fscreen from "fscreen";
|
|
|
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
|
import {
|
|
DisplayCaption,
|
|
DisplayInterface,
|
|
DisplayInterfaceEvents,
|
|
DisplayMeta,
|
|
} from "@/components/player/display/displayInterface";
|
|
import { LoadableSource } from "@/stores/player/utils/qualities";
|
|
import { processCdnLink } from "@/utils/cdn";
|
|
import {
|
|
canChangeVolume,
|
|
canFullscreen,
|
|
canFullscreenAnyElement,
|
|
} from "@/utils/detectFeatures";
|
|
import { makeEmitter } from "@/utils/events";
|
|
|
|
export interface ChromeCastDisplayInterfaceOptions {
|
|
controller: cast.framework.RemotePlayerController;
|
|
player: cast.framework.RemotePlayer;
|
|
instance: cast.framework.CastContext;
|
|
}
|
|
|
|
/*
|
|
** Chromecasting is unfinished, here is its limitations:
|
|
** 1. Captions - chromecast requires only VTT, but needs it from a URL. we only have SRT urls
|
|
** 2. HLS - we've having some issues with content types. sometimes it loads, sometimes it doesn't
|
|
*/
|
|
|
|
export function makeChromecastDisplayInterface(
|
|
ops: ChromeCastDisplayInterfaceOptions,
|
|
): DisplayInterface {
|
|
const { emit, on, off } = makeEmitter<DisplayInterfaceEvents>();
|
|
let isPaused = false;
|
|
let playbackRate = 1;
|
|
let source: LoadableSource | null = null;
|
|
let videoElement: HTMLVideoElement | null = null;
|
|
let containerElement: HTMLElement | null = null;
|
|
let isFullscreen = false;
|
|
let isPausedBeforeSeeking = false;
|
|
let isSeeking = false;
|
|
let startAt = 0;
|
|
let meta: DisplayMeta = {
|
|
title: "",
|
|
type: MWMediaType.MOVIE,
|
|
};
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
let caption: DisplayCaption | null = null;
|
|
|
|
function listenForEvents() {
|
|
const listen = async (e: cast.framework.RemotePlayerChangedEvent) => {
|
|
switch (e.field) {
|
|
case "volumeLevel":
|
|
if (await canChangeVolume()) emit("volumechange", e.value);
|
|
break;
|
|
case "currentTime":
|
|
emit("time", e.value);
|
|
break;
|
|
case "duration":
|
|
emit("duration", e.value ?? 0);
|
|
break;
|
|
case "mediaInfo":
|
|
if (e.value) emit("duration", e.value.duration ?? 0);
|
|
break;
|
|
case "playerState":
|
|
emit("loading", e.value === "BUFFERING");
|
|
if (e.value === "PLAYING") emit("play", undefined);
|
|
else if (e.value === "PAUSED") emit("pause", undefined);
|
|
isPaused = e.value === "PAUSED";
|
|
break;
|
|
case "isMuted":
|
|
emit("volumechange", e.value ? 1 : 0);
|
|
break;
|
|
case "displayStatus":
|
|
case "canSeek":
|
|
case "title":
|
|
case "isPaused":
|
|
case "canPause":
|
|
case "isMediaLoaded":
|
|
case "statusText":
|
|
case "isConnected":
|
|
case "displayName":
|
|
case "canControlVolume":
|
|
case "savedPlayerState":
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
ops.controller?.addEventListener(
|
|
cast.framework.RemotePlayerEventType.ANY_CHANGE,
|
|
listen,
|
|
);
|
|
return () => {
|
|
ops.controller?.removeEventListener(
|
|
cast.framework.RemotePlayerEventType.ANY_CHANGE,
|
|
listen,
|
|
);
|
|
};
|
|
}
|
|
|
|
function setupSource() {
|
|
if (!source) {
|
|
ops.controller?.stop();
|
|
return;
|
|
}
|
|
|
|
let type = "video/mp4";
|
|
if (source.type === "hls") type = "application/x-mpegurl";
|
|
|
|
const metaData = new chrome.cast.media.GenericMediaMetadata();
|
|
metaData.title = meta.title;
|
|
|
|
const mediaInfo = new chrome.cast.media.MediaInfo("video", type);
|
|
(mediaInfo as any).contentUrl = processCdnLink(source.url);
|
|
mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED;
|
|
mediaInfo.metadata = metaData;
|
|
mediaInfo.customData = {
|
|
playbackRate,
|
|
};
|
|
|
|
const request = new chrome.cast.media.LoadRequest(mediaInfo);
|
|
request.autoplay = true;
|
|
request.currentTime = startAt;
|
|
|
|
if (source.type === "hls") {
|
|
const staticMedia = chrome.cast.media as any;
|
|
const media = request.media as any;
|
|
media.hlsSegmentFormat = staticMedia.HlsSegmentFormat.FMP4;
|
|
media.hlsVideoSegmentFormat = staticMedia.HlsVideoSegmentFormat.FMP4;
|
|
}
|
|
|
|
const session = ops.instance.getCurrentSession();
|
|
session?.loadMedia(request);
|
|
}
|
|
|
|
function setSource() {
|
|
if (!videoElement || !source) return;
|
|
setupSource();
|
|
}
|
|
|
|
function destroyVideoElement() {
|
|
if (videoElement) videoElement = null;
|
|
}
|
|
|
|
function fullscreenChange() {
|
|
isFullscreen =
|
|
!!document.fullscreenElement || // other browsers
|
|
!!(document as any).webkitFullscreenElement; // safari
|
|
emit("fullscreen", isFullscreen);
|
|
if (!isFullscreen) emit("needstrack", false);
|
|
}
|
|
fscreen.addEventListener("fullscreenchange", fullscreenChange);
|
|
|
|
// start listening immediately
|
|
const stopListening = listenForEvents();
|
|
|
|
return {
|
|
on,
|
|
off,
|
|
getType() {
|
|
return "casting";
|
|
},
|
|
destroy: () => {
|
|
stopListening();
|
|
destroyVideoElement();
|
|
fscreen.removeEventListener("fullscreenchange", fullscreenChange);
|
|
},
|
|
load(loadOps) {
|
|
source = loadOps.source;
|
|
emit("loading", true);
|
|
startAt = loadOps.startAt;
|
|
setSource();
|
|
},
|
|
changeQuality() {
|
|
// cant control qualities
|
|
},
|
|
setCaption(newCaption) {
|
|
caption = newCaption;
|
|
setSource();
|
|
},
|
|
|
|
processVideoElement(video) {
|
|
destroyVideoElement();
|
|
videoElement = video;
|
|
setSource();
|
|
},
|
|
processContainerElement(container) {
|
|
containerElement = container;
|
|
},
|
|
setMeta(data) {
|
|
meta = data;
|
|
setSource();
|
|
},
|
|
|
|
pause() {
|
|
if (!isPaused) {
|
|
ops.controller.playOrPause();
|
|
isPaused = true;
|
|
}
|
|
},
|
|
play() {
|
|
if (isPaused) {
|
|
ops.controller.playOrPause();
|
|
isPaused = false;
|
|
}
|
|
},
|
|
setSeeking(active) {
|
|
if (active === isSeeking) return;
|
|
isSeeking = active;
|
|
|
|
// if it was playing when starting to seek, play again
|
|
if (!active) {
|
|
if (!isPausedBeforeSeeking) this.play();
|
|
return;
|
|
}
|
|
|
|
isPausedBeforeSeeking = isPaused ?? true;
|
|
this.pause();
|
|
},
|
|
setTime(t) {
|
|
if (!videoElement) return;
|
|
// clamp time between 0 and max duration
|
|
let time = Math.min(t, ops.player.duration);
|
|
time = Math.max(0, time);
|
|
|
|
if (Number.isNaN(time)) return;
|
|
emit("time", time);
|
|
ops.player.currentTime = time;
|
|
ops.controller.seek();
|
|
},
|
|
async setVolume(v) {
|
|
// clamp time between 0 and 1
|
|
let volume = Math.min(v, 1);
|
|
volume = Math.max(0, volume);
|
|
|
|
// update state
|
|
const isChangeable = await canChangeVolume();
|
|
if (isChangeable) {
|
|
ops.player.volumeLevel = volume;
|
|
ops.controller.setVolumeLevel();
|
|
emit("volumechange", volume);
|
|
} else {
|
|
// For browsers where it can't be changed
|
|
emit("volumechange", volume === 0 ? 0 : 1);
|
|
}
|
|
},
|
|
toggleFullscreen() {
|
|
if (isFullscreen) {
|
|
isFullscreen = false;
|
|
emit("fullscreen", isFullscreen);
|
|
emit("needstrack", false);
|
|
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);
|
|
}
|
|
},
|
|
togglePictureInPicture() {
|
|
// Can't PIP while Chromecasting
|
|
},
|
|
startAirplay() {
|
|
// cant airplay while chromecasting
|
|
},
|
|
setPlaybackRate(rate) {
|
|
playbackRate = rate;
|
|
setSource();
|
|
},
|
|
getCaptionList() {
|
|
return [];
|
|
},
|
|
getSubtitleTracks() {
|
|
return [];
|
|
},
|
|
async setSubtitlePreference() {
|
|
return Promise.resolve();
|
|
},
|
|
changeAudioTrack() {
|
|
// cant change audio tracks
|
|
},
|
|
};
|
|
}
|