22 changed files with 2523 additions and 521 deletions
-
30Podfile
-
53kplayer.xcodeproj/project.pbxproj
-
5kplayer/core/MediaItem.swift
-
29kplayer/core/NetworkManager.swift
-
41kplayer/detail/AVPlayerController.swift
-
4kplayer/detail/BrowserController.swift
-
28kplayer/detail/DetailViewController.swift
-
396kplayer/detail/VideoPlayerController.swift
-
2kplayer/master/MasterViewController.swift
-
34kplayer/photo/MediaPhotoController.swift
-
2kplayer/util/ImageLoadOperation.swift
-
29kplayer/util/UploadOperation.swift
-
631kplayer/video/BMPlayer.swift
-
29kplayer/video/BMPlayerClearityChooseButton.swift
-
769kplayer/video/BMPlayerControlView.swift
-
91kplayer/video/BMPlayerItem.swift
-
507kplayer/video/BMPlayerLayerView.swift
-
62kplayer/video/BMPlayerManager.swift
-
30kplayer/video/BMPlayerProtocols.swift
-
122kplayer/video/BMSubtitles.swift
-
26kplayer/video/BMTimeSlider.swift
-
86kplayer/video/KBMPlayer.swift
@ -1,396 +0,0 @@ |
|||||
// |
|
||||
// Created by Marco Schmickler on 26.05.15. |
|
||||
// Copyright (c) 2015 Marco Schmickler. All rights reserved. |
|
||||
// |
|
||||
|
|
||||
import Foundation |
|
||||
import UIKit |
|
||||
import MediaPlayer |
|
||||
import ALMoviePlayerController |
|
||||
import Haneke |
|
||||
|
|
||||
class VideoPlayerController: UIViewController, ItemController { |
|
||||
var moviePlayer: ALMoviePlayerController? |
|
||||
var currentItem: MediaItem? |
|
||||
|
|
||||
var completionHandler: (() -> Void)? |
|
||||
|
|
||||
var buttons = Dictionary<UIButton, MediaItem>() |
|
||||
|
|
||||
var barbutton: UIBarButtonItem? |
|
||||
var speedButton: UIBarButtonItem? |
|
||||
var playButton: UIBarButtonItem? |
|
||||
var backButton: UIBarButtonItem? |
|
||||
var reviewButton: UIBarButtonItem? |
|
||||
|
|
||||
let speedOptions = [ 0.25, 0.5, 0.75, 1.0, 2.0 ] |
|
||||
var speedOption = 3 |
|
||||
|
|
||||
var thumbnailTime: TimeInterval = 0.0 |
|
||||
|
|
||||
var edit = true |
|
||||
var allowEdit = true |
|
||||
var index = 0 |
|
||||
|
|
||||
func setCompletionHandler(handler: @escaping (() -> Void)) { |
|
||||
completionHandler = handler |
|
||||
} |
|
||||
|
|
||||
override func viewDidLoad() { |
|
||||
super.viewDidLoad() |
|
||||
|
|
||||
barbutton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(VideoPlayerController.twoFingersTwoTaps)); |
|
||||
navigationItem.rightBarButtonItems = [barbutton!] |
|
||||
|
|
||||
backButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(VideoPlayerController.back(_:))) |
|
||||
speedButton = UIBarButtonItem(title:"1.0", style:UIBarButtonItem.Style.plain, target: self, action: #selector(VideoPlayerController.speed(_:))) |
|
||||
playButton = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(VideoPlayerController.startstop(_:))) |
|
||||
reviewButton = UIBarButtonItem(title:"Edit ", style:UIBarButtonItem.Style.plain, target: self, action: #selector(VideoPlayerController.doEdit(_:))) |
|
||||
|
|
||||
navigationItem.leftBarButtonItems = [backButton!, playButton!, speedButton!, reviewButton!] |
|
||||
|
|
||||
if let c = currentItem, let url = c.playerURL { |
|
||||
print(url) |
|
||||
play(url as URL) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func setCurrentItem(item: MediaItem) { |
|
||||
currentItem = item |
|
||||
} |
|
||||
|
|
||||
@objc func doEdit(_ sender: AnyObject) { |
|
||||
if (!allowEdit) { |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
if (edit) { |
|
||||
edit = false |
|
||||
reviewButton!.tintColor = UIColor.blue |
|
||||
} |
|
||||
else { |
|
||||
edit = true |
|
||||
reviewButton!.tintColor = UIColor.yellow |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@objc func startstop(_ sender: AnyObject) { |
|
||||
if moviePlayer!.playbackState == MPMoviePlaybackState.playing { |
|
||||
moviePlayer!.pause() |
|
||||
} |
|
||||
else { |
|
||||
moviePlayer!.play() |
|
||||
|
|
||||
Timer.scheduledTimer(timeInterval: 0.5, |
|
||||
target: self, |
|
||||
selector: #selector(resumePlay), |
|
||||
userInfo: nil, |
|
||||
repeats: false) |
|
||||
} |
|
||||
print("play") |
|
||||
} |
|
||||
|
|
||||
@objc func speed(_ sender: AnyObject) { |
|
||||
speedOption += 1 |
|
||||
if speedOption >= speedOptions.count { |
|
||||
speedOption = 0 |
|
||||
} |
|
||||
|
|
||||
moviePlayer!.currentPlaybackRate = Float(speedOptions[speedOption]) |
|
||||
|
|
||||
speedButton!.title = "\(moviePlayer!.currentPlaybackRate)" |
|
||||
print("speed \(moviePlayer!.currentPlaybackRate)") |
|
||||
} |
|
||||
|
|
||||
@IBAction func back(_ sender: AnyObject) { |
|
||||
if moviePlayer!.playbackState == MPMoviePlaybackState.playing { |
|
||||
moviePlayer!.pause() |
|
||||
} |
|
||||
completionHandler!() |
|
||||
} |
|
||||
|
|
||||
func play(_ url: URL) { |
|
||||
self.moviePlayer = ALMoviePlayerController(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)) |
|
||||
if let player = self.moviePlayer { |
|
||||
|
|
||||
let movieControls = ALMoviePlayerControls(moviePlayer: player, style: ALMoviePlayerControlsStyleDefault)!; |
|
||||
movieControls.fadeDelay = 60 |
|
||||
player.controls = movieControls |
|
||||
movieControls.style = ALMoviePlayerControlsStyleEmbedded |
|
||||
|
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(VideoPlayerController.exitedFullscreen), name: NSNotification.Name.MPMoviePlayerDidExitFullscreen, object: nil); |
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(VideoPlayerController.enteredFullscreen), name: NSNotification.Name.MPMoviePlayerDidEnterFullscreen, object: nil); |
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(VideoPlayerController.showThumbnail(_:)), name: NSNotification.Name.MPMoviePlayerThumbnailImageRequestDidFinish, object: nil); |
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(VideoPlayerController.playerItemDidReachEnd(_:)), name: NSNotification.Name.MPMoviePlayerLoadStateDidChange, object: nil); |
|
||||
|
|
||||
player.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height) |
|
||||
player.view.sizeToFit() |
|
||||
player.scalingMode = MPMovieScalingMode.aspectFit |
|
||||
// player.controlStyle = MPMovieControlStyle.Embedded |
|
||||
player.movieSourceType = MPMovieSourceType.streaming |
|
||||
player.repeatMode = MPMovieRepeatMode.one |
|
||||
player.contentURL = url |
|
||||
|
|
||||
self.view.addSubview(player.view) |
|
||||
installGestures(player.view) |
|
||||
update() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@objc func playerItemDidReachEnd(_ note: Notification) { |
|
||||
print("finish") |
|
||||
// Timer.scheduledTimer(timeInterval: 0.6, target: self, selector: #selector(update), userInfo: nil, repeats: false) |
|
||||
} |
|
||||
|
|
||||
@objc func showThumbnail(_ note: Notification) { |
|
||||
let userInfo = note.userInfo! as NSDictionary |
|
||||
let thumbnail = userInfo.object(forKey: MPMoviePlayerThumbnailImageKey) as! UIImage |
|
||||
let time = userInfo.object(forKey: MPMoviePlayerThumbnailTimeKey) as! TimeInterval |
|
||||
|
|
||||
let newItem = MediaItem(name: currentItem!.name, path: currentItem!.path, root: currentItem!.root, type: ItemType.SNAPSHOT) |
|
||||
newItem.image = thumbnail |
|
||||
newItem.time = time |
|
||||
newItem.parent = currentItem! |
|
||||
currentItem!.children.append(newItem) |
|
||||
|
|
||||
print(newItem.time) |
|
||||
|
|
||||
addItemButton(newItem) |
|
||||
} |
|
||||
|
|
||||
func addItemButton(_ newItem: MediaItem) { |
|
||||
let frame = CGRect(x: 0, y: 0, width: 66.0, height: 44.0); |
|
||||
|
|
||||
let button = UIButton(frame: frame) |
|
||||
button.showsTouchWhenHighlighted = true |
|
||||
button.addTarget(self, action: #selector(thumbnailClicked(_:)), for: .touchDown) |
|
||||
|
|
||||
if newItem.image != nil { |
|
||||
let icon = newItem.image!.scaleToSize(66.0, height: 44.0) |
|
||||
|
|
||||
button.setBackgroundImage(icon, for: UIControl.State()); |
|
||||
} else { |
|
||||
if newItem.thumbUrl != nil { |
|
||||
let URL = Foundation.URL(string: newItem.thumbUrlAbsolute)! |
|
||||
|
|
||||
Shared.imageCache.fetch(URL: URL).onSuccess { |
|
||||
i in |
|
||||
let icon = i.scaleToSize(66.0, height: 44.0) |
|
||||
button.setBackgroundImage(icon, for: UIControl.State.normal); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
let barbutton = UIBarButtonItem(customView: button); |
|
||||
|
|
||||
if navigationItem.rightBarButtonItems == nil { |
|
||||
navigationItem.rightBarButtonItems = [] |
|
||||
} |
|
||||
navigationItem.rightBarButtonItems!.append(barbutton) |
|
||||
|
|
||||
buttons[button] = newItem |
|
||||
} |
|
||||
|
|
||||
@objc func thumbnailClicked(_ source: UIButton) { |
|
||||
|
|
||||
moviePlayer!.currentPlaybackTime = buttons[source]!.time! |
|
||||
moviePlayer!.currentPlaybackRate = Float(speedOptions[speedOption]) |
|
||||
|
|
||||
print("goto \(buttons[source]!.time!) is \(moviePlayer!.currentPlaybackTime)") |
|
||||
} |
|
||||
|
|
||||
@objc func update() { |
|
||||
if let player = self.moviePlayer { |
|
||||
|
|
||||
// if !(player.duration > 0.0) { |
|
||||
// print("again") |
|
||||
// Timer.scheduledTimer(timeInterval: 0.3, target: self, selector: #selector(update), userInfo: nil, repeats: false) |
|
||||
// return |
|
||||
// } |
|
||||
|
|
||||
reviewButton!.title = currentItem!.name |
|
||||
|
|
||||
if currentItem!.type == ItemType.SNAPSHOT { |
|
||||
player.currentPlaybackTime = currentItem!.time! |
|
||||
currentItem = currentItem!.parent |
|
||||
} else { |
|
||||
if !currentItem!.children.isEmpty { |
|
||||
player.currentPlaybackTime = currentItem!.children[0].time! |
|
||||
} |
|
||||
else { |
|
||||
print(player.duration) |
|
||||
player.currentPlaybackTime = player.duration / 2 |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
navigationItem.rightBarButtonItems = [barbutton!] |
|
||||
|
|
||||
for c in currentItem!.children { |
|
||||
addItemButton(c) |
|
||||
} |
|
||||
|
|
||||
player.play() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@objc func enteredFullscreen() { |
|
||||
let mp = UIApplication.shared.keyWindow; |
|
||||
if let moviePlayerContainer = mp!.recursiveSearchForViewWithName("MPVideoContainerView") { |
|
||||
if (moviePlayerContainer.gestureRecognizers != nil) { |
|
||||
return; |
|
||||
} |
|
||||
installGestures(moviePlayerContainer) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@objc func exitedFullscreen() { |
|
||||
moviePlayer!.view.removeFromSuperview(); |
|
||||
moviePlayer = nil; |
|
||||
NotificationCenter.default.removeObserver(self); |
|
||||
|
|
||||
} |
|
||||
|
|
||||
func installGestures(_ moviePlayer: UIView) { |
|
||||
let twoFingersTwoTapsGesture = UITapGestureRecognizer(target: self, action: #selector(twoFingersTwoTaps)) |
|
||||
twoFingersTwoTapsGesture.numberOfTapsRequired = 2 |
|
||||
twoFingersTwoTapsGesture.numberOfTouchesRequired = 2 |
|
||||
moviePlayer.addGestureRecognizer(twoFingersTwoTapsGesture) |
|
||||
|
|
||||
let sR = UISwipeGestureRecognizer(target: self, action: #selector(swipeRight)) |
|
||||
sR.direction = UISwipeGestureRecognizer.Direction.right |
|
||||
sR.numberOfTouchesRequired = 1 |
|
||||
moviePlayer.addGestureRecognizer(sR) |
|
||||
|
|
||||
let sL = UISwipeGestureRecognizer(target: self, action: #selector(swipeLeft)) |
|
||||
sL.direction = UISwipeGestureRecognizer.Direction.left |
|
||||
sL.numberOfTouchesRequired = 1 |
|
||||
moviePlayer.addGestureRecognizer(sL) |
|
||||
|
|
||||
let sR2 = UISwipeGestureRecognizer(target: self, action: #selector(swipeDown)) |
|
||||
sR2.direction = UISwipeGestureRecognizer.Direction.down |
|
||||
sR2.numberOfTouchesRequired = 1 |
|
||||
moviePlayer.addGestureRecognizer(sR2) |
|
||||
|
|
||||
let sR3 = UISwipeGestureRecognizer(target: self, action: #selector(swipeUp)) |
|
||||
sR3.direction = UISwipeGestureRecognizer.Direction.up |
|
||||
sR3.numberOfTouchesRequired = 1 |
|
||||
moviePlayer.addGestureRecognizer(sR3) |
|
||||
|
|
||||
} |
|
||||
|
|
||||
@objc func swipeUp() { |
|
||||
print("u") |
|
||||
if let player = self.moviePlayer { |
|
||||
print("Type: \(currentItem!.type) Count: \(currentItem!.children.count) Index: \(index) Current: \(currentItem!.index)") |
|
||||
|
|
||||
if !edit && (currentItem!.children.isEmpty || !(index < currentItem!.children.count - 1)) { |
|
||||
print ("switch") |
|
||||
var newIndex = currentItem!.index + 1 |
|
||||
|
|
||||
if currentItem!.parent!.children.count <= newIndex { |
|
||||
newIndex = 0 |
|
||||
} |
|
||||
|
|
||||
currentItem = currentItem!.parent!.children[newIndex] |
|
||||
|
|
||||
print("'Switched Type: \(currentItem!.type) Count: \(currentItem!.children.count) Index: \(index) Current: \(currentItem!.index)") |
|
||||
|
|
||||
index = 0 |
|
||||
player.contentURL = currentItem!.playerURL |
|
||||
player.play() |
|
||||
Timer.scheduledTimer(timeInterval: 1.2, target: self, selector: #selector(update), userInfo: nil, repeats: false) |
|
||||
|
|
||||
return |
|
||||
} |
|
||||
|
|
||||
if !(currentItem!.children.isEmpty) { |
|
||||
print ("switch internal") |
|
||||
if index < currentItem!.children.count - 1 { |
|
||||
index+=1; |
|
||||
} else { |
|
||||
index = 0; |
|
||||
} |
|
||||
let child = currentItem!.children[index] |
|
||||
player.currentPlaybackTime = child.time! |
|
||||
moviePlayer!.currentPlaybackRate = Float(speedOptions[speedOption]) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@objc func swipeRight() { |
|
||||
print("r") |
|
||||
if let player = self.moviePlayer { |
|
||||
moviePlayer!.currentPlaybackRate = Float(0.0) |
|
||||
player.currentPlaybackTime = player.currentPlaybackTime - 30.0 |
|
||||
Timer.scheduledTimer(timeInterval: 0.9, |
|
||||
target: self, |
|
||||
selector: #selector(resumePlay), |
|
||||
userInfo: nil, |
|
||||
repeats: false) |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@objc func swipeDown() { |
|
||||
print("d") |
|
||||
|
|
||||
if let player = self.moviePlayer { |
|
||||
if !edit { |
|
||||
var newIndex = currentItem!.index - 1 |
|
||||
|
|
||||
if newIndex < 0 { |
|
||||
newIndex = 0 |
|
||||
} |
|
||||
|
|
||||
currentItem = currentItem!.parent!.children[newIndex] |
|
||||
index = 0; |
|
||||
|
|
||||
player.contentURL = currentItem!.playerURL |
|
||||
player.play() |
|
||||
Timer.scheduledTimer(timeInterval: 1.2, target: self, selector: #selector(update), userInfo: nil, repeats: false) |
|
||||
|
|
||||
return |
|
||||
} |
|
||||
player.currentPlaybackTime = player.currentPlaybackTime + 10.0 |
|
||||
Timer.scheduledTimer(timeInterval: 0.9, |
|
||||
target: self, |
|
||||
selector: #selector(resumePlay), |
|
||||
userInfo: nil, |
|
||||
repeats: false) |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@objc func swipeLeft() { |
|
||||
print("l") |
|
||||
if let player = self.moviePlayer { |
|
||||
moviePlayer!.currentPlaybackRate = Float(0.0) |
|
||||
player.currentPlaybackTime = player.currentPlaybackTime + 30.0 |
|
||||
Timer.scheduledTimer(timeInterval: 0.9, |
|
||||
target: self, |
|
||||
selector: #selector(resumePlay), |
|
||||
userInfo: nil, |
|
||||
repeats: false) |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@objc func resumePlay() { |
|
||||
moviePlayer!.currentPlaybackRate = Float(speedOptions[speedOption]) |
|
||||
print("resumePlay") |
|
||||
} |
|
||||
|
|
||||
@objc func twoFingersTwoTaps() { |
|
||||
if edit { |
|
||||
thumbnailTime = moviePlayer!.currentPlaybackTime |
|
||||
print("tap \(thumbnailTime)") |
|
||||
moviePlayer!.requestThumbnailImages(atTimes: [thumbnailTime], |
|
||||
timeOption: MPMovieTimeOption.exact); |
|
||||
} |
|
||||
else { |
|
||||
NetworkManager.sharedInstance.favItem(currentItem!) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
} |
|
||||
@ -0,0 +1,631 @@ |
|||||
|
// |
||||
|
// 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, 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) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
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 weak var delegate: BMPlayerDelegate? |
||||
|
|
||||
|
open var backBlock:((Bool) -> Void)? |
||||
|
|
||||
|
/// Gesture to change volume / brightness |
||||
|
open var panGesture: UIPanGestureRecognizer! |
||||
|
|
||||
|
/// 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 |
||||
|
fileprivate 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 |
||||
|
fileprivate var isPlayToTheEnd = false |
||||
|
//视频画面比例 |
||||
|
fileprivate var aspectRatio: BMPlayerAspectRatio = .default |
||||
|
|
||||
|
//Cache is playing result to improve callback performance |
||||
|
fileprivate var isPlayingCache: Bool? = nil |
||||
|
|
||||
|
// 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] |
||||
|
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() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
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) { |
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// MARK: - Action Response |
||||
|
|
||||
|
@objc open func panDirection(_ pan: UIPanGestureRecognizer) { |
||||
|
// 根据在view上Pan的位置,确定是调音量还是亮度 |
||||
|
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 x > y { |
||||
|
if BMPlayerConf.enablePlaytimeGestures { |
||||
|
self.panDirection = BMPanDirection.horizontal |
||||
|
|
||||
|
if locationPoint.y > self.bounds.size.height * 2 / 3 { |
||||
|
self.isSkip = false |
||||
|
} else { |
||||
|
self.isSkip = true |
||||
|
self.skipAmount = 0.0 |
||||
|
} |
||||
|
|
||||
|
// 给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: |
||||
|
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 { |
||||
|
print( skipAmount) |
||||
|
if (skipAmount > 1000) { |
||||
|
self.sumTime += TimeInterval(30) |
||||
|
} |
||||
|
else if skipAmount > 0 { |
||||
|
self.sumTime += TimeInterval(10) |
||||
|
} |
||||
|
else if skipAmount > -1000 { |
||||
|
self.sumTime -= TimeInterval(10) |
||||
|
} |
||||
|
else { |
||||
|
self.sumTime -= TimeInterval(30) |
||||
|
} |
||||
|
} |
||||
|
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 |
||||
|
|
||||
|
case BMPanDirection.vertical: |
||||
|
self.isVolume = false |
||||
|
} |
||||
|
default: |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
open func verticalMoved(_ value: CGFloat) { |
||||
|
print(value) |
||||
|
if (value < -100) { |
||||
|
// controlView(controlView: controlView, didChooseDefinition: currentDefinition+1); |
||||
|
} |
||||
|
// if BMPlayerConf.enableVolumeGestures && self.isVolume{ |
||||
|
// self.volumeViewSlider.value -= Float(value / 10000) |
||||
|
// } |
||||
|
// else if BMPlayerConf.enableBrightnessGestures && !self.isVolume{ |
||||
|
// UIScreen.main.brightness -= value / 10000 |
||||
|
// } |
||||
|
} |
||||
|
|
||||
|
open func horizontalMoved(_ value: CGFloat) { |
||||
|
guard BMPlayerConf.enablePlaytimeGestures else { return } |
||||
|
|
||||
|
isSliderSliding = true |
||||
|
|
||||
|
if !isSkip { |
||||
|
if let playerItem = playerLayer?.playerItem { |
||||
|
// 每次滑动需要叠加时间,通过一定的比例,使滑动一直处于统一水平 |
||||
|
self.sumTime = self.sumTime + TimeInterval(value) / 100.0 * (TimeInterval(self.totalDuration) / 400) |
||||
|
|
||||
|
let totalTime = playerItem.duration |
||||
|
|
||||
|
// 防止出现NAN |
||||
|
if totalTime.timescale == 0 { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let totalDuration = TimeInterval(totalTime.value) / TimeInterval(totalTime.timescale) |
||||
|
if (self.sumTime >= totalDuration) { |
||||
|
self.sumTime = totalDuration |
||||
|
} |
||||
|
if (self.sumTime <= 0) { |
||||
|
self.sumTime = 0 |
||||
|
} |
||||
|
|
||||
|
controlView.showSeekToView(to: sumTime, total: totalDuration, isAdd: value > 0) |
||||
|
} |
||||
|
} |
||||
|
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() |
||||
|
} |
||||
|
|
||||
|
@available(*, deprecated:3.0, message:"Use newer init(customControlView:_)") |
||||
|
public convenience init(customControllView: BMPlayerControlView?) { |
||||
|
self.init(customControlView: customControllView) |
||||
|
} |
||||
|
|
||||
|
public init(customControlView: BMPlayerControlView?) { |
||||
|
super.init(frame:CGRect.zero) |
||||
|
self.customControlView = customControlView |
||||
|
initUI() |
||||
|
initUIData() |
||||
|
configureVolume() |
||||
|
preparePlayer() |
||||
|
} |
||||
|
|
||||
|
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 .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 |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
// |
||||
|
// File.swift |
||||
|
// Pods |
||||
|
// |
||||
|
// Created by BrikerMan on 16/5/21. |
||||
|
// |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
|
||||
|
class BMPlayerClearityChooseButton: UIButton { |
||||
|
override init(frame: CGRect) { |
||||
|
super.init(frame: frame) |
||||
|
initUI() |
||||
|
} |
||||
|
|
||||
|
required init?(coder aDecoder: NSCoder) { |
||||
|
super.init(coder: aDecoder) |
||||
|
initUI() |
||||
|
} |
||||
|
|
||||
|
func initUI() { |
||||
|
self.titleLabel?.font = UIFont.systemFont(ofSize: 12) |
||||
|
self.layer.cornerRadius = 2 |
||||
|
self.layer.borderWidth = 1 |
||||
|
self.layer.borderColor = UIColor(white: 1, alpha: 0.8).cgColor |
||||
|
self.setTitleColor(UIColor(white: 1, alpha: 0.9), for: .normal) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,769 @@ |
|||||
|
// |
||||
|
// BMPlayerControlView.swift |
||||
|
// Pods |
||||
|
// |
||||
|
// Created by BrikerMan on 16/4/29. |
||||
|
// |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
import NVActivityIndicatorView |
||||
|
|
||||
|
|
||||
|
@objc public protocol BMPlayerControlViewDelegate: class { |
||||
|
/** |
||||
|
call when control view choose a definition |
||||
|
|
||||
|
- parameter controlView: control view |
||||
|
- parameter index: index of definition |
||||
|
*/ |
||||
|
func controlView(controlView: BMPlayerControlView, didChooseDefinition index: Int) |
||||
|
|
||||
|
/** |
||||
|
call when control view pressed an button |
||||
|
|
||||
|
- parameter controlView: control view |
||||
|
- parameter button: button type |
||||
|
*/ |
||||
|
func controlView(controlView: BMPlayerControlView, didPressButton button: UIButton) |
||||
|
|
||||
|
/** |
||||
|
call when slider action trigged |
||||
|
|
||||
|
- parameter controlView: control view |
||||
|
- parameter slider: progress slider |
||||
|
- parameter event: action |
||||
|
*/ |
||||
|
func controlView(controlView: BMPlayerControlView, slider: UISlider, onSliderEvent event: UIControl.Event) |
||||
|
|
||||
|
/** |
||||
|
call when needs to change playback rate |
||||
|
|
||||
|
- parameter controlView: control view |
||||
|
- parameter rate: playback rate |
||||
|
*/ |
||||
|
@objc optional func controlView(controlView: BMPlayerControlView, didChangeVideoPlaybackRate rate: Float) |
||||
|
} |
||||
|
|
||||
|
open class BMPlayerControlView: UIView { |
||||
|
|
||||
|
open weak var delegate: BMPlayerControlViewDelegate? |
||||
|
open weak var player: BMPlayer? |
||||
|
|
||||
|
// MARK: Variables |
||||
|
open var resource: BMPlayerResource? |
||||
|
|
||||
|
open var selectedIndex = 0 |
||||
|
open var isFullscreen = false |
||||
|
open var isMaskShowing = true |
||||
|
|
||||
|
open var totalDuration: TimeInterval = 0 |
||||
|
open var delayItem: DispatchWorkItem? |
||||
|
|
||||
|
var playerLastState: BMPlayerState = .notSetURL |
||||
|
|
||||
|
fileprivate var isSelectDefinitionViewOpened = false |
||||
|
|
||||
|
// MARK: UI Components |
||||
|
/// main views which contains the topMaskView and bottom mask view |
||||
|
open var mainMaskView = UIView() |
||||
|
open var topMaskView = UIView() |
||||
|
open var bottomMaskView = UIView() |
||||
|
|
||||
|
/// Image view to show video cover |
||||
|
open var maskImageView = UIImageView() |
||||
|
|
||||
|
/// top views |
||||
|
open var topWrapperView = UIView() |
||||
|
open var backButton = UIButton(type : UIButton.ButtonType.custom) |
||||
|
open var titleLabel = UILabel() |
||||
|
open var chooseDefinitionView = UIView() |
||||
|
|
||||
|
/// bottom view |
||||
|
open var bottomWrapperView = UIView() |
||||
|
open var currentTimeLabel = UILabel() |
||||
|
open var totalTimeLabel = UILabel() |
||||
|
|
||||
|
/// Progress slider |
||||
|
open var timeSlider = BMTimeSlider() |
||||
|
|
||||
|
/// load progress view |
||||
|
open var progressView = UIProgressView() |
||||
|
|
||||
|
/* play button |
||||
|
playButton.isSelected = player.isPlaying |
||||
|
*/ |
||||
|
open var playButton = UIButton(type: UIButton.ButtonType.custom) |
||||
|
|
||||
|
/* fullScreen button |
||||
|
fullScreenButton.isSelected = player.isFullscreen |
||||
|
*/ |
||||
|
open var fullscreenButton = UIButton(type: UIButton.ButtonType.custom) |
||||
|
|
||||
|
open var subtitleLabel = UILabel() |
||||
|
open var subtitleBackView = UIView() |
||||
|
open var subtileAttribute: [NSAttributedString.Key : Any]? |
||||
|
|
||||
|
/// Activty Indector for loading |
||||
|
open var loadingIndicator = NVActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) |
||||
|
|
||||
|
open var seekToView = UIView() |
||||
|
open var seekToViewImage = UIImageView() |
||||
|
open var seekToLabel = UILabel() |
||||
|
|
||||
|
open var replayButton = UIButton(type: UIButton.ButtonType.custom) |
||||
|
|
||||
|
/// Gesture used to show / hide control view |
||||
|
open var tapGesture: UITapGestureRecognizer! |
||||
|
open var doubleTapGesture: UITapGestureRecognizer! |
||||
|
|
||||
|
// MARK: - handle player state change |
||||
|
/** |
||||
|
call on when play time changed, update duration here |
||||
|
|
||||
|
- parameter currentTime: current play time |
||||
|
- parameter totalTime: total duration |
||||
|
*/ |
||||
|
open func playTimeDidChange(currentTime: TimeInterval, totalTime: TimeInterval) { |
||||
|
currentTimeLabel.text = BMPlayer.formatSecondsToString(currentTime) |
||||
|
totalTimeLabel.text = BMPlayer.formatSecondsToString(totalTime) |
||||
|
timeSlider.value = Float(currentTime) / Float(totalTime) |
||||
|
showSubtile(from: resource?.subtitle, at: currentTime) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
change subtitle resource |
||||
|
|
||||
|
- Parameter subtitles: new subtitle object |
||||
|
*/ |
||||
|
open func update(subtitles: BMSubtitles?) { |
||||
|
resource?.subtitle = subtitles |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
call on load duration changed, update load progressView here |
||||
|
|
||||
|
- parameter loadedDuration: loaded duration |
||||
|
- parameter totalDuration: total duration |
||||
|
*/ |
||||
|
open func loadedTimeDidChange(loadedDuration: TimeInterval, totalDuration: TimeInterval) { |
||||
|
progressView.setProgress(Float(loadedDuration)/Float(totalDuration), animated: true) |
||||
|
} |
||||
|
|
||||
|
open func playerStateDidChange(state: BMPlayerState) { |
||||
|
switch state { |
||||
|
case .readyToPlay: |
||||
|
hideLoader() |
||||
|
|
||||
|
case .buffering: |
||||
|
showLoader() |
||||
|
|
||||
|
case .bufferFinished: |
||||
|
hideLoader() |
||||
|
|
||||
|
case .playedToTheEnd: |
||||
|
playButton.isSelected = false |
||||
|
showPlayToTheEndView() |
||||
|
controlViewAnimation(isShow: true) |
||||
|
|
||||
|
default: |
||||
|
break |
||||
|
} |
||||
|
playerLastState = state |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
Call when User use the slide to seek function |
||||
|
|
||||
|
- parameter toSecound: target time |
||||
|
- parameter totalDuration: total duration of the video |
||||
|
- parameter isAdd: isAdd |
||||
|
*/ |
||||
|
open func showSeekToView(to toSecound: TimeInterval, total totalDuration:TimeInterval, isAdd: Bool) { |
||||
|
seekToView.isHidden = false |
||||
|
seekToLabel.text = BMPlayer.formatSecondsToString(toSecound) |
||||
|
|
||||
|
let rotate = isAdd ? 0 : CGFloat(Double.pi) |
||||
|
seekToViewImage.transform = CGAffineTransform(rotationAngle: rotate) |
||||
|
|
||||
|
let targetTime = BMPlayer.formatSecondsToString(toSecound) |
||||
|
timeSlider.value = Float(toSecound / totalDuration) |
||||
|
currentTimeLabel.text = targetTime |
||||
|
} |
||||
|
|
||||
|
// MARK: - UI update related function |
||||
|
/** |
||||
|
Update UI details when player set with the resource |
||||
|
|
||||
|
- parameter resource: video resouce |
||||
|
- parameter index: defualt definition's index |
||||
|
*/ |
||||
|
open func prepareUI(for resource: BMPlayerResource, selectedIndex index: Int) { |
||||
|
self.resource = resource |
||||
|
self.selectedIndex = index |
||||
|
titleLabel.text = resource.name |
||||
|
prepareChooseDefinitionView() |
||||
|
autoFadeOutControlViewWithAnimation() |
||||
|
} |
||||
|
|
||||
|
open func playStateDidChange(isPlaying: Bool) { |
||||
|
autoFadeOutControlViewWithAnimation() |
||||
|
playButton.isSelected = isPlaying |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
auto fade out controll view with animtion |
||||
|
*/ |
||||
|
open func autoFadeOutControlViewWithAnimation() { |
||||
|
cancelAutoFadeOutAnimation() |
||||
|
delayItem = DispatchWorkItem { [weak self] in |
||||
|
if self?.playerLastState != .playedToTheEnd { |
||||
|
self?.controlViewAnimation(isShow: false) |
||||
|
} |
||||
|
} |
||||
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + BMPlayerConf.animateDelayTimeInterval, |
||||
|
execute: delayItem!) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
cancel auto fade out controll view with animtion |
||||
|
*/ |
||||
|
open func cancelAutoFadeOutAnimation() { |
||||
|
delayItem?.cancel() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
Implement of the control view animation, override if need's custom animation |
||||
|
|
||||
|
- parameter isShow: is to show the controlview |
||||
|
*/ |
||||
|
open func controlViewAnimation(isShow: Bool) { |
||||
|
let alpha: CGFloat = isShow ? 1.0 : 0.0 |
||||
|
self.isMaskShowing = isShow |
||||
|
|
||||
|
UIApplication.shared.setStatusBarHidden(!isShow, with: .fade) |
||||
|
|
||||
|
UIView.animate(withDuration: 0.3, animations: {[weak self] in |
||||
|
guard let wSelf = self else { return } |
||||
|
wSelf.topMaskView.alpha = alpha |
||||
|
|
||||
|
wSelf.bottomMaskView.alpha = alpha |
||||
|
wSelf.mainMaskView.backgroundColor = UIColor(white: 0, alpha: isShow ? 0.0 : 0.0) // todo marco |
||||
|
|
||||
|
if isShow { |
||||
|
if wSelf.isFullscreen { wSelf.chooseDefinitionView.alpha = 1.0 } |
||||
|
} else { |
||||
|
wSelf.replayButton.isHidden = true |
||||
|
wSelf.chooseDefinitionView.snp.updateConstraints { (make) in |
||||
|
make.height.equalTo(35) |
||||
|
} |
||||
|
wSelf.chooseDefinitionView.alpha = 0.0 |
||||
|
} |
||||
|
wSelf.layoutIfNeeded() |
||||
|
}) { [weak self](_) in |
||||
|
if isShow { |
||||
|
self?.autoFadeOutControlViewWithAnimation() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
Implement of the UI update when screen orient changed |
||||
|
|
||||
|
- parameter isForFullScreen: is for full screen |
||||
|
*/ |
||||
|
open func updateUI(_ isForFullScreen: Bool) { |
||||
|
isFullscreen = isForFullScreen |
||||
|
fullscreenButton.isSelected = isForFullScreen |
||||
|
chooseDefinitionView.isHidden = !BMPlayerConf.enableChooseDefinition || !isForFullScreen |
||||
|
if isForFullScreen { |
||||
|
if BMPlayerConf.topBarShowInCase.rawValue == 2 { |
||||
|
topMaskView.isHidden = true |
||||
|
} else { |
||||
|
topMaskView.isHidden = false |
||||
|
} |
||||
|
} else { |
||||
|
if BMPlayerConf.topBarShowInCase.rawValue >= 1 { |
||||
|
topMaskView.isHidden = true |
||||
|
} else { |
||||
|
topMaskView.isHidden = false |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
Call when video play's to the end, override if you need custom UI or animation when played to the end |
||||
|
*/ |
||||
|
open func showPlayToTheEndView() { |
||||
|
replayButton.isHidden = false |
||||
|
} |
||||
|
|
||||
|
open func hidePlayToTheEndView() { |
||||
|
replayButton.isHidden = true |
||||
|
} |
||||
|
|
||||
|
open func showLoader() { |
||||
|
loadingIndicator.isHidden = false |
||||
|
loadingIndicator.startAnimating() |
||||
|
} |
||||
|
|
||||
|
open func hideLoader() { |
||||
|
loadingIndicator.isHidden = true |
||||
|
} |
||||
|
|
||||
|
open func hideSeekToView() { |
||||
|
seekToView.isHidden = true |
||||
|
} |
||||
|
|
||||
|
open func showCoverWithLink(_ cover:String) { |
||||
|
self.showCover(url: URL(string: cover)) |
||||
|
} |
||||
|
|
||||
|
open func showCover(url: URL?) { |
||||
|
if let url = url { |
||||
|
DispatchQueue.global(qos: .default).async { [weak self] in |
||||
|
let data = try? Data(contentsOf: url) |
||||
|
DispatchQueue.main.async(execute: { [weak self] in |
||||
|
guard let `self` = self else { return } |
||||
|
if let data = data { |
||||
|
self.maskImageView.image = UIImage(data: data) |
||||
|
} else { |
||||
|
self.maskImageView.image = nil |
||||
|
} |
||||
|
self.hideLoader() |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
open func hideCoverImageView() { |
||||
|
self.maskImageView.isHidden = true |
||||
|
} |
||||
|
|
||||
|
open func prepareChooseDefinitionView() { |
||||
|
guard let resource = resource else { |
||||
|
return |
||||
|
} |
||||
|
for item in chooseDefinitionView.subviews { |
||||
|
item.removeFromSuperview() |
||||
|
} |
||||
|
|
||||
|
for i in 0..<resource.definitions.count { |
||||
|
let button = BMPlayerClearityChooseButton() |
||||
|
|
||||
|
if i == 0 { |
||||
|
button.tag = selectedIndex |
||||
|
} else if i <= selectedIndex { |
||||
|
button.tag = i - 1 |
||||
|
} else { |
||||
|
button.tag = i |
||||
|
} |
||||
|
|
||||
|
button.setTitle("\(resource.definitions[button.tag].definition)", for: UIControl.State()) |
||||
|
chooseDefinitionView.addSubview(button) |
||||
|
button.addTarget(self, action: #selector(self.onDefinitionSelected(_:)), for: UIControl.Event.touchUpInside) |
||||
|
button.snp.makeConstraints({ [weak self](make) in |
||||
|
guard let `self` = self else { return } |
||||
|
make.top.equalTo(chooseDefinitionView.snp.top).offset(35 * i) |
||||
|
make.width.equalTo(50) |
||||
|
make.height.equalTo(25) |
||||
|
make.centerX.equalTo(chooseDefinitionView) |
||||
|
}) |
||||
|
|
||||
|
if resource.definitions.count == 1 { |
||||
|
button.isEnabled = false |
||||
|
button.isHidden = true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
open func prepareToDealloc() { |
||||
|
self.delayItem = nil |
||||
|
} |
||||
|
|
||||
|
// MARK: - Action Response |
||||
|
/** |
||||
|
Call when some action button Pressed |
||||
|
|
||||
|
- parameter button: action Button |
||||
|
*/ |
||||
|
@objc open func onButtonPressed(_ button: UIButton) { |
||||
|
autoFadeOutControlViewWithAnimation() |
||||
|
if let type = ButtonType(rawValue: button.tag) { |
||||
|
switch type { |
||||
|
case .play, .replay: |
||||
|
if playerLastState == .playedToTheEnd { |
||||
|
hidePlayToTheEndView() |
||||
|
} |
||||
|
default: |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
delegate?.controlView(controlView: self, didPressButton: button) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
Call when the tap gesture tapped |
||||
|
|
||||
|
- parameter gesture: tap gesture |
||||
|
*/ |
||||
|
@objc open func onTapGestureTapped(_ gesture: UITapGestureRecognizer) { |
||||
|
if playerLastState == .playedToTheEnd { |
||||
|
return |
||||
|
} |
||||
|
controlViewAnimation(isShow: !isMaskShowing) |
||||
|
} |
||||
|
|
||||
|
@objc open func onDoubleTapGestureRecognized(_ gesture: UITapGestureRecognizer) { |
||||
|
guard let player = player else { return } |
||||
|
guard playerLastState == .readyToPlay || playerLastState == .buffering || playerLastState == .bufferFinished else { return } |
||||
|
|
||||
|
if player.isPlaying { |
||||
|
player.pause() |
||||
|
} else { |
||||
|
player.play() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - handle UI slider actions |
||||
|
@objc func progressSliderTouchBegan(_ sender: UISlider) { |
||||
|
delegate?.controlView(controlView: self, slider: sender, onSliderEvent: .touchDown) |
||||
|
} |
||||
|
|
||||
|
@objc func progressSliderValueChanged(_ sender: UISlider) { |
||||
|
hidePlayToTheEndView() |
||||
|
cancelAutoFadeOutAnimation() |
||||
|
let currentTime = Double(sender.value) * totalDuration |
||||
|
currentTimeLabel.text = BMPlayer.formatSecondsToString(currentTime) |
||||
|
delegate?.controlView(controlView: self, slider: sender, onSliderEvent: .valueChanged) |
||||
|
} |
||||
|
|
||||
|
@objc func progressSliderTouchEnded(_ sender: UISlider) { |
||||
|
autoFadeOutControlViewWithAnimation() |
||||
|
delegate?.controlView(controlView: self, slider: sender, onSliderEvent: .touchUpInside) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// MARK: - private functions |
||||
|
fileprivate func showSubtile(from subtitle: BMSubtitles?, at time: TimeInterval) { |
||||
|
if let subtitle = subtitle, let group = subtitle.search(for: time) { |
||||
|
subtitleBackView.isHidden = false |
||||
|
subtitleLabel.attributedText = NSAttributedString(string: group.text, |
||||
|
attributes: subtileAttribute) |
||||
|
} else { |
||||
|
subtitleBackView.isHidden = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@objc fileprivate func onDefinitionSelected(_ button:UIButton) { |
||||
|
let height = isSelectDefinitionViewOpened ? 35 : resource!.definitions.count * 40 |
||||
|
chooseDefinitionView.snp.updateConstraints { (make) in |
||||
|
make.height.equalTo(height) |
||||
|
} |
||||
|
|
||||
|
UIView.animate(withDuration: 0.3, animations: {[weak self] in |
||||
|
self?.layoutIfNeeded() |
||||
|
}) |
||||
|
isSelectDefinitionViewOpened = !isSelectDefinitionViewOpened |
||||
|
if selectedIndex != button.tag { |
||||
|
selectedIndex = button.tag |
||||
|
delegate?.controlView(controlView: self, didChooseDefinition: button.tag) |
||||
|
} |
||||
|
prepareChooseDefinitionView() |
||||
|
} |
||||
|
|
||||
|
@objc fileprivate func onReplyButtonPressed() { |
||||
|
replayButton.isHidden = true |
||||
|
} |
||||
|
|
||||
|
// MARK: - Init |
||||
|
override public init(frame: CGRect) { |
||||
|
super.init(frame: frame) |
||||
|
setupUIComponents() |
||||
|
addSnapKitConstraint() |
||||
|
customizeUIComponents() |
||||
|
} |
||||
|
|
||||
|
required public init?(coder aDecoder: NSCoder) { |
||||
|
super.init(coder: aDecoder) |
||||
|
setupUIComponents() |
||||
|
addSnapKitConstraint() |
||||
|
customizeUIComponents() |
||||
|
} |
||||
|
|
||||
|
/// Add Customize functions here |
||||
|
open func customizeUIComponents() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
func setupUIComponents() { |
||||
|
// Subtile view |
||||
|
subtitleLabel.numberOfLines = 0 |
||||
|
subtitleLabel.textAlignment = .center |
||||
|
subtitleLabel.textColor = UIColor.white |
||||
|
subtitleLabel.adjustsFontSizeToFitWidth = true |
||||
|
subtitleLabel.minimumScaleFactor = 0.5 |
||||
|
subtitleLabel.font = UIFont.systemFont(ofSize: 13) |
||||
|
|
||||
|
subtitleBackView.layer.cornerRadius = 2 |
||||
|
subtitleBackView.backgroundColor = UIColor.black.withAlphaComponent(0.4) |
||||
|
subtitleBackView.addSubview(subtitleLabel) |
||||
|
subtitleBackView.isHidden = true |
||||
|
|
||||
|
addSubview(subtitleBackView) |
||||
|
|
||||
|
// Main mask view |
||||
|
addSubview(mainMaskView) |
||||
|
mainMaskView.addSubview(topMaskView) |
||||
|
mainMaskView.addSubview(bottomMaskView) |
||||
|
mainMaskView.insertSubview(maskImageView, at: 0) |
||||
|
mainMaskView.clipsToBounds = true |
||||
|
mainMaskView.backgroundColor = UIColor(white: 0, alpha: 0.4 ) |
||||
|
|
||||
|
// Top views |
||||
|
topMaskView.addSubview(topWrapperView) |
||||
|
topWrapperView.addSubview(backButton) |
||||
|
topWrapperView.addSubview(titleLabel) |
||||
|
topWrapperView.addSubview(chooseDefinitionView) |
||||
|
|
||||
|
backButton.tag = BMPlayerControlView.ButtonType.back.rawValue |
||||
|
backButton.setImage(BMImageResourcePath("Pod_Asset_BMPlayer_back"), for: .normal) |
||||
|
backButton.addTarget(self, action: #selector(onButtonPressed(_:)), for: .touchUpInside) |
||||
|
|
||||
|
titleLabel.textColor = UIColor.white |
||||
|
titleLabel.text = "" |
||||
|
titleLabel.font = UIFont.systemFont(ofSize: 16) |
||||
|
|
||||
|
chooseDefinitionView.clipsToBounds = true |
||||
|
|
||||
|
// Bottom views |
||||
|
bottomMaskView.addSubview(bottomWrapperView) |
||||
|
bottomWrapperView.addSubview(playButton) |
||||
|
bottomWrapperView.addSubview(currentTimeLabel) |
||||
|
bottomWrapperView.addSubview(totalTimeLabel) |
||||
|
bottomWrapperView.addSubview(progressView) |
||||
|
bottomWrapperView.addSubview(timeSlider) |
||||
|
bottomWrapperView.addSubview(fullscreenButton) |
||||
|
|
||||
|
playButton.tag = BMPlayerControlView.ButtonType.play.rawValue |
||||
|
playButton.setImage(BMImageResourcePath("Pod_Asset_BMPlayer_play"), for: .normal) |
||||
|
playButton.setImage(BMImageResourcePath("Pod_Asset_BMPlayer_pause"), for: .selected) |
||||
|
playButton.addTarget(self, action: #selector(onButtonPressed(_:)), for: .touchUpInside) |
||||
|
|
||||
|
currentTimeLabel.textColor = UIColor.white |
||||
|
currentTimeLabel.font = UIFont.systemFont(ofSize: 12) |
||||
|
currentTimeLabel.text = "00:00" |
||||
|
currentTimeLabel.textAlignment = NSTextAlignment.center |
||||
|
|
||||
|
totalTimeLabel.textColor = UIColor.white |
||||
|
totalTimeLabel.font = UIFont.systemFont(ofSize: 12) |
||||
|
totalTimeLabel.text = "00:00" |
||||
|
totalTimeLabel.textAlignment = NSTextAlignment.center |
||||
|
|
||||
|
|
||||
|
timeSlider.maximumValue = 1.0 |
||||
|
timeSlider.minimumValue = 0.0 |
||||
|
timeSlider.value = 0.0 |
||||
|
timeSlider.setThumbImage(BMImageResourcePath("Pod_Asset_BMPlayer_slider_thumb"), for: .normal) |
||||
|
|
||||
|
timeSlider.maximumTrackTintColor = UIColor.clear |
||||
|
timeSlider.minimumTrackTintColor = BMPlayerConf.tintColor |
||||
|
|
||||
|
timeSlider.addTarget(self, action: #selector(progressSliderTouchBegan(_:)), |
||||
|
for: UIControl.Event.touchDown) |
||||
|
|
||||
|
timeSlider.addTarget(self, action: #selector(progressSliderValueChanged(_:)), |
||||
|
for: UIControl.Event.valueChanged) |
||||
|
|
||||
|
timeSlider.addTarget(self, action: #selector(progressSliderTouchEnded(_:)), |
||||
|
for: [UIControl.Event.touchUpInside,UIControl.Event.touchCancel, UIControl.Event.touchUpOutside]) |
||||
|
|
||||
|
progressView.tintColor = UIColor ( red: 1.0, green: 1.0, blue: 1.0, alpha: 0.6 ) |
||||
|
progressView.trackTintColor = UIColor ( red: 1.0, green: 1.0, blue: 1.0, alpha: 0.3 ) |
||||
|
|
||||
|
fullscreenButton.tag = BMPlayerControlView.ButtonType.fullscreen.rawValue |
||||
|
fullscreenButton.setImage(BMImageResourcePath("Pod_Asset_BMPlayer_fullscreen"), for: .normal) |
||||
|
fullscreenButton.setImage(BMImageResourcePath("Pod_Asset_BMPlayer_portialscreen"), for: .selected) |
||||
|
fullscreenButton.addTarget(self, action: #selector(onButtonPressed(_:)), for: .touchUpInside) |
||||
|
|
||||
|
mainMaskView.addSubview(loadingIndicator) |
||||
|
|
||||
|
loadingIndicator.type = BMPlayerConf.loaderType |
||||
|
loadingIndicator.color = BMPlayerConf.tintColor |
||||
|
|
||||
|
// View to show when slide to seek |
||||
|
addSubview(seekToView) |
||||
|
seekToView.addSubview(seekToViewImage) |
||||
|
seekToView.addSubview(seekToLabel) |
||||
|
|
||||
|
seekToLabel.font = UIFont.systemFont(ofSize: 13) |
||||
|
seekToLabel.textColor = UIColor ( red: 0.9098, green: 0.9098, blue: 0.9098, alpha: 1.0 ) |
||||
|
seekToView.backgroundColor = UIColor ( red: 0.0, green: 0.0, blue: 0.0, alpha: 0.7 ) |
||||
|
seekToView.layer.cornerRadius = 4 |
||||
|
seekToView.layer.masksToBounds = true |
||||
|
seekToView.isHidden = true |
||||
|
|
||||
|
seekToViewImage.image = BMImageResourcePath("Pod_Asset_BMPlayer_seek_to_image") |
||||
|
|
||||
|
addSubview(replayButton) |
||||
|
replayButton.isHidden = true |
||||
|
replayButton.setImage(BMImageResourcePath("Pod_Asset_BMPlayer_replay"), for: .normal) |
||||
|
replayButton.addTarget(self, action: #selector(onButtonPressed(_:)), for: .touchUpInside) |
||||
|
replayButton.tag = ButtonType.replay.rawValue |
||||
|
|
||||
|
tapGesture = UITapGestureRecognizer(target: self, action: #selector(onTapGestureTapped(_:))) |
||||
|
addGestureRecognizer(tapGesture) |
||||
|
|
||||
|
if BMPlayerManager.shared.enablePlayControlGestures { |
||||
|
doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(onDoubleTapGestureRecognized(_:))) |
||||
|
doubleTapGesture.numberOfTapsRequired = 2 |
||||
|
addGestureRecognizer(doubleTapGesture) |
||||
|
|
||||
|
tapGesture.require(toFail: doubleTapGesture) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func addSnapKitConstraint() { |
||||
|
// Main mask view |
||||
|
mainMaskView.snp.makeConstraints { [unowned self](make) in |
||||
|
make.edges.equalTo(self) |
||||
|
} |
||||
|
|
||||
|
maskImageView.snp.makeConstraints { [unowned self](make) in |
||||
|
make.edges.equalTo(self.mainMaskView) |
||||
|
} |
||||
|
|
||||
|
topMaskView.snp.makeConstraints { [unowned self](make) in |
||||
|
make.top.left.right.equalTo(self.mainMaskView) |
||||
|
} |
||||
|
|
||||
|
topWrapperView.snp.makeConstraints { [unowned self](make) in |
||||
|
make.height.equalTo(50) |
||||
|
if #available(iOS 11.0, *) { |
||||
|
make.top.left.right.equalTo(self.topMaskView.safeAreaLayoutGuide) |
||||
|
make.bottom.equalToSuperview() |
||||
|
} else { |
||||
|
make.top.equalToSuperview().offset(15) |
||||
|
make.bottom.left.right.equalToSuperview() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
bottomMaskView.snp.makeConstraints { [unowned self](make) in |
||||
|
make.bottom.left.right.equalTo(self.mainMaskView) |
||||
|
} |
||||
|
|
||||
|
bottomWrapperView.snp.makeConstraints { [unowned self](make) in |
||||
|
make.height.equalTo(50) |
||||
|
if #available(iOS 11.0, *) { |
||||
|
make.bottom.left.right.equalTo(self.bottomMaskView.safeAreaLayoutGuide) |
||||
|
make.top.equalToSuperview() |
||||
|
} else { |
||||
|
make.edges.equalToSuperview() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Top views |
||||
|
backButton.snp.makeConstraints { (make) in |
||||
|
make.width.height.equalTo(50) |
||||
|
make.left.bottom.equalToSuperview() |
||||
|
} |
||||
|
|
||||
|
titleLabel.snp.makeConstraints { [unowned self](make) in |
||||
|
make.left.equalTo(self.backButton.snp.right) |
||||
|
make.centerY.equalTo(self.backButton) |
||||
|
} |
||||
|
|
||||
|
chooseDefinitionView.snp.makeConstraints { [unowned self](make) in |
||||
|
make.right.equalToSuperview().offset(-20) |
||||
|
make.top.equalTo(self.titleLabel.snp.top).offset(-4) |
||||
|
make.width.equalTo(60) |
||||
|
make.height.equalTo(30) |
||||
|
} |
||||
|
|
||||
|
// Bottom views |
||||
|
playButton.snp.makeConstraints { (make) in |
||||
|
make.width.equalTo(50) |
||||
|
make.height.equalTo(50) |
||||
|
make.left.bottom.equalToSuperview() |
||||
|
} |
||||
|
|
||||
|
currentTimeLabel.snp.makeConstraints { [unowned self](make) in |
||||
|
make.left.equalTo(self.playButton.snp.right) |
||||
|
make.centerY.equalTo(self.playButton) |
||||
|
make.width.equalTo(40) |
||||
|
} |
||||
|
|
||||
|
timeSlider.snp.makeConstraints { [unowned self](make) in |
||||
|
make.centerY.equalTo(self.currentTimeLabel) |
||||
|
make.left.equalTo(self.currentTimeLabel.snp.right).offset(10).priority(750) |
||||
|
make.height.equalTo(30) |
||||
|
} |
||||
|
|
||||
|
progressView.snp.makeConstraints { [unowned self](make) in |
||||
|
make.centerY.left.right.equalTo(self.timeSlider) |
||||
|
make.height.equalTo(2) |
||||
|
} |
||||
|
|
||||
|
totalTimeLabel.snp.makeConstraints { [unowned self](make) in |
||||
|
make.centerY.equalTo(self.currentTimeLabel) |
||||
|
make.left.equalTo(self.timeSlider.snp.right).offset(5) |
||||
|
make.width.equalTo(40) |
||||
|
} |
||||
|
|
||||
|
fullscreenButton.snp.makeConstraints { [unowned self](make) in |
||||
|
make.width.equalTo(50) |
||||
|
make.height.equalTo(50) |
||||
|
make.centerY.equalTo(self.currentTimeLabel) |
||||
|
make.left.equalTo(self.totalTimeLabel.snp.right) |
||||
|
make.right.equalToSuperview() |
||||
|
} |
||||
|
|
||||
|
loadingIndicator.snp.makeConstraints { [unowned self](make) in |
||||
|
make.center.equalTo(self.mainMaskView) |
||||
|
} |
||||
|
|
||||
|
// View to show when slide to seek |
||||
|
seekToView.snp.makeConstraints { [unowned self](make) in |
||||
|
make.center.equalTo(self.snp.center) |
||||
|
make.width.equalTo(100) |
||||
|
make.height.equalTo(40) |
||||
|
} |
||||
|
|
||||
|
seekToViewImage.snp.makeConstraints { [unowned self](make) in |
||||
|
make.left.equalTo(self.seekToView.snp.left).offset(15) |
||||
|
make.centerY.equalTo(self.seekToView.snp.centerY) |
||||
|
make.height.equalTo(15) |
||||
|
make.width.equalTo(25) |
||||
|
} |
||||
|
|
||||
|
seekToLabel.snp.makeConstraints { [unowned self](make) in |
||||
|
make.left.equalTo(self.seekToViewImage.snp.right).offset(10) |
||||
|
make.centerY.equalTo(self.seekToView.snp.centerY) |
||||
|
} |
||||
|
|
||||
|
replayButton.snp.makeConstraints { [unowned self](make) in |
||||
|
make.center.equalTo(self.mainMaskView) |
||||
|
make.width.height.equalTo(50) |
||||
|
} |
||||
|
|
||||
|
subtitleBackView.snp.makeConstraints { [unowned self](make) in |
||||
|
make.bottom.equalTo(self.snp.bottom).offset(-5) |
||||
|
make.centerX.equalTo(self.snp.centerX) |
||||
|
make.width.lessThanOrEqualTo(self.snp.width).offset(-10).priority(750) |
||||
|
} |
||||
|
|
||||
|
subtitleLabel.snp.makeConstraints { [unowned self](make) in |
||||
|
make.left.equalTo(self.subtitleBackView.snp.left).offset(10) |
||||
|
make.right.equalTo(self.subtitleBackView.snp.right).offset(-10) |
||||
|
make.top.equalTo(self.subtitleBackView.snp.top).offset(2) |
||||
|
make.bottom.equalTo(self.subtitleBackView.snp.bottom).offset(-2) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fileprivate func BMImageResourcePath(_ fileName: String) -> UIImage? { |
||||
|
let bundle = Bundle(for: BMPlayer.self) |
||||
|
return UIImage(named: fileName, in: bundle, compatibleWith: nil) |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,91 @@ |
|||||
|
// |
||||
|
// BMPlayerItem.swift |
||||
|
// Pods |
||||
|
// |
||||
|
// Created by BrikerMan on 16/5/21. |
||||
|
// |
||||
|
// |
||||
|
|
||||
|
import Foundation |
||||
|
import AVFoundation |
||||
|
|
||||
|
public class BMPlayerResource { |
||||
|
public let name: String |
||||
|
public let cover: URL? |
||||
|
public var subtitle: BMSubtitles? |
||||
|
public let definitions: [BMPlayerResourceDefinition] |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
Player recource item with url, used to play single difinition video |
||||
|
|
||||
|
- parameter name: video name |
||||
|
- parameter url: video url |
||||
|
- parameter cover: video cover, will show before playing, and hide when play |
||||
|
- parameter subtitles: video subtitles |
||||
|
*/ |
||||
|
public convenience init(url: URL, name: String = "", cover: URL? = nil, subtitle: URL? = nil) { |
||||
|
let definition = BMPlayerResourceDefinition(url: url, definition: "") |
||||
|
|
||||
|
var subtitles: BMSubtitles? = nil |
||||
|
if let subtitle = subtitle { |
||||
|
subtitles = BMSubtitles(url: subtitle) |
||||
|
} |
||||
|
|
||||
|
self.init(name: name, definitions: [definition], cover: cover, subtitles: subtitles) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
Play resouce with multi definitions |
||||
|
|
||||
|
- parameter name: video name |
||||
|
- parameter definitions: video definitions |
||||
|
- parameter cover: video cover |
||||
|
- parameter subtitles: video subtitles |
||||
|
*/ |
||||
|
public init(name: String = "", definitions: [BMPlayerResourceDefinition], cover: URL? = nil, subtitles: BMSubtitles? = nil) { |
||||
|
self.name = name |
||||
|
self.cover = cover |
||||
|
self.subtitle = subtitles |
||||
|
self.definitions = definitions |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
open class BMPlayerResourceDefinition { |
||||
|
public let url: URL |
||||
|
public let definition: String |
||||
|
|
||||
|
/// An instance of NSDictionary that contains keys for specifying options for the initialization of the AVURLAsset. See AVURLAssetPreferPreciseDurationAndTimingKey and AVURLAssetReferenceRestrictionsKey above. |
||||
|
public var options: [String : Any]? |
||||
|
|
||||
|
open var avURLAsset: AVURLAsset { |
||||
|
get { |
||||
|
// guard !url.isFileURL, url.pathExtension != "m3u8" else { |
||||
|
return AVURLAsset(url: url) |
||||
|
// } |
||||
|
// return BMPlayerManager.asset(for: self) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
Video recource item with defination name and specifying options |
||||
|
|
||||
|
- parameter url: video url |
||||
|
- parameter definition: url deifination |
||||
|
- parameter options: specifying options for the initialization of the AVURLAsset |
||||
|
|
||||
|
you can add http-header or other options which mentions in https://developer.apple.com/reference/avfoundation/avurlasset/initialization_options |
||||
|
|
||||
|
to add http-header init options like this |
||||
|
``` |
||||
|
let header = ["User-Agent":"BMPlayer"] |
||||
|
let definiton.options = ["AVURLAssetHTTPHeaderFieldsKey":header] |
||||
|
``` |
||||
|
*/ |
||||
|
public init(url: URL, definition: String, options: [String : Any]? = nil) { |
||||
|
self.url = url |
||||
|
self.definition = definition |
||||
|
self.options = options |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,507 @@ |
|||||
|
// |
||||
|
// BMPlayerLayerView.swift |
||||
|
// Pods |
||||
|
// |
||||
|
// Created by BrikerMan on 16/4/28. |
||||
|
// |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
import AVFoundation |
||||
|
|
||||
|
/** |
||||
|
Player status emun |
||||
|
|
||||
|
- notSetURL: not set url yet |
||||
|
- readyToPlay: player ready to play |
||||
|
- buffering: player buffering |
||||
|
- bufferFinished: buffer finished |
||||
|
- playedToTheEnd: played to the End |
||||
|
- error: error with playing |
||||
|
*/ |
||||
|
public enum BMPlayerState { |
||||
|
case notSetURL |
||||
|
case readyToPlay |
||||
|
case buffering |
||||
|
case bufferFinished |
||||
|
case playedToTheEnd |
||||
|
case error |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
video aspect ratio types |
||||
|
|
||||
|
- `default`: video default aspect |
||||
|
- sixteen2NINE: 16:9 |
||||
|
- four2THREE: 4:3 |
||||
|
*/ |
||||
|
public enum BMPlayerAspectRatio : Int { |
||||
|
case `default` = 0 |
||||
|
case sixteen2NINE |
||||
|
case four2THREE |
||||
|
} |
||||
|
|
||||
|
public protocol BMPlayerLayerViewDelegate : class { |
||||
|
func bmPlayer(player: BMPlayerLayerView, playerStateDidChange state: BMPlayerState) |
||||
|
func bmPlayer(player: BMPlayerLayerView, loadedTimeDidChange loadedDuration: TimeInterval, totalDuration: TimeInterval) |
||||
|
func bmPlayer(player: BMPlayerLayerView, playTimeDidChange currentTime: TimeInterval, totalTime: TimeInterval) |
||||
|
func bmPlayer(player: BMPlayerLayerView, playerIsPlaying playing: Bool) |
||||
|
} |
||||
|
|
||||
|
open class BMPlayerLayerView: UIView { |
||||
|
|
||||
|
open weak var delegate: BMPlayerLayerViewDelegate? |
||||
|
|
||||
|
/// 视频跳转秒数置0 |
||||
|
open var seekTime = 0 |
||||
|
|
||||
|
/// 播放属性 |
||||
|
open var playerItem: AVPlayerItem? { |
||||
|
didSet { |
||||
|
onPlayerItemChange() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 播放属性 |
||||
|
open lazy var player: AVPlayer? = { |
||||
|
if let item = self.playerItem { |
||||
|
let player = AVPlayer(playerItem: item) |
||||
|
return player |
||||
|
} |
||||
|
return nil |
||||
|
}() |
||||
|
|
||||
|
|
||||
|
open var videoGravity = AVLayerVideoGravity.resizeAspect { |
||||
|
didSet { |
||||
|
self.playerLayer?.videoGravity = videoGravity |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
open var isPlaying: Bool = false { |
||||
|
didSet { |
||||
|
if oldValue != isPlaying { |
||||
|
delegate?.bmPlayer(player: self, playerIsPlaying: isPlaying) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var aspectRatio: BMPlayerAspectRatio = .default { |
||||
|
didSet { |
||||
|
self.setNeedsLayout() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 计时器 |
||||
|
var timer: Timer? |
||||
|
|
||||
|
fileprivate var urlAsset: AVURLAsset? |
||||
|
|
||||
|
fileprivate var lastPlayerItem: AVPlayerItem? |
||||
|
/// playerLayer |
||||
|
fileprivate var playerLayer: AVPlayerLayer? |
||||
|
/// 音量滑杆 |
||||
|
fileprivate var volumeViewSlider: UISlider! |
||||
|
/// 播放器的几种状态 |
||||
|
fileprivate var state = BMPlayerState.notSetURL { |
||||
|
didSet { |
||||
|
if state != oldValue { |
||||
|
delegate?.bmPlayer(player: self, playerStateDidChange: state) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
/// 是否为全屏 |
||||
|
fileprivate var isFullScreen = false |
||||
|
/// 是否锁定屏幕方向 |
||||
|
fileprivate var isLocked = false |
||||
|
/// 是否在调节音量 |
||||
|
fileprivate var isVolume = false |
||||
|
/// 是否播放本地文件 |
||||
|
fileprivate var isLocalVideo = false |
||||
|
/// slider上次的值 |
||||
|
fileprivate var sliderLastValue: Float = 0 |
||||
|
/// 是否点了重播 |
||||
|
fileprivate var repeatToPlay = false |
||||
|
/// 播放完了 |
||||
|
fileprivate var playDidEnd = false |
||||
|
// playbackBufferEmpty会反复进入,因此在bufferingOneSecond延时播放执行完之前再调用bufferingSomeSecond都忽略 |
||||
|
// 仅在bufferingSomeSecond里面使用 |
||||
|
fileprivate var isBuffering = false |
||||
|
fileprivate var hasReadyToPlay = false |
||||
|
fileprivate var shouldSeekTo: TimeInterval = 0 |
||||
|
|
||||
|
// MARK: - Actions |
||||
|
open func playURL(url: URL) { |
||||
|
let asset = AVURLAsset(url: url) |
||||
|
playAsset(asset: asset) |
||||
|
} |
||||
|
|
||||
|
open func playAsset(asset: AVURLAsset) { |
||||
|
urlAsset = asset |
||||
|
onSetVideoAsset() |
||||
|
play() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
open func play() { |
||||
|
if let player = player { |
||||
|
player.play() |
||||
|
setupTimer() |
||||
|
isPlaying = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
open func pause() { |
||||
|
player?.pause() |
||||
|
isPlaying = false |
||||
|
timer?.fireDate = Date.distantFuture |
||||
|
} |
||||
|
|
||||
|
deinit { |
||||
|
NotificationCenter.default.removeObserver(self) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// MARK: - layoutSubviews |
||||
|
override open func layoutSubviews() { |
||||
|
super.layoutSubviews() |
||||
|
switch self.aspectRatio { |
||||
|
case .default: |
||||
|
self.playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspect |
||||
|
self.playerLayer?.frame = self.bounds |
||||
|
break |
||||
|
case .sixteen2NINE: |
||||
|
self.playerLayer?.videoGravity = AVLayerVideoGravity.resize |
||||
|
self.playerLayer?.frame = CGRect(x: 0, y: 0, width: self.bounds.width, height: self.bounds.width/(16/9)) |
||||
|
break |
||||
|
case .four2THREE: |
||||
|
self.playerLayer?.videoGravity = AVLayerVideoGravity.resize |
||||
|
let _w = self.bounds.height * 4 / 3 |
||||
|
self.playerLayer?.frame = CGRect(x: (self.bounds.width - _w )/2, y: 0, width: _w, height: self.bounds.height) |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
open func resetPlayer() { |
||||
|
// 初始化状态变量 |
||||
|
|
||||
|
self.playDidEnd = false |
||||
|
self.playerItem = nil |
||||
|
self.lastPlayerItem = nil |
||||
|
self.seekTime = 0 |
||||
|
|
||||
|
self.timer?.invalidate() |
||||
|
|
||||
|
self.pause() |
||||
|
// 移除原来的layer |
||||
|
self.playerLayer?.removeFromSuperlayer() |
||||
|
// 替换PlayerItem为nil |
||||
|
self.player?.replaceCurrentItem(with: nil) |
||||
|
player?.removeObserver(self, forKeyPath: "rate") |
||||
|
|
||||
|
// 把player置为nil |
||||
|
self.player = nil |
||||
|
} |
||||
|
|
||||
|
open func prepareToDeinit() { |
||||
|
self.resetPlayer() |
||||
|
} |
||||
|
|
||||
|
open func onTimeSliderBegan() { |
||||
|
if self.player?.currentItem?.status == AVPlayerItem.Status.readyToPlay { |
||||
|
self.timer?.fireDate = Date.distantFuture |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
open func seek(to secounds: TimeInterval, completion:(()->Void)?) { |
||||
|
if secounds.isNaN { |
||||
|
return |
||||
|
} |
||||
|
setupTimer() |
||||
|
if self.player?.currentItem?.status == AVPlayerItem.Status.readyToPlay { |
||||
|
let draggedTime = CMTime(value: Int64(secounds), timescale: 1) |
||||
|
// self.player!.cancelPendingPrerolls() |
||||
|
self.playerItem!.cancelPendingSeeks() |
||||
|
self.player!.seek(to: draggedTime, toleranceBefore: CMTimeMake(value: 10, timescale: 1), toleranceAfter: CMTimeMake(value: 10, timescale: 1), completionHandler: { (finished) in |
||||
|
completion?() |
||||
|
}) |
||||
|
} else { |
||||
|
self.shouldSeekTo = secounds |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// MARK: - 设置视频URL |
||||
|
fileprivate func onSetVideoAsset() { |
||||
|
repeatToPlay = false |
||||
|
playDidEnd = false |
||||
|
configPlayer() |
||||
|
} |
||||
|
|
||||
|
fileprivate func onPlayerItemChange() { |
||||
|
if lastPlayerItem == playerItem { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if let item = lastPlayerItem { |
||||
|
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item) |
||||
|
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, object: item) |
||||
|
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemNewErrorLogEntry, object: item) |
||||
|
item.removeObserver(self, forKeyPath: "status") |
||||
|
item.removeObserver(self, forKeyPath: "loadedTimeRanges") |
||||
|
item.removeObserver(self, forKeyPath: "playbackBufferEmpty") |
||||
|
item.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") |
||||
|
} |
||||
|
|
||||
|
lastPlayerItem = playerItem |
||||
|
|
||||
|
if let item = playerItem { |
||||
|
NotificationCenter.default.addObserver(self, selector: #selector(moviePlayDidEnd), |
||||
|
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, |
||||
|
object: playerItem) |
||||
|
|
||||
|
item.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil) |
||||
|
item.addObserver(self, forKeyPath: "loadedTimeRanges", options: NSKeyValueObservingOptions.new, context: nil) |
||||
|
// 缓冲区空了,需要等待数据 |
||||
|
item.addObserver(self, forKeyPath: "playbackBufferEmpty", options: NSKeyValueObservingOptions.new, context: nil) |
||||
|
// 缓冲区有足够数据可以播放了 |
||||
|
item.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: NSKeyValueObservingOptions.new, context: nil) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Getting error from Notification payload |
||||
|
@objc fileprivate func newErrorLogEntry(_ notification: Notification) { |
||||
|
guard let object = notification.object, let playerItem = object as? AVPlayerItem else { |
||||
|
return |
||||
|
} |
||||
|
guard let errorLog: AVPlayerItemErrorLog = playerItem.errorLog() else { |
||||
|
return |
||||
|
} |
||||
|
print("Error: \(errorLog)") |
||||
|
} |
||||
|
|
||||
|
@objc fileprivate func failedToPlayToEndTime(_ notification: Notification) { |
||||
|
let error = notification.userInfo!["AVPlayerItemFailedToPlayToEndTimeErrorKey"] |
||||
|
print(error) |
||||
|
} |
||||
|
|
||||
|
fileprivate func configPlayer(){ |
||||
|
player?.removeObserver(self, forKeyPath: "rate") |
||||
|
playerItem = AVPlayerItem(asset: urlAsset!) |
||||
|
playerItem?.preferredForwardBufferDuration = TimeInterval(floatLiteral: 100.0) |
||||
|
playerItem?.canUseNetworkResourcesForLiveStreamingWhilePaused = true |
||||
|
|
||||
|
player = AVPlayer(playerItem: playerItem!) |
||||
|
player?.automaticallyWaitsToMinimizeStalling = true |
||||
|
player!.addObserver(self, forKeyPath: "rate", options: NSKeyValueObservingOptions.new, context: nil) |
||||
|
self.connectPlayerLayer() |
||||
|
setNeedsLayout() |
||||
|
layoutIfNeeded() |
||||
|
|
||||
|
NotificationCenter.default.addObserver(self, selector: #selector(self.connectPlayerLayer), name: UIApplication.willEnterForegroundNotification, object: nil) |
||||
|
NotificationCenter.default.addObserver(self, selector: #selector(self.disconnectPlayerLayer), name: UIApplication.didEnterBackgroundNotification, object: nil) |
||||
|
|
||||
|
let center = NotificationCenter.default |
||||
|
center.addObserver(self, selector: #selector(self.newErrorLogEntry), name: .AVPlayerItemNewErrorLogEntry, object: playerItem) |
||||
|
center.addObserver(self, selector:#selector(self.failedToPlayToEndTime), name: .AVPlayerItemFailedToPlayToEndTime, object: playerItem) |
||||
|
} |
||||
|
|
||||
|
func setupTimer() { |
||||
|
timer?.invalidate() |
||||
|
timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(playerTimerAction), userInfo: nil, repeats: true) |
||||
|
timer?.fireDate = Date() |
||||
|
} |
||||
|
|
||||
|
// MARK: - 计时器事件 |
||||
|
@objc fileprivate func playerTimerAction() { |
||||
|
guard let playerItem = playerItem else { return } |
||||
|
|
||||
|
if playerItem.duration.timescale != 0 { |
||||
|
let currentTime = CMTimeGetSeconds(self.player!.currentTime()) |
||||
|
let totalTime = TimeInterval(playerItem.duration.value) / TimeInterval(playerItem.duration.timescale) |
||||
|
delegate?.bmPlayer(player: self, playTimeDidChange: currentTime, totalTime: totalTime) |
||||
|
} |
||||
|
updateStatus(includeLoading: true) |
||||
|
} |
||||
|
|
||||
|
// Observe If AVPlayerItem.status Changed to Fail |
||||
|
func obsrveValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { |
||||
|
if let player: AVPlayer = object as? AVPlayer { |
||||
|
if (keyPath == #keyPath(AVPlayer.currentItem.status)) { |
||||
|
let newStatus: AVPlayerItem.Status |
||||
|
if let newStatusAsNumber = change?[NSKeyValueChangeKey.newKey] as? NSNumber { |
||||
|
newStatus = AVPlayerItem.Status(rawValue: newStatusAsNumber.intValue)! |
||||
|
} else { |
||||
|
newStatus = .unknown |
||||
|
} |
||||
|
if newStatus == .failed { |
||||
|
NSLog("Error: \(String(describing: self.player?.currentItem?.error?.localizedDescription)), error: \(String(describing: self.player?.currentItem?.error))") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fileprivate func updateStatus(includeLoading: Bool = false) { |
||||
|
if let player = player { |
||||
|
if let playerItem = playerItem, includeLoading { |
||||
|
if playerItem.isPlaybackLikelyToKeepUp || playerItem.isPlaybackBufferFull { |
||||
|
self.state = .bufferFinished |
||||
|
} else if playerItem.status == .failed { |
||||
|
self.state = .error |
||||
|
} else { |
||||
|
self.state = .buffering |
||||
|
} |
||||
|
} |
||||
|
if player.rate == 0.0 { |
||||
|
if player.error != nil { |
||||
|
self.state = .error |
||||
|
return |
||||
|
} |
||||
|
if let currentItem = player.currentItem { |
||||
|
if player.currentTime() >= currentItem.duration { |
||||
|
moviePlayDidEnd() |
||||
|
return |
||||
|
} |
||||
|
if currentItem.isPlaybackLikelyToKeepUp || currentItem.isPlaybackBufferFull { |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - Notification Event |
||||
|
@objc fileprivate func moviePlayDidEnd() { |
||||
|
if state != .playedToTheEnd { |
||||
|
if let playerItem = playerItem { |
||||
|
delegate?.bmPlayer(player: self, |
||||
|
playTimeDidChange: CMTimeGetSeconds(playerItem.duration), |
||||
|
totalTime: CMTimeGetSeconds(playerItem.duration)) |
||||
|
} |
||||
|
|
||||
|
self.state = .playedToTheEnd |
||||
|
self.isPlaying = false |
||||
|
self.playDidEnd = true |
||||
|
self.timer?.invalidate() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - KVO |
||||
|
override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { |
||||
|
if let item = object as? AVPlayerItem, let keyPath = keyPath { |
||||
|
if item == self.playerItem { |
||||
|
switch keyPath { |
||||
|
case "status": |
||||
|
if item.status == .failed || player?.status == AVPlayer.Status.failed { |
||||
|
self.state = .error |
||||
|
print(item.error) |
||||
|
} else if player?.status == AVPlayer.Status.readyToPlay { |
||||
|
self.state = .buffering |
||||
|
if shouldSeekTo != 0 { |
||||
|
seek(to: shouldSeekTo, completion: { [weak self] in |
||||
|
self?.shouldSeekTo = 0 |
||||
|
self?.hasReadyToPlay = true |
||||
|
self?.state = .readyToPlay |
||||
|
}) |
||||
|
} else { |
||||
|
self.hasReadyToPlay = true |
||||
|
self.state = .readyToPlay |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
case "loadedTimeRanges": |
||||
|
// 计算缓冲进度 |
||||
|
if let timeInterVarl = self.availableDuration() { |
||||
|
let duration = item.duration |
||||
|
let totalDuration = CMTimeGetSeconds(duration) |
||||
|
delegate?.bmPlayer(player: self, loadedTimeDidChange: timeInterVarl, totalDuration: totalDuration) |
||||
|
} |
||||
|
|
||||
|
case "playbackBufferEmpty": |
||||
|
// 当缓冲是空的时候 |
||||
|
if self.playerItem!.isPlaybackBufferEmpty { |
||||
|
self.state = .buffering |
||||
|
self.bufferingSomeSecond() |
||||
|
} |
||||
|
case "playbackLikelyToKeepUp": |
||||
|
if item.isPlaybackBufferEmpty { |
||||
|
if state != .bufferFinished && hasReadyToPlay { |
||||
|
self.state = .bufferFinished |
||||
|
self.playDidEnd = true |
||||
|
} |
||||
|
} |
||||
|
default: |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if keyPath == "rate" { |
||||
|
updateStatus() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
缓冲进度 |
||||
|
|
||||
|
- returns: 缓冲进度 |
||||
|
*/ |
||||
|
fileprivate func availableDuration() -> TimeInterval? { |
||||
|
if let loadedTimeRanges = player?.currentItem?.loadedTimeRanges, |
||||
|
let first = loadedTimeRanges.first { |
||||
|
|
||||
|
let timeRange = first.timeRangeValue |
||||
|
let startSeconds = CMTimeGetSeconds(timeRange.start) |
||||
|
let durationSecound = CMTimeGetSeconds(timeRange.duration) |
||||
|
let result = startSeconds + durationSecound |
||||
|
return result |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
缓冲比较差的时候 |
||||
|
*/ |
||||
|
fileprivate func bufferingSomeSecond() { |
||||
|
print("buffering some second") |
||||
|
self.state = .buffering |
||||
|
// playbackBufferEmpty会反复进入,因此在bufferingOneSecond延时播放执行完之前再调用bufferingSomeSecond都忽略 |
||||
|
|
||||
|
if isBuffering { |
||||
|
return |
||||
|
} |
||||
|
isBuffering = true |
||||
|
// 需要先暂停一小会之后再播放,否则网络状况不好的时候时间在走,声音播放不出来 |
||||
|
player?.pause() |
||||
|
let popTime = DispatchTime.now() + Double(Int64( Double(NSEC_PER_SEC) * 1.0 )) / Double(NSEC_PER_SEC) |
||||
|
|
||||
|
DispatchQueue.main.asyncAfter(deadline: popTime) {[weak self] in |
||||
|
guard let `self` = self else { return } |
||||
|
// 如果执行了play还是没有播放则说明还没有缓存好,则再次缓存一段时间 |
||||
|
self.isBuffering = false |
||||
|
if let item = self.playerItem { |
||||
|
if !item.isPlaybackLikelyToKeepUp { |
||||
|
self.bufferingSomeSecond() |
||||
|
} else { |
||||
|
// 如果此时用户已经暂停了,则不再需要开启播放了 |
||||
|
self.state = BMPlayerState.bufferFinished |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@objc fileprivate func connectPlayerLayer() { |
||||
|
playerLayer?.removeFromSuperlayer() |
||||
|
playerLayer = AVPlayerLayer(player: player) |
||||
|
playerLayer!.videoGravity = videoGravity |
||||
|
|
||||
|
layer.addSublayer(playerLayer!) |
||||
|
} |
||||
|
|
||||
|
@objc fileprivate func disconnectPlayerLayer() { |
||||
|
playerLayer?.removeFromSuperlayer() |
||||
|
playerLayer = nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,62 @@ |
|||||
|
// |
||||
|
// BMPlayerManager.swift |
||||
|
// Pods |
||||
|
// |
||||
|
// Created by BrikerMan on 16/5/21. |
||||
|
// |
||||
|
// |
||||
|
import UIKit |
||||
|
import AVFoundation |
||||
|
import NVActivityIndicatorView |
||||
|
|
||||
|
public let BMPlayerConf = BMPlayerManager.shared |
||||
|
|
||||
|
public enum BMPlayerTopBarShowCase: Int { |
||||
|
case always = 0 /// 始终显示 |
||||
|
case horizantalOnly = 1 /// 只在横屏界面显示 |
||||
|
case none = 2 /// 不显示 |
||||
|
} |
||||
|
|
||||
|
open class BMPlayerManager { |
||||
|
/// 单例 |
||||
|
public static let shared = BMPlayerManager() |
||||
|
|
||||
|
/// tint color |
||||
|
open var tintColor = UIColor.white |
||||
|
|
||||
|
/// Loader |
||||
|
open var loaderType = NVActivityIndicatorType.ballRotateChase |
||||
|
|
||||
|
/// should auto play |
||||
|
open var shouldAutoPlay = true |
||||
|
|
||||
|
open var topBarShowInCase = BMPlayerTopBarShowCase.always |
||||
|
|
||||
|
open var animateDelayTimeInterval = TimeInterval(5) |
||||
|
|
||||
|
/// should show log |
||||
|
open var allowLog = false |
||||
|
|
||||
|
/// use gestures to set brightness, volume and play position |
||||
|
open var enableBrightnessGestures = true |
||||
|
open var enableVolumeGestures = true |
||||
|
open var enablePlaytimeGestures = true |
||||
|
open var enablePlayControlGestures = true |
||||
|
|
||||
|
open var enableChooseDefinition = true |
||||
|
|
||||
|
internal static func asset(for resouce: BMPlayerResourceDefinition) -> AVURLAsset { |
||||
|
return AVURLAsset(url: resouce.url, options: resouce.options) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
打印log |
||||
|
|
||||
|
- parameter info: log信息 |
||||
|
*/ |
||||
|
func log(_ info:String) { |
||||
|
if allowLog { |
||||
|
print(info) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
// |
||||
|
// BMPlayerProtocols.swift |
||||
|
// Pods |
||||
|
// |
||||
|
// Created by BrikerMan on 16/4/30. |
||||
|
// |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
|
||||
|
extension BMPlayerControlView { |
||||
|
public enum ButtonType: Int { |
||||
|
case play = 101 |
||||
|
case pause = 102 |
||||
|
case back = 103 |
||||
|
case fullscreen = 105 |
||||
|
case replay = 106 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
extension BMPlayer { |
||||
|
static func formatSecondsToString(_ seconds: TimeInterval) -> String { |
||||
|
if seconds.isNaN { |
||||
|
return "00:00" |
||||
|
} |
||||
|
let min = Int(seconds / 60) |
||||
|
let sec = Int(seconds.truncatingRemainder(dividingBy: 60)) |
||||
|
return String(format: "%02d:%02d", min, sec) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,122 @@ |
|||||
|
// |
||||
|
// BMSubtitles.swift |
||||
|
// Pods |
||||
|
// |
||||
|
// Created by BrikerMan on 2017/4/2. |
||||
|
// |
||||
|
// |
||||
|
|
||||
|
import Foundation |
||||
|
|
||||
|
public class BMSubtitles { |
||||
|
public var groups: [Group] = [] |
||||
|
/// subtitles delay, positive:fast, negative:forward |
||||
|
public var delay: TimeInterval = 0 |
||||
|
|
||||
|
public struct Group: CustomStringConvertible { |
||||
|
var index: Int |
||||
|
var start: TimeInterval |
||||
|
var end : TimeInterval |
||||
|
var text : String |
||||
|
|
||||
|
init(_ index: Int, _ start: NSString, _ end: NSString, _ text: NSString) { |
||||
|
self.index = index |
||||
|
self.start = Group.parseDuration(start as String) |
||||
|
self.end = Group.parseDuration(end as String) |
||||
|
self.text = text as String |
||||
|
} |
||||
|
|
||||
|
static func parseDuration(_ fromStr:String) -> TimeInterval { |
||||
|
var h: TimeInterval = 0.0, m: TimeInterval = 0.0, s: TimeInterval = 0.0, c: TimeInterval = 0.0 |
||||
|
let scanner = Scanner(string: fromStr) |
||||
|
scanner.scanDouble(&h) |
||||
|
scanner.scanString(":", into: nil) |
||||
|
scanner.scanDouble(&m) |
||||
|
scanner.scanString(":", into: nil) |
||||
|
scanner.scanDouble(&s) |
||||
|
scanner.scanString(",", into: nil) |
||||
|
scanner.scanDouble(&c) |
||||
|
return (h * 3600.0) + (m * 60.0) + s + (c / 1000.0) |
||||
|
} |
||||
|
|
||||
|
public var description: String { |
||||
|
return "Subtile Group ==========\nindex : \(index),\nstart : \(start)\nend :\(end)\ntext :\(text)" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public init(url: URL, encoding: String.Encoding? = nil) { |
||||
|
DispatchQueue.global(qos: .background).async {[weak self] in |
||||
|
do { |
||||
|
let string: String |
||||
|
if let encoding = encoding { |
||||
|
string = try String(contentsOf: url, encoding: encoding) |
||||
|
} else { |
||||
|
string = try String(contentsOf: url) |
||||
|
} |
||||
|
self?.groups = BMSubtitles.parseSubRip(string) ?? [] |
||||
|
} catch { |
||||
|
print("| BMPlayer | [Error] failed to load \(url.absoluteString) \(error.localizedDescription)") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
Search for target group for time |
||||
|
|
||||
|
- parameter time: target time |
||||
|
|
||||
|
- returns: result group or nil |
||||
|
*/ |
||||
|
public func search(for time: TimeInterval) -> Group? { |
||||
|
let result = groups.first(where: { group -> Bool in |
||||
|
if group.start - delay <= time && group.end - delay >= time { |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
}) |
||||
|
return result |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
Parse str string into Group Array |
||||
|
|
||||
|
- parameter payload: target string |
||||
|
|
||||
|
- returns: result group |
||||
|
*/ |
||||
|
fileprivate static func parseSubRip(_ payload: String) -> [Group]? { |
||||
|
var groups: [Group] = [] |
||||
|
let scanner = Scanner(string: payload) |
||||
|
while !scanner.isAtEnd { |
||||
|
var indexString: NSString? |
||||
|
scanner.scanUpToCharacters(from: .newlines, into: &indexString) |
||||
|
|
||||
|
var startString: NSString? |
||||
|
scanner.scanUpTo(" --> ", into: &startString) |
||||
|
|
||||
|
// skip spaces and newlines by default. |
||||
|
scanner.scanString("-->", into: nil) |
||||
|
|
||||
|
var endString: NSString? |
||||
|
scanner.scanUpToCharacters(from: .newlines, into: &endString) |
||||
|
|
||||
|
var textString: NSString? |
||||
|
scanner.scanUpTo("\r\n\r\n", into: &textString) |
||||
|
|
||||
|
if let text = textString { |
||||
|
textString = text.trimmingCharacters(in: .whitespaces) as NSString |
||||
|
textString = text.replacingOccurrences(of: "\r", with: "") as NSString |
||||
|
} |
||||
|
|
||||
|
if let indexString = indexString, |
||||
|
let index = Int(indexString as String), |
||||
|
let start = startString, |
||||
|
let end = endString, |
||||
|
let text = textString { |
||||
|
let group = Group(index, start, end, text) |
||||
|
groups.append(group) |
||||
|
} |
||||
|
} |
||||
|
return groups |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
// |
||||
|
// BMTimeSlider.swift |
||||
|
// Pods |
||||
|
// |
||||
|
// Created by BrikerMan on 2017/4/2. |
||||
|
// |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
|
||||
|
public class BMTimeSlider: UISlider { |
||||
|
override open func trackRect(forBounds bounds: CGRect) -> CGRect { |
||||
|
let trackHeight: CGFloat = 2 |
||||
|
let position = CGPoint(x: 0, y: 14) |
||||
|
let customBounds = CGRect(origin: position, size: CGSize(width: bounds.size.width, height: trackHeight)) |
||||
|
super.trackRect(forBounds: customBounds) |
||||
|
return customBounds |
||||
|
} |
||||
|
|
||||
|
override open func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect { |
||||
|
let rect = super.thumbRect(forBounds: bounds, trackRect: rect, value: value) |
||||
|
let newx = rect.origin.x - 10 |
||||
|
let newRect = CGRect(x: newx, y: 0, width: 30, height: 30) |
||||
|
return newRect |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue