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) | ||
|  |     } | ||
|  | } |