feat: background task for mp4 downloads

This commit is contained in:
Adrian Castro
2024-03-27 23:57:02 +01:00
parent 1c5a63f8f1
commit 57cd3e642b
5 changed files with 114 additions and 58 deletions

View File

@@ -1,8 +1,8 @@
import type { ExpoConfig } from "expo/config"; import type { ExpoConfig } from "expo/config";
import withRemoveiOSNotificationEntitlement from "./config-plugins/withRemoveiOSNotificationEntitlement";
import withRNBackgroundDownloader from "./config-plugins/withRNBackgroundDownloader";
import { version } from "./package.json"; import { version } from "./package.json";
import withRemoveiOSNotificationEntitlement from "./src/plugins/withRemoveiOSNotificationEntitlement";
import withRNBackgroundDownloader from "./src/plugins/withRNBackgroundDownloader";
const defineConfig = (): ExpoConfig => ({ const defineConfig = (): ExpoConfig => ({
name: "movie-web", name: "movie-web",

View File

@@ -1,8 +1,15 @@
import type { DownloadTask } from "@kesha-antonov/react-native-background-downloader";
import type { Asset } from "expo-media-library"; import type { Asset } from "expo-media-library";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import React, { createContext, useContext, useEffect, useState } from "react"; import React, { createContext, useContext, useEffect, useState } from "react";
import * as FileSystem from "expo-file-system"; import * as FileSystem from "expo-file-system";
import * as MediaLibrary from "expo-media-library"; import * as MediaLibrary from "expo-media-library";
import {
checkForExistingDownloads,
completeHandler,
download,
setConfig,
} from "@kesha-antonov/react-native-background-downloader";
import VideoManager from "@salihgun/react-native-video-processor"; import VideoManager from "@salihgun/react-native-video-processor";
import { useToastController } from "@tamagui/toast"; import { useToastController } from "@tamagui/toast";
@@ -25,9 +32,15 @@ export interface DownloadItem {
asset?: Asset; asset?: Asset;
isHLS?: boolean; isHLS?: boolean;
media: ScrapeMedia; media: ScrapeMedia;
downloadResumable?: FileSystem.DownloadResumable; downloadTask?: DownloadTask;
} }
// @ts-expect-error - types are not up to date
setConfig({
isLogsEnabled: false,
progressInterval: 250,
});
interface DownloadManagerContextType { interface DownloadManagerContextType {
downloads: DownloadItem[]; downloads: DownloadItem[];
startDownload: ( startDownload: (
@@ -37,7 +50,7 @@ interface DownloadManagerContextType {
headers?: Record<string, string>, headers?: Record<string, string>,
) => Promise<Asset | void>; ) => Promise<Asset | void>;
removeDownload: (id: string) => void; removeDownload: (id: string) => void;
cancelDownload: (id: string) => Promise<void>; cancelDownload: (id: string) => void;
} }
const DownloadManagerContext = createContext< const DownloadManagerContext = createContext<
@@ -85,11 +98,11 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
return cancellationFlags[downloadId] ?? false; return cancellationFlags[downloadId] ?? false;
}; };
const cancelDownload = async (downloadId: string): Promise<void> => { const cancelDownload = (downloadId: string) => {
setCancellationFlag(downloadId, true); setCancellationFlag(downloadId, true);
const downloadItem = downloads.find((d) => d.id === downloadId); const downloadItem = downloads.find((d) => d.id === downloadId);
if (downloadItem?.downloadResumable) { if (downloadItem?.downloadTask) {
await downloadItem.downloadResumable.cancelAsync(); downloadItem.downloadTask.stop();
} }
toastController.show("Download cancelled", { toastController.show("Download cancelled", {
burntOptions: { preset: "done" }, burntOptions: { preset: "done" },
@@ -98,6 +111,35 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
}); });
}; };
// const initializeDownloader = async () => {
// setConfig({ isLogsEnabled: true }); // Set any global configs here
// const existingTasks = await checkForExistingDownloads();
// existingTasks.forEach(task => {
// // Reattach event listeners to existing tasks
// processTask(task);
// });
// };
const _processTask = (task: DownloadTask) => {
task
.progress(({ bytesDownloaded, bytesTotal }) => {
const progress = bytesDownloaded / bytesTotal;
updateDownloadItem(task.id, { progress });
})
.done(() => {
completeHandler(task.id);
})
.error(({ error, errorCode }) => {
console.error(`Download error: ${errorCode} - ${error}`);
});
const downloadItem = downloads.find((d) => d.id === task.id);
if (downloadItem) {
updateDownloadItem(task.id, { downloadTask: task });
}
};
const startDownload = async ( const startDownload = async (
url: string, url: string,
type: "mp4" | "hls", type: "mp4" | "hls",
@@ -143,34 +185,39 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
); );
}; };
const downloadMP4 = async ( interface DownloadProgress {
bytesDownloaded: number;
bytesTotal: number;
}
const downloadMP4 = (
url: string, url: string,
downloadId: string, downloadId: string,
headers: Record<string, string>, headers: Record<string, string>,
) => { ): Promise<Asset> => {
return new Promise<Asset>((resolve, reject) => {
let lastBytesWritten = 0; let lastBytesWritten = 0;
let lastTimestamp = Date.now(); let lastTimestamp = Date.now();
const callback = (downloadProgress: FileSystem.DownloadProgressData) => { const updateProgress = (downloadProgress: DownloadProgress) => {
const currentTime = Date.now(); const currentTime = Date.now();
const timeElapsed = (currentTime - lastTimestamp) / 1000; const timeElapsed = (currentTime - lastTimestamp) / 1000;
if (timeElapsed === 0) return; if (timeElapsed === 0) return;
const bytesWritten = downloadProgress.totalBytesWritten; const newBytes = downloadProgress.bytesDownloaded - lastBytesWritten;
const newBytes = bytesWritten - lastBytesWritten;
const speed = newBytes / timeElapsed / 1024; const speed = newBytes / timeElapsed / 1024;
const progress = const progress =
bytesWritten / downloadProgress.totalBytesExpectedToWrite; downloadProgress.bytesDownloaded / downloadProgress.bytesTotal;
updateDownloadItem(downloadId, { updateDownloadItem(downloadId, {
progress, progress,
speed, speed,
fileSize: downloadProgress.totalBytesExpectedToWrite, fileSize: downloadProgress.bytesTotal,
downloaded: bytesWritten, downloaded: downloadProgress.bytesDownloaded,
}); });
lastBytesWritten = bytesWritten; lastBytesWritten = downloadProgress.bytesDownloaded;
lastTimestamp = currentTime; lastTimestamp = currentTime;
}; };
@@ -179,30 +226,39 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
: null; : null;
if (!fileUri) { if (!fileUri) {
console.error("Cache directory is unavailable"); console.error("Cache directory is unavailable");
reject(new Error("Cache directory is unavailable"));
return; return;
} }
const downloadResumable = FileSystem.createDownloadResumable( const downloadTask = download({
id: downloadId,
url, url,
fileUri, destination: fileUri,
{ headers }, headers,
callback, isNotificationVisible: true,
); })
updateDownloadItem(downloadId, { downloadResumable }); .begin(() => {
updateDownloadItem(downloadId, { downloadTask });
try { })
const result = await downloadResumable.downloadAsync(); .progress(({ bytesDownloaded, bytesTotal }) => {
if (result) { updateProgress({ bytesDownloaded, bytesTotal });
console.log("Finished downloading to ", result.uri); })
const asset = await saveFileToMediaLibraryAndDeleteOriginal( .done(() => {
result.uri, saveFileToMediaLibraryAndDeleteOriginal(fileUri, downloadId)
downloadId, .then((asset) => {
); if (asset) {
return asset; resolve(asset);
} } else {
} catch (e) { reject(new Error("No asset returned"));
console.error(e);
} }
})
.catch((error) => reject(error));
})
.error(({ error, errorCode }) => {
console.error(`Download error: ${errorCode} - ${error}`);
reject(new Error(`Download error: ${errorCode} - ${error}`));
});
});
}; };
const downloadHLS = async ( const downloadHLS = async (

View File

@@ -17,7 +17,7 @@
"*.js", "*.js",
".expo/types/**/*.ts", ".expo/types/**/*.ts",
"expo-env.d.ts", "expo-env.d.ts",
"config-plugins/*", "src/plugins/*",
], ],
"exclude": ["node_modules"], "exclude": ["node_modules"],
} }