123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- //
- // SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
- // SPDX-License-Identifier: GPL-3.0-or-later
- //
- import Foundation
- import UIKit
- import MobileVLCKit
- @objc protocol VLCKitVideoViewControllerDelegate {
- @objc func vlckitVideoViewControllerDismissed(_ controller: VLCKitVideoViewController)
- }
- @objcMembers class VLCKitVideoViewController: UIViewController, VLCMediaPlayerDelegate {
- public weak var delegate: VLCKitVideoViewControllerDelegate?
- public static let supportedFileExtensions = ["webm", "mkv"]
- @IBOutlet weak var videoViewContainer: NCZoomableView!
- @IBOutlet weak var buttonView: UIStackView!
- @IBOutlet weak var jumpBackButton: UIButton!
- @IBOutlet weak var playPauseButton: UIButton!
- @IBOutlet weak var jumpForwardButton: UIButton!
- @IBOutlet weak var shareButton: UIButton!
- @IBOutlet weak var closeButton: UIButton!
- @IBOutlet weak var currentTimeLabel: UILabel!
- @IBOutlet weak var totalTimeLabel: UILabel!
- @IBOutlet weak var positionSlider: UISlider!
- private static let jumpInterval: Int32 = 10
- private var mediaPlayer: VLCMediaPlayer?
- private var filePath: String
- private var setPosition: Bool = false
- private var timeObserver: NSKeyValueObservation?
- private var remainingTimeObserver: NSKeyValueObservation?
- private var sliderValueObserver: NSKeyValueObservation?
- private var idleTimer: Timer?
- private var pauseAfterPlay: Bool = false
- private var videoView = UIView()
- init(filePath: String) {
- self.filePath = filePath
- super.init(nibName: "VLCKitVideoViewController", bundle: nil)
- }
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- override func viewDidLoad() {
- self.mediaPlayer = VLCMediaPlayer()
- self.mediaPlayer?.delegate = self
- self.timeObserver = self.mediaPlayer?.observe(\.time, changeHandler: { _, _ in
- self.updateInformation()
- })
- self.remainingTimeObserver = self.mediaPlayer?.observe(\.remainingTime, changeHandler: { _, _ in
- self.updateInformation()
- })
- self.sliderValueObserver = self.positionSlider?.observe(\.value, changeHandler: { _, _ in
- // Make sure the slider is filled to 100% at value 1 since we hid the thumb
- if self.positionSlider.value == 1 {
- self.positionSlider.maximumTrackTintColor = .systemBlue
- } else {
- self.positionSlider.maximumTrackTintColor = .none
- }
- })
- self.currentTimeLabel.text = "--:--"
- self.totalTimeLabel.text = "--:--"
- self.positionSlider.value = 0
- // Set close button icon as template
- self.closeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
- }
- @objc private func dismissViewController() {
- self.dismiss(animated: true)
- }
- override func viewDidLayoutSubviews() {
- super.viewDidLayoutSubviews()
- self.buttonView.layer.cornerRadius = self.buttonView.frame.size.height / 2
- self.closeButton.layer.cornerRadius = self.closeButton.frame.size.height / 2
- }
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- // Set the inital frame size when the view appeared
- self.videoView.frame = self.videoViewContainer.frame
- self.videoViewContainer.replaceContentView(self.videoView)
- self.mediaPlayer?.drawable = self.videoView
- // Allow toggle of controls
- let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggleControls))
- self.view.isUserInteractionEnabled = true
- self.view.addGestureRecognizer(tapGesture)
- self.videoView.addGestureRecognizer(tapGesture)
- // Make sure our singel tap recognizer does not interfere with the double tap recognizer of NCZoomableView
- if let doubleTapGestureRecognizer = self.videoViewContainer.doubleTapGestureRecoginzer {
- tapGesture.require(toFail: doubleTapGestureRecognizer)
- }
- self.resetMedia(drawFirstFrame: true)
- }
- override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- self.timeObserver?.invalidate()
- self.remainingTimeObserver?.invalidate()
- self.sliderValueObserver?.invalidate()
- self.mediaPlayer?.stop()
- }
- override func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
- self.delegate?.vlckitVideoViewControllerDismissed(self)
- }
- override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
- super.viewWillTransition(to: size, with: coordinator)
- guard let mediaPlayer = self.mediaPlayer else { return }
- // Since there's no way to redraw the current frame in VLCKit, we hide the view
- // if the playback was stopped to not have a disorted view
- if !mediaPlayer.isPlaying, self.mediaReachedEnd() {
- self.videoViewContainer.isHidden = true
- }
- coordinator.animate(alongsideTransition: nil) { _ in
- self.videoView.frame = self.videoViewContainer.frame
- self.videoViewContainer.contentViewSize = self.videoViewContainer.frame.size
- self.videoViewContainer.resizeContentView()
- }
- }
- override var preferredStatusBarStyle: UIStatusBarStyle {
- return .lightContent
- }
- private func updateInformation() {
- self.positionSlider.value = self.mediaPlayer?.position ?? 0
- if let remainingTime = self.mediaPlayer?.remainingTime, let currentTime = self.mediaPlayer?.time {
- self.currentTimeLabel.text = currentTime.stringValue
- self.totalTimeLabel.text = VLCTime(int: abs(currentTime.intValue) + abs(remainingTime.intValue)).stringValue
- } else {
- self.currentTimeLabel.text = "-:-"
- self.totalTimeLabel.text = "-:-"
- }
- if let mediaPlayer, mediaPlayer.isSeekable {
- jumpBackButton.isEnabled = true
- jumpForwardButton.isEnabled = true
- } else {
- jumpBackButton.isEnabled = false
- jumpForwardButton.isEnabled = false
- }
- }
- private func resetMedia(drawFirstFrame: Bool) {
- self.mediaPlayer?.media = VLCMedia(path: self.filePath)
- if drawFirstFrame {
- self.pauseAfterPlay = true
- self.mediaPlayer?.play()
- }
- }
- private func mediaReachedEnd() -> Bool {
- guard let mediaPlayer = self.mediaPlayer else { return false }
- return mediaPlayer.remainingTime?.stringValue == "00:00"
- }
- // MARK: Controls Visibility
- private func updateIdleTimer() {
- self.idleTimer?.invalidate()
- self.idleTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { _ in
- self.idleTimer = nil
- guard let mediaPlayer = self.mediaPlayer else { return }
- // Only hide the controls if we're playing something
- if mediaPlayer.isPlaying {
- self.hideControls()
- }
- })
- }
- @objc private func toggleControls() {
- if self.buttonView.alpha == 1 {
- self.setControlsAlpha(alpha: 0)
- } else {
- self.setControlsAlpha(alpha: 1)
- self.updateIdleTimer()
- }
- }
- private func showControls() {
- self.setControlsAlpha(alpha: 1)
- }
- private func hideControls() {
- self.setControlsAlpha(alpha: 0)
- }
- private func setControlsAlpha(alpha: CGFloat) {
- UIView.animate(withDuration: 0.3) {
- self.buttonView.alpha = alpha
- self.positionSlider.alpha = alpha
- self.totalTimeLabel.alpha = alpha
- self.currentTimeLabel.alpha = alpha
- self.closeButton.alpha = alpha
- }
- }
- // MARK: InterfaceBuilder Actions
- @IBAction func jumpBackButtonTap(_ sender: Any) {
- guard let mediaPlayer = self.mediaPlayer else { return }
- mediaPlayer.jumpBackward(Self.jumpInterval)
- updateInformation()
- updateIdleTimer()
- }
- @IBAction func jumpForwardButtonTap(_ sender: Any) {
- guard let mediaPlayer = self.mediaPlayer else { return }
- // VLCKit does not clamp to the media end, if the jump is beyond its length.
- // Hence the destination must be calculated manually to mimic first party video playback views.
- if let remainingTime = mediaPlayer.remainingTime {
- let remainingSeconds = abs(remainingTime.intValue) / 1000
- if remainingSeconds >= Self.jumpInterval {
- mediaPlayer.jumpForward(Self.jumpInterval)
- } else {
- mediaPlayer.jumpForward(remainingSeconds)
- }
- }
- updateInformation()
- updateIdleTimer()
- }
- @IBAction func playPauseButtonTap(_ sender: Any) {
- guard let mediaPlayer = self.mediaPlayer else { return }
- if mediaPlayer.isPlaying {
- mediaPlayer.pause()
- } else {
- // In VLCKit 3.3.17 the position parameter is not working correctly (fixed in 4.0.0)
- // see https://code.videolan.org/videolan/VLCKit/-/issues/583
- if self.mediaReachedEnd() {
- // When we reached the end of the media, start from the beginning again
- self.resetMedia(drawFirstFrame: false)
- }
- mediaPlayer.play()
- }
- self.updateIdleTimer()
- }
- @IBAction func shareButtonTap(_ sender: Any) {
- let activityItem = NSURL(fileURLWithPath: filePath)
- let activityVC = UIActivityViewController(activityItems: [activityItem], applicationActivities: nil)
- self.present(activityVC, animated: true, completion: nil)
- self.updateIdleTimer()
- }
- @IBAction func positionSliderAction(_ sender: Any) {
- // From the example of VLCKit we should limit the number of events to make sure,
- // the user actually sees I-frames when seeking
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
- guard let mediaPlayer = self.mediaPlayer else { return }
- if !self.setPosition {
- self.setPosition = false
- // When the media is in state "ended"/"stopped" we can't seek anymore, so we need to reset the media
- if self.mediaReachedEnd() {
- self.resetMedia(drawFirstFrame: true)
- }
- mediaPlayer.position = self.positionSlider.value
- self.updateIdleTimer()
- }
- }
- }
- @IBAction func closeButtonTap(_ sender: Any) {
- self.dismissViewController()
- }
- // MARK: VLCMediaPlayerDelegate
- func mediaPlayerStateChanged(_ aNotification: Notification!) {
- guard let mediaPlayer = self.mediaPlayer else { return }
- if mediaPlayer.isPlaying {
- // When state changed to playing because we reseted the stream and
- // started playing to load it, we want to pause it here again, as it wasn't playing before
- if self.pauseAfterPlay {
- self.pauseAfterPlay = false
- mediaPlayer.pause()
- return
- }
- self.playPauseButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
- self.videoViewContainer.isHidden = false
- } else {
- self.playPauseButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
- self.showControls()
- // Check if we reached the end, although maybe not reported by the position (fixed in 4.0.0)
- if self.mediaReachedEnd() {
- self.positionSlider.value = 1
- }
- }
- }
- }
|