// // SPPlayerDetailViewController.swift // MoviaBox // // Created by 曾觉新 on 2025/4/10. // import UIKit class SPPlayerDetailViewController: SPPlayerListViewController { override var PlayerCellClass: SPPlayerListCell.Type { return SPPlayerDetailCell.self } override var contentSize: CGSize { return CGSize(width: kSPScreenWidth, height: kSPScreenHeight - kSPTabbarSafeBottomMargin - 35) } var videoId: String? = "0" var shortPlayId: String? var activityId: String? var playHistoryModel: SPShortModel? private var detailModel: SPVideoDetailModel? ///上一次上报播放时长的节点 private var lastUploadTime: Int = 0 ///是否展示推荐数据 private var isShowRecommand = false private var recommandTimer: Timer? //MARK: UI属性 ///选集视图 private weak var episodeView: SPEpisodeView? private lazy var backButton: UIButton = { let button = UIButton(type: .custom) button.setImage(UIImage(named: "arrow_left_icon_01"), for: .normal) button.addTarget(self, action: #selector(handleBack), for: .touchUpInside) return button }() private lazy var titleLabel: UILabel = { let label = UILabel() label.font = .fontBold(ofSize: 18) label.textColor = .colorFFFFFF() return label }() private lazy var episodeLabel: UILabel = { let label = UILabel() label.font = .fontMedium(ofSize: 18) label.textColor = .colorFFFFFF(alpha: 0.4) return label }() private lazy var bottomView: UIView = { let view = UIView() view.backgroundColor = .color1C1C1E() return view }() deinit { NotificationCenter.default.removeObserver(self) } override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.addObserver(self, selector: #selector(buyVipFinishNotification), name: SPIAPManager.buyVipFinishNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(reachabilityDidChangeNotification), name: SPNetworkReachabilityManager.reachabilityDidChangeNotification, object: nil) self.autoNextEpisode = true self.dataSource = self self.delegate = self requestDetailData() _addAction() _setupUI() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.setNavigationBarHidden(true, animated: true) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // self.uploadPlayTime() } override func play() { guard let videoInfo = self.viewModel.currentPlayer?.videoInfo else { return } //视频需要付费 if videoInfo.is_lock == true { self.pause() //我的金币 let myCoin = (SPLoginManager.manager.userInfo?.coin_left_total ?? 0) + (SPLoginManager.manager.userInfo?.send_coin_left_total ?? 0) //解锁视频需要的金币 let videoCoin = videoInfo.coins ?? 0 if myCoin < videoCoin, self.viewModel.currentPlayer?.hasLockUpEpisode != true {//金币不够时打开充值页面 self.onPlayBuy() } return } super.play() SPVideoAPI.requestCreateVideoPlayHistory(videoId: videoInfo.short_play_video_id ?? "", shortPlayId: videoInfo.short_play_id ?? "") } override func currentPlayFinish() { if let videoInfo = self.viewModel.currentPlayer?.videoInfo, let shortPlayId = videoInfo.short_play_id, let videoId = videoInfo.short_play_video_id { if let activityId = self.activityId { //播放完成统计 SPVideoAPI.requestViewingFinish(shortPlayId: shortPlayId, videoId: videoId, activityId: activityId) } //播放完成上报进度为0 SPVideoAPI.requestUploadPlayTime(shortPlayId: shortPlayId, videoId: videoId, seconds: 0) self.viewModel.currentPlayer?.videoInfo?.play_seconds = 0 } super.currentPlayFinish() } override func currentPlayTimeDidChange(time: Int) { super.currentPlayTimeDidChange(time: time) //每播放5秒上报一次 播放时长小于上次上报时间也需要上报 if (time >= lastUploadTime + 5 || time < lastUploadTime) && time >= 5 { lastUploadTime = time uploadPlayTime() } } override func handleBack() { guard isShowRecommand else { super.handleBack() return } self.pause() let view = SPPlayerDetailRecommandView() view.clickCloseButton = { [weak self] in guard let self = self else { return } self._handleBack() } view.clickPlayButton = { [weak self] model in guard let self = self else { return } self.shortPlayId = model.short_play_id self.activityId = nil self.videoId = nil self.requestDetailData() } view.present(in: nil) } private func _handleBack() { super.handleBack() } } extension SPPlayerDetailViewController { private func _setupUI() { view.addSubview(backButton) view.addSubview(titleLabel) view.addSubview(episodeLabel) view.addSubview(bottomView) backButton.snp.makeConstraints { make in make.left.equalToSuperview().offset(5) make.top.equalToSuperview().offset(5 + kSPStatusbarHeight) make.width.height.equalTo(37) } titleLabel.snp.makeConstraints { make in make.left.equalTo(backButton.snp.right) make.centerY.equalTo(backButton) // make.right.equalToSuperview().offset(-15) make.width.lessThanOrEqualTo(kSPScreenWidth - 130) } episodeLabel.snp.makeConstraints { make in make.centerY.equalTo(titleLabel) make.left.equalTo(titleLabel.snp.right).offset(16) } bottomView.snp.makeConstraints { make in make.left.right.bottom.equalToSuperview() make.top.equalTo(self.collectionView.snp.bottom) } } private func _addAction() { self.viewModel.handleEpisode = { [weak self] in self?.onEpisode() } } } extension SPPlayerDetailViewController { private func onEpisode() { let view = SPEpisodeView() view.dataArr = detailModel?.episodeList ?? [] view.shortModel = detailModel?.shortPlayInfo view.currentIndex = self.currentIndexPath.row view.didSelectedIndex = { [weak self] (index) in self?.scrollToItem(indexPath: IndexPath(row: index, section: 0), animated: false) } view.present(in: nil) self.episodeView = view } ///打开支付页面 private func onPlayBuy() { guard let videoInfo = self.viewModel.currentPlayer?.videoInfo else { return } let view = SPPlayBuyView() view.shortPlayId = videoInfo.short_play_id view.videoId = videoInfo.short_play_video_id view.present(in: nil) } ///解锁视频 private func unlockVideo(indexPath: IndexPath) { guard let videoInfo = detailModel?.episodeList?[indexPath.row] else { return } guard let shortPlayId = videoInfo.short_play_id, let videoId = videoInfo.short_play_video_id else { return } SPWalletAPI.requestCoinUnlockVideo(shortPlayId: shortPlayId, videoId: videoId) { [weak self] model in guard let self = self else { return } guard let model = model else { return } switch model.status { case .jump: SPToast.show(text: "movia_jump_unlock_error".localized) case .noPlay: SPToast.show(text: "movia_buy_fail_toast_01".localized) case .notEnough: self.onPlayBuy() case .success: videoInfo.is_lock = false self.reloadData { [weak self] in guard let self = self else { return } self.play() } //更新用户信息 SPLoginManager.manager.updateUserInfo(completer: nil) default: break } } } ///上报播放进度 private func uploadPlayTime() { let videoInfo = self.viewModel.currentPlayer?.videoInfo let currentTime = self.viewModel.currentPlayer?.currentPosition ?? 0 let duration = self.viewModel.currentPlayer?.duration ?? 0 var time = currentTime if currentTime >= duration { time = 0 } self.viewModel.currentPlayer?.videoInfo?.play_seconds = time * 1000 guard let shortPlayId = videoInfo?.short_play_id, let videoId = videoInfo?.short_play_video_id else { return } //上报播放时长 SPVideoAPI.requestUploadPlayTime(shortPlayId: shortPlayId, videoId: videoId, seconds: time * 1000) } @objc private func buyVipFinishNotification() { guard SPLoginManager.manager.userInfo?.is_vip == true else { return } self.detailModel?.episodeList?.forEach({ $0.is_lock = false }) self.reloadData { [weak self] in self?.play() } } ///网络切换通知 @objc private func reachabilityDidChangeNotification() { if SPNetworkReachabilityManager.manager.isReachable == true && self.detailModel == nil { self.requestDetailData() } } @objc private func handleRecommandTimer() { self.isShowRecommand = true } } //MARK: -------------- SPPlayerListViewControllerDataSource -------------- extension SPPlayerDetailViewController: SPPlayerListViewControllerDataSource, SPPlayerListViewControllerDelegate { func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell { if let cell = oldCell as? SPPlayerDetailCell { cell.shortModel = detailModel?.shortPlayInfo cell.videoInfo = detailModel?.episodeList?[indexPath.row] cell.isLoop = false let upRow = indexPath.row - 1 if upRow >= 0, let videoInfo = detailModel?.episodeList?[upRow], videoInfo.is_lock == true { cell.hasLockUpEpisode = true } else { cell.hasLockUpEpisode = false } cell.clickUnlockButton = { [weak self] (cell) in guard let self = self else { return } guard let indexPath = self.collectionView.indexPath(for: cell) else { return } self.unlockVideo(indexPath: indexPath) } } return oldCell } func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int { return detailModel?.episodeList?.count ?? 0 } func sp_playerListViewController(_ viewController: SPPlayerListViewController, didChangeIndexPathForVisible indexPath: IndexPath) { self.episodeView?.currentIndex = indexPath.row let videoInfo = detailModel?.episodeList?[indexPath.row] titleLabel.text = detailModel?.shortPlayInfo?.name episodeLabel.text = "\(videoInfo?.episode ?? "0")/\(detailModel?.shortPlayInfo?.episode_total ?? 0)" } } //MARK: -------------- APP生命周期 -------------- extension SPPlayerDetailViewController { @objc override func willResignActiveNotification() { super.willResignActiveNotification() uploadPlayTime() } } extension SPPlayerDetailViewController { private func requestDetailData() { guard let shortPlayId = self.shortPlayId else { return } isShowRecommand = false recommandTimer?.invalidate() recommandTimer = nil recommandTimer = Timer.scheduledTimer(timeInterval: 6, target: YYWeakProxy(target: self), selector: #selector(handleRecommandTimer), userInfo: nil, repeats: false) SPVideoAPI.requestVideoDetail(videoId: videoId, shortPlayId: shortPlayId, activityId: activityId) { [weak self] model in guard let self = self else { return } if let model = model { self.detailModel = model self.reloadData { [weak self] in guard let self = self else { return } if let videoInfo = self.detailModel?.video_info { var row: Int? self.detailModel?.episodeList?.enumerated().forEach({ if $1.id == videoInfo.id { row = $0 } }) if let row = row { self.scrollToItem(indexPath: IndexPath(row: row, section: 0), animated: false) { [weak self] in guard let self = self else { return } //跳转到指定进度 // self.viewModel.currentPlayer?.seekToTime(toTime: (videoInfo.play_seconds ?? 0) / 1000) } } else { self.scrollToItem(indexPath: .init(row: 0, section: 0), animated: false) } } else { self.scrollToItem(indexPath: .init(row: 0, section: 0), animated: false) } } } } } }