A small web app for watching movies and shows easily
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

340 lines
11 KiB

import fscreen from "fscreen";
import { revokeCaptionBlob } from "@/backend/helpers/captions";
import {
canChangeVolume,
canFullscreen,
canFullscreenAnyElement,
canWebkitFullscreen,
} from "@/utils/detectFeatures";
import {
getStoredVolume,
setStoredVolume,
} from "@/video/components/hooks/volumeStore";
import { updateInterface } from "@/video/state/logic/interface";
import { updateSource } from "@/video/state/logic/source";
import { resetStateForSource } from "@/video/state/providers/helpers";
import { VideoPlayerStateProvider } from "./providerTypes";
import { SettingsStore } from "../../../state/settings/store";
import { getPlayerState } from "../cache";
import { updateMediaPlaying } from "../logic/mediaplaying";
import { updateProgress } from "../logic/progress";
// TODO HLS for casting?
export function createCastingStateProvider(
descriptor: string
): VideoPlayerStateProvider {
const state = getPlayerState(descriptor);
const ins = state.casting.instance;
const player = state.casting.player;
const controller = state.casting.controller;
return {
getId() {
return "casting";
},
play() {
if (state.mediaPlaying.isPaused) controller?.playOrPause();
},
pause() {
if (state.mediaPlaying.isPlaying) controller?.playOrPause();
},
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() {
// no airplay while casting
},
setTime(t) {
// clamp time between 0 and max duration
let time = Math.min(t, player?.duration ?? 0);
time = Math.max(0, time);
if (Number.isNaN(time)) return;
// update state
if (player) player.currentTime = time;
state.progress.time = time;
controller?.seek();
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();
},
togglePictureInPicture() {
// no picture in picture while casting
},
setPlaybackSpeed(num) {
const mediaInfo = new chrome.cast.media.MediaInfo(
state.meta?.meta.meta.id ?? "video",
"video/mp4"
);
(mediaInfo as any).contentUrl = state.source?.url;
mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED;
mediaInfo.metadata = new chrome.cast.media.MovieMediaMetadata();
mediaInfo.metadata.title = state.meta?.meta.meta.title ?? "";
mediaInfo.customData = {
playbackRate: num,
};
const request = new chrome.cast.media.LoadRequest(mediaInfo);
request.autoplay = true;
const session = ins?.getCurrentSession();
session?.loadMedia(request);
},
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) player.volumeLevel = volume;
state.mediaPlaying.volume = volume;
controller?.setVolumeLevel();
updateMediaPlaying(descriptor, state);
// update localstorage
setStoredVolume(volume);
},
setSource(source) {
if (!source) {
resetStateForSource(descriptor, state);
controller?.stop();
state.source = null;
updateSource(descriptor, state);
return;
}
const movieMeta = new chrome.cast.media.MovieMediaMetadata();
movieMeta.title = state.meta?.meta.meta.title ?? "";
const mediaInfo = new chrome.cast.media.MediaInfo(
state.meta?.meta.meta.id ?? "video",
"video/mp4"
);
(mediaInfo as any).contentUrl = source?.source;
mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED;
mediaInfo.metadata = movieMeta;
const loadRequest = new chrome.cast.media.LoadRequest(mediaInfo);
loadRequest.autoplay = true;
// start where video left off before cast
loadRequest.currentTime = state.progress.time;
let captions = null;
if (state.source?.caption?.id) {
let captionIndex: number | undefined;
const linkedCaptions = state.meta?.captions;
const captionLangIso = state.source?.caption?.id.slice(7);
let trackContentId = "";
if (linkedCaptions) {
for (let index = 0; index < linkedCaptions.length; index += 1) {
if (captionLangIso === linkedCaptions[index].langIso) {
captionIndex = index;
break;
}
}
if (captionIndex) {
trackContentId = linkedCaptions[captionIndex].url;
}
}
const subtitles = new chrome.cast.media.Track(
1,
chrome.cast.media.TrackType.TEXT
);
subtitles.trackContentId = trackContentId;
subtitles.trackContentType = "text/vtt";
subtitles.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
subtitles.name = "Subtitles";
subtitles.language = "en";
const tracks = [subtitles];
mediaInfo.tracks = tracks;
mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle();
mediaInfo.textTrackStyle.backgroundColor =
SettingsStore.get().captionSettings.style.backgroundColor;
mediaInfo.textTrackStyle.foregroundColor =
SettingsStore.get().captionSettings.style.color.concat("ff"); // needs to be in RGBA format
mediaInfo.textTrackStyle.fontScale =
SettingsStore.get().captionSettings.style.fontSize / 40; // scale factor way smaller than fortSize
loadRequest.activeTrackIds = [1];
captions = {
url: state.source.caption.url,
id: state.source.caption.id,
};
}
const session = ins?.getCurrentSession();
session?.loadMedia(loadRequest);
// update state
state.source = {
quality: source.quality,
type: source.type,
url: source.source,
caption: captions,
embedId: source.embedId,
providerId: source.providerId,
thumbnails: [],
};
resetStateForSource(descriptor, state);
updateSource(descriptor, state);
},
setCaption(id, url) {
if (state.source) {
revokeCaptionBlob(state.source.caption?.url);
state.source.caption = {
id,
url,
};
// media has to be loaded again to use the new captions
this.setSource({
quality: state.source.quality,
source: state.source.url,
type: state.source.type,
embedId: state.source.embedId,
providerId: state.source.providerId,
});
updateSource(descriptor, state);
}
},
clearCaption() {
if (state.source) {
revokeCaptionBlob(state.source.caption?.url);
state.source.caption = null;
const tracksInfoRequest = new chrome.cast.media.EditTracksInfoRequest(
[]
);
const session = ins?.getCurrentSession();
session?.getMediaSession()?.editTracksInfo(
tracksInfoRequest,
() => console.log("Captions cleared"),
(error) => console.log(error)
);
updateSource(descriptor, state);
}
},
providerStart() {
this.setVolume(getStoredVolume());
const listenToEvents = async (
e: cast.framework.RemotePlayerChangedEvent
) => {
switch (e.field) {
case "volumeLevel":
if (await canChangeVolume()) {
state.mediaPlaying.volume = e.value;
updateMediaPlaying(descriptor, state);
}
break;
case "currentTime":
state.progress.time = e.value;
updateProgress(descriptor, state);
break;
case "mediaInfo":
if (e.value) {
state.progress.duration = e.value.duration;
updateProgress(descriptor, state);
}
break;
case "playerState":
state.mediaPlaying.isLoading = e.value === "BUFFERING";
state.mediaPlaying.isPaused = e.value !== "PLAYING";
state.mediaPlaying.isPlaying = e.value === "PLAYING";
if (e.value === "PLAYING") {
state.mediaPlaying.hasPlayedOnce = true;
state.mediaPlaying.isFirstLoading = false;
}
updateMediaPlaying(descriptor, state);
break;
case "isMuted":
state.mediaPlaying.volume = e.value ? 1 : 0;
updateMediaPlaying(descriptor, state);
break;
case "displayStatus":
case "canSeek":
case "title":
case "isPaused":
break;
default:
console.log(e.type, e.field, e.value);
break;
}
};
const fullscreenchange = () => {
state.interface.isFullscreen = !!document.fullscreenElement;
updateInterface(descriptor, state);
};
const isFocused = (evt: any) => {
state.interface.isFocused = evt.type !== "mouseleave";
updateInterface(descriptor, state);
};
controller?.addEventListener(
cast.framework.RemotePlayerEventType.ANY_CHANGE,
listenToEvents
);
state.wrapperElement?.addEventListener("click", isFocused);
state.wrapperElement?.addEventListener("mouseenter", isFocused);
state.wrapperElement?.addEventListener("mouseleave", isFocused);
fscreen.addEventListener("fullscreenchange", fullscreenchange);
if (state.source)
this.setSource({
quality: state.source.quality,
source: state.source.url,
type: state.source.type,
embedId: state.source.embedId,
providerId: state.source.providerId,
});
return {
destroy: () => {
controller?.removeEventListener(
cast.framework.RemotePlayerEventType.ANY_CHANGE,
listenToEvents
);
state.wrapperElement?.removeEventListener("click", isFocused);
state.wrapperElement?.removeEventListener("mouseenter", isFocused);
state.wrapperElement?.removeEventListener("mouseleave", isFocused);
fscreen.removeEventListener("fullscreenchange", fullscreenchange);
ins?.endCurrentSession(true);
},
};
},
};
}