From b45a7b926ed91872eec3facda1d7ff29a730a4e0 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 29 Dec 2025 14:52:48 +0100 Subject: [PATCH 1/6] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 2 +- .../NCViewerMedia/NCPlayer/NCPlayer.swift | 25 +++++++++- .../NCPlayer/NCPlayerToolBar.swift | 46 ++++++++++++++----- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 8dd4c5545f..91f5407108 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -1554,7 +1554,7 @@ F79B645F26CA661600838ACA /* UIControl+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+Extension.swift"; sourceTree = ""; }; F79B869A265E19D40085C0E0 /* NSMutableAttributedString+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMutableAttributedString+Extension.swift"; sourceTree = ""; }; F79EDA9F26B004980007D134 /* NCPlayerToolBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCPlayerToolBar.swift; sourceTree = ""; }; - F79EDAA126B004980007D134 /* NCPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCPlayer.swift; sourceTree = ""; }; + F79EDAA126B004980007D134 /* NCPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 5; lastKnownFileType = sourcecode.swift; path = NCPlayer.swift; sourceTree = ""; }; F79FFB252A97C24A0055EEA4 /* NCNetworkingE2EEMarkFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EEMarkFolder.swift; sourceTree = ""; }; F7A03E2E2D425A14007AA677 /* NCFavoriteNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFavoriteNavigationController.swift; sourceTree = ""; }; F7A03E322D426115007AA677 /* NCMoreNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMoreNavigationController.swift; sourceTree = ""; }; diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift index bead951ef1..da9da3fcc8 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift @@ -7,7 +7,7 @@ import NextcloudKit import UIKit import MobileVLCKit -class NCPlayer: NSObject { +class NCPlayer: NSObject, VLCMediaDelegate { internal var url: URL? internal var player = VLCMediaPlayer() internal var dialogProvider: VLCDialogProvider? @@ -66,7 +66,13 @@ class NCPlayer: NSObject { self.singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didSingleTapWith(gestureRecognizer:))) print("Play URL: \(url)") - player.media = VLCMedia(url: url) + let media = VLCMedia(url: url) + media.delegate = self // <--- IMPORTANT: Listen for parsing events + + // 3. Explicitly ask VLC to parse network headers immediately + media.parse(options: .fetchNetwork) + + player.media = media player.delegate = self dialogProvider = VLCDialogProvider(library: VLCLibrary.shared(), customUI: true) @@ -198,6 +204,21 @@ class NCPlayer: NSObject { } } +extension NCPlayer { + // Called when VLC finishes parsing media metadata (audio or video) + func mediaDidFinishParsing(_ media: VLCMedia) { + // This fires when VLC finally knows the length (duration) + if media.length.intValue > 0 { + print("VLC Metadata Loaded: \(media.length)") + + DispatchQueue.main.async { + // Now that we have length, force the toolbar to update + self.playerToolBar?.update() + } + } + } +} + extension NCPlayer: VLCMediaPlayerDelegate { func mediaPlayerStateChanged(_ aNotification: Notification) { diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift index 5b138cd7fe..5597e94531 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift @@ -131,7 +131,7 @@ class NCPlayerToolBar: UIView { playbackSlider.value = position - labelCurrentTime.text = "--:--" + labelCurrentTime.text = "00:00" labelLeftTime.text = "--:--" if viewerMediaScreenMode == .normal { @@ -144,19 +144,41 @@ class NCPlayerToolBar: UIView { } public func update() { - guard let ncplayer = self.ncplayer, let length = ncplayer.player.media?.length.intValue else { return } - let position = ncplayer.player.position - let positionInSecond = position * Float(length / 1000) + guard let ncplayer = self.ncplayer, + let media = ncplayer.player.media else { + // If length is 0, we can't do math, so we leave it as --:-- + return + } + + let length = media.length.intValue + + // Get current position (0.0 to 1.0) + let position = ncplayer.player.position + + // Calculate seconds based on percentage * total length + // We do this manually because ncplayer.player.time might be 0 before playback starts + let currentSeconds = Double(position) * (Double(length) / 1000.0) + + // Convert to VLCTime for easy string formatting + let currentTimeObj = VLCTime(int: Int32(currentSeconds * 1000)) + let remainingTimeObj = VLCTime(int: Int32((Double(length) / 1000.0) - currentSeconds) * 1000) + +// // SLIDER & TIME +// if playbackSliderEvent == .ended { +// playbackSlider.value = position +// } + + // Update Labels + labelCurrentTime.text = currentTimeObj.stringValue + + // Logic for remaining time (usually typically shown as negative) + let remaining = remainingTimeObj.stringValue + labelLeftTime.text = "-\(remaining)" - // SLIDER & TIME - if playbackSliderEvent == .ended { - playbackSlider.value = position - } - labelCurrentTime.text = ncplayer.player.time.stringValue - labelLeftTime.text = ncplayer.player.remainingTime?.stringValue - MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyPlaybackDuration] = length / 1000 - MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = positionInSecond + // Update Control Center + MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyPlaybackDuration] = length / 1000 + MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentSeconds } public func updateTopToolBar(videoSubTitlesIndexes: [Any], audioTrackIndexes: [Any]) { From 848ad1b5dec37ebfc61fd0ed33e02df2caf5e25e Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 29 Dec 2025 18:28:38 +0100 Subject: [PATCH 2/6] WIP Signed-off-by: Milen Pivchev --- .../NCViewerMedia/NCPlayer/NCPlayer.swift | 654 +++++++++--------- .../NCPlayer/NCPlayerToolBar.swift | 56 +- 2 files changed, 350 insertions(+), 360 deletions(-) diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift index da9da3fcc8..f41e7e49d4 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift @@ -8,348 +8,342 @@ import UIKit import MobileVLCKit class NCPlayer: NSObject, VLCMediaDelegate { - internal var url: URL? - internal var player = VLCMediaPlayer() - internal var dialogProvider: VLCDialogProvider? - internal var metadata: tableMetadata - internal var singleTapGestureRecognizer: UITapGestureRecognizer? - internal var activityIndicator: UIActivityIndicatorView - internal let database = NCManageDatabase.shared - internal var width: Int? - internal var height: Int? - internal var length: Int? - internal var pauseAfterPlay: Bool = false - - internal weak var playerToolBar: NCPlayerToolBar? - internal weak var viewerMediaPage: NCViewerMediaPage? - - weak var imageVideoContainer: UIImageView? - - internal var counterSeconds: Double = 0 - - // MARK: - View Life Cycle - - init(imageVideoContainer: UIImageView, playerToolBar: NCPlayerToolBar?, metadata: tableMetadata, viewerMediaPage: NCViewerMediaPage?) { - self.imageVideoContainer = imageVideoContainer - self.playerToolBar = playerToolBar - self.metadata = metadata - self.viewerMediaPage = viewerMediaPage - - self.activityIndicator = UIActivityIndicatorView(style: .large) - self.activityIndicator.color = .white - self.activityIndicator.hidesWhenStopped = true - self.activityIndicator.translatesAutoresizingMaskIntoConstraints = false - - if let viewerMediaPage = viewerMediaPage { - viewerMediaPage.view.addSubview(activityIndicator) - NSLayoutConstraint.activate([ - activityIndicator.centerXAnchor.constraint(equalTo: viewerMediaPage.view.centerXAnchor), - activityIndicator.centerYAnchor.constraint(equalTo: viewerMediaPage.view.centerYAnchor) - ]) - } - - super.init() - } - - deinit { - player.stop() - print("deinit NCPlayer with ocId \(metadata.ocId)") - NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) - } - - func openAVPlayer(url: URL, autoplay: Bool = false) { - var position: Float = 0 - let userAgent = userAgent - - self.url = url - self.singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didSingleTapWith(gestureRecognizer:))) - - print("Play URL: \(url)") - let media = VLCMedia(url: url) - media.delegate = self // <--- IMPORTANT: Listen for parsing events - - // 3. Explicitly ask VLC to parse network headers immediately - media.parse(options: .fetchNetwork) - - player.media = media - player.delegate = self - - dialogProvider = VLCDialogProvider(library: VLCLibrary.shared(), customUI: true) - dialogProvider?.customRenderer = self - - // player?.media?.addOption("--network-caching=500") - player.media?.addOption(":http-user-agent=\(userAgent)") - - if let result = self.database.getVideo(metadata: metadata), - let resultPosition = result.position { - position = resultPosition - } - - if metadata.isVideo { - player.drawable = imageVideoContainer - if let view = player.drawable as? UIView, let singleTapGestureRecognizer = singleTapGestureRecognizer { - view.isUserInteractionEnabled = true - view.addGestureRecognizer(singleTapGestureRecognizer) - } - } - - player.play() - player.position = position - - if autoplay { - pauseAfterPlay = false - } else { - pauseAfterPlay = true - } - - playerToolBar?.setBarPlayer(position: position, ncplayer: self, metadata: metadata, viewerMediaPage: viewerMediaPage) - - NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil) - } - - func restartAVPlayer(position: Float, pauseAfterPlay: Bool) { - if let url = self.url, !player.isPlaying { - - player.media = VLCMedia(url: url) - player.position = position - playerToolBar?.setBarPlayer(position: position) - viewerMediaPage?.changeScreenMode(mode: .normal) - self.pauseAfterPlay = pauseAfterPlay - player.play() - - if metadata.isVideo { - if position == 0 { - imageVideoContainer?.image = NCUtility().getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024, userId: metadata.userId, urlBase: metadata.urlBase) - } else { - imageVideoContainer?.image = nil - } - } - } - } - - // MARK: - UIGestureRecognizerDelegate - - @objc func didSingleTapWith(gestureRecognizer: UITapGestureRecognizer) { - changeScreenMode() - } - - func changeScreenMode() { - guard let viewerMediaPage = viewerMediaPage else { return } - - if viewerMediaScreenMode == .full { - viewerMediaPage.changeScreenMode(mode: .normal) - } else { - viewerMediaPage.changeScreenMode(mode: .full) - } - } - - // MARK: - NotificationCenter - - @objc func applicationDidEnterBackground(_ notification: NSNotification) { - if metadata.isVideo { - playerPause() - } - } - - // MARK: - - - func isPlaying() -> Bool { - return player.isPlaying - } - - func playerPlay() { - playerToolBar?.playbackSliderEvent = .began - - if let result = self.database.getVideo(metadata: metadata), let position = result.position { - player.position = position - playerToolBar?.playbackSliderEvent = .moved - } - - player.play() - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - self.playerToolBar?.playbackSliderEvent = .ended - } - } - - @objc func playerStop() { - savePosition() - player.stop() - } - - @objc func playerPause() { - savePosition() - player.pause() - } - - func playerPosition(_ position: Float) { - self.database.addVideo(metadata: metadata, position: position) - player.position = position - } - - func savePosition() { - guard metadata.isVideo, isPlaying() else { return } - self.database.addVideo(metadata: metadata, position: player.position) - } - - func jumpForward(_ seconds: Int32) { - player.play() - player.jumpForward(seconds) - } - - func jumpBackward(_ seconds: Int32) { - player.play() - player.jumpBackward(seconds) - } + internal var url: URL? + internal var player = VLCMediaPlayer() + internal var dialogProvider: VLCDialogProvider? + internal var metadata: tableMetadata + internal var singleTapGestureRecognizer: UITapGestureRecognizer? + internal var activityIndicator: UIActivityIndicatorView + internal let database = NCManageDatabase.shared + internal var width: Int? + internal var height: Int? + internal var length: Int? + internal var pauseAfterPlay: Bool = false + + internal weak var playerToolBar: NCPlayerToolBar? + internal weak var viewerMediaPage: NCViewerMediaPage? + + weak var imageVideoContainer: UIImageView? + + internal var counterSeconds: Double = 0 + + // MARK: - View Life Cycle + + init(imageVideoContainer: UIImageView, playerToolBar: NCPlayerToolBar?, metadata: tableMetadata, viewerMediaPage: NCViewerMediaPage?) { + self.imageVideoContainer = imageVideoContainer + self.playerToolBar = playerToolBar + self.metadata = metadata + self.viewerMediaPage = viewerMediaPage + + self.activityIndicator = UIActivityIndicatorView(style: .large) + self.activityIndicator.color = .white + self.activityIndicator.hidesWhenStopped = true + self.activityIndicator.translatesAutoresizingMaskIntoConstraints = false + + if let viewerMediaPage = viewerMediaPage { + viewerMediaPage.view.addSubview(activityIndicator) + NSLayoutConstraint.activate([ + activityIndicator.centerXAnchor.constraint(equalTo: viewerMediaPage.view.centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: viewerMediaPage.view.centerYAnchor) + ]) + } + + super.init() + } + + deinit { + player.stop() + print("deinit NCPlayer with ocId \(metadata.ocId)") + NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) + } + + func openAVPlayer(url: URL, autoplay: Bool = false) { + var position: Float = 0 + let userAgent = userAgent + + self.url = url + self.singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didSingleTapWith(gestureRecognizer:))) + + print("Playing URL: \(url)") + let media = VLCMedia(url: url) + media.delegate = self + + media.parse(options: .fetchNetwork) + + player.media = media + player.delegate = self + + dialogProvider = VLCDialogProvider(library: VLCLibrary.shared(), customUI: true) + dialogProvider?.customRenderer = self + + player.media?.addOption(":http-user-agent=\(userAgent)") + + if let result = self.database.getVideo(metadata: metadata), + let resultPosition = result.position { + position = resultPosition + } + + if metadata.isVideo { + player.drawable = imageVideoContainer + if let view = player.drawable as? UIView, let singleTapGestureRecognizer = singleTapGestureRecognizer { + view.isUserInteractionEnabled = true + view.addGestureRecognizer(singleTapGestureRecognizer) + } + } + + player.play() + player.position = position + + if autoplay { + pauseAfterPlay = false + } else { + pauseAfterPlay = true + } + + playerToolBar?.setBarPlayer(position: position, ncplayer: self, metadata: metadata, viewerMediaPage: viewerMediaPage) + + NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + func restartAVPlayer(position: Float, pauseAfterPlay: Bool) { + if let url = self.url, !player.isPlaying { + + player.media = VLCMedia(url: url) + player.position = position + playerToolBar?.setBarPlayer(position: position) + viewerMediaPage?.changeScreenMode(mode: .normal) + self.pauseAfterPlay = pauseAfterPlay + player.play() + + if metadata.isVideo { + if position == 0 { + imageVideoContainer?.image = NCUtility().getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024, userId: metadata.userId, urlBase: metadata.urlBase) + } else { + imageVideoContainer?.image = nil + } + } + } + } + + // MARK: - UIGestureRecognizerDelegate + + @objc func didSingleTapWith(gestureRecognizer: UITapGestureRecognizer) { + changeScreenMode() + } + + func changeScreenMode() { + guard let viewerMediaPage = viewerMediaPage else { return } + + if viewerMediaScreenMode == .full { + viewerMediaPage.changeScreenMode(mode: .normal) + } else { + viewerMediaPage.changeScreenMode(mode: .full) + } + } + + // MARK: - NotificationCenter + + @objc func applicationDidEnterBackground(_ notification: NSNotification) { + if metadata.isVideo { + playerPause() + } + } + + // MARK: - + + func isPlaying() -> Bool { + return player.isPlaying + } + + func playerPlay() { + playerToolBar?.playbackSliderEvent = .began + + if let result = self.database.getVideo(metadata: metadata), let position = result.position { + player.position = position + playerToolBar?.playbackSliderEvent = .moved + } + + player.play() + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.playerToolBar?.playbackSliderEvent = .ended + } + } + + @objc func playerStop() { + savePosition() + player.stop() + } + + @objc func playerPause() { + savePosition() + player.pause() + } + + func playerPosition(_ position: Float) { + self.database.addVideo(metadata: metadata, position: position) + player.position = position + } + + func savePosition() { + guard metadata.isVideo, isPlaying() else { return } + self.database.addVideo(metadata: metadata, position: player.position) + } + + func jumpForward(_ seconds: Int32) { + player.play() + player.jumpForward(seconds) + } + + func jumpBackward(_ seconds: Int32) { + player.play() + player.jumpBackward(seconds) + } } extension NCPlayer { // Called when VLC finishes parsing media metadata (audio or video) func mediaDidFinishParsing(_ media: VLCMedia) { - // This fires when VLC finally knows the length (duration) - if media.length.intValue > 0 { - print("VLC Metadata Loaded: \(media.length)") - - DispatchQueue.main.async { - // Now that we have length, force the toolbar to update - self.playerToolBar?.update() - } - } - } + DispatchQueue.main.async { + // Now that we have length, force the toolbar to update + self.playerToolBar?.update() + } + // } + } } extension NCPlayer: VLCMediaPlayerDelegate { - func mediaPlayerStateChanged(_ aNotification: Notification) { - - if player.state == .buffering && player.isPlaying { - activityIndicator.startAnimating() - } else { - activityIndicator.stopAnimating() - } - - switch player.state { - case .stopped: - playerToolBar?.playButtonPlay() - - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) - - print("Played mode: STOPPED") - case .opening: - print("Played mode: OPENING") - case .buffering: - print("Played mode: BUFFERING") - case .ended: - self.database.addVideo(metadata: self.metadata, position: 0) - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - if let playRepeat = self.playerToolBar?.playRepeat { - self.restartAVPlayer(position: 0, pauseAfterPlay: !playRepeat) - } - } - playerToolBar?.playButtonPlay() - print("Played mode: ENDED") - case .error: - print("Played mode: ERROR") - case .playing: - guard let playerToolBar = playerToolBar else { return } - if playerToolBar.playerButtonView.isHidden { - playerToolBar.playerButtonView.isHidden = false - viewerMediaPage?.changeScreenMode(mode: .normal) - } - if pauseAfterPlay { - player.pause() - pauseAfterPlay = false - self.viewerMediaPage?.updateCommandCenter(ncplayer: self, title: metadata.fileNameView) - } else { - playerToolBar.playButtonPause() - // Set track audio/subtitle - let data = self.database.getVideo(metadata: metadata) - if let currentAudioTrackIndex = data?.currentAudioTrackIndex { - player.currentAudioTrackIndex = Int32(currentAudioTrackIndex) - } - if let currentVideoSubTitleIndex = data?.currentVideoSubTitleIndex { - player.currentVideoSubTitleIndex = Int32(currentVideoSubTitleIndex) - } - } - let size = player.videoSize - if let mediaLength = player.media?.length.intValue { - self.length = Int(mediaLength) - } - self.width = Int(size.width) - self.height = Int(size.height) - playerToolBar.updateTopToolBar(videoSubTitlesIndexes: player.videoSubTitlesIndexes, audioTrackIndexes: player.audioTrackIndexes) - self.database.addVideo(metadata: metadata, width: self.width, height: self.height, length: self.length) - - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerIsPlaying) - - print("Played mode: PLAYING") - case .paused: - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) - - playerToolBar?.playButtonPlay() - print("Played mode: PAUSED") - default: break - } - } - - func mediaPlayerTimeChanged(_ aNotification: Notification) { - activityIndicator.stopAnimating() - playerToolBar?.update() - } + func mediaPlayerStateChanged(_ aNotification: Notification) { + + if player.state == .buffering && player.isPlaying { + activityIndicator.startAnimating() + } else { + activityIndicator.stopAnimating() + } + + switch player.state { + case .stopped: + playerToolBar?.playButtonPlay() + + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) + + print("Played mode: STOPPED") + case .opening: + print("Played mode: OPENING") + case .buffering: + print("Played mode: BUFFERING") + case .ended: + self.database.addVideo(metadata: self.metadata, position: 0) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + if let playRepeat = self.playerToolBar?.playRepeat { + self.restartAVPlayer(position: 0, pauseAfterPlay: !playRepeat) + } + } + playerToolBar?.playButtonPlay() + print("Played mode: ENDED") + case .error: + print("Played mode: ERROR") + case .playing: + guard let playerToolBar = playerToolBar else { return } + if playerToolBar.playerButtonView.isHidden { + playerToolBar.playerButtonView.isHidden = false + viewerMediaPage?.changeScreenMode(mode: .normal) + } + if pauseAfterPlay { + player.pause() + pauseAfterPlay = false + self.viewerMediaPage?.updateCommandCenter(ncplayer: self, title: metadata.fileNameView) + } else { + playerToolBar.playButtonPause() + // Set track audio/subtitle + let data = self.database.getVideo(metadata: metadata) + if let currentAudioTrackIndex = data?.currentAudioTrackIndex { + player.currentAudioTrackIndex = Int32(currentAudioTrackIndex) + } + if let currentVideoSubTitleIndex = data?.currentVideoSubTitleIndex { + player.currentVideoSubTitleIndex = Int32(currentVideoSubTitleIndex) + } + } + let size = player.videoSize + if let mediaLength = player.media?.length.intValue { + self.length = Int(mediaLength) + } + self.width = Int(size.width) + self.height = Int(size.height) + playerToolBar.updateTopToolBar(videoSubTitlesIndexes: player.videoSubTitlesIndexes, audioTrackIndexes: player.audioTrackIndexes) + self.database.addVideo(metadata: metadata, width: self.width, height: self.height, length: self.length) + + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerIsPlaying) + + print("Played mode: PLAYING") + case .paused: + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) + + playerToolBar?.playButtonPlay() + print("Played mode: PAUSED") + default: break + } + } + + func mediaPlayerTimeChanged(_ aNotification: Notification) { + activityIndicator.stopAnimating() + playerToolBar?.update() + } } extension NCPlayer: VLCMediaThumbnailerDelegate { - func mediaThumbnailerDidTimeOut(_ mediaThumbnailer: VLCMediaThumbnailer) { } - func mediaThumbnailer(_ mediaThumbnailer: VLCMediaThumbnailer, didFinishThumbnail thumbnail: CGImage) { } + func mediaThumbnailerDidTimeOut(_ mediaThumbnailer: VLCMediaThumbnailer) { } + func mediaThumbnailer(_ mediaThumbnailer: VLCMediaThumbnailer, didFinishThumbnail thumbnail: CGImage) { } } extension NCPlayer: VLCCustomDialogRendererProtocol { - func showError(withTitle error: String, message: String) { - let alert = UIAlertController(title: error, message: message, preferredStyle: .alert) - - alert.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in - self.playerToolBar?.removeFromSuperview() - self.viewerMediaPage?.navigationController?.popViewController(animated: true) - })) - - self.viewerMediaPage?.present(alert, animated: true) - } - - func showLogin(withTitle title: String, message: String, defaultUsername username: String?, askingForStorage: Bool, withReference reference: NSValue) { - // UIAlertController other states... - } - - func showQuestion(withTitle title: String, message: String, type questionType: VLCDialogQuestionType, cancel cancelString: String?, action1String: String?, action2String: String?, withReference reference: NSValue) { - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - - if let action1String = action1String { - alert.addAction(UIAlertAction(title: action1String, style: .default, handler: { _ in - self.dialogProvider?.postAction(1, forDialogReference: reference) - })) - } - if let action2String = action2String { - alert.addAction(UIAlertAction(title: action2String, style: .default, handler: { _ in - self.dialogProvider?.postAction(2, forDialogReference: reference) - })) - } - if let cancelString = cancelString { - alert.addAction(UIAlertAction(title: cancelString, style: .cancel, handler: { _ in - self.dialogProvider?.postAction(3, forDialogReference: reference) - })) - } - - self.viewerMediaPage?.present(alert, animated: true) - } - - func showProgress(withTitle title: String, message: String, isIndeterminate: Bool, position: Float, cancel cancelString: String?, withReference reference: NSValue) { - // UIAlertController other states... - } - - func updateProgress(withReference reference: NSValue, message: String?, position: Float) { - // UIAlertController other states... - } - - func cancelDialog(withReference reference: NSValue) { - // UIAlertController other states... - } + func showError(withTitle error: String, message: String) { + let alert = UIAlertController(title: error, message: message, preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in + self.playerToolBar?.removeFromSuperview() + self.viewerMediaPage?.navigationController?.popViewController(animated: true) + })) + + self.viewerMediaPage?.present(alert, animated: true) + } + + func showLogin(withTitle title: String, message: String, defaultUsername username: String?, askingForStorage: Bool, withReference reference: NSValue) { + // UIAlertController other states... + } + + func showQuestion(withTitle title: String, message: String, type questionType: VLCDialogQuestionType, cancel cancelString: String?, action1String: String?, action2String: String?, withReference reference: NSValue) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + + if let action1String = action1String { + alert.addAction(UIAlertAction(title: action1String, style: .default, handler: { _ in + self.dialogProvider?.postAction(1, forDialogReference: reference) + })) + } + if let action2String = action2String { + alert.addAction(UIAlertAction(title: action2String, style: .default, handler: { _ in + self.dialogProvider?.postAction(2, forDialogReference: reference) + })) + } + if let cancelString = cancelString { + alert.addAction(UIAlertAction(title: cancelString, style: .cancel, handler: { _ in + self.dialogProvider?.postAction(3, forDialogReference: reference) + })) + } + + self.viewerMediaPage?.present(alert, animated: true) + } + + func showProgress(withTitle title: String, message: String, isIndeterminate: Bool, position: Float, cancel cancelString: String?, withReference reference: NSValue) { + // UIAlertController other states... + } + + func updateProgress(withReference reference: NSValue, message: String?, position: Float) { + // UIAlertController other states... + } + + func cancelDialog(withReference reference: NSValue) { + // UIAlertController other states... + } } diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift index 5597e94531..2e315cb819 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift @@ -131,7 +131,7 @@ class NCPlayerToolBar: UIView { playbackSlider.value = position - labelCurrentTime.text = "00:00" + labelCurrentTime.text = "--:--" labelLeftTime.text = "--:--" if viewerMediaScreenMode == .normal { @@ -145,40 +145,36 @@ class NCPlayerToolBar: UIView { public func update() { guard let ncplayer = self.ncplayer, - let media = ncplayer.player.media else { - // If length is 0, we can't do math, so we leave it as --:-- - return - } - - let length = media.length.intValue - - // Get current position (0.0 to 1.0) - let position = ncplayer.player.position + let media = ncplayer.player.media else { + return + } - // Calculate seconds based on percentage * total length - // We do this manually because ncplayer.player.time might be 0 before playback starts - let currentSeconds = Double(position) * (Double(length) / 1000.0) + let length: Int32 - // Convert to VLCTime for easy string formatting - let currentTimeObj = VLCTime(int: Int32(currentSeconds * 1000)) - let remainingTimeObj = VLCTime(int: Int32((Double(length) / 1000.0) - currentSeconds) * 1000) + if let result = self.database.getVideo(metadata: metadata), let resultLength = result.length { + length = Int32(resultLength) + } else { + length = media.length.intValue + } -// // SLIDER & TIME -// if playbackSliderEvent == .ended { -// playbackSlider.value = position -// } + let position = ncplayer.player.position - // Update Labels - labelCurrentTime.text = currentTimeObj.stringValue + let currentSeconds = Double(position) * (Double(length) / 1000.0) - // Logic for remaining time (usually typically shown as negative) - let remaining = remainingTimeObj.stringValue - labelLeftTime.text = "-\(remaining)" + let currentTimeObj = VLCTime(int: Int32(currentSeconds * 1000)) + let remainingTimeObj = VLCTime(int: Int32((Double(length) / 1000.0) - currentSeconds) * 1000) + labelCurrentTime.text = currentTimeObj.stringValue == "--:--" ? "00:00" : currentTimeObj.stringValue - // Update Control Center - MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyPlaybackDuration] = length / 1000 - MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentSeconds + let remaining = remainingTimeObj.stringValue + labelLeftTime.text = "-\(remaining)" + + if playbackSliderEvent == .ended { + playbackSlider.value = position + } + + MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyPlaybackDuration] = length / 1000 + MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentSeconds } public func updateTopToolBar(videoSubTitlesIndexes: [Any], audioTrackIndexes: [Any]) { @@ -429,7 +425,7 @@ extension NCPlayerToolBar { guard let metadata = self.metadata else { return } let storyboard = UIStoryboard(name: "NCSelect", bundle: nil) if let navigationController = storyboard.instantiateInitialViewController() as? UINavigationController, - let viewController = navigationController.topViewController as? NCSelect { + let viewController = navigationController.topViewController as? NCSelect { viewController.delegate = self viewController.typeOfCommandView = .nothing @@ -512,7 +508,7 @@ extension NCPlayerToolBar: NCSelectDelegate { // swiftlint:disable inclusive_language func addPlaybackSlave(type: String, metadata: tableMetadata) { - // swiftlint:enable inclusive_language + // swiftlint:enable inclusive_language let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, fileName: metadata.fileNameView, userId: metadata.userId, urlBase: metadata.urlBase) if type == "subtitle" { From bd15aaa9196724b3a60bfb5146db1867ad84c987 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 30 Dec 2025 12:42:54 +0100 Subject: [PATCH 3/6] WIOP Signed-off-by: Milen Pivchev --- .../NCViewerMedia/NCPlayer/NCPlayer.swift | 645 +++++++++--------- .../NCPlayer/NCPlayerToolBar.swift | 14 +- 2 files changed, 321 insertions(+), 338 deletions(-) diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift index f41e7e49d4..64c702e745 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift @@ -8,342 +8,331 @@ import UIKit import MobileVLCKit class NCPlayer: NSObject, VLCMediaDelegate { - internal var url: URL? - internal var player = VLCMediaPlayer() - internal var dialogProvider: VLCDialogProvider? - internal var metadata: tableMetadata - internal var singleTapGestureRecognizer: UITapGestureRecognizer? - internal var activityIndicator: UIActivityIndicatorView - internal let database = NCManageDatabase.shared - internal var width: Int? - internal var height: Int? - internal var length: Int? - internal var pauseAfterPlay: Bool = false - - internal weak var playerToolBar: NCPlayerToolBar? - internal weak var viewerMediaPage: NCViewerMediaPage? - - weak var imageVideoContainer: UIImageView? - - internal var counterSeconds: Double = 0 - - // MARK: - View Life Cycle - - init(imageVideoContainer: UIImageView, playerToolBar: NCPlayerToolBar?, metadata: tableMetadata, viewerMediaPage: NCViewerMediaPage?) { - self.imageVideoContainer = imageVideoContainer - self.playerToolBar = playerToolBar - self.metadata = metadata - self.viewerMediaPage = viewerMediaPage - - self.activityIndicator = UIActivityIndicatorView(style: .large) - self.activityIndicator.color = .white - self.activityIndicator.hidesWhenStopped = true - self.activityIndicator.translatesAutoresizingMaskIntoConstraints = false - - if let viewerMediaPage = viewerMediaPage { - viewerMediaPage.view.addSubview(activityIndicator) - NSLayoutConstraint.activate([ - activityIndicator.centerXAnchor.constraint(equalTo: viewerMediaPage.view.centerXAnchor), - activityIndicator.centerYAnchor.constraint(equalTo: viewerMediaPage.view.centerYAnchor) - ]) - } - - super.init() - } - - deinit { - player.stop() - print("deinit NCPlayer with ocId \(metadata.ocId)") - NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) - } - - func openAVPlayer(url: URL, autoplay: Bool = false) { - var position: Float = 0 - let userAgent = userAgent - - self.url = url - self.singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didSingleTapWith(gestureRecognizer:))) - - print("Playing URL: \(url)") - let media = VLCMedia(url: url) - media.delegate = self - - media.parse(options: .fetchNetwork) - - player.media = media - player.delegate = self - - dialogProvider = VLCDialogProvider(library: VLCLibrary.shared(), customUI: true) - dialogProvider?.customRenderer = self - - player.media?.addOption(":http-user-agent=\(userAgent)") - - if let result = self.database.getVideo(metadata: metadata), - let resultPosition = result.position { - position = resultPosition - } - - if metadata.isVideo { - player.drawable = imageVideoContainer - if let view = player.drawable as? UIView, let singleTapGestureRecognizer = singleTapGestureRecognizer { - view.isUserInteractionEnabled = true - view.addGestureRecognizer(singleTapGestureRecognizer) - } - } - - player.play() - player.position = position - - if autoplay { - pauseAfterPlay = false - } else { - pauseAfterPlay = true - } - - playerToolBar?.setBarPlayer(position: position, ncplayer: self, metadata: metadata, viewerMediaPage: viewerMediaPage) - - NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil) - } - - func restartAVPlayer(position: Float, pauseAfterPlay: Bool) { - if let url = self.url, !player.isPlaying { - - player.media = VLCMedia(url: url) - player.position = position - playerToolBar?.setBarPlayer(position: position) - viewerMediaPage?.changeScreenMode(mode: .normal) - self.pauseAfterPlay = pauseAfterPlay - player.play() - - if metadata.isVideo { - if position == 0 { - imageVideoContainer?.image = NCUtility().getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024, userId: metadata.userId, urlBase: metadata.urlBase) - } else { - imageVideoContainer?.image = nil - } - } - } - } - - // MARK: - UIGestureRecognizerDelegate - - @objc func didSingleTapWith(gestureRecognizer: UITapGestureRecognizer) { - changeScreenMode() - } - - func changeScreenMode() { - guard let viewerMediaPage = viewerMediaPage else { return } - - if viewerMediaScreenMode == .full { - viewerMediaPage.changeScreenMode(mode: .normal) - } else { - viewerMediaPage.changeScreenMode(mode: .full) - } - } - - // MARK: - NotificationCenter - - @objc func applicationDidEnterBackground(_ notification: NSNotification) { - if metadata.isVideo { - playerPause() - } - } - - // MARK: - - - func isPlaying() -> Bool { - return player.isPlaying - } - - func playerPlay() { - playerToolBar?.playbackSliderEvent = .began - - if let result = self.database.getVideo(metadata: metadata), let position = result.position { - player.position = position - playerToolBar?.playbackSliderEvent = .moved - } - - player.play() - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - self.playerToolBar?.playbackSliderEvent = .ended - } - } - - @objc func playerStop() { - savePosition() - player.stop() - } - - @objc func playerPause() { - savePosition() - player.pause() - } - - func playerPosition(_ position: Float) { - self.database.addVideo(metadata: metadata, position: position) - player.position = position - } - - func savePosition() { - guard metadata.isVideo, isPlaying() else { return } - self.database.addVideo(metadata: metadata, position: player.position) - } - - func jumpForward(_ seconds: Int32) { - player.play() - player.jumpForward(seconds) - } - - func jumpBackward(_ seconds: Int32) { - player.play() - player.jumpBackward(seconds) - } -} + internal var url: URL? + internal var player = VLCMediaPlayer() + internal var dialogProvider: VLCDialogProvider? + internal var metadata: tableMetadata + internal var singleTapGestureRecognizer: UITapGestureRecognizer? + internal var activityIndicator: UIActivityIndicatorView + internal let database = NCManageDatabase.shared + internal var width: Int? + internal var height: Int? + internal var length: Int? + internal var pauseAfterPlay: Bool = false + + internal weak var playerToolBar: NCPlayerToolBar? + internal weak var viewerMediaPage: NCViewerMediaPage? + + weak var imageVideoContainer: UIImageView? + + internal var counterSeconds: Double = 0 + + // MARK: - View Life Cycle + + init(imageVideoContainer: UIImageView, playerToolBar: NCPlayerToolBar?, metadata: tableMetadata, viewerMediaPage: NCViewerMediaPage?) { + self.imageVideoContainer = imageVideoContainer + self.playerToolBar = playerToolBar + self.metadata = metadata + self.viewerMediaPage = viewerMediaPage + + self.activityIndicator = UIActivityIndicatorView(style: .large) + self.activityIndicator.color = .white + self.activityIndicator.hidesWhenStopped = true + self.activityIndicator.translatesAutoresizingMaskIntoConstraints = false + + if let viewerMediaPage = viewerMediaPage { + viewerMediaPage.view.addSubview(activityIndicator) + NSLayoutConstraint.activate([ + activityIndicator.centerXAnchor.constraint(equalTo: viewerMediaPage.view.centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: viewerMediaPage.view.centerYAnchor) + ]) + } + + super.init() + } + + deinit { + player.stop() + print("deinit NCPlayer with ocId \(metadata.ocId)") + NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) + } + + func openAVPlayer(url: URL, autoplay: Bool = false) { + var position: Float = 0 + let userAgent = userAgent + + self.url = url + self.singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didSingleTapWith(gestureRecognizer:))) + + print("Playing URL: \(url)") + let media = VLCMedia(url: url) + + media.parse(options: .fetchNetwork) + + player.media = media + player.delegate = self + + dialogProvider = VLCDialogProvider(library: VLCLibrary.shared(), customUI: true) + dialogProvider?.customRenderer = self + + player.media?.addOption(":http-user-agent=\(userAgent)") + + if let result = self.database.getVideo(metadata: metadata), + let resultPosition = result.position { + position = resultPosition + } + + if metadata.isVideo { + player.drawable = imageVideoContainer + if let view = player.drawable as? UIView, let singleTapGestureRecognizer = singleTapGestureRecognizer { + view.isUserInteractionEnabled = true + view.addGestureRecognizer(singleTapGestureRecognizer) + } + } + + player.play() + player.position = position + + if autoplay { + pauseAfterPlay = false + } else { + pauseAfterPlay = true + } + + playerToolBar?.setBarPlayer(position: position, ncplayer: self, metadata: metadata, viewerMediaPage: viewerMediaPage) + + NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + func restartAVPlayer(position: Float, pauseAfterPlay: Bool) { + if let url = self.url, !player.isPlaying { + + player.media = VLCMedia(url: url) + player.position = position + playerToolBar?.setBarPlayer(position: position) + viewerMediaPage?.changeScreenMode(mode: .normal) + self.pauseAfterPlay = pauseAfterPlay + player.play() + + if metadata.isVideo { + if position == 0 { + imageVideoContainer?.image = NCUtility().getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024, userId: metadata.userId, urlBase: metadata.urlBase) + } else { + imageVideoContainer?.image = nil + } + } + } + } + + // MARK: - UIGestureRecognizerDelegate + + @objc func didSingleTapWith(gestureRecognizer: UITapGestureRecognizer) { + changeScreenMode() + } + + func changeScreenMode() { + guard let viewerMediaPage = viewerMediaPage else { return } + + if viewerMediaScreenMode == .full { + viewerMediaPage.changeScreenMode(mode: .normal) + } else { + viewerMediaPage.changeScreenMode(mode: .full) + } + } -extension NCPlayer { - // Called when VLC finishes parsing media metadata (audio or video) - func mediaDidFinishParsing(_ media: VLCMedia) { - DispatchQueue.main.async { - // Now that we have length, force the toolbar to update - self.playerToolBar?.update() - } - // } - } + // MARK: - NotificationCenter + + @objc func applicationDidEnterBackground(_ notification: NSNotification) { + if metadata.isVideo { + playerPause() + } + } + + // MARK: - + + func isPlaying() -> Bool { + return player.isPlaying + } + + func playerPlay() { + playerToolBar?.playbackSliderEvent = .began + + if let result = self.database.getVideo(metadata: metadata), let position = result.position { + player.position = position + playerToolBar?.playbackSliderEvent = .moved + } + + player.play() + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.playerToolBar?.playbackSliderEvent = .ended + } + } + + @objc func playerStop() { + savePosition() + player.stop() + } + + @objc func playerPause() { + savePosition() + player.pause() + } + + func playerPosition(_ position: Float) { + self.database.addVideo(metadata: metadata, position: position) + player.position = position + } + + func savePosition() { + guard metadata.isVideo, isPlaying() else { return } + self.database.addVideo(metadata: metadata, position: player.position) + } + + func jumpForward(_ seconds: Int32) { + player.play() + player.jumpForward(seconds) + } + + func jumpBackward(_ seconds: Int32) { + player.play() + player.jumpBackward(seconds) + } } extension NCPlayer: VLCMediaPlayerDelegate { - func mediaPlayerStateChanged(_ aNotification: Notification) { - - if player.state == .buffering && player.isPlaying { - activityIndicator.startAnimating() - } else { - activityIndicator.stopAnimating() - } - - switch player.state { - case .stopped: - playerToolBar?.playButtonPlay() - - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) - - print("Played mode: STOPPED") - case .opening: - print("Played mode: OPENING") - case .buffering: - print("Played mode: BUFFERING") - case .ended: - self.database.addVideo(metadata: self.metadata, position: 0) - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - if let playRepeat = self.playerToolBar?.playRepeat { - self.restartAVPlayer(position: 0, pauseAfterPlay: !playRepeat) - } - } - playerToolBar?.playButtonPlay() - print("Played mode: ENDED") - case .error: - print("Played mode: ERROR") - case .playing: - guard let playerToolBar = playerToolBar else { return } - if playerToolBar.playerButtonView.isHidden { - playerToolBar.playerButtonView.isHidden = false - viewerMediaPage?.changeScreenMode(mode: .normal) - } - if pauseAfterPlay { - player.pause() - pauseAfterPlay = false - self.viewerMediaPage?.updateCommandCenter(ncplayer: self, title: metadata.fileNameView) - } else { - playerToolBar.playButtonPause() - // Set track audio/subtitle - let data = self.database.getVideo(metadata: metadata) - if let currentAudioTrackIndex = data?.currentAudioTrackIndex { - player.currentAudioTrackIndex = Int32(currentAudioTrackIndex) - } - if let currentVideoSubTitleIndex = data?.currentVideoSubTitleIndex { - player.currentVideoSubTitleIndex = Int32(currentVideoSubTitleIndex) - } - } - let size = player.videoSize - if let mediaLength = player.media?.length.intValue { - self.length = Int(mediaLength) - } - self.width = Int(size.width) - self.height = Int(size.height) - playerToolBar.updateTopToolBar(videoSubTitlesIndexes: player.videoSubTitlesIndexes, audioTrackIndexes: player.audioTrackIndexes) - self.database.addVideo(metadata: metadata, width: self.width, height: self.height, length: self.length) - - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerIsPlaying) - - print("Played mode: PLAYING") - case .paused: - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) - - playerToolBar?.playButtonPlay() - print("Played mode: PAUSED") - default: break - } - } - - func mediaPlayerTimeChanged(_ aNotification: Notification) { - activityIndicator.stopAnimating() - playerToolBar?.update() - } + func mediaPlayerStateChanged(_ aNotification: Notification) { + + if player.state == .buffering && player.isPlaying { + activityIndicator.startAnimating() + } else { + activityIndicator.stopAnimating() + } + + switch player.state { + case .stopped: + playerToolBar?.playButtonPlay() + + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) + + print("Played mode: STOPPED") + case .opening: + print("Played mode: OPENING") + case .buffering: + print("Played mode: BUFFERING") + case .ended: + self.database.addVideo(metadata: self.metadata, position: 0) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + if let playRepeat = self.playerToolBar?.playRepeat { + self.restartAVPlayer(position: 0, pauseAfterPlay: !playRepeat) + } + } + playerToolBar?.playButtonPlay() + print("Played mode: ENDED") + case .error: + print("Played mode: ERROR") + case .playing: + guard let playerToolBar = playerToolBar else { return } + if playerToolBar.playerButtonView.isHidden { + playerToolBar.playerButtonView.isHidden = false + viewerMediaPage?.changeScreenMode(mode: .normal) + } + if pauseAfterPlay { + player.pause() + pauseAfterPlay = false + self.viewerMediaPage?.updateCommandCenter(ncplayer: self, title: metadata.fileNameView) + } else { + playerToolBar.playButtonPause() + // Set track audio/subtitle + let data = self.database.getVideo(metadata: metadata) + if let currentAudioTrackIndex = data?.currentAudioTrackIndex { + player.currentAudioTrackIndex = Int32(currentAudioTrackIndex) + } + if let currentVideoSubTitleIndex = data?.currentVideoSubTitleIndex { + player.currentVideoSubTitleIndex = Int32(currentVideoSubTitleIndex) + } + } + let size = player.videoSize + if let mediaLength = player.media?.length.intValue { + self.length = Int(mediaLength) + } + self.width = Int(size.width) + self.height = Int(size.height) + playerToolBar.updateTopToolBar(videoSubTitlesIndexes: player.videoSubTitlesIndexes, audioTrackIndexes: player.audioTrackIndexes) + playerToolBar.updatePlaybackPosition() + self.database.addVideo(metadata: metadata, width: self.width, height: self.height, length: self.length) + + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerIsPlaying) + + print("Played mode: PLAYING") + case .paused: + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) + + playerToolBar?.playButtonPlay() + print("Played mode: PAUSED") + default: break + } + } + + func mediaPlayerTimeChanged(_ aNotification: Notification) { + activityIndicator.stopAnimating() + playerToolBar?.updatePlaybackPosition() + } } extension NCPlayer: VLCMediaThumbnailerDelegate { - func mediaThumbnailerDidTimeOut(_ mediaThumbnailer: VLCMediaThumbnailer) { } - func mediaThumbnailer(_ mediaThumbnailer: VLCMediaThumbnailer, didFinishThumbnail thumbnail: CGImage) { } + func mediaThumbnailerDidTimeOut(_ mediaThumbnailer: VLCMediaThumbnailer) { } + func mediaThumbnailer(_ mediaThumbnailer: VLCMediaThumbnailer, didFinishThumbnail thumbnail: CGImage) { } } extension NCPlayer: VLCCustomDialogRendererProtocol { - func showError(withTitle error: String, message: String) { - let alert = UIAlertController(title: error, message: message, preferredStyle: .alert) - - alert.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in - self.playerToolBar?.removeFromSuperview() - self.viewerMediaPage?.navigationController?.popViewController(animated: true) - })) - - self.viewerMediaPage?.present(alert, animated: true) - } - - func showLogin(withTitle title: String, message: String, defaultUsername username: String?, askingForStorage: Bool, withReference reference: NSValue) { - // UIAlertController other states... - } - - func showQuestion(withTitle title: String, message: String, type questionType: VLCDialogQuestionType, cancel cancelString: String?, action1String: String?, action2String: String?, withReference reference: NSValue) { - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - - if let action1String = action1String { - alert.addAction(UIAlertAction(title: action1String, style: .default, handler: { _ in - self.dialogProvider?.postAction(1, forDialogReference: reference) - })) - } - if let action2String = action2String { - alert.addAction(UIAlertAction(title: action2String, style: .default, handler: { _ in - self.dialogProvider?.postAction(2, forDialogReference: reference) - })) - } - if let cancelString = cancelString { - alert.addAction(UIAlertAction(title: cancelString, style: .cancel, handler: { _ in - self.dialogProvider?.postAction(3, forDialogReference: reference) - })) - } - - self.viewerMediaPage?.present(alert, animated: true) - } - - func showProgress(withTitle title: String, message: String, isIndeterminate: Bool, position: Float, cancel cancelString: String?, withReference reference: NSValue) { - // UIAlertController other states... - } - - func updateProgress(withReference reference: NSValue, message: String?, position: Float) { - // UIAlertController other states... - } - - func cancelDialog(withReference reference: NSValue) { - // UIAlertController other states... - } + func showError(withTitle error: String, message: String) { + let alert = UIAlertController(title: error, message: message, preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in + self.playerToolBar?.removeFromSuperview() + self.viewerMediaPage?.navigationController?.popViewController(animated: true) + })) + + self.viewerMediaPage?.present(alert, animated: true) + } + + func showLogin(withTitle title: String, message: String, defaultUsername username: String?, askingForStorage: Bool, withReference reference: NSValue) { + // UIAlertController other states... + } + + func showQuestion(withTitle title: String, message: String, type questionType: VLCDialogQuestionType, cancel cancelString: String?, action1String: String?, action2String: String?, withReference reference: NSValue) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + + if let action1String = action1String { + alert.addAction(UIAlertAction(title: action1String, style: .default, handler: { _ in + self.dialogProvider?.postAction(1, forDialogReference: reference) + })) + } + if let action2String = action2String { + alert.addAction(UIAlertAction(title: action2String, style: .default, handler: { _ in + self.dialogProvider?.postAction(2, forDialogReference: reference) + })) + } + if let cancelString = cancelString { + alert.addAction(UIAlertAction(title: cancelString, style: .cancel, handler: { _ in + self.dialogProvider?.postAction(3, forDialogReference: reference) + })) + } + + self.viewerMediaPage?.present(alert, animated: true) + } + + func showProgress(withTitle title: String, message: String, isIndeterminate: Bool, position: Float, cancel cancelString: String?, withReference reference: NSValue) { + // UIAlertController other states... + } + + func updateProgress(withReference reference: NSValue, message: String?, position: Float) { + // UIAlertController other states... + } + + func cancelDialog(withReference reference: NSValue) { + // UIAlertController other states... + } } diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift index 2e315cb819..aded4ffbc8 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift @@ -143,19 +143,13 @@ class NCPlayerToolBar: UIView { MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = position } - public func update() { + public func updatePlaybackPosition() { guard let ncplayer = self.ncplayer, let media = ncplayer.player.media else { return } - let length: Int32 - - if let result = self.database.getVideo(metadata: metadata), let resultLength = result.length { - length = Int32(resultLength) - } else { - length = media.length.intValue - } + let length = media.length.intValue let position = ncplayer.player.position @@ -168,11 +162,11 @@ class NCPlayerToolBar: UIView { let remaining = remainingTimeObj.stringValue labelLeftTime.text = "-\(remaining)" - + if playbackSliderEvent == .ended { playbackSlider.value = position } - + MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyPlaybackDuration] = length / 1000 MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentSeconds } From e81c82e57decc99df74910ef76cb97952cf34328 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 30 Dec 2025 13:19:06 +0100 Subject: [PATCH 4/6] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 2 +- iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 91f5407108..44bad97001 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -1554,7 +1554,7 @@ F79B645F26CA661600838ACA /* UIControl+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+Extension.swift"; sourceTree = ""; }; F79B869A265E19D40085C0E0 /* NSMutableAttributedString+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMutableAttributedString+Extension.swift"; sourceTree = ""; }; F79EDA9F26B004980007D134 /* NCPlayerToolBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCPlayerToolBar.swift; sourceTree = ""; }; - F79EDAA126B004980007D134 /* NCPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 5; lastKnownFileType = sourcecode.swift; path = NCPlayer.swift; sourceTree = ""; }; + F79EDAA126B004980007D134 /* NCPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = NCPlayer.swift; sourceTree = ""; }; F79FFB252A97C24A0055EEA4 /* NCNetworkingE2EEMarkFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EEMarkFolder.swift; sourceTree = ""; }; F7A03E2E2D425A14007AA677 /* NCFavoriteNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFavoriteNavigationController.swift; sourceTree = ""; }; F7A03E322D426115007AA677 /* NCMoreNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMoreNavigationController.swift; sourceTree = ""; }; diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift index 64c702e745..669535a8f9 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift @@ -68,7 +68,7 @@ class NCPlayer: NSObject, VLCMediaDelegate { print("Playing URL: \(url)") let media = VLCMedia(url: url) - media.parse(options: .fetchNetwork) + media.parse(options: url.isFileURL ? .fetchLocal : .fetchNetwork) player.media = media player.delegate = self @@ -218,6 +218,7 @@ extension NCPlayer: VLCMediaPlayerDelegate { print("Played mode: STOPPED") case .opening: + playerToolBar?.playbackSliderEvent = .began print("Played mode: OPENING") case .buffering: print("Played mode: BUFFERING") @@ -259,8 +260,8 @@ extension NCPlayer: VLCMediaPlayerDelegate { } self.width = Int(size.width) self.height = Int(size.height) - playerToolBar.updateTopToolBar(videoSubTitlesIndexes: player.videoSubTitlesIndexes, audioTrackIndexes: player.audioTrackIndexes) playerToolBar.updatePlaybackPosition() + playerToolBar.updateTopToolBar(videoSubTitlesIndexes: player.videoSubTitlesIndexes, audioTrackIndexes: player.audioTrackIndexes) self.database.addVideo(metadata: metadata, width: self.width, height: self.height, length: self.length) NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerIsPlaying) From 531e963cb9e6ffb9b62c57a0f6ad78305b872abe Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 30 Dec 2025 13:32:13 +0100 Subject: [PATCH 5/6] Refactor Signed-off-by: Milen Pivchev --- .../Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift index 669535a8f9..6f42f51b1f 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift @@ -216,12 +216,12 @@ extension NCPlayer: VLCMediaPlayerDelegate { NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) - print("Played mode: STOPPED") + print("Player mode: STOPPED") case .opening: playerToolBar?.playbackSliderEvent = .began - print("Played mode: OPENING") + print("Player mode: OPENING") case .buffering: - print("Played mode: BUFFERING") + print("Player mode: BUFFERING") case .ended: self.database.addVideo(metadata: self.metadata, position: 0) DispatchQueue.main.asyncAfter(deadline: .now() + 1) { @@ -230,9 +230,9 @@ extension NCPlayer: VLCMediaPlayerDelegate { } } playerToolBar?.playButtonPlay() - print("Played mode: ENDED") + print("Player mode: ENDED") case .error: - print("Played mode: ERROR") + print("Player mode: ERROR") case .playing: guard let playerToolBar = playerToolBar else { return } if playerToolBar.playerButtonView.isHidden { @@ -266,12 +266,12 @@ extension NCPlayer: VLCMediaPlayerDelegate { NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerIsPlaying) - print("Played mode: PLAYING") + print("Player mode: PLAYING") case .paused: NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) playerToolBar?.playButtonPlay() - print("Played mode: PAUSED") + print("Player mode: PAUSED") default: break } } From ffc528464434f8a9de36d89446e550c9b3d375aa Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 12 Jan 2026 11:45:40 +0100 Subject: [PATCH 6/6] Deinit now playing window (#3943) * Deinit now playing info Signed-off-by: Milen Pivchev * Refactor Signed-off-by: Milen Pivchev --------- Signed-off-by: Milen Pivchev --- iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift | 9 ++++----- .../Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift | 8 +++++--- iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift index 6f42f51b1f..15e991f3fb 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift @@ -212,13 +212,12 @@ extension NCPlayer: VLCMediaPlayerDelegate { switch player.state { case .stopped: - playerToolBar?.playButtonPlay() + playerToolBar?.showPlayButton() NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) print("Player mode: STOPPED") case .opening: - playerToolBar?.playbackSliderEvent = .began print("Player mode: OPENING") case .buffering: print("Player mode: BUFFERING") @@ -229,7 +228,7 @@ extension NCPlayer: VLCMediaPlayerDelegate { self.restartAVPlayer(position: 0, pauseAfterPlay: !playRepeat) } } - playerToolBar?.playButtonPlay() + playerToolBar?.showPlayButton() print("Player mode: ENDED") case .error: print("Player mode: ERROR") @@ -244,7 +243,7 @@ extension NCPlayer: VLCMediaPlayerDelegate { pauseAfterPlay = false self.viewerMediaPage?.updateCommandCenter(ncplayer: self, title: metadata.fileNameView) } else { - playerToolBar.playButtonPause() + playerToolBar.showPauseButton() // Set track audio/subtitle let data = self.database.getVideo(metadata: metadata) if let currentAudioTrackIndex = data?.currentAudioTrackIndex { @@ -270,7 +269,7 @@ extension NCPlayer: VLCMediaPlayerDelegate { case .paused: NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterPlayerStoppedPlaying) - playerToolBar?.playButtonPlay() + playerToolBar?.showPlayButton() print("Player mode: PAUSED") default: break } diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift index aded4ffbc8..ccb930df56 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift @@ -31,11 +31,13 @@ class NCPlayerToolBar: UIView { @IBOutlet weak var repeatButton: UIButton! enum sliderEventType { + case none case began case ended case moved } - var playbackSliderEvent: sliderEventType = .ended + + var playbackSliderEvent: sliderEventType = .none var isFullscreen: Bool = false var playRepeat: Bool = false @@ -196,13 +198,13 @@ class NCPlayerToolBar: UIView { }) } - func playButtonPause() { + func showPauseButton() { buttonImage = UIImage(systemName: "pause.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal) playButton.setImage(buttonImage, for: .normal) MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 1 } - func playButtonPlay() { + func showPlayButton() { buttonImage = UIImage(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal) playButton.setImage(buttonImage, for: .normal) MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 0 diff --git a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift index 85d3021d85..48bc45de63 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift @@ -356,7 +356,7 @@ class NCViewerMediaPage: UIViewController { func clearCommandCenter() { UIApplication.shared.endReceivingRemoteControlEvents() - MPNowPlayingInfoCenter.default().nowPlayingInfo = [:] + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil MPRemoteCommandCenter.shared().playCommand.isEnabled = false MPRemoteCommandCenter.shared().pauseCommand.isEnabled = false