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.
		
		
		
		
		
			
		
			
				
	
	
		
			223 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			223 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Swift
		
	
//
 | 
						|
//  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
 | 
						|
//
 | 
						|
 | 
						|
import Foundation
 | 
						|
import AVFoundation
 | 
						|
 | 
						|
@objc
 | 
						|
public class VideoPlayerView: UIView {
 | 
						|
    @objc
 | 
						|
    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
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
@objc
 | 
						|
public protocol PlayerProgressBarDelegate {
 | 
						|
    func playerProgressBarDidStartScrubbing(_ playerProgressBar: PlayerProgressBar)
 | 
						|
    func playerProgressBar(_ playerProgressBar: PlayerProgressBar, scrubbedToTime time: CMTime)
 | 
						|
    func playerProgressBar(_ playerProgressBar: PlayerProgressBar, didFinishScrubbingAtTime time: CMTime, shouldResumePlayback: Bool)
 | 
						|
}
 | 
						|
 | 
						|
// Allows the user to tap anywhere on the slider to set it's position,
 | 
						|
// without first having to grab the thumb.
 | 
						|
class TrackingSlider: UISlider {
 | 
						|
 | 
						|
    override init(frame: CGRect) {
 | 
						|
        super.init(frame: frame)
 | 
						|
    }
 | 
						|
 | 
						|
    override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
 | 
						|
        return true
 | 
						|
    }
 | 
						|
 | 
						|
    required init?(coder aDecoder: NSCoder) {
 | 
						|
        notImplemented()
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
@objc
 | 
						|
public class PlayerProgressBar: UIView {
 | 
						|
 | 
						|
    @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 = TrackingSlider()
 | 
						|
    private let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
 | 
						|
    weak private var progressObserver: AnyObject?
 | 
						|
 | 
						|
    private let kPreferredTimeScale: CMTimeScale = 100
 | 
						|
 | 
						|
    @objc
 | 
						|
    public var player: AVPlayer? {
 | 
						|
        didSet {
 | 
						|
            guard let item = player?.currentItem else {
 | 
						|
                owsFailDebug("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) {
 | 
						|
        notImplemented()
 | 
						|
    }
 | 
						|
 | 
						|
    override public init(frame: CGRect) {
 | 
						|
        super.init(frame: frame)
 | 
						|
 | 
						|
        // Background
 | 
						|
        backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
 | 
						|
        if !UIAccessibilityIsReduceTransparencyEnabled() {
 | 
						|
            addSubview(blurEffectView)
 | 
						|
            blurEffectView.ows_autoPinToSuperviewEdges()
 | 
						|
        }
 | 
						|
 | 
						|
        // Configure controls
 | 
						|
 | 
						|
        let kLabelFont = UIFont.monospacedDigitSystemFont(ofSize: 12, weight: UIFont.Weight.regular)
 | 
						|
        positionLabel.font = kLabelFont
 | 
						|
        remainingLabel.font = kLabelFont
 | 
						|
 | 
						|
        // We use a smaller thumb for the progress slider.
 | 
						|
        slider.setThumbImage(#imageLiteral(resourceName: "sliderProgressThumb"), for: .normal)
 | 
						|
        slider.maximumTrackTintColor = UIColor.ows_black
 | 
						|
        slider.minimumTrackTintColor = UIColor.ows_black
 | 
						|
 | 
						|
        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)
 | 
						|
 | 
						|
        // Panning is a no-op. We just absorb pan gesture's originating in the video controls
 | 
						|
        // from propogating so we don't inadvertently change pages while trying to scrub in
 | 
						|
        // the MediaPageView.
 | 
						|
        let panAbsorber = UIPanGestureRecognizer(target: self, action: nil)
 | 
						|
        self.addGestureRecognizer(panAbsorber)
 | 
						|
 | 
						|
        // 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 {
 | 
						|
            owsFailDebug("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 {
 | 
						|
            owsFailDebug("player isn't set.")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        guard let item = player.currentItem else {
 | 
						|
            owsFailDebug("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 {
 | 
						|
            owsFailDebug("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)
 | 
						|
    }
 | 
						|
}
 |