From d6a8748a58ed5d85f362a8b80c7a9654f510f9fe Mon Sep 17 00:00:00 2001 From: Janne Koschinski <janne@kuschku.de> Date: Tue, 5 Jan 2021 00:06:43 +0100 Subject: [PATCH] Cleanup content loading --- package-lock.json | 23 ++++ package.json | 1 + src/App.tsx | 25 ++-- src/api/ApiClient.ts | 38 ------ src/api/ApiClientContext.ts | 13 -- src/api/ApiEndpointContext.ts | 11 ++ src/api/hooks.ts | 30 +++++ src/api/queries.ts | 53 ++++++++ src/index.tsx | 2 +- src/routes/ContentRoute.tsx | 11 +- src/routes/main/MainPage.tsx | 121 +++---------------- src/routes/main/MediaEntry.tsx | 80 ++++++++++++ src/routes/main/MediaEpisode.tsx | 31 +++++ src/routes/player/Player.tsx | 57 +++------ src/routes/player/video/DashVideoElement.tsx | 2 + src/routes/player/video/PlayerApi.ts | 2 + src/routes/player/video/RawVideoElement.tsx | 2 + src/util/media/usePaused.ts | 22 ++++ src/util/parseUrl.ts | 11 ++ 19 files changed, 324 insertions(+), 211 deletions(-) delete mode 100644 src/api/ApiClient.ts delete mode 100644 src/api/ApiClientContext.ts create mode 100644 src/api/ApiEndpointContext.ts create mode 100644 src/api/hooks.ts create mode 100644 src/api/queries.ts create mode 100644 src/routes/main/MediaEntry.tsx create mode 100644 src/routes/main/MediaEpisode.tsx create mode 100644 src/util/media/usePaused.ts create mode 100644 src/util/parseUrl.ts diff --git a/package-lock.json b/package-lock.json index bbc2a3f..1a50ed0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9327,6 +9327,15 @@ "object-visit": "^1.0.0" } }, + "match-sorter": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.1.0.tgz", + "integrity": "sha512-sKPMf4kbF7Dm5Crx0bbfLpokK68PUJ/0STUIOPa1ZmTZEA3lCaPK3gapQR573oLmvdkTfGojzySkIwuq6Z6xRQ==", + "requires": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -12022,6 +12031,15 @@ "tiny-warning": "^1.0.2" } }, + "react-query": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.5.9.tgz", + "integrity": "sha512-thlxrnl7cDg6qmk+N2ADjDVDJkoU3c7ZFJivYph0XoBDgkRIpb3A+tpqH7o6gu7JXZum9lfX1o294UfYfTiwvg==", + "requires": { + "@babel/runtime": "^7.5.5", + "match-sorter": "^6.0.2" + } + }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -12363,6 +12381,11 @@ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" }, + "remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=" + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", diff --git a/package.json b/package.json index 1abea20..ac45e49 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "react": "^17.0.1", "react-dom": "^17.0.1", "react-jss": "^10.5.0", + "react-query": "^3.5.9", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-scripts": "4.0.1", diff --git a/src/App.tsx b/src/App.tsx index be09b03..4e6359e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,18 +2,23 @@ import {BrowserRouter, Route} from 'react-router-dom'; import {MainPage} from "./routes/main/MainPage"; import {PlayerPage} from "./routes/player/PlayerPage"; import {ContentRoute} from "./routes/ContentRoute"; +import {QueryClient, QueryClientProvider} from "react-query"; + +const queryClient = new QueryClient(); export function App() { return ( - <BrowserRouter> - <Route path="/player/:contentId" exact> - <ContentRoute> - <PlayerPage/> - </ContentRoute> - </Route> - <Route path="/" exact> - <MainPage/> - </Route> - </BrowserRouter> + <QueryClientProvider client={queryClient}> + <BrowserRouter> + <Route path="/player/:contentId" exact> + <ContentRoute> + <PlayerPage/> + </ContentRoute> + </Route> + <Route path="/" exact> + <MainPage/> + </Route> + </BrowserRouter> + </QueryClientProvider> ) } diff --git a/src/api/ApiClient.ts b/src/api/ApiClient.ts deleted file mode 100644 index 8bcce03..0000000 --- a/src/api/ApiClient.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {RequestClient} from "../util/request/RequestClient"; -import {GenreWithContent} from "./models/dto/GenreWithContent"; -import {ContentMeta} from "./models/dto/ContentMeta"; -import {Instalment} from "./models/Instalment"; -import {Genre} from "./models/Genre"; -import {Content} from "./models/Content"; - -export class ApiClient extends RequestClient { - public async listGenres(): Promise<Genre[]> { - return await this.request(`api/v1/genres`, { - method: "GET" - }); - } - - public async getGenre(genreId: string): Promise<GenreWithContent> { - return await this.request(`api/v1/genres/${genreId}`, { - method: "GET" - }); - } - - public async listContent(): Promise<Content[]> { - return await this.request(`api/v1/content`, { - method: "GET" - }); - } - - public async getContent(contentId: string): Promise<ContentMeta> { - return await this.request(`api/v1/content/${contentId}`, { - method: "GET" - }); - } - - public async listEpisodes(contentId: string): Promise<Instalment[]> { - return await this.request(`api/v1/content/${contentId}/episodes`, { - method: "GET" - }); - } -} diff --git a/src/api/ApiClientContext.ts b/src/api/ApiClientContext.ts deleted file mode 100644 index 44218d5..0000000 --- a/src/api/ApiClientContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {createContext, useContext} from "react"; -import {ApiClient} from "./ApiClient"; - -const ApiClientContext = createContext<ApiClient>( - new ApiClient( - localStorage.getItem("API_ENDPOINT") || - new URL("/", window.location.href).toString() - ) -); - -export const ApiClientProvider = ApiClientContext.Provider; -export const ApiClientConsumer = ApiClientContext.Consumer; -export const useApiClient = () => useContext<ApiClient>(ApiClientContext); diff --git a/src/api/ApiEndpointContext.ts b/src/api/ApiEndpointContext.ts new file mode 100644 index 0000000..5a6b490 --- /dev/null +++ b/src/api/ApiEndpointContext.ts @@ -0,0 +1,11 @@ +import {createContext, useContext} from "react"; +import {parseUrl} from "../util/parseUrl"; + +const ApiEndpointContext = createContext<URL>( + parseUrl(localStorage.getItem("API_ENDPOINT")) + || new URL("/", window.location.href) +); + +export const ApiEndpointProvider = ApiEndpointContext.Provider; +export const ApiEndpointConsumer = ApiEndpointContext.Consumer; +export const useApiEndpoint = () => useContext<URL>(ApiEndpointContext); diff --git a/src/api/hooks.ts b/src/api/hooks.ts new file mode 100644 index 0000000..074f081 --- /dev/null +++ b/src/api/hooks.ts @@ -0,0 +1,30 @@ +import {Content} from "./models/Content"; +import {useQuery} from "react-query"; +import {getContent, listEpisodes} from "./queries"; +import {Instalment} from "./models/Instalment"; +import {useApiEndpoint} from "./ApiEndpointContext"; +import {ContentMeta} from "./models/dto/ContentMeta"; + +export function useEpisodes(item: Content | undefined): [Instalment[], boolean, unknown] { + const apiEndpoint = useApiEndpoint(); + + const {data, isLoading, error} = useQuery( + ["episodes", item?.ids?.uuid], + () => item?.kind === "show" + ? listEpisodes(apiEndpoint, item.ids.uuid) + : [] + ) + + return [data || [], isLoading, error]; +} + +export function useContent(id: string): [ContentMeta | null, boolean, unknown] { + const apiEndpoint = useApiEndpoint(); + + const {data, isLoading, error} = useQuery( + ["media", id], + () => getContent(apiEndpoint, id) + ) + + return [data || null, isLoading, error]; +} diff --git a/src/api/queries.ts b/src/api/queries.ts new file mode 100644 index 0000000..d7ff6f0 --- /dev/null +++ b/src/api/queries.ts @@ -0,0 +1,53 @@ +import {Genre} from "./models/Genre"; +import {GenreWithContent} from "./models/dto/GenreWithContent"; +import {Content} from "./models/Content"; +import {ContentMeta} from "./models/dto/ContentMeta"; +import {Instalment} from "./models/Instalment"; +import {RequestErrorKind} from "../util/request/RequestErrorKind"; +import {RequestError} from "../util/request/RequestError"; + +async function request<T>( + baseUrl: URL, + path: string, + headers?: RequestInit +): Promise<T> { + const url = new URL(path, baseUrl).toString(); + const response = await fetch(url, headers); + if (response.ok) { + return await response.json(); + } else if (response.status in RequestErrorKind) { + throw new RequestError(response.statusText, response.status) + } else { + throw new RequestError(response.statusText, RequestErrorKind.UnknownError) + } +} + +export async function listGenres(baseUrl: URL): Promise<Genre[]> { + return await request(baseUrl, `api/v1/genres`, { + method: "GET" + }); +} + +export async function getGenre(baseUrl: URL, genreId: string): Promise<GenreWithContent> { + return await request(baseUrl, `api/v1/genres/${genreId}`, { + method: "GET" + }); +} + +export async function listContent(baseUrl: URL): Promise<Content[]> { + return await request(baseUrl, `api/v1/content`, { + method: "GET" + }); +} + +export async function getContent(baseUrl: URL, contentId: string): Promise<ContentMeta> { + return await request(baseUrl, `api/v1/content/${contentId}`, { + method: "GET" + }); +} + +export async function listEpisodes(baseUrl: URL, contentId: string): Promise<Instalment[]> { + return await request(baseUrl, `api/v1/content/${contentId}/episodes`, { + method: "GET" + }); +} diff --git a/src/index.tsx b/src/index.tsx index 33c5e0b..2b54e57 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,5 @@ import {produce} from 'immer' -import { StrictMode } from 'react'; +import {StrictMode} from 'react'; import ReactDOM from 'react-dom'; import {defaults} from 'react-sweet-state'; diff --git a/src/routes/ContentRoute.tsx b/src/routes/ContentRoute.tsx index 805a29c..5a9ec8b 100644 --- a/src/routes/ContentRoute.tsx +++ b/src/routes/ContentRoute.tsx @@ -1,17 +1,12 @@ -import { PropsWithChildren, useEffect, useState } from "react"; +import {PropsWithChildren} from "react"; import {useParams} from "react-router"; -import {useApiClient} from "../api/ApiClientContext"; -import {ContentMeta} from "../api/models/dto/ContentMeta"; import {CurrentContentProvider} from "../util/CurrentContentContext"; +import {useContent} from "../api/hooks"; export function ContentRoute(props: PropsWithChildren<{}>) { const {children} = props; const {contentId} = useParams<{ contentId: string }>(); - const apiClient = useApiClient(); - const [meta, setMeta] = useState<ContentMeta | null>(null); - useEffect(() => { - apiClient.getContent(contentId).then(setMeta); - }, [apiClient, contentId, setMeta]); + const [meta/*, isLoading, error*/] = useContent(contentId); return ( <CurrentContentProvider value={meta}> diff --git a/src/routes/main/MainPage.tsx b/src/routes/main/MainPage.tsx index 18cc5a1..752224f 100644 --- a/src/routes/main/MainPage.tsx +++ b/src/routes/main/MainPage.tsx @@ -1,112 +1,25 @@ -import { useEffect, useState } from "react"; -import {createUseStyles} from "react-jss"; -import {Link} from "react-router-dom"; -import {useApiClient} from "../../api/ApiClientContext"; -import {Instalment} from "../../api/models/Instalment"; -import {getLocalizedDescription, getLocalizedName, getLocalizedRating, Content} from "../../api/models/Content"; -import {Locale} from "../../util/locale/Locale"; +import {useApiEndpoint} from "../../api/ApiEndpointContext"; +import {Content, getLocalizedName} from "../../api/models/Content"; import {sortLexicallyAsc} from "../../util/sort/sortLexically"; +import {useQuery} from "react-query"; +import {listContent} from "../../api/queries"; +import {MediaEntry} from "./MediaEntry"; +import {useLocale} from "../../util/locale/LocalizedContext"; -interface Props { - -} - -export function MainPage(props: Props) { - const apiClient = useApiClient(); - const [data, setData] = useState<Content[]>(); - useEffect(() => { - apiClient.listContent().then(setData); - }, [apiClient]); - const locale: Locale = { - language: "en", - region: "DE", - }; - const [episodes, setEpisodes] = useState<{ [key: string]: Instalment[] }>({}); - const contentToLoad = data?.filter(it => it.kind === "show" && episodes[it.ids.uuid] === undefined); - useEffect(() => { - if (contentToLoad?.length !== undefined && contentToLoad.length > 0) { - const contentBeingLoaded = Object.fromEntries(contentToLoad?.map(it => [it.ids.uuid, []]) || []); - setEpisodes({...contentBeingLoaded, ...episodes}); - Promise.all(contentToLoad?.map(it => apiClient.listEpisodes(it.ids.uuid).then(eps => [it.ids.uuid, eps])) || []) - .then(it => setEpisodes({...Object.fromEntries(it), ...episodes})); - } - }, [apiClient, episodes, contentToLoad]); - - const classes = useStyles(); +export function MainPage() { + const apiEndpoint = useApiEndpoint(); + const locale = useLocale(); + const {data/*, isLoading, isError*/} = useQuery( + "media", + () => listContent(apiEndpoint) + ); + const media = data || []; return ( <div> - {data?.sort(sortLexicallyAsc(item => getLocalizedName(item, locale)?.name || "")) - .map((item: Content) => { - const title = getLocalizedName(item, locale); - const description = getLocalizedDescription(item, locale); - const rating = getLocalizedRating(item, locale); - const poster = item.images.find(it => it.kind === "poster"); - const backdrop = item.images.find(it => it.kind === "backdrop"); - - return ( - <div key={item.ids.uuid} className={classes.movie}> - <h1>{title?.name}</h1> - <p><strong>{rating?.certification}</strong></p> - <p><strong>{description?.tagline}</strong></p> - <p>{description?.overview}</p> - {item.kind === "movie" && ( - <p> - <Link to={"/player/" + item.ids.uuid}>Play</Link> - </p> - )} - {item.kind === "show" && ( - <ul> - {episodes[item.ids.uuid]?.map(episode => { - const episodeTitle = getLocalizedName(episode.content, locale); - const episodeDescription = getLocalizedDescription(episode.content, locale); - return ( - <li key={episode.content.ids.uuid}> - <p> - <strong>S{episode.season}E{episode.episode}</strong> – {episode.airDate} - </p> - <p><strong>{episodeTitle?.name}</strong></p> - <p><strong>{episodeDescription?.tagline}</strong></p> - <p>{episodeDescription?.overview}</p> - <p> - <Link to={"/player/" + episode.content.ids.uuid}>Play</Link> - </p> - </li> - ) - })} - </ul> - )} - {poster && ( - <img - className={classes.poster} - alt={`Movie poster for ${title?.name}`} - src={poster.src} - /> - )} - {backdrop && ( - <img - className={classes.poster} - alt={`Movie backdrop for ${title?.name}`} - src={backdrop.src} - /> - )} - </div> - ) - })} + {media + .sort(sortLexicallyAsc(item => getLocalizedName(item, locale)?.name || "")) + .map((item: Content) => (<MediaEntry item={item}/>))} </div> ); } - -const useStyles = createUseStyles({ - movie: { - maxWidth: "40rem", - margin: { - left: "auto", - right: "auto", - } - }, - poster: { - maxWidth: "20rem", - maxHeight: "20rem", - } -}); diff --git a/src/routes/main/MediaEntry.tsx b/src/routes/main/MediaEntry.tsx new file mode 100644 index 0000000..042417c --- /dev/null +++ b/src/routes/main/MediaEntry.tsx @@ -0,0 +1,80 @@ +import {Content, getLocalizedDescription, getLocalizedName, getLocalizedRating} from "../../api/models/Content"; +import {Fragment} from "react"; +import {Link} from "react-router-dom"; +import {createUseStyles} from "react-jss"; +import {useLocale} from "../../util/locale/LocalizedContext"; +import {useEpisodes} from "../../api/hooks"; +import {MediaEpisode} from "./MediaEpisode"; + +export interface Props { + item: Content +} + +export function MediaEntry( + {item}: Props +) { + const locale = useLocale(); + const classes = useStyles(); + + const [episodes/*, episodesLoading*/, episodesError] = useEpisodes(item); + + const title = getLocalizedName(item, locale); + const description = getLocalizedDescription(item, locale); + const rating = getLocalizedRating(item, locale); + const poster = item.images.find(it => it.kind === "poster"); + const backdrop = item.images.find(it => it.kind === "backdrop"); + + return ( + <div key={item.ids.uuid} className={classes.movie}> + <h1>{title?.name}</h1> + <p><strong>{rating?.certification}</strong></p> + <p><strong>{description?.tagline}</strong></p> + <p>{description?.overview}</p> + {item.kind === "movie" && ( + <p> + <Link to={"/player/" + item.ids.uuid}>Play</Link> + </p> + )} + {item.kind === "show" && ( + <Fragment> + {episodesError ? ( + <p>{""+episodesError}</p> + ) : ( + <ul> + {episodes.map(episode => + <MediaEpisode item={episode} />)} + </ul> + )} + </Fragment> + )} + {poster && ( + <img + className={classes.poster} + alt={`Movie poster for ${title?.name}`} + src={poster.src} + /> + )} + {backdrop && ( + <img + className={classes.poster} + alt={`Movie backdrop for ${title?.name}`} + src={backdrop.src} + /> + )} + </div> + ) +} + +const useStyles = createUseStyles({ + movie: { + maxWidth: "40rem", + margin: { + left: "auto", + right: "auto", + } + }, + poster: { + maxWidth: "20rem", + maxHeight: "20rem", + } +}); diff --git a/src/routes/main/MediaEpisode.tsx b/src/routes/main/MediaEpisode.tsx new file mode 100644 index 0000000..498b80a --- /dev/null +++ b/src/routes/main/MediaEpisode.tsx @@ -0,0 +1,31 @@ +import {Instalment} from "../../api/models/Instalment"; +import {getLocalizedDescription, getLocalizedName} from "../../api/models/Content"; +import {Link} from "react-router-dom"; +import {useLocale} from "../../util/locale/LocalizedContext"; + +export interface Props { + item: Instalment, + disabled?: boolean +} + +export function MediaEpisode({item, disabled}: Props) { + const locale = useLocale(); + const episodeTitle = getLocalizedName(item.content, locale); + const episodeDescription = getLocalizedDescription(item.content, locale); + + return ( + <li key={item.content.ids.uuid}> + <p> + <strong>S{item.season}E{item.episode}</strong> – {item.airDate} + </p> + <p><strong>{episodeTitle?.name}</strong></p> + <p><strong>{episodeDescription?.tagline}</strong></p> + <p>{episodeDescription?.overview}</p> + {disabled ? ( + <p><u>Play</u></p> + ) : ( + <p><Link to={"/player/" + item.content.ids.uuid}>Play</Link></p> + )} + </li> + ) +} diff --git a/src/routes/player/Player.tsx b/src/routes/player/Player.tsx index a4cfea0..b529f79 100644 --- a/src/routes/player/Player.tsx +++ b/src/routes/player/Player.tsx @@ -1,4 +1,4 @@ -import {Fragment, useEffect, useState} from "react"; +import {Fragment, useState} from "react"; import {createUseStyles} from "react-jss"; import {Link} from "react-router-dom"; import {ContentMeta} from "../../api/models/dto/ContentMeta"; @@ -17,8 +17,9 @@ import {Subtitle} from "../../api/models/Subtitle"; import {MousePosition} from "../../util/mouse/MousePosition"; import {VideoProvider} from "./video/VideoContext"; import {SubtitleRenderer} from "./subtitles/SubtitleRenderer"; -import {useApiClient} from "../../api/ApiClientContext"; -import {Instalment, sortInstalments} from "../../api/models/Instalment"; +import {useEpisodes} from "../../api/hooks"; +import {MediaEpisode} from "../main/MediaEpisode"; +import {usePaused} from "../../util/media/usePaused"; interface Props { meta: ContentMeta, @@ -28,19 +29,11 @@ interface Props { export function Player( {meta, media}: Props ) { - const apiClient = useApiClient(); const {content, instalment} = meta; const classes = useStyles(); const locale = useLocale(); - const [relatedInstalments, setRelatedInstalments] = useState<Instalment[]>([]); - useEffect(() => { - setRelatedInstalments([]); - if (instalment?.content.ids.uuid !== undefined) { - apiClient.listEpisodes(instalment.content.ids.uuid) - .then(result => setRelatedInstalments(sortInstalments(result))); - } - }, [apiClient, instalment]); + const [relatedEpisodes/*, relatedEpisodesLoading*/, relatedEpisodesError] = useEpisodes(instalment?.content); const name = getLocalizedName(content, locale); const description = getLocalizedDescription(content, locale); @@ -51,6 +44,7 @@ export function Player( const [playerApi, setPlayerApi] = useState<PlayerApi | null>(null); const [mousePosition, setMousePosition] = useState<MousePosition | null>(null); + const paused = usePaused(playerApi); const position = usePosition(playerApi); const duration = useDuration(playerApi); const [audioTracks, currentTrack, setCurrentTrack] = useAudioTracks(playerApi); @@ -73,29 +67,18 @@ export function Player( {instalment?.content && ( <Fragment> <h3>{getLocalizedName(instalment.content, locale)?.name}</h3> - <ol> - {relatedInstalments.map(episode => { - const episodeTitle = getLocalizedName(episode.content, locale); - const episodeDescription = getLocalizedDescription(episode.content, locale); - return ( - <li key={episode.content.ids.uuid}> - <p> - <strong>S{episode.season}E{episode.episode}</strong> – {episode.airDate} - </p> - <p><strong>{episodeTitle?.name}</strong></p> - <p><strong>{episodeDescription?.tagline}</strong></p> - <p>{episodeDescription?.overview}</p> - {episode.content.ids.uuid !== meta.content.ids.uuid ? ( - <p> - <Link to={"/player/" + episode.content.ids.uuid}>Play</Link> - </p> - ) : ( - <p><u>Playing</u></p> - )} - </li> - ) - })} - </ol> + {relatedEpisodesError ? ( + <p>{"" + relatedEpisodesError}</p> + ) : ( + <ul> + {relatedEpisodes.map(episode => + <MediaEpisode + item={episode} + disabled={episode.content.ids.uuid === content.ids.uuid} + /> + )} + </ul> + )} </Fragment> )} <div className={classes.player}> @@ -119,13 +102,13 @@ export function Player( </p> <button onClick={playerApi?.play} - disabled={playerApi?.isPaused() === false} + disabled={!paused} > Play </button> <button onClick={playerApi?.pause} - disabled={playerApi?.isPaused() === true} + disabled={paused} > Pause </button> diff --git a/src/routes/player/video/DashVideoElement.tsx b/src/routes/player/video/DashVideoElement.tsx index afa2f81..3f7c0a6 100644 --- a/src/routes/player/video/DashVideoElement.tsx +++ b/src/routes/player/video/DashVideoElement.tsx @@ -19,6 +19,8 @@ export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>( useImperativeHandle(ref, () => ({ METADATA_EVENT: dashjs.MediaPlayer.events.PLAYBACK_METADATA_LOADED, TIMECHANGE_EVENT: dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, + PLAY_EVENT: dashjs.MediaPlayer.events.PLAYBACK_PLAYING, + PAUSE_EVENT: dashjs.MediaPlayer.events.PLAYBACK_PAUSED, play() { if (!player.isReady()) { return; diff --git a/src/routes/player/video/PlayerApi.ts b/src/routes/player/video/PlayerApi.ts index bf11227..48ff51b 100644 --- a/src/routes/player/video/PlayerApi.ts +++ b/src/routes/player/video/PlayerApi.ts @@ -3,6 +3,8 @@ import {MediaInfo} from "dashjs"; export interface PlayerApi { METADATA_EVENT: string TIMECHANGE_EVENT: string + PLAY_EVENT: string + PAUSE_EVENT: string play(): void diff --git a/src/routes/player/video/RawVideoElement.tsx b/src/routes/player/video/RawVideoElement.tsx index b378ebd..5b1765c 100644 --- a/src/routes/player/video/RawVideoElement.tsx +++ b/src/routes/player/video/RawVideoElement.tsx @@ -18,6 +18,8 @@ export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(f useImperativeHandle(ref, () => ({ METADATA_EVENT: "loadedmetadata", TIMECHANGE_EVENT: "timeupdate", + PLAY_EVENT: "play", + PAUSE_EVENT: "pause", play() { if (!videoElement) { return; diff --git a/src/util/media/usePaused.ts b/src/util/media/usePaused.ts new file mode 100644 index 0000000..019e98a --- /dev/null +++ b/src/util/media/usePaused.ts @@ -0,0 +1,22 @@ +import {useEffect, useState} from "react"; +import {PlayerApi} from "../../routes/player/video/PlayerApi"; + +export const usePaused = (video: PlayerApi | null) => { + const [paused, setPaused] = useState<boolean>(false); + useEffect(() => { + if (video !== null) { + const listener = () => { + window.requestAnimationFrame(() => { + setPaused(video.isPaused()); + }) + }; + 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; +} diff --git a/src/util/parseUrl.ts b/src/util/parseUrl.ts new file mode 100644 index 0000000..55e1b29 --- /dev/null +++ b/src/util/parseUrl.ts @@ -0,0 +1,11 @@ +export function parseUrl(url: string | null | undefined): URL | null { + if (url === null || url === undefined || url === "") { + return null; + } + + try { + return new URL(url); + } catch (_) { + return null; + } +} -- GitLab