feat: autoplay

This commit is contained in:
Adrian Castro
2024-03-25 20:20:07 +01:00
parent 37e61d1296
commit 0aa9c9d8f7
5 changed files with 134 additions and 43 deletions

View File

@@ -1,4 +1,6 @@
import { useLocalSearchParams, useRouter } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import type { ScrapeMedia } from "@movie-web/provider-utils";
import type { ItemData } from "~/components/item/item"; import type { ItemData } from "~/components/item/item";
import { ScraperProcess } from "~/components/player/ScraperProcess"; import { ScraperProcess } from "~/components/player/ScraperProcess";
@@ -12,15 +14,15 @@ export default function VideoPlayerWrapper() {
const asset = usePlayerStore((state) => state.asset); const asset = usePlayerStore((state) => state.asset);
const { presentFullscreenPlayer } = usePlayer(); const { presentFullscreenPlayer } = usePlayer();
const router = useRouter();
const params = useLocalSearchParams(); const params = useLocalSearchParams();
const data = params.data const data = params.data
? (JSON.parse(params.data as string) as ItemData) ? (JSON.parse(params.data as string) as ItemData)
: null; : undefined;
const media = params.media
? (JSON.parse(params.media as string) as ScrapeMedia)
: undefined;
const download = params.download === "true"; const download = params.download === "true";
if (!data) return router.back();
void presentFullscreenPlayer(); void presentFullscreenPlayer();
if (asset) { if (asset) {
@@ -32,7 +34,7 @@ export default function VideoPlayerWrapper() {
} }
if (playerStatus === PlayerStatus.SCRAPING) { if (playerStatus === PlayerStatus.SCRAPING) {
return <ScraperProcess data={data} />; return <ScraperProcess data={data} media={media} />;
} }
if (playerStatus === PlayerStatus.READY) { if (playerStatus === PlayerStatus.READY) {

View File

@@ -4,7 +4,11 @@ import { ScrollView } from "react-native-gesture-handler";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { View } from "tamagui"; import { View } from "tamagui";
import type { HlsBasedStream } from "@movie-web/provider-utils"; import type {
HlsBasedStream,
RunOutput,
ScrapeMedia,
} from "@movie-web/provider-utils";
import { import {
extractTracksFromHLS, extractTracksFromHLS,
findHighestQuality, findHighestQuality,
@@ -12,21 +16,27 @@ import {
import type { ItemData } from "../item/item"; import type { ItemData } from "../item/item";
import type { AudioTrack } from "./AudioTrackSelector"; import type { AudioTrack } from "./AudioTrackSelector";
import type { PlayerMeta } from "~/stores/player/slices/video";
import { useDownloadManager } from "~/hooks/DownloadManagerContext"; import { useDownloadManager } from "~/hooks/DownloadManagerContext";
import { useMeta } from "~/hooks/player/useMeta"; import { useMeta } from "~/hooks/player/useMeta";
import { useScrape } from "~/hooks/player/useSourceScrape"; import { useScrape } from "~/hooks/player/useSourceScrape";
import { convertMetaToScrapeMedia } from "~/lib/meta";
import { constructFullUrl } from "~/lib/url"; import { constructFullUrl } from "~/lib/url";
import { PlayerStatus } from "~/stores/player/slices/interface"; import { PlayerStatus } from "~/stores/player/slices/interface";
import { convertMetaToScrapeMedia } from "~/stores/player/slices/video";
import { usePlayerStore } from "~/stores/player/store"; import { usePlayerStore } from "~/stores/player/store";
import { ScrapeCard, ScrapeItem } from "./ScrapeCard"; import { ScrapeCard, ScrapeItem } from "./ScrapeCard";
interface ScraperProcessProps { interface ScraperProcessProps {
data: ItemData; data?: ItemData;
media?: ScrapeMedia;
download?: boolean; download?: boolean;
} }
export const ScraperProcess = ({ data, download }: ScraperProcessProps) => { export const ScraperProcess = ({
data,
media,
download,
}: ScraperProcessProps) => {
const router = useRouter(); const router = useRouter();
const { startDownload } = useDownloadManager(); const { startDownload } = useDownloadManager();
@@ -43,10 +53,19 @@ export const ScraperProcess = ({ data, download }: ScraperProcessProps) => {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
if (!data) return router.back(); if (!data && !media) return router.back();
const meta = await convertMovieIdToMeta(data.id, data.type);
if (!meta) return; let streamResult: RunOutput | null = null;
const streamResult = await startScraping(convertMetaToScrapeMedia(meta)); let meta: PlayerMeta | undefined = undefined;
if (!media && data) {
meta = await convertMovieIdToMeta(data.id, data.type);
if (!meta) return router.back();
}
const scrapeMedia = media ?? (meta && convertMetaToScrapeMedia(meta));
if (!scrapeMedia) return router.back();
streamResult = await startScraping(scrapeMedia);
if (!streamResult) return router.back(); if (!streamResult) return router.back();
if (download) { if (download) {
@@ -76,7 +95,7 @@ export const ScraperProcess = ({ data, download }: ScraperProcessProps) => {
if (tracks?.audio.length) { if (tracks?.audio.length) {
const audioTracks: AudioTrack[] = tracks.audio.map((track) => ({ const audioTracks: AudioTrack[] = tracks.audio.map((track) => ({
uri: constructFullUrl( uri: constructFullUrl(
(streamResult.stream as HlsBasedStream).playlist, (streamResult?.stream as HlsBasedStream).playlist,
track.uri, track.uri,
), ),
name: track.properties[0]?.attributes.name?.toString() ?? "Unknown", name: track.properties[0]?.attributes.name?.toString() ?? "Unknown",
@@ -106,6 +125,7 @@ export const ScraperProcess = ({ data, download }: ScraperProcessProps) => {
convertMovieIdToMeta, convertMovieIdToMeta,
data, data,
download, download,
media,
router, router,
setAudioTracks, setAudioTracks,
setHlsTracks, setHlsTracks,

View File

@@ -1,3 +1,4 @@
import type { AVPlaybackStatus } from "expo-av";
import type { SharedValue } from "react-native-reanimated"; import type { SharedValue } from "react-native-reanimated";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Dimensions, Platform } from "react-native"; import { Dimensions, Platform } from "react-native";
@@ -23,6 +24,7 @@ import { useBrightness } from "~/hooks/player/useBrightness";
import { usePlaybackSpeed } from "~/hooks/player/usePlaybackSpeed"; import { usePlaybackSpeed } from "~/hooks/player/usePlaybackSpeed";
import { usePlayer } from "~/hooks/player/usePlayer"; import { usePlayer } from "~/hooks/player/usePlayer";
import { useVolume } from "~/hooks/player/useVolume"; import { useVolume } from "~/hooks/player/useVolume";
import { convertMetaToScrapeMedia, getNextEpisode } from "~/lib/meta";
import { useAudioTrackStore } from "~/stores/audio"; import { useAudioTrackStore } from "~/stores/audio";
import { usePlayerStore } from "~/stores/player/store"; import { usePlayerStore } from "~/stores/player/store";
import { usePlayerSettingsStore } from "~/stores/settings"; import { usePlayerSettingsStore } from "~/stores/settings";
@@ -61,8 +63,10 @@ export const VideoPlayer = () => {
const setIsIdle = usePlayerStore((state) => state.setIsIdle); const setIsIdle = usePlayerStore((state) => state.setIsIdle);
const toggleAudio = usePlayerStore((state) => state.toggleAudio); const toggleAudio = usePlayerStore((state) => state.toggleAudio);
const toggleState = usePlayerStore((state) => state.toggleState); const toggleState = usePlayerStore((state) => state.toggleState);
const meta = usePlayerStore((state) => state.meta);
const setMeta = usePlayerStore((state) => state.setMeta);
const { gestureControls } = usePlayerSettingsStore(); const { gestureControls, autoPlay } = usePlayerSettingsStore();
const updateResizeMode = (newMode: ResizeMode) => { const updateResizeMode = (newMode: ResizeMode) => {
setResizeMode(newMode); setResizeMode(newMode);
@@ -212,6 +216,27 @@ export const VideoPlayer = () => {
} }
}; };
const onPlaybackStatusUpdate = async (status: AVPlaybackStatus) => {
setStatus(status);
if (
status.isLoaded &&
status.didJustFinish &&
!status.isLooping &&
autoPlay
) {
if (meta?.type !== "show") return;
const nextEpisodeMeta = await getNextEpisode(meta);
if (!nextEpisodeMeta) return;
setMeta(nextEpisodeMeta);
const media = convertMetaToScrapeMedia(nextEpisodeMeta);
router.replace({
pathname: "/videoPlayer",
params: { media: JSON.stringify(media) },
});
}
};
return ( return (
<GestureDetector gesture={composedGesture}> <GestureDetector gesture={composedGesture}>
<View <View
@@ -230,7 +255,7 @@ export const VideoPlayer = () => {
rate={currentSpeed} rate={currentSpeed}
onLoadStart={onVideoLoadStart} onLoadStart={onVideoLoadStart}
onReadyForDisplay={onReadyForDisplay} onReadyForDisplay={onReadyForDisplay}
onPlaybackStatusUpdate={setStatus} onPlaybackStatusUpdate={onPlaybackStatusUpdate}
style={[ style={[
{ {
position: "absolute", position: "absolute",

70
apps/expo/src/lib/meta.ts Normal file
View File

@@ -0,0 +1,70 @@
import type { ScrapeMedia } from "@movie-web/provider-utils";
import { fetchMediaDetails, fetchSeasonDetails } from "@movie-web/tmdb";
import type { PlayerMeta } from "~/stores/player/slices/video";
export const convertMetaToScrapeMedia = (meta: PlayerMeta): ScrapeMedia => {
if (meta.type === "movie") {
return {
title: meta.title,
releaseYear: meta.releaseYear,
type: "movie",
tmdbId: meta.tmdbId,
imdbId: meta.imdbId,
};
}
if (meta.type === "show") {
return {
title: meta.title,
releaseYear: meta.releaseYear,
type: "show",
tmdbId: meta.tmdbId,
season: meta.season!,
episode: meta.episode!,
imdbId: meta.imdbId,
};
}
throw new Error("Invalid meta type");
};
export const getNextEpisode = async (
meta: PlayerMeta,
): Promise<PlayerMeta | undefined> => {
if (meta.type === "show") {
const currentEpisode = meta.episode!;
const nextEpisode = meta.episodes!.find(
(episode) => episode.number === currentEpisode.number + 1,
);
if (!nextEpisode) {
const media = await fetchMediaDetails(meta.tmdbId, "tv");
if (!media) return;
const nextSeason = media.result.seasons.find(
(season) => season.season_number === meta.season!.number + 1,
);
if (!nextSeason) return;
const seasonDetails = await fetchSeasonDetails(
meta.tmdbId,
nextSeason.season_number,
);
if (!seasonDetails) return;
return {
...meta,
season: {
number: nextSeason.season_number,
tmdbId: meta.season!.tmdbId,
},
episode: {
number: seasonDetails.episodes[0]?.episode_number ?? 1,
tmdbId: seasonDetails.episodes[0]?.id.toString() ?? "",
title: seasonDetails.episodes[0]?.name,
},
};
}
return {
...meta,
episode: nextEpisode,
};
}
throw new Error("Invalid meta type");
};

View File

@@ -1,8 +1,6 @@
import type { AVPlaybackSourceObject, AVPlaybackStatus, Video } from "expo-av"; import type { AVPlaybackSourceObject, AVPlaybackStatus, Video } from "expo-av";
import type { Asset } from "expo-media-library"; import type { Asset } from "expo-media-library";
import type { ScrapeMedia } from "@movie-web/provider-utils";
import type { MakeSlice } from "./types"; import type { MakeSlice } from "./types";
import { PlayerStatus } from "./interface"; import { PlayerStatus } from "./interface";
@@ -43,30 +41,6 @@ export interface VideoSlice {
resetVideo(): void; resetVideo(): void;
} }
export const convertMetaToScrapeMedia = (meta: PlayerMeta): ScrapeMedia => {
if (meta.type === "movie") {
return {
title: meta.title,
releaseYear: meta.releaseYear,
type: "movie",
tmdbId: meta.tmdbId,
imdbId: meta.imdbId,
};
}
if (meta.type === "show") {
return {
title: meta.title,
releaseYear: meta.releaseYear,
type: "show",
tmdbId: meta.tmdbId,
season: meta.season!,
episode: meta.episode!,
imdbId: meta.imdbId,
};
}
throw new Error("Invalid meta type");
};
export const createVideoSlice: MakeSlice<VideoSlice> = (set) => ({ export const createVideoSlice: MakeSlice<VideoSlice> = (set) => ({
videoRef: null, videoRef: null,
videoSrc: null, videoSrc: null,