123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526 |
- //
- // NCPlayerToolBar.swift
- // Nextcloud
- //
- // Created by Marino Faggiana on 01/07/21.
- // Copyright © 2021 Marino Faggiana. All rights reserved.
- //
- // Author Marino Faggiana <marino.faggiana@nextcloud.com>
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU General Public License for more details.
- //
- // You should have received a copy of the GNU General Public License
- // along with this program. If not, see <http://www.gnu.org/licenses/>.
- //
- import Foundation
- import NextcloudKit
- import CoreMedia
- import UIKit
- import AVKit
- import MediaPlayer
- import MobileVLCKit
- import FloatingPanel
- import Alamofire
- class NCPlayerToolBar: UIView {
- @IBOutlet weak var utilityView: UIView!
- @IBOutlet weak var fullscreenButton: UIButton!
- @IBOutlet weak var subtitleButton: UIButton!
- @IBOutlet weak var audioButton: UIButton!
- @IBOutlet weak var playerButtonView: UIStackView!
- @IBOutlet weak var backButton: UIButton!
- @IBOutlet weak var playButton: UIButton!
- @IBOutlet weak var forwardButton: UIButton!
- @IBOutlet weak var playbackSliderView: UIView!
- @IBOutlet weak var playbackSlider: NCPlayerToolBarSlider!
- @IBOutlet weak var labelLeftTime: UILabel!
- @IBOutlet weak var labelCurrentTime: UILabel!
- @IBOutlet weak var repeatButton: UIButton!
- enum sliderEventType {
- case began
- case ended
- case moved
- }
- var playbackSliderEvent: sliderEventType = .ended
- var isFullscreen: Bool = false
- var playRepeat: Bool = false
- private let hud = NCHud()
- private var ncplayer: NCPlayer?
- private var metadata: tableMetadata?
- private let audioSession = AVAudioSession.sharedInstance()
- private var pointSize: CGFloat = 0
- private let utilityFileSystem = NCUtilityFileSystem()
- private let utility = NCUtility()
- private let database = NCManageDatabase.shared
- private weak var viewerMediaPage: NCViewerMediaPage?
- private var buttonImage = UIImage()
- // MARK: - View Life Cycle
- override func awakeFromNib() {
- super.awakeFromNib()
- self.backgroundColor = UIColor.black.withAlphaComponent(0.1)
- fullscreenButton.setImage(utility.loadImage(named: "arrow.up.left.and.arrow.down.right", colors: [.white]), for: .normal)
- subtitleButton.setImage(utility.loadImage(named: "captions.bubble", colors: [.white]), for: .normal)
- subtitleButton.isEnabled = false
- audioButton.setImage(utility.loadImage(named: "speaker.zzz", colors: [.white]), for: .normal)
- audioButton.isEnabled = false
- if UIDevice.current.userInterfaceIdiom == .pad {
- pointSize = 60
- } else {
- pointSize = 50
- }
- playerButtonView.spacing = pointSize
- playerButtonView.isHidden = true
- buttonImage = UIImage(systemName: "gobackward.10", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal)
- backButton.setImage(buttonImage, for: .normal)
- buttonImage = UIImage(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal)
- playButton.setImage(buttonImage, for: .normal)
- buttonImage = UIImage(systemName: "goforward.10", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal)
- forwardButton.setImage(buttonImage, for: .normal)
- playbackSlider.addTapGesture()
- playbackSlider.setThumbImage(UIImage(systemName: "circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15)), for: .normal)
- playbackSlider.value = 0
- playbackSlider.tintColor = .white
- playbackSlider.addTarget(self, action: #selector(playbackValChanged(slider:event:)), for: .valueChanged)
- repeatButton.setImage(utility.loadImage(named: "repeat", colors: [NCBrandColor.shared.iconImageColor2]), for: .normal)
- utilityView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap(gestureRecognizer:))))
- playbackSliderView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap(gestureRecognizer:))))
- playbackSliderView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(tap(gestureRecognizer:))))
- playerButtonView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap(gestureRecognizer:))))
- labelCurrentTime.textColor = .white
- labelLeftTime.textColor = .white
- // Normally hide
- self.alpha = 0
- self.isHidden = true
- }
- required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- }
- deinit {
- print("deinit NCPlayerToolBar")
- }
- // MARK: -
- func setBarPlayer(position: Float, ncplayer: NCPlayer? = nil, metadata: tableMetadata? = nil, viewerMediaPage: NCViewerMediaPage? = nil) {
- if let ncplayer = ncplayer {
- self.ncplayer = ncplayer
- }
- if let metadata = metadata {
- self.metadata = metadata
- }
- if let viewerMediaPage = viewerMediaPage {
- self.viewerMediaPage = viewerMediaPage
- }
- playerButtonView.isHidden = true
- buttonImage = UIImage(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal)
- playButton.setImage(buttonImage, for: .normal)
- playbackSlider.value = position
- labelCurrentTime.text = "--:--"
- labelLeftTime.text = "--:--"
- if viewerMediaScreenMode == .normal {
- show()
- } else {
- hide()
- }
- MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = position
- }
- public func update() {
- guard let ncplayer = self.ncplayer, let length = ncplayer.player.media?.length.intValue else { return }
- let position = ncplayer.player.position
- let positionInSecond = position * Float(length / 1000)
- // SLIDER & TIME
- if playbackSliderEvent == .ended {
- playbackSlider.value = position
- }
- labelCurrentTime.text = ncplayer.player.time.stringValue
- labelLeftTime.text = ncplayer.player.remainingTime?.stringValue
- MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyPlaybackDuration] = length / 1000
- MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = positionInSecond
- }
- public func updateTopToolBar(videoSubTitlesIndexes: [Any], audioTrackIndexes: [Any]) {
- if let metadata = metadata, metadata.isVideo {
- self.subtitleButton.isEnabled = true
- self.audioButton.isEnabled = true
- }
- }
- // MARK: -
- public func show() {
- UIView.animate(withDuration: 0.5, animations: {
- self.alpha = 1
- }, completion: { (_: Bool) in
- self.isHidden = false
- })
- }
- func hide() {
- UIView.animate(withDuration: 0.5, animations: {
- self.alpha = 0
- }, completion: { (_: Bool) in
- self.isHidden = true
- })
- }
- func playButtonPause() {
- buttonImage = UIImage(systemName: "pause.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal)
- playButton.setImage(buttonImage, for: .normal)
- MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 1
- }
- func playButtonPlay() {
- buttonImage = UIImage(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))!.withTintColor(.white, renderingMode: .alwaysOriginal)
- playButton.setImage(buttonImage, for: .normal)
- MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 0
- }
- // MARK: - Event / Gesture
- @objc func playbackValChanged(slider: UISlider, event: UIEvent) {
- guard let ncplayer = ncplayer else { return }
- let newPosition = playbackSlider.value
- if let touchEvent = event.allTouches?.first {
- switch touchEvent.phase {
- case .began:
- viewerMediaPage?.timerAutoHide?.invalidate()
- playbackSliderEvent = .began
- case .moved:
- ncplayer.playerPosition(newPosition)
- playbackSliderEvent = .moved
- case .ended:
- ncplayer.playerPosition(newPosition)
- DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
- self.playbackSliderEvent = .ended
- self.viewerMediaPage?.startTimerAutoHide()
- }
- default:
- break
- }
- } else {
- ncplayer.playerPosition(newPosition)
- self.viewerMediaPage?.startTimerAutoHide()
- }
- }
- // MARK: - Action
- @objc func tap(gestureRecognizer: UITapGestureRecognizer) { }
- @IBAction func tapFullscreen(_ sender: Any) {
- isFullscreen = !isFullscreen
- if isFullscreen {
- fullscreenButton.setImage(utility.loadImage(named: "arrow.down.right.and.arrow.up.left", colors: [.white]), for: .normal)
- } else {
- fullscreenButton.setImage(utility.loadImage(named: "arrow.up.left.and.arrow.down.right", colors: [.white]), for: .normal)
- }
- viewerMediaPage?.changeScreenMode(mode: viewerMediaScreenMode)
- }
- @IBAction func tapSubTitle(_ sender: Any) {
- guard let player = ncplayer?.player else { return }
- let spuTracks = player.videoSubTitlesNames
- let spuTrackIndexes = player.videoSubTitlesIndexes
- toggleMenuSubTitle(spuTracks: spuTracks, spuTrackIndexes: spuTrackIndexes)
- }
- @IBAction func tapAudio(_ sender: Any) {
- guard let player = ncplayer?.player else { return }
- let audioTracks = player.audioTrackNames
- let audioTrackIndexes = player.audioTrackIndexes
- toggleMenuAudio(audioTracks: audioTracks, audioTrackIndexes: audioTrackIndexes)
- }
- @IBAction func tapPlayerPause(_ sender: Any) {
- guard let ncplayer = ncplayer else { return }
- if ncplayer.isPlaying() {
- ncplayer.playerPause()
- } else {
- ncplayer.playerPlay()
- }
- self.viewerMediaPage?.startTimerAutoHide()
- }
- @IBAction func tapForward(_ sender: Any) {
- guard let ncplayer = ncplayer else { return }
- ncplayer.jumpForward(10)
- self.viewerMediaPage?.startTimerAutoHide()
- }
- @IBAction func tapBack(_ sender: Any) {
- guard let ncplayer = ncplayer else { return }
- ncplayer.jumpBackward(10)
- self.viewerMediaPage?.startTimerAutoHide()
- }
- @IBAction func tapRepeat(_ sender: Any) {
- if playRepeat {
- playRepeat = false
- repeatButton.setImage(utility.loadImage(named: "repeat", colors: [NCBrandColor.shared.iconImageColor2]), for: .normal)
- } else {
- playRepeat = true
- repeatButton.setImage(utility.loadImage(named: "repeat", colors: [.white]), for: .normal)
- }
- }
- }
- extension NCPlayerToolBar {
- func toggleMenuSubTitle(spuTracks: [Any], spuTrackIndexes: [Any]) {
- var actions = [NCMenuAction]()
- var subTitleIndex: Int?
- if let data = self.database.getVideo(metadata: metadata), let idx = data.currentVideoSubTitleIndex {
- subTitleIndex = idx
- } else if let idx = ncplayer?.player.currentVideoSubTitleIndex {
- subTitleIndex = Int(idx)
- }
- if !spuTracks.isEmpty {
- for index in 0...spuTracks.count - 1 {
- guard let title = spuTracks[index] as? String, let idx = spuTrackIndexes[index] as? Int32, let metadata = self.metadata else { return }
- actions.append(
- NCMenuAction(
- title: title,
- icon: UIImage(),
- onTitle: title,
- onIcon: UIImage(),
- selected: (subTitleIndex ?? -9999) == idx,
- on: (subTitleIndex ?? -9999) == idx,
- action: { _ in
- self.ncplayer?.player.currentVideoSubTitleIndex = idx
- self.database.addVideo(metadata: metadata, currentVideoSubTitleIndex: Int(idx))
- }
- )
- )
- }
- actions.append(.seperator(order: 0))
- }
- actions.append(
- NCMenuAction(
- title: NSLocalizedString("_add_subtitle_", comment: ""),
- icon: UIImage(),
- onTitle: NSLocalizedString("_add_subtitle_", comment: ""),
- onIcon: UIImage(),
- selected: false,
- on: false,
- action: { _ in
- guard let metadata = self.metadata else { return }
- let storyboard = UIStoryboard(name: "NCSelect", bundle: nil)
- if let navigationController = storyboard.instantiateInitialViewController() as? UINavigationController,
- let viewController = navigationController.topViewController as? NCSelect {
- viewController.delegate = self
- viewController.typeOfCommandView = .nothing
- viewController.includeDirectoryE2EEncryption = false
- viewController.enableSelectFile = true
- viewController.type = "subtitle"
- viewController.serverUrl = metadata.serverUrl
- viewController.session = NCSession.shared.getSession(account: metadata.account)
- self.viewerMediaPage?.present(navigationController, animated: true, completion: nil)
- }
- }
- )
- )
- viewerMediaPage?.presentMenu(with: actions, menuColor: UIColor(hexString: "#1C1C1EFF"), textColor: .white)
- }
- func toggleMenuAudio(audioTracks: [Any], audioTrackIndexes: [Any]) {
- var actions = [NCMenuAction]()
- var audioIndex: Int?
- if let data = self.database.getVideo(metadata: metadata), let idx = data.currentAudioTrackIndex {
- audioIndex = idx
- } else if let idx = ncplayer?.player.currentAudioTrackIndex {
- audioIndex = Int(idx)
- }
- if !audioTracks.isEmpty {
- for index in 0...audioTracks.count - 1 {
- guard let title = audioTracks[index] as? String, let idx = audioTrackIndexes[index] as? Int32, let metadata = self.metadata else { return }
- actions.append(
- NCMenuAction(
- title: title,
- icon: UIImage(),
- onTitle: title,
- onIcon: UIImage(),
- selected: (audioIndex ?? -9999) == idx,
- on: (audioIndex ?? -9999) == idx,
- action: { _ in
- self.ncplayer?.player.currentAudioTrackIndex = idx
- self.database.addVideo(metadata: metadata, currentAudioTrackIndex: Int(idx))
- }
- )
- )
- }
- actions.append(.seperator(order: 0))
- }
- actions.append(
- NCMenuAction(
- title: NSLocalizedString("_add_audio_", comment: ""),
- icon: UIImage(),
- onTitle: NSLocalizedString("_add_audio_", comment: ""),
- onIcon: UIImage(),
- selected: false,
- on: false,
- action: { _ in
- guard let metadata = self.metadata else { return }
- let storyboard = UIStoryboard(name: "NCSelect", bundle: nil)
- if let navigationController = storyboard.instantiateInitialViewController() as? UINavigationController,
- let viewController = navigationController.topViewController as? NCSelect {
- viewController.delegate = self
- viewController.typeOfCommandView = .nothing
- viewController.includeDirectoryE2EEncryption = false
- viewController.enableSelectFile = true
- viewController.type = "audio"
- viewController.serverUrl = metadata.serverUrl
- viewController.session = NCSession.shared.getSession(account: metadata.account)
- self.viewerMediaPage?.present(navigationController, animated: true, completion: nil)
- }
- }
- )
- )
- viewerMediaPage?.presentMenu(with: actions, menuColor: UIColor(hexString: "#1C1C1EFF"), textColor: .white)
- }
- }
- extension NCPlayerToolBar: NCSelectDelegate {
- func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool, session: NCSession.Session) {
- if let metadata = metadata, let viewerMediaPage = viewerMediaPage {
- let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName
- let fileNameLocalPath = NCUtilityFileSystem().getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView)
- if utilityFileSystem.fileProviderStorageExists(metadata) {
- addPlaybackSlave(type: type, metadata: metadata)
- } else {
- var downloadRequest: DownloadRequest?
- hud.initHudRing(view: viewerMediaPage.view,
- text: NSLocalizedString("_downloading_", comment: ""),
- tapToCancelDetailText: true) {
- if let request = downloadRequest {
- request.cancel()
- }
- }
- NextcloudKit.shared.download(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: metadata.account, requestHandler: { request in
- downloadRequest = request
- }, taskHandler: { _ in
- }, progressHandler: { progress in
- self.hud.progress(progress.fractionCompleted)
- }) { _, _, _, _, _, _, error in
- self.hud.dismiss()
- if error == .success {
- self.hud.success()
- self.addPlaybackSlave(type: type, metadata: metadata)
- } else if error.errorCode != 200 {
- self.hud.error(text: error.errorDescription)
- }
- }
- }
- }
- }
- // swiftlint:disable inclusive_language
- func addPlaybackSlave(type: String, metadata: tableMetadata) {
- // swiftlint:enable inclusive_language
- let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView)
- if type == "subtitle" {
- self.ncplayer?.player.addPlaybackSlave(URL(fileURLWithPath: fileNameLocalPath), type: .subtitle, enforce: true)
- } else if type == "audio" {
- self.ncplayer?.player.addPlaybackSlave(URL(fileURLWithPath: fileNameLocalPath), type: .audio, enforce: true)
- }
- }
- }
- // https://stackoverflow.com/questions/13196263/custom-uislider-increase-hot-spot-size
- //
- class NCPlayerToolBarSlider: UISlider {
- private var thumbTouchSize = CGSize(width: 100, height: 100)
- override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
- let increasedBounds = bounds.insetBy(dx: -thumbTouchSize.width, dy: -thumbTouchSize.height)
- let containsPoint = increasedBounds.contains(point)
- return containsPoint
- }
- override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
- let percentage = CGFloat((value - minimumValue) / (maximumValue - minimumValue))
- let thumbSizeHeight = thumbRect(forBounds: bounds, trackRect: trackRect(forBounds: bounds), value: 0).size.height
- let thumbPosition = thumbSizeHeight + (percentage * (bounds.size.width - (2 * thumbSizeHeight)))
- let touchLocation = touch.location(in: self)
- return touchLocation.x <= (thumbPosition + thumbTouchSize.width) && touchLocation.x >= (thumbPosition - thumbTouchSize.width)
- }
- public func addTapGesture() {
- let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
- addGestureRecognizer(tap)
- }
- @objc private func handleTap(_ sender: UITapGestureRecognizer) {
- let location = sender.location(in: self)
- let percent = minimumValue + Float(location.x / bounds.width) * (maximumValue - minimumValue)
- setValue(percent, animated: true)
- sendActions(for: .valueChanged)
- }
- }
|