VLCKitVideoViewController.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. //
  2. // SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. //
  5. import Foundation
  6. import UIKit
  7. import MobileVLCKit
  8. @objc protocol VLCKitVideoViewControllerDelegate {
  9. @objc func vlckitVideoViewControllerDismissed(_ controller: VLCKitVideoViewController)
  10. }
  11. @objcMembers class VLCKitVideoViewController: UIViewController, VLCMediaPlayerDelegate {
  12. public weak var delegate: VLCKitVideoViewControllerDelegate?
  13. public static let supportedFileExtensions = ["webm", "mkv"]
  14. @IBOutlet weak var videoViewContainer: NCZoomableView!
  15. @IBOutlet weak var buttonView: UIStackView!
  16. @IBOutlet weak var jumpBackButton: UIButton!
  17. @IBOutlet weak var playPauseButton: UIButton!
  18. @IBOutlet weak var jumpForwardButton: UIButton!
  19. @IBOutlet weak var shareButton: UIButton!
  20. @IBOutlet weak var closeButton: UIButton!
  21. @IBOutlet weak var currentTimeLabel: UILabel!
  22. @IBOutlet weak var totalTimeLabel: UILabel!
  23. @IBOutlet weak var positionSlider: UISlider!
  24. private static let jumpInterval: Int32 = 10
  25. private var mediaPlayer: VLCMediaPlayer?
  26. private var filePath: String
  27. private var setPosition: Bool = false
  28. private var timeObserver: NSKeyValueObservation?
  29. private var remainingTimeObserver: NSKeyValueObservation?
  30. private var sliderValueObserver: NSKeyValueObservation?
  31. private var idleTimer: Timer?
  32. private var pauseAfterPlay: Bool = false
  33. private var videoView = UIView()
  34. init(filePath: String) {
  35. self.filePath = filePath
  36. super.init(nibName: "VLCKitVideoViewController", bundle: nil)
  37. }
  38. required init?(coder: NSCoder) {
  39. fatalError("init(coder:) has not been implemented")
  40. }
  41. override func viewDidLoad() {
  42. self.mediaPlayer = VLCMediaPlayer()
  43. self.mediaPlayer?.delegate = self
  44. self.timeObserver = self.mediaPlayer?.observe(\.time, changeHandler: { _, _ in
  45. self.updateInformation()
  46. })
  47. self.remainingTimeObserver = self.mediaPlayer?.observe(\.remainingTime, changeHandler: { _, _ in
  48. self.updateInformation()
  49. })
  50. self.sliderValueObserver = self.positionSlider?.observe(\.value, changeHandler: { _, _ in
  51. // Make sure the slider is filled to 100% at value 1 since we hid the thumb
  52. if self.positionSlider.value == 1 {
  53. self.positionSlider.maximumTrackTintColor = .systemBlue
  54. } else {
  55. self.positionSlider.maximumTrackTintColor = .none
  56. }
  57. })
  58. self.currentTimeLabel.text = "--:--"
  59. self.totalTimeLabel.text = "--:--"
  60. self.positionSlider.value = 0
  61. // Set close button icon as template
  62. self.closeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
  63. }
  64. @objc private func dismissViewController() {
  65. self.dismiss(animated: true)
  66. }
  67. override func viewDidLayoutSubviews() {
  68. super.viewDidLayoutSubviews()
  69. self.buttonView.layer.cornerRadius = self.buttonView.frame.size.height / 2
  70. self.closeButton.layer.cornerRadius = self.closeButton.frame.size.height / 2
  71. }
  72. override func viewDidAppear(_ animated: Bool) {
  73. super.viewDidAppear(animated)
  74. // Set the inital frame size when the view appeared
  75. self.videoView.frame = self.videoViewContainer.frame
  76. self.videoViewContainer.replaceContentView(self.videoView)
  77. self.mediaPlayer?.drawable = self.videoView
  78. // Allow toggle of controls
  79. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggleControls))
  80. self.view.isUserInteractionEnabled = true
  81. self.view.addGestureRecognizer(tapGesture)
  82. self.videoView.addGestureRecognizer(tapGesture)
  83. // Make sure our singel tap recognizer does not interfere with the double tap recognizer of NCZoomableView
  84. if let doubleTapGestureRecognizer = self.videoViewContainer.doubleTapGestureRecoginzer {
  85. tapGesture.require(toFail: doubleTapGestureRecognizer)
  86. }
  87. self.resetMedia(drawFirstFrame: true)
  88. }
  89. override func viewWillDisappear(_ animated: Bool) {
  90. super.viewWillDisappear(animated)
  91. self.timeObserver?.invalidate()
  92. self.remainingTimeObserver?.invalidate()
  93. self.sliderValueObserver?.invalidate()
  94. self.mediaPlayer?.stop()
  95. }
  96. override func viewDidDisappear(_ animated: Bool) {
  97. super.viewDidDisappear(animated)
  98. self.delegate?.vlckitVideoViewControllerDismissed(self)
  99. }
  100. override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  101. super.viewWillTransition(to: size, with: coordinator)
  102. guard let mediaPlayer = self.mediaPlayer else { return }
  103. // Since there's no way to redraw the current frame in VLCKit, we hide the view
  104. // if the playback was stopped to not have a disorted view
  105. if !mediaPlayer.isPlaying, self.mediaReachedEnd() {
  106. self.videoViewContainer.isHidden = true
  107. }
  108. coordinator.animate(alongsideTransition: nil) { _ in
  109. self.videoView.frame = self.videoViewContainer.frame
  110. self.videoViewContainer.contentViewSize = self.videoViewContainer.frame.size
  111. self.videoViewContainer.resizeContentView()
  112. }
  113. }
  114. override var preferredStatusBarStyle: UIStatusBarStyle {
  115. return .lightContent
  116. }
  117. private func updateInformation() {
  118. self.positionSlider.value = self.mediaPlayer?.position ?? 0
  119. if let remainingTime = self.mediaPlayer?.remainingTime, let currentTime = self.mediaPlayer?.time {
  120. self.currentTimeLabel.text = currentTime.stringValue
  121. self.totalTimeLabel.text = VLCTime(int: abs(currentTime.intValue) + abs(remainingTime.intValue)).stringValue
  122. } else {
  123. self.currentTimeLabel.text = "-:-"
  124. self.totalTimeLabel.text = "-:-"
  125. }
  126. if let mediaPlayer, mediaPlayer.isSeekable {
  127. jumpBackButton.isEnabled = true
  128. jumpForwardButton.isEnabled = true
  129. } else {
  130. jumpBackButton.isEnabled = false
  131. jumpForwardButton.isEnabled = false
  132. }
  133. }
  134. private func resetMedia(drawFirstFrame: Bool) {
  135. self.mediaPlayer?.media = VLCMedia(path: self.filePath)
  136. if drawFirstFrame {
  137. self.pauseAfterPlay = true
  138. self.mediaPlayer?.play()
  139. }
  140. }
  141. private func mediaReachedEnd() -> Bool {
  142. guard let mediaPlayer = self.mediaPlayer else { return false }
  143. return mediaPlayer.remainingTime?.stringValue == "00:00"
  144. }
  145. // MARK: Controls Visibility
  146. private func updateIdleTimer() {
  147. self.idleTimer?.invalidate()
  148. self.idleTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { _ in
  149. self.idleTimer = nil
  150. guard let mediaPlayer = self.mediaPlayer else { return }
  151. // Only hide the controls if we're playing something
  152. if mediaPlayer.isPlaying {
  153. self.hideControls()
  154. }
  155. })
  156. }
  157. @objc private func toggleControls() {
  158. if self.buttonView.alpha == 1 {
  159. self.setControlsAlpha(alpha: 0)
  160. } else {
  161. self.setControlsAlpha(alpha: 1)
  162. self.updateIdleTimer()
  163. }
  164. }
  165. private func showControls() {
  166. self.setControlsAlpha(alpha: 1)
  167. }
  168. private func hideControls() {
  169. self.setControlsAlpha(alpha: 0)
  170. }
  171. private func setControlsAlpha(alpha: CGFloat) {
  172. UIView.animate(withDuration: 0.3) {
  173. self.buttonView.alpha = alpha
  174. self.positionSlider.alpha = alpha
  175. self.totalTimeLabel.alpha = alpha
  176. self.currentTimeLabel.alpha = alpha
  177. self.closeButton.alpha = alpha
  178. }
  179. }
  180. // MARK: InterfaceBuilder Actions
  181. @IBAction func jumpBackButtonTap(_ sender: Any) {
  182. guard let mediaPlayer = self.mediaPlayer else { return }
  183. mediaPlayer.jumpBackward(Self.jumpInterval)
  184. updateInformation()
  185. updateIdleTimer()
  186. }
  187. @IBAction func jumpForwardButtonTap(_ sender: Any) {
  188. guard let mediaPlayer = self.mediaPlayer else { return }
  189. // VLCKit does not clamp to the media end, if the jump is beyond its length.
  190. // Hence the destination must be calculated manually to mimic first party video playback views.
  191. if let remainingTime = mediaPlayer.remainingTime {
  192. let remainingSeconds = abs(remainingTime.intValue) / 1000
  193. if remainingSeconds >= Self.jumpInterval {
  194. mediaPlayer.jumpForward(Self.jumpInterval)
  195. } else {
  196. mediaPlayer.jumpForward(remainingSeconds)
  197. }
  198. }
  199. updateInformation()
  200. updateIdleTimer()
  201. }
  202. @IBAction func playPauseButtonTap(_ sender: Any) {
  203. guard let mediaPlayer = self.mediaPlayer else { return }
  204. if mediaPlayer.isPlaying {
  205. mediaPlayer.pause()
  206. } else {
  207. // In VLCKit 3.3.17 the position parameter is not working correctly (fixed in 4.0.0)
  208. // see https://code.videolan.org/videolan/VLCKit/-/issues/583
  209. if self.mediaReachedEnd() {
  210. // When we reached the end of the media, start from the beginning again
  211. self.resetMedia(drawFirstFrame: false)
  212. }
  213. mediaPlayer.play()
  214. }
  215. self.updateIdleTimer()
  216. }
  217. @IBAction func shareButtonTap(_ sender: Any) {
  218. let activityItem = NSURL(fileURLWithPath: filePath)
  219. let activityVC = UIActivityViewController(activityItems: [activityItem], applicationActivities: nil)
  220. self.present(activityVC, animated: true, completion: nil)
  221. self.updateIdleTimer()
  222. }
  223. @IBAction func positionSliderAction(_ sender: Any) {
  224. // From the example of VLCKit we should limit the number of events to make sure,
  225. // the user actually sees I-frames when seeking
  226. DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
  227. guard let mediaPlayer = self.mediaPlayer else { return }
  228. if !self.setPosition {
  229. self.setPosition = false
  230. // When the media is in state "ended"/"stopped" we can't seek anymore, so we need to reset the media
  231. if self.mediaReachedEnd() {
  232. self.resetMedia(drawFirstFrame: true)
  233. }
  234. mediaPlayer.position = self.positionSlider.value
  235. self.updateIdleTimer()
  236. }
  237. }
  238. }
  239. @IBAction func closeButtonTap(_ sender: Any) {
  240. self.dismissViewController()
  241. }
  242. // MARK: VLCMediaPlayerDelegate
  243. func mediaPlayerStateChanged(_ aNotification: Notification!) {
  244. guard let mediaPlayer = self.mediaPlayer else { return }
  245. if mediaPlayer.isPlaying {
  246. // When state changed to playing because we reseted the stream and
  247. // started playing to load it, we want to pause it here again, as it wasn't playing before
  248. if self.pauseAfterPlay {
  249. self.pauseAfterPlay = false
  250. mediaPlayer.pause()
  251. return
  252. }
  253. self.playPauseButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
  254. self.videoViewContainer.isHidden = false
  255. } else {
  256. self.playPauseButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
  257. self.showControls()
  258. // Check if we reached the end, although maybe not reported by the position (fixed in 4.0.0)
  259. if self.mediaReachedEnd() {
  260. self.positionSlider.value = 1
  261. }
  262. }
  263. }
  264. }