|  |  |  | // | 
					
						
							|  |  |  | //  Copyright (c) 2018 Open Whisper Systems. All rights reserved. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import Foundation | 
					
						
							|  |  |  | import AVFoundation | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @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 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @objc | 
					
						
							|  |  |  | public protocol PlayerProgressBarDelegate { | 
					
						
							|  |  |  |     func playerProgressBarDidStartScrubbing(_ playerProgressBar: PlayerProgressBar) | 
					
						
							|  |  |  |     func playerProgressBar(_ playerProgressBar: PlayerProgressBar, scrubbedToTime time: CMTime) | 
					
						
							|  |  |  |     func playerProgressBar(_ playerProgressBar: PlayerProgressBar, didFinishScrubbingAtTime time: CMTime, shouldResumePlayback: Bool) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @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) | 
					
						
							|  |  |  |         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) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // 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) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |