Skip to content
Snippets Groups Projects
Verified Commit 8eb7a305 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Fix unnecessary re-renders

parent 18480083
Branches
No related tags found
No related merge requests found
Showing
with 295 additions and 111 deletions
...@@ -4,3 +4,24 @@ export interface Image { ...@@ -4,3 +4,24 @@ export interface Image {
language: string, language: string,
src: string src: string
} }
export function findImage(
images: Image[],
type: string,
languages: (string | null)[]
): Image | null {
const imageList = images.filter(it => it.kind === type)
for (let language of languages) {
const image = imageList.find(it => it.language === language);
if (image) {
return image;
}
}
if (languages.includes(null) && imageList.length) {
return imageList[0];
}
return null;
}
...@@ -5,33 +5,12 @@ import {createUseStyles} from "react-jss"; ...@@ -5,33 +5,12 @@ import {createUseStyles} from "react-jss";
import {useLocale} from "../../util/locale/LocalizedContext"; import {useLocale} from "../../util/locale/LocalizedContext";
import {useShowEpisodes} from "../../api/ApiHooks"; import {useShowEpisodes} from "../../api/ApiHooks";
import {MediaEpisode} from "./MediaEpisode"; import {MediaEpisode} from "./MediaEpisode";
import {Image} from "../../api/models/Image"; import {findImage} from "../../api/models/Image";
export interface Props { export interface Props {
item: Content item: Content
} }
function findImage(
images: Image[],
type: string,
languages: (string | null)[]
): Image | null {
const imageList = images.filter(it => it.kind === type)
for (let language of languages) {
const image = imageList.find(it => it.language === language);
if (image) {
return image;
}
}
if (languages.includes(null) && imageList.length) {
return imageList[0];
}
return null;
}
export function MediaEntry( export function MediaEntry(
{item}: Props {item}: Props
) { ) {
...@@ -43,10 +22,11 @@ export function MediaEntry( ...@@ -43,10 +22,11 @@ export function MediaEntry(
const title = getLocalizedName(item, locale); const title = getLocalizedName(item, locale);
const description = getLocalizedDescription(item, locale); const description = getLocalizedDescription(item, locale);
const rating = getLocalizedRating(item, locale); const rating = getLocalizedRating(item, locale);
const [logo, poster, backdrop] = useMemo(() => [ const [logo, poster, backdrop, still] = useMemo(() => [
findImage(item.images, "logo", [locale.language]), findImage(item.images, "logo", [locale.language]),
findImage(item.images, "poster", [locale.language, item.originalLanguage, null]), findImage(item.images, "poster", [locale.language, item.originalLanguage, null]),
findImage(item.images, "backdrop", [locale.language, item.originalLanguage, null]) findImage(item.images, "backdrop", [locale.language, item.originalLanguage, null]),
findImage(item.images, "still", [null, locale.language, item.originalLanguage])
], [item.images, item.originalLanguage, locale.language]); ], [item.images, item.originalLanguage, locale.language]);
return ( return (
...@@ -96,6 +76,13 @@ export function MediaEntry( ...@@ -96,6 +76,13 @@ export function MediaEntry(
src={backdrop.src} src={backdrop.src}
/> />
)} )}
{still && (
<img
className={classes.poster}
alt={`Movie still for ${title?.name}`}
src={still.src}
/>
)}
</div> </div>
) )
} }
......
...@@ -2,6 +2,9 @@ import {Instalment} from "../../api/models/Instalment"; ...@@ -2,6 +2,9 @@ import {Instalment} from "../../api/models/Instalment";
import {getLocalizedDescription, getLocalizedName} from "../../api/models/Content"; import {getLocalizedDescription, getLocalizedName} from "../../api/models/Content";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {useLocale} from "../../util/locale/LocalizedContext"; import {useLocale} from "../../util/locale/LocalizedContext";
import {useMemo} from "react";
import {findImage} from "../../api/models/Image";
import {createUseStyles} from "react-jss";
export interface Props { export interface Props {
item: Instalment, item: Instalment,
...@@ -9,9 +12,16 @@ export interface Props { ...@@ -9,9 +12,16 @@ export interface Props {
} }
export function MediaEpisode({item, disabled}: Props) { export function MediaEpisode({item, disabled}: Props) {
const classes = useStyles();
const locale = useLocale(); const locale = useLocale();
const episodeTitle = getLocalizedName(item.content, locale); const episodeTitle = getLocalizedName(item.content, locale);
const episodeDescription = getLocalizedDescription(item.content, locale); const episodeDescription = getLocalizedDescription(item.content, locale);
const [backdrop, still] = useMemo(() => [
findImage(item.content.images, "backdrop", [locale.language, item.content.originalLanguage, null]),
findImage(item.content.images, "still", [null, locale.language, item.content.originalLanguage])
], [item.content.images, item.content.originalLanguage, locale.language]);
const episodeImage = still || backdrop;
return ( return (
<li> <li>
...@@ -21,6 +31,13 @@ export function MediaEpisode({item, disabled}: Props) { ...@@ -21,6 +31,13 @@ export function MediaEpisode({item, disabled}: Props) {
<p><strong>{episodeTitle?.name}</strong></p> <p><strong>{episodeTitle?.name}</strong></p>
<p><strong>{episodeDescription?.tagline}</strong></p> <p><strong>{episodeDescription?.tagline}</strong></p>
<p>{episodeDescription?.overview}</p> <p>{episodeDescription?.overview}</p>
{episodeImage && (
<img
className={classes.poster}
alt={episodeTitle?.name}
src={episodeImage.src}
/>
)}
{disabled ? ( {disabled ? (
<p><u>Play</u></p> <p><u>Play</u></p>
) : ( ) : (
...@@ -29,3 +46,10 @@ export function MediaEpisode({item, disabled}: Props) { ...@@ -29,3 +46,10 @@ export function MediaEpisode({item, disabled}: Props) {
</li> </li>
) )
} }
const useStyles = createUseStyles({
poster: {
maxWidth: "20rem",
maxHeight: "20rem",
}
});
import {Fragment, useState} from "react"; import {ChangeEvent, Fragment, useCallback, useState} from "react";
import {createUseStyles} from "react-jss"; import {createUseStyles} from "react-jss";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {ContentMeta} from "../../api/models/dto/ContentMeta"; import {ContentMeta} from "../../api/models/dto/ContentMeta";
...@@ -6,8 +6,6 @@ import {Media} from "../../api/models/Media"; ...@@ -6,8 +6,6 @@ import {Media} from "../../api/models/Media";
import {getLocalizedDescription, getLocalizedName, getLocalizedRating} from "../../api/models/Content"; import {getLocalizedDescription, getLocalizedName, getLocalizedRating} from "../../api/models/Content";
import {formatDuration} from "../../util/formatDuration"; import {formatDuration} from "../../util/formatDuration";
import {useLocale} from "../../util/locale/LocalizedContext"; import {useLocale} from "../../util/locale/LocalizedContext";
import {useDuration} from "../../util/media/useDuration";
import {usePosition} from "../../util/media/usePosition";
import {SeekBar} from "./SeekBar"; import {SeekBar} from "./SeekBar";
import {VideoElement} from "./video/VideoElement"; import {VideoElement} from "./video/VideoElement";
import {PlayerApi} from "./video/PlayerApi"; import {PlayerApi} from "./video/PlayerApi";
...@@ -20,6 +18,8 @@ import {SubtitleRenderer} from "./subtitles/SubtitleRenderer"; ...@@ -20,6 +18,8 @@ import {SubtitleRenderer} from "./subtitles/SubtitleRenderer";
import {useShowEpisodes} from "../../api/ApiHooks"; import {useShowEpisodes} from "../../api/ApiHooks";
import {MediaEpisode} from "../main/MediaEpisode"; import {MediaEpisode} from "../main/MediaEpisode";
import {usePaused} from "../../util/media/usePaused"; import {usePaused} from "../../util/media/usePaused";
import {useCurrentTime} from "../../util/media/useCurrentTime";
import {useDuration} from "../../util/media/useDuration";
interface Props { interface Props {
meta: ContentMeta, meta: ContentMeta,
...@@ -27,8 +27,11 @@ interface Props { ...@@ -27,8 +27,11 @@ interface Props {
} }
export function Player( export function Player(
{meta, media}: Props props: Props
) { ) {
console.log("Rendering Player")
const {meta, media} = props;
const {content, instalment} = meta; const {content, instalment} = meta;
const classes = useStyles(); const classes = useStyles();
const locale = useLocale(); const locale = useLocale();
...@@ -44,18 +47,43 @@ export function Player( ...@@ -44,18 +47,43 @@ export function Player(
const [playerApi, setPlayerApi] = useState<PlayerApi | null>(null); const [playerApi, setPlayerApi] = useState<PlayerApi | null>(null);
const [mousePosition, setMousePosition] = useState<MousePosition | null>(null); const [mousePosition, setMousePosition] = useState<MousePosition | null>(null);
const [volume, setVolume] = useState<number>(1);
const paused = usePaused(playerApi); const paused = usePaused(playerApi);
const position = usePosition(playerApi); const currentTime = useCurrentTime(playerApi)
const duration = useDuration(playerApi); const duration = useDuration(playerApi);
const [audioTracks, currentTrack, setCurrentTrack] = useAudioTracks(playerApi); const [audioTracks, currentTrack, setCurrentTrack] = useAudioTracks(playerApi);
useDebugInfo("player", playerApi); useDebugInfo("player", playerApi);
useDebugInfo("content", content); useDebugInfo("content", content);
useDebugInfo("subtitle", subtitle); useDebugInfo("subtitle", subtitle);
useDebugInfo("position", position); useDebugInfo("currentTime", currentTime);
useDebugInfo("duration", duration); useDebugInfo("duration", duration);
useDebugInfo("audioTracks", audioTracks); useDebugInfo("audioTracks", audioTracks);
const onPause = useCallback(() => {
if (playerApi) {
playerApi.pause()
}
}, [playerApi]);
const onPlay = useCallback(() => {
if (playerApi) {
playerApi.play()
}
}, [playerApi]);
const onFastForward = useCallback(() => {
if (playerApi) {
playerApi.setCurrentTime(playerApi.getCurrentTime() + 10)
}
}, [playerApi]);
const onRewind = useCallback(() => {
if (playerApi) {
playerApi.setCurrentTime(playerApi.getCurrentTime() - 10)
}
}, [playerApi]);
return ( return (
<VideoProvider value={playerApi?.getVideoElement() || null}> <VideoProvider value={playerApi?.getVideoElement() || null}>
<div> <div>
...@@ -73,6 +101,7 @@ export function Player( ...@@ -73,6 +101,7 @@ export function Player(
<ul> <ul>
{relatedEpisodes.map(episode => {relatedEpisodes.map(episode =>
<MediaEpisode <MediaEpisode
key={episode.content.ids.uuid}
item={episode} item={episode}
disabled={episode.content.ids.uuid === content.ids.uuid} disabled={episode.content.ids.uuid === content.ids.uuid}
/> />
...@@ -98,20 +127,42 @@ export function Player( ...@@ -98,20 +127,42 @@ export function Player(
</div> </div>
</div> </div>
<p style={{fontVariant: "tabular-nums"}}> <p style={{fontVariant: "tabular-nums"}}>
{formatDuration(mousePosition ? (mousePosition.relative * duration) : position)} / {formatDuration(duration)} {formatDuration(mousePosition ? (mousePosition.relative * duration) : currentTime)} / {formatDuration(duration)}
</p> </p>
<button <button
onClick={playerApi?.play} onClick={onPlay}
disabled={!paused} disabled={!paused}
> >
Play Play
</button> </button>
<button <button
onClick={playerApi?.pause} onClick={onPause}
disabled={paused} disabled={paused}
> >
Pause Pause
</button> </button>
<button
onClick={onRewind}
>
Rewind
</button>
<button
onClick={onFastForward}
>
Fast Forward
</button>
<p>
<input
type="range"
min="0"
max="100"
value={volume * 100}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setVolume(event.target.valueAsNumber / 100);
playerApi?.setVolume(event.target.valueAsNumber / 100)
}}
/>
</p>
<h3>Audio</h3> <h3>Audio</h3>
<ul> <ul>
{audioTracks.map(track => ( {audioTracks.map(track => (
...@@ -151,7 +202,7 @@ export function Player( ...@@ -151,7 +202,7 @@ export function Player(
mousePosition={mousePosition} mousePosition={mousePosition}
setMousePosition={setMousePosition} setMousePosition={setMousePosition}
duration={duration} duration={duration}
position={position} position={currentTime}
/> />
</div> </div>
</VideoProvider> </VideoProvider>
......
...@@ -6,6 +6,8 @@ import {PlayerError} from "./PlayerError"; ...@@ -6,6 +6,8 @@ import {PlayerError} from "./PlayerError";
import {PlayerLoading} from "./PlayerLoading"; import {PlayerLoading} from "./PlayerLoading";
export function PlayerPage() { export function PlayerPage() {
console.log("Rendering player page");
const content = useCurrentContent(); const content = useCurrentContent();
const playabilityRating = usePlayabilityRating(); const playabilityRating = usePlayabilityRating();
......
...@@ -13,6 +13,7 @@ interface Props { ...@@ -13,6 +13,7 @@ interface Props {
} }
export function PreviewBar({previewSrc, duration, position, hidden}: Props) { export function PreviewBar({previewSrc, duration, position, hidden}: Props) {
//console.log("Rendering PreviewBar")
const classes = useStyles(); const classes = useStyles();
const [previewTrack, setPreviewTrack] = useState<HTMLTrackElement | null>(null); const [previewTrack, setPreviewTrack] = useState<HTMLTrackElement | null>(null);
......
...@@ -12,6 +12,8 @@ interface Props { ...@@ -12,6 +12,8 @@ interface Props {
} }
export function PreviewViewer({previewTrack, position}: Props) { export function PreviewViewer({previewTrack, position}: Props) {
//console.log("Rendering PreviewViewer#1")
const classes = useStyles(); const classes = useStyles();
const cues = useTextTrackCues(previewTrack); const cues = useTextTrackCues(previewTrack);
...@@ -25,7 +27,9 @@ export function PreviewViewer({previewTrack, position}: Props) { ...@@ -25,7 +27,9 @@ export function PreviewViewer({previewTrack, position}: Props) {
url.hash = ""; url.hash = "";
return url.toString(); return url.toString();
}))), [cues, previewTrack]); }))), [cues, previewTrack]);
return useMemo(() => ( return useMemo(() => {
//console.log("Rendering PreviewViewer#2")
return (
<Fragment> <Fragment>
<HeadPortal> <HeadPortal>
{sources?.map(it => ( {sources?.map(it => (
...@@ -34,7 +38,8 @@ export function PreviewViewer({previewTrack, position}: Props) { ...@@ -34,7 +38,8 @@ export function PreviewViewer({previewTrack, position}: Props) {
</HeadPortal> </HeadPortal>
<img alt="" className={classes.preview} src={sprite || undefined}/> <img alt="" className={classes.preview} src={sprite || undefined}/>
</Fragment> </Fragment>
), [classes.preview, sources, sprite]); )
}, [classes.preview, sources, sprite]);
} }
const useStyles = createUseStyles({ const useStyles = createUseStyles({
......
...@@ -42,8 +42,11 @@ export function SeekBar({videoApi, previewSrc, mousePosition, setMousePosition, ...@@ -42,8 +42,11 @@ export function SeekBar({videoApi, previewSrc, mousePosition, setMousePosition,
const onMouseMove = useCallback((event: MouseEvent<HTMLDivElement>) => { const onMouseMove = useCallback((event: MouseEvent<HTMLDivElement>) => {
const position = getMousePosition(event); const position = getMousePosition(event);
const visible = position !== null;
if (visible || isVisible !== visible) {
setMousePosition(position); setMousePosition(position);
}, [setMousePosition]); }
}, [isVisible, setMousePosition]);
const seekHead = useMemo(() => ( const seekHead = useMemo(() => (
<div <div
......
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {Subtitle} from "../../../api/models/Subtitle"; import {Subtitle} from "../../../api/models/Subtitle";
import SubtitlesOctopus from "../../../util/subtitles/ass/SubtitlesOctopus"; import SubtitlesOctopus from "../../../util/subtitles/ssa/SubtitlesOctopus";
interface Props { interface Props {
videoElement: HTMLVideoElement | null, videoElement: HTMLVideoElement | null,
...@@ -9,7 +9,7 @@ interface Props { ...@@ -9,7 +9,7 @@ interface Props {
className?: string, className?: string,
} }
export function AssRenderer( export function SsaRenderer(
{videoElement, subtitle, className}: Props {videoElement, subtitle, className}: Props
): JSX.Element { ): JSX.Element {
const [subtitleCanvas, setSubtitleCanvas] = useState<HTMLCanvasElement | null>(null); const [subtitleCanvas, setSubtitleCanvas] = useState<HTMLCanvasElement | null>(null);
...@@ -24,7 +24,7 @@ export function AssRenderer( ...@@ -24,7 +24,7 @@ export function AssRenderer(
legacyWorkerUrl: '/assets/js/subtitles-octopus-worker-legacy.js', legacyWorkerUrl: '/assets/js/subtitles-octopus-worker-legacy.js',
lossyRender: true, lossyRender: true,
onError: (error: any) => { onError: (error: any) => {
console.log("Error rendering ass subtitles:", error); console.log("Error rendering SSA subtitles:", error);
}, },
onReady: () => { onReady: () => {
instance.setCurrentTime(videoElement?.currentTime); instance.setCurrentTime(videoElement?.currentTime);
......
import {Subtitle} from "../../../api/models/Subtitle"; import {Subtitle} from "../../../api/models/Subtitle";
import {Fragment} from "react"; import {Fragment} from "react";
import {TtmlRenderer} from "./TtmlRenderer"; import {TtmlRenderer} from "./TtmlRenderer";
import {AssRenderer} from "./AssRenderer"; import {SsaRenderer} from "./SsaRenderer";
interface Props { interface Props {
videoElement: HTMLVideoElement | null, videoElement: HTMLVideoElement | null,
...@@ -17,7 +17,7 @@ export function SubtitleRenderer( ...@@ -17,7 +17,7 @@ export function SubtitleRenderer(
case "ttml": case "ttml":
return (<TtmlRenderer {...props} />); return (<TtmlRenderer {...props} />);
case "ass": case "ass":
return (<AssRenderer {...props} />); return (<SsaRenderer {...props} />);
default: default:
return (<Fragment/>); return (<Fragment/>);
} }
......
import {forwardRef, PropsWithChildren, useEffect, useImperativeHandle, useMemo, useState,} from "react"; import {forwardRef, PropsWithChildren, useEffect, useImperativeHandle, useRef, useState,} from "react";
import {Media} from "../../../api/models/Media"; import {Media} from "../../../api/models/Media";
import dashjs, {MediaInfo} from "dashjs"; import dashjs, {MediaInfo} from "dashjs";
import {PlayerApi} from "./PlayerApi"; import {PlayerApi} from "./PlayerApi";
...@@ -13,33 +13,35 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>( ...@@ -13,33 +13,35 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(
{media, autoPlay, className, children}, {media, autoPlay, className, children},
ref ref
) { ) {
const player = useMemo(() => dashjs.MediaPlayer().create(), []); const playerContainer = useRef<dashjs.MediaPlayerClass>(dashjs.MediaPlayer().create());
const player = playerContainer.current;
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null); const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
METADATA_EVENT: dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED, DURATIONCHANGE_EVENT: dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED,
CANPLAY_EVENT: dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED,
TIMECHANGE_EVENT: dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, TIMECHANGE_EVENT: dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED,
PLAY_EVENT: dashjs.MediaPlayer.events.PLAYBACK_PLAYING, PLAY_EVENT: dashjs.MediaPlayer.events.PLAYBACK_PLAYING,
PAUSE_EVENT: dashjs.MediaPlayer.events.PLAYBACK_PAUSED, PAUSE_EVENT: dashjs.MediaPlayer.events.PLAYBACK_PAUSED,
play() { play(): void {
if (!player.isReady()) { if (!player.isReady()) {
return; return;
} }
player.play(); player.play();
}, },
pause() { pause(): void {
if (!player.isReady()) { if (!player.isReady()) {
return; return;
} }
player.pause(); player.pause();
}, },
isPaused() { isPaused(): boolean {
if (!player.isReady()) { if (!player.isReady()) {
return true; return true;
} }
return player.isPaused(); return player.isPaused();
}, },
setPlaybackRate(value: number) { setPlaybackRate(value: number): void {
if (!player.isReady()) { if (!player.isReady()) {
return; return;
} }
...@@ -51,12 +53,26 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>( ...@@ -51,12 +53,26 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(
} }
return player.getPlaybackRate(); return player.getPlaybackRate();
}, },
setCurrentTime(value: number) { setCurrentTime(value: number): void {
if (!player.isReady()) { if (!player.isReady()) {
return; return;
} }
player.seek(value); player.seek(value);
}, },
setVolume(value: number): void {
if (!videoElement) {
return;
}
videoElement.volume = value;
},
getVolume(): number {
if (!videoElement) {
return 0;
}
return videoElement.volume;
},
getCurrentTime(): number { getCurrentTime(): number {
if (!player.isReady()) { if (!player.isReady()) {
return 0; return 0;
...@@ -90,7 +106,7 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>( ...@@ -90,7 +106,7 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(
return player.getCurrentTrackFor("audio"); return player.getCurrentTrackFor("audio");
}, },
setCurrentAudioTrack(track: MediaInfo) { setCurrentAudioTrack(track: MediaInfo): void {
if (!player.isReady()) { if (!player.isReady()) {
return; return;
} }
...@@ -99,10 +115,10 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>( ...@@ -99,10 +115,10 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(
} }
player.setCurrentTrack(track); player.setCurrentTrack(track);
}, },
addEventListener(event: string, listener: () => void) { addEventListener(event: string, listener: () => void): void {
player.on(event, listener); player.on(event, listener);
}, },
removeEventListener(event: string, listener: () => void) { removeEventListener(event: string, listener: () => void): void {
player.off(event, listener); player.off(event, listener);
}, },
debug(): any { debug(): any {
......
import {MediaInfo} from "dashjs"; import {MediaInfo} from "dashjs";
export interface PlayerApi { export interface PlayerApi {
METADATA_EVENT: string DURATIONCHANGE_EVENT: string
CANPLAY_EVENT: string
TIMECHANGE_EVENT: string TIMECHANGE_EVENT: string
PLAY_EVENT: string PLAY_EVENT: string
PAUSE_EVENT: string PAUSE_EVENT: string
...@@ -22,6 +23,10 @@ export interface PlayerApi { ...@@ -22,6 +23,10 @@ export interface PlayerApi {
getDuration(): number getDuration(): number
setVolume(value: number): void
getVolume(): number
canPlay(): boolean canPlay(): boolean
getAudioTracks(): MediaInfo[] getAudioTracks(): MediaInfo[]
......
...@@ -16,18 +16,20 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f ...@@ -16,18 +16,20 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null); const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
METADATA_EVENT: "loadedmetadata", DURATIONCHANGE_EVENT: "durationchange",
CANPLAY_EVENT: "canplay",
TIMECHANGE_EVENT: "timeupdate", TIMECHANGE_EVENT: "timeupdate",
PLAY_EVENT: "play", PLAY_EVENT: "play",
PAUSE_EVENT: "pause", PAUSE_EVENT: "pause",
play() {
play(): void {
if (!videoElement) { if (!videoElement) {
return; return;
} }
videoElement.play(); videoElement.play();
}, },
pause() { pause(): void {
if (!videoElement) { if (!videoElement) {
return; return;
} }
...@@ -41,7 +43,7 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f ...@@ -41,7 +43,7 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f
return videoElement.paused; return videoElement.paused;
}, },
setPlaybackRate(value: number) { setPlaybackRate(value: number): void {
if (!videoElement) { if (!videoElement) {
return; return;
} }
...@@ -55,7 +57,21 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f ...@@ -55,7 +57,21 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f
return videoElement.playbackRate; return videoElement.playbackRate;
}, },
setCurrentTime(value: number) { setVolume(value: number): void {
if (!videoElement) {
return;
}
videoElement.volume = value;
},
getVolume(): number {
if (!videoElement) {
return 0;
}
return videoElement.volume;
},
setCurrentTime(value: number): void {
if (!videoElement) { if (!videoElement) {
return; return;
} }
...@@ -91,14 +107,14 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f ...@@ -91,14 +107,14 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f
getCurrentAudioTrack(): MediaInfo | null { getCurrentAudioTrack(): MediaInfo | null {
return null; return null;
}, },
addEventListener(event: string, listener: () => void) { addEventListener(event: string, listener: () => void): void {
if (!videoElement) { if (!videoElement) {
return; return;
} }
videoElement.addEventListener(event, listener); videoElement.addEventListener(event, listener);
}, },
removeEventListener(event: string, listener: () => void) { removeEventListener(event: string, listener: () => void): void {
if (!videoElement) { if (!videoElement) {
return; return;
} }
......
...@@ -26,9 +26,9 @@ export function useAudioTracks(video: PlayerApi | null): ...@@ -26,9 +26,9 @@ export function useAudioTracks(video: PlayerApi | null):
setAudioTracks(video.getAudioTracks()); setAudioTracks(video.getAudioTracks());
setCurrentTrack(video.getCurrentAudioTrack); setCurrentTrack(video.getCurrentAudioTrack);
} else { } else {
video?.addEventListener(video.METADATA_EVENT, listener) video?.addEventListener(video.CANPLAY_EVENT, listener)
return () => { return () => {
video?.removeEventListener(video.METADATA_EVENT, listener) video?.removeEventListener(video.CANPLAY_EVENT, listener)
} }
} }
}, [listener, video]); }, [listener, video]);
......
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {PlayerApi} from "../../routes/player/video/PlayerApi"; import {PlayerApi} from "../../routes/player/video/PlayerApi";
export const usePosition = (video: PlayerApi | null) => { export const useCurrentTime = (video: PlayerApi | null) => {
const [position, setPosition] = useState<number>(0); const [position, setPosition] = useState<number>(0);
useEffect(() => { useEffect(() => {
if (video !== null) { if (video !== null) {
......
import {useEffect, useState} from "react";
import {PlayerApi} from "../../routes/player/video/PlayerApi"; import {PlayerApi} from "../../routes/player/video/PlayerApi";
import {useVideoApi} from "./useVideoApi";
import {useCallback} from "react";
export const useDuration = (video: PlayerApi | null) => { export const useDuration = (video: PlayerApi | null) => {
const [duration, setDuration] = useState<number>(0); const callback = useCallback((videoApi: PlayerApi) =>
useEffect(() => { videoApi.getDuration(), []);
if (video !== null) { return useVideoApi<number>(
if (video.canPlay()) { [video?.DURATIONCHANGE_EVENT],
setDuration(video.getDuration()); video,
} else { 0,
const listener = () => { callback
window.requestAnimationFrame(() => { )
setDuration(video.getDuration());
})
};
video.addEventListener(video.METADATA_EVENT, listener)
return () => {
video.removeEventListener(video.METADATA_EVENT, listener)
}
}
}
}, [video]);
return duration;
} }
import {useEffect, useState} from "react";
import {PlayerApi} from "../../routes/player/video/PlayerApi"; import {PlayerApi} from "../../routes/player/video/PlayerApi";
import {useVideoApi} from "./useVideoApi";
import {useCallback} from "react";
export const usePaused = (video: PlayerApi | null) => { export const usePaused = (video: PlayerApi | null) => {
const [paused, setPaused] = useState<boolean>(false); const callback = useCallback((videoApi: PlayerApi) =>
useEffect(() => { videoApi.isPaused(), []);
if (video !== null) { return useVideoApi<boolean>(
const listener = () => { [video?.PLAY_EVENT, video?.PAUSE_EVENT],
window.requestAnimationFrame(() => { video,
setPaused(video.isPaused()); true,
}) callback
}; );
video.addEventListener(video.PLAY_EVENT, listener)
video.addEventListener(video.PAUSE_EVENT, listener)
return () => {
video.removeEventListener(video.PLAY_EVENT, listener)
video.removeEventListener(video.PAUSE_EVENT, listener)
}
}
}, [video]);
return paused;
} }
import {PlayerApi} from "../../routes/player/video/PlayerApi";
import {useVideoApi} from "./useVideoApi";
interface PlayerState {
currentTime: number,
duration: number,
paused: boolean,
}
export const usePlayerState = (video: PlayerApi | null) => useVideoApi<PlayerState>(
[
video?.DURATIONCHANGE_EVENT,
video?.TIMECHANGE_EVENT,
video?.PLAY_EVENT,
video?.PAUSE_EVENT,
],
video,
{
currentTime: 0,
duration: 0,
paused: true,
},
(videoApi: PlayerApi) => {
return {
currentTime: videoApi.getCurrentTime(),
duration: videoApi.getDuration(),
paused: videoApi.isPaused(),
}
}
)
import {useEffect, useState} from "react";
import {PlayerApi} from "../../routes/player/video/PlayerApi";
export function useVideoApi<T>(
events: (string|undefined|null)[],
video: PlayerApi | null,
initial: T,
mapper: (video: PlayerApi) => T
): T {
const [result, setResult] = useState<T>(initial);
useEffect(() => {
if (video !== null) {
let requestFrame: number | null = null;
const listener = () => {
if (requestFrame !== null) {
window.cancelAnimationFrame(requestFrame)
}
requestFrame = window.requestAnimationFrame(() => {
setResult(mapper(video));
})
};
listener();
for (let event in events) {
if (event !== null && event !== undefined) {
video.addEventListener(event, listener);
}
}
return () => {
if (requestFrame) {
window.cancelAnimationFrame(requestFrame);
}
for (let event in events) {
if (event !== null && event !== undefined) {
video.removeEventListener(event, listener);
}
}
}
}
}, [events, mapper, video]);
return result;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment