mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-24 19:31:49 +01:00
added video player + progress tracking
Co-authored-by: James Hawkins <jhawki2005@gmail.com>
This commit is contained in:
parent
d8dfbe4ee0
commit
a3d7f3ff24
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
26
src/components/media/VideoPlayer.tsx
Normal file
26
src/components/media/VideoPlayer.tsx
Normal 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>
|
||||
);
|
||||
}
|
30
src/hooks/usePortableMedia.ts
Normal file
30
src/hooks/usePortableMedia.ts
Normal 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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user