diff --git a/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx b/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx index 397df9de..2699354d 100644 --- a/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx +++ b/app/(auth)/(tabs)/(home)/downloads/[seriesId].tsx @@ -1,12 +1,13 @@ import {Text} from "@/components/common/Text"; import {useDownload} from "@/providers/DownloadProvider"; import {router, useLocalSearchParams, useNavigation} from "expo-router"; -import React, {useEffect, useMemo, useState} from "react"; -import {ScrollView, View} from "react-native"; +import React, {useCallback, useEffect, useMemo, useState} from "react"; +import {ScrollView, TouchableOpacity, View} from "react-native"; import {EpisodeCard} from "@/components/downloads/EpisodeCard"; import {BaseItemDto} from "@jellyfin/sdk/lib/generated-client/models"; import {SeasonDropdown, SeasonIndexState} from "@/components/series/SeasonDropdown"; import {storage} from "@/utils/mmkv"; +import {Ionicons} from "@expo/vector-icons"; export default function page() { const navigation = useNavigation(); @@ -17,7 +18,7 @@ export default function page() { }; const [seasonIndexState, setSeasonIndexState] = useState({}); - const {downloadedFiles} = useDownload(); + const {downloadedFiles, deleteItems} = useDownload(); const series = useMemo(() => { try { @@ -64,6 +65,10 @@ export default function page() { } }, [series]); + const deleteSeries = useCallback( + async () => deleteItems(groupBySeason), + [groupBySeason] + ); return ( <> {series.length > 0 && @@ -78,9 +83,16 @@ export default function page() { [series[0].item.ParentId ?? ""]: season.ParentIndexNumber, })); }}/> - - {groupBySeason.length} - + + + {groupBySeason.length} + + + + + + + } {groupBySeason.map((episode, index) => ( diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx index 7f169b6c..39f46c8c 100644 --- a/app/(auth)/(tabs)/(home)/downloads/index.tsx +++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx @@ -6,17 +6,24 @@ import { DownloadedItem, useDownload } from "@/providers/DownloadProvider"; import { queueAtom } from "@/utils/atoms/queue"; import { useSettings } from "@/utils/atoms/settings"; import { Ionicons } from "@expo/vector-icons"; -import { useRouter } from "expo-router"; +import {useNavigation, useRouter} from "expo-router"; import { useAtom } from "jotai"; -import { useMemo } from "react"; -import { Alert, ScrollView, TouchableOpacity, View } from "react-native"; +import React, {useEffect, useMemo, useRef} from "react"; +import {Alert, ScrollView, TouchableOpacity, View} from "react-native"; +import { Button } from "@/components/Button"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import {DownloadSize} from "@/components/downloads/DownloadSize"; +import {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView} from "@gorhom/bottom-sheet"; +import {toast} from "sonner-native"; +import {writeToLog} from "@/utils/log"; export default function page() { + const navigation = useNavigation(); const [queue, setQueue] = useAtom(queueAtom); - const { removeProcess, downloadedFiles } = useDownload(); + const { removeProcess, downloadedFiles, deleteFileByType } = useDownload(); const router = useRouter(); const [settings] = useSettings(); + const bottomSheetModalRef = useRef(null); const movies = useMemo(() => { try { @@ -46,107 +53,160 @@ export default function page() { const insets = useSafeAreaInsets(); - return ( - - - - {settings?.downloadMethod === "remux" && ( - - Queue - - Queue and downloads will be lost on app restart - - - {queue.map((q, index) => ( - - router.push(`/(auth)/items/page?id=${q.item.Id}`) - } - className="relative bg-neutral-900 border border-neutral-800 p-4 rounded-2xl overflow-hidden flex flex-row items-center justify-between" - key={index} - > - - {q.item.Name} - {q.item.Type} - - { - removeProcess(q.id); - setQueue((prev) => { - if (!prev) return []; - return [...prev.filter((i) => i.id !== q.id)]; - }); - }} - > - - - - ))} - + useEffect(() => { + navigation.setOptions({ + headerRight: () => ( + + f.item) || []}/> + + ) + }) + }, [downloadedFiles]); - {queue.length === 0 && ( - No items in queue - )} + const deleteMovies = () => deleteFileByType("Movie") + .then(() => toast.success("Deleted all movies successfully!")) + .catch((reason) => { + writeToLog("ERROR", reason); + toast.error("Failed to delete all movies"); + }); + const deleteShows = () => deleteFileByType("Movie") + .then(() => toast.success("Deleted all TV-Series successfully!")) + .catch((reason) => { + writeToLog("ERROR", reason); + toast.error("Failed to delete all TV-Series"); + }); + const deleteAllMedia = async () => await Promise.all([deleteMovies(), deleteShows()]) + + return ( + <> + + + + {settings?.downloadMethod === "remux" && ( + + Queue + + Queue and downloads will be lost on app restart + + + {queue.map((q, index) => ( + + router.push(`/(auth)/items/page?id=${q.item.Id}`) + } + className="relative bg-neutral-900 border border-neutral-800 p-4 rounded-2xl overflow-hidden flex flex-row items-center justify-between" + key={index} + > + + {q.item.Name} + {q.item.Type} + + { + removeProcess(q.id); + setQueue((prev) => { + if (!prev) return []; + return [...prev.filter((i) => i.id !== q.id)]; + }); + }} + > + + + + ))} + + + {queue.length === 0 && ( + No items in queue + )} + + )} + + + + + {movies.length > 0 && ( + + + Movies + + {movies?.length} + + + + + {movies?.map((item) => ( + + + + ))} + + + + )} + {groupedBySeries.length > 0 && ( + + + TV-Series + + {groupedBySeries?.length} + + + + + {groupedBySeries?.map((items) => ( + + i.item)} + key={items[0].item.SeriesId} + /> + + ))} + + + + )} + {downloadedFiles?.length === 0 && ( + + No downloaded items )} - - - - {movies.length > 0 && ( - - - Movies - - {movies?.length} - - - - - {movies?.map((item) => ( - - - - ))} - - - + + ( + )} - {groupedBySeries.length > 0 && ( - - - TV-Series - - {groupedBySeries?.length} - - - - - {groupedBySeries?.map((items) => ( - - i.item)} - key={items[0].item.SeriesId} - /> - - ))} - - + > + + + + + - )} - {downloadedFiles?.length === 0 && ( - - No downloaded items - - )} - - + + + ); } diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index 6c5cc32a..2e3e2ebb 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -2,32 +2,41 @@ import { Button } from "@/components/Button"; import { Text } from "@/components/common/Text"; import { ListItem } from "@/components/ListItem"; import { SettingToggles } from "@/components/settings/SettingToggles"; -import { useDownload } from "@/providers/DownloadProvider"; +import {bytesToReadable, useDownload} from "@/providers/DownloadProvider"; import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider"; -import { clearLogs, readFromLog } from "@/utils/log"; +import {clearLogs, useLog} from "@/utils/log"; import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import * as Haptics from "expo-haptics"; import { useAtom } from "jotai"; -import { Alert, ScrollView, View } from "react-native"; +import {Alert, ScrollView, View} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { toast } from "sonner-native"; +import * as Progress from 'react-native-progress'; +import * as FileSystem from "expo-file-system"; export default function settings() { const { logout } = useJellyfin(); - const { deleteAllFiles } = useDownload(); + const { deleteAllFiles, appSizeUsage } = useDownload(); + const { logs } = useLog(); const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const { data: logs } = useQuery({ - queryKey: ["logs"], - queryFn: async () => readFromLog(), - refetchInterval: 1000, - }); - const insets = useSafeAreaInsets(); + const {data: size , isLoading: appSizeLoading } = useQuery({ + queryKey: ["appSize", appSizeUsage], + queryFn: async () => { + const app = await appSizeUsage + + const remaining = await FileSystem.getFreeDiskStorageAsync() + const total = await FileSystem.getTotalDiskCapacityAsync() + + return {app, remaining, total, used: (total - remaining) / total} + } + }) + const openQuickConnectAuthCodeInput = () => { Alert.prompt( "Quick connect", @@ -57,6 +66,27 @@ export default function settings() { ); }; + const onDeleteClicked = async () => { + try { + await deleteAllFiles(); + Haptics.notificationAsync( + Haptics.NotificationFeedbackType.Success + ); + } catch (e) { + Haptics.notificationAsync( + Haptics.NotificationFeedbackType.Error + ); + toast.error("Error deleting files"); + } + } + + const onClearLogsClicked = async () => { + clearLogs(); + Haptics.notificationAsync( + Haptics.NotificationFeedbackType.Success + ); + }; + return ( + @@ -92,42 +125,36 @@ export default function settings() { - - Account and storage - - - - + + Storage + + {size && App usage: {bytesToReadable(size.app)}} + + {size && ( + Available: {bytesToReadable(size.remaining)}, Total: {bytesToReadable(size.total)} + )} + + Logs diff --git a/app/_layout.tsx b/app/_layout.tsx index 1da71807..a9c71662 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -9,7 +9,7 @@ import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider"; import { orientationAtom } from "@/utils/atoms/orientation"; import { Settings, useSettings } from "@/utils/atoms/settings"; import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks"; -import { writeToLog } from "@/utils/log"; +import {LogProvider, writeToLog} from "@/utils/log"; import { storage } from "@/utils/mmkv"; import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server"; import { ActionSheetProvider } from "@expo/react-native-action-sheet"; @@ -304,56 +304,58 @@ function Layout() { } return ( - + - - - - + + + + diff --git a/bun.lockb b/bun.lockb index 38309af1..971e5d45 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index fef2f91e..22b00324 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -31,6 +31,7 @@ import Animated, { } from "react-native-reanimated"; import { Button } from "./Button"; import { SelectedOptions } from "./ItemContent"; +import { chromecastProfile } from "@/utils/profiles/chromecast"; interface Props extends React.ComponentProps { item: BaseItemDto; @@ -111,18 +112,11 @@ export const PlayButton: React.FC = ({ if (state && state !== PlayServicesState.SUCCESS) CastContext.showPlayServicesErrorDialog(state); else { - // If we're opening a currently playing item, don't restart the media. - // Instead just open controls. - if (isOpeningCurrentlyPlayingMedia) { - CastContext.showExpandedControls(); - return; - } - // Get a new URL with the Chromecast device profile: const data = await getStreamUrl({ api, item, - deviceProfile: ios, + deviceProfile: chromecastProfile, startTimeTicks: item?.UserData?.PlaybackPositionTicks!, userId: user?.Id, audioStreamIndex: selectedOptions.audioIndex, diff --git a/components/downloads/DownloadSize.tsx b/components/downloads/DownloadSize.tsx index a69fc4a0..d1b11bf0 100644 --- a/components/downloads/DownloadSize.tsx +++ b/components/downloads/DownloadSize.tsx @@ -2,9 +2,10 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import React, {useEffect, useMemo, useState} from "react"; import {Text} from "@/components/common/Text"; import useDownloadHelper from "@/utils/download"; -import {useDownload} from "@/providers/DownloadProvider"; +import {bytesToReadable, useDownload} from "@/providers/DownloadProvider"; +import {TextProps} from "react-native"; -interface DownloadSizeProps { +interface DownloadSizeProps extends TextProps { items: BaseItemDto[]; } @@ -13,7 +14,7 @@ interface DownloadSizes { itemsNeedingSize: BaseItemDto[]; } -export const DownloadSize: React.FC = ({ items }) => { +export const DownloadSize: React.FC = ({ items, ...props }) => { const { downloadedFiles, saveDownloadedItemInfo } = useDownload(); const { getDownloadSize } = useDownloadHelper(); const [size, setSize] = useState(); @@ -53,17 +54,9 @@ export const DownloadSize: React.FC = ({ items }) => { return size }, [size]) - const bytesToReadable = (bytes: number) => { - const gb = bytes / 1e+9; - - if (gb >= 1) - return `${gb.toFixed(2)} GB` - return `${(bytes / 1024 / 1024).toFixed(2)} MB` - } - return ( <> - {sizeText} + {sizeText} ); }; \ No newline at end of file diff --git a/components/downloads/SeriesCard.tsx b/components/downloads/SeriesCard.tsx index 1b665829..4c6efa1f 100644 --- a/components/downloads/SeriesCard.tsx +++ b/components/downloads/SeriesCard.tsx @@ -1,20 +1,49 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import {TouchableOpacity, View} from "react-native"; import { Text } from "../common/Text"; -import React, {useEffect, useMemo, useState} from "react"; +import React, {useCallback, useMemo} from "react"; import {storage} from "@/utils/mmkv"; import {Image} from "expo-image"; import {Ionicons} from "@expo/vector-icons"; import {router} from "expo-router"; import {DownloadSize} from "@/components/downloads/DownloadSize"; +import {useDownload} from "@/providers/DownloadProvider"; +import {useActionSheet} from "@expo/react-native-action-sheet"; export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({items}) => { + const { deleteItems } = useDownload(); + const { showActionSheetWithOptions } = useActionSheet(); + const base64Image = useMemo(() => { return storage.getString(items[0].SeriesId!); }, []); + const deleteSeries = useCallback( + async () => deleteItems(items), + [items] + ); + + const showActionSheet = useCallback(() => { + const options = ["Delete", "Cancel"]; + const destructiveButtonIndex = 0; + + showActionSheetWithOptions({ + options, + destructiveButtonIndex, + }, + (selectedIndex) => { + if (selectedIndex == destructiveButtonIndex) { + deleteSeries(); + } + } + ); + }, [showActionSheetWithOptions, deleteSeries]); + return ( - router.push(`/downloads/${items[0].SeriesId}`)}> + router.push(`/downloads/${items[0].SeriesId}`)} + onLongPress={showActionSheet} + > {base64Image ? ( ; @@ -67,6 +69,7 @@ function useDownloadProvider() { const [settings] = useSettings(); const router = useRouter(); const [api] = useAtom(apiAtom); + const { logs } = useLog(); const {saveSeriesPrimaryImage} = useDownloadHelper(); const { saveImage } = useImageStorage(); @@ -388,19 +391,20 @@ function useDownloadProvider() { ); const deleteAllFiles = async (): Promise => { - try { - await deleteLocalFiles(); - removeDownloadedItemsFromStorage(); - await cancelAllServerJobs(); - queryClient.invalidateQueries({ queryKey: ["downloadedItems"] }); - toast.success("All files, folders, and jobs deleted successfully"); - } catch (error) { - console.error("Failed to delete all files, folders, and jobs:", error); + Promise.all([ + deleteLocalFiles(), + removeDownloadedItemsFromStorage(), + cancelAllServerJobs(), + queryClient.invalidateQueries({queryKey: ["downloadedItems"]}), + ]).then(() => + toast.success("All files, folders, and jobs deleted successfully") + ).catch((reason) => { + console.error("Failed to delete all files, folders, and jobs:", reason); toast.error("An error occurred while deleting files and jobs"); - } + }); }; - const deleteLocalFiles = async (): Promise => { + const forEveryDirectoryFile = async (includeMMKV: boolean = true, callback: (file: FileInfo) => void) => { const baseDirectory = FileSystem.documentDirectory; if (!baseDirectory) { throw new Error("Base directory not found"); @@ -408,25 +412,36 @@ function useDownloadProvider() { const dirContents = await FileSystem.readDirectoryAsync(baseDirectory); for (const item of dirContents) { - const itemPath = `${baseDirectory}${item}`; - const itemInfo = await FileSystem.getInfoAsync(itemPath); + // Exclude mmkv directory. + // Deleting this deletes all user information as well. Logout should handle this. + if (item == "mmkv" && !includeMMKV) + continue + const itemInfo = await FileSystem.getInfoAsync(`${baseDirectory}${item}`); if (itemInfo.exists) { - if (itemInfo.isDirectory) { - await FileSystem.deleteAsync(itemPath, { idempotent: true }); - } else { - await FileSystem.deleteAsync(itemPath, { idempotent: true }); - } + callback(itemInfo) } } + } + + const deleteLocalFiles = async (): Promise => { + await forEveryDirectoryFile(false, (file) => { + console.warn("Deleting file", file.uri) + FileSystem.deleteAsync(file.uri, {idempotent: true}) + } + ) }; - const removeDownloadedItemsFromStorage = (): void => { - try { - storage.delete("downloadedItems"); - } catch (error) { - console.error("Failed to remove downloadedItems from storage:", error); - throw error; - } + const removeDownloadedItemsFromStorage = async () => { + // delete any saved images first + Promise.all([ + deleteFileByType("Movie"), + deleteFileByType("Episode"), + ]).then(() => + storage.delete("downloadedItems") + ).catch((reason) => { + console.error("Failed to remove downloadedItems from storage:", reason); + throw reason + }) }; const cancelAllServerJobs = async (): Promise => { @@ -434,7 +449,8 @@ function useDownloadProvider() { throw new Error("No auth header available"); } if (!settings?.optimizedVersionsServerUrl) { - throw new Error("No server URL configured"); + console.error("No server URL configured"); + return } const deviceId = await getOrSetDeviceId(); @@ -494,6 +510,43 @@ function useDownloadProvider() { } }; + const deleteItems = async (items: BaseItemDto[]) => { + Promise.all(items.map(i => { + if (i.Id) + return deleteFile(i.Id) + return + })).then(() => + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success) + ) + } + + const deleteFileByType = async (type: BaseItemDto['Type']) => { + await Promise.all( + downloadedFiles + ?.filter(file => file.item.Type == type) + ?.flatMap(file => { + const promises = []; + if (type == "Episode" && file.item.SeriesId) + promises.push(deleteFile(file.item.SeriesId)) + promises.push(deleteFile(file.item.Id!)) + return promises; + }) + || [] + ); + } + + const appSizeUsage = useMemo(async () => { + const sizes: number[] = []; + await forEveryDirectoryFile( + true, + file => { + if (file.exists) sizes.push(file.size) + } + ) + + return sizes.reduce((sum, size) => sum + size, 0); + }, [logs, downloadedFiles]) + function getDownloadedItem(itemId: string): DownloadedItem | null { try { const downloadedItems = storage.getString("downloadedItems"); @@ -566,11 +619,14 @@ function useDownloadProvider() { downloadedFiles, deleteAllFiles, deleteFile, + deleteItems, saveDownloadedItemInfo, removeProcess, setProcesses, startDownload, getDownloadedItem, + deleteFileByType, + appSizeUsage }; } @@ -591,3 +647,19 @@ export function useDownload() { } return context; } + +export function bytesToReadable(bytes: number): string { + const gb = bytes / 1e9; + + if (gb >= 1) + return `${gb.toFixed(2)} GB` + + const mb = bytes / 1024 / 1024 + if (mb >= 1) + return `${mb.toFixed(2)} MB` + + const kb = bytes / 1024 + if (kb >= 1) + return `${kb.toFixed(2)} KB` + return `${bytes.toFixed(2)} B` +} \ No newline at end of file diff --git a/utils/log.ts b/utils/log.tsx similarity index 59% rename from utils/log.ts rename to utils/log.tsx index 0072c82c..5658ec3d 100644 --- a/utils/log.ts +++ b/utils/log.tsx @@ -1,5 +1,7 @@ import { atomWithStorage, createJSONStorage } from "jotai/utils"; import { storage } from "./mmkv"; +import {useQuery} from "@tanstack/react-query"; +import React, {createContext, useContext} from "react"; type LogLevel = "INFO" | "WARN" | "ERROR"; @@ -17,6 +19,24 @@ const mmkvStorage = createJSONStorage(() => ({ })); const logsAtom = atomWithStorage("logs", [], mmkvStorage); +const LogContext = createContext | null>(null); +const DownloadContext = createContext | null>(null); + +function useLogProvider() { + const { data: logs } = useQuery({ + queryKey: ["logs"], + queryFn: async () => readFromLog(), + refetchInterval: 1000, + }); + + return { + logs + } +} + + export const writeToLog = (level: LogLevel, message: string, data?: any) => { const newEntry: LogEntry = { timestamp: new Date().toISOString(), @@ -44,4 +64,22 @@ export const clearLogs = () => { storage.delete("logs"); }; +export function useLog() { + const context = useContext(LogContext); + if (context === null) { + throw new Error("useLog must be used within a LogProvider"); + } + return context; +} + +export function LogProvider({children}: { children: React.ReactNode }) { + const provider = useLogProvider(); + + return ( + + {children} + + ) +} + export default logsAtom; diff --git a/utils/profiles/chromecast.ts b/utils/profiles/chromecast.ts index b686be31..9c13dcaa 100644 --- a/utils/profiles/chromecast.ts +++ b/utils/profiles/chromecast.ts @@ -1,18 +1,25 @@ -import { - DeviceProfile -} from "@jellyfin/sdk/lib/generated-client/models"; +import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; export const chromecastProfile: DeviceProfile = { Name: "Chromecast Video Profile", - Id: "chromecast-001", - MaxStreamingBitrate: 4000000, // 4 Mbps - MaxStaticBitrate: 4000000, // 4 Mbps + MaxStreamingBitrate: 8000000, // 8 Mbps + MaxStaticBitrate: 8000000, // 8 Mbps MusicStreamingTranscodingBitrate: 384000, // 384 kbps + CodecProfiles: [ + { + Type: "Video", + Codec: "h264", + }, + { + Type: "Audio", + Codec: "aac,mp3,flac,opus,vorbis", + }, + ], DirectPlayProfiles: [ { - Container: "mp4,webm", + Container: "mp4", Type: "Video", - VideoCodec: "h264,vp8,vp9", + VideoCodec: "h264", AudioCodec: "aac,mp3,opus,vorbis", }, { @@ -34,89 +41,32 @@ export const chromecastProfile: DeviceProfile = { ], TranscodingProfiles: [ { - Container: "ts", Type: "Video", - VideoCodec: "h264", - AudioCodec: "aac,mp3", + Context: "Streaming", Protocol: "hls", - Context: "Streaming", - MaxAudioChannels: "2", - MinSegments: 2, - BreakOnNonKeyFrames: true, + Container: "ts", + VideoCodec: "h264, hevc", + AudioCodec: "aac,mp3,ac3", + CopyTimestamps: false, + EnableSubtitlesInManifest: true, }, { - Container: "mp4", - Type: "Video", - VideoCodec: "h264", - AudioCodec: "aac", + Type: "Audio", + Context: "Streaming", Protocol: "http", - Context: "Streaming", - MaxAudioChannels: "2", - }, - { Container: "mp3", - Type: "Audio", AudioCodec: "mp3", - Protocol: "http", - Context: "Streaming", MaxAudioChannels: "2", }, - { - Container: "aac", - Type: "Audio", - AudioCodec: "aac", - Protocol: "http", - Context: "Streaming", - MaxAudioChannels: "2", - }, - ], - ContainerProfiles: [ - { - Type: "Video", - Container: "mp4", - }, - { - Type: "Video", - Container: "webm", - }, - ], - CodecProfiles: [ - { - Type: "Video", - Codec: "h264", - Conditions: [ - { - Condition: "LessThanEqual", - Property: "VideoBitDepth", - Value: "8", - }, - { - Condition: "LessThanEqual", - Property: "VideoLevel", - Value: "41", - }, - ], - }, - { - Type: "Video", - Codec: "vp9", - Conditions: [ - { - Condition: "LessThanEqual", - Property: "VideoBitDepth", - Value: "10", - }, - ], - }, ], SubtitleProfiles: [ { Format: "vtt", - Method: "Hls", + Method: "Encode", }, { Format: "vtt", - Method: "External", + Method: "Encode", }, ], }; diff --git a/utils/profiles/ios10.js b/utils/profiles/ios10.js deleted file mode 100644 index 349eff95..00000000 --- a/utils/profiles/ios10.js +++ /dev/null @@ -1,180 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -import MediaTypes from '../../constants/MediaTypes'; - -import BaseProfile from './base'; - -/** - * Device profile for Expo Video player on iOS 10 - */ -export default { - ...BaseProfile, - Name: 'Expo iOS 10 Video Profile', - CodecProfiles: [ - // iOS<13 only supports max h264 level 4.2 in ts containers - { - Codec: 'h264', - Conditions: [ - { - Condition: 'NotEquals', - IsRequired: false, - Property: 'IsAnamorphic', - Value: 'true' - }, - { - Condition: 'EqualsAny', - IsRequired: false, - Property: 'VideoProfile', - Value: 'high|main|baseline|constrained baseline' - }, - { - Condition: 'NotEquals', - IsRequired: false, - Property: 'IsInterlaced', - Value: 'true' - }, - { - Condition: 'LessThanEqual', - IsRequired: false, - Property: 'VideoLevel', - Value: '42' - } - ], - Container: 'ts', - Type: MediaTypes.Video - }, - ...BaseProfile.CodecProfiles - ], - DirectPlayProfiles: [ - { - AudioCodec: 'aac,mp3,dca,dts,alac', - Container: 'mp4,m4v', - Type: MediaTypes.Video, - VideoCodec: 'h264,vc1' - }, - { - AudioCodec: 'aac,mp3,dca,dts,alac', - Container: 'mov', - Type: MediaTypes.Video, - VideoCodec: 'h264' - }, - { - Container: 'mp3', - Type: MediaTypes.Audio - }, - { - Container: 'aac', - Type: MediaTypes.Audio - }, - { - AudioCodec: 'aac', - Container: 'm4a', - Type: MediaTypes.Audio - }, - { - AudioCodec: 'aac', - Container: 'm4b', - Type: MediaTypes.Audio - }, - { - Container: 'alac', - Type: MediaTypes.Audio - }, - { - AudioCodec: 'alac', - Container: 'm4a', - Type: MediaTypes.Audio - }, - { - AudioCodec: 'alac', - Container: 'm4b', - Type: MediaTypes.Audio - }, - { - Container: 'wav', - Type: MediaTypes.Audio - } - ], - TranscodingProfiles: [ - { - AudioCodec: 'aac', - BreakOnNonKeyFrames: true, - Container: 'aac', - Context: 'Streaming', - MaxAudioChannels: '6', - MinSegments: '2', - Protocol: 'hls', - Type: MediaTypes.Audio - }, - { - AudioCodec: 'aac', - Container: 'aac', - Context: 'Streaming', - MaxAudioChannels: '6', - Protocol: 'http', - Type: MediaTypes.Audio - }, - { - AudioCodec: 'mp3', - Container: 'mp3', - Context: 'Streaming', - MaxAudioChannels: '6', - Protocol: 'http', - Type: MediaTypes.Audio - }, - { - AudioCodec: 'wav', - Container: 'wav', - Context: 'Streaming', - MaxAudioChannels: '6', - Protocol: 'http', - Type: MediaTypes.Audio - }, - { - AudioCodec: 'mp3', - Container: 'mp3', - Context: 'Static', - MaxAudioChannels: '6', - Protocol: 'http', - Type: MediaTypes.Audio - }, - { - AudioCodec: 'aac', - Container: 'aac', - Context: 'Static', - MaxAudioChannels: '6', - Protocol: 'http', - Type: MediaTypes.Audio - }, - { - AudioCodec: 'wav', - Container: 'wav', - Context: 'Static', - MaxAudioChannels: '6', - Protocol: 'http', - Type: MediaTypes.Audio - }, - { - AudioCodec: 'aac,mp3', - BreakOnNonKeyFrames: true, - Container: 'ts', - Context: 'Streaming', - MaxAudioChannels: '6', - MinSegments: '2', - Protocol: 'hls', - Type: MediaTypes.Video, - VideoCodec: 'h264' - }, - { - AudioCodec: 'aac,mp3,dca,dts,alac', - Container: 'mp4', - Context: 'Static', - Protocol: 'http', - Type: MediaTypes.Video, - VideoCodec: 'h264' - } - ] -}; diff --git a/utils/profiles/ios12.js b/utils/profiles/ios12.js deleted file mode 100644 index dc509ad8..00000000 --- a/utils/profiles/ios12.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -import iOSProfile from './ios'; - -/** - * Device profile for Expo Video player on iOS 11-12 - */ -export default { - ...iOSProfile, - Name: 'Expo iOS 12 Video Profile', - CodecProfiles: [ - // iOS<13 only supports max h264 level 4.2 in ts containers - { - Codec: 'h264', - Conditions: [ - { - Condition: 'NotEquals', - IsRequired: false, - Property: 'IsAnamorphic', - Value: 'true' - }, - { - Condition: 'EqualsAny', - IsRequired: false, - Property: 'VideoProfile', - Value: 'high|main|baseline|constrained baseline' - }, - { - Condition: 'NotEquals', - IsRequired: false, - Property: 'IsInterlaced', - Value: 'true' - }, - { - Condition: 'LessThanEqual', - IsRequired: false, - Property: 'VideoLevel', - Value: '42' - } - ], - Container: 'ts', - Type: 'Video' - }, - ...iOSProfile.CodecProfiles - ] -}; diff --git a/utils/profiles/iosFmp4.js b/utils/profiles/iosFmp4.js deleted file mode 100644 index 5e47eb39..00000000 --- a/utils/profiles/iosFmp4.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -import MediaTypes from '../../constants/MediaTypes'; - -import iOSProfile from './ios'; - -/** - * Device profile for Expo Video player on iOS 13+ with fMP4 support - */ -export default { - ...iOSProfile, - Name: 'Expo iOS fMP4 Video Profile', - TranscodingProfiles: [ - // Add all audio profiles from default profile - ...iOSProfile.TranscodingProfiles.filter(profile => profile.Type === MediaTypes.Audio), - // Add fMP4 profile - { - AudioCodec: 'aac,mp3,flac,alac', - BreakOnNonKeyFrames: true, - Container: 'mp4', - Context: 'Streaming', - MaxAudioChannels: '6', - MinSegments: '2', - Protocol: 'hls', - Type: MediaTypes.Video, - VideoCodec: 'hevc,h264' - }, - // Add all video profiles from default profile - ...iOSProfile.TranscodingProfiles.filter(profile => profile.Type === MediaTypes.Video) - ] -}; - diff --git a/utils/profiles/old.js b/utils/profiles/old.js deleted file mode 100644 index f356320b..00000000 --- a/utils/profiles/old.js +++ /dev/null @@ -1,259 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -import MediaTypes from "../../constants/MediaTypes"; - -/** - * Device profile for old phones (aka does not support HEVC) - * - * This file is a modified version of the original file. - * - * Link to original: https://github.com/jellyfin/jellyfin-expo/blob/e7b7e736a8602c94612917ef02de22f87c7c28f2/utils/profiles/ios.js#L4 - */ -export default { - MaxStreamingBitrate: 3000000, - MaxStaticBitrate: 3000000, - MusicStreamingTranscodingBitrate: 256000, - DirectPlayProfiles: [ - { - Container: "mp4,m4v", - Type: "Video", - VideoCodec: "h264", - AudioCodec: "aac,mp3,mp2", - }, - { - Container: "mkv", - Type: "Video", - VideoCodec: "h264", - AudioCodec: "aac,mp3,mp2", - }, - { - Container: "mov", - Type: "Video", - VideoCodec: "h264", - AudioCodec: "aac,mp3,mp2", - }, - { - Container: "mp3", - Type: "Audio", - }, - { - Container: "aac", - Type: "Audio", - }, - { - Container: "m4a", - AudioCodec: "aac", - Type: "Audio", - }, - { - Container: "m4b", - AudioCodec: "aac", - Type: "Audio", - }, - { - Container: "hls", - Type: "Video", - VideoCodec: "h264", - AudioCodec: "aac,mp3,mp2", - }, - ], - TranscodingProfiles: [ - { - Container: "mp4", - Type: "Audio", - AudioCodec: "aac", - Context: "Streaming", - Protocol: "hls", - MaxAudioChannels: "2", - MinSegments: "1", - BreakOnNonKeyFrames: true, - }, - { - Container: "aac", - Type: "Audio", - AudioCodec: "aac", - Context: "Streaming", - Protocol: "http", - MaxAudioChannels: "2", - }, - { - Container: "mp3", - Type: "Audio", - AudioCodec: "mp3", - Context: "Streaming", - Protocol: "http", - MaxAudioChannels: "2", - }, - { - Container: "mp3", - Type: "Audio", - AudioCodec: "mp3", - Context: "Static", - Protocol: "http", - MaxAudioChannels: "2", - }, - { - Container: "aac", - Type: "Audio", - AudioCodec: "aac", - Context: "Static", - Protocol: "http", - MaxAudioChannels: "2", - }, - { - Container: "mp4", - Type: "Video", - AudioCodec: "aac,mp2", - VideoCodec: "h264", - Context: "Streaming", - Protocol: "hls", - MaxAudioChannels: "2", - MinSegments: "1", - BreakOnNonKeyFrames: true, - Conditions: [ - { - Condition: "LessThanEqual", - Property: "Width", - Value: "960", - IsRequired: false, - }, - { - Condition: "LessThanEqual", - Property: "Height", - Value: "960", - IsRequired: false, - }, - { - Condition: "LessThanEqual", - Property: "VideoFramerate", - Value: "60", - IsRequired: false, - }, - ], - }, - { - Container: "ts", - Type: "Video", - AudioCodec: "aac,mp3,mp2", - VideoCodec: "h264", - Context: "Streaming", - Protocol: "hls", - MaxAudioChannels: "2", - MinSegments: "1", - BreakOnNonKeyFrames: true, - Conditions: [ - { - Condition: "LessThanEqual", - Property: "Width", - Value: "960", - IsRequired: false, - }, - { - Condition: "LessThanEqual", - Property: "Height", - Value: "960", - IsRequired: false, - }, - { - Condition: "LessThanEqual", - Property: "VideoFramerate", - Value: "60", - IsRequired: false, - }, - ], - }, - ], - ContainerProfiles: [], - CodecProfiles: [ - { - Type: "VideoAudio", - Codec: "aac", - Conditions: [ - { - Condition: "Equals", - Property: "IsSecondaryAudio", - Value: "false", - IsRequired: false, - }, - ], - }, - { - Type: "VideoAudio", - Conditions: [ - { - Condition: "Equals", - Property: "IsSecondaryAudio", - Value: "false", - IsRequired: false, - }, - ], - }, - { - Type: "Video", - Codec: "h264", - Conditions: [ - { - Condition: "NotEquals", - Property: "IsAnamorphic", - Value: "true", - IsRequired: false, - }, - { - Condition: "EqualsAny", - Property: "VideoProfile", - Value: "high|main|baseline|constrained baseline", - IsRequired: false, - }, - { - Condition: "EqualsAny", - Property: "VideoRangeType", - Value: "SDR", - IsRequired: false, - }, - { - Condition: "LessThanEqual", - Property: "VideoLevel", - Value: "52", - IsRequired: false, - }, - { - Condition: "NotEquals", - Property: "IsInterlaced", - Value: "true", - IsRequired: false, - }, - ], - }, - { - Type: "Video", - Conditions: [ - { - Condition: "LessThanEqual", - Property: "Width", - Value: "960", - IsRequired: false, - }, - { - Condition: "LessThanEqual", - Property: "Height", - Value: "960", - IsRequired: false, - }, - { - Condition: "LessThanEqual", - Property: "VideoFramerate", - Value: "65", - IsRequired: false, - }, - ], - }, - ], - SubtitleProfiles: [ - { - Method: "Encode", - }, - ], -};