视频详情播放,创建历史记录,数据加密
This commit is contained in:
parent
335ede5be7
commit
0f79f340d9
@ -13,6 +13,8 @@ extension AppDelegate {
|
||||
// UIView.et_Awake()
|
||||
tabBarConfig()
|
||||
// keyBoardStyle()
|
||||
|
||||
SPToast.config()
|
||||
}
|
||||
|
||||
|
||||
|
@ -18,6 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
self.appConfig()
|
||||
|
||||
SPLoginManager.manager.requestVisitorLogin(completer: nil)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ class SPNavigationController: UINavigationController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.jx_transitionAwake()
|
||||
// Do any additional setup after loading the view.
|
||||
}
|
||||
|
||||
|
@ -12,11 +12,13 @@ class SPTabBarController: UITabBarController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let nav1 = createNavigationController(viewController: SPHomePageController(), title: "Home".localized, image: UIImage(named: "tabbar_icon_01_selected"), selectedImage: UIImage(named: "tabbar_icon_01_selected"))
|
||||
let nav1 = createNavigationController(viewController: SPHomePageController(), title: "Home".localized, image: UIImage(named: "tabbar_icon_01"), selectedImage: UIImage(named: "tabbar_icon_01_selected"))
|
||||
|
||||
let nav2 = createNavigationController(viewController: SPForYouViewController(), title: "For You".localized, image: UIImage(named: "tabbar_icon_01_selected"), selectedImage: UIImage(named: "tabbar_icon_01_selected"))
|
||||
let nav2 = createNavigationController(viewController: SPForYouViewController(), title: "For You".localized, image: UIImage(named: "tabbar_icon_02"), selectedImage: UIImage(named: "tabbar_icon_02_selected"))
|
||||
|
||||
self.viewControllers = [nav1, nav2]
|
||||
let nav5 = createNavigationController(viewController: SPMineViewController(), title: "Profile".localized, image: UIImage(named: "tabbar_icon_05"), selectedImage: UIImage(named: "tabbar_icon_05_selected"))
|
||||
|
||||
self.viewControllers = [nav1, nav2, nav5]
|
||||
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ class SPViewController: UIViewController, JYPageChildContollerProtocol {
|
||||
super.viewDidLoad()
|
||||
self.isViewDidLoad = true
|
||||
self.edgesForExtendedLayout = []
|
||||
self.view.backgroundColor = .black
|
||||
|
||||
if let navi = navigationController {
|
||||
if navi.visibleViewController == self {
|
||||
|
@ -24,4 +24,5 @@ class SPHomeAPI: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
41
ShortPlay/Base/Networking/API/SPVideoAPI.swift
Normal file
41
ShortPlay/Base/Networking/API/SPVideoAPI.swift
Normal file
@ -0,0 +1,41 @@
|
||||
//
|
||||
// SPVideoAPI.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/10.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPVideoAPI: NSObject {
|
||||
|
||||
///获取视频详情
|
||||
static func requestVideoDetail(videoId: String, shortPlayId: String, completer: ((_ model: SPVideoDetailModel?) -> Void)?) {
|
||||
|
||||
var param = SPNetworkParameters(path: "/getVideoDetails")
|
||||
param.method = .get
|
||||
param.parameters = [
|
||||
"video_id" : videoId,
|
||||
"short_play_id" : shortPlayId
|
||||
]
|
||||
|
||||
SPNetwork.request(parameters: param) { (response: SPNetworkResponse<SPVideoDetailModel>) in
|
||||
completer?(response.data)
|
||||
}
|
||||
}
|
||||
|
||||
///创建播放记录
|
||||
static func requestRequestVideoPlayHistory(videoId: String, shortPlayId: String) {
|
||||
var param = SPNetworkParameters(path: "/createHistory")
|
||||
param.isLoding = false
|
||||
param.isToast = false
|
||||
param.parameters = [
|
||||
"video_id" : videoId,
|
||||
"short_play_id" : shortPlayId
|
||||
]
|
||||
|
||||
SPNetwork.request(parameters: param) { (response: SPNetworkResponse<String>) in
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -90,6 +90,7 @@ extension SPApi: TargetType {
|
||||
"system-type" : "ios",
|
||||
"idfa" : JXUUID.idfa(),
|
||||
"model" : UIDevice.sp_machineModelName(),
|
||||
// "security" : "false",
|
||||
]
|
||||
//登录信息
|
||||
dic["authorization"] = userToken
|
||||
|
173
ShortPlay/Base/Networking/Base/SPCryptService.swift
Normal file
173
ShortPlay/Base/Networking/Base/SPCryptService.swift
Normal file
@ -0,0 +1,173 @@
|
||||
//
|
||||
// SPCryptService.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/10.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPCryptService: NSObject {
|
||||
|
||||
static let BF_SIZE = 2048
|
||||
static let EN_STR_TAG = "$"
|
||||
|
||||
// 生成随机盐
|
||||
static func randSalt() -> Data {
|
||||
let size = Int.random(in: 16...64) // 随机盐的长度
|
||||
var salt = Data(capacity: size)
|
||||
|
||||
for _ in 0..<size {
|
||||
salt.append(UInt8.random(in: 0...255))
|
||||
}
|
||||
|
||||
return salt
|
||||
}
|
||||
|
||||
// 加密数据
|
||||
static func encrypt(_ data: String) -> String? {
|
||||
guard !data.isEmpty else {
|
||||
return data
|
||||
}
|
||||
|
||||
// 确保数据是UTF-8编码
|
||||
guard let utf8Data = data.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 随机盐
|
||||
let salt = randSalt()
|
||||
let saltLen = UInt8(salt.count)
|
||||
|
||||
// 加密数据
|
||||
guard let encryptedData = encryptWithSalt(utf8Data, salt: salt) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建完整加密数据: [盐长度(1字节)][盐][加密数据]
|
||||
var fullEncryptedData = Data()
|
||||
fullEncryptedData.append(saltLen)
|
||||
fullEncryptedData.append(salt)
|
||||
fullEncryptedData.append(encryptedData)
|
||||
|
||||
// 返回加密后的数据(16进制表示)
|
||||
return EN_STR_TAG + fullEncryptedData.hexEncodedString()
|
||||
}
|
||||
|
||||
// 使用盐加密数据
|
||||
static func encryptWithSalt(_ data: Data, salt: Data) -> Data? {
|
||||
var result = Data(capacity: data.count)
|
||||
let saltLen = salt.count
|
||||
|
||||
for (i, byte) in data.enumerated() {
|
||||
let saltByte = salt[i % saltLen]
|
||||
result.append(calSalt(v: byte, s: saltByte))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 解密数据
|
||||
static func decrypt(_ data: String) -> String? {
|
||||
guard !data.isEmpty else {
|
||||
return data
|
||||
}
|
||||
|
||||
// 检查是否是加密字符串 如果不是加密串,返回原始字符串
|
||||
guard data.hasPrefix(EN_STR_TAG) else {
|
||||
print("Invalid encoded string.")
|
||||
return data
|
||||
}
|
||||
|
||||
// 提取16进制数据并解码
|
||||
let hexString = String(data.dropFirst(EN_STR_TAG.count))
|
||||
|
||||
guard let encodedData = NSData(hexString: hexString) as? Data else {
|
||||
// guard let encodedData = Data(hexString: hexString) else {
|
||||
print("Invalid hex string.")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard encodedData.count > 0 else {
|
||||
print("Empty encoded data.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解析盐长度和盐
|
||||
let saltLen = Int(encodedData[0])
|
||||
guard encodedData.count > saltLen + 1 else {
|
||||
print("Invalid encoded data format.")
|
||||
return nil
|
||||
}
|
||||
|
||||
let salt = encodedData.subdata(in: 1..<(1+saltLen))
|
||||
let encryptedData = encodedData.subdata(in: (1+saltLen)..<encodedData.count)
|
||||
|
||||
// 解密
|
||||
guard let decryptedData = decryptWithSalt(encryptedData, salt: salt) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 将解密数据转换为字符串
|
||||
return String(data: decryptedData, encoding: .utf8)
|
||||
}
|
||||
|
||||
// 使用盐解密数据
|
||||
static func decryptWithSalt(_ data: Data, salt: Data) -> Data? {
|
||||
var result = Data(capacity: data.count)
|
||||
let saltLen = salt.count
|
||||
for (i, byte) in data.enumerated() {
|
||||
let saltByte = salt[i % saltLen]
|
||||
result.append(calRemoveSalt(v: byte, s: saltByte))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 计算加盐值
|
||||
private static func calSalt(v: UInt8, s: UInt8) -> UInt8 {
|
||||
let r = 255 - v
|
||||
if s > r {
|
||||
return s - r - 1
|
||||
}
|
||||
return v + s
|
||||
}
|
||||
|
||||
// 计算去盐值
|
||||
private static func calRemoveSalt(v: UInt8, s: UInt8) -> UInt8 {
|
||||
if v >= s {
|
||||
return v - s
|
||||
}
|
||||
return 255 - (s - v) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Data 扩展,用于16进制编码/解码
|
||||
extension Data {
|
||||
// 16进制字符串转换为Data
|
||||
// init?(hexString: String) {
|
||||
// let len = hexString.count / 2
|
||||
// var data = Data(capacity: len)
|
||||
//
|
||||
// var index = hexString.startIndex
|
||||
// for _ in 0..<len {
|
||||
// let nextIndex = hexString.index(index, offsetBy: 2)
|
||||
// let byteString = hexString[index..<nextIndex]
|
||||
//
|
||||
// guard let num = UInt8(byteString, radix: 16) else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// data.append(num)
|
||||
// index = nextIndex
|
||||
// }
|
||||
//
|
||||
// self = data
|
||||
// }
|
||||
|
||||
// Data转换为16进制字符串
|
||||
func hexEncodedString() -> String {
|
||||
return map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
|
||||
}
|
@ -96,57 +96,65 @@ class SPNetwork: NSObject {
|
||||
}
|
||||
|
||||
do {
|
||||
let tempData: [String : Any] = try response.mapJSON() as! [String : Any]
|
||||
let tempData = try response.mapString()
|
||||
spLog(message: parameters.parameters)
|
||||
spLog(message: parameters.path)
|
||||
spLog(message: tempData as NSDictionary)
|
||||
var response = SPNetworkResponse<T>.deserialize(from: tempData)
|
||||
|
||||
if response != nil {
|
||||
|
||||
DispatchQueue.global().async {
|
||||
let response: SPNetworkResponse<T> = _deserialize(data: tempData)
|
||||
|
||||
if response?.code == SPNetworkCodeSucceed {
|
||||
|
||||
} else {
|
||||
if parameters.isToast {
|
||||
SPToast.show(text: response?.msg)
|
||||
DispatchQueue.main.async {
|
||||
if response.code != SPNetworkCodeSucceed {
|
||||
if parameters.isToast {
|
||||
SPToast.show(text: response.msg)
|
||||
}
|
||||
}
|
||||
completion?(response)
|
||||
}
|
||||
|
||||
response?.rawData = tempData
|
||||
completion?(response!)
|
||||
|
||||
} else {
|
||||
response = SPNetworkResponse<T>()
|
||||
response?.code = -1
|
||||
if parameters.isToast {
|
||||
SPToast.show(text: "Error".localized)
|
||||
// ETHUD.showToast(text: "系统错误".localized)
|
||||
}
|
||||
completion?(response!)
|
||||
}
|
||||
|
||||
} catch {
|
||||
var response = SPNetworkResponse<T>()
|
||||
response.code = -1
|
||||
var res = SPNetworkResponse<T>()
|
||||
res.code = -1
|
||||
if parameters.isToast {
|
||||
SPToast.show(text: "Error".localized)
|
||||
// ETHUD.showToast(text: "系统错误".localized)
|
||||
}
|
||||
completion?(response)
|
||||
completion?(res)
|
||||
}
|
||||
case .failure(let error):
|
||||
spLog(message: error)
|
||||
var response = SPNetworkResponse<T>()
|
||||
response.code = -1
|
||||
var res = SPNetworkResponse<T>()
|
||||
res.code = -1
|
||||
if parameters.isToast {
|
||||
SPToast.show(text: "Error".localized)
|
||||
// ETHUD.showToast(text: "网络异常".localized)
|
||||
}
|
||||
completion?(response)
|
||||
completion?(res)
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
///解析数据
|
||||
static private func _deserialize<T>(data: String) -> SPNetworkResponse<T> {
|
||||
var response: SPNetworkResponse<T>?
|
||||
|
||||
let time = Date().timeIntervalSince1970
|
||||
if let decrypted = SPCryptService.decrypt(data) {
|
||||
spLog(message: decrypted)
|
||||
response = SPNetworkResponse<T>.deserialize(from: decrypted)
|
||||
response?.rawData = decrypted
|
||||
}
|
||||
spLog(message: Date().timeIntervalSince1970 - time)
|
||||
|
||||
|
||||
if let response = response {
|
||||
return response
|
||||
} else {
|
||||
var response = SPNetworkResponse<T>()
|
||||
response.code = -1
|
||||
response.msg = "Error".localized
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ class SPForYouViewController: SPPlayerListViewController {
|
||||
|
||||
requestDataArr(page: 1)
|
||||
|
||||
self.delegate = self
|
||||
self.dataSource = self
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
@ -24,6 +26,38 @@ class SPForYouViewController: SPPlayerListViewController {
|
||||
|
||||
}
|
||||
|
||||
//MARK: -------------- SPPlayerListViewControllerDelegate --------------
|
||||
extension SPForYouViewController: SPPlayerListViewControllerDelegate {
|
||||
func sp_playerViewControllerLoadMoreData(playerViewController: SPPlayerListViewController) {
|
||||
guard let pagination = self.pagination else { return }
|
||||
guard let page = self.pagination?.current_page else { return }
|
||||
let pageSize = pagination.page_size ?? 0
|
||||
if pagination.page_total ?? 0 <= pageSize * page {
|
||||
return
|
||||
}
|
||||
self.requestDataArr(page: page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: -------------- SPPlayerListViewControllerDataSource --------------
|
||||
extension SPForYouViewController: SPPlayerListViewControllerDataSource {
|
||||
|
||||
func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell {
|
||||
|
||||
if let cell = oldCell as? SPPlayerListCell {
|
||||
if let model = dataArr[indexPath.row] as? SPShortModel {
|
||||
cell.model = model
|
||||
cell.videoInfo = model.video_info
|
||||
}
|
||||
}
|
||||
return oldCell
|
||||
}
|
||||
|
||||
func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int {
|
||||
return oldNumber
|
||||
}
|
||||
}
|
||||
|
||||
extension SPForYouViewController {
|
||||
|
||||
private func requestDataArr(page: Int) {
|
||||
|
@ -19,7 +19,7 @@ class SPHomePageController: SPViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
sp_setupUI()
|
||||
}
|
||||
|
||||
|
||||
@ -30,6 +30,18 @@ class SPHomePageController: SPViewController {
|
||||
|
||||
}
|
||||
|
||||
extension SPHomePageController {
|
||||
|
||||
private func sp_setupUI() {
|
||||
addChild(pageView)
|
||||
view.addSubview(pageView.view)
|
||||
|
||||
pageView.view.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: -------------- JYPageControllerDelegate & JYPageControllerDataSource --------------
|
||||
extension SPHomePageController: JYPageControllerDelegate, JYPageControllerDataSource {
|
||||
func pageController(_ pageController: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect {
|
||||
|
21
ShortPlay/Class/Mine/Controller/SPMineViewController.swift
Normal file
21
ShortPlay/Class/Mine/Controller/SPMineViewController.swift
Normal file
@ -0,0 +1,21 @@
|
||||
//
|
||||
// SPMineViewController.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/10.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPMineViewController: SPViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -7,6 +7,32 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc protocol SPPlayerListViewControllerDelegate {
|
||||
|
||||
///加载新数据
|
||||
@objc optional func sp_playerViewControllerLoadNewDataV2(playerViewController: SPPlayerListViewController)
|
||||
|
||||
///将要加载更多数据
|
||||
@objc optional func sp_playerViewControllerShouldLoadMoreData(playerViewController: SPPlayerListViewController) -> Bool
|
||||
///加载更多数据
|
||||
@objc optional func sp_playerViewControllerLoadMoreData(playerViewController: SPPlayerListViewController)
|
||||
///向上加载更多数据
|
||||
@objc optional func sp_playerViewControllerLoadUpMoreData(playerViewController: SPPlayerListViewController)
|
||||
|
||||
///新页面展示完成
|
||||
// @objc optional func yd_playerViewController(playerListViewController: BCListPlayerViewController, didShowPlayerPage playerViewController: YDBasePlayerViewController)
|
||||
}
|
||||
|
||||
@objc protocol SPPlayerListViewControllerDataSource {
|
||||
|
||||
|
||||
func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell
|
||||
|
||||
func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int
|
||||
|
||||
|
||||
}
|
||||
|
||||
class SPPlayerListViewController: SPViewController {
|
||||
|
||||
|
||||
@ -14,11 +40,20 @@ class SPPlayerListViewController: SPViewController {
|
||||
return CGSize(width: kSPScreenWidth, height: kSPScreenHeight - kSPTabBarHeight)
|
||||
}
|
||||
|
||||
var PlayerCellClass: SPPlayerListCell.Type {
|
||||
return SPPlayerListCell.self
|
||||
}
|
||||
|
||||
private var dataArr: [Any] = []
|
||||
weak var delegate: SPPlayerListViewControllerDelegate?
|
||||
weak var dataSource: SPPlayerListViewControllerDataSource?
|
||||
|
||||
private(set) var dataArr: [Any] = []
|
||||
var pagination: SPListPaginationModel?
|
||||
|
||||
private var viewModel = SPPlayerListViewModel()
|
||||
///自动下一级
|
||||
var autoNextEpisode = false
|
||||
|
||||
private(set) var viewModel = SPPlayerListViewModel()
|
||||
|
||||
private(set) var currentIndexPath = IndexPath(row: 0, section: 0)
|
||||
|
||||
@ -30,7 +65,7 @@ class SPPlayerListViewController: SPViewController {
|
||||
return layout
|
||||
}()
|
||||
|
||||
private lazy var collectionView: SPCollectionView = {
|
||||
private(set) lazy var collectionView: SPCollectionView = {
|
||||
let collectionView = SPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
@ -39,10 +74,14 @@ class SPPlayerListViewController: SPViewController {
|
||||
collectionView.showsHorizontalScrollIndicator = false
|
||||
collectionView.bounces = false
|
||||
collectionView.scrollsToTop = false
|
||||
SPPlayerListCell.registerCell(collectionView: collectionView)
|
||||
PlayerCellClass.registerCell(collectionView: collectionView)
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
@ -55,6 +94,7 @@ class SPPlayerListViewController: SPViewController {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil)
|
||||
|
||||
sp_setupUI()
|
||||
sp_addActio()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
@ -63,7 +103,7 @@ class SPPlayerListViewController: SPViewController {
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if !self.dataArr.isEmpty && self.viewModel.isPlaying {
|
||||
if getDataCount() > 0 && self.viewModel.isPlaying {
|
||||
self.viewModel.currentPlayer?.start()
|
||||
}
|
||||
}
|
||||
@ -77,9 +117,7 @@ class SPPlayerListViewController: SPViewController {
|
||||
func setDataArr(dataArr: [Any]) {
|
||||
self.dataArr = dataArr
|
||||
|
||||
CATransaction.begin()
|
||||
self.collectionView.reloadData()
|
||||
CATransaction.commit()
|
||||
reloadData()
|
||||
}
|
||||
|
||||
|
||||
@ -97,14 +135,24 @@ class SPPlayerListViewController: SPViewController {
|
||||
|
||||
self.viewModel.isPlaying = true
|
||||
|
||||
if self.dataArr.count - currentIndexPath.row <= 2 {
|
||||
// self.loadMoreData()
|
||||
if getDataCount() - currentIndexPath.row <= 2 {
|
||||
self.loadMoreData()
|
||||
}
|
||||
|
||||
if currentIndexPath.row <= 2 {
|
||||
// self.loadUpMoreData()
|
||||
}
|
||||
}
|
||||
|
||||
func reloadData() {
|
||||
CATransaction.begin()
|
||||
self.collectionView.reloadData()
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
func getDataCount() -> Int {
|
||||
return self.collectionView(self.collectionView, numberOfItemsInSection: 0)
|
||||
}
|
||||
}
|
||||
|
||||
extension SPPlayerListViewController {
|
||||
@ -116,28 +164,115 @@ extension SPPlayerListViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func sp_addActio() {
|
||||
self.viewModel.handlePauseOrPlay = { [weak self] in
|
||||
self?.clickPauseOrPlay()
|
||||
}
|
||||
|
||||
self.viewModel.handlePlayFinish = { [weak self] in
|
||||
self?.currentPlayFinish()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SPPlayerListViewController {
|
||||
///点击播放或暂停
|
||||
private func clickPauseOrPlay() {
|
||||
// let model = self.dataArr[currentIndexPath.section]
|
||||
///打开支付页面
|
||||
// if self.onVideoPay() {
|
||||
// return
|
||||
// }
|
||||
|
||||
if self.viewModel.isPlaying {
|
||||
self.viewModel.isPlaying = false
|
||||
self.viewModel.currentPlayer?.pause()
|
||||
} else {
|
||||
self.viewModel.isPlaying = true
|
||||
self.viewModel.currentPlayer?.start()
|
||||
}
|
||||
}
|
||||
|
||||
///当前播放完成
|
||||
private func currentPlayFinish() {
|
||||
guard self.autoNextEpisode else { return }
|
||||
|
||||
scrollToNextEpisode()
|
||||
}
|
||||
|
||||
///滑动至下一级
|
||||
private func scrollToNextEpisode() {
|
||||
|
||||
var contentOffset = self.collectionView.contentOffset
|
||||
|
||||
if hasNextEpisode() {
|
||||
contentOffset.y = contentOffset.y + self.contentSize.height
|
||||
self.collectionView.setContentOffset(contentOffset, animated: true)
|
||||
} else {
|
||||
self.viewModel.currentPlayer?.replay()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///是否还有下一级
|
||||
private func hasNextEpisode() -> Bool {
|
||||
let contentOffset = self.collectionView.contentOffset
|
||||
let contentSize = self.collectionView.contentSize
|
||||
if contentOffset.y >= contentSize.height - self.contentSize.height {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: -------------- UICollectionViewDelegate & UICollectionViewDataSource --------------
|
||||
extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = SPPlayerListCell.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath)
|
||||
cell.model = dataArr[indexPath.row]
|
||||
var cell: UICollectionViewCell = PlayerCellClass.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath)
|
||||
|
||||
if self.viewModel.currentPlayer == nil, indexPath == currentIndexPath {
|
||||
self.currentIndexPath = indexPath
|
||||
self.viewModel.currentPlayer = cell
|
||||
if let newCell = self.dataSource?.sp_playerListViewController(self, collectionView, cellForItemAt: indexPath, oldCell: cell) {
|
||||
cell = newCell
|
||||
}
|
||||
|
||||
if let cell = cell as? SPPlayerListCell {
|
||||
if cell.viewModel == nil {
|
||||
cell.viewModel = viewModel
|
||||
}
|
||||
// let model = dataArr[indexPath.row]
|
||||
// cell.model = model
|
||||
|
||||
}
|
||||
|
||||
if self.viewModel.currentPlayer == nil, indexPath == currentIndexPath, let playerProtocol = cell as? SPPlayerProtocol {
|
||||
self.currentIndexPath = indexPath
|
||||
self.viewModel.currentPlayer = playerProtocol
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return dataArr.count
|
||||
let count = dataArr.count
|
||||
|
||||
if let newCount = self.dataSource?.sp_playerListViewController(self, collectionView, numberOfItemsInSection: section, oldNumber: count) {
|
||||
return newCount
|
||||
} else {
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
//滑动停止
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
scrollDidEnd(scrollView)
|
||||
}
|
||||
|
||||
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||
scrollDidEnd(scrollView)
|
||||
}
|
||||
|
||||
private func scrollDidEnd(_ scrollView: UIScrollView) {
|
||||
let offsetY = scrollView.contentOffset.y
|
||||
let indexPaths = self.collectionView.indexPathsForVisibleItems
|
||||
for indexPath in indexPaths {
|
||||
@ -164,7 +299,7 @@ extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionView
|
||||
extension SPPlayerListViewController {
|
||||
|
||||
@objc func didBecomeActiveNotification() {
|
||||
if !self.dataArr.isEmpty && self.viewModel.isPlaying && isDidAppear {
|
||||
if getDataCount() > 0 && self.viewModel.isPlaying && isDidAppear {
|
||||
self.viewModel.currentPlayer?.start()
|
||||
}
|
||||
}
|
||||
@ -175,3 +310,20 @@ extension SPPlayerListViewController {
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension SPPlayerListViewController {
|
||||
|
||||
private func loadMoreData() {
|
||||
let isLoad = self.delegate?.sp_playerViewControllerShouldLoadMoreData?(playerViewController: self)
|
||||
if isLoad != false {
|
||||
self.delegate?.sp_playerViewControllerLoadMoreData?(playerViewController: self)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadUpMoreData() {
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||
// guard let self = self else { return }
|
||||
// }
|
||||
self.delegate?.sp_playerViewControllerLoadUpMoreData?(playerViewController: self)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,83 @@
|
||||
//
|
||||
// SPTVPlayerListViewController.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/10.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPTVPlayerListViewController: SPPlayerListViewController {
|
||||
|
||||
override var PlayerCellClass: SPPlayerListCell.Type {
|
||||
return SPTVPlayerListCell.self
|
||||
}
|
||||
|
||||
override var contentSize: CGSize {
|
||||
return CGSize(width: kSPScreenWidth, height: kSPScreenHeight)
|
||||
}
|
||||
|
||||
|
||||
var videoId: String?
|
||||
var shortPlayId: String?
|
||||
|
||||
private var detailModel: SPVideoDetailModel?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.autoNextEpisode = true
|
||||
self.dataSource = self
|
||||
|
||||
requestDetailData()
|
||||
}
|
||||
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.navigationController?.setNavigationBarHidden(true, animated: true)
|
||||
|
||||
}
|
||||
|
||||
override func play() {
|
||||
super.play()
|
||||
if let _ = self.viewModel.currentPlayer?.model as? SPVideoDetailModel,
|
||||
let videoInfo = self.viewModel.currentPlayer?.videoInfo
|
||||
{
|
||||
SPVideoAPI.requestRequestVideoPlayHistory(videoId: videoInfo.short_play_video_id ?? "", shortPlayId: videoInfo.short_play_id ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: -------------- SPPlayerListViewControllerDataSource --------------
|
||||
extension SPTVPlayerListViewController: SPPlayerListViewControllerDataSource {
|
||||
func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell {
|
||||
if let cell = oldCell as? SPPlayerListCell {
|
||||
cell.model = detailModel
|
||||
cell.videoInfo = detailModel?.episodeList?[indexPath.row]
|
||||
cell.isLoop = false
|
||||
}
|
||||
return oldCell
|
||||
}
|
||||
|
||||
func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int {
|
||||
return detailModel?.episodeList?.count ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension SPTVPlayerListViewController {
|
||||
|
||||
private func requestDetailData() {
|
||||
guard let videoId = self.videoId, let shortPlayId = self.shortPlayId else { return }
|
||||
|
||||
SPVideoAPI.requestVideoDetail(videoId: videoId, shortPlayId: shortPlayId) { [weak self] model in
|
||||
guard let self = self else { return }
|
||||
if let model = model {
|
||||
self.detailModel = model
|
||||
self.reloadData()
|
||||
self.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,9 +13,12 @@ protocol SPPlayerProtocol: NSObjectProtocol {
|
||||
var playerFinishHadle: (() -> Void)? { get set }
|
||||
|
||||
var model: Any? { get set }
|
||||
var videoInfo: SPVideoInfoModel? { get set }
|
||||
|
||||
var isCurrent: Bool { get set }
|
||||
|
||||
var rate: Float { get set }
|
||||
|
||||
///播放准备
|
||||
func prepare()
|
||||
|
||||
@ -24,5 +27,8 @@ protocol SPPlayerProtocol: NSObjectProtocol {
|
||||
|
||||
///暂停播放
|
||||
func pause()
|
||||
|
||||
///从头播放
|
||||
func replay()
|
||||
|
||||
}
|
||||
|
41
ShortPlay/Class/Player/Model/SPSpeedModel.swift
Normal file
41
ShortPlay/Class/Player/Model/SPSpeedModel.swift
Normal file
@ -0,0 +1,41 @@
|
||||
//
|
||||
// SPSpeedModel.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/10.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct SPSpeedModel {
|
||||
|
||||
enum Speed: String {
|
||||
case x0_75 = "0.75x"
|
||||
case x1 = "1.0x"
|
||||
case x1_25 = "1.25x"
|
||||
case x1_5 = "1.5x"
|
||||
case x2 = "2.0x"
|
||||
|
||||
func getRate() -> Float {
|
||||
switch self {
|
||||
case .x0_75:
|
||||
return 0.75
|
||||
|
||||
case .x1:
|
||||
return 1
|
||||
|
||||
case .x1_25:
|
||||
return 1.25
|
||||
|
||||
case .x1_5:
|
||||
return 1.5
|
||||
|
||||
case .x2:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var speed: Speed = .x1
|
||||
|
||||
}
|
24
ShortPlay/Class/Player/Model/SPVideoDetailModel.swift
Normal file
24
ShortPlay/Class/Player/Model/SPVideoDetailModel.swift
Normal file
@ -0,0 +1,24 @@
|
||||
//
|
||||
// SPVideoDetailModel.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/10.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SmartCodable
|
||||
|
||||
class SPVideoDetailModel: SPModel, SmartCodable {
|
||||
|
||||
var business_model: String?
|
||||
var video_info: SPVideoInfoModel?
|
||||
var shortPlayInfo: SPShortModel?
|
||||
var episodeList: [SPVideoInfoModel]?
|
||||
var is_collect: Bool?
|
||||
var show_share_coin: Int?
|
||||
var share_coin: Int?
|
||||
var install_coins: Int?
|
||||
var revolution: Int?
|
||||
var unlock_video_ad_count: Int?
|
||||
var discount: Int?
|
||||
}
|
@ -9,6 +9,21 @@ import UIKit
|
||||
|
||||
class SPPlayerControlView: UIView {
|
||||
|
||||
weak var viewModel: SPPlayerListViewModel? {
|
||||
didSet {
|
||||
viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil)
|
||||
}
|
||||
}
|
||||
|
||||
var model: Any? {
|
||||
didSet {
|
||||
guard let model = model as? SPShortModel else { return }
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
///滑动进度条
|
||||
var panProgressFinishBlock: ((_ progress: CGFloat) -> Void)?
|
||||
|
||||
@ -18,6 +33,12 @@ class SPPlayerControlView: UIView {
|
||||
progressView.progress = progress
|
||||
}
|
||||
}
|
||||
|
||||
var isCurrent: Bool = false {
|
||||
didSet {
|
||||
updatePlayIconState()
|
||||
}
|
||||
}
|
||||
|
||||
private(set) lazy var progressView: SPPlayerProgressView = {
|
||||
let view = SPPlayerProgressView()
|
||||
@ -37,12 +58,33 @@ class SPPlayerControlView: UIView {
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var playImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.backgroundColor = .red
|
||||
imageView.isHidden = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
viewModel?.removeObserver(self, forKeyPath: "isPlaying")
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(hadlePlayAndOrPaused))
|
||||
self.addGestureRecognizer(tap)
|
||||
|
||||
|
||||
sp_setupUI()
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if keyPath == "isPlaying" {
|
||||
updatePlayIconState()
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
@ -53,6 +95,7 @@ extension SPPlayerControlView {
|
||||
|
||||
private func sp_setupUI() {
|
||||
addSubview(progressView)
|
||||
addSubview(playImageView)
|
||||
|
||||
progressView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(10)
|
||||
@ -60,7 +103,35 @@ extension SPPlayerControlView {
|
||||
make.bottom.equalToSuperview().offset(-20)
|
||||
make.height.equalTo(30)
|
||||
}
|
||||
|
||||
playImageView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.width.height.equalTo(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SPPlayerControlView {
|
||||
|
||||
private func updatePlayIconState() {
|
||||
let isPlaying = self.viewModel?.isPlaying ?? false
|
||||
if isCurrent {
|
||||
playImageView.isHidden = isPlaying
|
||||
} else {
|
||||
playImageView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func hadlePlayAndOrPaused() {
|
||||
// self.viewModel?.handlePauseOrPlay?()
|
||||
|
||||
guard let model = model as? SPShortModel else { return }
|
||||
let vc = SPTVPlayerListViewController()
|
||||
vc.shortPlayId = model.short_play_id
|
||||
vc.videoId = model.video_info?.short_play_video_id
|
||||
SPAPPTool.topViewController()?.navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,8 +9,18 @@ import UIKit
|
||||
|
||||
class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
|
||||
|
||||
weak var viewModel: SPPlayerListViewModel? {
|
||||
didSet {
|
||||
controlView.viewModel = viewModel
|
||||
}
|
||||
}
|
||||
|
||||
var isLoop = true {
|
||||
didSet {
|
||||
player.isLoop = isLoop
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private lazy var player: SPPlayer = {
|
||||
let player = SPPlayer()
|
||||
player.playerView = playerView
|
||||
@ -48,16 +58,44 @@ class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
||||
//MARK: SPPlayerProtocol
|
||||
var model: Any? {
|
||||
didSet {
|
||||
guard let model = model as? SPShortModel else { return }
|
||||
player.setPlayUrl(url: model.video_info?.video_url ?? "")
|
||||
coverImageView.sp_setImage(url: model.image_url)
|
||||
self.controlView.progress = 0
|
||||
self.coverImageView.isHidden = false
|
||||
|
||||
if let model = model as? SPShortModel {
|
||||
self.controlView.model = model
|
||||
coverImageView.sp_setImage(url: model.image_url)
|
||||
} else if let model = model as? SPVideoDetailModel {
|
||||
self.controlView.model = model.shortPlayInfo
|
||||
coverImageView.sp_setImage(url: model.shortPlayInfo?.image_url)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var isCurrent: Bool = false
|
||||
var videoInfo: SPVideoInfoModel? {
|
||||
didSet {
|
||||
player.setPlayUrl(url: videoInfo?.video_url ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
var isCurrent: Bool = false {
|
||||
didSet {
|
||||
controlView.isCurrent = isCurrent
|
||||
}
|
||||
}
|
||||
|
||||
var rate: Float {
|
||||
set {
|
||||
return player.rate = newValue
|
||||
}
|
||||
get {
|
||||
return player.rate
|
||||
}
|
||||
}
|
||||
|
||||
///播放完成
|
||||
var playerFinishHadle: (() -> Void)?
|
||||
@ -74,13 +112,17 @@ class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
|
||||
player.pause()
|
||||
}
|
||||
|
||||
func replay() {
|
||||
player.replay()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SPPlayerListCell {
|
||||
|
||||
private func sp_setupUI() {
|
||||
contentView.addSubview(coverImageView)
|
||||
contentView.addSubview(playerView)
|
||||
contentView.addSubview(coverImageView)
|
||||
contentView.addSubview(controlView)
|
||||
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
@ -103,7 +145,7 @@ extension SPPlayerListCell {
|
||||
extension SPPlayerListCell: SPPlayerDelegate {
|
||||
|
||||
func sp_playCompletion(_ player: SPPlayer) {
|
||||
|
||||
self.playerFinishHadle?()
|
||||
}
|
||||
|
||||
func sp_playLoadingEnd(_ player: SPPlayer) {
|
||||
@ -118,4 +160,10 @@ extension SPPlayerListCell: SPPlayerDelegate {
|
||||
|
||||
}
|
||||
|
||||
func sp_player(_ player: SPPlayer, playStateDidChanged state: SPPlayer.PlayState) {
|
||||
if state == .playing {
|
||||
self.coverImageView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
12
ShortPlay/Class/Player/View/SPTVPlayerListCell.swift
Normal file
12
ShortPlay/Class/Player/View/SPTVPlayerListCell.swift
Normal file
@ -0,0 +1,12 @@
|
||||
//
|
||||
// SPTVPlayerListCell.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/10.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPTVPlayerListCell: SPPlayerListCell {
|
||||
|
||||
}
|
@ -9,7 +9,7 @@ import UIKit
|
||||
|
||||
class SPPlayerListViewModel: NSObject {
|
||||
|
||||
var isPlaying = false
|
||||
@objc dynamic var isPlaying: Bool = true
|
||||
|
||||
private var _currentPlayer: SPPlayerProtocol?
|
||||
var currentPlayer: SPPlayerProtocol? {
|
||||
@ -18,6 +18,9 @@ class SPPlayerListViewModel: NSObject {
|
||||
_currentPlayer?.pause()
|
||||
|
||||
_currentPlayer = newValue
|
||||
_currentPlayer?.playerFinishHadle = { [weak self] in
|
||||
self?.handlePlayFinish?()
|
||||
}
|
||||
_currentPlayer?.isCurrent = true
|
||||
}
|
||||
get {
|
||||
@ -25,5 +28,19 @@ class SPPlayerListViewModel: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var speed: SPSpeedModel.Speed = .x1
|
||||
|
||||
///设置倍速播放
|
||||
func setSpeedPlay(speed: SPSpeedModel.Speed) {
|
||||
self.speed = speed
|
||||
currentPlayer?.rate = speed.getRate()
|
||||
}
|
||||
|
||||
|
||||
///点暂停或播放
|
||||
var handlePauseOrPlay: (() -> Void)?
|
||||
///播放完成
|
||||
var handlePlayFinish: (() -> Void)?
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,9 +9,16 @@ import UIKit
|
||||
|
||||
class SPToast: NSObject {
|
||||
|
||||
static func config() {
|
||||
CSToastManager.setTapToDismissEnabled(false)
|
||||
CSToastManager.setDefaultDuration(2)
|
||||
CSToastManager.setDefaultPosition(CSToastPositionCenter)
|
||||
}
|
||||
|
||||
static func show(text: String?) {
|
||||
guard let text = text else { return }
|
||||
SPAPPTool.getKeyWindow()?.makeToast(text, duration: 2, position: nil)
|
||||
// SPAPPTool.getKeyWindow()?.makeToast(text, duration: 2, position: nil)
|
||||
SPAPPTool.getKeyWindow()?.makeToast(text)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,6 +15,9 @@ import ZFPlayer
|
||||
// ///更新当前进度
|
||||
// @objc optional func sp_onCurrentPositionUpdate(_ player: SPPlayer, position: Int)
|
||||
|
||||
///播放状态变化
|
||||
@objc optional func sp_player(_ player: SPPlayer, playStateDidChanged state: SPPlayer.PlayState)
|
||||
|
||||
///播放时间发生变化
|
||||
@objc optional func sp_playTimeChanged(_ player: SPPlayer, currentTime: Int, duration: Int)
|
||||
|
||||
@ -26,14 +29,27 @@ import ZFPlayer
|
||||
|
||||
///缓冲完成
|
||||
@objc optional func sp_playLoadingEnd(_ player: SPPlayer)
|
||||
|
||||
}
|
||||
|
||||
class SPPlayer: NSObject {
|
||||
|
||||
@objc enum PlayState: Int {
|
||||
case unknown
|
||||
case playing
|
||||
case paused
|
||||
case failed
|
||||
case stopped
|
||||
}
|
||||
|
||||
weak var delegate: SPPlayerDelegate?
|
||||
|
||||
private(set) lazy var isPlaying = false
|
||||
private(set) lazy var playState: PlayState = .unknown
|
||||
|
||||
/**
|
||||
是否添加息屏监控
|
||||
*/
|
||||
private var isAddIdleTimerDisabledObserver = false
|
||||
|
||||
///总进度
|
||||
var duration: Int {
|
||||
@ -44,6 +60,16 @@ class SPPlayer: NSObject {
|
||||
return Int(self.player.currentTime)
|
||||
}
|
||||
|
||||
///0.5 - 2
|
||||
var rate: Float {
|
||||
set {
|
||||
player.rate = newValue
|
||||
}
|
||||
get {
|
||||
return player.rate
|
||||
}
|
||||
}
|
||||
|
||||
var playerView: UIView? {
|
||||
didSet {
|
||||
playerView?.addSubview(player.view)
|
||||
@ -55,50 +81,99 @@ class SPPlayer: NSObject {
|
||||
|
||||
private lazy var player: ZFAVPlayerManager = {
|
||||
let player = ZFAVPlayerManager()
|
||||
player.shouldAutoPlay = false
|
||||
return player
|
||||
}()
|
||||
|
||||
var isLoop = true
|
||||
|
||||
deinit {
|
||||
self.stop()
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
player.scalingMode = .aspectFill
|
||||
sp_addAction()
|
||||
}
|
||||
|
||||
/**
|
||||
添加息屏监控
|
||||
*/
|
||||
private func addIdleTimerDisabledObserver() {
|
||||
if !isAddIdleTimerDisabledObserver {
|
||||
isAddIdleTimerDisabledObserver = true
|
||||
UIApplication.shared.addObserver(self, forKeyPath: "idleTimerDisabled", options: NSKeyValueObservingOptions.new, context: nil)
|
||||
}
|
||||
}
|
||||
/**
|
||||
删除息屏监控
|
||||
*/
|
||||
private func removeIdleTimerDisabledObserver() {
|
||||
if isAddIdleTimerDisabledObserver {
|
||||
isAddIdleTimerDisabledObserver = false
|
||||
UIApplication.shared.removeObserver(self, forKeyPath: "idleTimerDisabled")
|
||||
}
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if !UIApplication.shared.isIdleTimerDisabled {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
}
|
||||
}
|
||||
|
||||
func setPlayUrl(url: String) {
|
||||
let proxyURL = KTVHTTPCache.proxyURL(withOriginalURL: URL(string: url))
|
||||
self.player.assetURL = proxyURL
|
||||
|
||||
// self.player.assetURL = URL(string: url)
|
||||
self.prepare()
|
||||
// self.prepare()
|
||||
}
|
||||
|
||||
///准备播放
|
||||
func prepare() {
|
||||
self.player.prepareToPlay()
|
||||
// self.player.prepareToPlay()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
self.isPlaying = false
|
||||
player.stop()
|
||||
|
||||
self.removeIdleTimerDisabledObserver()
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
|
||||
func start() {
|
||||
self.isPlaying = true
|
||||
player.play()
|
||||
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
self.addIdleTimerDisabledObserver()
|
||||
|
||||
}
|
||||
|
||||
///暂停
|
||||
func pause() {
|
||||
self.isPlaying = false
|
||||
player.pause()
|
||||
|
||||
self.removeIdleTimerDisabledObserver()
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
|
||||
///从头播放
|
||||
func replay() {
|
||||
self.isPlaying = true
|
||||
self.player.replay()
|
||||
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
self.addIdleTimerDisabledObserver()
|
||||
}
|
||||
|
||||
func seekToTime(toTime: Int) {
|
||||
// self.player.seek(toTime: Int64(toTime), seekMode: AVP_SEEKMODE_ACCURATE)
|
||||
self.player.seek(toTime: TimeInterval(toTime), completionHandler: nil)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension SPPlayer {
|
||||
@ -115,7 +190,25 @@ extension SPPlayer {
|
||||
guard let self = self else { return }
|
||||
if playState == .playStatePlaying, !isPlaying {
|
||||
self.pause()
|
||||
} else if playState == .playStatePaused, isPlaying {
|
||||
self.start()
|
||||
}
|
||||
switch playState {
|
||||
case .playStateUnknown:
|
||||
self.playState = .unknown
|
||||
case .playStatePlaying:
|
||||
self.playState = .playing
|
||||
case .playStatePaused:
|
||||
self.playState = .paused
|
||||
case .playStatePlayStopped:
|
||||
self.playState = .stopped
|
||||
case .playStatePlayFailed:
|
||||
self.playState = .failed
|
||||
|
||||
default:
|
||||
self.playState = .unknown
|
||||
}
|
||||
self.delegate?.sp_player?(self, playStateDidChanged: self.playState)
|
||||
spLog(message: "播放状态====\(playState)")
|
||||
}
|
||||
|
||||
@ -124,8 +217,21 @@ extension SPPlayer {
|
||||
guard let self = self else { return }
|
||||
if loadState == .playable, !isPlaying {
|
||||
self.pause()
|
||||
} else if loadState == .playable, isPlaying, self.player.playState != .playStatePlaying {
|
||||
self.start()
|
||||
}
|
||||
switch loadState {
|
||||
case .prepare:
|
||||
spLog(message: "加载状态====准备完成")
|
||||
case .playable:
|
||||
spLog(message: "加载状态====可播放")
|
||||
case .playthroughOK:
|
||||
spLog(message: "加载状态====将自动播放")
|
||||
case .stalled:
|
||||
spLog(message: "加载状态====如果已启动,将自动暂停")
|
||||
default:
|
||||
break
|
||||
}
|
||||
spLog(message: "加载状态====\(loadState)")
|
||||
}
|
||||
|
||||
//错误信息
|
||||
@ -139,6 +245,7 @@ extension SPPlayer {
|
||||
if isLoop {
|
||||
self.player.replay()
|
||||
} else {
|
||||
self.isPlaying = false
|
||||
self.delegate?.sp_playCompletion?(self)
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,12 @@
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
|
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@2x.png
vendored
Normal file
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 957 B |
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@3x.png
vendored
Normal file
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
@ -5,10 +5,12 @@
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
|
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@2x.png
vendored
Normal file
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@3x.png
vendored
Normal file
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
@ -5,10 +5,12 @@
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
|
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@2x.png
vendored
Normal file
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@3x.png
vendored
Normal file
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
@ -9,3 +9,4 @@
|
||||
"Home" = "Home";
|
||||
"For You" = "For You";
|
||||
"Error" = "Error";
|
||||
"Profile" = "Profile";
|
||||
|
Loading…
x
Reference in New Issue
Block a user