视频详情播放,创建历史记录,数据加密

This commit is contained in:
曾觉新 2025-04-10 18:20:52 +08:00
parent 335ede5be7
commit 0f79f340d9
34 changed files with 939 additions and 67 deletions

View File

@ -13,6 +13,8 @@ extension AppDelegate {
// UIView.et_Awake() // UIView.et_Awake()
tabBarConfig() tabBarConfig()
// keyBoardStyle() // keyBoardStyle()
SPToast.config()
} }

View File

@ -18,6 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
self.appConfig() self.appConfig()
SPLoginManager.manager.requestVisitorLogin(completer: nil) SPLoginManager.manager.requestVisitorLogin(completer: nil)
return true return true
} }

View File

@ -11,7 +11,7 @@ class SPNavigationController: UINavigationController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
self.jx_transitionAwake()
// Do any additional setup after loading the view. // Do any additional setup after loading the view.
} }

View File

@ -12,11 +12,13 @@ class SPTabBarController: UITabBarController {
override func viewDidLoad() { override func viewDidLoad() {
super.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]
} }

View File

@ -27,6 +27,7 @@ class SPViewController: UIViewController, JYPageChildContollerProtocol {
super.viewDidLoad() super.viewDidLoad()
self.isViewDidLoad = true self.isViewDidLoad = true
self.edgesForExtendedLayout = [] self.edgesForExtendedLayout = []
self.view.backgroundColor = .black
if let navi = navigationController { if let navi = navigationController {
if navi.visibleViewController == self { if navi.visibleViewController == self {

View File

@ -24,4 +24,5 @@ class SPHomeAPI: NSObject {
} }
} }
} }

View 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
}
}
}

View File

@ -90,6 +90,7 @@ extension SPApi: TargetType {
"system-type" : "ios", "system-type" : "ios",
"idfa" : JXUUID.idfa(), "idfa" : JXUUID.idfa(),
"model" : UIDevice.sp_machineModelName(), "model" : UIDevice.sp_machineModelName(),
// "security" : "false",
] ]
// //
dic["authorization"] = userToken dic["authorization"] = userToken

View 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 {
// 16Data
// 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
// }
// Data16
func hexEncodedString() -> String {
return map { String(format: "%02hhx", $0) }.joined()
}
}

View File

@ -96,57 +96,65 @@ class SPNetwork: NSObject {
} }
do { do {
let tempData: [String : Any] = try response.mapJSON() as! [String : Any] let tempData = try response.mapString()
spLog(message: parameters.parameters) spLog(message: parameters.parameters)
spLog(message: parameters.path) spLog(message: parameters.path)
spLog(message: tempData as NSDictionary)
var response = SPNetworkResponse<T>.deserialize(from: tempData)
if response != nil {
if response?.code == SPNetworkCodeSucceed { DispatchQueue.global().async {
let response: SPNetworkResponse<T> = _deserialize(data: tempData)
} else { DispatchQueue.main.async {
if parameters.isToast { if response.code != SPNetworkCodeSucceed {
SPToast.show(text: response?.msg) 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 { } catch {
var response = SPNetworkResponse<T>() var res = SPNetworkResponse<T>()
response.code = -1 res.code = -1
if parameters.isToast { if parameters.isToast {
SPToast.show(text: "Error".localized) SPToast.show(text: "Error".localized)
// ETHUD.showToast(text: "".localized)
} }
completion?(response) completion?(res)
} }
case .failure(let error): case .failure(let error):
spLog(message: error) spLog(message: error)
var response = SPNetworkResponse<T>() var res = SPNetworkResponse<T>()
response.code = -1 res.code = -1
if parameters.isToast { if parameters.isToast {
SPToast.show(text: "Error".localized) SPToast.show(text: "Error".localized)
// ETHUD.showToast(text: "".localized)
} }
completion?(response) completion?(res)
break 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
}
} }
} }

View File

@ -14,6 +14,8 @@ class SPForYouViewController: SPPlayerListViewController {
requestDataArr(page: 1) requestDataArr(page: 1)
self.delegate = self
self.dataSource = self
} }
override func viewWillAppear(_ animated: Bool) { 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 { extension SPForYouViewController {
private func requestDataArr(page: Int) { private func requestDataArr(page: Int) {

View File

@ -19,7 +19,7 @@ class SPHomePageController: SPViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.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 -------------- //MARK: -------------- JYPageControllerDelegate & JYPageControllerDataSource --------------
extension SPHomePageController: JYPageControllerDelegate, JYPageControllerDataSource { extension SPHomePageController: JYPageControllerDelegate, JYPageControllerDataSource {
func pageController(_ pageController: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect { func pageController(_ pageController: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect {

View File

@ -0,0 +1,21 @@
//
// SPMineViewController.swift
// ShortPlay
//
// Created by on 2025/4/10.
//
import UIKit
class SPMineViewController: SPViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}

View File

@ -7,6 +7,32 @@
import UIKit 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 { class SPPlayerListViewController: SPViewController {
@ -14,11 +40,20 @@ class SPPlayerListViewController: SPViewController {
return CGSize(width: kSPScreenWidth, height: kSPScreenHeight - kSPTabBarHeight) 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? var pagination: SPListPaginationModel?
private var viewModel = SPPlayerListViewModel() ///
var autoNextEpisode = false
private(set) var viewModel = SPPlayerListViewModel()
private(set) var currentIndexPath = IndexPath(row: 0, section: 0) private(set) var currentIndexPath = IndexPath(row: 0, section: 0)
@ -30,7 +65,7 @@ class SPPlayerListViewController: SPViewController {
return layout return layout
}() }()
private lazy var collectionView: SPCollectionView = { private(set) lazy var collectionView: SPCollectionView = {
let collectionView = SPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) let collectionView = SPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self collectionView.delegate = self
collectionView.dataSource = self collectionView.dataSource = self
@ -39,10 +74,14 @@ class SPPlayerListViewController: SPViewController {
collectionView.showsHorizontalScrollIndicator = false collectionView.showsHorizontalScrollIndicator = false
collectionView.bounces = false collectionView.bounces = false
collectionView.scrollsToTop = false collectionView.scrollsToTop = false
SPPlayerListCell.registerCell(collectionView: collectionView) PlayerCellClass.registerCell(collectionView: collectionView)
return collectionView return collectionView
}() }()
deinit {
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -55,6 +94,7 @@ class SPPlayerListViewController: SPViewController {
NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil)
sp_setupUI() sp_setupUI()
sp_addActio()
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
@ -63,7 +103,7 @@ class SPPlayerListViewController: SPViewController {
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
if !self.dataArr.isEmpty && self.viewModel.isPlaying { if getDataCount() > 0 && self.viewModel.isPlaying {
self.viewModel.currentPlayer?.start() self.viewModel.currentPlayer?.start()
} }
} }
@ -77,9 +117,7 @@ class SPPlayerListViewController: SPViewController {
func setDataArr(dataArr: [Any]) { func setDataArr(dataArr: [Any]) {
self.dataArr = dataArr self.dataArr = dataArr
CATransaction.begin() reloadData()
self.collectionView.reloadData()
CATransaction.commit()
} }
@ -97,14 +135,24 @@ class SPPlayerListViewController: SPViewController {
self.viewModel.isPlaying = true self.viewModel.isPlaying = true
if self.dataArr.count - currentIndexPath.row <= 2 { if getDataCount() - currentIndexPath.row <= 2 {
// self.loadMoreData() self.loadMoreData()
} }
if currentIndexPath.row <= 2 { if currentIndexPath.row <= 2 {
// self.loadUpMoreData() // self.loadUpMoreData()
} }
} }
func reloadData() {
CATransaction.begin()
self.collectionView.reloadData()
CATransaction.commit()
}
func getDataCount() -> Int {
return self.collectionView(self.collectionView, numberOfItemsInSection: 0)
}
} }
extension SPPlayerListViewController { 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 -------------- //MARK: -------------- UICollectionViewDelegate & UICollectionViewDataSource --------------
extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionViewDataSource { extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = SPPlayerListCell.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath) var cell: UICollectionViewCell = PlayerCellClass.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath)
cell.model = dataArr[indexPath.row]
if self.viewModel.currentPlayer == nil, indexPath == currentIndexPath { if let newCell = self.dataSource?.sp_playerListViewController(self, collectionView, cellForItemAt: indexPath, oldCell: cell) {
self.currentIndexPath = indexPath cell = newCell
self.viewModel.currentPlayer = cell
} }
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 return cell
} }
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 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) { func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
scrollDidEnd(scrollView)
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
scrollDidEnd(scrollView)
}
private func scrollDidEnd(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y let offsetY = scrollView.contentOffset.y
let indexPaths = self.collectionView.indexPathsForVisibleItems let indexPaths = self.collectionView.indexPathsForVisibleItems
for indexPath in indexPaths { for indexPath in indexPaths {
@ -164,7 +299,7 @@ extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionView
extension SPPlayerListViewController { extension SPPlayerListViewController {
@objc func didBecomeActiveNotification() { @objc func didBecomeActiveNotification() {
if !self.dataArr.isEmpty && self.viewModel.isPlaying && isDidAppear { if getDataCount() > 0 && self.viewModel.isPlaying && isDidAppear {
self.viewModel.currentPlayer?.start() 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)
}
}

View File

@ -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()
}
}
}
}

View File

@ -13,9 +13,12 @@ protocol SPPlayerProtocol: NSObjectProtocol {
var playerFinishHadle: (() -> Void)? { get set } var playerFinishHadle: (() -> Void)? { get set }
var model: Any? { get set } var model: Any? { get set }
var videoInfo: SPVideoInfoModel? { get set }
var isCurrent: Bool { get set } var isCurrent: Bool { get set }
var rate: Float { get set }
/// ///
func prepare() func prepare()
@ -25,4 +28,7 @@ protocol SPPlayerProtocol: NSObjectProtocol {
/// ///
func pause() func pause()
///
func replay()
} }

View 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
}

View 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?
}

View File

@ -9,6 +9,21 @@ import UIKit
class SPPlayerControlView: UIView { 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)? var panProgressFinishBlock: ((_ progress: CGFloat) -> Void)?
@ -19,6 +34,12 @@ class SPPlayerControlView: UIView {
} }
} }
var isCurrent: Bool = false {
didSet {
updatePlayIconState()
}
}
private(set) lazy var progressView: SPPlayerProgressView = { private(set) lazy var progressView: SPPlayerProgressView = {
let view = SPPlayerProgressView() let view = SPPlayerProgressView()
view.panStart = { [weak self] in view.panStart = { [weak self] in
@ -37,12 +58,33 @@ class SPPlayerControlView: UIView {
return view 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) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
let tap = UITapGestureRecognizer(target: self, action: #selector(hadlePlayAndOrPaused))
self.addGestureRecognizer(tap)
sp_setupUI() sp_setupUI()
} }
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "isPlaying" {
updatePlayIconState()
}
}
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@ -53,6 +95,7 @@ extension SPPlayerControlView {
private func sp_setupUI() { private func sp_setupUI() {
addSubview(progressView) addSubview(progressView)
addSubview(playImageView)
progressView.snp.makeConstraints { make in progressView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(10) make.left.equalToSuperview().offset(10)
@ -60,7 +103,35 @@ extension SPPlayerControlView {
make.bottom.equalToSuperview().offset(-20) make.bottom.equalToSuperview().offset(-20)
make.height.equalTo(30) 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)
}
} }

View File

@ -9,7 +9,17 @@ import UIKit
class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol { class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
weak var viewModel: SPPlayerListViewModel? {
didSet {
controlView.viewModel = viewModel
}
}
var isLoop = true {
didSet {
player.isLoop = isLoop
}
}
private lazy var player: SPPlayer = { private lazy var player: SPPlayer = {
let player = SPPlayer() let player = SPPlayer()
@ -48,16 +58,44 @@ class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
//MARK: SPPlayerProtocol //MARK: SPPlayerProtocol
var model: Any? { var model: Any? {
didSet { didSet {
guard let model = model as? SPShortModel else { return } self.controlView.progress = 0
player.setPlayUrl(url: model.video_info?.video_url ?? "") self.coverImageView.isHidden = false
coverImageView.sp_setImage(url: model.image_url)
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)? var playerFinishHadle: (() -> Void)?
@ -74,13 +112,17 @@ class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
player.pause() player.pause()
} }
func replay() {
player.replay()
}
} }
extension SPPlayerListCell { extension SPPlayerListCell {
private func sp_setupUI() { private func sp_setupUI() {
contentView.addSubview(coverImageView)
contentView.addSubview(playerView) contentView.addSubview(playerView)
contentView.addSubview(coverImageView)
contentView.addSubview(controlView) contentView.addSubview(controlView)
coverImageView.snp.makeConstraints { make in coverImageView.snp.makeConstraints { make in
@ -103,7 +145,7 @@ extension SPPlayerListCell {
extension SPPlayerListCell: SPPlayerDelegate { extension SPPlayerListCell: SPPlayerDelegate {
func sp_playCompletion(_ player: SPPlayer) { func sp_playCompletion(_ player: SPPlayer) {
self.playerFinishHadle?()
} }
func sp_playLoadingEnd(_ player: SPPlayer) { 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
}
}
} }

View File

@ -0,0 +1,12 @@
//
// SPTVPlayerListCell.swift
// ShortPlay
//
// Created by on 2025/4/10.
//
import UIKit
class SPTVPlayerListCell: SPPlayerListCell {
}

View File

@ -9,7 +9,7 @@ import UIKit
class SPPlayerListViewModel: NSObject { class SPPlayerListViewModel: NSObject {
var isPlaying = false @objc dynamic var isPlaying: Bool = true
private var _currentPlayer: SPPlayerProtocol? private var _currentPlayer: SPPlayerProtocol?
var currentPlayer: SPPlayerProtocol? { var currentPlayer: SPPlayerProtocol? {
@ -18,6 +18,9 @@ class SPPlayerListViewModel: NSObject {
_currentPlayer?.pause() _currentPlayer?.pause()
_currentPlayer = newValue _currentPlayer = newValue
_currentPlayer?.playerFinishHadle = { [weak self] in
self?.handlePlayFinish?()
}
_currentPlayer?.isCurrent = true _currentPlayer?.isCurrent = true
} }
get { 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)?
} }

View File

@ -9,9 +9,16 @@ import UIKit
class SPToast: NSObject { class SPToast: NSObject {
static func config() {
CSToastManager.setTapToDismissEnabled(false)
CSToastManager.setDefaultDuration(2)
CSToastManager.setDefaultPosition(CSToastPositionCenter)
}
static func show(text: String?) { static func show(text: String?) {
guard let text = text else { return } 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)
} }
} }

View File

@ -15,6 +15,9 @@ import ZFPlayer
// /// // ///
// @objc optional func sp_onCurrentPositionUpdate(_ player: SPPlayer, position: Int) // @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) @objc optional func sp_playTimeChanged(_ player: SPPlayer, currentTime: Int, duration: Int)
@ -26,14 +29,27 @@ import ZFPlayer
/// ///
@objc optional func sp_playLoadingEnd(_ player: SPPlayer) @objc optional func sp_playLoadingEnd(_ player: SPPlayer)
} }
class SPPlayer: NSObject { class SPPlayer: NSObject {
@objc enum PlayState: Int {
case unknown
case playing
case paused
case failed
case stopped
}
weak var delegate: SPPlayerDelegate? weak var delegate: SPPlayerDelegate?
private(set) lazy var isPlaying = false private(set) lazy var isPlaying = false
private(set) lazy var playState: PlayState = .unknown
/**
*/
private var isAddIdleTimerDisabledObserver = false
/// ///
var duration: Int { var duration: Int {
@ -44,6 +60,16 @@ class SPPlayer: NSObject {
return Int(self.player.currentTime) return Int(self.player.currentTime)
} }
///0.5 - 2
var rate: Float {
set {
player.rate = newValue
}
get {
return player.rate
}
}
var playerView: UIView? { var playerView: UIView? {
didSet { didSet {
playerView?.addSubview(player.view) playerView?.addSubview(player.view)
@ -55,50 +81,99 @@ class SPPlayer: NSObject {
private lazy var player: ZFAVPlayerManager = { private lazy var player: ZFAVPlayerManager = {
let player = ZFAVPlayerManager() let player = ZFAVPlayerManager()
player.shouldAutoPlay = false
return player return player
}() }()
var isLoop = true var isLoop = true
deinit {
self.stop()
}
override init() { override init() {
super.init() super.init()
player.scalingMode = .aspectFill player.scalingMode = .aspectFill
sp_addAction() 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) { func setPlayUrl(url: String) {
let proxyURL = KTVHTTPCache.proxyURL(withOriginalURL: URL(string: url)) let proxyURL = KTVHTTPCache.proxyURL(withOriginalURL: URL(string: url))
self.player.assetURL = proxyURL self.player.assetURL = proxyURL
// self.prepare()
// self.player.assetURL = URL(string: url)
self.prepare()
} }
/// ///
func prepare() { func prepare() {
self.player.prepareToPlay() // self.player.prepareToPlay()
} }
func stop() { func stop() {
self.isPlaying = false self.isPlaying = false
player.stop() player.stop()
self.removeIdleTimerDisabledObserver()
UIApplication.shared.isIdleTimerDisabled = false
} }
func start() { func start() {
self.isPlaying = true self.isPlaying = true
player.play() player.play()
UIApplication.shared.isIdleTimerDisabled = true
self.addIdleTimerDisabledObserver()
} }
/// ///
func pause() { func pause() {
self.isPlaying = false self.isPlaying = false
player.pause() 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) { func seekToTime(toTime: Int) {
// self.player.seek(toTime: Int64(toTime), seekMode: AVP_SEEKMODE_ACCURATE) // self.player.seek(toTime: Int64(toTime), seekMode: AVP_SEEKMODE_ACCURATE)
self.player.seek(toTime: TimeInterval(toTime), completionHandler: nil) self.player.seek(toTime: TimeInterval(toTime), completionHandler: nil)
} }
} }
extension SPPlayer { extension SPPlayer {
@ -115,7 +190,25 @@ extension SPPlayer {
guard let self = self else { return } guard let self = self else { return }
if playState == .playStatePlaying, !isPlaying { if playState == .playStatePlaying, !isPlaying {
self.pause() 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)") spLog(message: "播放状态====\(playState)")
} }
@ -124,8 +217,21 @@ extension SPPlayer {
guard let self = self else { return } guard let self = self else { return }
if loadState == .playable, !isPlaying { if loadState == .playable, !isPlaying {
self.pause() 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 { if isLoop {
self.player.replay() self.player.replay()
} else { } else {
self.isPlaying = false
self.delegate?.sp_playCompletion?(self) self.delegate?.sp_playCompletion?(self)
} }
} }

View File

@ -5,10 +5,12 @@
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "Frame@2x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "Frame@3x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -5,10 +5,12 @@
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "Frame@2x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "Frame@3x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -5,10 +5,12 @@
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "Frame@2x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "Frame@3x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -9,3 +9,4 @@
"Home" = "Home"; "Home" = "Home";
"For You" = "For You"; "For You" = "For You";
"Error" = "Error"; "Error" = "Error";
"Profile" = "Profile";