diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj
index d546868fde..0a35fe2a21 100644
--- a/Nextcloud.xcodeproj/project.pbxproj
+++ b/Nextcloud.xcodeproj/project.pbxproj
@@ -6303,7 +6303,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 4;
+ CURRENT_PROJECT_VERSION = 5;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = NKUJUXUJ3B;
@@ -6371,7 +6371,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 4;
+ CURRENT_PROJECT_VERSION = 5;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = NKUJUXUJ3B;
diff --git a/iOSClient/Images.xcassets/Vlc-Logo.imageset/Contents.json b/iOSClient/Images.xcassets/Vlc-Logo.imageset/Contents.json
new file mode 100644
index 0000000000..4981181699
--- /dev/null
+++ b/iOSClient/Images.xcassets/Vlc-Logo.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "Vlc-Logo.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iOSClient/Images.xcassets/Vlc-Logo.imageset/Vlc-Logo.svg b/iOSClient/Images.xcassets/Vlc-Logo.imageset/Vlc-Logo.svg
new file mode 100644
index 0000000000..8f4587979c
--- /dev/null
+++ b/iOSClient/Images.xcassets/Vlc-Logo.imageset/Vlc-Logo.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/iOSClient/Settings/NCPreferences.swift b/iOSClient/Settings/NCPreferences.swift
index badd971c0b..df114bfd77 100644
--- a/iOSClient/Settings/NCPreferences.swift
+++ b/iOSClient/Settings/NCPreferences.swift
@@ -604,6 +604,37 @@ final class NCPreferences: NSObject {
setUserDefaults(weekString, forKey: "cleaningWeek")
}
+ // MARK: - Video
+
+ func alwaysUseVLCForVideo(account: String, ocId: String) -> Bool {
+ let key = alwaysUseVLCForVideoKey(
+ account: account,
+ ocId: ocId
+ )
+
+ return UserDefaults.standard.object(forKey: key) as? Bool == true
+ }
+
+ func setAlwaysUseVLCForVideo(_ value: Bool, account: String, ocId: String) {
+ let key = alwaysUseVLCForVideoKey(
+ account: account,
+ ocId: ocId
+ )
+
+ if value {
+ UserDefaults.standard.set(true, forKey: key)
+ } else {
+ UserDefaults.standard.removeObject(forKey: key)
+ }
+ }
+
+ private func alwaysUseVLCForVideoKey(
+ account: String,
+ ocId: String
+ ) -> String {
+ "Preferences_alwaysUseVLCForVideo_\(account)|\(ocId)"
+ }
+
// MARK: -
private func migrate(key: String) {
diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings
index b299172ca7..1cc0c4a28a 100644
--- a/iOSClient/Supporting Files/en.lproj/Localizable.strings
+++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings
@@ -708,6 +708,8 @@
"_no_assistant_installed_" = "Assistant is not installed on this server. Ask your administrator to install the Assistant app";
"_open_in_office_" = "Open in Office";
"_select_date_" = "Select date";
+"_play_with_vlc_" = "Play with VLC";
+"_always_play_with_vlc_" = "Always play with VLC";
// Tip
"_tip_pdf_thumbnails_" = "Swipe left from the right edge of the screen to show the thumbnails";
diff --git a/iOSClient/Viewer/NCViewerMedia/Content/Video/NCVideoPlaybackController.swift b/iOSClient/Viewer/NCViewerMedia/Content/Video/NCVideoPlaybackController.swift
index 50cda8d7a9..ae94593397 100644
--- a/iOSClient/Viewer/NCViewerMedia/Content/Video/NCVideoPlaybackController.swift
+++ b/iOSClient/Viewer/NCViewerMedia/Content/Video/NCVideoPlaybackController.swift
@@ -47,7 +47,7 @@ final class NCVideoPlaybackController: ObservableObject {
private var currentOcId: String?
private var currentEtag: String?
private var currentURL: URL?
- private var currentFileName: String?
+ private var currentUserAgent: String?
private var loadToken = UUID()
private init() { }
@@ -94,7 +94,7 @@ final class NCVideoPlaybackController: ObservableObject {
currentOcId = metadata.ocId
currentEtag = metadata.etag
currentURL = url
- currentFileName = fileName
+ currentUserAgent = userAgent
engine = .loading
if url.isFileURL,
@@ -105,6 +105,15 @@ final class NCVideoPlaybackController: ObservableObject {
configureAudioSession()
+ if NCPreferences().alwaysUseVLCForVideo(account: metadata.account, ocId: metadata.ocId) {
+ resolveWithVLC(
+ url: url,
+ userAgent: userAgent,
+ token: token
+ )
+ return
+ }
+
if shouldUseVLCWithoutAVFoundation(
url: url,
fileName: fileName
@@ -118,7 +127,6 @@ final class NCVideoPlaybackController: ObservableObject {
}
prepareAVFoundation(
- metadata: metadata,
url: url,
userAgent: userAgent,
httpHeaders: url.isFileURL ? [:] : httpHeaders,
@@ -126,6 +134,52 @@ final class NCVideoPlaybackController: ObservableObject {
)
}
+ // Changes only the prepared engine. Playback still starts from the cover.
+ func switchToVLC() {
+ guard let currentURL else {
+ return
+ }
+
+ resolveWithVLC(
+ url: currentURL,
+ userAgent: currentUserAgent,
+ token: loadToken
+ )
+ }
+
+ func retryAVFoundation() {
+ guard let currentURL else {
+ return
+ }
+
+ let token = UUID()
+ loadToken = token
+
+ statusObservation?.invalidate()
+ statusObservation = nil
+
+ avProbePlayer?.pause()
+ avProbePlayer = nil
+ avProbeItem = nil
+
+ engine = .loading
+
+ var httpHeaders: [String: String] = [:]
+
+ if let currentUserAgent,
+ !currentUserAgent.isEmpty,
+ !currentURL.isFileURL {
+ httpHeaders["User-Agent"] = currentUserAgent
+ }
+
+ prepareAVFoundation(
+ url: currentURL,
+ userAgent: currentUserAgent,
+ httpHeaders: httpHeaders,
+ token: token
+ )
+ }
+
func stopIfCurrent(ocId: String) {
guard currentOcId == ocId else {
return
@@ -133,6 +187,7 @@ final class NCVideoPlaybackController: ObservableObject {
stop()
}
+
// Releases the current prepared playback state and pending AVFoundation probes.
func stop() {
loadToken = UUID()
@@ -147,15 +202,13 @@ final class NCVideoPlaybackController: ObservableObject {
currentOcId = nil
currentEtag = nil
currentURL = nil
- currentFileName = nil
-
+ currentUserAgent = nil
engine = .loading
}
// MARK: - AVFoundation
private func prepareAVFoundation(
- metadata: tableMetadata,
url: URL,
userAgent: String?,
httpHeaders: [String: String],
diff --git a/iOSClient/Viewer/NCViewerMedia/Content/Video/VLC/NCVideoVLCPresenter.swift b/iOSClient/Viewer/NCViewerMedia/Content/Video/VLC/NCVideoVLCPresenter.swift
index d03666ac88..57b94ee7c6 100644
--- a/iOSClient/Viewer/NCViewerMedia/Content/Video/VLC/NCVideoVLCPresenter.swift
+++ b/iOSClient/Viewer/NCViewerMedia/Content/Video/VLC/NCVideoVLCPresenter.swift
@@ -146,6 +146,9 @@ enum NCVideoVLCPresenter {
return
}
+ // Stop VLC synchronously before changing the Media Viewer page.
+ currentViewController.stop()
+
currentViewController.dismiss(animated: false) {
clearCurrent(currentViewController)
}
diff --git a/iOSClient/Viewer/NCViewerMedia/Content/Video/VLC/NCVideoVLCViewController.swift b/iOSClient/Viewer/NCViewerMedia/Content/Video/VLC/NCVideoVLCViewController.swift
index 45b1a4e570..0543992d78 100644
--- a/iOSClient/Viewer/NCViewerMedia/Content/Video/VLC/NCVideoVLCViewController.swift
+++ b/iOSClient/Viewer/NCViewerMedia/Content/Video/VLC/NCVideoVLCViewController.swift
@@ -22,6 +22,7 @@ final class NCVideoVLCViewController: UIViewController {
private var shouldAutoPlayOnStart: Bool
private var isChromeHidden: Bool
private weak var contextMenuController: NCMainTabBarController?
+ private var isReplayFromBeginningRequested = false
// MARK: - Paging Callbacks
@@ -545,8 +546,10 @@ final class NCVideoVLCViewController: UIViewController {
stopControlsHideTimer()
}
- private func stop() {
+ func stop() {
+ stopControlsHideTimer()
isPlaybackRequested = false
+ isReplayFromBeginningRequested = false
mediaPlayer.stop()
mediaPlayer.media = nil
@@ -558,6 +561,40 @@ final class NCVideoVLCViewController: UIViewController {
clearVLCTrackMenuItems()
}
+ func restartPlaybackFromBeginning() {
+ isReplayFromBeginningRequested = true
+ isPlaybackRequested = true
+ updatePlayPauseButton()
+
+ if mediaPlayer.state == .stopped {
+ startReplayAfterStop()
+ } else {
+ mediaPlayer.stop()
+ }
+ }
+
+ private func startReplayAfterStop() {
+ guard isReplayFromBeginningRequested else {
+ return
+ }
+
+ isReplayFromBeginningRequested = false
+
+ let media = VLCMedia(url: url)
+
+ if let userAgent,
+ !userAgent.isEmpty,
+ !url.isFileURL {
+ media.addOption(":http-user-agent=\(userAgent)")
+ }
+
+ mediaPlayer.media = media
+ mediaPlayer.play()
+
+ startProgressTimer()
+ scheduleControlsHide()
+ }
+
private func attachDrawable() {
guard drawableView.bounds.width > 0,
drawableView.bounds.height > 0 else {
@@ -577,9 +614,24 @@ final class NCVideoVLCViewController: UIViewController {
case .playing:
isPlaybackRequested = true
+ case .ended:
+ isPlaybackRequested = false
+ stopProgressTimer()
+ updatePlayPauseButton()
+ updateProgressLabels(position: 1)
+ showControls(animated: true)
+ stopControlsHideTimer()
+ return
+
+ case .stopped:
+ if isReplayFromBeginningRequested {
+ startReplayAfterStop()
+ return
+ }
+
+ isPlaybackRequested = false
+
case .paused,
- .stopped,
- .ended,
.error:
isPlaybackRequested = false
diff --git a/iOSClient/Viewer/NCViewerMedia/Content/Video/VLC/NCVideoVLCViewControls.swift b/iOSClient/Viewer/NCViewerMedia/Content/Video/VLC/NCVideoVLCViewControls.swift
index 5ae0690174..b9549d120c 100644
--- a/iOSClient/Viewer/NCViewerMedia/Content/Video/VLC/NCVideoVLCViewControls.swift
+++ b/iOSClient/Viewer/NCViewerMedia/Content/Video/VLC/NCVideoVLCViewControls.swift
@@ -189,10 +189,14 @@ extension NCVideoVLCViewController: NCVideoControlsViewDelegate {
updatePlayPauseButton()
showControls(animated: false)
stopControlsHideTimer()
+ } else if mediaPlayer.state == .ended ||
+ mediaPlayer.state == .stopped {
+ restartPlaybackFromBeginning()
} else {
isPlaybackRequested = true
updatePlayPauseButton()
mediaPlayer.play()
+ startProgressTimer()
scheduleControlsHide()
}
diff --git a/iOSClient/Viewer/NCViewerMedia/NCMediaViewerHostingController.swift b/iOSClient/Viewer/NCViewerMedia/NCMediaViewerHostingController.swift
index 2b8b68d8a9..8819cae17c 100644
--- a/iOSClient/Viewer/NCViewerMedia/NCMediaViewerHostingController.swift
+++ b/iOSClient/Viewer/NCViewerMedia/NCMediaViewerHostingController.swift
@@ -39,27 +39,38 @@ final class NCMediaViewerHostingController: UIHostingController UIMenu? {
+ guard metadata.classFile == NKTypeClassFile.video.rawValue else {
+ return nil
+ }
+
+ let playback = NCVideoPlaybackController.shared
+
+ guard playback.isCurrentVideo(
+ ocId: metadata.ocId,
+ etag: metadata.etag
+ ) else {
+ return nil
+ }
+
+ let alwaysUseVLC = NCPreferences().alwaysUseVLCForVideo(
+ account: metadata.account,
+ ocId: metadata.ocId
+ )
+
+ let alwaysUseVLCAction: UIAction
+
+ switch playback.engine {
+ case .avFoundation:
+ alwaysUseVLCAction = UIAction(
+ title: NSLocalizedString("_always_play_with_vlc_", comment: ""),
+ image: UIImage(named: "Vlc-Logo")?.withRenderingMode(.alwaysTemplate),
+ state: .off
+ ) { _ in
+ NCPreferences().setAlwaysUseVLCForVideo(
+ true,
+ account: metadata.account,
+ ocId: metadata.ocId
+ )
+
+ NCVideoPlaybackController.shared.switchToVLC()
+ }
+
+ case .vlc:
+ guard alwaysUseVLC else {
+ return nil
+ }
+
+ alwaysUseVLCAction = UIAction(
+ title: NSLocalizedString("_always_play_with_vlc_", comment: ""),
+ image: UIImage(named: "Vlc-Logo")?.withRenderingMode(.alwaysTemplate),
+ state: .on
+ ) { _ in
+ NCPreferences().setAlwaysUseVLCForVideo(
+ false,
+ account: metadata.account,
+ ocId: metadata.ocId
+ )
+
+ NCVideoPlaybackController.shared.retryAVFoundation()
+ }
+
+ case .loading,
+ .failed:
+ return nil
+ }
+
+ return UIMenu(
+ title: "",
+ options: .displayInline,
+ children: [
+ alwaysUseVLCAction
+ ]
+ )
+ }
+
/// Creates a media viewer hosting controller.
init(
model: NCMediaViewerModel,