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
- [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

View file

@ -10,8 +10,8 @@ function App() {
<WatchedContextProvider>
<Switch>
<Route exact path="/" component={SearchView} />
<Route exact path="/media/movie" component={MovieView} />
<Route exact path="/media/series" component={SeriesView} />
<Route exact path="/media/movie/:media" component={MovieView} />
<Route exact path="/media/series/:media" component={SeriesView} />
</Switch>
</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 { 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 = <MediaCardContent {...props} />;
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,
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<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 {
setWatched: React.Dispatch<React.SetStateAction<WatchedStoreData>>;
updateProgress(media: MWPortableMedia, progress: number, total: number): void;
watched: WatchedStoreData;
}
const WatchedContext = createContext<WatchedStoreDataWrapper>({
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<WatchedStoreData>(
const [watched, setWatchedReal] = useState<WatchedStoreData>(
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,

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() {
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 (
<div>
<p>Movie view here</p>
<p>{JSON.stringify(mediaPortable, null, 2)}</p>
<p></p>
{streamUrl ? (
<VideoPlayer source={streamUrl} onProgress={updateProgress} />
) : null}
</div>
);
}