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) {