NCPlayerToolBar.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. //
  2. // NCPlayerToolBar.swift
  3. // Nextcloud
  4. //
  5. // Created by Marino Faggiana on 01/07/21.
  6. // Copyright © 2021 Marino Faggiana. All rights reserved.
  7. //
  8. // Author Marino Faggiana <marino.faggiana@nextcloud.com>
  9. //
  10. // This program is free software: you can redistribute it and/or modify
  11. // it under the terms of the GNU General Public License as published by
  12. // the Free Software Foundation, either version 3 of the License, or
  13. // (at your option) any later version.
  14. //
  15. // This program is distributed in the hope that it will be useful,
  16. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. // GNU General Public License for more details.
  19. //
  20. // You should have received a copy of the GNU General Public License
  21. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. //
  23. import Foundation
  24. import NextcloudKit
  25. import CoreMedia
  26. import UIKit
  27. import AVKit
  28. import MediaPlayer
  29. import MobileVLCKit
  30. import FloatingPanel
  31. import Alamofire
  32. class NCPlayerToolBar: UIView {
  33. @IBOutlet weak var utilityView: UIView!
  34. @IBOutlet weak var fullscreenButton: UIButton!
  35. @IBOutlet weak var subtitleButton: UIButton!
  36. @IBOutlet weak var audioButton: UIButton!
  37. @IBOutlet weak var playerButtonView: UIStackView!
  38. @IBOutlet weak var backButton: UIButton!
  39. @IBOutlet weak var playButton: UIButton!
  40. @IBOutlet weak var forwardButton: UIButton!
  41. @IBOutlet weak var playbackSliderView: UIView!
  42. @IBOutlet weak var playbackSlider: NCPlayerToolBarSlider!
  43. @IBOutlet weak var labelLeftTime: UILabel!
  44. @IBOutlet weak var labelCurrentTime: UILabel!
  45. @IBOutlet weak var repeatButton: UIButton!
  46. enum sliderEventType {
  47. case began
  48. case ended
  49. case moved
  50. }
  51. var playbackSliderEvent: sliderEventType = .ended
  52. var isFullscreen: Bool = false
  53. var playRepeat: Bool = false
  54. private let hud = NCHud()
  55. private var ncplayer: NCPlayer?
  56. private var metadata: tableMetadata?
  57. private let audioSession = AVAudioSession.sharedInstance()
  58. private var pointSize: CGFloat = 0
  59. private let utilityFileSystem = NCUtilityFileSystem()
  60. private let utility = NCUtility()
  61. private let database = NCManageDatabase.shared
  62. private weak var viewerMediaPage: NCViewerMediaPage?
  63. private var buttonImage = UIImage()
  64. // MARK: - View Life Cycle
  65. override func awakeFromNib() {
  66. super.awakeFromNib()
  67. self.backgroundColor = UIColor.black.withAlphaComponent(0.1)
  68. fullscreenButton.setImage(utility.loadImage(named: "arrow.up.left.and.arrow.down.right", colors: [.white]), for: .normal)
  69. subtitleButton.setImage(utility.loadImage(named: "captions.bubble", colors: [.white]), for: .normal)
  70. subtitleButton.isEnabled = false
  71. audioButton.setImage(utility.loadImage(named: "speaker.zzz", colors: [.white]), for: .normal)
  72. audioButton.isEnabled = false
  73. if UIDevice.current.userInterfaceIdiom == .pad {
  74. pointSize = 60
  75. } else {
  76. pointSize = 50
  77. }
  78. playerButtonView.spacing = pointSize
  79. playerButtonView.isHidden = true
  80. buttonImage = UIImage(systemName: "gobackward.10", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal)
  81. backButton.setImage(buttonImage, for: .normal)
  82. buttonImage = UIImage(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal)
  83. playButton.setImage(buttonImage, for: .normal)
  84. buttonImage = UIImage(systemName: "goforward.10", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal)
  85. forwardButton.setImage(buttonImage, for: .normal)
  86. playbackSlider.addTapGesture()
  87. playbackSlider.setThumbImage(UIImage(systemName: "circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15)), for: .normal)
  88. playbackSlider.value = 0
  89. playbackSlider.tintColor = .white
  90. playbackSlider.addTarget(self, action: #selector(playbackValChanged(slider:event:)), for: .valueChanged)
  91. repeatButton.setImage(utility.loadImage(named: "repeat", colors: [NCBrandColor.shared.iconImageColor2]), for: .normal)
  92. utilityView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap(gestureRecognizer:))))
  93. playbackSliderView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap(gestureRecognizer:))))
  94. playbackSliderView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(tap(gestureRecognizer:))))
  95. playerButtonView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap(gestureRecognizer:))))
  96. labelCurrentTime.textColor = .white
  97. labelLeftTime.textColor = .white
  98. // Normally hide
  99. self.alpha = 0
  100. self.isHidden = true
  101. }
  102. required init?(coder aDecoder: NSCoder) {
  103. super.init(coder: aDecoder)
  104. }
  105. deinit {
  106. print("deinit NCPlayerToolBar")
  107. }
  108. // MARK: -
  109. func setBarPlayer(position: Float, ncplayer: NCPlayer? = nil, metadata: tableMetadata? = nil, viewerMediaPage: NCViewerMediaPage? = nil) {
  110. if let ncplayer = ncplayer {
  111. self.ncplayer = ncplayer
  112. }
  113. if let metadata = metadata {
  114. self.metadata = metadata
  115. }
  116. if let viewerMediaPage = viewerMediaPage {
  117. self.viewerMediaPage = viewerMediaPage
  118. }
  119. playerButtonView.isHidden = true
  120. buttonImage = UIImage(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal)
  121. playButton.setImage(buttonImage, for: .normal)
  122. playbackSlider.value = position
  123. labelCurrentTime.text = "--:--"
  124. labelLeftTime.text = "--:--"
  125. if viewerMediaScreenMode == .normal {
  126. show()
  127. } else {
  128. hide()
  129. }
  130. MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = position
  131. }
  132. public func update() {
  133. guard let ncplayer = self.ncplayer, let length = ncplayer.player.media?.length.intValue else { return }
  134. let position = ncplayer.player.position
  135. let positionInSecond = position * Float(length / 1000)
  136. // SLIDER & TIME
  137. if playbackSliderEvent == .ended {
  138. playbackSlider.value = position
  139. }
  140. labelCurrentTime.text = ncplayer.player.time.stringValue
  141. labelLeftTime.text = ncplayer.player.remainingTime?.stringValue
  142. MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyPlaybackDuration] = length / 1000
  143. MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = positionInSecond
  144. }
  145. public func updateTopToolBar(videoSubTitlesIndexes: [Any], audioTrackIndexes: [Any]) {
  146. if let metadata = metadata, metadata.isVideo {
  147. self.subtitleButton.isEnabled = true
  148. self.audioButton.isEnabled = true
  149. }
  150. }
  151. // MARK: -
  152. public func show() {
  153. UIView.animate(withDuration: 0.5, animations: {
  154. self.alpha = 1
  155. }, completion: { (_: Bool) in
  156. self.isHidden = false
  157. })
  158. }
  159. func hide() {
  160. UIView.animate(withDuration: 0.5, animations: {
  161. self.alpha = 0
  162. }, completion: { (_: Bool) in
  163. self.isHidden = true
  164. })
  165. }
  166. func playButtonPause() {
  167. buttonImage = UIImage(systemName: "pause.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal)
  168. playButton.setImage(buttonImage, for: .normal)
  169. MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 1
  170. }
  171. func playButtonPlay() {
  172. buttonImage = UIImage(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal)
  173. playButton.setImage(buttonImage, for: .normal)
  174. MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 0
  175. }
  176. // MARK: - Event / Gesture
  177. @objc func playbackValChanged(slider: UISlider, event: UIEvent) {
  178. guard let ncplayer = ncplayer else { return }
  179. let newPosition = playbackSlider.value
  180. if let touchEvent = event.allTouches?.first {
  181. switch touchEvent.phase {
  182. case .began:
  183. viewerMediaPage?.timerAutoHide?.invalidate()
  184. playbackSliderEvent = .began
  185. case .moved:
  186. ncplayer.playerPosition(newPosition)
  187. playbackSliderEvent = .moved
  188. case .ended:
  189. ncplayer.playerPosition(newPosition)
  190. DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
  191. self.playbackSliderEvent = .ended
  192. self.viewerMediaPage?.startTimerAutoHide()
  193. }
  194. default:
  195. break
  196. }
  197. } else {
  198. ncplayer.playerPosition(newPosition)
  199. self.viewerMediaPage?.startTimerAutoHide()
  200. }
  201. }
  202. // MARK: - Action
  203. @objc func tap(gestureRecognizer: UITapGestureRecognizer) { }
  204. @IBAction func tapFullscreen(_ sender: Any) {
  205. isFullscreen = !isFullscreen
  206. if isFullscreen {
  207. fullscreenButton.setImage(utility.loadImage(named: "arrow.down.right.and.arrow.up.left", colors: [.white]), for: .normal)
  208. } else {
  209. fullscreenButton.setImage(utility.loadImage(named: "arrow.up.left.and.arrow.down.right", colors: [.white]), for: .normal)
  210. }
  211. viewerMediaPage?.changeScreenMode(mode: viewerMediaScreenMode)
  212. }
  213. @IBAction func tapSubTitle(_ sender: Any) {
  214. guard let player = ncplayer?.player else { return }
  215. let spuTracks = player.videoSubTitlesNames
  216. let spuTrackIndexes = player.videoSubTitlesIndexes
  217. toggleMenuSubTitle(spuTracks: spuTracks, spuTrackIndexes: spuTrackIndexes)
  218. }
  219. @IBAction func tapAudio(_ sender: Any) {
  220. guard let player = ncplayer?.player else { return }
  221. let audioTracks = player.audioTrackNames
  222. let audioTrackIndexes = player.audioTrackIndexes
  223. toggleMenuAudio(audioTracks: audioTracks, audioTrackIndexes: audioTrackIndexes)
  224. }
  225. @IBAction func tapPlayerPause(_ sender: Any) {
  226. guard let ncplayer = ncplayer else { return }
  227. if ncplayer.isPlaying() {
  228. ncplayer.playerPause()
  229. } else {
  230. ncplayer.playerPlay()
  231. }
  232. self.viewerMediaPage?.startTimerAutoHide()
  233. }
  234. @IBAction func tapForward(_ sender: Any) {
  235. guard let ncplayer = ncplayer else { return }
  236. ncplayer.jumpForward(10)
  237. self.viewerMediaPage?.startTimerAutoHide()
  238. }
  239. @IBAction func tapBack(_ sender: Any) {
  240. guard let ncplayer = ncplayer else { return }
  241. ncplayer.jumpBackward(10)
  242. self.viewerMediaPage?.startTimerAutoHide()
  243. }
  244. @IBAction func tapRepeat(_ sender: Any) {
  245. if playRepeat {
  246. playRepeat = false
  247. repeatButton.setImage(utility.loadImage(named: "repeat", colors: [NCBrandColor.shared.iconImageColor2]), for: .normal)
  248. } else {
  249. playRepeat = true
  250. repeatButton.setImage(utility.loadImage(named: "repeat", colors: [.white]), for: .normal)
  251. }
  252. }
  253. }
  254. extension NCPlayerToolBar {
  255. func toggleMenuSubTitle(spuTracks: [Any], spuTrackIndexes: [Any]) {
  256. var actions = [NCMenuAction]()
  257. var subTitleIndex: Int?
  258. if let data = self.database.getVideo(metadata: metadata), let idx = data.currentVideoSubTitleIndex {
  259. subTitleIndex = idx
  260. } else if let idx = ncplayer?.player.currentVideoSubTitleIndex {
  261. subTitleIndex = Int(idx)
  262. }
  263. if !spuTracks.isEmpty {
  264. for index in 0...spuTracks.count - 1 {
  265. guard let title = spuTracks[index] as? String, let idx = spuTrackIndexes[index] as? Int32, let metadata = self.metadata else { return }
  266. actions.append(
  267. NCMenuAction(
  268. title: title,
  269. icon: UIImage(),
  270. onTitle: title,
  271. onIcon: UIImage(),
  272. selected: (subTitleIndex ?? -9999) == idx,
  273. on: (subTitleIndex ?? -9999) == idx,
  274. action: { _ in
  275. self.ncplayer?.player.currentVideoSubTitleIndex = idx
  276. self.database.addVideo(metadata: metadata, currentVideoSubTitleIndex: Int(idx))
  277. }
  278. )
  279. )
  280. }
  281. actions.append(.seperator(order: 0))
  282. }
  283. actions.append(
  284. NCMenuAction(
  285. title: NSLocalizedString("_add_subtitle_", comment: ""),
  286. icon: UIImage(),
  287. onTitle: NSLocalizedString("_add_subtitle_", comment: ""),
  288. onIcon: UIImage(),
  289. selected: false,
  290. on: false,
  291. action: { _ in
  292. guard let metadata = self.metadata else { return }
  293. let storyboard = UIStoryboard(name: "NCSelect", bundle: nil)
  294. if let navigationController = storyboard.instantiateInitialViewController() as? UINavigationController,
  295. let viewController = navigationController.topViewController as? NCSelect {
  296. viewController.delegate = self
  297. viewController.typeOfCommandView = .nothing
  298. viewController.includeDirectoryE2EEncryption = false
  299. viewController.enableSelectFile = true
  300. viewController.type = "subtitle"
  301. viewController.serverUrl = metadata.serverUrl
  302. viewController.session = NCSession.shared.getSession(account: metadata.account)
  303. self.viewerMediaPage?.present(navigationController, animated: true, completion: nil)
  304. }
  305. }
  306. )
  307. )
  308. viewerMediaPage?.presentMenu(with: actions, menuColor: UIColor(hexString: "#1C1C1EFF"), textColor: .white)
  309. }
  310. func toggleMenuAudio(audioTracks: [Any], audioTrackIndexes: [Any]) {
  311. var actions = [NCMenuAction]()
  312. var audioIndex: Int?
  313. if let data = self.database.getVideo(metadata: metadata), let idx = data.currentAudioTrackIndex {
  314. audioIndex = idx
  315. } else if let idx = ncplayer?.player.currentAudioTrackIndex {
  316. audioIndex = Int(idx)
  317. }
  318. if !audioTracks.isEmpty {
  319. for index in 0...audioTracks.count - 1 {
  320. guard let title = audioTracks[index] as? String, let idx = audioTrackIndexes[index] as? Int32, let metadata = self.metadata else { return }
  321. actions.append(
  322. NCMenuAction(
  323. title: title,
  324. icon: UIImage(),
  325. onTitle: title,
  326. onIcon: UIImage(),
  327. selected: (audioIndex ?? -9999) == idx,
  328. on: (audioIndex ?? -9999) == idx,
  329. action: { _ in
  330. self.ncplayer?.player.currentAudioTrackIndex = idx
  331. self.database.addVideo(metadata: metadata, currentAudioTrackIndex: Int(idx))
  332. }
  333. )
  334. )
  335. }
  336. actions.append(.seperator(order: 0))
  337. }
  338. actions.append(
  339. NCMenuAction(
  340. title: NSLocalizedString("_add_audio_", comment: ""),
  341. icon: UIImage(),
  342. onTitle: NSLocalizedString("_add_audio_", comment: ""),
  343. onIcon: UIImage(),
  344. selected: false,
  345. on: false,
  346. action: { _ in
  347. guard let metadata = self.metadata else { return }
  348. let storyboard = UIStoryboard(name: "NCSelect", bundle: nil)
  349. if let navigationController = storyboard.instantiateInitialViewController() as? UINavigationController,
  350. let viewController = navigationController.topViewController as? NCSelect {
  351. viewController.delegate = self
  352. viewController.typeOfCommandView = .nothing
  353. viewController.includeDirectoryE2EEncryption = false
  354. viewController.enableSelectFile = true
  355. viewController.type = "audio"
  356. viewController.serverUrl = metadata.serverUrl
  357. viewController.session = NCSession.shared.getSession(account: metadata.account)
  358. self.viewerMediaPage?.present(navigationController, animated: true, completion: nil)
  359. }
  360. }
  361. )
  362. )
  363. viewerMediaPage?.presentMenu(with: actions, menuColor: UIColor(hexString: "#1C1C1EFF"), textColor: .white)
  364. }
  365. }
  366. extension NCPlayerToolBar: NCSelectDelegate {
  367. func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool, session: NCSession.Session) {
  368. if let metadata = metadata, let viewerMediaPage = viewerMediaPage {
  369. let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName
  370. let fileNameLocalPath = NCUtilityFileSystem().getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView)
  371. if utilityFileSystem.fileProviderStorageExists(metadata) {
  372. addPlaybackSlave(type: type, metadata: metadata)
  373. } else {
  374. var downloadRequest: DownloadRequest?
  375. hud.initHudRing(view: viewerMediaPage.view,
  376. text: NSLocalizedString("_downloading_", comment: ""),
  377. tapToCancelDetailText: true) {
  378. if let request = downloadRequest {
  379. request.cancel()
  380. }
  381. }
  382. NextcloudKit.shared.download(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: metadata.account, requestHandler: { request in
  383. downloadRequest = request
  384. }, taskHandler: { _ in
  385. }, progressHandler: { progress in
  386. self.hud.progress(progress.fractionCompleted)
  387. }) { _, _, _, _, _, _, error in
  388. self.hud.dismiss()
  389. if error == .success {
  390. self.hud.success()
  391. self.addPlaybackSlave(type: type, metadata: metadata)
  392. } else if error.errorCode != 200 {
  393. self.hud.error(text: error.errorDescription)
  394. }
  395. }
  396. }
  397. }
  398. }
  399. // swiftlint:disable inclusive_language
  400. func addPlaybackSlave(type: String, metadata: tableMetadata) {
  401. // swiftlint:enable inclusive_language
  402. let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView)
  403. if type == "subtitle" {
  404. self.ncplayer?.player.addPlaybackSlave(URL(fileURLWithPath: fileNameLocalPath), type: .subtitle, enforce: true)
  405. } else if type == "audio" {
  406. self.ncplayer?.player.addPlaybackSlave(URL(fileURLWithPath: fileNameLocalPath), type: .audio, enforce: true)
  407. }
  408. }
  409. }
  410. // https://stackoverflow.com/questions/13196263/custom-uislider-increase-hot-spot-size
  411. //
  412. class NCPlayerToolBarSlider: UISlider {
  413. private var thumbTouchSize = CGSize(width: 100, height: 100)
  414. override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
  415. let increasedBounds = bounds.insetBy(dx: -thumbTouchSize.width, dy: -thumbTouchSize.height)
  416. let containsPoint = increasedBounds.contains(point)
  417. return containsPoint
  418. }
  419. override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
  420. let percentage = CGFloat((value - minimumValue) / (maximumValue - minimumValue))
  421. let thumbSizeHeight = thumbRect(forBounds: bounds, trackRect: trackRect(forBounds: bounds), value: 0).size.height
  422. let thumbPosition = thumbSizeHeight + (percentage * (bounds.size.width - (2 * thumbSizeHeight)))
  423. let touchLocation = touch.location(in: self)
  424. return touchLocation.x <= (thumbPosition + thumbTouchSize.width) && touchLocation.x >= (thumbPosition - thumbTouchSize.width)
  425. }
  426. public func addTapGesture() {
  427. let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
  428. addGestureRecognizer(tap)
  429. }
  430. @objc private func handleTap(_ sender: UITapGestureRecognizer) {
  431. let location = sender.location(in: self)
  432. let percent = minimumValue + Float(location.x / bounds.width) * (maximumValue - minimumValue)
  433. setValue(percent, animated: true)
  434. sendActions(for: .valueChanged)
  435. }
  436. }