From a3d7f3ff242d070220a7e66126bd584e7f6e9927 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 20 Feb 2022 16:45:46 +0100 Subject: [PATCH] added video player + progress tracking Co-authored-by: James Hawkins --- README.md | 2 +- src/App.tsx | 4 +-- src/components/media/MediaCard.tsx | 18 +++++++++-- src/components/media/VideoPlayer.tsx | 26 +++++++++++++++ src/hooks/usePortableMedia.ts | 30 +++++++++++++++++ src/providers/index.ts | 13 ++++++++ src/state/watched/context.tsx | 48 ++++++++++++++++++++++++---- src/views/MovieView.tsx | 27 ++++++++++++++++ 8 files changed, 156 insertions(+), 12 deletions(-) create mode 100644 src/components/media/VideoPlayer.tsx create mode 100644 src/hooks/usePortableMedia.ts diff --git a/README.md b/README.md index 28cfd66c..661cbf0a 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Check out [this project's issues](https://github.com/JamesHawkinss/movie-web/iss - [ ] Link Github and Discord in error boundary - [x] Store watched percentage - [ ] Implement movie + series view -- [ ] Add provider stream method +- [x] Add provider stream method - [x] Better looking error boundary - [x] sort search results so they aren't sorted by provider - [ ] Get rid of react warnings diff --git a/src/App.tsx b/src/App.tsx index 4f5a4cc4..1823dc00 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,8 +10,8 @@ function App() { - - + + ); diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 161d091b..95d1a49d 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,6 +1,12 @@ -import { getProviderFromId, MWMedia, MWMediaType } from "providers"; +import { + convertMediaToPortable, + getProviderFromId, + MWMedia, + MWMediaType, +} from "providers"; import { Link } from "react-router-dom"; import { Icon, Icons } from "components/Icon"; +import { serializePortableMedia } from "hooks/usePortableMedia"; export interface MediaCardProps { media: MWMedia; @@ -87,5 +93,13 @@ export function MediaCard(props: MediaCardProps) { const content = ; if (!props.linkable) return {content}; - return {content}; + return ( + + {content} + + ); } diff --git a/src/components/media/VideoPlayer.tsx b/src/components/media/VideoPlayer.tsx new file mode 100644 index 00000000..91294438 --- /dev/null +++ b/src/components/media/VideoPlayer.tsx @@ -0,0 +1,26 @@ +import { MWMediaStream, MWPortableMedia } from "providers"; +import { useRef } from "react"; + +export interface VideoPlayerProps { + source: MWMediaStream; + onProgress?: (event: ProgressEvent) => void; +} + +export function VideoPlayer(props: VideoPlayerProps) { + const videoRef = useRef(null); + const mustUseHls = props.source.type === "m3u8"; + + return ( + + ); +} diff --git a/src/hooks/usePortableMedia.ts b/src/hooks/usePortableMedia.ts new file mode 100644 index 00000000..6b3aa88f --- /dev/null +++ b/src/hooks/usePortableMedia.ts @@ -0,0 +1,30 @@ +import { MWMedia, MWPortableMedia } from "providers"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router"; + +export function usePortableMedia(): MWPortableMedia | undefined { + const { media } = useParams<{ media: string }>(); + const [mediaObject, setMediaObject] = useState( + undefined + ); + + useEffect(() => { + try { + setMediaObject(deserializePortableMedia(media)); + } catch (err) { + console.error("Failed to deserialize portable media", err); + setMediaObject(undefined); + } + }, [media, setMediaObject]); + + return mediaObject; +} + +export function deserializePortableMedia(media: string): MWPortableMedia { + return JSON.parse(atob(decodeURIComponent(media))); +} + +export function serializePortableMedia(media: MWPortableMedia): string { + const data = encodeURIComponent(btoa(JSON.stringify(media))); + return data; +} diff --git a/src/providers/index.ts b/src/providers/index.ts index e967a80c..121d0eef 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -7,6 +7,7 @@ import { MWMediaType, MWPortableMedia, MWQuery, + MWMediaStream, } from "./types"; import { MWWrappedMediaProvider, WrapProvider } from "./wrapper"; export * from "./types"; @@ -102,3 +103,15 @@ export async function convertPortableToMedia( const provider = getProviderFromId(portable.providerId); return await provider?.getMediaFromPortable(portable); } + +/* + ** find provider from portable and get stream from that provider + */ +export async function getStream( + media: MWPortableMedia +): Promise { + const provider = getProviderFromId(media.providerId); + if (!provider) return undefined; + + return await provider.getStream(media); +} diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index c2e05a89..68865261 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -13,11 +13,13 @@ interface WatchedStoreData { interface WatchedStoreDataWrapper { setWatched: React.Dispatch>; + updateProgress(media: MWPortableMedia, progress: number, total: number): void; watched: WatchedStoreData; } const WatchedContext = createContext({ setWatched: () => {}, + updateProgress: () => {}, watched: { items: [], }, @@ -26,18 +28,50 @@ WatchedContext.displayName = "WatchedContext"; export function WatchedContextProvider(props: { children: ReactNode }) { const watchedLocalstorage = VideoProgressStore.get(); - const [watched, setWatched] = useState( + const [watched, setWatchedReal] = useState( watchedLocalstorage as WatchedStoreData ); + + function setWatched(data: any) { + setWatchedReal((old) => { + let newData = data; + if (data.constructor === Function) { + newData = data(old); + } + watchedLocalstorage.save(newData); + return newData; + }); + } + const contextValue = { setWatched(data: any) { - setWatched((old) => { - let newData = data; - if (data.constructor === Function) { - newData = data(old); + return setWatched(data); + }, + updateProgress( + media: MWPortableMedia, + progress: number, + total: number + ): void { + setWatched((data: WatchedStoreData) => { + let item = getWatchedFromPortable(data, media); + if (!item) { + item = { + mediaId: media.mediaId, + mediaType: media.mediaType, + providerId: media.providerId, + percentage: 0, + progress: 0, + episode: media.episode, + season: media.season, + }; + data.items.push(item); } - watchedLocalstorage.save(newData); - return newData; + + // update actual item + item.progress = progress; + item.percentage = Math.round((progress / total) * 100); + + return data; }); }, watched, diff --git a/src/views/MovieView.tsx b/src/views/MovieView.tsx index d18f71eb..a147a767 100644 --- a/src/views/MovieView.tsx +++ b/src/views/MovieView.tsx @@ -1,7 +1,34 @@ +import { VideoPlayer } from "components/media/VideoPlayer"; +import { usePortableMedia } from "hooks/usePortableMedia"; +import { MWPortableMedia, getStream, MWMediaStream } from "providers"; +import { useEffect, useState } from "react"; +import { useWatchedContext } from "state/watched"; + export function MovieView() { + const mediaPortable: MWPortableMedia | undefined = usePortableMedia(); + const [streamUrl, setStreamUrl] = useState(); + const store = useWatchedContext(); + + useEffect(() => { + (async () => { + setStreamUrl(mediaPortable && (await getStream(mediaPortable))); + })(); + }, [mediaPortable, setStreamUrl]); + + function updateProgress(e: Event) { + if (!mediaPortable) return; + const el: HTMLVideoElement = e.currentTarget as HTMLVideoElement; + store.updateProgress(mediaPortable, el.currentTime, el.duration); + } + return (

Movie view here

+

{JSON.stringify(mediaPortable, null, 2)}

+

+ {streamUrl ? ( + + ) : null}
); }