mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
feat: (iOS) Switch Video Players
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.modules/vlc-player/Frameworks/*.xcframework filter=lfs diff=lfs merge=lfs -text
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ npm-debug.*
|
|||||||
*.orig.*
|
*.orig.*
|
||||||
web-build/
|
web-build/
|
||||||
modules/vlc-player/android/build
|
modules/vlc-player/android/build
|
||||||
|
modules/vlc-player/android/.gradle
|
||||||
bun.lockb
|
bun.lockb
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { Controls } from "@/components/video-player/controls/Controls";
|
|||||||
import { getDownloadedFileUrl } from "@/hooks/useDownloadedFileOpener";
|
import { getDownloadedFileUrl } from "@/hooks/useDownloadedFileOpener";
|
||||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
||||||
import { useWebSocket } from "@/hooks/useWebsockets";
|
import { useWebSocket } from "@/hooks/useWebsockets";
|
||||||
import { VlcPlayerView } from "@/modules/vlc-player";
|
import { VlcPlayerView } from "@/modules";
|
||||||
import {
|
import {
|
||||||
PipStartedPayload,
|
PipStartedPayload,
|
||||||
PlaybackStatePayload,
|
PlaybackStatePayload,
|
||||||
ProgressUpdatePayload,
|
ProgressUpdatePayload,
|
||||||
VlcPlayerViewRef,
|
VlcPlayerViewRef,
|
||||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
} from "@/modules/VlcPlayer.types";
|
||||||
const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null;
|
const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null;
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
|
import {ScreenOrientationEnum, useSettings, VideoPlayer} from "@/utils/atoms/settings";
|
||||||
import { BitrateSelector, BITRATES } from "@/components/BitrateSelector";
|
import { BitrateSelector, BITRATES } from "@/components/BitrateSelector";
|
||||||
import {
|
import {
|
||||||
BACKGROUND_FETCH_TASK,
|
BACKGROUND_FETCH_TASK,
|
||||||
@@ -22,6 +22,7 @@ import { ListItem } from "../list/ListItem";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import DisabledSetting from "@/components/settings/DisabledSetting";
|
import DisabledSetting from "@/components/settings/DisabledSetting";
|
||||||
import Dropdown from "@/components/common/Dropdown";
|
import Dropdown from "@/components/common/Dropdown";
|
||||||
|
import {isNumber} from "lodash";
|
||||||
|
|
||||||
export const OtherSettings: React.FC = () => {
|
export const OtherSettings: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -142,6 +143,36 @@ export const OtherSettings: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
{(Platform.OS === "ios" || Platform.isTVOS)&& (
|
||||||
|
<ListItem
|
||||||
|
title={t("home.settings.other.video_player")}
|
||||||
|
disabled={pluginSettings?.defaultPlayer?.locked}
|
||||||
|
>
|
||||||
|
<Dropdown
|
||||||
|
data={Object.values(VideoPlayer).filter(isNumber)}
|
||||||
|
disabled={pluginSettings?.defaultPlayer?.locked}
|
||||||
|
keyExtractor={String}
|
||||||
|
titleExtractor={(item) => t(`home.settings.other.video_players.${VideoPlayer[item]}`)}
|
||||||
|
title={
|
||||||
|
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
||||||
|
<Text className="mr-1 text-[#8E8D91]">
|
||||||
|
{t(`home.settings.other.video_players.${VideoPlayer[settings.defaultPlayer]}`)}
|
||||||
|
</Text>
|
||||||
|
<Ionicons
|
||||||
|
name="chevron-expand-sharp"
|
||||||
|
size={18}
|
||||||
|
color="#5A5960"
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
}
|
||||||
|
label={t("home.settings.other.orientation")}
|
||||||
|
onSelected={(defaultPlayer) =>
|
||||||
|
updateSettings({ defaultPlayer })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.other.show_custom_menu_links")}
|
title={t("home.settings.other.show_custom_menu_links")}
|
||||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
|
|||||||
@@ -1,65 +1,40 @@
|
|||||||
import { Text } from "@/components/common/Text";
|
import {Text} from "@/components/common/Text";
|
||||||
import { Loader } from "@/components/Loader";
|
import {Loader} from "@/components/Loader";
|
||||||
import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes";
|
import {useAdjacentItems} from "@/hooks/useAdjacentEpisodes";
|
||||||
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
|
import {useCreditSkipper} from "@/hooks/useCreditSkipper";
|
||||||
import { useHaptic } from "@/hooks/useHaptic";
|
import {useHaptic} from "@/hooks/useHaptic";
|
||||||
import { useIntroSkipper } from "@/hooks/useIntroSkipper";
|
import {useIntroSkipper} from "@/hooks/useIntroSkipper";
|
||||||
import { useTrickplay } from "@/hooks/useTrickplay";
|
import {useTrickplay} from "@/hooks/useTrickplay";
|
||||||
import {
|
import {TrackInfo, VlcPlayerViewRef,} from "@/modules/VlcPlayer.types";
|
||||||
TrackInfo,
|
import {apiAtom} from "@/providers/JellyfinProvider";
|
||||||
VlcPlayerViewRef,
|
import {useSettings, VideoPlayer} from "@/utils/atoms/settings";
|
||||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
import {getDefaultPlaySettings,} from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import {getItemById} from "@/utils/jellyfin/user-library/getItemById";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import {writeToLog} from "@/utils/log";
|
||||||
import {
|
import {formatTimeString, msToTicks, secondsToMs, ticksToMs, ticksToSeconds,} from "@/utils/time";
|
||||||
getDefaultPlaySettings,
|
import {Ionicons, MaterialIcons} from "@expo/vector-icons";
|
||||||
previousIndexes,
|
import {BaseItemDto, MediaSourceInfo,} from "@jellyfin/sdk/lib/generated-client";
|
||||||
} from "@/utils/jellyfin/getDefaultPlaySettings";
|
import {Image} from "expo-image";
|
||||||
import { getItemById } from "@/utils/jellyfin/user-library/getItemById";
|
import {useLocalSearchParams, useRouter} from "expo-router";
|
||||||
import { writeToLog } from "@/utils/log";
|
|
||||||
import {
|
|
||||||
formatTimeString,
|
|
||||||
msToTicks,
|
|
||||||
secondsToMs,
|
|
||||||
ticksToMs,
|
|
||||||
ticksToSeconds,
|
|
||||||
} from "@/utils/time";
|
|
||||||
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
|
|
||||||
import {
|
|
||||||
BaseItemDto,
|
|
||||||
MediaSourceInfo,
|
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
|
||||||
import { Image } from "expo-image";
|
|
||||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
import { useAtom } from "jotai";
|
import {useAtom} from "jotai";
|
||||||
import { debounce } from "lodash";
|
import {debounce} from "lodash";
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, {useCallback, useEffect, useRef, useState} from "react";
|
||||||
import {
|
import {Platform, TouchableOpacity, useWindowDimensions, View,} from "react-native";
|
||||||
Platform,
|
import {Slider} from "react-native-awesome-slider";
|
||||||
TouchableOpacity,
|
import {runOnJS, SharedValue, useAnimatedReaction, useSharedValue,} from "react-native-reanimated";
|
||||||
useWindowDimensions,
|
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
||||||
View,
|
import {VideoRef} from "react-native-video";
|
||||||
} from "react-native";
|
|
||||||
import { Slider } from "react-native-awesome-slider";
|
|
||||||
import {
|
|
||||||
runOnJS,
|
|
||||||
SharedValue,
|
|
||||||
useAnimatedReaction,
|
|
||||||
useSharedValue,
|
|
||||||
} from "react-native-reanimated";
|
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
import { VideoRef } from "react-native-video";
|
|
||||||
import AudioSlider from "./AudioSlider";
|
import AudioSlider from "./AudioSlider";
|
||||||
import BrightnessSlider from "./BrightnessSlider";
|
import BrightnessSlider from "./BrightnessSlider";
|
||||||
import { ControlProvider } from "./contexts/ControlContext";
|
import {ControlProvider} from "./contexts/ControlContext";
|
||||||
import { VideoProvider } from "./contexts/VideoContext";
|
import {VideoProvider} from "./contexts/VideoContext";
|
||||||
import DropdownView from "./dropdown/DropdownView";
|
import DropdownView from "./dropdown/DropdownView";
|
||||||
import { EpisodeList } from "./EpisodeList";
|
import {EpisodeList} from "./EpisodeList";
|
||||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||||
import SkipButton from "./SkipButton";
|
import SkipButton from "./SkipButton";
|
||||||
import { useControlsTimeout } from "./useControlsTimeout";
|
import {useControlsTimeout} from "./useControlsTimeout";
|
||||||
import { VideoTouchOverlay } from "./VideoTouchOverlay";
|
import {VideoTouchOverlay} from "./VideoTouchOverlay";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -494,7 +469,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<View className="flex flex-row items-center space-x-2 ">
|
<View className="flex flex-row items-center space-x-2 ">
|
||||||
{!Platform.isTV && (
|
{!Platform.isTV && settings.defaultPlayer == VideoPlayer.VLC_4 && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={startPictureInPicture}
|
onPress={startPictureInPicture}
|
||||||
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { TrackInfo } from "@/modules/vlc-player";
|
|
||||||
import {
|
import {
|
||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TrackInfo } from "@/modules/vlc-player";
|
import { TrackInfo } from "@/modules/VlcPlayer.types";
|
||||||
import React, { createContext, useContext, useState, ReactNode, useEffect, useMemo } from "react";
|
import React, { createContext, useContext, useState, ReactNode, useEffect, useMemo } from "react";
|
||||||
import { useControlContext } from "./ControlContext";
|
import { useControlContext } from "./ControlContext";
|
||||||
import { Track } from "../types";
|
import { Track } from "../types";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
TrackInfo,
|
TrackInfo,
|
||||||
VlcPlayerViewRef,
|
VlcPlayerViewRef,
|
||||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
} from "@/modules/VlcPlayer.types";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { TouchableOpacity, View, ViewProps } from "react-native";
|
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|||||||
@@ -6,16 +6,30 @@ import {
|
|||||||
VlcPlayerViewRef,
|
VlcPlayerViewRef,
|
||||||
VlcPlayerSource,
|
VlcPlayerSource,
|
||||||
} from "./VlcPlayer.types";
|
} from "./VlcPlayer.types";
|
||||||
|
import {useSettings, VideoPlayer} from "@/utils/atoms/settings";
|
||||||
|
import {Platform} from "react-native";
|
||||||
|
|
||||||
interface NativeViewRef extends VlcPlayerViewRef {
|
interface NativeViewRef extends VlcPlayerViewRef {
|
||||||
setNativeProps?: (props: Partial<VlcPlayerViewProps>) => void;
|
setNativeProps?: (props: Partial<VlcPlayerViewProps>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NativeViewManager = requireNativeViewManager("VlcPlayer");
|
const VLCViewManager = requireNativeViewManager("VlcPlayer");
|
||||||
|
const VLC3ViewManager = requireNativeViewManager("VlcPlayer3");
|
||||||
|
|
||||||
// Create a forwarded ref version of the native view
|
// Create a forwarded ref version of the native view
|
||||||
const NativeView = React.forwardRef<NativeViewRef, VlcPlayerViewProps>(
|
const NativeView = React.forwardRef<NativeViewRef, VlcPlayerViewProps>(
|
||||||
(props, ref) => <NativeViewManager {...props} ref={ref} />
|
(props, ref) => {
|
||||||
|
const [settings] = useSettings();
|
||||||
|
|
||||||
|
if (Platform.OS === "ios" || Platform.isTVOS) {
|
||||||
|
if (settings.defaultPlayer == VideoPlayer.VLC_3) {
|
||||||
|
console.log("[Apple] Using Vlc Player 3")
|
||||||
|
return <VLC3ViewManager {...props} ref={ref}/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Using default Vlc Player")
|
||||||
|
return <VLCViewManager {...props} ref={ref}/>
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
||||||
27
modules/index.ts
Normal file
27
modules/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import VlcPlayerView from "./VlcPlayerView";
|
||||||
|
import {
|
||||||
|
PlaybackStatePayload,
|
||||||
|
ProgressUpdatePayload,
|
||||||
|
VideoLoadStartPayload,
|
||||||
|
VideoStateChangePayload,
|
||||||
|
VideoProgressPayload,
|
||||||
|
VlcPlayerSource,
|
||||||
|
TrackInfo,
|
||||||
|
ChapterInfo,
|
||||||
|
VlcPlayerViewProps,
|
||||||
|
VlcPlayerViewRef,
|
||||||
|
} from "./VlcPlayer.types";
|
||||||
|
|
||||||
|
export {
|
||||||
|
VlcPlayerView,
|
||||||
|
VlcPlayerViewProps,
|
||||||
|
VlcPlayerViewRef,
|
||||||
|
PlaybackStatePayload,
|
||||||
|
ProgressUpdatePayload,
|
||||||
|
VideoLoadStartPayload,
|
||||||
|
VideoStateChangePayload,
|
||||||
|
VideoProgressPayload,
|
||||||
|
VlcPlayerSource,
|
||||||
|
TrackInfo,
|
||||||
|
ChapterInfo,
|
||||||
|
};
|
||||||
6
modules/vlc-player-3/expo-module.config.json
Normal file
6
modules/vlc-player-3/expo-module.config.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"platforms": ["ios", "tvos"],
|
||||||
|
"ios": {
|
||||||
|
"modules": ["VlcPlayer3Module"]
|
||||||
|
}
|
||||||
|
}
|
||||||
23
modules/vlc-player-3/ios/VlcPlayer3.podspec
Normal file
23
modules/vlc-player-3/ios/VlcPlayer3.podspec
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'VlcPlayer3'
|
||||||
|
s.version = '3.6.1b1'
|
||||||
|
s.summary = 'A sample project summary'
|
||||||
|
s.description = 'A sample project description'
|
||||||
|
s.author = ''
|
||||||
|
s.homepage = 'https://docs.expo.dev/modules/'
|
||||||
|
s.platforms = { :ios => '13.4', :tvos => '13.4' }
|
||||||
|
s.source = { git: '' }
|
||||||
|
s.static_framework = true
|
||||||
|
|
||||||
|
s.dependency 'ExpoModulesCore'
|
||||||
|
s.ios.dependency 'MobileVLCKit', s.version
|
||||||
|
s.tvos.dependency 'TVVLCKit', s.version
|
||||||
|
|
||||||
|
# Swift/Objective-C compatibility
|
||||||
|
s.pod_target_xcconfig = {
|
||||||
|
'DEFINES_MODULE' => 'YES',
|
||||||
|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||||
|
}
|
||||||
|
|
||||||
|
s.source_files = "*.{h,m,mm,swift,hpp,cpp}"
|
||||||
|
end
|
||||||
71
modules/vlc-player-3/ios/VlcPlayer3Module.swift
Normal file
71
modules/vlc-player-3/ios/VlcPlayer3Module.swift
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import ExpoModulesCore
|
||||||
|
|
||||||
|
public class VlcPlayer3Module: Module {
|
||||||
|
public func definition() -> ModuleDefinition {
|
||||||
|
Name("VlcPlayer3")
|
||||||
|
View(VlcPlayer3View.self) {
|
||||||
|
Prop("source") { (view: VlcPlayer3View, source: [String: Any]) in
|
||||||
|
view.setSource(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("paused") { (view: VlcPlayer3View, paused: Bool) in
|
||||||
|
if paused {
|
||||||
|
view.pause()
|
||||||
|
} else {
|
||||||
|
view.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Events(
|
||||||
|
"onPlaybackStateChanged",
|
||||||
|
"onVideoStateChange",
|
||||||
|
"onVideoLoadStart",
|
||||||
|
"onVideoLoadEnd",
|
||||||
|
"onVideoProgress",
|
||||||
|
"onVideoError",
|
||||||
|
"onPipStarted"
|
||||||
|
)
|
||||||
|
|
||||||
|
AsyncFunction("startPictureInPicture") { (view: VlcPlayer3View) in
|
||||||
|
view.startPictureInPicture()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("play") { (view: VlcPlayer3View) in
|
||||||
|
view.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("pause") { (view: VlcPlayer3View) in
|
||||||
|
view.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("stop") { (view: VlcPlayer3View) in
|
||||||
|
view.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("seekTo") { (view: VlcPlayer3View, time: Int32) in
|
||||||
|
view.seekTo(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("setAudioTrack") { (view: VlcPlayer3View, trackIndex: Int) in
|
||||||
|
view.setAudioTrack(trackIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("getAudioTracks") { (view: VlcPlayer3View) -> [[String: Any]]? in
|
||||||
|
return view.getAudioTracks()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("setSubtitleTrack") { (view: VlcPlayer3View, trackIndex: Int) in
|
||||||
|
view.setSubtitleTrack(trackIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("getSubtitleTracks") { (view: VlcPlayer3View) -> [[String: Any]]? in
|
||||||
|
return view.getSubtitleTracks()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("setSubtitleURL") {
|
||||||
|
(view: VlcPlayer3View, url: String, name: String) in
|
||||||
|
view.setSubtitleURL(url, name: name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
388
modules/vlc-player-3/ios/VlcPlayer3View.swift
Normal file
388
modules/vlc-player-3/ios/VlcPlayer3View.swift
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
import ExpoModulesCore
|
||||||
|
#if os(tvOS)
|
||||||
|
import TVVLCKit
|
||||||
|
#else
|
||||||
|
import MobileVLCKit
|
||||||
|
#endif
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class VlcPlayer3View: ExpoView {
|
||||||
|
private var mediaPlayer: VLCMediaPlayer?
|
||||||
|
private var videoView: UIView?
|
||||||
|
private var progressUpdateInterval: TimeInterval = 1.0 // Update interval set to 1 second
|
||||||
|
private var isPaused: Bool = false
|
||||||
|
private var currentGeometryCString: [CChar]?
|
||||||
|
private var lastReportedState: VLCMediaPlayerState?
|
||||||
|
private var lastReportedIsPlaying: Bool?
|
||||||
|
private var customSubtitles: [(internalName: String, originalName: String)] = []
|
||||||
|
private var startPosition: Int32 = 0
|
||||||
|
private var isMediaReady: Bool = false
|
||||||
|
private var externalTrack: [String: String]?
|
||||||
|
private var progressTimer: DispatchSourceTimer?
|
||||||
|
private var isStopping: Bool = false // Define isStopping here
|
||||||
|
private var lastProgressCall = Date().timeIntervalSince1970
|
||||||
|
var hasSource = false
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
|
required init(appContext: AppContext? = nil) {
|
||||||
|
super.init(appContext: appContext)
|
||||||
|
setupView()
|
||||||
|
setupNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Setup
|
||||||
|
|
||||||
|
private func setupView() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.backgroundColor = .black
|
||||||
|
self.videoView = UIView()
|
||||||
|
self.videoView?.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
if let videoView = self.videoView {
|
||||||
|
self.addSubview(videoView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
videoView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||||
|
videoView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||||
|
videoView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||||
|
videoView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupNotifications() {
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self, selector: #selector(applicationWillResignActive),
|
||||||
|
name: UIApplication.willResignActiveNotification, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self, selector: #selector(applicationDidBecomeActive),
|
||||||
|
name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Methods
|
||||||
|
func startPictureInPicture() { }
|
||||||
|
|
||||||
|
@objc func play() {
|
||||||
|
self.mediaPlayer?.play()
|
||||||
|
self.isPaused = false
|
||||||
|
print("Play")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func pause() {
|
||||||
|
self.mediaPlayer?.pause()
|
||||||
|
self.isPaused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func seekTo(_ time: Int32) {
|
||||||
|
guard let player = self.mediaPlayer else { return }
|
||||||
|
|
||||||
|
let wasPlaying = player.isPlaying
|
||||||
|
if wasPlaying {
|
||||||
|
self.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let duration = player.media?.length.intValue {
|
||||||
|
print("Seeking to time: \(time) Video Duration \(duration)")
|
||||||
|
|
||||||
|
// If the specified time is greater than the duration, seek to the end
|
||||||
|
let seekTime = time > duration ? duration - 1000 : time
|
||||||
|
player.time = VLCTime(int: seekTime)
|
||||||
|
|
||||||
|
if wasPlaying {
|
||||||
|
self.play()
|
||||||
|
}
|
||||||
|
self.updatePlayerState()
|
||||||
|
} else {
|
||||||
|
print("Error: Unable to retrieve video duration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func setSource(_ source: [String: Any]) {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if self.hasSource {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let mediaOptions = source["mediaOptions"] as? [String: Any] ?? [:]
|
||||||
|
self.externalTrack = source["externalTrack"] as? [String: String]
|
||||||
|
var initOptions = source["initOptions"] as? [Any] ?? []
|
||||||
|
self.startPosition = source["startPosition"] as? Int32 ?? 0
|
||||||
|
initOptions.append("--start-time=\(self.startPosition)")
|
||||||
|
|
||||||
|
guard let uri = source["uri"] as? String, !uri.isEmpty else {
|
||||||
|
print("Error: Invalid or empty URI")
|
||||||
|
self.onVideoError?(["error": "Invalid or empty URI"])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let autoplay = source["autoplay"] as? Bool ?? false
|
||||||
|
let isNetwork = source["isNetwork"] as? Bool ?? false
|
||||||
|
|
||||||
|
self.onVideoLoadStart?(["target": self.reactTag ?? NSNull()])
|
||||||
|
self.mediaPlayer = VLCMediaPlayer(options: initOptions)
|
||||||
|
self.mediaPlayer?.delegate = self
|
||||||
|
self.mediaPlayer?.drawable = self.videoView
|
||||||
|
self.mediaPlayer?.scaleFactor = 0
|
||||||
|
|
||||||
|
let media: VLCMedia
|
||||||
|
if isNetwork {
|
||||||
|
print("Loading network file: \(uri)")
|
||||||
|
media = VLCMedia(url: URL(string: uri)!)
|
||||||
|
} else {
|
||||||
|
print("Loading local file: \(uri)")
|
||||||
|
if uri.starts(with: "file://"), let url = URL(string: uri) {
|
||||||
|
media = VLCMedia(url: url)
|
||||||
|
} else {
|
||||||
|
media = VLCMedia(path: uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Debug: Media options: \(mediaOptions)")
|
||||||
|
media.addOptions(mediaOptions)
|
||||||
|
|
||||||
|
self.mediaPlayer?.media = media
|
||||||
|
self.hasSource = true
|
||||||
|
|
||||||
|
if autoplay {
|
||||||
|
print("Playing...")
|
||||||
|
self.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func setAudioTrack(_ trackIndex: Int) {
|
||||||
|
self.mediaPlayer?.currentAudioTrackIndex = Int32(trackIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getAudioTracks() -> [[String: Any]]? {
|
||||||
|
guard let trackNames = mediaPlayer?.audioTrackNames,
|
||||||
|
let trackIndexes = mediaPlayer?.audioTrackIndexes
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return zip(trackNames, trackIndexes).map { name, index in
|
||||||
|
return ["name": name, "index": index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func setSubtitleTrack(_ trackIndex: Int) {
|
||||||
|
print("Debug: Attempting to set subtitle track to index: \(trackIndex)")
|
||||||
|
self.mediaPlayer?.currentVideoSubTitleIndex = Int32(trackIndex)
|
||||||
|
print(
|
||||||
|
"Debug: Current subtitle track index after setting: \(self.mediaPlayer?.currentVideoSubTitleIndex ?? -1)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func setSubtitleURL(_ subtitleURL: String, name: String) {
|
||||||
|
guard let url = URL(string: subtitleURL) else {
|
||||||
|
print("Error: Invalid subtitle URL")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: true)
|
||||||
|
if let result = result {
|
||||||
|
let internalName = "Track \(self.customSubtitles.count + 1)"
|
||||||
|
print("Subtitle added with result: \(result) \(internalName)")
|
||||||
|
self.customSubtitles.append((internalName: internalName, originalName: name))
|
||||||
|
} else {
|
||||||
|
print("Failed to add subtitle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getSubtitleTracks() -> [[String: Any]]? {
|
||||||
|
guard let mediaPlayer = self.mediaPlayer else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = mediaPlayer.numberOfSubtitlesTracks
|
||||||
|
print("Debug: Number of subtitle tracks: \(count)")
|
||||||
|
|
||||||
|
guard count > 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var tracks: [[String: Any]] = []
|
||||||
|
|
||||||
|
if let names = mediaPlayer.videoSubTitlesNames as? [String],
|
||||||
|
let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber]
|
||||||
|
{
|
||||||
|
for (index, name) in zip(indexes, names) {
|
||||||
|
if let customSubtitle = customSubtitles.first(where: { $0.internalName == name }) {
|
||||||
|
tracks.append(["name": customSubtitle.originalName, "index": index.intValue])
|
||||||
|
} else {
|
||||||
|
tracks.append(["name": name, "index": index.intValue])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Debug: Subtitle tracks: \(tracks)")
|
||||||
|
return tracks
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func stop(completion: (() -> Void)? = nil) {
|
||||||
|
guard !isStopping else {
|
||||||
|
completion?()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isStopping = true
|
||||||
|
|
||||||
|
// If we're not on the main thread, dispatch to main thread
|
||||||
|
if !Thread.isMainThread {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.performStop(completion: completion)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
performStop(completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Methods
|
||||||
|
|
||||||
|
@objc private func applicationWillResignActive() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func applicationDidBecomeActive() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performStop(completion: (() -> Void)? = nil) {
|
||||||
|
// Stop the media player
|
||||||
|
mediaPlayer?.stop()
|
||||||
|
|
||||||
|
// Remove observer
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
|
||||||
|
// Clear the video view
|
||||||
|
videoView?.removeFromSuperview()
|
||||||
|
videoView = nil
|
||||||
|
|
||||||
|
// Release the media player
|
||||||
|
mediaPlayer?.delegate = nil
|
||||||
|
mediaPlayer = nil
|
||||||
|
|
||||||
|
isStopping = false
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateVideoProgress() {
|
||||||
|
guard let player = self.mediaPlayer else { return }
|
||||||
|
|
||||||
|
let currentTimeMs = player.time.intValue
|
||||||
|
let durationMs = player.media?.length.intValue ?? 0
|
||||||
|
|
||||||
|
print("Debug: Current time: \(currentTimeMs)")
|
||||||
|
if currentTimeMs >= 0 && currentTimeMs < durationMs {
|
||||||
|
if player.isPlaying && !self.isMediaReady {
|
||||||
|
self.isMediaReady = true
|
||||||
|
// Set external track subtitle when starting.
|
||||||
|
if let externalTrack = self.externalTrack {
|
||||||
|
if let name = externalTrack["name"], !name.isEmpty {
|
||||||
|
let deliveryUrl = externalTrack["DeliveryUrl"] ?? ""
|
||||||
|
self.setSubtitleURL(deliveryUrl, name: name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.onVideoProgress?([
|
||||||
|
"currentTime": currentTimeMs,
|
||||||
|
"duration": durationMs,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Expo Events
|
||||||
|
|
||||||
|
@objc var onPlaybackStateChanged: RCTDirectEventBlock?
|
||||||
|
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||||
|
@objc var onVideoStateChange: RCTDirectEventBlock?
|
||||||
|
@objc var onVideoProgress: RCTDirectEventBlock?
|
||||||
|
@objc var onVideoLoadEnd: RCTDirectEventBlock?
|
||||||
|
@objc var onVideoError: RCTDirectEventBlock?
|
||||||
|
@objc var onPipStarted: RCTDirectEventBlock?
|
||||||
|
|
||||||
|
// MARK: - Deinitialization
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
performStop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VlcPlayer3View: VLCMediaPlayerDelegate {
|
||||||
|
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
||||||
|
// self?.updateVideoProgress()
|
||||||
|
let timeNow = Date().timeIntervalSince1970
|
||||||
|
if timeNow - lastProgressCall >= 1 {
|
||||||
|
lastProgressCall = timeNow
|
||||||
|
updateVideoProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mediaPlayerStateChanged(_ aNotification: Notification) {
|
||||||
|
self.updatePlayerState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePlayerState() {
|
||||||
|
guard let player = self.mediaPlayer else { return }
|
||||||
|
let currentState = player.state
|
||||||
|
|
||||||
|
var stateInfo: [String: Any] = [
|
||||||
|
"target": self.reactTag ?? NSNull(),
|
||||||
|
"currentTime": player.time.intValue,
|
||||||
|
"duration": player.media?.length.intValue ?? 0,
|
||||||
|
"error": false,
|
||||||
|
]
|
||||||
|
|
||||||
|
if player.isPlaying {
|
||||||
|
stateInfo["isPlaying"] = true
|
||||||
|
stateInfo["isBuffering"] = false
|
||||||
|
stateInfo["state"] = "Playing"
|
||||||
|
} else {
|
||||||
|
stateInfo["isPlaying"] = false
|
||||||
|
stateInfo["state"] = "Paused"
|
||||||
|
}
|
||||||
|
|
||||||
|
if player.state == VLCMediaPlayerState.buffering {
|
||||||
|
stateInfo["isBuffering"] = true
|
||||||
|
stateInfo["state"] = "Buffering"
|
||||||
|
} else if player.state == VLCMediaPlayerState.error {
|
||||||
|
print("player.state ~ error")
|
||||||
|
stateInfo["state"] = "Error"
|
||||||
|
self.onVideoLoadEnd?(stateInfo)
|
||||||
|
} else if player.state == VLCMediaPlayerState.opening {
|
||||||
|
print("player.state ~ opening")
|
||||||
|
stateInfo["state"] = "Opening"
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.lastReportedState != currentState
|
||||||
|
|| self.lastReportedIsPlaying != player.isPlaying
|
||||||
|
{
|
||||||
|
self.lastReportedState = currentState
|
||||||
|
self.lastReportedIsPlaying = player.isPlaying
|
||||||
|
self.onVideoStateChange?(stateInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VlcPlayer3View: VLCMediaDelegate {
|
||||||
|
// Implement VLCMediaDelegate methods if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VLCMediaPlayerState {
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .opening: return "Opening"
|
||||||
|
case .buffering: return "Buffering"
|
||||||
|
case .playing: return "Playing"
|
||||||
|
case .paused: return "Paused"
|
||||||
|
case .stopped: return "Stopped"
|
||||||
|
case .ended: return "Ended"
|
||||||
|
case .error: return "Error"
|
||||||
|
case .esAdded: return "ESAdded"
|
||||||
|
@unknown default: return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
modules/vlc-player-3/src/VlcPlayer3Module.ts
Normal file
5
modules/vlc-player-3/src/VlcPlayer3Module.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { requireNativeModule } from 'expo-modules-core';
|
||||||
|
|
||||||
|
// It loads the native module object from the JSI or falls back to
|
||||||
|
// the bridge module (from NativeModulesProxy) if the remote debugger is on.
|
||||||
|
export default requireNativeModule('VlcPlayer3');
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
|||||||
#Sun Nov 17 18:25:45 AEDT 2024
|
|
||||||
gradle.version=8.9
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import {
|
|
||||||
EventEmitter,
|
|
||||||
EventSubscription,
|
|
||||||
} from "expo-modules-core";
|
|
||||||
|
|
||||||
import VlcPlayerModule from "./src/VlcPlayerModule";
|
|
||||||
import VlcPlayerView from "./src/VlcPlayerView";
|
|
||||||
import {
|
|
||||||
PlaybackStatePayload,
|
|
||||||
ProgressUpdatePayload,
|
|
||||||
VideoLoadStartPayload,
|
|
||||||
VideoStateChangePayload,
|
|
||||||
VideoProgressPayload,
|
|
||||||
VlcPlayerSource,
|
|
||||||
TrackInfo,
|
|
||||||
ChapterInfo,
|
|
||||||
VlcPlayerViewProps,
|
|
||||||
VlcPlayerViewRef,
|
|
||||||
} from "./src/VlcPlayer.types";
|
|
||||||
|
|
||||||
const emitter = new EventEmitter(VlcPlayerModule);
|
|
||||||
|
|
||||||
export function addPlaybackStateListener(
|
|
||||||
listener: (event: PlaybackStatePayload) => void
|
|
||||||
): EventSubscription {
|
|
||||||
return emitter.addListener<PlaybackStatePayload>(
|
|
||||||
"onPlaybackStateChanged",
|
|
||||||
listener
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addVideoLoadStartListener(
|
|
||||||
listener: (event: VideoLoadStartPayload) => void
|
|
||||||
): EventSubscription {
|
|
||||||
return emitter.addListener<VideoLoadStartPayload>(
|
|
||||||
"onVideoLoadStart",
|
|
||||||
listener
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addVideoStateChangeListener(
|
|
||||||
listener: (event: VideoStateChangePayload) => void
|
|
||||||
): EventSubscription {
|
|
||||||
return emitter.addListener<VideoStateChangePayload>(
|
|
||||||
"onVideoStateChange",
|
|
||||||
listener
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addVideoProgressListener(
|
|
||||||
listener: (event: VideoProgressPayload) => void
|
|
||||||
): EventSubscription {
|
|
||||||
return emitter.addListener<VideoProgressPayload>("onVideoProgress", listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
VlcPlayerView,
|
|
||||||
VlcPlayerViewProps,
|
|
||||||
VlcPlayerViewRef,
|
|
||||||
PlaybackStatePayload,
|
|
||||||
ProgressUpdatePayload,
|
|
||||||
VideoLoadStartPayload,
|
|
||||||
VideoStateChangePayload,
|
|
||||||
VideoProgressPayload,
|
|
||||||
VlcPlayerSource,
|
|
||||||
TrackInfo,
|
|
||||||
ChapterInfo,
|
|
||||||
};
|
|
||||||
@@ -19,6 +19,5 @@ Pod::Spec.new do |s|
|
|||||||
'DEFINES_MODULE' => 'YES',
|
'DEFINES_MODULE' => 'YES',
|
||||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||||
}
|
}
|
||||||
|
s.source_files = "*.{h,m,mm,swift,hpp,cpp}"
|
||||||
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import UIKit
|
|||||||
import VLCKit
|
import VLCKit
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
public class VLCPlayerView: UIView {
|
public class VLCPlayerView: UIView {
|
||||||
func setupView(parent: UIView) {
|
func setupView(parent: UIView) {
|
||||||
self.backgroundColor = .black
|
self.backgroundColor = .black
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
"UNKNOWN": "Unbekannt"
|
"UNKNOWN": "Unbekannt"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "Sicherer Bereich in den Steuerungen",
|
"safe_area_in_controls": "Sicherer Bereich in den Steuerungen",
|
||||||
|
"video_player": "Video player",
|
||||||
|
"video_players": {
|
||||||
|
"VLC_3": "VLC 3",
|
||||||
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
|
},
|
||||||
"show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen",
|
"show_custom_menu_links": "Benutzerdefinierte Menülinks anzeigen",
|
||||||
"hide_libraries": "Bibliotheken ausblenden",
|
"hide_libraries": "Bibliotheken ausblenden",
|
||||||
"select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.",
|
"select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.",
|
||||||
|
|||||||
@@ -129,11 +129,16 @@
|
|||||||
"UNKNOWN": "Unknown"
|
"UNKNOWN": "Unknown"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "Safe area in controls",
|
"safe_area_in_controls": "Safe area in controls",
|
||||||
|
"video_player": "Video player",
|
||||||
|
"video_players": {
|
||||||
|
"VLC_3": "VLC 3",
|
||||||
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
|
},
|
||||||
"show_custom_menu_links": "Show Custom Menu Links",
|
"show_custom_menu_links": "Show Custom Menu Links",
|
||||||
"hide_libraries": "Hide Libraries",
|
"hide_libraries": "Hide Libraries",
|
||||||
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
||||||
"disable_haptic_feedback": "Disable Haptic Feedback",
|
"disable_haptic_feedback": "Disable Haptic Feedback",
|
||||||
"default_quality": "Default quality"
|
"default_quality": "Default quality",
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"downloads_title": "Downloads",
|
"downloads_title": "Downloads",
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
"UNKNOWN": "Desconocida"
|
"UNKNOWN": "Desconocida"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "Área segura en controles",
|
"safe_area_in_controls": "Área segura en controles",
|
||||||
|
"video_player": "Video player",
|
||||||
|
"video_players": {
|
||||||
|
"VLC_3": "VLC 3",
|
||||||
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
|
},
|
||||||
"show_custom_menu_links": "Mostrar enlaces de menú personalizados",
|
"show_custom_menu_links": "Mostrar enlaces de menú personalizados",
|
||||||
"hide_libraries": "Ocultar bibliotecas",
|
"hide_libraries": "Ocultar bibliotecas",
|
||||||
"select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
|
"select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
"UNKNOWN": "Inconnu"
|
"UNKNOWN": "Inconnu"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "Zone de sécurité dans les contrôles",
|
"safe_area_in_controls": "Zone de sécurité dans les contrôles",
|
||||||
|
"video_player": "Video player",
|
||||||
|
"video_players": {
|
||||||
|
"VLC_3": "VLC 3",
|
||||||
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
|
},
|
||||||
"show_custom_menu_links": "Afficher les liens personnalisés",
|
"show_custom_menu_links": "Afficher les liens personnalisés",
|
||||||
"hide_libraries": "Cacher des bibliothèques",
|
"hide_libraries": "Cacher des bibliothèques",
|
||||||
"select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l’onglet Bibliothèque et les sections de la page d’accueil.",
|
"select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l’onglet Bibliothèque et les sections de la page d’accueil.",
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
"UNKNOWN": "Sconosciuto"
|
"UNKNOWN": "Sconosciuto"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "Area sicura per i controlli",
|
"safe_area_in_controls": "Area sicura per i controlli",
|
||||||
|
"video_player": "Video player",
|
||||||
|
"video_players": {
|
||||||
|
"VLC_3": "VLC 3",
|
||||||
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
|
},
|
||||||
"show_custom_menu_links": "Mostra i link del menu personalizzato",
|
"show_custom_menu_links": "Mostra i link del menu personalizzato",
|
||||||
"hide_libraries": "Nascondi Librerie",
|
"hide_libraries": "Nascondi Librerie",
|
||||||
"select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.",
|
"select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.",
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
"UNKNOWN": "不明"
|
"UNKNOWN": "不明"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "コントロールの安全エリア",
|
"safe_area_in_controls": "コントロールの安全エリア",
|
||||||
|
"video_player": "Video player",
|
||||||
|
"video_players": {
|
||||||
|
"VLC_3": "VLC 3",
|
||||||
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
|
},
|
||||||
"show_custom_menu_links": "カスタムメニューのリンクを表示",
|
"show_custom_menu_links": "カスタムメニューのリンクを表示",
|
||||||
"hide_libraries": "ライブラリを非表示",
|
"hide_libraries": "ライブラリを非表示",
|
||||||
"select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。",
|
"select_liraries_you_want_to_hide": "ライブラリタブとホームページセクションから非表示にするライブラリを選択します。",
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
"UNKNOWN": "Onbekend"
|
"UNKNOWN": "Onbekend"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "Veilig gebied in bedieningen",
|
"safe_area_in_controls": "Veilig gebied in bedieningen",
|
||||||
|
"video_player": "Video player",
|
||||||
|
"video_players": {
|
||||||
|
"VLC_3": "VLC 3",
|
||||||
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
|
},
|
||||||
"show_custom_menu_links": "Aangepaste menulinks tonen",
|
"show_custom_menu_links": "Aangepaste menulinks tonen",
|
||||||
"hide_libraries": "Verberg Bibliotheken",
|
"hide_libraries": "Verberg Bibliotheken",
|
||||||
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.",
|
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.",
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
"UNKNOWN": "Bilinmeyen"
|
"UNKNOWN": "Bilinmeyen"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "Kontrollerde Güvenli Alan",
|
"safe_area_in_controls": "Kontrollerde Güvenli Alan",
|
||||||
|
"video_player": "Video player",
|
||||||
|
"video_players": {
|
||||||
|
"VLC_3": "VLC 3",
|
||||||
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
|
},
|
||||||
"show_custom_menu_links": "Özel Menü Bağlantılarını Göster",
|
"show_custom_menu_links": "Özel Menü Bağlantılarını Göster",
|
||||||
"hide_libraries": "Kütüphaneleri Gizle",
|
"hide_libraries": "Kütüphaneleri Gizle",
|
||||||
"select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.",
|
"select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.",
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
"UNKNOWN": "未知"
|
"UNKNOWN": "未知"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "控制中的安全区域",
|
"safe_area_in_controls": "控制中的安全区域",
|
||||||
|
"video_player": "Video player",
|
||||||
|
"video_players": {
|
||||||
|
"VLC_3": "VLC 3",
|
||||||
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
|
},
|
||||||
"show_custom_menu_links": "显示自定义菜单链接",
|
"show_custom_menu_links": "显示自定义菜单链接",
|
||||||
"hide_libraries": "隐藏媒体库",
|
"hide_libraries": "隐藏媒体库",
|
||||||
"select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。",
|
"select_liraries_you_want_to_hide": "选择您想从媒体库页面和主页隐藏的媒体库。",
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
"UNKNOWN": "未知"
|
"UNKNOWN": "未知"
|
||||||
},
|
},
|
||||||
"safe_area_in_controls": "控制中的安全區域",
|
"safe_area_in_controls": "控制中的安全區域",
|
||||||
|
"video_player": "Video player",
|
||||||
|
"video_players": {
|
||||||
|
"VLC_3": "VLC 3",
|
||||||
|
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||||
|
},
|
||||||
"show_custom_menu_links": "顯示自定義菜單鏈接",
|
"show_custom_menu_links": "顯示自定義菜單鏈接",
|
||||||
"hide_libraries": "隱藏媒體庫",
|
"hide_libraries": "隱藏媒體庫",
|
||||||
"select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。",
|
"select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { Bitrate, BITRATES } from "@/components/BitrateSelector";
|
import { Bitrate, BITRATES } from "@/components/BitrateSelector";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { writeInfoLog } from "@/utils/log";
|
import { writeInfoLog } from "@/utils/log";
|
||||||
|
import {Video} from "@/utils/jellyseerr/server/models/Movie";
|
||||||
|
|
||||||
const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004";
|
const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004";
|
||||||
const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS";
|
const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS";
|
||||||
@@ -112,6 +113,12 @@ export type HomeSectionNextUpResolver = {
|
|||||||
enableRewatching?: boolean;
|
enableRewatching?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum VideoPlayer {
|
||||||
|
// NATIVE, //todo: changes will make this a lot more easier to implement if we want. delete if not wanted
|
||||||
|
VLC_3,
|
||||||
|
VLC_4
|
||||||
|
}
|
||||||
|
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
home?: Home | null;
|
home?: Home | null;
|
||||||
autoRotate?: boolean;
|
autoRotate?: boolean;
|
||||||
@@ -146,6 +153,7 @@ export type Settings = {
|
|||||||
jellyseerrServerUrl?: string;
|
jellyseerrServerUrl?: string;
|
||||||
hiddenLibraries?: string[];
|
hiddenLibraries?: string[];
|
||||||
enableH265ForChromecast: boolean;
|
enableH265ForChromecast: boolean;
|
||||||
|
defaultPlayer: VideoPlayer;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Lockable<T> {
|
export interface Lockable<T> {
|
||||||
@@ -200,6 +208,7 @@ const defaultValues: Settings = {
|
|||||||
jellyseerrServerUrl: undefined,
|
jellyseerrServerUrl: undefined,
|
||||||
hiddenLibraries: [],
|
hiddenLibraries: [],
|
||||||
enableH265ForChromecast: false,
|
enableH265ForChromecast: false,
|
||||||
|
defaultPlayer: VideoPlayer.VLC_3, // ios only setting. does not matter what this is for android
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadSettings = (): Partial<Settings> => {
|
const loadSettings = (): Partial<Settings> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user