Swift Media Player
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

802 lines
26 KiB

//
// BMPlayer.swift
// Pods
//
// Created by BrikerMan on 16/4/28.
//
//
import UIKit
import SnapKit
import MediaPlayer
/// BMPlayerDelegate to obserbe player state
public protocol BMPlayerDelegate : class {
func bmPlayer(player: BMPlayer)
func bmPlayer(player: BMPlayer, playerStateDidChange state: BMPlayerState)
func bmPlayer(player: BMPlayer, loadedTimeDidChange loadedDuration: TimeInterval, totalDuration: TimeInterval)
func bmPlayer(player: BMPlayer, playTimeDidChange currentTime : TimeInterval, totalTime: TimeInterval)
func bmPlayer(player: BMPlayer, playerIsPlaying playing: Bool)
func bmPlayer(player: BMPlayer, playerOrientChanged isFullscreen: Bool)
func moveUp()
func moveDown()
}
/**
internal enum to check the pan direction
- horizontal: horizontal
- vertical: vertical
*/
enum BMPanDirection: Int {
case horizontal = 0
case vertical = 1
}
open class BMPlayer: UIView {
open var zoom = Float(1.0)
open var aspectx = Float(1.0)
open var aspecty = Float(1.0)
var xpos = 0.0
var ypos = 0.0
var loopEnd = 0.0
open var pinchGesture: UIPinchGestureRecognizer!
open var moveGesture: UIPanGestureRecognizer!
open weak var delegate: BMPlayerDelegate?
open var backBlock:((Bool) -> Void)?
/// Gesture to change volume / brightness
open var panGesture: UIPanGestureRecognizer!
private func initGesture() {
pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(self.pinched(_:)))
self.addGestureRecognizer(pinchGesture)
// moveGesture = UIPanGestureRecognizer(target: self, action: #selector(self.moved(_:)))
// moveGesture.minimumNumberOfTouches = 2
// moveGesture.maximumNumberOfTouches = 2
// self.addGestureRecognizer(moveGesture)
}
func panDir(_ pan: UIPanGestureRecognizer) {
let velocityPoint = pan.velocity(in: self)
xpos += (Double(velocityPoint.x) / 50.0)
ypos += (Double(velocityPoint.y) / 50.0)
transformLayer()
}
@objc fileprivate func moved(_ gestureRecognizer: UIPanGestureRecognizer) {
}
@objc fileprivate func pinched(_ gestureRecognizer: UIPinchGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
zoom *= Float(gestureRecognizer.scale)
if zoom < 0.1 {
zoom = 1.0
xpos = 0
ypos = 0
}
gestureRecognizer.scale = 1.0
transformLayer()
}
}
func transformLayer() {
let t = CATransform3DMakeTranslation(CGFloat(xpos), CGFloat(ypos), 0.0)
playerLayer!.layer.transform = CATransform3DScale(t, CGFloat(zoom * aspectx), CGFloat(zoom * aspecty), 1.0)
}
/// AVLayerVideoGravityType
open var videoGravity = AVLayerVideoGravity.resizeAspect {
didSet {
self.playerLayer?.videoGravity = videoGravity
}
}
open var isPlaying: Bool {
get {
return playerLayer?.isPlaying ?? false
}
}
//Closure fired when play time changed
open var playTimeDidChange:((TimeInterval, TimeInterval) -> Void)?
//Closure fired when play state chaged
@available(*, deprecated, message: "Use newer `isPlayingStateChanged`")
open var playStateDidChange:((Bool) -> Void)?
open var playOrientChanged:((Bool) -> Void)?
open var isPlayingStateChanged:((Bool) -> Void)?
open var playStateChanged:((BMPlayerState) -> Void)?
open var avPlayer: AVPlayer? {
return playerLayer?.player
}
open var playerLayer: BMPlayerLayerView?
fileprivate var resource: BMPlayerResource!
fileprivate var currentDefinition = 0
fileprivate var controlView: BMPlayerControlView!
fileprivate var customControlView: BMPlayerControlView?
fileprivate var isFullScreen:Bool {
get {
return UIApplication.shared.statusBarOrientation.isLandscape
}
}
///
fileprivate var panDirection = BMPanDirection.horizontal
/// 竿
fileprivate var volumeViewSlider: UISlider!
fileprivate let BMPlayerAnimationTimeInterval: Double = 4.0
fileprivate let BMPlayerControlBarAutoFadeOutTimeInterval: Double = 0.5
///
fileprivate var sumTime : TimeInterval = 0
var totalDuration : TimeInterval = 0
fileprivate var currentPosition : TimeInterval = 0
fileprivate var shouldSeekTo : TimeInterval = 0
fileprivate var isURLSet = false
fileprivate var isSliderSliding = false
fileprivate var isPauseByUser = false
fileprivate var isVolume = false
fileprivate var isSkip = false
fileprivate var skipAmount = 0.0
fileprivate var isMaskShowing = false
fileprivate var isSlowed = false
fileprivate var isMirrored = false
var isPlayToTheEnd = false
//
fileprivate var aspectRatio: BMPlayerAspectRatio = .default
//Cache is playing result to improve callback performance
fileprivate var isPlayingCache: Bool? = nil
var isScrub = false
var isSeekInProgress = false
var chaseTime = CMTime.zero
var continueTime = 0.0
// MARK: - Public functions
/**
Play
- parameter resource: media resource
- parameter definitionIndex: starting definition index, default start with the first definition
*/
open func setVideo(resource: BMPlayerResource, definitionIndex: Int = 0) {
isURLSet = false
self.resource = resource
currentDefinition = definitionIndex
controlView.prepareUI(for: resource, selectedIndex: definitionIndex)
if BMPlayerConf.shouldAutoPlay {
isURLSet = true
let asset = resource.definitions[definitionIndex]
let size = asset.avURLAsset.videoSize()
if (size.height < 1500) {
zoom = LocalManager.sharedInstance.settings.scale
if (zoom < 1.0) {
xpos = -100
}
transformLayer()
}
playerLayer?.playAsset(asset: asset.avURLAsset)
} else {
controlView.showCover(url: resource.cover)
controlView.hideLoader()
}
}
/**
auto start playing, call at viewWillAppear, See more at pause
*/
open func autoPlay() {
if !isPauseByUser && isURLSet && !isPlayToTheEnd {
play()
}
}
func isFile() -> Bool {
guard resource != nil else { return false }
let asset = resource.definitions[currentDefinition]
return asset.url.isFileURL
}
/**
Play
*/
open func play() {
guard resource != nil else { return }
if !isURLSet {
let asset = resource.definitions[currentDefinition]
playerLayer?.playAsset(asset: asset.avURLAsset)
controlView.hideCoverImageView()
isURLSet = true
}
panGesture.isEnabled = true
playerLayer?.play()
isPauseByUser = false
if let d = delegate { d.bmPlayer(player: self) }
}
/**
Pause
- parameter allow: should allow to response `autoPlay` function
*/
open func pause(allowAutoPlay allow: Bool = false) {
playerLayer?.pause()
isPauseByUser = !allow
}
/**
seek
- parameter to: target time
*/
open func seek(_ to:TimeInterval, completion: (()->Void)? = nil) {
chaseTime = CMTime(value: Int64(to * 10000), timescale: CMTimeScale(10000))
playerLayer?.seek(to: to, completion: completion)
}
/**
update UI to fullScreen
*/
open func updateUI(_ isFullScreen: Bool) {
controlView.updateUI(isFullScreen)
}
/**
increade volume with step, default step 0.1
- parameter step: step
*/
open func addVolume(step: Float = 0.1) {
self.volumeViewSlider.value += step
}
/**
decreace volume with step, default step 0.1
- parameter step: step
*/
open func reduceVolume(step: Float = 0.1) {
self.volumeViewSlider.value -= step
}
/**
prepare to dealloc player, call at View or Controllers deinit funciton.
*/
open func prepareToDealloc() {
playerLayer?.prepareToDeinit()
controlView.prepareToDealloc()
}
/**
If you want to create BMPlayer with custom control in storyboard.
create a subclass and override this method.
- return: costom control which you want to use
*/
open func storyBoardCustomControl() -> BMPlayerControlView? {
return nil
}
// https://gist.github.com/shaps80/ac16b906938ad256e1f47b52b4809512
public func forceSeekSmoothlyToTime(newChaseTime: Double) {
chaseTime = CMTime.zero
seekSmoothlyToTime(newChaseTime: CMTime(value: Int64(newChaseTime * 10000), timescale: CMTimeScale(10000)))
}
public func seekSmoothlyToTime(newChaseTime: Double) {
seekSmoothlyToTime(newChaseTime: CMTime(value: Int64(newChaseTime * 10000), timescale: CMTimeScale(10000)))
}
public func seekSmoothlyToTime(newChaseTime: CMTime) {
if CMTimeCompare(newChaseTime, chaseTime) != 0 {
chaseTime = newChaseTime
if !isSeekInProgress {
trySeekToChaseTime()
}
}
}
private func trySeekToChaseTime() {
guard playerLayer!.player?.status == .readyToPlay else { return }
actuallySeekToTime()
}
private func actuallySeekToTime() {
isSeekInProgress = true
let seekTimeInProgress = chaseTime
playerLayer!.player?.seek(to: seekTimeInProgress, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero) { [weak self] _ in
guard let `self` = self else { return }
if CMTimeCompare(seekTimeInProgress, self.chaseTime) == 0 {
self.isSeekInProgress = false
} else {
self.trySeekToChaseTime()
}
}
}
// MARK: - Action Response
@objc open func panDirection(_ pan: UIPanGestureRecognizer) {
let resolution = 10000.0
if pan.numberOfTouches <= 1 {
// viewPan
let locationPoint = pan.location(in: self)
//
// point
let velocityPoint = pan.velocity(in: self)
//
switch pan.state {
case UIGestureRecognizer.State.began:
// 使
let x = abs(velocityPoint.x)
let y = abs(velocityPoint.y)
if (locationPoint.y < self.bounds.size.height * 1 / 8) {
self.isSkip = (zoom <= 1.0)
isScrub = false
if (!isPlaying) {
isScrub = true
let currentTime = CMTimeGetSeconds(playerLayer!.player!.currentTime())
continueTime = currentTime
let vel = velocityPoint.x + velocityPoint.y
var time = CMTimeMake(value: Int64((currentTime + (Float64(vel) / resolution))*10000), timescale: 10000)
seekSmoothlyToTime(newChaseTime: time)
}
} else {
isScrub = false
self.isSkip = true
self.skipAmount = 0.0
}
if x > y {
if BMPlayerConf.enablePlaytimeGestures {
self.panDirection = BMPanDirection.horizontal
// sumTime
if let player = playerLayer?.player {
let time = player.currentTime()
self.sumTime = TimeInterval(time.value) / TimeInterval(time.timescale)
}
}
} else {
self.panDirection = BMPanDirection.vertical
if locationPoint.x > self.bounds.size.width / 2 {
self.isVolume = false
} else {
self.isVolume = false
}
}
case UIGestureRecognizer.State.changed:
if !self.isSkip && isPlaying {
xpos += (Double(velocityPoint.x) * 0.1);
ypos += (Double(velocityPoint.y) * 0.1);
transformLayer()
}
if (isScrub) {
let vel = velocityPoint.x + velocityPoint.y
var time = CMTimeMake(value: Int64((Float64(vel) / resolution)*10000), timescale: 10000)
seekSmoothlyToTime(newChaseTime: CMTimeAdd(chaseTime, time))
}
switch self.panDirection {
case BMPanDirection.horizontal:
self.horizontalMoved(velocityPoint.x)
case BMPanDirection.vertical:
self.verticalMoved(velocityPoint.y)
}
case UIGestureRecognizer.State.ended:
//
// bug
switch (self.panDirection) {
case BMPanDirection.horizontal:
controlView.hideSeekToView()
isSliderSliding = false
if isSkip && !isScrub {
print(skipAmount)
if (skipAmount > 1500) {
self.sumTime += TimeInterval(30)
if sumTime > loopEnd {
loopEnd = 1000000;
}
} else if skipAmount > 0 {
self.sumTime += TimeInterval(10)
} else if skipAmount > -1500 {
self.sumTime -= TimeInterval(10)
} else {
self.sumTime -= TimeInterval(30)
}
controlView.controlViewAnimation(isShow: true)
if isPlayToTheEnd {
isPlayToTheEnd = false
seek(self.sumTime, completion: { [weak self] in
self?.play()
})
} else {
seek(self.sumTime, completion: { [weak self] in
self?.autoPlay()
})
}
}
// sumTime
self.sumTime = 0.0
if (isScrub && !isFile()) {
seek(continueTime)
}
isScrub = false
case BMPanDirection.vertical:
self.isVolume = false
}
default:
break
}
}
else {
panDir(pan)
}
}
open func verticalMoved(_ value: CGFloat) {
print(value)
if (value < -100) {
delegate?.moveDown()
// controlView(controlView: controlView, didChooseDefinition: currentDefinition+1);
}
if (value > 100) {
delegate?.moveUp()
// controlView(controlView: controlView, didChooseDefinition: currentDefinition+1);
}
if !isSkip {
}
}
open func horizontalMoved(_ value: CGFloat) {
guard BMPlayerConf.enablePlaytimeGestures else { return }
isSliderSliding = true
if !isSkip {
}
else {
skipAmount += Double(value)
}
}
@objc open func onOrientationChanged() {
self.updateUI(isFullScreen)
delegate?.bmPlayer(player: self, playerOrientChanged: isFullScreen)
playOrientChanged?(isFullScreen)
}
@objc fileprivate func fullScreenButtonPressed() {
controlView.updateUI(!self.isFullScreen)
if isFullScreen {
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UIApplication.shared.setStatusBarHidden(false, with: .fade)
UIApplication.shared.statusBarOrientation = .portrait
} else {
UIDevice.current.setValue(UIInterfaceOrientation.landscapeRight.rawValue, forKey: "orientation")
UIApplication.shared.setStatusBarHidden(false, with: .fade)
UIApplication.shared.statusBarOrientation = .landscapeRight
}
}
// MARK: -
deinit {
playerLayer?.pause()
playerLayer?.prepareToDeinit()
NotificationCenter.default.removeObserver(self, name: UIApplication.didChangeStatusBarOrientationNotification, object: nil)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
if let customControlView = storyBoardCustomControl() {
self.customControlView = customControlView
}
initUI()
initUIData()
configureVolume()
preparePlayer()
initGesture()
}
// @available(*, deprecated:3.0, message:"Use newer init(customControlView:_)")
// public convenience init(customControllView: BMPlayerControlView?) {
// self.init(customControlView: customControllView)
// }
@objc public init(customControlView: BMPlayerControlView?) {
super.init(frame:CGRect.zero)
self.customControlView = customControlView
initUI()
initUIData()
configureVolume()
preparePlayer()
initGesture()
}
public convenience init() {
self.init(customControlView:nil)
}
// MARK: -
fileprivate func initUI() {
self.backgroundColor = UIColor.black
if let customView = customControlView {
controlView = customView
} else {
controlView = BMPlayerControlView()
}
addSubview(controlView)
controlView.updateUI(isFullScreen)
controlView.delegate = self
controlView.player = self
controlView.snp.makeConstraints { (make) in
make.edges.equalTo(self)
}
panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panDirection(_:)))
// panGesture.minimumNumberOfTouches = 1
// panGesture.maximumNumberOfTouches = 1
self.addGestureRecognizer(panGesture)
}
fileprivate func initUIData() {
NotificationCenter.default.addObserver(self, selector: #selector(self.onOrientationChanged), name: UIApplication.didChangeStatusBarOrientationNotification, object: nil)
}
fileprivate func configureVolume() {
let volumeView = MPVolumeView()
for view in volumeView.subviews {
if let slider = view as? UISlider {
self.volumeViewSlider = slider
}
}
}
fileprivate func preparePlayer() {
playerLayer = BMPlayerLayerView()
playerLayer!.videoGravity = videoGravity
insertSubview(playerLayer!, at: 0)
playerLayer!.snp.makeConstraints { [weak self](make) in
guard let `self` = self else { return }
make.edges.equalTo(self)
}
playerLayer!.delegate = self
controlView.showLoader()
self.layoutIfNeeded()
}
}
extension BMPlayer: BMPlayerLayerViewDelegate {
public func bmPlayer(player: BMPlayerLayerView, playerIsPlaying playing: Bool) {
controlView.playStateDidChange(isPlaying: playing)
delegate?.bmPlayer(player: self, playerIsPlaying: playing)
playStateDidChange?(player.isPlaying)
isPlayingStateChanged?(player.isPlaying)
}
public func bmPlayer(player: BMPlayerLayerView, loadedTimeDidChange loadedDuration: TimeInterval, totalDuration: TimeInterval) {
BMPlayerManager.shared.log("loadedTimeDidChange - \(loadedDuration) - \(totalDuration)")
controlView.loadedTimeDidChange(loadedDuration: loadedDuration, totalDuration: totalDuration)
delegate?.bmPlayer(player: self, loadedTimeDidChange: loadedDuration, totalDuration: totalDuration)
controlView.totalDuration = totalDuration
self.totalDuration = totalDuration
}
public func bmPlayer(player: BMPlayerLayerView, playerStateDidChange state: BMPlayerState) {
BMPlayerManager.shared.log("playerStateDidChange - \(state)")
controlView.playerStateDidChange(state: state)
switch state {
case .readyToPlay:
if !isPauseByUser {
play()
}
if shouldSeekTo != 0 {
seek(shouldSeekTo, completion: {[weak self] in
guard let `self` = self else { return }
if !self.isPauseByUser {
self.play()
} else {
self.pause()
}
})
shouldSeekTo = 0
}
case .bufferFinished:
autoPlay()
case .playedToTheEnd:
isPlayToTheEnd = true
default:
break
}
panGesture.isEnabled = state != .playedToTheEnd
delegate?.bmPlayer(player: self, playerStateDidChange: state)
playStateChanged?(state)
}
public func bmPlayer(player: BMPlayerLayerView, playTimeDidChange currentTime: TimeInterval, totalTime: TimeInterval) {
// BMPlayerManager.shared.log("playTimeDidChange - \(currentTime) - \(totalTime)")
delegate?.bmPlayer(player: self, playTimeDidChange: currentTime, totalTime: totalTime)
self.currentPosition = currentTime
totalDuration = totalTime
if isSliderSliding {
return
}
controlView.playTimeDidChange(currentTime: currentTime, totalTime: totalTime)
controlView.totalDuration = totalDuration
playTimeDidChange?(currentTime, totalTime)
}
}
extension BMPlayer: BMPlayerControlViewDelegate {
open func controlView(controlView: BMPlayerControlView,
didChooseDefinition index: Int) {
shouldSeekTo = currentPosition
playerLayer?.resetPlayer()
currentDefinition = index
playerLayer?.playAsset(asset: resource.definitions[index].avURLAsset)
}
open func controlView(controlView: BMPlayerControlView,
didPressButton button: UIButton) {
if let action = BMPlayerControlView.ButtonType(rawValue: button.tag) {
switch action {
case .back:
backBlock?(isFullScreen)
if isFullScreen {
fullScreenButtonPressed()
} else {
playerLayer?.prepareToDeinit()
}
case .play:
if button.isSelected {
pause()
} else {
if isPlayToTheEnd {
seek(0, completion: {[weak self] in
self?.play()
})
controlView.hidePlayToTheEndView()
isPlayToTheEnd = false
}
play()
}
case .replay:
isPlayToTheEnd = false
seek(0)
play()
case .fullscreen:
fullScreenButtonPressed()
default:
print("[Error] unhandled Action")
}
}
}
open func controlView(controlView: BMPlayerControlView,
slider: UISlider,
onSliderEvent event: UIControl.Event) {
switch event {
case .touchDown:
playerLayer?.onTimeSliderBegan()
isSliderSliding = true
case .valueChanged:
let target = self.totalDuration * Double(slider.value)
seekSmoothlyToTime(newChaseTime: CMTime(seconds: target, preferredTimescale: 10000))
break
case .touchUpInside :
isSliderSliding = false
let target = self.totalDuration * Double(slider.value)
if isPlayToTheEnd {
isPlayToTheEnd = false
seek(target, completion: {[weak self] in
self?.play()
})
controlView.hidePlayToTheEndView()
} else {
seek(target, completion: {[weak self] in
self?.autoPlay()
})
}
default:
break
}
}
open func controlView(controlView: BMPlayerControlView, didChangeVideoAspectRatio: BMPlayerAspectRatio) {
self.playerLayer?.aspectRatio = self.aspectRatio
}
open func controlView(controlView: BMPlayerControlView, didChangeVideoPlaybackRate rate: Float) {
self.playerLayer?.player?.rate = rate
}
}
extension AVAsset{
func videoSize()->CGSize{
let tracks = self.tracks(withMediaType: AVMediaType.video)
if (tracks.count > 0){
let videoTrack = tracks[0]
let size = videoTrack.naturalSize
let txf = videoTrack.preferredTransform
let realVidSize = size.applying(txf)
print(videoTrack)
print(txf)
print(size)
print(realVidSize)
return realVidSize
}
return CGSize(width: 0, height: 0)
}
}