mirror of https://github.com/oxen-io/session-ios
Compare commits
4 Commits
89b38dc2f5
...
109a81f33f
Author | SHA1 | Date |
---|---|---|
Morgan Pretty | 109a81f33f | 7 months ago |
Morgan Pretty | 05460ca2b3 | 7 months ago |
Morgan Pretty | de7d85f4cb | 7 months ago |
Morgan Pretty | bd98db2612 | 7 months ago |
@ -1,74 +0,0 @@
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import SessionMessagingKit
|
||||
import SignalCoreKit
|
||||
|
||||
public protocol OWSVideoPlayerDelegate: AnyObject {
|
||||
func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer)
|
||||
}
|
||||
|
||||
public class OWSVideoPlayer {
|
||||
|
||||
public let avPlayer: AVPlayer
|
||||
let audioActivity: AudioActivity
|
||||
|
||||
public weak var delegate: OWSVideoPlayerDelegate?
|
||||
|
||||
@objc public init(url: URL) {
|
||||
self.avPlayer = AVPlayer(url: url)
|
||||
self.audioActivity = AudioActivity(audioDescription: "[OWSVideoPlayer] url:\(url)", behavior: .playback)
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(playerItemDidPlayToCompletion(_:)),
|
||||
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
||||
object: avPlayer.currentItem)
|
||||
}
|
||||
|
||||
// MARK: Playback Controls
|
||||
|
||||
@objc
|
||||
public func pause() {
|
||||
avPlayer.pause()
|
||||
Environment.shared?.audioSession.endAudioActivity(self.audioActivity)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func play() {
|
||||
let success = (Environment.shared?.audioSession.startAudioActivity(self.audioActivity) == true)
|
||||
assert(success)
|
||||
|
||||
guard let item = avPlayer.currentItem else {
|
||||
owsFailDebug("video player item was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
if item.currentTime() == item.duration {
|
||||
// Rewind for repeated plays, but only if it previously played to end.
|
||||
avPlayer.seek(to: CMTime.zero, toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
}
|
||||
|
||||
avPlayer.play()
|
||||
}
|
||||
|
||||
@objc
|
||||
public func stop() {
|
||||
avPlayer.pause()
|
||||
avPlayer.seek(to: CMTime.zero, toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
Environment.shared?.audioSession.endAudioActivity(self.audioActivity)
|
||||
}
|
||||
|
||||
@objc(seekToTime:)
|
||||
public func seek(to time: CMTime) {
|
||||
avPlayer.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
|
||||
}
|
||||
|
||||
// MARK: private
|
||||
|
||||
@objc
|
||||
private func playerItemDidPlayToCompletion(_ notification: Notification) {
|
||||
self.delegate?.videoPlayerDidPlayToCompletion(self)
|
||||
Environment.shared?.audioSession.endAudioActivity(self.audioActivity)
|
||||
}
|
||||
}
|
@ -1,260 +0,0 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
import SessionUIKit
|
||||
import SignalCoreKit
|
||||
|
||||
@objc
|
||||
public class VideoPlayerView: UIView {
|
||||
@objc
|
||||
public 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 blurView = UIVisualEffectView()
|
||||
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))
|
||||
|
||||
updateState()
|
||||
|
||||
// OPTIMIZE We need a high frequency observer for smooth slider updates while playing,
|
||||
// but could use a much less frequent observer for label updates
|
||||
progressObserver = player?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: kPreferredTimeScale), queue: nil, using: { [weak self] _ in
|
||||
// If it is playing update the time
|
||||
if self?.player?.rate != 0 && self?.player?.error == nil {
|
||||
self?.updateState()
|
||||
}
|
||||
}) as AnyObject
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
// Background & blur
|
||||
let backgroundView = UIView()
|
||||
backgroundView.themeBackgroundColor = .backgroundSecondary
|
||||
backgroundView.alpha = Values.lowOpacity
|
||||
addSubview(backgroundView)
|
||||
backgroundView.pin(to: self)
|
||||
|
||||
if !UIAccessibility.isReduceTransparencyEnabled {
|
||||
addSubview(blurView)
|
||||
blurView.pin(to: self)
|
||||
|
||||
ThemeManager.onThemeChange(observer: blurView) { [weak blurView] theme, _ in
|
||||
switch theme.interfaceStyle {
|
||||
case .light: blurView?.effect = UIBlurEffect(style: .light)
|
||||
default: blurView?.effect = UIBlurEffect(style: .dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.themeMinimumTrackTintColor = .backgroundPrimary
|
||||
slider.themeMaximumTrackTintColor = .backgroundPrimary
|
||||
|
||||
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
|
||||
|
||||
public 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)
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
public func manuallySetValue(_ positionSeconds: CGFloat, durationSeconds: CGFloat) {
|
||||
let remainingSeconds = (durationSeconds - positionSeconds)
|
||||
|
||||
slider.minimumValue = 0
|
||||
slider.maximumValue = Float(durationSeconds)
|
||||
|
||||
positionLabel.text = formatter.string(from: positionSeconds)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue