mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-29 14:46:06 +00: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
|
- [ ] 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
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
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,
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -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,18 +28,50 @@ 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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function setWatched(data: any) {
|
||||||
|
setWatchedReal((old) => {
|
||||||
|
let newData = data;
|
||||||
|
if (data.constructor === Function) {
|
||||||
|
newData = data(old);
|
||||||
|
}
|
||||||
|
watchedLocalstorage.save(newData);
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const contextValue = {
|
const contextValue = {
|
||||||
setWatched(data: any) {
|
setWatched(data: any) {
|
||||||
setWatched((old) => {
|
return setWatched(data);
|
||||||
let newData = data;
|
},
|
||||||
if (data.constructor === Function) {
|
updateProgress(
|
||||||
newData = data(old);
|
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,
|
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() {
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue