mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			202 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Swift
		
	
		
		
			
		
	
	
			202 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Swift
		
	
| 
								 
											8 years ago
										 
									 | 
							
								//
							 | 
						||
| 
								 | 
							
								//  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
							 | 
						||
| 
								 | 
							
								//
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import Foundation
							 | 
						||
| 
								 
											8 years ago
										 
									 | 
							
								import AVFoundation
							 | 
						||
| 
								 
											8 years ago
										 
									 | 
							
								
							 | 
						||
| 
								 | 
							
								@available(iOS 9.0, *)
							 | 
						||
| 
								 | 
							
								@objc
							 | 
						||
| 
								 | 
							
								public class VideoPlayerView: UIView {
							 | 
						||
| 
								 | 
							
								    var player: AVPlayer? {
							 | 
						||
| 
								 | 
							
								        get {
							 | 
						||
| 
								 | 
							
								            return playerLayer.player
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        set {
							 | 
						||
| 
								 | 
							
								            playerLayer.player = newValue
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    var playerLayer: AVPlayerLayer {
							 | 
						||
| 
								 | 
							
								        return layer as! AVPlayerLayer
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // Override UIView property
							 | 
						||
| 
								 | 
							
								    override public static var layerClass: AnyClass {
							 | 
						||
| 
								 | 
							
								        return AVPlayerLayer.self
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								@available(iOS 9.0, *)
							 | 
						||
| 
								 | 
							
								@objc
							 | 
						||
| 
								 | 
							
								public protocol PlayerProgressBarDelegate {
							 | 
						||
| 
								 | 
							
								    func playerProgressBarDidStartScrubbing(_ playerProgressBar: PlayerProgressBar)
							 | 
						||
| 
								 | 
							
								    func playerProgressBar(_ playerProgressBar: PlayerProgressBar, scrubbedToTime time: CMTime)
							 | 
						||
| 
								 | 
							
								    func playerProgressBar(_ playerProgressBar: PlayerProgressBar, didFinishScrubbingAtTime time: CMTime, shouldResumePlayback: Bool)
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								@available(iOS 9.0, *)
							 | 
						||
| 
								 | 
							
								@objc
							 | 
						||
| 
								 | 
							
								public class PlayerProgressBar: UIView {
							 | 
						||
| 
								 | 
							
								    public let TAG = "[PlayerProgressBar]"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @objc
							 | 
						||
| 
								 | 
							
								    public weak var delegate: PlayerProgressBarDelegate?
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private lazy var formatter: DateComponentsFormatter = {
							 | 
						||
| 
								 | 
							
								        let formatter = DateComponentsFormatter()
							 | 
						||
| 
								 | 
							
								        formatter.unitsStyle = .positional
							 | 
						||
| 
								 | 
							
								        formatter.allowedUnits = [.minute, .second ]
							 | 
						||
| 
								 | 
							
								        formatter.zeroFormattingBehavior = [ .pad ]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return formatter
							 | 
						||
| 
								 | 
							
								    }()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // MARK: Subviews
							 | 
						||
| 
								 | 
							
								    private let positionLabel = UILabel()
							 | 
						||
| 
								 | 
							
								    private let remainingLabel = UILabel()
							 | 
						||
| 
								 | 
							
								    private let slider = UISlider()
							 | 
						||
| 
								 | 
							
								    private let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
							 | 
						||
| 
								 | 
							
								    weak private var progressObserver: AnyObject?
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private let kPreferredTimeScale: CMTimeScale = 100
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    public var player: AVPlayer? {
							 | 
						||
| 
								 | 
							
								        didSet {
							 | 
						||
| 
								 | 
							
								            guard let item = player?.currentItem else {
							 | 
						||
| 
								 | 
							
								                owsFail("No player item")
							 | 
						||
| 
								 | 
							
								                return
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            slider.minimumValue = 0
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            let duration: CMTime = item.asset.duration
							 | 
						||
| 
								 | 
							
								            slider.maximumValue = Float(CMTimeGetSeconds(duration))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            // OPTIMIZE We need a high frequency observer for smooth slider updates,
							 | 
						||
| 
								 | 
							
								            // but could use a much less frequent observer for label updates
							 | 
						||
| 
								 | 
							
								            progressObserver = player?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: kPreferredTimeScale), queue: nil, using: { [weak self] (_) in
							 | 
						||
| 
								 | 
							
								                self?.updateState()
							 | 
						||
| 
								 | 
							
								            }) as AnyObject
							 | 
						||
| 
								 | 
							
								            updateState()
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    required public init?(coder aDecoder: NSCoder) {
							 | 
						||
| 
								 | 
							
								        fatalError("init(coder:) has not been implemented")
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    override public init(frame: CGRect) {
							 | 
						||
| 
								 | 
							
								        super.init(frame: frame)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        // Background
							 | 
						||
| 
								 | 
							
								        backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
							 | 
						||
| 
								 | 
							
								        if !UIAccessibilityIsReduceTransparencyEnabled() {
							 | 
						||
| 
								 | 
							
								            addSubview(blurEffectView)
							 | 
						||
| 
								 | 
							
								            blurEffectView.autoPinToSuperviewEdges()
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        // Configure controls
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        let kLabelFont = UIFont.monospacedDigitSystemFont(ofSize: 12, weight: UIFontWeightRegular)
							 | 
						||
| 
								 | 
							
								        positionLabel.font = kLabelFont
							 | 
						||
| 
								 | 
							
								        remainingLabel.font = kLabelFont
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        // We use a smaller thumb for the progress slider.
							 | 
						||
| 
								 | 
							
								        slider.setThumbImage(#imageLiteral(resourceName: "sliderProgressThumb"), for: .normal)
							 | 
						||
| 
								 
											8 years ago
										 
									 | 
							
								        slider.maximumTrackTintColor = UIColor.ows_black
							 | 
						||
| 
								 | 
							
								        slider.minimumTrackTintColor = UIColor.ows_black
							 | 
						||
| 
								 
											8 years ago
										 
									 | 
							
								
							 | 
						||
| 
								 | 
							
								        slider.addTarget(self, action: #selector(handleSliderTouchDown), for: .touchDown)
							 | 
						||
| 
								 | 
							
								        slider.addTarget(self, action: #selector(handleSliderTouchUp), for: .touchUpInside)
							 | 
						||
| 
								 | 
							
								        slider.addTarget(self, action: #selector(handleSliderTouchUp), for: .touchUpOutside)
							 | 
						||
| 
								 | 
							
								        slider.addTarget(self, action: #selector(handleSliderValueChanged), for: .valueChanged)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        // Layout Subviews
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        addSubview(positionLabel)
							 | 
						||
| 
								 | 
							
								        addSubview(remainingLabel)
							 | 
						||
| 
								 | 
							
								        addSubview(slider)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        positionLabel.autoPinEdge(toSuperviewMargin: .leading)
							 | 
						||
| 
								 | 
							
								        positionLabel.autoVCenterInSuperview()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        let kSliderMargin: CGFloat = 8
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        slider.autoPinEdge(.leading, to: .trailing, of: positionLabel, withOffset: kSliderMargin)
							 | 
						||
| 
								 | 
							
								        slider.autoVCenterInSuperview()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        remainingLabel.autoPinEdge(.leading, to: .trailing, of: slider, withOffset: kSliderMargin)
							 | 
						||
| 
								 | 
							
								        remainingLabel.autoPinEdge(toSuperviewMargin: .trailing)
							 | 
						||
| 
								 | 
							
								        remainingLabel.autoVCenterInSuperview()
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // MARK: Gesture handling
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    var wasPlayingWhenScrubbingStarted: Bool = false
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @objc
							 | 
						||
| 
								 | 
							
								    private func handleSliderTouchDown(_ slider: UISlider) {
							 | 
						||
| 
								 | 
							
								        guard let player = self.player else {
							 | 
						||
| 
								 | 
							
								            owsFail("player was nil")
							 | 
						||
| 
								 | 
							
								            return
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self.wasPlayingWhenScrubbingStarted = (player.rate != 0) && (player.error == nil)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self.delegate?.playerProgressBarDidStartScrubbing(self)
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @objc
							 | 
						||
| 
								 | 
							
								    private func handleSliderTouchUp(_ slider: UISlider) {
							 | 
						||
| 
								 | 
							
								        let sliderTime = time(slider: slider)
							 | 
						||
| 
								 | 
							
								        self.delegate?.playerProgressBar(self, didFinishScrubbingAtTime: sliderTime, shouldResumePlayback:wasPlayingWhenScrubbingStarted)
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @objc
							 | 
						||
| 
								 | 
							
								    private func handleSliderValueChanged(_ slider: UISlider) {
							 | 
						||
| 
								 | 
							
								        let sliderTime = time(slider: slider)
							 | 
						||
| 
								 | 
							
								        self.delegate?.playerProgressBar(self, scrubbedToTime: sliderTime)
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // MARK: Render cycle
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private func updateState() {
							 | 
						||
| 
								 | 
							
								        guard let player = player else {
							 | 
						||
| 
								 | 
							
								            owsFail("\(TAG) player isn't set.")
							 | 
						||
| 
								 | 
							
								            return
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        guard let item = player.currentItem else {
							 | 
						||
| 
								 | 
							
								            owsFail("\(TAG) player has no item.")
							 | 
						||
| 
								 | 
							
								            return
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        let position = player.currentTime()
							 | 
						||
| 
								 | 
							
								        let positionSeconds: Float64 = CMTimeGetSeconds(position)
							 | 
						||
| 
								 | 
							
								        positionLabel.text = formatter.string(from: positionSeconds)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        let duration: CMTime = item.asset.duration
							 | 
						||
| 
								 | 
							
								        let remainingTime = duration - position
							 | 
						||
| 
								 | 
							
								        let remainingSeconds = CMTimeGetSeconds(remainingTime)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        guard let remainingString = formatter.string(from: remainingSeconds) else {
							 | 
						||
| 
								 | 
							
								            owsFail("unable to format time remaining")
							 | 
						||
| 
								 | 
							
								            remainingLabel.text = "0:00"
							 | 
						||
| 
								 | 
							
								            return
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        // show remaining time as negative
							 | 
						||
| 
								 | 
							
								        remainingLabel.text = "-\(remainingString)"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        slider.setValue(Float(positionSeconds), animated: false)
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // MARK: Util
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private func time(slider: UISlider) -> CMTime {
							 | 
						||
| 
								 | 
							
								        let seconds: Double = Double(slider.value)
							 | 
						||
| 
								 | 
							
								        return CMTime(seconds: seconds, preferredTimescale: kPreferredTimeScale)
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								}
							 |