mirror of https://github.com/oxen-io/session-ios
parent
918e3f7dfe
commit
86d61eee30
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "VideoPlayer_Slider_Thumb_15x15_@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "VideoPlayer_Slider_Thumb_15x15_@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "VideoPlayer_Slider_Thumb_15x15_@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,200 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@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)
|
||||
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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue