NCPlayerToolBar.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  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. 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.isPlay() {
  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 = NCManageDatabase.shared.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. NCManageDatabase.shared.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. self.viewerMediaPage?.present(navigationController, animated: true, completion: nil)
  303. }
  304. }
  305. )
  306. )
  307. viewerMediaPage?.presentMenu(with: actions, menuColor: UIColor(hexString: "#1C1C1EFF"), textColor: .white)
  308. }
  309. func toggleMenuAudio(audioTracks: [Any], audioTrackIndexes: [Any]) {
  310. var actions = [NCMenuAction]()
  311. var audioIndex: Int?
  312. if let data = NCManageDatabase.shared.getVideo(metadata: metadata), let idx = data.currentAudioTrackIndex {
  313. audioIndex = idx
  314. } else if let idx = ncplayer?.player.currentAudioTrackIndex {
  315. audioIndex = Int(idx)
  316. }
  317. if !audioTracks.isEmpty {
  318. for index in 0...audioTracks.count - 1 {
  319. guard let title = audioTracks[index] as? String, let idx = audioTrackIndexes[index] as? Int32, let metadata = self.metadata else { return }
  320. actions.append(
  321. NCMenuAction(
  322. title: title,
  323. icon: UIImage(),
  324. onTitle: title,
  325. onIcon: UIImage(),
  326. selected: (audioIndex ?? -9999) == idx,
  327. on: (audioIndex ?? -9999) == idx,
  328. action: { _ in
  329. self.ncplayer?.player.currentAudioTrackIndex = idx
  330. NCManageDatabase.shared.addVideo(metadata: metadata, currentAudioTrackIndex: Int(idx))
  331. }
  332. )
  333. )
  334. }
  335. actions.append(.seperator(order: 0))
  336. }
  337. actions.append(
  338. NCMenuAction(
  339. title: NSLocalizedString("_add_audio_", comment: ""),
  340. icon: UIImage(),
  341. onTitle: NSLocalizedString("_add_audio_", comment: ""),
  342. onIcon: UIImage(),
  343. selected: false,
  344. on: false,
  345. action: { _ in
  346. guard let metadata = self.metadata else { return }
  347. let storyboard = UIStoryboard(name: "NCSelect", bundle: nil)
  348. if let navigationController = storyboard.instantiateInitialViewController() as? UINavigationController,
  349. let viewController = navigationController.topViewController as? NCSelect {
  350. viewController.delegate = self
  351. viewController.typeOfCommandView = .nothing
  352. viewController.includeDirectoryE2EEncryption = false
  353. viewController.enableSelectFile = true
  354. viewController.type = "audio"
  355. viewController.serverUrl = metadata.serverUrl
  356. self.viewerMediaPage?.present(navigationController, animated: true, completion: nil)
  357. }
  358. }
  359. )
  360. )
  361. viewerMediaPage?.presentMenu(with: actions, menuColor: UIColor(hexString: "#1C1C1EFF"), textColor: .white)
  362. }
  363. }
  364. extension NCPlayerToolBar: NCSelectDelegate {
  365. func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool) {
  366. if let metadata = metadata, let viewerMediaPage = viewerMediaPage {
  367. let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName
  368. let fileNameLocalPath = NCUtilityFileSystem().getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView)
  369. if utilityFileSystem.fileProviderStorageExists(metadata) {
  370. addPlaybackSlave(type: type, metadata: metadata)
  371. } else {
  372. var downloadRequest: DownloadRequest?
  373. hud.indicatorView = JGProgressHUDRingIndicatorView()
  374. hud.textLabel.text = NSLocalizedString("_downloading_", comment: "")
  375. hud.detailTextLabel.text = NSLocalizedString("_tap_to_cancel_", comment: "")
  376. hud.detailTextLabel.textColor = NCBrandColor.shared.iconImageColor2
  377. if let indicatorView = hud.indicatorView as? JGProgressHUDRingIndicatorView {
  378. indicatorView.ringWidth = 1.5
  379. indicatorView.ringColor = NCBrandColor.shared.brandElement
  380. }
  381. hud.tapOnHUDViewBlock = { _ in
  382. if let request = downloadRequest {
  383. request.cancel()
  384. }
  385. }
  386. hud.show(in: viewerMediaPage.view)
  387. NextcloudKit.shared.download(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, requestHandler: { request in
  388. downloadRequest = request
  389. }, taskHandler: { _ in
  390. }, progressHandler: { progress in
  391. self.hud.progress = Float(progress.fractionCompleted)
  392. }) { _, _, _, _, _, _, error in
  393. self.hud.dismiss()
  394. if error == .success {
  395. self.addPlaybackSlave(type: type, metadata: metadata)
  396. } else if error.errorCode != 200 {
  397. NCContentPresenter().showError(error: error)
  398. }
  399. }
  400. }
  401. }
  402. }
  403. // swiftlint:disable inclusive_language
  404. func addPlaybackSlave(type: String, metadata: tableMetadata) {
  405. // swiftlint:enable inclusive_language
  406. let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView)
  407. if type == "subtitle" {
  408. self.ncplayer?.player.addPlaybackSlave(URL(fileURLWithPath: fileNameLocalPath), type: .subtitle, enforce: true)
  409. } else if type == "audio" {
  410. self.ncplayer?.player.addPlaybackSlave(URL(fileURLWithPath: fileNameLocalPath), type: .audio, enforce: true)
  411. }
  412. }
  413. }
  414. // https://stackoverflow.com/questions/13196263/custom-uislider-increase-hot-spot-size
  415. //
  416. class NCPlayerToolBarSlider: UISlider {
  417. private var thumbTouchSize = CGSize(width: 100, height: 100)
  418. override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
  419. let increasedBounds = bounds.insetBy(dx: -thumbTouchSize.width, dy: -thumbTouchSize.height)
  420. let containsPoint = increasedBounds.contains(point)
  421. return containsPoint
  422. }
  423. override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
  424. let percentage = CGFloat((value - minimumValue) / (maximumValue - minimumValue))
  425. let thumbSizeHeight = thumbRect(forBounds: bounds, trackRect: trackRect(forBounds: bounds), value: 0).size.height
  426. let thumbPosition = thumbSizeHeight + (percentage * (bounds.size.width - (2 * thumbSizeHeight)))
  427. let touchLocation = touch.location(in: self)
  428. return touchLocation.x <= (thumbPosition + thumbTouchSize.width) && touchLocation.x >= (thumbPosition - thumbTouchSize.width)
  429. }
  430. public func addTapGesture() {
  431. let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
  432. addGestureRecognizer(tap)
  433. }
  434. @objc private func handleTap(_ sender: UITapGestureRecognizer) {
  435. let location = sender.location(in: self)
  436. let percent = minimumValue + Float(location.x / bounds.width) * (maximumValue - minimumValue)
  437. setValue(percent, animated: true)
  438. sendActions(for: .valueChanged)
  439. }
  440. }