mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
very rudementary chromecasting
This commit is contained in:
parent
43d4869f7e
commit
18b434c9ac
5 changed files with 254 additions and 10 deletions
|
@ -176,7 +176,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||
}
|
||||
|
||||
function unloadSource() {
|
||||
if (videoElement) videoElement.removeAttribute("src");
|
||||
if (videoElement) videoElement.src = "";
|
||||
if (hls) {
|
||||
hls.destroy();
|
||||
hls = null;
|
||||
|
|
190
src/components/player/display/chromecast.ts
Normal file
190
src/components/player/display/chromecast.ts
Normal file
|
@ -0,0 +1,190 @@
|
|||
import fscreen from "fscreen";
|
||||
|
||||
import {
|
||||
DisplayInterface,
|
||||
DisplayInterfaceEvents,
|
||||
} from "@/components/player/display/displayInterface";
|
||||
import { LoadableSource } from "@/stores/player/utils/qualities";
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO check all functionality
|
||||
// TODO listen for events to update the state
|
||||
export function makeChromecastDisplayInterface(
|
||||
ops: ChromeCastDisplayInterfaceOptions
|
||||
): DisplayInterface {
|
||||
const { emit, on, off } = makeEmitter<DisplayInterfaceEvents>();
|
||||
const 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 automaticQuality = false;
|
||||
// let preferenceQuality: SourceQuality | null = null;
|
||||
|
||||
function setupSource() {
|
||||
if (!source) {
|
||||
ops.controller?.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.type === "hls") {
|
||||
// TODO hls support
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO movie meta
|
||||
const movieMeta = new chrome.cast.media.MovieMediaMetadata();
|
||||
movieMeta.title = "";
|
||||
|
||||
const mediaInfo = new chrome.cast.media.MediaInfo("video", "video/mp4");
|
||||
(mediaInfo as any).contentUrl = source.url;
|
||||
mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED;
|
||||
mediaInfo.metadata = movieMeta;
|
||||
mediaInfo.customData = {
|
||||
playbackRate,
|
||||
};
|
||||
|
||||
const request = new chrome.cast.media.LoadRequest(mediaInfo);
|
||||
request.autoplay = true;
|
||||
|
||||
ops.player.currentTime = startAt;
|
||||
const session = ops.instance.getCurrentSession();
|
||||
session?.loadMedia(request);
|
||||
ops.controller.seek();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return {
|
||||
on,
|
||||
off,
|
||||
destroy: () => {
|
||||
destroyVideoElement();
|
||||
fscreen.removeEventListener("fullscreenchange", fullscreenChange);
|
||||
},
|
||||
load(loadOps) {
|
||||
// automaticQuality = loadOps.automaticQuality;
|
||||
// preferenceQuality = loadOps.preferredQuality;
|
||||
source = loadOps.source;
|
||||
emit("loading", true);
|
||||
startAt = loadOps.startAt;
|
||||
setSource();
|
||||
},
|
||||
changeQuality(_newAutomaticQuality, _newPreferredQuality) {
|
||||
// if (source?.type !== "hls") return;
|
||||
// automaticQuality = newAutomaticQuality;
|
||||
// preferenceQuality = newPreferredQuality;
|
||||
},
|
||||
|
||||
processVideoElement(video) {
|
||||
destroyVideoElement();
|
||||
videoElement = video;
|
||||
setSource();
|
||||
},
|
||||
processContainerElement(container) {
|
||||
containerElement = container;
|
||||
},
|
||||
|
||||
pause() {
|
||||
if (!isPaused) ops.controller.playOrPause();
|
||||
},
|
||||
play() {
|
||||
if (isPaused) ops.controller.playOrPause();
|
||||
},
|
||||
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();
|
||||
} 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);
|
||||
}
|
||||
},
|
||||
startAirplay() {
|
||||
// cant airplay while chromecasting
|
||||
},
|
||||
setPlaybackRate(rate) {
|
||||
playbackRate = rate;
|
||||
setSource();
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { makeVideoElementDisplayInterface } from "@/components/player/display/base";
|
||||
import { makeChromecastDisplayInterface } from "@/components/player/display/chromecast";
|
||||
import { useChromecastAvailable } from "@/hooks/useChromecastAvailable";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
|
@ -8,8 +10,49 @@ export function CastingInternal() {
|
|||
const setController = usePlayerStore((s) => s.casting.setController);
|
||||
const setPlayer = usePlayerStore((s) => s.casting.setPlayer);
|
||||
const setIsCasting = usePlayerStore((s) => s.casting.setIsCasting);
|
||||
const isCasting = usePlayerStore((s) => s.interface.isCasting);
|
||||
const setDisplay = usePlayerStore((s) => s.setDisplay);
|
||||
const redisplaySource = usePlayerStore((s) => s.redisplaySource);
|
||||
const available = useChromecastAvailable();
|
||||
|
||||
const controller = usePlayerStore((s) => s.casting.controller);
|
||||
const player = usePlayerStore((s) => s.casting.player);
|
||||
const instance = usePlayerStore((s) => s.casting.instance);
|
||||
|
||||
const dataRef = useRef({
|
||||
controller,
|
||||
player,
|
||||
instance,
|
||||
});
|
||||
useEffect(() => {
|
||||
dataRef.current = {
|
||||
controller,
|
||||
player,
|
||||
instance,
|
||||
};
|
||||
}, [controller, player, instance]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCasting) {
|
||||
if (
|
||||
dataRef.current.controller &&
|
||||
dataRef.current.instance &&
|
||||
dataRef.current.player
|
||||
) {
|
||||
const newDisplay = makeChromecastDisplayInterface({
|
||||
controller: dataRef.current.controller,
|
||||
instance: dataRef.current.instance,
|
||||
player: dataRef.current.player,
|
||||
});
|
||||
setDisplay(newDisplay);
|
||||
redisplaySource(0); // TODO right start time
|
||||
}
|
||||
} else {
|
||||
const newDisplay = makeVideoElementDisplayInterface();
|
||||
setDisplay(newDisplay);
|
||||
}
|
||||
}, [isCasting, setDisplay, redisplaySource]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!available) return;
|
||||
|
||||
|
@ -20,23 +63,23 @@ export function CastingInternal() {
|
|||
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
|
||||
});
|
||||
|
||||
const player = new cast.framework.RemotePlayer();
|
||||
setPlayer(player);
|
||||
const controller = new cast.framework.RemotePlayerController(player);
|
||||
setController(controller);
|
||||
const newPlayer = new cast.framework.RemotePlayer();
|
||||
setPlayer(newPlayer);
|
||||
const newControlller = new cast.framework.RemotePlayerController(newPlayer);
|
||||
setController(newControlller);
|
||||
|
||||
function connectionChanged(e: cast.framework.RemotePlayerChangedEvent) {
|
||||
if (e.field === "isConnected") {
|
||||
setIsCasting(e.value);
|
||||
}
|
||||
}
|
||||
controller.addEventListener(
|
||||
newControlller.addEventListener(
|
||||
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
||||
connectionChanged
|
||||
);
|
||||
|
||||
return () => {
|
||||
controller.removeEventListener(
|
||||
newControlller.removeEventListener(
|
||||
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
||||
connectionChanged
|
||||
);
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ReactNode } from "react";
|
|||
import { BrandPill } from "@/components/layout/BrandPill";
|
||||
import { Player } from "@/components/player";
|
||||
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
|
||||
import { useChromecastAvailable } from "@/hooks/useChromecastAvailable";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
|
|
@ -63,6 +63,7 @@ export interface SourceSlice {
|
|||
setCaption(caption: Caption | null): void;
|
||||
setSourceId(id: string | null): void;
|
||||
enableAutomaticQuality(): void;
|
||||
redisplaySource(startAt: number): void;
|
||||
}
|
||||
|
||||
export function metaToScrapeMedia(meta: PlayerMeta): ScrapeMedia {
|
||||
|
@ -123,7 +124,6 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
|
|||
setSource(stream: SourceSliceSource, startAt: number) {
|
||||
let qualities: string[] = [];
|
||||
if (stream.type === "file") qualities = Object.keys(stream.qualities);
|
||||
const store = get();
|
||||
const qualityPreferences = useQualityStore.getState();
|
||||
const loadableStream = selectQuality(stream, qualityPreferences.quality);
|
||||
|
||||
|
@ -132,6 +132,18 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
|
|||
s.qualities = qualities as SourceQuality[];
|
||||
s.currentQuality = loadableStream.quality;
|
||||
});
|
||||
const store = get();
|
||||
store.redisplaySource(startAt);
|
||||
},
|
||||
redisplaySource(startAt: number) {
|
||||
const store = get();
|
||||
const quality = store.currentQuality;
|
||||
if (!store.source) return;
|
||||
const qualityPreferences = useQualityStore.getState();
|
||||
const loadableStream = selectQuality(store.source, {
|
||||
automaticQuality: qualityPreferences.quality.automaticQuality,
|
||||
lastChosenQuality: quality,
|
||||
});
|
||||
|
||||
store.display?.load({
|
||||
source: loadableStream.stream,
|
||||
|
|
Loading…
Reference in a new issue