mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-05 13:38:27 +01:00
Merge
This commit is contained in:
@@ -516,7 +516,7 @@ export default function page() {
|
|||||||
return () => setIsMounted(false);
|
return () => setIsMounted(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (itemStatus.isLoading || streamStatus.isLoading) {
|
if (itemStatus.isLoading || streamStatus.isLoading || !item || !stream) {
|
||||||
return (
|
return (
|
||||||
<View className='w-screen h-screen flex flex-col items-center justify-center bg-black'>
|
<View className='w-screen h-screen flex flex-col items-center justify-center bg-black'>
|
||||||
<Loader />
|
<Loader />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import type { TrackInfo } from "@/modules/VlcPlayer.types";
|
import type { TrackInfo } from "@/modules/VlcPlayer.types";
|
||||||
import { useSettings, VideoPlayer } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import type { Track } from "../types";
|
import type { Track } from "../types";
|
||||||
import { useControlContext } from "./ControlContext";
|
import { useControlContext } from "./ControlContext";
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [audioTracks, setAudioTracks] = useState<Track[] | null>(null);
|
const [audioTracks, setAudioTracks] = useState<Track[] | null>(null);
|
||||||
const [subtitleTracks, setSubtitleTracks] = useState<Track[] | null>(null);
|
const [subtitleTracks, setSubtitleTracks] = useState<Track[] | null>(null);
|
||||||
const [settings] = useSettings();
|
const [_settings] = useSettings();
|
||||||
|
|
||||||
const ControlContext = useControlContext();
|
const ControlContext = useControlContext();
|
||||||
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
const isVideoLoaded = ControlContext?.isVideoLoaded;
|
||||||
@@ -136,7 +136,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Step 2: Apply VLC indexing logic
|
// Step 2: Apply VLC indexing logic
|
||||||
let textSubIndex = settings.defaultPlayer === VideoPlayer.VLC_4 ? 0 : 1;
|
let textSubIndex = 0;
|
||||||
const processedSubs: Track[] = sortedSubs?.map((sub) => {
|
const processedSubs: Track[] = sortedSubs?.map((sub) => {
|
||||||
// Always increment for non-transcoding subtitles
|
// Always increment for non-transcoding subtitles
|
||||||
// Only increment for text-based subtitles when transcoding
|
// Only increment for text-based subtitles when transcoding
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { requireNativeViewManager } from "expo-modules-core";
|
import { requireNativeViewManager } from "expo-modules-core";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { ViewStyle } from "react-native";
|
||||||
import { VideoPlayer, useSettings } from "@/utils/atoms/settings";
|
|
||||||
import { Platform, ViewStyle } from "react-native";
|
|
||||||
import type {
|
import type {
|
||||||
VlcPlayerSource,
|
VlcPlayerSource,
|
||||||
VlcPlayerViewProps,
|
VlcPlayerViewProps,
|
||||||
@@ -14,22 +12,10 @@ interface NativeViewRef extends VlcPlayerViewRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VLCViewManager = 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) => {
|
(props, ref) => <VLCViewManager {...props} ref={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>(
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"platforms": ["ios", "tvos"],
|
|
||||||
"ios": {
|
|
||||||
"modules": ["VlcPlayer3Module"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,392 +0,0 @@
|
|||||||
import ExpoModulesCore
|
|
||||||
|
|
||||||
#if os(tvOS)
|
|
||||||
import TVVLCKit
|
|
||||||
#else
|
|
||||||
import MobileVLCKit
|
|
||||||
#endif
|
|
||||||
|
|
||||||
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 externalSubtitles: [[String: String]]?
|
|
||||||
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
|
|
||||||
self.externalSubtitles = source["externalSubtitles"] as? [[String: String]]
|
|
||||||
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.setInitialExternalSubtitles()
|
|
||||||
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: false)
|
|
||||||
if let result = result {
|
|
||||||
let internalName = "Track \(self.customSubtitles.count)"
|
|
||||||
print("Subtitle added with result: \(result) \(internalName)")
|
|
||||||
self.customSubtitles.append((internalName: internalName, originalName: name))
|
|
||||||
} else {
|
|
||||||
print("Failed to add subtitle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setInitialExternalSubtitles() {
|
|
||||||
if let externalSubtitles = self.externalSubtitles {
|
|
||||||
for subtitle in externalSubtitles {
|
|
||||||
if let subtitleName = subtitle["name"],
|
|
||||||
let subtitleURL = subtitle["DeliveryUrl"]
|
|
||||||
{
|
|
||||||
print("Setting external subtitle: \(subtitleName) \(subtitleURL)")
|
|
||||||
self.setSubtitleURL(subtitleURL, name: subtitleName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 {
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
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");
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'VlcPlayer'
|
s.name = 'VlcPlayer'
|
||||||
s.version = '4.0.0a10'
|
s.version = '4.0.0a13'
|
||||||
s.summary = 'A sample project summary'
|
s.summary = 'A sample project summary'
|
||||||
s.description = 'A sample project description'
|
s.description = 'A sample project description'
|
||||||
s.author = ''
|
s.author = ''
|
||||||
|
|||||||
@@ -68,4 +68,4 @@ public class VlcPlayerModule: Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ extension VLCPlayerWrapper: VLCPictureInPictureMediaControlling {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isMediaSeekable() -> Bool {
|
func isMediaSeekable() -> Bool {
|
||||||
return player.isSeekable
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMediaPlaying() -> Bool {
|
func isMediaPlaying() -> Bool {
|
||||||
@@ -118,6 +118,7 @@ extension VLCPlayerWrapper: VLCMediaPlayerDelegate {
|
|||||||
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
self.performInitialSeek()
|
||||||
let timeNow = Date().timeIntervalSince1970
|
let timeNow = Date().timeIntervalSince1970
|
||||||
if timeNow - self.lastProgressCall >= 1 {
|
if timeNow - self.lastProgressCall >= 1 {
|
||||||
self.lastProgressCall = timeNow
|
self.lastProgressCall = timeNow
|
||||||
@@ -135,11 +136,22 @@ extension VLCPlayerWrapper: VLCMediaPlayerDelegate {
|
|||||||
pipController.invalidatePlaybackState()
|
pipController.invalidatePlaybackState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - VLCMediaDelegate
|
// Workaround: When playing an HLS video for the first time, seeking to a specific time immediately can cause a crash.
|
||||||
extension VLCPlayerWrapper: VLCMediaDelegate {
|
// To avoid this, we wait until the video has started playing before performing the initial seek.
|
||||||
// Implement VLCMediaDelegate methods if needed
|
func performInitialSeek() {
|
||||||
|
guard let vlcPlayerView = self.playerView.superview as? VlcPlayerView,
|
||||||
|
!vlcPlayerView.initialSeekPerformed,
|
||||||
|
vlcPlayerView.startPosition > 0,
|
||||||
|
vlcPlayerView.isTranscoding,
|
||||||
|
player.isSeekable else { return }
|
||||||
|
vlcPlayerView.initialSeekPerformed = true
|
||||||
|
// Use a logger from the VlcPlayerView if available, or create a new one
|
||||||
|
let logger = (vlcPlayerView).logger
|
||||||
|
logger.debug("First time update, performing initial seek to \(vlcPlayerView.startPosition) seconds")
|
||||||
|
player.time = VLCTime(int: vlcPlayerView.startPosition * 1000)
|
||||||
|
self.updateVideoProgress?()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VlcPlayerView: ExpoView {
|
class VlcPlayerView: ExpoView {
|
||||||
@@ -149,11 +161,13 @@ class VlcPlayerView: ExpoView {
|
|||||||
private var progressUpdateInterval: TimeInterval = 1.0 // Update interval set to 1 second
|
private var progressUpdateInterval: TimeInterval = 1.0 // Update interval set to 1 second
|
||||||
private var isPaused: Bool = false
|
private var isPaused: Bool = false
|
||||||
private var customSubtitles: [(internalName: String, originalName: String)] = []
|
private var customSubtitles: [(internalName: String, originalName: String)] = []
|
||||||
private var startPosition: Int32 = 0
|
var startPosition: Int32 = 0
|
||||||
private var externalTrack: [String: String]?
|
private var externalTrack: [String: String]?
|
||||||
private var isStopping: Bool = false // Define isStopping here
|
private var isStopping: Bool = false // Define isStopping here
|
||||||
private var externalSubtitles: [[String: String]]?
|
private var externalSubtitles: [[String: String]]?
|
||||||
var hasSource = false
|
var hasSource = false
|
||||||
|
var initialSeekPerformed = false
|
||||||
|
var isTranscoding: Bool = false
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
required init(appContext: AppContext? = nil) {
|
required init(appContext: AppContext? = nil) {
|
||||||
@@ -251,6 +265,9 @@ class VlcPlayerView: ExpoView {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if uri.contains("m3u8") {
|
||||||
|
self.isTranscoding = true
|
||||||
|
}
|
||||||
let autoplay = source["autoplay"] as? Bool ?? false
|
let autoplay = source["autoplay"] as? Bool ?? false
|
||||||
let isNetwork = source["isNetwork"] as? Bool ?? false
|
let isNetwork = source["isNetwork"] as? Bool ?? false
|
||||||
|
|
||||||
@@ -277,8 +294,11 @@ class VlcPlayerView: ExpoView {
|
|||||||
self.hasSource = true
|
self.hasSource = true
|
||||||
if autoplay {
|
if autoplay {
|
||||||
logger.info("Playing...")
|
logger.info("Playing...")
|
||||||
|
// The Video is not transcoding so it its safe to seek to the start position.
|
||||||
|
if !self.isTranscoding {
|
||||||
|
self.vlc.player.time = VLCTime(number: NSNumber(value: self.startPosition * 1000))
|
||||||
|
}
|
||||||
self.play()
|
self.play()
|
||||||
self.vlc.player.time = VLCTime(number: NSNumber(value: self.startPosition * 1000))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
import { BITRATES, type Bitrate } from "@/components/BitrateSelector";
|
|
||||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { Video } from "@/utils/jellyseerr/server/models/Movie";
|
|
||||||
import { writeInfoLog } from "@/utils/log";
|
|
||||||
import {
|
import {
|
||||||
type BaseItemKind,
|
type BaseItemKind,
|
||||||
type CultureDto,
|
type CultureDto,
|
||||||
@@ -14,9 +9,13 @@ import {
|
|||||||
import { atom, useAtom, useAtomValue } from "jotai";
|
import { atom, useAtom, useAtomValue } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
import { BITRATES, type Bitrate } from "@/components/BitrateSelector";
|
||||||
|
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||||
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import { writeInfoLog } from "@/utils/log";
|
||||||
import { storage } from "../mmkv";
|
import { storage } from "../mmkv";
|
||||||
|
|
||||||
const STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004";
|
const _STREAMYFIN_PLUGIN_ID = "1e9e5d386e6746158719e98a5c34f004";
|
||||||
const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS";
|
const STREAMYFIN_PLUGIN_SETTINGS = "STREAMYFIN_PLUGIN_SETTINGS";
|
||||||
|
|
||||||
export type DownloadQuality = "original" | "high" | "low";
|
export type DownloadQuality = "original" | "high" | "low";
|
||||||
@@ -129,7 +128,6 @@ export type HomeSectionLatestResolver = {
|
|||||||
|
|
||||||
export enum VideoPlayer {
|
export enum VideoPlayer {
|
||||||
// NATIVE, //todo: changes will make this a lot more easier to implement if we want. delete if not wanted
|
// NATIVE, //todo: changes will make this a lot more easier to implement if we want. delete if not wanted
|
||||||
VLC_3 = 0,
|
|
||||||
VLC_4 = 1,
|
VLC_4 = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +286,7 @@ export const useSettings = () => {
|
|||||||
writeInfoLog("Got plugin settings", data?.settings);
|
writeInfoLog("Got plugin settings", data?.settings);
|
||||||
return data?.settings;
|
return data?.settings;
|
||||||
},
|
},
|
||||||
(err) => undefined,
|
(_err) => undefined,
|
||||||
);
|
);
|
||||||
setPluginSettings(settings);
|
setPluginSettings(settings);
|
||||||
return settings;
|
return settings;
|
||||||
|
|||||||
Reference in New Issue
Block a user