mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-18 11:50:26 +01:00
Merge pull request #261 from Alexk2309/fix/refactor-vlc-media-player
Refactored perfomance change for IOS
This commit is contained in:
@@ -52,6 +52,7 @@ import DropdownViewDirect from "./dropdown/DropdownViewDirect";
|
|||||||
import DropdownViewTranscoding from "./dropdown/DropdownViewTranscoding";
|
import DropdownViewTranscoding from "./dropdown/DropdownViewTranscoding";
|
||||||
import BrightnessSlider from "./BrightnessSlider";
|
import BrightnessSlider from "./BrightnessSlider";
|
||||||
import SkipButton from "./SkipButton";
|
import SkipButton from "./SkipButton";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -245,13 +246,25 @@ export const Controls: React.FC<Props> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
prefetchAllTrickplayImages();
|
prefetchAllTrickplayImages();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleControls = () => setShowControls(!showControls);
|
const toggleControls = () => setShowControls(!showControls);
|
||||||
|
|
||||||
|
const handleSliderStart = useCallback(() => {
|
||||||
|
if (showControls === false) return;
|
||||||
|
|
||||||
|
setIsSliding(true);
|
||||||
|
wasPlayingRef.current = isPlaying;
|
||||||
|
lastProgressRef.current = progress.value;
|
||||||
|
|
||||||
|
pause();
|
||||||
|
isSeeking.value = true;
|
||||||
|
}, [showControls, isPlaying]);
|
||||||
|
|
||||||
|
const [isSliding, setIsSliding] = useState(false);
|
||||||
const handleSliderComplete = useCallback(
|
const handleSliderComplete = useCallback(
|
||||||
async (value: number) => {
|
async (value: number) => {
|
||||||
isSeeking.value = false;
|
isSeeking.value = false;
|
||||||
progress.value = value;
|
progress.value = value;
|
||||||
|
setIsSliding(false);
|
||||||
|
|
||||||
await seek(
|
await seek(
|
||||||
Math.max(0, Math.floor(isVlc ? value : ticksToSeconds(value)))
|
Math.max(0, Math.floor(isVlc ? value : ticksToSeconds(value)))
|
||||||
@@ -262,27 +275,20 @@ export const Controls: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 0 });
|
const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 0 });
|
||||||
|
const handleSliderChange = useCallback(
|
||||||
|
debounce((value: number) => {
|
||||||
|
const progressInTicks = msToTicks(value);
|
||||||
|
console.log("Progress in ticks", progressInTicks);
|
||||||
|
calculateTrickplayUrl(progressInTicks);
|
||||||
|
|
||||||
const handleSliderChange = (value: number) => {
|
const progressInSeconds = Math.floor(ticksToSeconds(progressInTicks));
|
||||||
const progressInTicks = isVlc ? msToTicks(value) : value;
|
const hours = Math.floor(progressInSeconds / 3600);
|
||||||
calculateTrickplayUrl(progressInTicks);
|
const minutes = Math.floor((progressInSeconds % 3600) / 60);
|
||||||
|
const seconds = progressInSeconds % 60;
|
||||||
const progressInSeconds = Math.floor(ticksToSeconds(progressInTicks));
|
setTime({ hours, minutes, seconds });
|
||||||
const hours = Math.floor(progressInSeconds / 3600);
|
}, 20), // 100ms debounce delay
|
||||||
const minutes = Math.floor((progressInSeconds % 3600) / 60);
|
[]
|
||||||
const seconds = progressInSeconds % 60;
|
);
|
||||||
setTime({ hours, minutes, seconds });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSliderStart = useCallback(() => {
|
|
||||||
if (showControls === false) return;
|
|
||||||
|
|
||||||
wasPlayingRef.current = isPlaying;
|
|
||||||
lastProgressRef.current = progress.value;
|
|
||||||
|
|
||||||
pause();
|
|
||||||
isSeeking.value = true;
|
|
||||||
}, [showControls, isPlaying]);
|
|
||||||
|
|
||||||
const handleSkipBackward = useCallback(async () => {
|
const handleSkipBackward = useCallback(async () => {
|
||||||
if (!settings?.rewindSkipTime) return;
|
if (!settings?.rewindSkipTime) return;
|
||||||
@@ -326,6 +332,71 @@ export const Controls: React.FC<Props> = ({
|
|||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const memoizedRenderBubble = useCallback(() => {
|
||||||
|
if (!trickPlayUrl || !trickplayInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { x, y, url } = trickPlayUrl;
|
||||||
|
const tileWidth = 150;
|
||||||
|
const tileHeight = 150 / trickplayInfo.aspectRatio!;
|
||||||
|
|
||||||
|
console.log("time, ", time);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: -57,
|
||||||
|
bottom: 15,
|
||||||
|
paddingTop: 30,
|
||||||
|
paddingBottom: 5,
|
||||||
|
width: tileWidth * 1.5,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: tileWidth,
|
||||||
|
height: tileHeight,
|
||||||
|
alignSelf: "center",
|
||||||
|
transform: [{ scale: 1.4 }],
|
||||||
|
borderRadius: 5,
|
||||||
|
}}
|
||||||
|
className="bg-neutral-800 overflow-hidden"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
cachePolicy={"memory-disk"}
|
||||||
|
style={{
|
||||||
|
width: 150 * trickplayInfo?.data.TileWidth!,
|
||||||
|
height:
|
||||||
|
(150 / trickplayInfo.aspectRatio!) *
|
||||||
|
trickplayInfo?.data.TileHeight!,
|
||||||
|
transform: [
|
||||||
|
{ translateX: -x * tileWidth },
|
||||||
|
{ translateY: -y * tileHeight },
|
||||||
|
],
|
||||||
|
resizeMode: "cover",
|
||||||
|
}}
|
||||||
|
source={{ uri: url }}
|
||||||
|
contentFit="cover"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
marginTop: 30,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`${time.hours > 0 ? `${time.hours}:` : ""}${
|
||||||
|
time.minutes < 10 ? `0${time.minutes}` : time.minutes
|
||||||
|
}:${time.seconds < 10 ? `0${time.seconds}` : time.seconds}`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}, [trickPlayUrl, trickplayInfo, time]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlProvider
|
<ControlProvider
|
||||||
item={item}
|
item={item}
|
||||||
@@ -594,69 +665,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
containerStyle={{
|
containerStyle={{
|
||||||
borderRadius: 100,
|
borderRadius: 100,
|
||||||
}}
|
}}
|
||||||
renderBubble={() => {
|
renderBubble={() => isSliding && memoizedRenderBubble()}
|
||||||
if (!trickPlayUrl || !trickplayInfo) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { x, y, url } = trickPlayUrl;
|
|
||||||
const tileWidth = 150;
|
|
||||||
const tileHeight = 150 / trickplayInfo.aspectRatio!;
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: -57,
|
|
||||||
bottom: 15,
|
|
||||||
paddingTop: 30,
|
|
||||||
paddingBottom: 5,
|
|
||||||
width: tileWidth * 1.5, // Adjust the width of the outer container if needed
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.6)", // Outer box background color (optional)
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: tileWidth,
|
|
||||||
height: tileHeight,
|
|
||||||
alignSelf: "center",
|
|
||||||
transform: [{ scale: 1.4 }],
|
|
||||||
borderRadius: 5, // Optional border radius
|
|
||||||
}}
|
|
||||||
className=" bg-neutral-800 overflow-hidden"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
cachePolicy={"memory-disk"}
|
|
||||||
style={{
|
|
||||||
width: 150 * trickplayInfo?.data.TileWidth!,
|
|
||||||
height:
|
|
||||||
(150 / trickplayInfo.aspectRatio!) *
|
|
||||||
trickplayInfo?.data.TileHeight!,
|
|
||||||
transform: [
|
|
||||||
{ translateX: -x * tileWidth },
|
|
||||||
{ translateY: -y * tileHeight },
|
|
||||||
],
|
|
||||||
resizeMode: "cover",
|
|
||||||
}}
|
|
||||||
source={{ uri: url }}
|
|
||||||
contentFit="cover"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
marginTop: 30,
|
|
||||||
fontSize: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`${time.hours > 0 ? `${time.hours}:` : ""}${
|
|
||||||
time.minutes < 10 ? `0${time.minutes}` : time.minutes
|
|
||||||
}:${
|
|
||||||
time.seconds < 10 ? `0${time.seconds}` : time.seconds
|
|
||||||
}`}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
sliderHeight={10}
|
sliderHeight={10}
|
||||||
thumbWidth={0}
|
thumbWidth={0}
|
||||||
progress={progress}
|
progress={progress}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ public class VlcPlayerModule: Module {
|
|||||||
Name("VlcPlayer")
|
Name("VlcPlayer")
|
||||||
View(VlcPlayerView.self) {
|
View(VlcPlayerView.self) {
|
||||||
Prop("source") { (view: VlcPlayerView, source: [String: Any]) in
|
Prop("source") { (view: VlcPlayerView, source: [String: Any]) in
|
||||||
if !view.hasSource {
|
view.setSource(source)
|
||||||
view.setSource(source)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Prop("paused") { (view: VlcPlayerView, paused: Bool) in
|
Prop("paused") { (view: VlcPlayerView, paused: Bool) in
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class VlcPlayerView: ExpoView {
|
|||||||
private var externalTrack: [String: String]?
|
private var externalTrack: [String: String]?
|
||||||
private var progressTimer: DispatchSourceTimer?
|
private var progressTimer: DispatchSourceTimer?
|
||||||
private var isStopping: Bool = false // Define isStopping here
|
private var isStopping: Bool = false // Define isStopping here
|
||||||
|
private var lastProgressCall = Date().timeIntervalSince1970
|
||||||
var hasSource = false
|
var hasSource = false
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
@@ -24,7 +25,6 @@ class VlcPlayerView: ExpoView {
|
|||||||
super.init(appContext: appContext)
|
super.init(appContext: appContext)
|
||||||
setupView()
|
setupView()
|
||||||
setupNotifications()
|
setupNotifications()
|
||||||
setupProgressTimer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
@@ -56,64 +56,49 @@ class VlcPlayerView: ExpoView {
|
|||||||
name: UIApplication.didBecomeActiveNotification, object: nil)
|
name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupProgressTimer() {
|
|
||||||
progressTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
|
|
||||||
progressTimer?.schedule(deadline: .now(), repeating: progressUpdateInterval)
|
|
||||||
progressTimer?.setEventHandler { [weak self] in
|
|
||||||
self?.updateVideoProgress()
|
|
||||||
}
|
|
||||||
progressTimer?.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Public Methods
|
// MARK: - Public Methods
|
||||||
|
|
||||||
@objc func play() {
|
@objc func play() {
|
||||||
DispatchQueue.main.async { [weak self] in
|
self.mediaPlayer?.play()
|
||||||
self?.mediaPlayer?.play()
|
self.isPaused = false
|
||||||
self?.isPaused = false
|
print("Play")
|
||||||
print("Play")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func pause() {
|
@objc func pause() {
|
||||||
DispatchQueue.main.async { [weak self] in
|
self.mediaPlayer?.pause()
|
||||||
self?.mediaPlayer?.pause()
|
self.isPaused = true
|
||||||
self?.isPaused = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func seekTo(_ time: Int32) {
|
@objc func seekTo(_ time: Int32) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
guard let player = self.mediaPlayer else { return }
|
||||||
guard let self = self, 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)
|
||||||
|
|
||||||
let wasPlaying = player.isPlaying
|
|
||||||
if wasPlaying {
|
if wasPlaying {
|
||||||
player.pause()
|
self.play()
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Wait for a short moment to ensure the seek has been processed
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
||||||
if wasPlaying {
|
|
||||||
player.play()
|
|
||||||
}
|
|
||||||
self.updatePlayerState()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print("Error: Unable to retrieve video duration")
|
|
||||||
}
|
}
|
||||||
|
self.updatePlayerState()
|
||||||
|
} else {
|
||||||
|
print("Error: Unable to retrieve video duration")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func setSource(_ source: [String: Any]) {
|
@objc func setSource(_ source: [String: Any]) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
if self.hasSource {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let mediaOptions = source["mediaOptions"] as? [String: Any] ?? [:]
|
let mediaOptions = source["mediaOptions"] as? [String: Any] ?? [:]
|
||||||
self.externalTrack = source["externalTrack"] as? [String: String]
|
self.externalTrack = source["externalTrack"] as? [String: String]
|
||||||
@@ -152,13 +137,8 @@ class VlcPlayerView: ExpoView {
|
|||||||
print("Debug: Media options: \(mediaOptions)")
|
print("Debug: Media options: \(mediaOptions)")
|
||||||
media.addOptions(mediaOptions)
|
media.addOptions(mediaOptions)
|
||||||
|
|
||||||
// Apply subtitle options
|
|
||||||
let subtitleTrackIndex = source["subtitleTrackIndex"] as? Int ?? -1
|
|
||||||
print("Debug: Subtitle track index from source: \(subtitleTrackIndex)")
|
|
||||||
self.setSubtitleTrack(subtitleTrackIndex)
|
|
||||||
|
|
||||||
self.mediaPlayer?.media = media
|
self.mediaPlayer?.media = media
|
||||||
hasSource = true
|
self.hasSource = true
|
||||||
|
|
||||||
if autoplay {
|
if autoplay {
|
||||||
print("Playing...")
|
print("Playing...")
|
||||||
@@ -168,9 +148,7 @@ class VlcPlayerView: ExpoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func setAudioTrack(_ trackIndex: Int) {
|
@objc func setAudioTrack(_ trackIndex: Int) {
|
||||||
DispatchQueue.main.async {
|
self.mediaPlayer?.currentAudioTrackIndex = Int32(trackIndex)
|
||||||
self.mediaPlayer?.currentAudioTrackIndex = Int32(trackIndex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func getAudioTracks() -> [[String: Any]]? {
|
@objc func getAudioTracks() -> [[String: Any]]? {
|
||||||
@@ -187,29 +165,25 @@ class VlcPlayerView: ExpoView {
|
|||||||
|
|
||||||
@objc func setSubtitleTrack(_ trackIndex: Int) {
|
@objc func setSubtitleTrack(_ trackIndex: Int) {
|
||||||
print("Debug: Attempting to set subtitle track to index: \(trackIndex)")
|
print("Debug: Attempting to set subtitle track to index: \(trackIndex)")
|
||||||
DispatchQueue.main.async {
|
self.mediaPlayer?.currentVideoSubTitleIndex = Int32(trackIndex)
|
||||||
self.mediaPlayer?.currentVideoSubTitleIndex = Int32(trackIndex)
|
print(
|
||||||
print(
|
"Debug: Current subtitle track index after setting: \(self.mediaPlayer?.currentVideoSubTitleIndex ?? -1)"
|
||||||
"Debug: Current subtitle track index after setting: \(self.mediaPlayer?.currentVideoSubTitleIndex ?? -1)"
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func setSubtitleURL(_ subtitleURL: String, name: String) {
|
@objc func setSubtitleURL(_ subtitleURL: String, name: String) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
guard let url = URL(string: subtitleURL) else {
|
||||||
guard let self = self, let url = URL(string: subtitleURL) else {
|
print("Error: Invalid subtitle URL")
|
||||||
print("Error: Invalid subtitle URL")
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: true)
|
let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: true)
|
||||||
if let result = result {
|
if let result = result {
|
||||||
let internalName = "Track \(self.customSubtitles.count + 1)"
|
let internalName = "Track \(self.customSubtitles.count + 1)"
|
||||||
print("Subtitle added with result: \(result) \(internalName)")
|
print("Subtitle added with result: \(result) \(internalName)")
|
||||||
self.customSubtitles.append((internalName: internalName, originalName: name))
|
self.customSubtitles.append((internalName: internalName, originalName: name))
|
||||||
} else {
|
} else {
|
||||||
print("Failed to add subtitle")
|
print("Failed to add subtitle")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +288,7 @@ class VlcPlayerView: ExpoView {
|
|||||||
let currentTimeMs = player.time.intValue
|
let currentTimeMs = player.time.intValue
|
||||||
let durationMs = player.media?.length.intValue ?? 0
|
let durationMs = player.media?.length.intValue ?? 0
|
||||||
|
|
||||||
|
print("Debug: Current time: \(currentTimeMs)")
|
||||||
if currentTimeMs >= 0 && currentTimeMs < durationMs {
|
if currentTimeMs >= 0 && currentTimeMs < durationMs {
|
||||||
if player.isPlaying && !self.isMediaReady {
|
if player.isPlaying && !self.isMediaReady {
|
||||||
self.isMediaReady = true
|
self.isMediaReady = true
|
||||||
@@ -345,11 +320,19 @@ class VlcPlayerView: ExpoView {
|
|||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
performStop()
|
performStop()
|
||||||
progressTimer?.cancel()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension VlcPlayerView: VLCMediaPlayerDelegate {
|
extension VlcPlayerView: VLCMediaPlayerDelegate {
|
||||||
|
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
||||||
|
// self?.updateVideoProgress()
|
||||||
|
let timeNow = Date().timeIntervalSince1970
|
||||||
|
if timeNow - lastProgressCall >= 1 {
|
||||||
|
lastProgressCall = timeNow
|
||||||
|
updateVideoProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mediaPlayerStateChanged(_ aNotification: Notification) {
|
func mediaPlayerStateChanged(_ aNotification: Notification) {
|
||||||
self.updatePlayerState()
|
self.updatePlayerState()
|
||||||
}
|
}
|
||||||
@@ -395,10 +378,6 @@ extension VlcPlayerView: VLCMediaPlayerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
|
||||||
// No need to call updateVideoProgress here as it's handled by the timer
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension VlcPlayerView: VLCMediaDelegate {
|
extension VlcPlayerView: VLCMediaDelegate {
|
||||||
|
|||||||
Reference in New Issue
Block a user