diff --git a/apps/expo/src/app/loading.tsx b/apps/expo/src/app/loading.tsx new file mode 100644 index 0000000..9b76086 --- /dev/null +++ b/apps/expo/src/app/loading.tsx @@ -0,0 +1,108 @@ +import { getVideoStream, transformSearchResultToScrapeMedia } from "@movie-web/provider-utils"; +import { fetchMediaDetails } from "@movie-web/tmdb"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { useEffect, useState } from "react"; +import { Text } from 'react-native'; +import type { ItemData } from "~/components/item/item"; +import ScreenLayout from "~/components/layout/ScreenLayout"; +import type { VideoPlayerData } from "./videoPlayer"; + +export default function LoadingScreenWrapper() { + const params = useLocalSearchParams(); + const data = params.data + ? (JSON.parse(params.data as string) as ItemData) + : null; + return ; +} + +function LoadingScreen({ data }: { data: ItemData | null }) { + const router = useRouter(); + const [eventLog, setEventLog] = useState([]); + + const handleEvent = (event: unknown) => { + const formattedEvent = formatEvent(event); + setEventLog((prevLog) => [...prevLog, formattedEvent]); + }; + + useEffect(() => { + const fetchVideo = async () => { + if (!data) return null; + const { id, type } = data; + const media = await fetchMediaDetails(id, type).catch(() => null); + if (!media) return null; + + const { result } = media; + let season: number | undefined; // defaults to 1 when undefined + let episode: number | undefined; + + if (type === "tv") { + // season = ?? undefined; + // episode = ?? undefined; + } + + const scrapeMedia = transformSearchResultToScrapeMedia( + type, + result, + season, + episode, + ); + + const stream = await getVideoStream({ + media: scrapeMedia, + onEvent: handleEvent, + }).catch(() => null); + if (!stream) return null; + + return { stream, scrapeMedia } + }; + + const initialize = async () => { + const video = await fetchVideo(); + if (!video || !data) { + return router.push({ pathname: "/(tabs)" }); + } + + const videoPlayerData: VideoPlayerData = { + item: data, + stream: video.stream, + media: video.scrapeMedia + }; + + router.replace({ + pathname: "/videoPlayer", + params: { data: JSON.stringify(videoPlayerData) }, + }); + }; + + void initialize(); + }, [data, router]); + + return ( + + {eventLog.map((event, index) => ( + + {event} + + ))} + + ); + } + +function formatEvent(event: unknown): string { + if (typeof event === 'string') { + return `Start: ID - ${event}`; + } else if (typeof event === 'object' && event !== null) { + const evt = event as Record; + if ('percentage' in evt) { + return `Update: ${String(evt.percentage)}% - Status: ${String(evt.status)}`; + } else if ('sourceIds' in evt) { + return `Initialization: Source IDs - ${String(evt.sourceIds)}`; + } else if ('sourceId' in evt) { + return `Discovered Embeds: Source ID - ${String(evt.sourceId)}`; + } + } + return JSON.stringify(event); + } \ No newline at end of file diff --git a/apps/expo/src/app/videoPlayer.tsx b/apps/expo/src/app/videoPlayer.tsx index ad52d09..22272fc 100644 --- a/apps/expo/src/app/videoPlayer.tsx +++ b/apps/expo/src/app/videoPlayer.tsx @@ -6,12 +6,12 @@ import * as NavigationBar from "expo-navigation-bar"; import { useLocalSearchParams, useRouter } from "expo-router"; import * as StatusBar from "expo-status-bar"; +import type { + ScrapeMedia, + Stream} from "@movie-web/provider-utils"; import { findHighestQuality, - getVideoStream, - transformSearchResultToScrapeMedia, } from "@movie-web/provider-utils"; -import { fetchMediaDetails } from "@movie-web/tmdb"; import type { ItemData } from "~/components/item/item"; import type { HeaderData } from "~/components/player/Header"; @@ -22,13 +22,19 @@ import { usePlayerStore } from "~/stores/player/store"; export default function VideoPlayerWrapper() { const params = useLocalSearchParams(); const data = params.data - ? (JSON.parse(params.data as string) as ItemData) + ? (JSON.parse(params.data as string) as VideoPlayerData) : null; return ; } +export interface VideoPlayerData { + item: ItemData; + stream: Stream; + media: ScrapeMedia; +} + interface VideoPlayerProps { - data: ItemData | null; + data: VideoPlayerData | null; } const VideoPlayer: React.FC = ({ data }) => { @@ -48,56 +54,28 @@ const VideoPlayer: React.FC = ({ data }) => { useEffect(() => { const initializePlayer = async () => { - const fetchVideo = async () => { - if (!data) return null; - const { id, type } = data; - const media = await fetchMediaDetails(id, type).catch(() => null); - if (!media) return null; - - const { result } = media; - let season: number | undefined; // defaults to 1 when undefined - let episode: number | undefined; - - if (type === "tv") { - // season = ?? undefined; - // episode = ?? undefined; - } - - const scrapeMedia = transformSearchResultToScrapeMedia( - type, - result, - season, - episode, - ); - - setHeaderData({ - title: data.title, - year: data.year, - season: - scrapeMedia.type === "show" ? scrapeMedia.season.number : undefined, - episode: - scrapeMedia.type === "show" - ? scrapeMedia.episode.number - : undefined, - }); - - const stream = await getVideoStream({ - media: scrapeMedia, - forceVTT: true, - }).catch(() => null); - if (!stream) { - await dismissFullscreenPlayer(); - return router.push("/(tabs)"); - } - return stream; - }; - - StatusBar.setStatusBarHidden(true); + StatusBar.setStatusBarHidden(true); await NavigationBar.setVisibilityAsync("hidden"); setIsLoading(true); - const stream = await fetchVideo(); + + if (!data) { + await dismissFullscreenPlayer(); + return router.push("/(tabs)"); + } + + const { item, stream, media } = data; + + setHeaderData({ + title: item.title, + year: item.year, + season: + media.type === "show" ? media.season.number : undefined, + episode: + media.type === "show" + ? media.episode.number + : undefined, + }); - if (stream) { let highestQuality; let url; @@ -125,11 +103,7 @@ const VideoPlayer: React.FC = ({ data }) => { }); setIsLoading(false); - } else { - await dismissFullscreenPlayer(); - return router.push("/(tabs)"); - } - }; + }; setIsLoading(true); void presentFullscreenPlayer(); diff --git a/apps/expo/src/components/item/item.tsx b/apps/expo/src/components/item/item.tsx index d09c5a9..32a9c01 100644 --- a/apps/expo/src/components/item/item.tsx +++ b/apps/expo/src/components/item/item.tsx @@ -17,7 +17,7 @@ export default function Item({ data }: { data: ItemData }) { const handlePress = () => { router.push({ - pathname: "/videoPlayer", + pathname: "/loading", params: { data: JSON.stringify(data) }, }); }; diff --git a/packages/provider-utils/src/index.ts b/packages/provider-utils/src/index.ts index c59ca8d..ad1b133 100644 --- a/packages/provider-utils/src/index.ts +++ b/packages/provider-utils/src/index.ts @@ -1,3 +1,6 @@ export const name = "provider-utils"; export * from "./video"; export * from "./util"; + +import type { Stream, ScrapeMedia } from "@movie-web/providers"; +export type { Stream, ScrapeMedia }; diff --git a/packages/provider-utils/src/video.ts b/packages/provider-utils/src/video.ts index f32310f..383a6b9 100644 --- a/packages/provider-utils/src/video.ts +++ b/packages/provider-utils/src/video.ts @@ -3,6 +3,7 @@ import { default as toWebVTT } from "srt-webvtt"; import type { FileBasedStream, Qualities, + RunnerOptions, ScrapeMedia, Stream, } from "@movie-web/providers"; @@ -15,9 +16,11 @@ import { export async function getVideoStream({ media, forceVTT, + onEvent, }: { media: ScrapeMedia; forceVTT?: boolean; + onEvent?: (event: unknown) => void; }): Promise { const providers = makeProviders({ fetcher: makeStandardFetcher(fetch), @@ -25,7 +28,17 @@ export async function getVideoStream({ consistentIpForRequests: true, }); - const result = await providers.runAll({ media }); + const options: RunnerOptions = { + media, + events: { + init: onEvent, + update: onEvent, + discoverEmbeds: onEvent, + start: onEvent, + } + }; + + const result = await providers.runAll(options); if (!result) return null; if (forceVTT) {