mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
WIP
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ npm-debug.*
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
modules/vlc-player/android/build
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
329
.idea/caches/deviceStreaming.xml
generated
Normal file
329
.idea/caches/deviceStreaming.xml
generated
Normal file
@@ -0,0 +1,329 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceStreaming">
|
||||
<option name="deviceSelectionList">
|
||||
<list>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="27" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="F01L" />
|
||||
<option name="id" value="F01L" />
|
||||
<option name="manufacturer" value="FUJITSU" />
|
||||
<option name="name" value="F-01L" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1280" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="28" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="SH-01L" />
|
||||
<option name="id" value="SH-01L" />
|
||||
<option name="manufacturer" value="SHARP" />
|
||||
<option name="name" value="AQUOS sense2 SH-01L" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="Lenovo" />
|
||||
<option name="codename" value="TB370FU" />
|
||||
<option name="id" value="TB370FU" />
|
||||
<option name="manufacturer" value="Lenovo" />
|
||||
<option name="name" value="Tab P12" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1840" />
|
||||
<option name="screenY" value="2944" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a51" />
|
||||
<option name="id" value="a51" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy A51" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="akita" />
|
||||
<option name="id" value="akita" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="b0q" />
|
||||
<option name="id" value="b0q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S22 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="32" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="bluejay" />
|
||||
<option name="id" value="bluejay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="caiman" />
|
||||
<option name="id" value="caiman" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="960" />
|
||||
<option name="screenY" value="2142" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="comet" />
|
||||
<option name="id" value="comet" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro Fold" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="2076" />
|
||||
<option name="screenY" value="2152" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="crownqlteue" />
|
||||
<option name="id" value="crownqlteue" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Note9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2220" />
|
||||
<option name="screenY" value="1080" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="dm3q" />
|
||||
<option name="id" value="dm3q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S23 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="e1q" />
|
||||
<option name="id" value="e1q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S24" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix_camera" />
|
||||
<option name="id" value="felix_camera" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts8uwifi" />
|
||||
<option name="id" value="gts8uwifi" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S8 Ultra" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1848" />
|
||||
<option name="screenY" value="2960" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="husky" />
|
||||
<option name="id" value="husky" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8 Pro" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="java" />
|
||||
<option name="id" value="java" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="G20" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="komodo" />
|
||||
<option name="id" value="komodo" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro XL" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="lynx" />
|
||||
<option name="id" value="lynx" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="oriole" />
|
||||
<option name="id" value="oriole" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="panther" />
|
||||
<option name="id" value="panther" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q5q" />
|
||||
<option name="id" value="q5q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold5" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1812" />
|
||||
<option name="screenY" value="2176" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q6q" />
|
||||
<option name="id" value="q6q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1856" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="r11" />
|
||||
<option name="id" value="r11" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Watch" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
<option name="type" value="WEAR_OS" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="redfin" />
|
||||
<option name="id" value="redfin" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 5" />
|
||||
<option name="screenDensity" value="440" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="shiba" />
|
||||
<option name="id" value="shiba" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tangorpro" />
|
||||
<option name="id" value="tangorpro" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Tablet" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tokay" />
|
||||
<option name="id" value="tokay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/streamyfin.iml" filepath="$PROJECT_DIR$/.idea/streamyfin.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/streamyfin.iml
generated
Normal file
9
.idea/streamyfin.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -11,5 +11,7 @@
|
||||
},
|
||||
"[swift]": {
|
||||
"editor.defaultFormatter": "sswg.swift-lang"
|
||||
}
|
||||
},
|
||||
"java.configuration.updateBuildConfiguration": "interactive",
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { BITRATES } from "@/components/BitrateSelector";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { Controls } from "@/components/video-player/Controls";
|
||||
import { useOrientation } from "@/hooks/useOrientation";
|
||||
import { useOrientationSettings } from "@/hooks/useOrientationSettings";
|
||||
import { useWebSocket } from "@/hooks/useWebsockets";
|
||||
import { TrackInfo } from "@/modules/vlc-player";
|
||||
import { VlcPlayerView } from "@/modules/vlc-player";
|
||||
import {
|
||||
PlaybackStatePayload,
|
||||
ProgressUpdatePayload,
|
||||
VlcPlayerViewRef,
|
||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
import { writeToLog } from "@/utils/log";
|
||||
import native from "@/utils/profiles/native";
|
||||
import android from "@/utils/profiles/android";
|
||||
import { secondsToTicks } from "@/utils/secondsToTicks";
|
||||
import { msToTicks, ticksToMs } from "@/utils/time";
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import {
|
||||
@@ -21,32 +25,26 @@ import {
|
||||
} from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useAtomValue } from "jotai";
|
||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { Pressable, useWindowDimensions, View } from "react-native";
|
||||
import { Alert, Pressable, useWindowDimensions, View } from "react-native";
|
||||
import { SystemBars } from "react-native-edge-to-edge";
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
import Video, {
|
||||
OnProgressData,
|
||||
SelectedTrack,
|
||||
SelectedTrackType,
|
||||
VideoRef,
|
||||
} from "react-native-video";
|
||||
|
||||
const Player = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const videoRef = useRef<VlcPlayerViewRef>(null);
|
||||
const user = useAtomValue(userAtom);
|
||||
const [settings] = useSettings();
|
||||
const videoRef = useRef<VideoRef | null>(null);
|
||||
const api = useAtomValue(apiAtom);
|
||||
|
||||
const firstTime = useRef(true);
|
||||
const dimensions = useWindowDimensions();
|
||||
const windowDimensions = useWindowDimensions();
|
||||
|
||||
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isBuffering, setIsBuffering] = useState(true);
|
||||
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
||||
|
||||
const progress = useSharedValue(0);
|
||||
const isSeeking = useSharedValue(false);
|
||||
@@ -67,12 +65,10 @@ const Player = () => {
|
||||
}>();
|
||||
|
||||
const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined;
|
||||
const subtitleIndex = subtitleIndexStr
|
||||
? parseInt(subtitleIndexStr, 10)
|
||||
: undefined;
|
||||
const subtitleIndex = subtitleIndexStr ? parseInt(subtitleIndexStr, 10) : -1;
|
||||
const bitrateValue = bitrateValueStr
|
||||
? parseInt(bitrateValueStr, 10)
|
||||
: undefined;
|
||||
: BITRATES[0].value;
|
||||
|
||||
const {
|
||||
data: item,
|
||||
@@ -81,15 +77,7 @@ const Player = () => {
|
||||
} = useQuery({
|
||||
queryKey: ["item", itemId],
|
||||
queryFn: async () => {
|
||||
if (!api) {
|
||||
throw new Error("No api");
|
||||
}
|
||||
|
||||
if (!itemId) {
|
||||
console.warn("No itemId");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!api) return;
|
||||
const res = await getUserLibraryApi(api).getItem({
|
||||
itemId,
|
||||
userId: user?.Id,
|
||||
@@ -97,6 +85,7 @@ const Player = () => {
|
||||
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!itemId && !!api,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
@@ -110,20 +99,11 @@ const Player = () => {
|
||||
itemId,
|
||||
audioIndex,
|
||||
subtitleIndex,
|
||||
bitrateValue,
|
||||
user,
|
||||
mediaSourceId,
|
||||
bitrateValue,
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!api) {
|
||||
throw new Error("No api");
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
console.warn("No item", itemId, item);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!api) return;
|
||||
const res = await getStreamUrl({
|
||||
api,
|
||||
item,
|
||||
@@ -133,17 +113,16 @@ const Player = () => {
|
||||
maxStreamingBitrate: bitrateValue,
|
||||
mediaSourceId: mediaSourceId,
|
||||
subtitleStreamIndex: subtitleIndex,
|
||||
deviceProfile: android,
|
||||
deviceProfile: native,
|
||||
});
|
||||
|
||||
if (!res) return null;
|
||||
|
||||
const { mediaSource, sessionId, url } = res;
|
||||
|
||||
if (!sessionId || !mediaSource || !url) {
|
||||
console.warn("No sessionId or mediaSource or url", url);
|
||||
return null;
|
||||
}
|
||||
if (!sessionId || !mediaSource || !url) return null;
|
||||
|
||||
console.log(url);
|
||||
|
||||
return {
|
||||
mediaSource,
|
||||
@@ -151,43 +130,44 @@ const Player = () => {
|
||||
url,
|
||||
};
|
||||
},
|
||||
enabled: !!item,
|
||||
enabled: !!itemId && !!api && !!item,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const poster = usePoster(item, api);
|
||||
const videoSource = useVideoSource(item, api, poster, stream?.url);
|
||||
|
||||
const togglePlay = useCallback(
|
||||
async (ticks: number) => {
|
||||
async (ms: number) => {
|
||||
if (!api || !stream) return;
|
||||
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
if (isPlaying) {
|
||||
videoRef.current?.pause();
|
||||
await getPlaystateApi(api!).onPlaybackProgress({
|
||||
await videoRef.current?.pause();
|
||||
|
||||
await getPlaystateApi(api).onPlaybackProgress({
|
||||
itemId: item?.Id!,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
positionTicks: Math.floor(ticks),
|
||||
positionTicks: msToTicks(ms),
|
||||
isPaused: true,
|
||||
playMethod: stream?.url.includes("m3u8")
|
||||
playMethod: stream.url?.includes("m3u8")
|
||||
? "Transcode"
|
||||
: "DirectStream",
|
||||
playSessionId: stream?.sessionId,
|
||||
playSessionId: stream.sessionId,
|
||||
});
|
||||
console.log("ACtually marked as paused");
|
||||
} else {
|
||||
videoRef.current?.resume();
|
||||
await getPlaystateApi(api!).onPlaybackProgress({
|
||||
videoRef.current?.play();
|
||||
await getPlaystateApi(api).onPlaybackProgress({
|
||||
itemId: item?.Id!,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
positionTicks: Math.floor(ticks),
|
||||
positionTicks: msToTicks(ms),
|
||||
isPaused: false,
|
||||
playMethod: stream?.url.includes("m3u8")
|
||||
? "Transcode"
|
||||
: "DirectStream",
|
||||
playSessionId: stream?.sessionId,
|
||||
playSessionId: stream.sessionId,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -195,9 +175,8 @@ const Player = () => {
|
||||
isPlaying,
|
||||
api,
|
||||
item,
|
||||
videoRef,
|
||||
settings,
|
||||
stream,
|
||||
videoRef,
|
||||
audioIndex,
|
||||
subtitleIndex,
|
||||
mediaSourceId,
|
||||
@@ -205,7 +184,7 @@ const Player = () => {
|
||||
);
|
||||
|
||||
const play = useCallback(() => {
|
||||
videoRef.current?.resume();
|
||||
videoRef.current?.play();
|
||||
reportPlaybackStart();
|
||||
}, [videoRef]);
|
||||
|
||||
@@ -215,89 +194,60 @@ const Player = () => {
|
||||
|
||||
const stop = useCallback(() => {
|
||||
setIsPlaybackStopped(true);
|
||||
videoRef.current?.pause();
|
||||
videoRef.current?.stop();
|
||||
reportPlaybackStopped();
|
||||
}, [videoRef]);
|
||||
|
||||
const seek = useCallback(
|
||||
(seconds: number) => {
|
||||
videoRef.current?.seek(seconds);
|
||||
},
|
||||
[videoRef]
|
||||
);
|
||||
|
||||
const reportPlaybackStopped = async () => {
|
||||
if (!item?.Id) return;
|
||||
const currentTimeInTicks = msToTicks(progress.value);
|
||||
|
||||
await getPlaystateApi(api!).onPlaybackStopped({
|
||||
itemId: item.Id,
|
||||
itemId: item?.Id!,
|
||||
mediaSourceId: mediaSourceId,
|
||||
positionTicks: Math.floor(progress.value),
|
||||
playSessionId: stream?.sessionId,
|
||||
positionTicks: currentTimeInTicks,
|
||||
playSessionId: stream?.sessionId!,
|
||||
});
|
||||
};
|
||||
|
||||
const reportPlaybackStart = async () => {
|
||||
if (!item?.Id) return;
|
||||
await getPlaystateApi(api!).onPlaybackStart({
|
||||
itemId: item.Id,
|
||||
if (!api || !stream) return;
|
||||
await getPlaystateApi(api).onPlaybackStart({
|
||||
itemId: item?.Id!,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream?.sessionId,
|
||||
playMethod: stream.url?.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream?.sessionId ? stream?.sessionId : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const onProgress = useCallback(
|
||||
async (data: OnProgressData) => {
|
||||
async (data: ProgressUpdatePayload) => {
|
||||
if (isSeeking.value === true) return;
|
||||
if (isPlaybackStopped === true) return;
|
||||
if (!item?.Id || !api || !stream) return;
|
||||
|
||||
const ticks = secondsToTicks(data.currentTime);
|
||||
const { currentTime } = data.nativeEvent;
|
||||
|
||||
progress.value = ticks;
|
||||
cacheProgress.value = secondsToTicks(data.playableDuration);
|
||||
|
||||
// TODO: Use this when streaming with HLS url, but NOT when direct playing
|
||||
// TODO: since playable duration is always 0 then.
|
||||
// setIsBuffering(data.playableDuration === 0);
|
||||
|
||||
if (!item?.Id || data.currentTime === 0) {
|
||||
return;
|
||||
if (isBuffering) {
|
||||
setIsBuffering(false);
|
||||
}
|
||||
|
||||
await getPlaystateApi(api!).onPlaybackProgress({
|
||||
progress.value = currentTime;
|
||||
const currentTimeInTicks = msToTicks(currentTime);
|
||||
|
||||
await getPlaystateApi(api).onPlaybackProgress({
|
||||
itemId: item.Id,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
positionTicks: Math.round(ticks),
|
||||
positionTicks: Math.floor(currentTimeInTicks),
|
||||
isPaused: !isPlaying,
|
||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream?.sessionId,
|
||||
playSessionId: stream.sessionId,
|
||||
});
|
||||
},
|
||||
[
|
||||
item,
|
||||
isPlaying,
|
||||
api,
|
||||
isPlaybackStopped,
|
||||
isSeeking,
|
||||
stream,
|
||||
mediaSourceId,
|
||||
audioIndex,
|
||||
subtitleIndex,
|
||||
]
|
||||
);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
play();
|
||||
|
||||
return () => {
|
||||
stop();
|
||||
};
|
||||
}, [play, stop])
|
||||
[item?.Id, isPlaying, api, isPlaybackStopped]
|
||||
);
|
||||
|
||||
useOrientation();
|
||||
@@ -310,38 +260,25 @@ const Player = () => {
|
||||
stopPlayback: stop,
|
||||
});
|
||||
|
||||
const [selectedTextTrack, setSelectedTextTrack] = useState<
|
||||
SelectedTrack | undefined
|
||||
>();
|
||||
const onPlaybackStateChanged = (e: PlaybackStatePayload) => {
|
||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||
|
||||
const [embededTextTracks, setEmbededTextTracks] = useState<
|
||||
{
|
||||
index: number;
|
||||
language?: string | undefined;
|
||||
selected?: boolean | undefined;
|
||||
title?: string | undefined;
|
||||
type: any;
|
||||
}[]
|
||||
>([]);
|
||||
if (state === "Playing") {
|
||||
setIsPlaying(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const [audioTracks, setAudioTracks] = useState<TrackInfo[]>([]);
|
||||
const [selectedAudioTrack, setSelectedAudioTrack] = useState<
|
||||
SelectedTrack | undefined
|
||||
>(undefined);
|
||||
if (state === "Paused") {
|
||||
setIsPlaying(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const getAudioTracks = (): TrackInfo[] => {
|
||||
return audioTracks.map((t) => ({
|
||||
name: t.name,
|
||||
index: t.index,
|
||||
}));
|
||||
};
|
||||
|
||||
const getSubtitleTracks = (): TrackInfo[] => {
|
||||
return embededTextTracks.map((t) => ({
|
||||
name: t.title ?? "",
|
||||
index: t.index,
|
||||
language: t.language,
|
||||
}));
|
||||
if (isPlaying) {
|
||||
setIsPlaying(true);
|
||||
setIsBuffering(false);
|
||||
} else if (isBuffering) {
|
||||
setIsBuffering(true);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoadingItem || isLoadingStreamUrl)
|
||||
@@ -358,88 +295,60 @@ const Player = () => {
|
||||
</View>
|
||||
);
|
||||
|
||||
if (!stream || !item) return null;
|
||||
|
||||
const startPosition = item?.UserData?.PlaybackPositionTicks
|
||||
? ticksToMs(item.UserData.PlaybackPositionTicks)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
width: windowDimensions.width,
|
||||
height: windowDimensions.height,
|
||||
position: "relative",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
className="flex flex-col items-center justify-center"
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setShowControls(!showControls);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 0,
|
||||
}}
|
||||
className="absolute z-0 h-full w-full"
|
||||
>
|
||||
{videoSource ? (
|
||||
<Video
|
||||
ref={videoRef}
|
||||
source={videoSource}
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
resizeMode={ignoreSafeAreas ? "cover" : "contain"}
|
||||
onProgress={onProgress}
|
||||
onError={(e) => {
|
||||
console.error("Error playing video", e);
|
||||
}}
|
||||
onLoad={() => {
|
||||
if (firstTime.current === true) {
|
||||
play();
|
||||
firstTime.current = false;
|
||||
}
|
||||
}}
|
||||
progressUpdateInterval={500}
|
||||
playWhenInactive={true}
|
||||
allowsExternalPlayback={true}
|
||||
playInBackground={true}
|
||||
pictureInPicture={true}
|
||||
showNotificationControls={true}
|
||||
ignoreSilentSwitch="ignore"
|
||||
fullscreen={false}
|
||||
onPlaybackStateChanged={(state) => {
|
||||
if (isSeeking.value === false) setIsPlaying(state.isPlaying);
|
||||
}}
|
||||
onTextTracks={(data) => {
|
||||
setEmbededTextTracks(data.textTracks as any);
|
||||
}}
|
||||
onBuffer={(e) => {
|
||||
setIsBuffering(e.isBuffering);
|
||||
}}
|
||||
onAudioTracks={(e) => {
|
||||
console.log("onAudioTracks: ", e.audioTracks);
|
||||
setAudioTracks(
|
||||
e.audioTracks.map((t) => ({
|
||||
index: t.index,
|
||||
name: t.title ?? "",
|
||||
language: t.language,
|
||||
}))
|
||||
);
|
||||
}}
|
||||
selectedTextTrack={selectedTextTrack}
|
||||
selectedAudioTrack={selectedAudioTrack}
|
||||
/>
|
||||
) : (
|
||||
<Text>No video source...</Text>
|
||||
)}
|
||||
<VlcPlayerView
|
||||
ref={videoRef}
|
||||
source={{
|
||||
uri: stream.url,
|
||||
autoplay: true,
|
||||
isNetwork: true,
|
||||
startPosition,
|
||||
initOptions: ["--sub-text-scale=60"],
|
||||
}}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
onVideoProgress={onProgress}
|
||||
progressUpdateInterval={1000}
|
||||
onVideoStateChange={onPlaybackStateChanged}
|
||||
onVideoLoadStart={() => {}}
|
||||
onVideoLoadEnd={() => {
|
||||
setIsVideoLoaded(true);
|
||||
}}
|
||||
onVideoError={(e) => {
|
||||
console.error("Video Error:", e.nativeEvent);
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"An error occurred while playing the video. Check logs in settings."
|
||||
);
|
||||
writeToLog("ERROR", "Video Error", e.nativeEvent);
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
{item && (
|
||||
{videoRef.current && (
|
||||
<Controls
|
||||
videoRef={videoRef}
|
||||
enableTrickplay={true}
|
||||
mediaSource={stream.mediaSource}
|
||||
item={item}
|
||||
videoRef={videoRef}
|
||||
togglePlay={togglePlay}
|
||||
isPlaying={isPlaying}
|
||||
isSeeking={isSeeking}
|
||||
@@ -450,24 +359,19 @@ const Player = () => {
|
||||
setShowControls={setShowControls}
|
||||
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
||||
ignoreSafeAreas={ignoreSafeAreas}
|
||||
seek={seek}
|
||||
play={play}
|
||||
pause={pause}
|
||||
getSubtitleTracks={getSubtitleTracks}
|
||||
setSubtitleTrack={(i) =>
|
||||
setSelectedTextTrack({
|
||||
type: SelectedTrackType.INDEX,
|
||||
value: i,
|
||||
})
|
||||
}
|
||||
getAudioTracks={getAudioTracks}
|
||||
setAudioTrack={(i) => {
|
||||
console.log("setAudioTrack ~", i);
|
||||
setSelectedAudioTrack({
|
||||
type: SelectedTrackType.INDEX,
|
||||
value: i,
|
||||
});
|
||||
}}
|
||||
isVideoLoaded={isVideoLoaded}
|
||||
play={videoRef.current?.play}
|
||||
pause={videoRef.current?.pause}
|
||||
seek={videoRef.current?.seekTo}
|
||||
enableTrickplay={true}
|
||||
getAudioTracks={videoRef.current?.getAudioTracks}
|
||||
getSubtitleTracks={videoRef.current?.getSubtitleTracks}
|
||||
offline={false}
|
||||
setSubtitleTrack={videoRef.current.setSubtitleTrack}
|
||||
setSubtitleURL={videoRef.current.setSubtitleURL}
|
||||
setAudioTrack={videoRef.current.setAudioTrack}
|
||||
stop={stop}
|
||||
isVlc
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
@@ -475,7 +379,7 @@ const Player = () => {
|
||||
};
|
||||
|
||||
export function usePoster(
|
||||
item: BaseItemDto | null | undefined,
|
||||
item: BaseItemDto,
|
||||
api: Api | null
|
||||
): string | undefined {
|
||||
const poster = useMemo(() => {
|
||||
@@ -493,37 +397,4 @@ export function usePoster(
|
||||
return poster ?? undefined;
|
||||
}
|
||||
|
||||
export function useVideoSource(
|
||||
item: BaseItemDto | null | undefined,
|
||||
api: Api | null,
|
||||
poster: string | undefined,
|
||||
url?: string | null
|
||||
) {
|
||||
const videoSource = useMemo(() => {
|
||||
if (!item || !api || !url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startPosition = item?.UserData?.PlaybackPositionTicks
|
||||
? Math.round(item.UserData.PlaybackPositionTicks / 10000)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
uri: url,
|
||||
isNetwork: true,
|
||||
startPosition,
|
||||
headers: getAuthHeaders(api),
|
||||
metadata: {
|
||||
artist: item?.AlbumArtist ?? undefined,
|
||||
title: item?.Name || "Unknown",
|
||||
description: item?.Overview ?? undefined,
|
||||
imageUri: poster,
|
||||
subtitle: item?.Album ?? undefined,
|
||||
},
|
||||
};
|
||||
}, [item, api, poster, url]);
|
||||
|
||||
return videoSource;
|
||||
}
|
||||
|
||||
export default Player;
|
||||
|
||||
70
modules/vlc-player/android/build.gradle
Normal file
70
modules/vlc-player/android/build.gradle
Normal file
@@ -0,0 +1,70 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
group = 'expo.modules.vlcplayer'
|
||||
version = '0.6.0'
|
||||
|
||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||
apply from: expoModulesCorePlugin
|
||||
applyKotlinExpoModulesCorePlugin()
|
||||
useCoreDependencies()
|
||||
useExpoPublishing()
|
||||
|
||||
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
||||
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
||||
// Most of the time, you may like to manage the Android SDK versions yourself.
|
||||
def useManagedAndroidSdkVersions = false
|
||||
if (useManagedAndroidSdkVersions) {
|
||||
useDefaultAndroidSdkVersions()
|
||||
} else {
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:7.1.3"
|
||||
}
|
||||
}
|
||||
project.android {
|
||||
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
||||
defaultConfig {
|
||||
minSdkVersion safeExtGet("minSdkVersion", 21)
|
||||
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.videolan.android:libvlc-all:4.0.0-eap15'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.31"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "expo.modules.vlcplayer"
|
||||
compileSdkVersion 34
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode 1
|
||||
versionName "0.6.0"
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += ["-Xshow-kotlin-compiler-errors"]
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
2
modules/vlc-player/android/src/main/AndroidManifest.xml
Normal file
2
modules/vlc-player/android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<manifest>
|
||||
</manifest>
|
||||
@@ -0,0 +1,69 @@
|
||||
package expo.modules.vlcplayer
|
||||
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class VlcPlayerModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("VlcPlayer")
|
||||
|
||||
View(VlcPlayerView::class) {
|
||||
Prop("source") { view: VlcPlayerView, source: Map<String, Any> ->
|
||||
view.setSource(source)
|
||||
}
|
||||
|
||||
Prop("paused") { view: VlcPlayerView, paused: Boolean ->
|
||||
if (paused) {
|
||||
view.pause()
|
||||
} else {
|
||||
view.play()
|
||||
}
|
||||
}
|
||||
|
||||
Events(
|
||||
"onPlaybackStateChanged",
|
||||
"onVideoStateChange",
|
||||
"onVideoLoadStart",
|
||||
"onVideoLoadEnd",
|
||||
"onVideoProgress",
|
||||
"onVideoError"
|
||||
)
|
||||
|
||||
AsyncFunction("play") { view: VlcPlayerView ->
|
||||
view.play()
|
||||
}
|
||||
|
||||
AsyncFunction("pause") { view: VlcPlayerView ->
|
||||
view.pause()
|
||||
}
|
||||
|
||||
AsyncFunction("stop") { view: VlcPlayerView ->
|
||||
view.stop()
|
||||
}
|
||||
|
||||
AsyncFunction("seekTo") { view: VlcPlayerView, time: Int ->
|
||||
view.seekTo(time)
|
||||
}
|
||||
|
||||
// AsyncFunction("setAudioTrack") { view: VlcPlayerView, trackIndex: Int ->
|
||||
// view.setAudioTrack(trackIndex)
|
||||
// }
|
||||
|
||||
// AsyncFunction("getAudioTracks") { view: VlcPlayerView -> List<Map<String, Ansy>>? ->
|
||||
// view.getAudioTracks()
|
||||
// }
|
||||
|
||||
// AsyncFunction("setSubtitleTrack") { view: VlcPlayerView, trackIndex: Int ->
|
||||
// view.setSubtitleTrack(trackIndex)
|
||||
// }
|
||||
|
||||
// AsyncFunction("getSubtitleTracks") { view: VlcPlayerView -> List<Map<String, Any>>? ->
|
||||
// view.getSubtitleTracks()
|
||||
// }
|
||||
|
||||
// AsyncFunction("setSubtitleURL") { view: VlcPlayerView, url: String, name: String ->
|
||||
// view.setSubtitleURL(url, name)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package expo.modules.vlcplayer
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
import org.videolan.libvlc.LibVLC
|
||||
import org.videolan.libvlc.Media
|
||||
import org.videolan.libvlc.MediaPlayer
|
||||
import org.videolan.libvlc.util.VLCVideoLayout
|
||||
|
||||
// Needs to inhert from MediaPlayer.EventListener
|
||||
class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context, appContext), LifecycleObserver, MediaPlayer.EventListener {
|
||||
|
||||
private var libVLC: LibVLC? = null
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
private lateinit var videoLayout: VLCVideoLayout
|
||||
private var isPaused: Boolean = false
|
||||
private var isMediaReady: Boolean = false
|
||||
private var lastReportedState: Int? = null
|
||||
private var lastReportedIsPlaying: Boolean? = null
|
||||
private var startPosition: Int? = null
|
||||
|
||||
init {
|
||||
setupView()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
setBackgroundColor(android.graphics.Color.WHITE)
|
||||
videoLayout = VLCVideoLayout(context).apply {
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
}
|
||||
addView(videoLayout)
|
||||
}
|
||||
|
||||
fun setSource(source: Map<String, Any>) {
|
||||
val mediaOptions = source["mediaOptions"] as? Map<String, Any> ?: emptyMap()
|
||||
val initOptions = source["initOptions"] as? List<String> ?: emptyList()
|
||||
val uri = source["uri"] as? String
|
||||
val autoplay = source["autoplay"] as? Boolean ?: false
|
||||
val isNetwork = source["isNetwork"] as? Boolean ?: false
|
||||
startPosition = source["startPosition"] as? Int ?: 0
|
||||
|
||||
// Handle video load start event
|
||||
// onVideoLoadStart?.invoke(mapOf("target" to reactTag ?: "null"))
|
||||
|
||||
libVLC = LibVLC(context, initOptions)
|
||||
mediaPlayer = MediaPlayer(libVLC)
|
||||
mediaPlayer?.attachViews(videoLayout, null, false, false)
|
||||
mediaPlayer?.setEventListener(this)
|
||||
|
||||
Log.d("VlcPlayerView", "Loading network file: $uri")
|
||||
val media = Media(libVLC, Uri.parse(uri))
|
||||
mediaPlayer?.media = media
|
||||
|
||||
|
||||
Log.d("VlcPlayerView", "Debug: Media options: $mediaOptions")
|
||||
// media.addOptions(mediaOptions)
|
||||
|
||||
// Apply subtitle options
|
||||
// val subtitleTrackIndex = source["subtitleTrackIndex"] as? Int ?: -1
|
||||
// Log.d("VlcPlayerView", "Debug: Subtitle track index from source: $subtitleTrackIndex")
|
||||
|
||||
// if (subtitleTrackIndex >= -1) {
|
||||
// setSubtitleTrack(subtitleTrackIndex)
|
||||
// Log.d("VlcPlayerView", "Debug: Set subtitle track to index: $subtitleTrackIndex")
|
||||
// } else {
|
||||
// Log.d("VlcPlayerView", "Debug: Subtitle track index is less than -1, not setting")
|
||||
// }
|
||||
|
||||
|
||||
if (autoplay) {
|
||||
Log.d("VlcPlayerView", "Playing...")
|
||||
play()
|
||||
// if (startPosition > 0) {
|
||||
// Log.d("VlcPlayerView", "Debug: Starting at position: $startPosition")
|
||||
// seekTo(startPosition)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
fun play() {
|
||||
mediaPlayer?.play()
|
||||
isPaused = false
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
mediaPlayer?.pause()
|
||||
isPaused = true
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
mediaPlayer?.stop()
|
||||
}
|
||||
|
||||
fun seekTo(time: Int) {
|
||||
mediaPlayer?.let { player ->
|
||||
val wasPlaying = player.isPlaying
|
||||
if (wasPlaying) {
|
||||
player.pause()
|
||||
}
|
||||
|
||||
val duration = player.length.toInt()
|
||||
val seekTime = if (time > duration) duration - 1000 else time
|
||||
player.time = seekTime.toLong()
|
||||
|
||||
if (wasPlaying) {
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fun setAudioTrack(trackIndex: Int) {
|
||||
// mediaPlayer?.setAudioTrack(trackIndex)
|
||||
// }
|
||||
|
||||
// fun getAudioTracks(): List<Map<String, Any>>? {
|
||||
// val trackNames = mediaPlayer?.audioTrackNames ?: return null
|
||||
// val trackIndexes = mediaPlayer?.audioTracks ?: return null
|
||||
|
||||
// return trackNames.zip(trackIndexes).map { (name, index) ->
|
||||
// mapOf("name" to name, "index" to index)
|
||||
// }
|
||||
// }
|
||||
|
||||
// fun setSubtitleTrack(trackIndex: Int) {
|
||||
// mediaPlayer?.setSpuTrack(trackIndex)
|
||||
// }
|
||||
|
||||
// fun getSubtitleTracks(): List<Map<String, Any>>? {
|
||||
// val trackNames = mediaPlayer?.spuTrackNames ?: return null
|
||||
// val trackIndexes = mediaPlayer?.spuTracks ?: return null
|
||||
|
||||
// return trackNames.zip(trackIndexes).map { (name, index) ->
|
||||
// mapOf("name" to name, "index" to index)
|
||||
// }
|
||||
// }
|
||||
|
||||
// fun setSubtitleURL(subtitleURL: String, name: String) {
|
||||
// val media = mediaPlayer?.media ?: return
|
||||
// media.addSlave(Media.Slave(Media.Slave.Type.Subtitle, subtitleURL, true))
|
||||
// }
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
mediaPlayer?.release()
|
||||
libVLC?.release()
|
||||
}
|
||||
|
||||
override fun onEvent(event: MediaPlayer.Event) {
|
||||
when (event.type) {
|
||||
MediaPlayer.Event.Playing,
|
||||
MediaPlayer.Event.Paused,
|
||||
MediaPlayer.Event.Stopped,
|
||||
MediaPlayer.Event.Buffering,
|
||||
MediaPlayer.Event.EndReached,
|
||||
MediaPlayer.Event.EncounteredError -> updatePlayerState(event)
|
||||
MediaPlayer.Event.TimeChanged -> updateVideoProgress()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePlayerState(event: MediaPlayer.Event) {
|
||||
val player = mediaPlayer ?: return
|
||||
val currentState = event.type
|
||||
|
||||
val stateInfo = mutableMapOf<String, Any>(
|
||||
"target" to "null", // Replace with actual target if needed
|
||||
"currentTime" to player.time.toInt(),
|
||||
"duration" to (player.media?.duration?.toInt() ?: 0),
|
||||
"error" to false
|
||||
)
|
||||
|
||||
when (currentState) {
|
||||
MediaPlayer.Event.Playing -> {
|
||||
stateInfo["isPlaying"] = true
|
||||
stateInfo["isBuffering"] = false
|
||||
stateInfo["state"] = "Playing"
|
||||
}
|
||||
MediaPlayer.Event.Paused -> {
|
||||
stateInfo["isPlaying"] = false
|
||||
stateInfo["state"] = "Paused"
|
||||
}
|
||||
MediaPlayer.Event.Buffering -> {
|
||||
stateInfo["isBuffering"] = true
|
||||
stateInfo["state"] = "Buffering"
|
||||
}
|
||||
MediaPlayer.Event.EncounteredError -> {
|
||||
Log.e("VlcPlayerView", "player.state ~ error")
|
||||
stateInfo["state"] = "Error"
|
||||
onVideoLoadEnd?.invoke(stateInfo)
|
||||
}
|
||||
MediaPlayer.Event.Opening -> {
|
||||
Log.d("VlcPlayerView", "player.state ~ opening")
|
||||
stateInfo["state"] = "Opening"
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if the media has finished loading
|
||||
if (player.isPlaying && !isMediaReady) {
|
||||
isMediaReady = true
|
||||
onVideoLoadEnd?.invoke(stateInfo)
|
||||
seekToStartTime()
|
||||
}
|
||||
|
||||
if (lastReportedState != currentState || lastReportedIsPlaying != player.isPlaying) {
|
||||
lastReportedState = currentState
|
||||
lastReportedIsPlaying = player.isPlaying
|
||||
onVideoStateChange?.invoke(stateInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun seekToStartTime() {
|
||||
val player = mediaPlayer ?: return
|
||||
val startPosition = startPosition ?: return
|
||||
|
||||
if (startPosition > 0) {
|
||||
Log.d("VlcPlayerView", "Debug: Seeking to start position: $startPosition")
|
||||
player.time = startPosition.toLong()
|
||||
|
||||
// Ensure the player continues playing after seeking
|
||||
if (!player.isPlaying) {
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateVideoProgress() {
|
||||
val player = mediaPlayer ?: return
|
||||
|
||||
val currentTimeMs = player.time.toInt()
|
||||
val durationMs = player.media?.duration?.toInt() ?: 0
|
||||
|
||||
println("currentTimeMs: $currentTimeMs")
|
||||
if (currentTimeMs >= 0 && currentTimeMs < durationMs) {
|
||||
onVideoProgress?.invoke(
|
||||
mapOf(
|
||||
"currentTime" to currentTimeMs,
|
||||
"duration" to durationMs
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var onVideoLoadEnd: ((Map<String, Any>) -> Unit)? = null
|
||||
var onVideoStateChange: ((Map<String, Any>) -> Unit)? = null
|
||||
var onVideoProgress: ((Map<String, Any>) -> Unit)? = null
|
||||
}
|
||||
Reference in New Issue
Block a user