mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 07:11:50 +01:00
implement more progres controls
This commit is contained in:
parent
c5a8065db9
commit
a0c24209bb
14
src/video/components/actions/LoadingAction.tsx
Normal file
14
src/video/components/actions/LoadingAction.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Spinner } from "@/components/layout/Spinner";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
|
|
||||||
|
export function LoadingAction() {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const mediaPlaying = useMediaPlaying(descriptor);
|
||||||
|
|
||||||
|
const isLoading = mediaPlaying.isFirstLoading || mediaPlaying.isLoading;
|
||||||
|
|
||||||
|
if (!isLoading) return null;
|
||||||
|
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
32
src/video/components/actions/MiddlePauseAction.tsx
Normal file
32
src/video/components/actions/MiddlePauseAction.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
export function MiddlePauseAction() {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
const mediaPlaying = useMediaPlaying(descriptor);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (mediaPlaying?.isPlaying) controls.pause();
|
||||||
|
else controls.play();
|
||||||
|
}, [controls, mediaPlaying]);
|
||||||
|
|
||||||
|
if (mediaPlaying.hasPlayedOnce) return null;
|
||||||
|
if (mediaPlaying.isPlaying) return null;
|
||||||
|
if (mediaPlaying.isFirstLoading) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={handleClick}
|
||||||
|
className="group pointer-events-auto flex h-16 w-16 items-center justify-center rounded-full bg-denim-400 text-white transition-[background-color,transform] hover:scale-125 hover:bg-denim-500 active:scale-100"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={Icons.PLAY}
|
||||||
|
className="text-2xl transition-transform group-hover:scale-125"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
81
src/video/components/actions/ProgressAction.tsx
Normal file
81
src/video/components/actions/ProgressAction.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
makePercentage,
|
||||||
|
makePercentageString,
|
||||||
|
useProgressBar,
|
||||||
|
} from "@/hooks/useProgressBar";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useProgress } from "@/video/state/logic/progress";
|
||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export function ProgressAction() {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
const videoTime = useProgress(descriptor);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const dragRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const commitTime = useCallback(
|
||||||
|
(percentage) => {
|
||||||
|
controls.setTime(percentage * videoTime.duration);
|
||||||
|
},
|
||||||
|
[controls, videoTime]
|
||||||
|
);
|
||||||
|
const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
|
||||||
|
ref,
|
||||||
|
commitTime
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO make dragging update timer
|
||||||
|
useEffect(() => {
|
||||||
|
if (dragRef.current === dragging) return;
|
||||||
|
dragRef.current = dragging;
|
||||||
|
controls.setSeeking(dragging);
|
||||||
|
}, [dragRef, dragging, controls]);
|
||||||
|
|
||||||
|
let watchProgress = makePercentageString(
|
||||||
|
makePercentage((videoTime.time / videoTime.duration) * 100)
|
||||||
|
);
|
||||||
|
if (dragging)
|
||||||
|
watchProgress = makePercentageString(makePercentage(dragPercentage));
|
||||||
|
|
||||||
|
const bufferProgress = makePercentageString(
|
||||||
|
makePercentage((videoTime.buffered / videoTime.duration) * 100)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group pointer-events-auto w-full cursor-pointer rounded-full px-2">
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="-my-3 flex h-8 items-center"
|
||||||
|
onMouseDown={dragMouseDown}
|
||||||
|
onTouchStart={dragMouseDown}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`relative h-1 flex-1 rounded-full bg-gray-500 bg-opacity-50 transition-[height] duration-100 group-hover:h-2 ${
|
||||||
|
dragging ? "!h-2" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 left-0 flex items-center justify-end rounded-full bg-gray-300 bg-opacity-20"
|
||||||
|
style={{
|
||||||
|
width: bufferProgress,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 left-0 flex items-center justify-end rounded-full bg-bink-600"
|
||||||
|
style={{
|
||||||
|
width: watchProgress,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`absolute h-1 w-1 translate-x-1/2 rounded-full bg-white opacity-0 transition-[transform,opacity] group-hover:scale-[400%] group-hover:opacity-100 ${
|
||||||
|
dragging ? "!scale-[400%] !opacity-100" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
48
src/video/components/actions/SkipTimeAction.tsx
Normal file
48
src/video/components/actions/SkipTimeAction.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useProgress } from "@/video/state/logic/progress";
|
||||||
|
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SkipTimeBackwardAction() {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
const videoTime = useProgress(descriptor);
|
||||||
|
|
||||||
|
const skipBackward = () => {
|
||||||
|
controls.setTime(videoTime.time - 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VideoPlayerIconButton icon={Icons.SKIP_BACKWARD} onClick={skipBackward} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SkipTimeForwardAction() {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
const videoTime = useProgress(descriptor);
|
||||||
|
|
||||||
|
const skipForward = () => {
|
||||||
|
controls.setTime(videoTime.time + 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VideoPlayerIconButton icon={Icons.SKIP_FORWARD} onClick={skipForward} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SkipTimeAction(props: Props) {
|
||||||
|
return (
|
||||||
|
<div className={props.className}>
|
||||||
|
<div className="flex select-none items-center text-white">
|
||||||
|
<SkipTimeBackwardAction />
|
||||||
|
<SkipTimeForwardAction />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
50
src/video/components/actions/TimeAction.tsx
Normal file
50
src/video/components/actions/TimeAction.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useProgress } from "@/video/state/logic/progress";
|
||||||
|
|
||||||
|
function durationExceedsHour(secs: number): boolean {
|
||||||
|
return secs > 60 * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSeconds(secs: number, showHours = false): string {
|
||||||
|
if (Number.isNaN(secs)) {
|
||||||
|
if (showHours) return "0:00:00";
|
||||||
|
return "0:00";
|
||||||
|
}
|
||||||
|
|
||||||
|
let time = secs;
|
||||||
|
const seconds = Math.floor(time % 60);
|
||||||
|
|
||||||
|
time /= 60;
|
||||||
|
const minutes = Math.floor(time % 60);
|
||||||
|
|
||||||
|
time /= 60;
|
||||||
|
const hours = Math.floor(time);
|
||||||
|
|
||||||
|
const paddedSecs = seconds.toString().padStart(2, "0");
|
||||||
|
const paddedMins = minutes.toString().padStart(2, "0");
|
||||||
|
|
||||||
|
if (!showHours) return [paddedMins, paddedSecs].join(":");
|
||||||
|
return [hours, paddedMins, paddedSecs].join(":");
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
noDuration?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TimeAction(props: Props) {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const videoTime = useProgress(descriptor);
|
||||||
|
|
||||||
|
const hasHours = durationExceedsHour(videoTime.duration);
|
||||||
|
const time = formatSeconds(videoTime.time, hasHours);
|
||||||
|
const duration = formatSeconds(videoTime.duration, hasHours);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={props.className}>
|
||||||
|
<p className="select-none text-white">
|
||||||
|
{time} {props.noDuration ? "" : `/ ${duration}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
24
src/video/components/controllers/SourceController.tsx
Normal file
24
src/video/components/controllers/SourceController.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
interface SourceControllerProps {
|
||||||
|
source: string;
|
||||||
|
type: MWStreamType;
|
||||||
|
quality: MWStreamQuality;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SourceController(props: SourceControllerProps) {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
const didInitialize = useRef<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (didInitialize.current) return;
|
||||||
|
controls.setSource(props);
|
||||||
|
didInitialize.current = true;
|
||||||
|
}, [props, controls]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -19,12 +19,6 @@ export function VideoElementInternal() {
|
|||||||
}, [descriptor]);
|
}, [descriptor]);
|
||||||
|
|
||||||
// TODO autoplay and muted
|
// TODO autoplay and muted
|
||||||
return (
|
// this element is remotely controlled by a state provider
|
||||||
<video
|
return <video ref={ref} playsInline className="h-full w-full" />;
|
||||||
ref={ref}
|
|
||||||
playsInline
|
|
||||||
className="h-full w-full"
|
|
||||||
src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export type VideoPlayerEvent = "mediaplaying";
|
export type VideoPlayerEvent = "mediaplaying" | "source" | "progress";
|
||||||
|
|
||||||
function createEventString(id: string, event: VideoPlayerEvent): string {
|
function createEventString(id: string, event: VideoPlayerEvent): string {
|
||||||
return `_vid:::${id}:::${event}`;
|
return `_vid:::${id}:::${event}`;
|
||||||
|
@ -25,6 +25,8 @@ function initPlayer(): VideoPlayerState {
|
|||||||
isSeries: false,
|
isSeries: false,
|
||||||
},
|
},
|
||||||
canAirplay: false,
|
canAirplay: false,
|
||||||
|
stateProvider: null,
|
||||||
|
source: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,5 +11,14 @@ export function useControls(descriptor: string): VideoPlayerStateController {
|
|||||||
play() {
|
play() {
|
||||||
state.stateProvider?.play();
|
state.stateProvider?.play();
|
||||||
},
|
},
|
||||||
|
setSource(source) {
|
||||||
|
state.stateProvider?.setSource(source);
|
||||||
|
},
|
||||||
|
setSeeking(active) {
|
||||||
|
state.stateProvider?.setSeeking(active);
|
||||||
|
},
|
||||||
|
setTime(time) {
|
||||||
|
state.stateProvider?.setTime(time);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,9 @@ export type VideoMediaPlayingEvent = {
|
|||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
isSeeking: boolean;
|
||||||
hasPlayedOnce: boolean;
|
hasPlayedOnce: boolean;
|
||||||
|
isFirstLoading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getMediaPlayingFromState(
|
function getMediaPlayingFromState(
|
||||||
@ -18,6 +20,8 @@ function getMediaPlayingFromState(
|
|||||||
isLoading: state.isLoading,
|
isLoading: state.isLoading,
|
||||||
isPaused: state.isPaused,
|
isPaused: state.isPaused,
|
||||||
isPlaying: state.isPlaying,
|
isPlaying: state.isPlaying,
|
||||||
|
isSeeking: state.isSeeking,
|
||||||
|
isFirstLoading: state.isFirstLoading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
45
src/video/state/logic/progress.ts
Normal file
45
src/video/state/logic/progress.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getPlayerState } from "../cache";
|
||||||
|
import { listenEvent, sendEvent, unlistenEvent } from "../events";
|
||||||
|
import { VideoPlayerState } from "../types";
|
||||||
|
|
||||||
|
export type VideoProgressEvent = {
|
||||||
|
time: number;
|
||||||
|
duration: number;
|
||||||
|
buffered: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getProgressFromState(state: VideoPlayerState): VideoProgressEvent {
|
||||||
|
return {
|
||||||
|
time: state.time,
|
||||||
|
duration: state.duration,
|
||||||
|
buffered: state.buffered,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateProgress(descriptor: string, state: VideoPlayerState) {
|
||||||
|
sendEvent<VideoProgressEvent>(
|
||||||
|
descriptor,
|
||||||
|
"progress",
|
||||||
|
getProgressFromState(state)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProgress(descriptor: string): VideoProgressEvent {
|
||||||
|
const state = getPlayerState(descriptor);
|
||||||
|
const [data, setData] = useState<VideoProgressEvent>(
|
||||||
|
getProgressFromState(state)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function update(payload: CustomEvent<VideoProgressEvent>) {
|
||||||
|
setData(payload.detail);
|
||||||
|
}
|
||||||
|
listenEvent(descriptor, "progress", update);
|
||||||
|
return () => {
|
||||||
|
unlistenEvent(descriptor, "progress", update);
|
||||||
|
};
|
||||||
|
}, [descriptor]);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
40
src/video/state/logic/source.ts
Normal file
40
src/video/state/logic/source.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getPlayerState } from "../cache";
|
||||||
|
import { listenEvent, sendEvent, unlistenEvent } from "../events";
|
||||||
|
import { VideoPlayerState } from "../types";
|
||||||
|
|
||||||
|
export type VideoSourceEvent = {
|
||||||
|
source: null | {
|
||||||
|
quality: MWStreamQuality;
|
||||||
|
url: string;
|
||||||
|
type: MWStreamType;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSourceFromState(state: VideoPlayerState): VideoSourceEvent {
|
||||||
|
return {
|
||||||
|
source: state.source ? { ...state.source } : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSource(descriptor: string, state: VideoPlayerState) {
|
||||||
|
sendEvent<VideoSourceEvent>(descriptor, "source", getSourceFromState(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSource(descriptor: string): VideoSourceEvent {
|
||||||
|
const state = getPlayerState(descriptor);
|
||||||
|
const [data, setData] = useState<VideoSourceEvent>(getSourceFromState(state));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function update(payload: CustomEvent<VideoSourceEvent>) {
|
||||||
|
setData(payload.detail);
|
||||||
|
}
|
||||||
|
listenEvent(descriptor, "source", update);
|
||||||
|
return () => {
|
||||||
|
unlistenEvent(descriptor, "source", update);
|
||||||
|
};
|
||||||
|
}, [descriptor]);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
@ -1,6 +1,17 @@
|
|||||||
|
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
||||||
|
|
||||||
|
type VideoPlayerSource = {
|
||||||
|
source: string;
|
||||||
|
type: MWStreamType;
|
||||||
|
quality: MWStreamQuality;
|
||||||
|
} | null;
|
||||||
|
|
||||||
export type VideoPlayerStateController = {
|
export type VideoPlayerStateController = {
|
||||||
pause: () => void;
|
pause: () => void;
|
||||||
play: () => void;
|
play: () => void;
|
||||||
|
setSource: (source: VideoPlayerSource) => void;
|
||||||
|
setTime(time: number): void;
|
||||||
|
setSeeking(active: boolean): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VideoPlayerStateProvider = VideoPlayerStateController & {
|
export type VideoPlayerStateProvider = VideoPlayerStateController & {
|
||||||
|
@ -16,3 +16,12 @@ export function unsetStateProvider(descriptor: string) {
|
|||||||
const state = getPlayerState(descriptor);
|
const state = getPlayerState(descriptor);
|
||||||
state.stateProvider = null;
|
state.stateProvider = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleBuffered(time: number, buffered: TimeRanges): number {
|
||||||
|
for (let i = 0; i < buffered.length; i += 1) {
|
||||||
|
if (buffered.start(buffered.length - 1 - i) < time) {
|
||||||
|
return buffered.end(buffered.length - 1 - i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
import Hls from "hls.js";
|
||||||
|
import { MWStreamType } from "@/backend/helpers/streams";
|
||||||
import { getPlayerState } from "../cache";
|
import { getPlayerState } from "../cache";
|
||||||
import { updateMediaPlaying } from "../logic/mediaplaying";
|
import { updateMediaPlaying } from "../logic/mediaplaying";
|
||||||
import { VideoPlayerStateProvider } from "./providerTypes";
|
import { VideoPlayerStateProvider } from "./providerTypes";
|
||||||
|
import { updateProgress } from "../logic/progress";
|
||||||
|
import { handleBuffered } from "./utils";
|
||||||
|
|
||||||
export function createVideoStateProvider(
|
export function createVideoStateProvider(
|
||||||
descriptor: string,
|
descriptor: string,
|
||||||
player: HTMLVideoElement
|
playerEl: HTMLVideoElement
|
||||||
): VideoPlayerStateProvider {
|
): VideoPlayerStateProvider {
|
||||||
|
const player = playerEl;
|
||||||
const state = getPlayerState(descriptor);
|
const state = getPlayerState(descriptor);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -15,7 +20,72 @@ export function createVideoStateProvider(
|
|||||||
pause() {
|
pause() {
|
||||||
player.pause();
|
player.pause();
|
||||||
},
|
},
|
||||||
|
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.time = time;
|
||||||
|
updateProgress(descriptor, state);
|
||||||
|
},
|
||||||
|
setSeeking(active) {
|
||||||
|
// 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.isPaused;
|
||||||
|
this.pause();
|
||||||
|
},
|
||||||
|
setSource(source) {
|
||||||
|
if (!source) {
|
||||||
|
player.src = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source?.type === MWStreamType.HLS) {
|
||||||
|
if (player.canPlayType("application/vnd.apple.mpegurl")) {
|
||||||
|
player.src = source.source;
|
||||||
|
} else {
|
||||||
|
// HLS support
|
||||||
|
if (!Hls.isSupported()) {
|
||||||
|
state.error = {
|
||||||
|
name: `Not supported`,
|
||||||
|
description: "Your browser does not support HLS video",
|
||||||
|
};
|
||||||
|
// TODO dispatch error
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hls = new Hls({ enableWorker: false });
|
||||||
|
|
||||||
|
hls.on(Hls.Events.ERROR, (event, data) => {
|
||||||
|
if (data.fatal) {
|
||||||
|
state.error = {
|
||||||
|
name: `error ${data.details}`,
|
||||||
|
description: data.error?.message ?? "Something went wrong",
|
||||||
|
};
|
||||||
|
// TODO dispatch error
|
||||||
|
}
|
||||||
|
console.error("HLS error", data);
|
||||||
|
});
|
||||||
|
|
||||||
|
hls.attachMedia(player);
|
||||||
|
hls.loadSource(source.source);
|
||||||
|
}
|
||||||
|
} else if (source.type === MWStreamType.MP4) {
|
||||||
|
player.src = source.source;
|
||||||
|
}
|
||||||
|
},
|
||||||
providerStart() {
|
providerStart() {
|
||||||
|
// TODO stored volume
|
||||||
const pause = () => {
|
const pause = () => {
|
||||||
state.isPaused = true;
|
state.isPaused = true;
|
||||||
state.isPlaying = false;
|
state.isPlaying = false;
|
||||||
@ -28,13 +98,56 @@ export function createVideoStateProvider(
|
|||||||
state.hasPlayedOnce = true;
|
state.hasPlayedOnce = true;
|
||||||
updateMediaPlaying(descriptor, state);
|
updateMediaPlaying(descriptor, state);
|
||||||
};
|
};
|
||||||
|
const waiting = () => {
|
||||||
|
state.isLoading = true;
|
||||||
|
updateMediaPlaying(descriptor, state);
|
||||||
|
};
|
||||||
|
const seeking = () => {
|
||||||
|
state.isSeeking = true;
|
||||||
|
updateMediaPlaying(descriptor, state);
|
||||||
|
};
|
||||||
|
const seeked = () => {
|
||||||
|
state.isSeeking = false;
|
||||||
|
updateMediaPlaying(descriptor, state);
|
||||||
|
};
|
||||||
|
const loadedmetadata = () => {
|
||||||
|
state.duration = player.duration;
|
||||||
|
updateProgress(descriptor, state);
|
||||||
|
};
|
||||||
|
const timeupdate = () => {
|
||||||
|
state.duration = player.duration;
|
||||||
|
state.time = player.currentTime;
|
||||||
|
updateProgress(descriptor, state);
|
||||||
|
};
|
||||||
|
const progress = () => {
|
||||||
|
state.buffered = handleBuffered(player.currentTime, player.buffered);
|
||||||
|
updateProgress(descriptor, state);
|
||||||
|
};
|
||||||
|
const canplay = () => {
|
||||||
|
state.isFirstLoading = false;
|
||||||
|
updateMediaPlaying(descriptor, state);
|
||||||
|
};
|
||||||
|
|
||||||
player.addEventListener("pause", pause);
|
player.addEventListener("pause", pause);
|
||||||
player.addEventListener("playing", playing);
|
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);
|
||||||
return {
|
return {
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
player.removeEventListener("pause", pause);
|
player.removeEventListener("pause", pause);
|
||||||
player.removeEventListener("playing", playing);
|
player.removeEventListener("playing", playing);
|
||||||
|
player.removeEventListener("seeking", seeking);
|
||||||
|
player.removeEventListener("seeked", seeked);
|
||||||
|
player.removeEventListener("timeupdate", timeupdate);
|
||||||
|
player.removeEventListener("loadedmetadata", loadedmetadata);
|
||||||
|
player.removeEventListener("progress", progress);
|
||||||
|
player.removeEventListener("waiting", waiting);
|
||||||
|
player.removeEventListener("canplay", canplay);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
||||||
import { VideoPlayerStateProvider } from "./providers/providerTypes";
|
import { VideoPlayerStateProvider } from "./providers/providerTypes";
|
||||||
|
|
||||||
export type VideoPlayerState = {
|
export type VideoPlayerState = {
|
||||||
@ -30,10 +31,16 @@ export type VideoPlayerState = {
|
|||||||
episodes?: { id: string; number: number; title: string }[];
|
episodes?: { id: string; number: number; title: string }[];
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
error: null | {
|
error: null | {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
canAirplay: boolean;
|
canAirplay: boolean;
|
||||||
stateProvider: VideoPlayerStateProvider | null;
|
stateProvider: VideoPlayerStateProvider | null;
|
||||||
|
source: null | {
|
||||||
|
quality: MWStreamQuality;
|
||||||
|
url: string;
|
||||||
|
type: MWStreamType;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,14 @@
|
|||||||
// } from "@/hooks/useChromecastAvailable";
|
// } from "@/hooks/useChromecastAvailable";
|
||||||
// import { useEffect, useRef } from "react";
|
// import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
||||||
|
import { LoadingAction } from "@/video/components/actions/LoadingAction";
|
||||||
|
import { MiddlePauseAction } from "@/video/components/actions/MiddlePauseAction";
|
||||||
import { PauseAction } from "@/video/components/actions/PauseAction";
|
import { PauseAction } from "@/video/components/actions/PauseAction";
|
||||||
|
import { ProgressAction } from "@/video/components/actions/ProgressAction";
|
||||||
|
import { SkipTimeAction } from "@/video/components/actions/SkipTimeAction";
|
||||||
|
import { TimeAction } from "@/video/components/actions/TimeAction";
|
||||||
|
import { SourceController } from "@/video/components/controllers/SourceController";
|
||||||
import { VideoPlayerBase } from "@/video/components/VideoPlayerBase";
|
import { VideoPlayerBase } from "@/video/components/VideoPlayerBase";
|
||||||
|
|
||||||
// function ChromeCastButton() {
|
// function ChromeCastButton() {
|
||||||
@ -25,6 +32,16 @@ export function TestView() {
|
|||||||
return (
|
return (
|
||||||
<VideoPlayerBase>
|
<VideoPlayerBase>
|
||||||
<PauseAction />
|
<PauseAction />
|
||||||
|
<SourceController
|
||||||
|
quality={MWStreamQuality.QUNKNOWN}
|
||||||
|
source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4"
|
||||||
|
type={MWStreamType.MP4}
|
||||||
|
/>
|
||||||
|
<MiddlePauseAction />
|
||||||
|
<ProgressAction />
|
||||||
|
<LoadingAction />
|
||||||
|
<TimeAction />
|
||||||
|
<SkipTimeAction />
|
||||||
</VideoPlayerBase>
|
</VideoPlayerBase>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user