NCPlayerToolBar.swift 21 KB

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