added video player + progress tracking

Co-authored-by: James Hawkins <jhawki2005@gmail.com>
This commit is contained in:
Jelle van Snik 2022-02-20 16:45:46 +01:00
parent d8dfbe4ee0
commit a3d7f3ff24
8 changed files with 156 additions and 12 deletions

View File

@ -42,7 +42,7 @@ Check out [this project's issues](https://github.com/JamesHawkinss/movie-web/iss
- [ ] Link Github and Discord in error boundary - [ ] Link Github and Discord in error boundary
- [x] Store watched percentage - [x] Store watched percentage
- [ ] Implement movie + series view - [ ] Implement movie + series view
- [ ] Add provider stream method - [x] Add provider stream method
- [x] Better looking error boundary - [x] Better looking error boundary
- [x] sort search results so they aren't sorted by provider - [x] sort search results so they aren't sorted by provider
- [ ] Get rid of react warnings - [ ] Get rid of react warnings

View File

@ -10,8 +10,8 @@ function App() {
<WatchedContextProvider> <WatchedContextProvider>
<Switch> <Switch>
<Route exact path="/" component={SearchView} /> <Route exact path="/" component={SearchView} />
<Route exact path="/media/movie" component={MovieView} /> <Route exact path="/media/movie/:media" component={MovieView} />
<Route exact path="/media/series" component={SeriesView} /> <Route exact path="/media/series/:media" component={SeriesView} />
</Switch> </Switch>
</WatchedContextProvider> </WatchedContextProvider>
); );

View File

@ -1,6 +1,12 @@
import { getProviderFromId, MWMedia, MWMediaType } from "providers"; import {
convertMediaToPortable,
getProviderFromId,
MWMedia,
MWMediaType,
} from "providers";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Icon, Icons } from "components/Icon"; import { Icon, Icons } from "components/Icon";
import { serializePortableMedia } from "hooks/usePortableMedia";
export interface MediaCardProps { export interface MediaCardProps {
media: MWMedia; media: MWMedia;
@ -87,5 +93,13 @@ export function MediaCard(props: MediaCardProps) {
const content = <MediaCardContent {...props} />; const content = <MediaCardContent {...props} />;
if (!props.linkable) return <span>{content}</span>; if (!props.linkable) return <span>{content}</span>;
return <Link to={`/media/${link}`}>{content}</Link>; return (
<Link
to={`/media/${link}/${serializePortableMedia(
convertMediaToPortable(props.media)
)}`}
>
{content}
</Link>
);
} }

View File

@ -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<HTMLVideoElement | null>(null);
const mustUseHls = props.source.type === "m3u8";
return (
<video
className="videoElement"
ref={videoRef}
onProgress={(e) =>
props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent)
}
controls
autoPlay
>
{!mustUseHls ? <source src={props.source.url} type="video/mp4" /> : null}
</video>
);
}

View File

@ -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<MWPortableMedia | undefined>(
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;
}

View File

@ -7,6 +7,7 @@ import {
MWMediaType, MWMediaType,
MWPortableMedia, MWPortableMedia,
MWQuery, MWQuery,
MWMediaStream,
} from "./types"; } from "./types";
import { MWWrappedMediaProvider, WrapProvider } from "./wrapper"; import { MWWrappedMediaProvider, WrapProvider } from "./wrapper";
export * from "./types"; export * from "./types";
@ -102,3 +103,15 @@ export async function convertPortableToMedia(
const provider = getProviderFromId(portable.providerId); const provider = getProviderFromId(portable.providerId);
return await provider?.getMediaFromPortable(portable); return await provider?.getMediaFromPortable(portable);
} }
/*
** find provider from portable and get stream from that provider
*/
export async function getStream(
media: MWPortableMedia
): Promise<MWMediaStream | undefined> {
const provider = getProviderFromId(media.providerId);
if (!provider) return undefined;
return await provider.getStream(media);
}

View File

@ -13,11 +13,13 @@ interface WatchedStoreData {
interface WatchedStoreDataWrapper { interface WatchedStoreDataWrapper {
setWatched: React.Dispatch<React.SetStateAction<WatchedStoreData>>; setWatched: React.Dispatch<React.SetStateAction<WatchedStoreData>>;
updateProgress(media: MWPortableMedia, progress: number, total: number): void;
watched: WatchedStoreData; watched: WatchedStoreData;
} }
const WatchedContext = createContext<WatchedStoreDataWrapper>({ const WatchedContext = createContext<WatchedStoreDataWrapper>({
setWatched: () => {}, setWatched: () => {},
updateProgress: () => {},
watched: { watched: {
items: [], items: [],
}, },
@ -26,12 +28,12 @@ WatchedContext.displayName = "WatchedContext";
export function WatchedContextProvider(props: { children: ReactNode }) { export function WatchedContextProvider(props: { children: ReactNode }) {
const watchedLocalstorage = VideoProgressStore.get(); const watchedLocalstorage = VideoProgressStore.get();
const [watched, setWatched] = useState<WatchedStoreData>( const [watched, setWatchedReal] = useState<WatchedStoreData>(
watchedLocalstorage as WatchedStoreData watchedLocalstorage as WatchedStoreData
); );
const contextValue = {
setWatched(data: any) { function setWatched(data: any) {
setWatched((old) => { setWatchedReal((old) => {
let newData = data; let newData = data;
if (data.constructor === Function) { if (data.constructor === Function) {
newData = data(old); newData = data(old);
@ -39,6 +41,38 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
watchedLocalstorage.save(newData); watchedLocalstorage.save(newData);
return newData; return newData;
}); });
}
const contextValue = {
setWatched(data: any) {
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);
}
// update actual item
item.progress = progress;
item.percentage = Math.round((progress / total) * 100);
return data;
});
}, },
watched, watched,
}; };

View File

@ -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() { export function MovieView() {
const mediaPortable: MWPortableMedia | undefined = usePortableMedia();
const [streamUrl, setStreamUrl] = useState<MWMediaStream | undefined>();
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 ( return (
<div> <div>
<p>Movie view here</p> <p>Movie view here</p>
<p>{JSON.stringify(mediaPortable, null, 2)}</p>
<p></p>
{streamUrl ? (
<VideoPlayer source={streamUrl} onProgress={updateProgress} />
) : null}
</div> </div>
); );
} }