This commit is contained in:
Fredrik Burmester
2024-11-11 09:18:49 +01:00
parent 1fdf7ca42f
commit 3d20b7956f
3 changed files with 247 additions and 22 deletions

View File

@@ -49,6 +49,12 @@ import { VideoRef } from "react-native-video";
import * as DropdownMenu from "zeego/dropdown-menu";
import { Text } from "../common/Text";
import { Loader } from "../Loader";
import BottomSheet, {
BottomSheetBackdrop,
BottomSheetBackdropProps,
BottomSheetModal,
BottomSheetView,
} from "@gorhom/bottom-sheet";
interface Props {
item: BaseItemDto;
@@ -391,6 +397,32 @@ export const Controls: React.FC<Props> = ({
)[];
}, [item, isVideoLoaded, subtitleTracks, mediaSource]);
/**
* Bottom sheet
*/
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const handlePresentModalPress = useCallback(() => {
bottomSheetModalRef.current?.present();
}, []);
const handleSheetChanges = useCallback((index: number) => {}, []);
const closeModal = useCallback(() => {
bottomSheetModalRef.current?.dismiss();
}, []);
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
/>
),
[]
);
return (
<View
style={{
@@ -402,7 +434,58 @@ export const Controls: React.FC<Props> = ({
height: "100%",
}}
>
{/* <VideoDebugInfo playerRef={videoRef} /> */}
<BottomSheetModal
ref={bottomSheetModalRef}
index={0}
snapPoints={["50%"]}
handleIndicatorStyle={{
backgroundColor: "white",
}}
backgroundStyle={{
backgroundColor: "#171717",
}}
onChange={handleSheetChanges}
backdropComponent={renderBackdrop}
>
<BottomSheetView style={{ padding: 16 }}>
<Text className="font-bold text-white mb-2 text-lg">
Audio Tracks
</Text>
{audioTracks?.map((track, index) => (
<TouchableOpacity
key={`audio-${index}`}
onPress={() => {
setAudioTrack && setAudioTrack(track.index);
closeModal();
}}
className="border border-neutral-700 rounded-lg p-4 mb-2 bg-neutral-800"
>
<Text style={{ color: "white" }}>
{track.name} {track.language ? `(${track.language})` : null}
</Text>
</TouchableOpacity>
))}
<Text className="font-bold text-white mb-2 text-lg">Subtitles</Text>
{allSubtitleTracks?.map((sub, index) => (
<TouchableOpacity
key={`subtitle-${index}`}
onPress={() => {
if (sub.isExternal) {
setSubtitleURL &&
setSubtitleURL(api?.basePath + sub.deliveryUrl, sub.name);
} else {
setSubtitleTrack && setSubtitleTrack(sub.index);
}
closeModal();
}}
className="border border-neutral-700 rounded-lg p-4 mb-2 bg-neutral-800"
>
<Text style={{ color: "white" }}>{sub.name}</Text>
</TouchableOpacity>
))}
</BottomSheetView>
</BottomSheetModal>
<View
style={{
position: "absolute",
@@ -411,14 +494,16 @@ export const Controls: React.FC<Props> = ({
}}
className="p-4"
>
<DropdownMenu.Root>
<TouchableOpacity
onPress={() => {
handlePresentModalPress();
}}
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
>
<Ionicons name="ellipsis-horizontal" size={24} color={"white"} />
</TouchableOpacity>
{/* <DropdownMenu.Root>
<DropdownMenu.Trigger>
<TouchableOpacity
onPress={() => console.log("open settings")}
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
>
<Ionicons name="ellipsis-horizontal" size={24} color={"white"} />
</TouchableOpacity>
</DropdownMenu.Trigger>
<DropdownMenu.Content
loop={true}
@@ -495,7 +580,7 @@ export const Controls: React.FC<Props> = ({
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.Content>
</DropdownMenu.Root>
</DropdownMenu.Root> */}
</View>
<View

View File

@@ -11,6 +11,7 @@ import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import native from "@/utils/profiles/native";
import android from "@/utils/profiles/android";
import { secondsToTicks } from "@/utils/secondsToTicks";
import { Api } from "@jellyfin/sdk";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
@@ -22,17 +23,9 @@ import { useQuery } from "@tanstack/react-query";
import * as Haptics from "expo-haptics";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { useAtomValue } from "jotai";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Pressable, StatusBar, useWindowDimensions, View } from "react-native";
import { SystemBars } from "react-native-edge-to-edge";
import React, { useCallback, useMemo, useRef, useState } from "react";
import { Pressable, useWindowDimensions, View } from "react-native";
import { useSharedValue } from "react-native-reanimated";
import { SafeAreaView } from "react-native-safe-area-context";
import Video, {
OnProgressData,
SelectedTrack,
@@ -140,7 +133,7 @@ const Player = () => {
maxStreamingBitrate: bitrateValue,
mediaSourceId: mediaSourceId,
subtitleStreamIndex: subtitleIndex,
deviceProfile: native,
deviceProfile: android,
});
if (!res) return null;
@@ -383,6 +376,8 @@ const Player = () => {
height: "100%",
width: "100%",
position: "absolute",
top: 0,
left: 0,
zIndex: 0,
}}
>
@@ -396,7 +391,9 @@ const Player = () => {
}}
resizeMode={ignoreSafeAreas ? "cover" : "contain"}
onProgress={onProgress}
onError={() => {}}
onError={(e) => {
console.error("Error playing video", e);
}}
onLoad={() => {
if (firstTime.current === true) {
play();
@@ -468,7 +465,7 @@ const Player = () => {
console.log("setAudioTrack ~", i);
setSelectedAudioTrack({
type: SelectedTrackType.INDEX,
value: 10,
value: i,
});
}}
/>

143
utils/profiles/android.js Normal file
View File

@@ -0,0 +1,143 @@
/**
* 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 Native video player
*/
export default {
Name: "1. Native iOS Video Profile",
MaxStaticBitrate: 100000000,
MaxStreamingBitrate: 120000000,
MusicStreamingTranscodingBitrate: 384000,
CodecProfiles: [
{
Type: MediaTypes.Video,
Codec: "h264,h265,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
},
{
Type: MediaTypes.Audio,
Codec: "aac,mp3,flac,alac,opus,vorbis,pcm,wma",
},
],
DirectPlayProfiles: [
{
Type: MediaTypes.Video,
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp",
VideoCodec: "h264,h265,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
AudioCodec: "aac,mp3,flac,alac,opus,vorbis,wma",
},
{
Type: MediaTypes.Audio,
Container: "mp3,aac,flac,alac,wav,ogg,wma",
AudioCodec: "mp3,aac,flac,alac,opus,vorbis,wma,pcm",
},
],
TranscodingProfiles: [
{
Type: MediaTypes.Video,
Context: "Streaming",
Protocol: "hls",
Container: "ts",
VideoCodec: "h264",
AudioCodec: "aac,mp3,ac3",
MaxAudioChannels: "8",
MinSegments: "2",
BreakOnNonKeyFrames: true,
},
{
Type: MediaTypes.Audio,
Context: "Streaming",
Protocol: "http",
Container: "mp3",
AudioCodec: "mp3",
MaxAudioChannels: "2",
},
],
ResponseProfiles: [
{
Container: "mkv",
MimeType: "video/x-matroska",
Type: MediaTypes.Video,
},
{
Container: "mp4",
MimeType: "video/mp4",
Type: MediaTypes.Video,
},
],
SubtitleProfiles: [
{ Format: "srt", Method: "Embed" },
{ Format: "srt", Method: "External" },
{ Format: "srt", Method: "Encode" },
{ Format: "ass", Method: "Embed" },
{ Format: "ass", Method: "External" },
{ Format: "ass", Method: "Encode" },
{ Format: "ssa", Method: "Embed" },
{ Format: "ssa", Method: "External" },
{ Format: "ssa", Method: "Encode" },
{ Format: "sub", Method: "Embed" },
{ Format: "sub", Method: "External" },
{ Format: "sub", Method: "Encode" },
{ Format: "vtt", Method: "Embed" },
{ Format: "vtt", Method: "External" },
{ Format: "vtt", Method: "Encode" },
{ Format: "ttml", Method: "Embed" },
{ Format: "ttml", Method: "External" },
{ Format: "ttml", Method: "Encode" },
{ Format: "pgs", Method: "Embed" },
{ Format: "pgs", Method: "External" },
{ Format: "pgs", Method: "Encode" },
{ Format: "dvdsub", Method: "Embed" },
{ Format: "dvdsub", Method: "External" },
{ Format: "dvdsub", Method: "Encode" },
{ Format: "dvbsub", Method: "Embed" },
{ Format: "dvbsub", Method: "External" },
{ Format: "dvbsub", Method: "Encode" },
{ Format: "xsub", Method: "Embed" },
{ Format: "xsub", Method: "External" },
{ Format: "xsub", Method: "Encode" },
{ Format: "mov_text", Method: "Embed" },
{ Format: "mov_text", Method: "External" },
{ Format: "mov_text", Method: "Encode" },
{ Format: "scc", Method: "Embed" },
{ Format: "scc", Method: "External" },
{ Format: "scc", Method: "Encode" },
{ Format: "smi", Method: "Embed" },
{ Format: "smi", Method: "External" },
{ Format: "smi", Method: "Encode" },
{ Format: "teletext", Method: "Embed" },
{ Format: "teletext", Method: "External" },
{ Format: "teletext", Method: "Encode" },
{ Format: "microdvd", Method: "Embed" },
{ Format: "microdvd", Method: "External" },
{ Format: "microdvd", Method: "Encode" },
{ Format: "mpl2", Method: "Embed" },
{ Format: "mpl2", Method: "External" },
{ Format: "mpl2", Method: "Encode" },
{ Format: "pjs", Method: "Embed" },
{ Format: "pjs", Method: "External" },
{ Format: "pjs", Method: "Encode" },
{ Format: "realtext", Method: "Embed" },
{ Format: "realtext", Method: "External" },
{ Format: "realtext", Method: "Encode" },
{ Format: "stl", Method: "Embed" },
{ Format: "stl", Method: "External" },
{ Format: "stl", Method: "Encode" },
{ Format: "subrip", Method: "Embed" },
{ Format: "subrip", Method: "External" },
{ Format: "subrip", Method: "Encode" },
{ Format: "subviewer", Method: "Embed" },
{ Format: "subviewer", Method: "External" },
{ Format: "subviewer", Method: "Encode" },
{ Format: "text", Method: "Embed" },
{ Format: "text", Method: "External" },
{ Format: "text", Method: "Encode" },
{ Format: "vplayer", Method: "Embed" },
{ Format: "vplayer", Method: "External" },
{ Format: "vplayer", Method: "Encode" },
],
};