NCPlayerToolBar.swift 20 KB

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