25 changed files with 139 additions and 3488 deletions
-
48kplayer.xcodeproj/project.pbxproj
-
1kplayer/AppDelegate.swift
-
39kplayer/core/DatabaseManager.swift
-
10kplayer/core/ItemType.swift
-
3kplayer/core/KSettings.swift
-
2kplayer/core/MediaItem.swift
-
66kplayer/detail/BrowserController.swift
-
78kplayer/detail/DetailViewController.swift
-
12kplayer/detail/EditItemView.swift
-
821kplayer/detail/VideoController.swift
-
3kplayer/master/KSettingsView.swift
-
9kplayer/master/MasterViewController.swift
-
7kplayer/master/NetworkDelegate.swift
-
16kplayer/photo/PhotoController.swift
-
820kplayer/video/BMPlayer.swift
-
29kplayer/video/BMPlayerClearityChooseButton.swift
-
19kplayer/video/BMPlayerCompositionResourceDefinition.swift
-
757kplayer/video/BMPlayerControlView.swift
-
91kplayer/video/BMPlayerItem.swift
-
506kplayer/video/BMPlayerLayerView.swift
-
62kplayer/video/BMPlayerManager.swift
-
30kplayer/video/BMPlayerProtocols.swift
-
122kplayer/video/BMSubtitles.swift
-
26kplayer/video/BMTimeSlider.swift
-
30kplayer/video/KVideoPlayer.swift
@ -1,821 +0,0 @@ |
|||
// |
|||
// Created by Marco Schmickler on 26.05.15. |
|||
// Copyright (c) 2015 Marco Schmickler. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
import UIKit |
|||
import Haneke |
|||
import AVFoundation |
|||
import SwiftUI |
|||
|
|||
protocol DownloadDelegate { |
|||
func killFFMPEG() |
|||
|
|||
func dlserverlen(result: @escaping (String) -> ()) |
|||
func download(url: URL, path: String, result: @escaping (URL) -> () ) |
|||
func downloadToServer(path: String, url: URL, result: @escaping (String) -> ()) |
|||
|
|||
func inProgress() -> Int |
|||
} |
|||
|
|||
protocol ItemController { |
|||
func setCurrentItem(item: MediaItem) |
|||
func setItems(items: [MediaItem]) |
|||
func setCompletionHandler(handler: @escaping (() -> Void)) |
|||
} |
|||
|
|||
// Trimmer: https://github.com/Tomohiro-Yamashita/VideoTimelineView |
|||
|
|||
class VideoController: UIViewController, ItemController, BMPlayerDelegate, EditItemDelegate { |
|||
var player = BMPlayer() |
|||
|
|||
// played on startup |
|||
var currentItem: MediaItem? |
|||
|
|||
var currentSnapshot: MediaItem? |
|||
var allItems = [MediaItem]() |
|||
var detailDelegate: DetailDelegate? |
|||
var downloadDelegate: DownloadDelegate? |
|||
|
|||
var loopStart = 0.0 |
|||
|
|||
var completionHandler: (() -> Void)? |
|||
|
|||
var buttons = Dictionary<UIButton, MediaItem>() |
|||
|
|||
var barbutton: UIBarButtonItem? |
|||
var speedButton: UIBarButtonItem? |
|||
var loopButton: UIBarButtonItem? |
|||
var aspectButton: UIBarButtonItem? |
|||
var playButton: UIBarButtonItem? |
|||
var favButton: UIBarButtonItem? |
|||
var backButton: UIBarButtonItem? |
|||
var reviewButton: UIBarButtonItem? |
|||
|
|||
let speedOptions = [ 0.25, 0.5, 1, 2 ] |
|||
var speedOption = 2 |
|||
|
|||
var aspect = 1 |
|||
|
|||
var loopMode = false |
|||
|
|||
var thumbnailTime: TimeInterval = 0.0 |
|||
|
|||
var edit = false |
|||
var allowEdit = true |
|||
var index = 0 |
|||
|
|||
var ffmpeg = false |
|||
|
|||
var urls : [URL]? |
|||
|
|||
var kv : EditItemView? |
|||
var hc : UIHostingController<EditItemView>? |
|||
|
|||
override func viewDidLoad() { |
|||
super.viewDidLoad() |
|||
|
|||
barbutton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(VideoController.captureThumbnail)); |
|||
navigationItem.rightBarButtonItems = [barbutton!] |
|||
|
|||
backButton = UIBarButtonItem(title: "0:00", style:UIBarButtonItem.Style.plain, target: self, action: #selector(VideoController.back(_:))) |
|||
speedButton = UIBarButtonItem(title:"1.0", style:UIBarButtonItem.Style.plain, target: self, action: #selector(VideoController.speed(_:))) |
|||
loopButton = UIBarButtonItem(title:"1.0", style:UIBarButtonItem.Style.plain, target: self, action: #selector(VideoController.loop(_:))) |
|||
aspectButton = UIBarButtonItem(title:"1", style:UIBarButtonItem.Style.plain, target: self, action: #selector(VideoController.aspect(_:))) |
|||
playButton = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(VideoController.startstop(_:))) |
|||
favButton = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(VideoController.favorite(_:))) |
|||
reviewButton = UIBarButtonItem(title:"Edit ", style:UIBarButtonItem.Style.plain, target: self, action: #selector(VideoController.doEdit(_:))) |
|||
|
|||
navigationItem.leftBarButtonItems = [backButton!, speedButton!, playButton!, loopButton!, aspectButton!, favButton!, reviewButton!] |
|||
//MediaItem(name: "extern", path: "", root: "", type: ItemType.FAVROOT) |
|||
|
|||
// pc.view.isHidden = true |
|||
|
|||
view.addSubview(player) |
|||
player.snp.makeConstraints { (make) in |
|||
// make.top.equalTo(self.view).offset(100) |
|||
make.left.right.equalTo(self.view) |
|||
make.height.equalTo(self.view) |
|||
// Note here, the aspect ratio 16:9 priority is lower than 1000 on the line, because the 4S iPhone aspect ratio is not 16:9 |
|||
// make.height.equalTo(player.snp.width).multipliedBy(9.0/16.0).priority(750) |
|||
} |
|||
|
|||
player.delegate = self |
|||
// Back button event |
|||
player.backBlock = { (b) in |
|||
let _ = self.navigationController?.popViewController(animated: true) |
|||
} |
|||
|
|||
if let c = currentItem, let url = c.playerURL { |
|||
print(url) |
|||
play(url as URL) |
|||
// player.playerLayer!.player!.volume = 0.0 |
|||
|
|||
update() |
|||
} |
|||
loopMode = detailDelegate!.settings().autoloop |
|||
if detailDelegate!.settings().edit { |
|||
doEdit(self) |
|||
} |
|||
updateLoop() |
|||
} |
|||
|
|||
func editItem() { |
|||
cancelEdit() |
|||
|
|||
let currentItem = player.playerLayer?.player?.currentItem |
|||
let totalTime : Double |
|||
|
|||
if let playerItem = currentItem { |
|||
totalTime = TimeInterval(playerItem.duration.value) / TimeInterval(playerItem.duration.timescale) |
|||
} |
|||
else { |
|||
totalTime = 1000 |
|||
} |
|||
|
|||
if (currentSnapshot!.length < 0.001) { |
|||
setEnd() |
|||
} |
|||
|
|||
kv = EditItemView(item: currentSnapshot!, len: totalTime, delegate: self) |
|||
hc = UIHostingController(rootView: kv!) |
|||
addChild(hc!) |
|||
|
|||
view.addSubview(hc!.view) |
|||
|
|||
hc!.view.backgroundColor = .clear |
|||
hc!.view.snp.makeConstraints { (make) in |
|||
make.width.equalTo(400) |
|||
make.height.equalTo(300) |
|||
make.right.equalToSuperview() |
|||
make.top.equalToSuperview() |
|||
} |
|||
} |
|||
|
|||
func captureZoom() { |
|||
if let item = currentSnapshot { |
|||
item.scale = Double(self.player.zoom) |
|||
item.offset = CGPoint(x: self.player.xpos, y: self.player.ypos) |
|||
} |
|||
} |
|||
|
|||
func setStart() { |
|||
if let item = currentSnapshot { |
|||
let ctime = currentTime() |
|||
|
|||
item.time = ctime |
|||
|
|||
item.objectWillChange.send() |
|||
} |
|||
} |
|||
|
|||
private func currentTime() -> Double { |
|||
CMTimeGetSeconds(player.playerLayer!.playerItem!.currentTime()) |
|||
} |
|||
|
|||
func setEnd() { |
|||
if let item = currentSnapshot { |
|||
let ctime = currentTime() |
|||
|
|||
if (ctime > item.time) { |
|||
item.length = ctime - item.time |
|||
item.objectWillChange.send() |
|||
} |
|||
} |
|||
} |
|||
|
|||
func cancelEdit() { |
|||
if hc == nil { |
|||
return |
|||
} |
|||
|
|||
hc!.view.removeFromSuperview() |
|||
kv = nil |
|||
hc = nil |
|||
} |
|||
|
|||
func seek(_ value: Double) { |
|||
self.player.seekSmoothlyToTime(newChaseTime: value) |
|||
} |
|||
|
|||
override var prefersStatusBarHidden: Bool { |
|||
return true |
|||
} |
|||
|
|||
func setItems(items: [MediaItem]) { |
|||
allItems = items |
|||
} |
|||
|
|||
func setCurrentItem(item: MediaItem) { |
|||
currentItem = item |
|||
} |
|||
|
|||
func setCompletionHandler(handler: @escaping (() -> Void)) { |
|||
completionHandler = handler |
|||
} |
|||
|
|||
@objc func doEdit(_ sender: AnyObject) { |
|||
if (!allowEdit) { |
|||
return |
|||
} |
|||
|
|||
if (edit) { |
|||
edit = false |
|||
reviewButton!.tintColor = UIColor.blue |
|||
} |
|||
else { |
|||
edit = true |
|||
loopMode = false |
|||
updateLoop() |
|||
reviewButton!.tintColor = UIColor.yellow |
|||
} |
|||
} |
|||
|
|||
func showAlert(title:String, message:String ) { |
|||
|
|||
let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) |
|||
|
|||
let okAction = UIAlertAction(title: "Ok", style: .default, handler: nil) |
|||
alertVC.addAction(okAction) |
|||
|
|||
DispatchQueue.main.async() { () -> Void in |
|||
self.present(alertVC, animated: true, completion: nil) |
|||
} |
|||
} |
|||
|
|||
@objc func favorite(_ sender: AnyObject) { |
|||
downloadDelegate?.killFFMPEG() |
|||
|
|||
print("favorite") |
|||
let inProgress = downloadDelegate?.inProgress() |
|||
let alertController = UIAlertController(title: "In Progress: \(inProgress)", message: "Download", preferredStyle: .alert) |
|||
|
|||
downloadDelegate?.dlserverlen { c in |
|||
alertController.title = "On Server: \(c)"; |
|||
} |
|||
|
|||
if let c = currentSnapshot { |
|||
if (c.loop) { |
|||
let oneAction = UIAlertAction(title: "One", style: .default) { (action) in |
|||
self.save(currentSnapshot: c, name: "1") |
|||
} |
|||
let twoAction = UIAlertAction(title: "Two", style: .default) { (action) in |
|||
self.save(currentSnapshot: c, name: "2") |
|||
} |
|||
let threeAction = UIAlertAction(title: "Three", style: .default) { (action) in |
|||
self.save(currentSnapshot: c, name: "3") |
|||
} |
|||
alertController.addAction(oneAction) |
|||
alertController.addAction(twoAction) |
|||
alertController.addAction(threeAction) |
|||
} |
|||
} |
|||
|
|||
let downloadAction = UIAlertAction(title: "Download to fav", style: .default) { (action) in |
|||
let url = self.currentItem!.playerURL |
|||
self.downloadDelegate?.download(url: url!, path: "download") { url in |
|||
self.showAlert(title: url.lastPathComponent, message: "ready") |
|||
} |
|||
} |
|||
|
|||
let url = self.currentItem!.playerURL |
|||
if url!.pathExtension != "m3u8" { |
|||
alertController.addAction(downloadAction) |
|||
} |
|||
|
|||
let serverAction = UIAlertAction(title: "Download to server", style: .default) { (action) in |
|||
let url = self.currentItem!.playerURL |
|||
if url!.pathExtension == "m3u8" { |
|||
self.ffmpeg = true |
|||
} |
|||
self.downloadDelegate?.downloadToServer(path: self.currentItem!.root, url: url!, result: { |
|||
(r) in |
|||
print(r) |
|||
self.showAlert(title: "download ready", message: r) |
|||
if (r == "exists") { |
|||
|
|||
} |
|||
}) |
|||
} |
|||
alertController.addAction(serverAction) |
|||
|
|||
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (action) in |
|||
} |
|||
alertController.addAction(cancelAction) |
|||
|
|||
present(alertController, animated: true) |
|||
} |
|||
|
|||
func save(currentSnapshot c: MediaItem, name: String) { |
|||
do { |
|||
try FileHelper.createDir(name: name) |
|||
|
|||
var file = FileHelper.getDocumentsDirectory().appendingPathComponent(name).appendingPathComponent(c.name) |
|||
|
|||
if file.pathExtension != "mp4" { |
|||
file = file.appendingPathExtension("mp4") |
|||
} |
|||
print (file) |
|||
var dur = player.loopEnd - loopStart |
|||
if (dur < 0) { |
|||
return |
|||
} |
|||
|
|||
VideoHelper.export(item: player.avPlayer!.currentItem!, clipStart: loopStart, clipDuration: dur, file: file, progress: { p in print(p) }) { url in |
|||
self.showAlert(title: c.name, message: "saved") |
|||
} |
|||
var s : MediaModel; |
|||
if (c.type == ItemType.SNAPSHOT && c.parent != nil) { |
|||
s = c.parent!.toMediaModel() |
|||
do { |
|||
if c.thumbUrlAbsolute != "" { |
|||
let local = try Data(contentsOf: URL(string: c.thumbUrlAbsolute)!) |
|||
let tfile = FileHelper.getDocumentsDirectory().appendingPathComponent(name).appendingPathComponent(s.name).appendingPathExtension("jpg") |
|||
try local.write(to: tfile) |
|||
} |
|||
} |
|||
catch { |
|||
// ignore |
|||
} |
|||
} |
|||
else { |
|||
s = c.toMediaModel() |
|||
} |
|||
|
|||
let jfile = FileHelper.getDocumentsDirectory().appendingPathComponent(name).appendingPathComponent(s.name).appendingPathExtension("json") |
|||
let jsonEncoder = JSONEncoder() |
|||
let jsonData = try! jsonEncoder.encode(s) |
|||
let json = String(data: jsonData, encoding: String.Encoding.utf8) |
|||
print(json) |
|||
try json!.write(to: jfile, atomically: true, encoding: .utf8) |
|||
} catch { |
|||
print(error) |
|||
} |
|||
} |
|||
|
|||
@objc func startstop(_ sender: AnyObject) { |
|||
if player.isPlaying { |
|||
player.pause() |
|||
} |
|||
else { |
|||
if player.isPlayToTheEnd { |
|||
player.seekSmoothlyToTime(newChaseTime: 0) |
|||
player.isPlayToTheEnd = false |
|||
} |
|||
player.play() |
|||
} |
|||
print("play") |
|||
} |
|||
|
|||
@objc func loop(_ sender: AnyObject) { |
|||
if edit { |
|||
loopMode = false |
|||
} |
|||
else { |
|||
loopMode = !loopMode |
|||
} |
|||
updateLoop() |
|||
} |
|||
|
|||
func updateLoop() { |
|||
if loopMode { |
|||
loopButton!.title = "loop" |
|||
} |
|||
else { |
|||
loopButton!.title = "cont" |
|||
} |
|||
} |
|||
|
|||
@objc func aspect(_ sender: AnyObject) { |
|||
aspect += 1 |
|||
if aspect > 3 { |
|||
aspect = 1 |
|||
} |
|||
switch aspect { |
|||
case 1: |
|||
player.aspectx = 1.0 |
|||
player.aspecty = 1.0 |
|||
case 2: |
|||
player.aspectx = 0.9 |
|||
player.aspecty = 1.0 |
|||
case 3: |
|||
player.aspectx = 1.0 |
|||
player.aspecty = 0.9 |
|||
default: |
|||
print("aspect") |
|||
} |
|||
// todo player.verticalMoved(0) |
|||
player.transformLayer() |
|||
aspectButton!.title = "\(aspect)" |
|||
} |
|||
|
|||
@objc func speed(_ sender: AnyObject) { |
|||
speedOption += 1 |
|||
if speedOption >= speedOptions.count { |
|||
speedOption = 0 |
|||
} |
|||
let speed = Float(speedOptions[speedOption]) |
|||
player.playerLayer!.player!.rate = speed |
|||
|
|||
speedButton!.title = "\(speed)" |
|||
print("speed \(speed)") |
|||
} |
|||
|
|||
@IBAction func back(_ sender: AnyObject) { |
|||
player.playerLayer?.pause() |
|||
player.playerLayer?.prepareToDeinit() |
|||
|
|||
completionHandler!() |
|||
} |
|||
|
|||
func play(_ url: URL) { |
|||
var def = [BMPlayerResourceDefinition]() |
|||
var index = 0; |
|||
var count = 0; |
|||
|
|||
if allItems.isEmpty { |
|||
print("no items found") |
|||
return |
|||
} |
|||
|
|||
for i in allItems { |
|||
if let a = urls { |
|||
let r = BMPlayerCompositionResourceDefinition(url: i.playerURL!, definition: i.name) |
|||
r.assets = a |
|||
def.append(r) |
|||
} |
|||
else { |
|||
|
|||
let r = BMPlayerResourceDefinition(url: i.playerURL!, definition: i.name); |
|||
def.append(r) |
|||
if (LocalManager.sharedInstance.fixDocumentURL(url.absoluteString) == LocalManager.sharedInstance.fixDocumentURL(i.playerURL?.absoluteString)) { |
|||
index = count |
|||
} |
|||
count += 1 |
|||
} |
|||
} |
|||
let asset = BMPlayerResource(name: "video", definitions: def) |
|||
// let asset = BMPlayerResource(url: url) |
|||
|
|||
player.setVideo(resource: asset, definitionIndex: index) |
|||
player.playerLayer!.player!.automaticallyWaitsToMinimizeStalling = false |
|||
if let item = player.playerLayer?.playerItem { |
|||
item.canUseNetworkResourcesForLiveStreamingWhilePaused = true |
|||
item.preferredForwardBufferDuration = 10 |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
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) { |
|||
if let currentSnapshot = buttons[source] { |
|||
if (edit) { |
|||
self.currentSnapshot = currentSnapshot |
|||
editItem() |
|||
} |
|||
else { |
|||
gotoSnapshot(currentSnapshot: currentSnapshot) |
|||
} |
|||
} |
|||
} |
|||
|
|||
private func gotoSnapshot(currentSnapshot: MediaItem) { |
|||
player.forceSeekSmoothlyToTime(newChaseTime: currentSnapshot.time) |
|||
loopStart = currentSnapshot.time |
|||
player.loopEnd = loopStart + currentSnapshot.length |
|||
|
|||
if loopMode && currentSnapshot.scale > 0 { |
|||
player.zoom = Float(currentSnapshot.scale) |
|||
player.xpos = currentSnapshot.offset.x |
|||
player.ypos = currentSnapshot.offset.y |
|||
player.transformLayer() |
|||
} |
|||
|
|||
self.currentSnapshot = currentSnapshot |
|||
} |
|||
|
|||
@objc func update() { |
|||
reviewButton!.title = currentItem!.name |
|||
|
|||
if currentItem!.type == ItemType.SNAPSHOT { |
|||
player.seek(currentItem!.time) |
|||
loopStart = currentItem!.time |
|||
player.loopEnd = currentItem!.time + currentItem!.length |
|||
currentItem = currentItem!.parent |
|||
} else { |
|||
if !currentItem!.children.isEmpty { |
|||
player.seekSmoothlyToTime(newChaseTime: currentItem!.children[0].time) |
|||
} |
|||
else { |
|||
let duration = player.playerLayer!.playerItem!.duration |
|||
if !duration.isIndefinite { |
|||
print(duration) |
|||
player.seekSmoothlyToTime(newChaseTime: duration.seconds / 2.0) |
|||
} |
|||
} |
|||
} |
|||
|
|||
navigationItem.rightBarButtonItems = [barbutton!] |
|||
|
|||
for c in currentItem!.children { |
|||
addItemButton(c) |
|||
} |
|||
|
|||
player.play() |
|||
} |
|||
|
|||
func installGestures(_ moviePlayer: UIView) { |
|||
let twoFingersTwoTapsGesture = UITapGestureRecognizer(target: self, action: #selector(captureThumbnail)) |
|||
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") |
|||
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.playWithURL(currentItem!.playerURL) |
|||
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! |
|||
// player.currentPlaybackRate = Float(speedOptions[speedOption]) |
|||
} |
|||
|
|||
} |
|||
|
|||
@objc func swipeRight() { |
|||
// print("r") |
|||
// player.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 !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") |
|||
// player.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() { |
|||
// player.currentPlaybackRate = Float(speedOptions[speedOption]) |
|||
print("resumePlay") |
|||
} |
|||
|
|||
@objc func captureThumbnail() { |
|||
if edit { |
|||
let asset = player.playerLayer!.playerItem!.asset |
|||
|
|||
do { |
|||
let imgGenerator = AVAssetImageGenerator(asset: asset) |
|||
imgGenerator.appliesPreferredTrackTransform = true |
|||
let time = player.playerLayer!.playerItem!.currentTime() |
|||
let cgImage = try imgGenerator.copyCGImage(at: time, actualTime: nil) |
|||
let thumbnail = UIImage(cgImage: cgImage) |
|||
|
|||
showThumbnail(thumbnail: thumbnail, time: time) |
|||
|
|||
} catch let error { |
|||
print("*** Error generating thumbnail: \(error.localizedDescription)") |
|||
} |
|||
// thumbnailTime = player.currentPlaybackTime |
|||
print("tap \(thumbnailTime)") |
|||
// moviePlayer!.requestThumbnailImages(atTimes: [thumbnailTime], |
|||
// timeOption: MPMovieTimeOption.exact); |
|||
} |
|||
else { |
|||
detailDelegate!.favItem(currentItem!) |
|||
} |
|||
} |
|||
|
|||
func showThumbnail(thumbnail: UIImage, time: CMTime) { |
|||
let newItem = MediaItem(name: currentItem!.name, path: currentItem!.path, root: currentItem!.root, type: ItemType.SNAPSHOT) |
|||
newItem.image = thumbnail |
|||
newItem.time = time.seconds |
|||
newItem.parent = currentItem! |
|||
newItem.local = currentItem!.local |
|||
currentItem!.children.append(newItem) |
|||
|
|||
print(newItem.time) |
|||
|
|||
addItemButton(newItem) |
|||
} |
|||
|
|||
func moveUp() { |
|||
if !loopMode { |
|||
return |
|||
} |
|||
|
|||
let t = Date().timeIntervalSince1970 |
|||
if lastMove + 2 > t { |
|||
return |
|||
} |
|||
lastMove = t |
|||
|
|||
if let c = currentItem?.children { |
|||
if !c.isEmpty{ |
|||
if let s = currentSnapshot { |
|||
if var i = c.firstIndex { x in x===s } { |
|||
print(i) |
|||
i+=1 |
|||
if i >= c.count { |
|||
i = 0 |
|||
} |
|||
gotoSnapshot(currentSnapshot: c[i]) |
|||
} |
|||
} |
|||
else { |
|||
gotoSnapshot(currentSnapshot: c[0]) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
var lastMove = 0.0 |
|||
|
|||
func moveDown() { |
|||
if !loopMode { |
|||
return |
|||
} |
|||
|
|||
let t = Date().timeIntervalSince1970 |
|||
if lastMove + 2 > t { |
|||
return |
|||
} |
|||
lastMove = t |
|||
|
|||
if let c = currentItem?.children { |
|||
if !c.isEmpty{ |
|||
if let s = currentSnapshot { |
|||
if var i = c.firstIndex { x in x===s } { |
|||
print(i) |
|||
i-=1 |
|||
if i < 0 { |
|||
i = c.count-1 |
|||
} |
|||
gotoSnapshot(currentSnapshot: c[i]) |
|||
} |
|||
} |
|||
else { |
|||
gotoSnapshot(currentSnapshot: c[0]) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
func bmPlayer(player: BMPlayer) { |
|||
let speed = Float(speedOptions[speedOption]) |
|||
if let pl = player.playerLayer!.player { |
|||
pl.rate = speed |
|||
} |
|||
} |
|||
|
|||
func bmPlayer(player: BMPlayer, playerStateDidChange state: BMPlayerState) { |
|||
print("state") |
|||
|
|||
} |
|||
|
|||
func bmPlayer(player: BMPlayer, loadedTimeDidChange loadedDuration: TimeInterval, totalDuration: TimeInterval) { |
|||
// print("load") |
|||
} |
|||
|
|||
func bmPlayer(player: BMPlayer, playTimeDidChange currentTime: TimeInterval, totalTime: TimeInterval) { |
|||
if loopMode { |
|||
if currentTime > player.loopEnd && loopStart < player.loopEnd { |
|||
player.forceSeekSmoothlyToTime(newChaseTime: loopStart) |
|||
} |
|||
} |
|||
|
|||
if let b = backButton { |
|||
b.title = BMPlayer.formatSecondsToString(currentTime) |
|||
} |
|||
} |
|||
|
|||
func bmPlayer(player: BMPlayer, playerIsPlaying playing: Bool) { |
|||
print("playing") |
|||
} |
|||
|
|||
func bmPlayer(player: BMPlayer, playerOrientChanged isFullscreen: Bool) { |
|||
print("orient") |
|||
} |
|||
} |
|||
@ -1,820 +0,0 @@ |
|||
// |
|||
// BMPlayer.swift |
|||
// Pods |
|||
// |
|||
// Created by BrikerMan on 16/4/28. |
|||
// |
|||
// |
|||
|
|||
import UIKit |
|||
import SnapKit |
|||
import MediaPlayer |
|||
|
|||
/// BMPlayerDelegate to obserbe player state |
|||
public protocol BMPlayerDelegate : class { |
|||
func bmPlayer(player: BMPlayer) |
|||
func bmPlayer(player: BMPlayer, playerStateDidChange state: BMPlayerState) |
|||
func bmPlayer(player: BMPlayer, loadedTimeDidChange loadedDuration: TimeInterval, totalDuration: TimeInterval) |
|||
func bmPlayer(player: BMPlayer, playTimeDidChange currentTime : TimeInterval, totalTime: TimeInterval) |
|||
func bmPlayer(player: BMPlayer, playerIsPlaying playing: Bool) |
|||
func bmPlayer(player: BMPlayer, playerOrientChanged isFullscreen: Bool) |
|||
func moveUp() |
|||
func moveDown() |
|||
} |
|||
|
|||
/** |
|||
internal enum to check the pan direction |
|||
|
|||
- horizontal: horizontal |
|||
- vertical: vertical |
|||
*/ |
|||
enum BMPanDirection: Int { |
|||
case horizontal = 0 |
|||
case vertical = 1 |
|||
} |
|||
|
|||
open class BMPlayer: UIView, UIGestureRecognizerDelegate { |
|||
open var zoom = Float(1.0) |
|||
open var aspectx = Float(1.0) |
|||
open var aspecty = Float(1.0) |
|||
var xpos = 0.0 |
|||
var ypos = 0.0 |
|||
var loopEnd = 0.0 |
|||
|
|||
open var pinchGesture: UIPinchGestureRecognizer! |
|||
open var moveGesture: UIPanGestureRecognizer! |
|||
|
|||
open weak var delegate: BMPlayerDelegate? |
|||
|
|||
open var backBlock:((Bool) -> Void)? |
|||
|
|||
/// Gesture to change volume / brightness |
|||
open var panGesture: UIPanGestureRecognizer! |
|||
|
|||
private func initGesture() { |
|||
pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(self.pinched(_:))) |
|||
self.addGestureRecognizer(pinchGesture) |
|||
// moveGesture = UIPanGestureRecognizer(target: self, action: #selector(self.moved(_:))) |
|||
// moveGesture.minimumNumberOfTouches = 2 |
|||
// moveGesture.maximumNumberOfTouches = 2 |
|||
// self.addGestureRecognizer(moveGesture) |
|||
} |
|||
|
|||
func panDir(_ pan: UIPanGestureRecognizer) { |
|||
|
|||
let velocityPoint = pan.velocity(in: self) |
|||
|
|||
xpos += (Double(velocityPoint.x) / 50.0) |
|||
ypos += (Double(velocityPoint.y) / 50.0) |
|||
|
|||
transformLayer() |
|||
} |
|||
|
|||
@objc fileprivate func moved(_ gestureRecognizer: UIPanGestureRecognizer) { |
|||
} |
|||
|
|||
@objc fileprivate func pinched(_ gestureRecognizer: UIPinchGestureRecognizer) { |
|||
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed { |
|||
zoom *= Float(gestureRecognizer.scale) |
|||
|
|||
if zoom < 0.1 { |
|||
zoom = 1.0 |
|||
xpos = 0 |
|||
ypos = 0 |
|||
} |
|||
|
|||
gestureRecognizer.scale = 1.0 |
|||
|
|||
transformLayer() |
|||
} |
|||
|
|||
} |
|||
|
|||
func transformLayer() { |
|||
let t = CATransform3DMakeTranslation(CGFloat(xpos), CGFloat(ypos), 0.0) |
|||
playerLayer!.layer.transform = CATransform3DScale(t, CGFloat(zoom * aspectx), CGFloat(zoom * aspecty), 1.0) |
|||
} |
|||
|
|||
/// AVLayerVideoGravityType |
|||
open var videoGravity = AVLayerVideoGravity.resizeAspect { |
|||
didSet { |
|||
self.playerLayer?.videoGravity = videoGravity |
|||
} |
|||
} |
|||
|
|||
open var isPlaying: Bool { |
|||
get { |
|||
return playerLayer?.isPlaying ?? false |
|||
} |
|||
} |
|||
|
|||
//Closure fired when play time changed |
|||
open var playTimeDidChange:((TimeInterval, TimeInterval) -> Void)? |
|||
|
|||
//Closure fired when play state chaged |
|||
@available(*, deprecated, message: "Use newer `isPlayingStateChanged`") |
|||
open var playStateDidChange:((Bool) -> Void)? |
|||
|
|||
open var playOrientChanged:((Bool) -> Void)? |
|||
|
|||
open var isPlayingStateChanged:((Bool) -> Void)? |
|||
|
|||
open var playStateChanged:((BMPlayerState) -> Void)? |
|||
|
|||
open var avPlayer: AVPlayer? { |
|||
return playerLayer?.player |
|||
} |
|||
|
|||
open var playerLayer: BMPlayerLayerView? |
|||
|
|||
fileprivate var resource: BMPlayerResource! |
|||
|
|||
fileprivate var currentDefinition = 0 |
|||
|
|||
var controlView: BMPlayerControlView! |
|||
|
|||
fileprivate var customControlView: BMPlayerControlView? |
|||
|
|||
fileprivate var isFullScreen:Bool { |
|||
get { |
|||
return UIApplication.shared.statusBarOrientation.isLandscape |
|||
} |
|||
} |
|||
|
|||
/// 滑动方向 |
|||
fileprivate var panDirection = BMPanDirection.horizontal |
|||
|
|||
/// 音量滑竿 |
|||
fileprivate var volumeViewSlider: UISlider! |
|||
|
|||
fileprivate let BMPlayerAnimationTimeInterval: Double = 4.0 |
|||
fileprivate let BMPlayerControlBarAutoFadeOutTimeInterval: Double = 0.5 |
|||
|
|||
/// 用来保存时间状态 |
|||
fileprivate var sumTime : TimeInterval = 0 |
|||
var totalDuration : TimeInterval = 0 |
|||
fileprivate var currentPosition : TimeInterval = 0 |
|||
fileprivate var shouldSeekTo : TimeInterval = 0 |
|||
|
|||
fileprivate var isURLSet = false |
|||
fileprivate var isSliderSliding = false |
|||
fileprivate var isPauseByUser = false |
|||
fileprivate var isVolume = false |
|||
fileprivate var isSkip = false |
|||
fileprivate var skipAmount = 0.0 |
|||
|
|||
fileprivate var isMaskShowing = false |
|||
fileprivate var isSlowed = false |
|||
fileprivate var isMirrored = false |
|||
var isPlayToTheEnd = false |
|||
//视频画面比例 |
|||
fileprivate var aspectRatio: BMPlayerAspectRatio = .default |
|||
|
|||
//Cache is playing result to improve callback performance |
|||
fileprivate var isPlayingCache: Bool? = nil |
|||
|
|||
var isScrub = false |
|||
var isSeekInProgress = false |
|||
var chaseTime = CMTime.zero |
|||
var continueTime = 0.0 |
|||
|
|||
// MARK: - Public functions |
|||
|
|||
/** |
|||
Play |
|||
|
|||
- parameter resource: media resource |
|||
- parameter definitionIndex: starting definition index, default start with the first definition |
|||
*/ |
|||
open func setVideo(resource: BMPlayerResource, definitionIndex: Int = 0) { |
|||
isURLSet = false |
|||
self.resource = resource |
|||
|
|||
currentDefinition = definitionIndex |
|||
controlView.prepareUI(for: resource, selectedIndex: definitionIndex) |
|||
|
|||
if BMPlayerConf.shouldAutoPlay { |
|||
isURLSet = true |
|||
let asset = resource.definitions[definitionIndex] |
|||
let size = asset.avURLAsset.videoSize() |
|||
|
|||
if (size.height < 1500) { |
|||
zoom = LocalManager.sharedInstance.settings.scale |
|||
if (zoom < 1.0) { |
|||
xpos = -100 |
|||
} |
|||
|
|||
transformLayer() |
|||
} |
|||
|
|||
playerLayer?.playAsset(asset: asset.avURLAsset) |
|||
|
|||
} else { |
|||
controlView.showCover(url: resource.cover) |
|||
controlView.hideLoader() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
auto start playing, call at viewWillAppear, See more at pause |
|||
*/ |
|||
open func autoPlay() { |
|||
if !isPauseByUser && isURLSet && !isPlayToTheEnd { |
|||
play() |
|||
} |
|||
} |
|||
|
|||
func isFile() -> Bool { |
|||
guard resource != nil else { return false } |
|||
|
|||
let asset = resource.definitions[currentDefinition] |
|||
|
|||
return asset.url.isFileURL |
|||
} |
|||
|
|||
/** |
|||
Play |
|||
*/ |
|||
open func play() { |
|||
guard resource != nil else { return } |
|||
|
|||
if !isURLSet { |
|||
let asset = resource.definitions[currentDefinition] |
|||
playerLayer?.playAsset(asset: asset.avURLAsset) |
|||
controlView.hideCoverImageView() |
|||
isURLSet = true |
|||
} |
|||
|
|||
panGesture.isEnabled = true |
|||
playerLayer?.play() |
|||
isPauseByUser = false |
|||
|
|||
if let d = delegate { d.bmPlayer(player: self) } |
|||
} |
|||
|
|||
/** |
|||
Pause |
|||
|
|||
- parameter allow: should allow to response `autoPlay` function |
|||
*/ |
|||
open func pause(allowAutoPlay allow: Bool = false) { |
|||
playerLayer?.pause() |
|||
isPauseByUser = !allow |
|||
} |
|||
|
|||
/** |
|||
seek |
|||
|
|||
- parameter to: target time |
|||
*/ |
|||
open func seek(_ to:TimeInterval, completion: (()->Void)? = nil) { |
|||
chaseTime = CMTime(value: Int64(to * 10000), timescale: CMTimeScale(10000)) |
|||
playerLayer?.seek(to: to, completion: completion) |
|||
} |
|||
|
|||
/** |
|||
update UI to fullScreen |
|||
*/ |
|||
open func updateUI(_ isFullScreen: Bool) { |
|||
controlView.updateUI(isFullScreen) |
|||
} |
|||
|
|||
/** |
|||
increade volume with step, default step 0.1 |
|||
|
|||
- parameter step: step |
|||
*/ |
|||
open func addVolume(step: Float = 0.1) { |
|||
self.volumeViewSlider.value += step |
|||
} |
|||
|
|||
/** |
|||
decreace volume with step, default step 0.1 |
|||
|
|||
- parameter step: step |
|||
*/ |
|||
open func reduceVolume(step: Float = 0.1) { |
|||
self.volumeViewSlider.value -= step |
|||
} |
|||
|
|||
/** |
|||
prepare to dealloc player, call at View or Controllers deinit funciton. |
|||
*/ |
|||
open func prepareToDealloc() { |
|||
playerLayer?.prepareToDeinit() |
|||
controlView.prepareToDealloc() |
|||
} |
|||
|
|||
/** |
|||
If you want to create BMPlayer with custom control in storyboard. |
|||
create a subclass and override this method. |
|||
|
|||
- return: costom control which you want to use |
|||
*/ |
|||
open func storyBoardCustomControl() -> BMPlayerControlView? { |
|||
return nil |
|||
} |
|||
|
|||
// https://gist.github.com/shaps80/ac16b906938ad256e1f47b52b4809512 |
|||
public func forceSeekSmoothlyToTime(newChaseTime: Double) { |
|||
chaseTime = CMTime.zero |
|||
seekSmoothlyToTime(newChaseTime: CMTime(value: Int64(newChaseTime * 10000), timescale: CMTimeScale(10000))) |
|||
} |
|||
public func seekSmoothlyToTime(newChaseTime: Double) { |
|||
seekSmoothlyToTime(newChaseTime: CMTime(value: Int64(newChaseTime * 10000), timescale: CMTimeScale(10000))) |
|||
} |
|||
|
|||
public func seekSmoothlyToTime(newChaseTime: CMTime) { |
|||
if CMTimeCompare(newChaseTime, chaseTime) != 0 { |
|||
chaseTime = newChaseTime |
|||
|
|||
if !isSeekInProgress { |
|||
trySeekToChaseTime() |
|||
} |
|||
} |
|||
} |
|||
|
|||
private func trySeekToChaseTime() { |
|||
guard playerLayer!.player?.status == .readyToPlay else { return } |
|||
actuallySeekToTime() |
|||
} |
|||
|
|||
private func actuallySeekToTime() { |
|||
isSeekInProgress = true |
|||
let seekTimeInProgress = chaseTime |
|||
|
|||
playerLayer!.player?.seek(to: seekTimeInProgress, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero) { [weak self] _ in |
|||
guard let `self` = self else { return } |
|||
|
|||
if CMTimeCompare(seekTimeInProgress, self.chaseTime) == 0 { |
|||
self.isSeekInProgress = false |
|||
} else { |
|||
self.trySeekToChaseTime() |
|||
} |
|||
} |
|||
} |
|||
|
|||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { |
|||
if touch.view?.isDescendant(of: controlView.topWrapperView) == true { |
|||
return true |
|||
} |
|||
else { |
|||
return true |
|||
} |
|||
} |
|||
// MARK: - Action Response |
|||
|
|||
@objc open func panDirection(_ pan: UIPanGestureRecognizer) { |
|||
let resolution = 10000.0 |
|||
|
|||
if pan.numberOfTouches <= 1 { |
|||
// 根据在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 (locationPoint.y < self.bounds.size.height * 1 / 7) { |
|||
self.isSkip = (zoom <= 1.0) |
|||
isScrub = false |
|||
|
|||
if (!isPlaying) { |
|||
isScrub = true |
|||
let currentTime = CMTimeGetSeconds(playerLayer!.player!.currentTime()) |
|||
continueTime = currentTime |
|||
let vel = velocityPoint.x + velocityPoint.y |
|||
var time = CMTimeMake(value: Int64((currentTime + (Float64(vel) / resolution))*10000), timescale: 10000) |
|||
seekSmoothlyToTime(newChaseTime: time) |
|||
} |
|||
|
|||
} else { |
|||
isScrub = false |
|||
self.isSkip = true |
|||
self.skipAmount = 0.0 |
|||
} |
|||
|
|||
if x > y { |
|||
if BMPlayerConf.enablePlaytimeGestures { |
|||
self.panDirection = BMPanDirection.horizontal |
|||
|
|||
|
|||
// 给sumTime初值 |
|||
if let player = playerLayer?.player { |
|||
let time = player.currentTime() |
|||
self.sumTime = TimeInterval(time.value) / TimeInterval(time.timescale) |
|||
} |
|||
} |
|||
} else { |
|||
self.panDirection = BMPanDirection.vertical |
|||
if locationPoint.x > self.bounds.size.width / 2 { |
|||
self.isVolume = false |
|||
} else { |
|||
self.isVolume = false |
|||
} |
|||
} |
|||
|
|||
case UIGestureRecognizer.State.changed: |
|||
if !self.isSkip && isPlaying { |
|||
xpos += (Double(velocityPoint.x) * 0.1); |
|||
ypos += (Double(velocityPoint.y) * 0.1); |
|||
transformLayer() |
|||
} |
|||
if (isScrub) { |
|||
let vel = velocityPoint.x + velocityPoint.y |
|||
var time = CMTimeMake(value: Int64((Float64(vel) / resolution)*10000), timescale: 10000) |
|||
seekSmoothlyToTime(newChaseTime: CMTimeAdd(chaseTime, time)) |
|||
} |
|||
switch self.panDirection { |
|||
case BMPanDirection.horizontal: |
|||
self.horizontalMoved(velocityPoint.x) |
|||
case BMPanDirection.vertical: |
|||
self.verticalMoved(velocityPoint.y) |
|||
} |
|||
|
|||
case UIGestureRecognizer.State.ended: |
|||
// 移动结束也需要判断垂直或者平移 |
|||
// 比如水平移动结束时,要快进到指定位置,如果这里没有判断,当我们调节音量完之后,会出现屏幕跳动的bug |
|||
switch (self.panDirection) { |
|||
case BMPanDirection.horizontal: |
|||
controlView.hideSeekToView() |
|||
isSliderSliding = false |
|||
|
|||
if isSkip && !isScrub { |
|||
print(skipAmount) |
|||
if (locationPoint.y < self.bounds.size.height * 1 / 8) { |
|||
if skipAmount > 0 { |
|||
self.sumTime += TimeInterval(5) |
|||
} else if skipAmount < -0 { |
|||
self.sumTime -= TimeInterval(5) |
|||
} |
|||
} |
|||
else { |
|||
if (skipAmount > 1500) { |
|||
self.sumTime += TimeInterval(30) |
|||
|
|||
if sumTime > loopEnd { |
|||
loopEnd = 1000000; |
|||
} |
|||
} else if skipAmount > 0 { |
|||
self.sumTime += TimeInterval(10) |
|||
} else if skipAmount > -1500 { |
|||
self.sumTime -= TimeInterval(10) |
|||
} else { |
|||
self.sumTime -= TimeInterval(30) |
|||
} |
|||
} |
|||
controlView.controlViewAnimation(isShow: true) |
|||
|
|||
if isPlayToTheEnd { |
|||
isPlayToTheEnd = false |
|||
seek(self.sumTime, completion: { [weak self] in |
|||
self?.play() |
|||
}) |
|||
} else { |
|||
seek(self.sumTime, completion: { [weak self] in |
|||
self?.autoPlay() |
|||
}) |
|||
} |
|||
} |
|||
// 把sumTime滞空,不然会越加越多 |
|||
self.sumTime = 0.0 |
|||
|
|||
if (isScrub && !isFile()) { |
|||
seek(continueTime) |
|||
} |
|||
|
|||
isScrub = false |
|||
|
|||
case BMPanDirection.vertical: |
|||
self.isVolume = false |
|||
} |
|||
default: |
|||
break |
|||
} |
|||
} |
|||
else { |
|||
panDir(pan) |
|||
} |
|||
} |
|||
|
|||
open func verticalMoved(_ value: CGFloat) { |
|||
print(value) |
|||
if (value < -100) { |
|||
delegate?.moveDown() |
|||
// controlView(controlView: controlView, didChooseDefinition: currentDefinition+1); |
|||
} |
|||
|
|||
if (value > 100) { |
|||
delegate?.moveUp() |
|||
// controlView(controlView: controlView, didChooseDefinition: currentDefinition+1); |
|||
} |
|||
|
|||
if !isSkip { |
|||
} |
|||
} |
|||
|
|||
open func horizontalMoved(_ value: CGFloat) { |
|||
guard BMPlayerConf.enablePlaytimeGestures else { return } |
|||
|
|||
isSliderSliding = true |
|||
|
|||
if !isSkip { |
|||
} |
|||
else { |
|||
skipAmount += Double(value) |
|||
} |
|||
} |
|||
|
|||
@objc open func onOrientationChanged() { |
|||
self.updateUI(isFullScreen) |
|||
delegate?.bmPlayer(player: self, playerOrientChanged: isFullScreen) |
|||
playOrientChanged?(isFullScreen) |
|||
} |
|||
|
|||
@objc fileprivate func fullScreenButtonPressed() { |
|||
controlView.updateUI(!self.isFullScreen) |
|||
if isFullScreen { |
|||
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") |
|||
UIApplication.shared.setStatusBarHidden(false, with: .fade) |
|||
UIApplication.shared.statusBarOrientation = .portrait |
|||
} else { |
|||
UIDevice.current.setValue(UIInterfaceOrientation.landscapeRight.rawValue, forKey: "orientation") |
|||
UIApplication.shared.setStatusBarHidden(false, with: .fade) |
|||
UIApplication.shared.statusBarOrientation = .landscapeRight |
|||
} |
|||
} |
|||
|
|||
// MARK: - 生命周期 |
|||
deinit { |
|||
playerLayer?.pause() |
|||
playerLayer?.prepareToDeinit() |
|||
NotificationCenter.default.removeObserver(self, name: UIApplication.didChangeStatusBarOrientationNotification, object: nil) |
|||
} |
|||
|
|||
|
|||
required public init?(coder aDecoder: NSCoder) { |
|||
super.init(coder: aDecoder) |
|||
if let customControlView = storyBoardCustomControl() { |
|||
self.customControlView = customControlView |
|||
} |
|||
initUI() |
|||
initUIData() |
|||
configureVolume() |
|||
preparePlayer() |
|||
initGesture() |
|||
} |
|||
|
|||
// @available(*, deprecated:3.0, message:"Use newer init(customControlView:_)") |
|||
// public convenience init(customControllView: BMPlayerControlView?) { |
|||
// self.init(customControlView: customControllView) |
|||
// } |
|||
|
|||
@objc public init(customControlView: BMPlayerControlView?) { |
|||
super.init(frame:CGRect.zero) |
|||
self.customControlView = customControlView |
|||
initUI() |
|||
initUIData() |
|||
configureVolume() |
|||
preparePlayer() |
|||
initGesture() |
|||
} |
|||
|
|||
public convenience init() { |
|||
self.init(customControlView:nil) |
|||
} |
|||
|
|||
// MARK: - 初始化 |
|||
fileprivate func initUI() { |
|||
self.backgroundColor = UIColor.black |
|||
|
|||
if let customView = customControlView { |
|||
controlView = customView |
|||
} else { |
|||
controlView = BMPlayerControlView() |
|||
} |
|||
|
|||
addSubview(controlView) |
|||
controlView.updateUI(isFullScreen) |
|||
controlView.delegate = self |
|||
controlView.player = self |
|||
controlView.snp.makeConstraints { (make) in |
|||
make.edges.equalTo(self) |
|||
} |
|||
|
|||
panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panDirection(_:))) |
|||
panGesture.delegate = self |
|||
// panGesture.minimumNumberOfTouches = 1 |
|||
// panGesture.maximumNumberOfTouches = 1 |
|||
self.addGestureRecognizer(panGesture) |
|||
} |
|||
|
|||
fileprivate func initUIData() { |
|||
NotificationCenter.default.addObserver(self, selector: #selector(self.onOrientationChanged), name: UIApplication.didChangeStatusBarOrientationNotification, object: nil) |
|||
} |
|||
|
|||
fileprivate func configureVolume() { |
|||
let volumeView = MPVolumeView() |
|||
for view in volumeView.subviews { |
|||
if let slider = view as? UISlider { |
|||
self.volumeViewSlider = slider |
|||
} |
|||
} |
|||
} |
|||
|
|||
fileprivate func preparePlayer() { |
|||
playerLayer = BMPlayerLayerView() |
|||
playerLayer!.videoGravity = videoGravity |
|||
insertSubview(playerLayer!, at: 0) |
|||
playerLayer!.snp.makeConstraints { [weak self](make) in |
|||
guard let `self` = self else { return } |
|||
make.edges.equalTo(self) |
|||
} |
|||
playerLayer!.delegate = self |
|||
controlView.showLoader() |
|||
self.layoutIfNeeded() |
|||
} |
|||
} |
|||
|
|||
extension BMPlayer: BMPlayerLayerViewDelegate { |
|||
public func bmPlayer(player: BMPlayerLayerView, playerIsPlaying playing: Bool) { |
|||
controlView.playStateDidChange(isPlaying: playing) |
|||
delegate?.bmPlayer(player: self, playerIsPlaying: playing) |
|||
playStateDidChange?(player.isPlaying) |
|||
isPlayingStateChanged?(player.isPlaying) |
|||
} |
|||
|
|||
public func bmPlayer(player: BMPlayerLayerView, loadedTimeDidChange loadedDuration: TimeInterval, totalDuration: TimeInterval) { |
|||
BMPlayerManager.shared.log("loadedTimeDidChange - \(loadedDuration) - \(totalDuration)") |
|||
controlView.loadedTimeDidChange(loadedDuration: loadedDuration, totalDuration: totalDuration) |
|||
delegate?.bmPlayer(player: self, loadedTimeDidChange: loadedDuration, totalDuration: totalDuration) |
|||
controlView.totalDuration = totalDuration |
|||
self.totalDuration = totalDuration |
|||
} |
|||
|
|||
public func bmPlayer(player: BMPlayerLayerView, playerStateDidChange state: BMPlayerState) { |
|||
BMPlayerManager.shared.log("playerStateDidChange - \(state)") |
|||
|
|||
controlView.playerStateDidChange(state: state) |
|||
switch state { |
|||
case .readyToPlay: |
|||
if !isPauseByUser { |
|||
play() |
|||
} |
|||
if shouldSeekTo != 0 { |
|||
seek(shouldSeekTo, completion: {[weak self] in |
|||
guard let `self` = self else { return } |
|||
if !self.isPauseByUser { |
|||
self.play() |
|||
} else { |
|||
self.pause() |
|||
} |
|||
}) |
|||
shouldSeekTo = 0 |
|||
} |
|||
|
|||
case .bufferFinished: |
|||
autoPlay() |
|||
|
|||
case .playedToTheEnd: |
|||
isPlayToTheEnd = true |
|||
|
|||
default: |
|||
break |
|||
} |
|||
panGesture.isEnabled = state != .playedToTheEnd |
|||
delegate?.bmPlayer(player: self, playerStateDidChange: state) |
|||
playStateChanged?(state) |
|||
} |
|||
|
|||
public func bmPlayer(player: BMPlayerLayerView, playTimeDidChange currentTime: TimeInterval, totalTime: TimeInterval) { |
|||
// BMPlayerManager.shared.log("playTimeDidChange - \(currentTime) - \(totalTime)") |
|||
delegate?.bmPlayer(player: self, playTimeDidChange: currentTime, totalTime: totalTime) |
|||
self.currentPosition = currentTime |
|||
totalDuration = totalTime |
|||
if isSliderSliding { |
|||
return |
|||
} |
|||
controlView.playTimeDidChange(currentTime: currentTime, totalTime: totalTime) |
|||
controlView.totalDuration = totalDuration |
|||
playTimeDidChange?(currentTime, totalTime) |
|||
} |
|||
} |
|||
|
|||
extension BMPlayer: BMPlayerControlViewDelegate { |
|||
open func controlView(controlView: BMPlayerControlView, |
|||
didChooseDefinition index: Int) { |
|||
shouldSeekTo = currentPosition |
|||
playerLayer?.resetPlayer() |
|||
currentDefinition = index |
|||
playerLayer?.playAsset(asset: resource.definitions[index].avURLAsset) |
|||
} |
|||
|
|||
open func controlView(controlView: BMPlayerControlView, |
|||
didPressButton button: UIButton) { |
|||
if let action = BMPlayerControlView.ButtonType(rawValue: button.tag) { |
|||
switch action { |
|||
case .back: |
|||
backBlock?(isFullScreen) |
|||
if isFullScreen { |
|||
fullScreenButtonPressed() |
|||
} else { |
|||
playerLayer?.prepareToDeinit() |
|||
} |
|||
|
|||
case .play: |
|||
if button.isSelected { |
|||
pause() |
|||
} else { |
|||
if isPlayToTheEnd { |
|||
seek(0, completion: {[weak self] in |
|||
self?.play() |
|||
}) |
|||
controlView.hidePlayToTheEndView() |
|||
isPlayToTheEnd = false |
|||
} |
|||
play() |
|||
} |
|||
|
|||
case .replay: |
|||
isPlayToTheEnd = false |
|||
seek(0) |
|||
play() |
|||
|
|||
case .fullscreen: |
|||
fullScreenButtonPressed() |
|||
|
|||
default: |
|||
print("[Error] unhandled Action") |
|||
} |
|||
} |
|||
} |
|||
|
|||
open func controlView(controlView: BMPlayerControlView, |
|||
slider: UISlider, |
|||
onSliderEvent event: UIControl.Event) { |
|||
switch event { |
|||
case .touchDown: |
|||
playerLayer?.onTimeSliderBegan() |
|||
isSliderSliding = true |
|||
|
|||
case .valueChanged: |
|||
let target = self.totalDuration * Double(slider.value) |
|||
|
|||
seekSmoothlyToTime(newChaseTime: CMTime(seconds: target, preferredTimescale: 10000)) |
|||
|
|||
break |
|||
case .touchUpInside : |
|||
isSliderSliding = false |
|||
let target = self.totalDuration * Double(slider.value) |
|||
|
|||
if isPlayToTheEnd { |
|||
isPlayToTheEnd = false |
|||
seek(target, completion: {[weak self] in |
|||
self?.play() |
|||
}) |
|||
controlView.hidePlayToTheEndView() |
|||
} else { |
|||
seek(target, completion: {[weak self] in |
|||
self?.autoPlay() |
|||
}) |
|||
} |
|||
default: |
|||
break |
|||
} |
|||
} |
|||
|
|||
open func controlView(controlView: BMPlayerControlView, didChangeVideoAspectRatio: BMPlayerAspectRatio) { |
|||
self.playerLayer?.aspectRatio = self.aspectRatio |
|||
} |
|||
|
|||
open func controlView(controlView: BMPlayerControlView, didChangeVideoPlaybackRate rate: Float) { |
|||
self.playerLayer?.player?.rate = rate |
|||
} |
|||
} |
|||
|
|||
extension AVAsset{ |
|||
func videoSize()->CGSize{ |
|||
let tracks = self.tracks(withMediaType: AVMediaType.video) |
|||
if (tracks.count > 0){ |
|||
let videoTrack = tracks[0] |
|||
let size = videoTrack.naturalSize |
|||
let txf = videoTrack.preferredTransform |
|||
let realVidSize = size.applying(txf) |
|||
print(videoTrack) |
|||
print(txf) |
|||
print(size) |
|||
print(realVidSize) |
|||
return realVidSize |
|||
} |
|||
return CGSize(width: 0, height: 0) |
|||
} |
|||
|
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
// |
|||
// 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) |
|||
} |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
// |
|||
// Created by Marco Schmickler on 05.05.21. |
|||
// Copyright (c) 2021 Marco Schmickler. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
import AVFoundation |
|||
|
|||
class BMPlayerCompositionResourceDefinition : BMPlayerResourceDefinition { |
|||
var assets: [URL]? |
|||
|
|||
override var avURLAsset: AVAsset { |
|||
get { |
|||
let asset = VideoHelper.combine(urls: assets!) |
|||
|
|||
return asset |
|||
} |
|||
} |
|||
} |
|||
@ -1,757 +0,0 @@ |
|||
// |
|||
// 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.0 ) |
|||
|
|||
// Top views |
|||
topMaskView.addSubview(topWrapperView) |
|||
|
|||
// 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: 14) |
|||
currentTimeLabel.text = "00:00" |
|||
currentTimeLabel.textAlignment = NSTextAlignment.center |
|||
currentTimeLabel.backgroundColor = UIColor.black |
|||
|
|||
totalTimeLabel.textColor = UIColor.white |
|||
totalTimeLabel.font = UIFont.systemFont(ofSize: 14) |
|||
totalTimeLabel.text = "00:00" |
|||
totalTimeLabel.textAlignment = NSTextAlignment.center |
|||
totalTimeLabel.backgroundColor = UIColor.black |
|||
|
|||
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) |
|||
} |
|||
} |
|||
|
|||
@ -1,91 +0,0 @@ |
|||
// |
|||
// 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: AVAsset { |
|||
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 |
|||
} |
|||
} |
|||
@ -1,506 +0,0 @@ |
|||
// |
|||
// 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: AVAsset? |
|||
|
|||
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: AVAsset) { |
|||
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)*10000, timescale: 10000) |
|||
// self.player!.cancelPendingPrerolls() |
|||
self.playerItem!.cancelPendingSeeks() |
|||
self.player!.seek(to: draggedTime, toleranceBefore: CMTimeMake(value: 1, timescale: 1), toleranceAfter: CMTimeMake(value: 1, 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 |
|||
} |
|||
} |
|||
|
|||
@ -1,62 +0,0 @@ |
|||
// |
|||
// 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(100) |
|||
|
|||
/// 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) |
|||
} |
|||
} |
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
// |
|||
// 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) |
|||
} |
|||
} |
|||
@ -1,122 +0,0 @@ |
|||
// |
|||
// 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 |
|||
} |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
// |
|||
// 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 |
|||
} |
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
// |
|||
// Created by Marco Schmickler on 14.11.21. |
|||
// Copyright (c) 2021 Marco Schmickler. All rights reserved. |
|||
// |
|||
|
|||
|
|||
import SwiftUI |
|||
import AVKit |
|||
|
|||
struct KVideoPlayer: View { |
|||
private let player = AVPlayer(url: URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!) |
|||
|
|||
var body: some View { |
|||
VideoPlayer(player: player) |
|||
.onAppear() { |
|||
// Start the player going, otherwise controls don't appear |
|||
player.play() |
|||
} |
|||
.onDisappear() { |
|||
// Stop the player when the view disappears |
|||
player.pause() |
|||
} |
|||
} |
|||
} |
|||
|
|||
struct KVideoPlayer_Previews: PreviewProvider { |
|||
static var previews: some View { |
|||
KVideoPlayer() |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue