From fc3ba26bf16ca8b64716d11c40c12414d5d33cc9 Mon Sep 17 00:00:00 2001 From: yicheng <11733500+yichengchen@users.noreply.github.com> Date: Thu, 23 May 2024 16:55:35 +0800 Subject: [PATCH 1/9] tmp: add NewCommonPlayerViewController --- BilibiliLive.xcodeproj/project.pbxproj | 24 +++ .../Component/Player/DanmuViewPlugin.swift | 102 +++++++++++++ .../NewCommonPlayerViewController.swift | 139 ++++++++++++++++++ .../Component/Video/BVideoPlayPlugin.swift | 122 +++++++++++++++ .../Video/NewVideoPlayerViewController.swift | 46 ++++++ .../Video/NewVideoPlayerViewModel.swift | 105 +++++++++++++ .../Component/Video/VideoDanmuProvider.swift | 16 +- .../Video/VideoDetailViewController.swift | 4 +- .../Video/VideoPlayerViewController.swift | 19 +-- BilibiliLive/Extensions/String+Error.swift | 12 ++ BilibiliLive/Keys.swift | 3 + .../Vendor/DanmakuKit/DanmakuView.swift | 3 + 12 files changed, 580 insertions(+), 15 deletions(-) create mode 100644 BilibiliLive/Component/Player/DanmuViewPlugin.swift create mode 100644 BilibiliLive/Component/Player/NewCommonPlayerViewController.swift create mode 100644 BilibiliLive/Component/Video/BVideoPlayPlugin.swift create mode 100644 BilibiliLive/Component/Video/NewVideoPlayerViewController.swift create mode 100644 BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift create mode 100644 BilibiliLive/Extensions/String+Error.swift diff --git a/BilibiliLive.xcodeproj/project.pbxproj b/BilibiliLive.xcodeproj/project.pbxproj index 3235a2ee..38b7455b 100644 --- a/BilibiliLive.xcodeproj/project.pbxproj +++ b/BilibiliLive.xcodeproj/project.pbxproj @@ -17,6 +17,12 @@ 490EC3E7290CC8F8001E00B6 /* RankingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490EC3E6290CC8F8001E00B6 /* RankingViewController.swift */; }; 490EC3E9290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490EC3E8290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift */; }; 492731EE29096677005F5B0A /* HotViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492731ED29096677005F5B0A /* HotViewController.swift */; }; + 492AD7092BFF1E6C007221C8 /* NewCommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */; }; + 492AD70B2BFF23B1007221C8 /* DanmuViewPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */; }; + 492AD70D2BFF33DF007221C8 /* NewVideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */; }; + 492AD70F2BFF6761007221C8 /* NewVideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */; }; + 492AD7112C001C7B007221C8 /* BVideoPlayPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */; }; + 492AD7132C001CA7007221C8 /* String+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7122C001CA7007221C8 /* String+Error.swift */; }; 493307FD2BF230DB003622ED /* LivePlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493307FC2BF230DB003622ED /* LivePlayerViewModel.swift */; }; 49389D6228AFEA2900B9DAFD /* VideoDanmuProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49389D6128AFEA2900B9DAFD /* VideoDanmuProvider.swift */; }; 49389D8928B0A1B700B9DAFD /* UIViewController+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49389D8828B0A1B700B9DAFD /* UIViewController+Ext.swift */; }; @@ -143,6 +149,12 @@ 490EC3E6290CC8F8001E00B6 /* RankingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingViewController.swift; sourceTree = ""; }; 490EC3E8290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLSettingLineCollectionViewCell.swift; sourceTree = ""; }; 492731ED29096677005F5B0A /* HotViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotViewController.swift; sourceTree = ""; }; + 492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCommonPlayerViewController.swift; sourceTree = ""; }; + 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DanmuViewPlugin.swift; sourceTree = ""; }; + 492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewVideoPlayerViewController.swift; sourceTree = ""; }; + 492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewVideoPlayerViewModel.swift; sourceTree = ""; }; + 492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BVideoPlayPlugin.swift; sourceTree = ""; }; + 492AD7122C001CA7007221C8 /* String+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Error.swift"; sourceTree = ""; }; 493307FC2BF230DB003622ED /* LivePlayerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LivePlayerViewModel.swift; sourceTree = ""; }; 49389D6128AFEA2900B9DAFD /* VideoDanmuProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDanmuProvider.swift; sourceTree = ""; }; 49389D8828B0A1B700B9DAFD /* UIViewController+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Ext.swift"; sourceTree = ""; }; @@ -367,6 +379,9 @@ 49389D6128AFEA2900B9DAFD /* VideoDanmuProvider.swift */, 498DB1DE291BC24700F95607 /* BMaskProvider.swift */, 49FB8EBF291F4C520045D5DE /* VMaskProvider.swift */, + 492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */, + 492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */, + 492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */, ); path = Video; sourceTree = ""; @@ -384,6 +399,7 @@ AEA5FDE1290F6E2600E7C0B2 /* String.swift */, AE2B41562914C02000BF2B0B /* Int.swift */, 49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */, + 492AD7122C001CA7007221C8 /* String+Error.swift */, ); path = Extensions; sourceTree = ""; @@ -626,6 +642,8 @@ 49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */, F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */, 49FB8EE829208EBE0045D5DE /* SidxParseUtil.swift */, + 492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */, + 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */, ); path = Player; sourceTree = ""; @@ -812,6 +830,7 @@ 498CF2A72B63AABE0009793E /* decode.c in Sources */, F90AAE04265549B5008DE7C2 /* FeedViewController.swift in Sources */, 2DBE4C4D2628818F00D20413 /* HistoryViewController.swift in Sources */, + 492AD70D2BFF33DF007221C8 /* NewVideoPlayerViewController.swift in Sources */, F9171D6629026AC5002868C7 /* TitleSupplementaryView.swift in Sources */, F9B9EAE7261AC6F80045C2C6 /* BLTabBarViewController.swift in Sources */, F9562C92261A0D2200573B74 /* VideoPlayerViewController.swift in Sources */, @@ -840,14 +859,17 @@ F927ED682610113A00EAB8E3 /* LiveDanMuProvider.swift in Sources */, 498CF2922B63AABE0009793E /* histogram.c in Sources */, 49474213290509F6005D6885 /* DateFormatter.swift in Sources */, + 492AD7112C001C7B007221C8 /* BVideoPlayPlugin.swift in Sources */, F927ED742610395300EAB8E3 /* DanmakuView.swift in Sources */, F927ED782610395300EAB8E3 /* DanmakuTrack.swift in Sources */, + 492AD7092BFF1E6C007221C8 /* NewCommonPlayerViewController.swift in Sources */, 498CF29C2B63AABE0009793E /* compress_fragment.c in Sources */, 499C760F2930E068003160FB /* NVASocket.swift in Sources */, 498CF2912B63AABE0009793E /* backward_references_hq.c in Sources */, 494742112905053E005D6885 /* BLMotionCollectionViewCell.swift in Sources */, 494741E7290391A7005D6885 /* BLButton.swift in Sources */, F927ED992610AD8D00EAB8E3 /* LiveViewController.swift in Sources */, + 492AD7132C001CA7007221C8 /* String+Error.swift in Sources */, 490EC3E7290CC8F8001E00B6 /* RankingViewController.swift in Sources */, F927ED732610395300EAB8E3 /* DanmakuCell.swift in Sources */, 498CF2A92B63AABE0009793E /* bit_reader.c in Sources */, @@ -866,6 +888,7 @@ 498CF2AB2B63AABE0009793E /* LMBrotliCompression.c in Sources */, 498CF2AC2B63AABE0009793E /* NSData+BrotliCompression.m in Sources */, 498CF2A32B63AABE0009793E /* transform.c in Sources */, + 492AD70B2BFF23B1007221C8 /* DanmuViewPlugin.swift in Sources */, 49D39F28263AD40000F14497 /* WebRequest.swift in Sources */, 498DB1DF291BC24700F95607 /* BMaskProvider.swift in Sources */, 494741C029002797005D6885 /* UserDefault+..swift in Sources */, @@ -873,6 +896,7 @@ AE4889B228FE55DA00E8C5CD /* FavoriteViewController.swift in Sources */, F9B57356260F5F7400771ED5 /* LivePlayerViewController.swift in Sources */, 498CF29F2B63AABE0009793E /* static_dict.c in Sources */, + 492AD70F2BFF6761007221C8 /* NewVideoPlayerViewModel.swift in Sources */, 498CF2A02B63AABE0009793E /* literal_cost.c in Sources */, F99D28F72619F5F000F8E66A /* FollowsViewController.swift in Sources */, F9D382B426359EF90070508F /* ApiRequest.swift in Sources */, diff --git a/BilibiliLive/Component/Player/DanmuViewPlugin.swift b/BilibiliLive/Component/Player/DanmuViewPlugin.swift new file mode 100644 index 00000000..a6e6916d --- /dev/null +++ b/BilibiliLive/Component/Player/DanmuViewPlugin.swift @@ -0,0 +1,102 @@ +// +// DanmuViewPlugin.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/23. +// + +import AVKit +import Combine +import UIKit + +protocol DanmuProviderProtocol { + var observerPlayerTime: Bool { get } + var onSendTextModel: PassthroughSubject { get } + func playerTimeChange(time: TimeInterval) +} + +class DanmuViewPlugin { + var showDanmu = Settings.defaultDanmuStatus { + didSet { danMuView.isHidden = !showDanmu } + } + + init(provider: DanmuProviderProtocol) { + danmuProvider = provider + provider.onSendTextModel + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.shoot($0) + }.store(in: &cancellable) + } + + private let danmuProvider: DanmuProviderProtocol + private let danMuView = DanmakuView() + private var timeObserver: Any? + private var cancellable = Set() + + private func shoot(_ model: DanmakuCellModel) { + danMuView.shoot(danmaku: model) + } +} + +extension DanmuViewPlugin: CommonPlayerPlugin { + func playerWillStart(player: AVPlayer) { + guard danmuProvider.observerPlayerTime else { + return + } + player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), + queue: DispatchQueue.global()) { [weak self] time in + guard let self else { return } + if danMuView.isHidden { return } + let seconds = time.seconds + danmuProvider.playerTimeChange(time: seconds) + } + } + + func playerDidCleanUp(player: AVPlayer) { + if let timeObserver { + player.removeTimeObserver(timeObserver) + } + } + + func addViewToPlayerOverlay(container: UIView) { + container.addSubview(danMuView) + danMuView.makeConstraintsToBindToSuperview() + danMuView.setNeedsLayout() + danMuView.layoutIfNeeded() + danMuView.isHidden = showDanmu + danMuView.paddingTop = 5 + danMuView.trackHeight = 50 + danMuView.displayArea = Settings.danmuArea.percent + danMuView.recaculateTracks() + } + + func playerDidStart(player: AVPlayer) { + danMuView.play() + } + + func playerDidPause(player: AVPlayer) { + danMuView.pause() + } + + func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] { + let danmuImage = UIImage(systemName: "list.bullet.rectangle.fill") + let danmuImageDisable = UIImage(systemName: "list.bullet.rectangle") + let danmuAction = UIAction(title: "Show Danmu", image: danMuView.isHidden ? danmuImageDisable : danmuImage) { + [weak self] action in + guard let self = self else { return } + Settings.defaultDanmuStatus.toggle() + self.danMuView.isHidden.toggle() + action.image = self.danMuView.isHidden ? danmuImageDisable : danmuImage + } + let danmuDurationMenu = UIMenu(title: "弹幕展示时长", options: [.displayInline, .singleSelection], children: [4, 6, 8].map { dur in + UIAction(title: "\(dur) 秒", state: dur == Settings.danmuDuration ? .on : .off) { _ in Settings.danmuDuration = dur } + }) + let danmuAILevelMenu = UIMenu(title: "弹幕屏蔽等级", options: [.displayInline, .singleSelection], children: [Int32](1...10).map { level in + UIAction(title: "\(level)", state: level == Settings.danmuAILevel ? .on : .off) { _ in Settings.danmuAILevel = level } + }) + let danmuSettingMenu = UIMenu(title: "弹幕设置", image: UIImage(systemName: "keyboard.badge.ellipsis"), children: [danmuDurationMenu, danmuAILevelMenu]) + + return [danmuAction, danmuSettingMenu] + } +} diff --git a/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift b/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift new file mode 100644 index 00000000..87f6d3f8 --- /dev/null +++ b/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift @@ -0,0 +1,139 @@ +// +// NewCommonPlayerViewController.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/23. +// + +import AVKit +import UIKit + +protocol CommonPlayerPlugin { + func addViewToPlayerOverlay(container: UIView) + func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] + + func playerDidLoad(playerVC: AVPlayerViewController) + func playerDidChange(player: AVPlayer) + func playerItemDidChange(playerItem: AVPlayerItem) + + func playerWillStart(player: AVPlayer) + func playerDidStart(player: AVPlayer) + func playerDidPause(player: AVPlayer) + func playerDidEnd(player: AVPlayer) + func playerDidFail(player: AVPlayer) + func playerDidCleanUp(player: AVPlayer) +} + +extension CommonPlayerPlugin { + func addViewToPlayerOverlay(container: UIView) {} + func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] { return [] } + + func playerWillStart(player: AVPlayer) {} + func playerDidStart(player: AVPlayer) {} + func playerDidPause(player: AVPlayer) {} + func playerDidEnd(player: AVPlayer) {} + func playerDidFail(player: AVPlayer) {} + func playerDidCleanUp(player: AVPlayer) {} + + func playerDidLoad(playerVC: AVPlayerViewController) {} + func playerDidChange(player: AVPlayer) {} + func playerItemDidChange(playerItem: AVPlayerItem) {} +} + +class NewCommonPlayerViewController: UIViewController { + private let playerVC = AVPlayerViewController() + private var activePlugins = [CommonPlayerPlugin]() + private var observations = Set() + private var rateObserver: NSKeyValueObservation? + private var statusObserver: NSKeyValueObservation? + + override func viewDidLoad() { + super.viewDidLoad() + addChild(playerVC) + view.addSubview(playerVC.view) + playerVC.didMove(toParent: self) + playerVC.view.snp.makeConstraints { $0.edges.equalToSuperview() } + let playerObservation = playerVC.observe(\.player) { [weak self] vc, obs in + if let oldPlayer = obs.oldValue, let oldPlayer { + self?.activePlugins.forEach { $0.playerDidCleanUp(player: oldPlayer) } + } + self?.playerDidChange(player: vc.player) + } + observations.insert(playerObservation) + activePlugins.forEach { $0.playerDidLoad(playerVC: playerVC) } + } + + override var preferredFocusEnvironments: [UIFocusEnvironment] { + return [playerVC.view] + } + + func addPlugin(plugin: CommonPlayerPlugin) { + plugin.addViewToPlayerOverlay(container: playerVC.contentOverlayView!) + activePlugins.append(plugin) + plugin.playerDidLoad(playerVC: playerVC) + } + + func showErrorAlertAndExit(title: String = "播放失败", message: String = "未知错误") { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let actionOk = UIAlertAction(title: "OK", style: .default) { + [weak self] _ in + self?.dismiss(animated: true, completion: nil) + } + alertController.addAction(actionOk) + present(alertController, animated: true, completion: nil) + } +} + +extension NewCommonPlayerViewController { + private func playerDidChange(player: AVPlayer?) { + if let player { + activePlugins.forEach { $0.playerDidChange(player: player) } + rateObserver = player.observe(\.rate, options: [.old, .new]) { + [weak self] _player, obs in + DispatchQueue.main.async { [weak self] in + self?.playerRateDidChange(player: player) + } + } + if let playItem = player.currentItem { + observePlayerItem(playItem) + } + var menus = [UIMenuElement]() + activePlugins.forEach { + let newMenus = $0.addMenuItems(current: menus) + menus.append(contentsOf: newMenus) + } + playerVC.transportBarCustomMenuItems = menus + } else { + rateObserver = nil + } + } + + private func playerRateDidChange(player: AVPlayer) { + if player.rate > 0 { + activePlugins.forEach { $0.playerDidStart(player: player) } + } else if player.rate == 0 { + activePlugins.forEach { $0.playerDidPause(player: player) } + } + } + + private func observePlayerItem(_ playerItem: AVPlayerItem) { + statusObserver = playerItem.observe(\.status, options: [.new, .old]) { + [weak self] item, _ in + guard let self, let player = playerVC.player else { return } + switch item.status { + case .readyToPlay: + activePlugins.forEach { $0.playerWillStart(player: player) } + player.play() + case .failed: + activePlugins.forEach { $0.playerDidFail(player: player) } + default: + break + } + } + NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) + NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: .main) { [weak self] note in + guard let self, let player = playerVC.player else { return } + self.activePlugins.forEach { $0.playerDidEnd(player: player) } + } + } +} diff --git a/BilibiliLive/Component/Video/BVideoPlayPlugin.swift b/BilibiliLive/Component/Video/BVideoPlayPlugin.swift new file mode 100644 index 00000000..ae47c572 --- /dev/null +++ b/BilibiliLive/Component/Video/BVideoPlayPlugin.swift @@ -0,0 +1,122 @@ +// +// BVideoPlayPlugin.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/24. +// + +import AVKit + +class BVideoPlayPlugin: CommonPlayerPlugin { + private weak var playerVC: AVPlayerViewController? + private var playerDelegate: BilibiliVideoResourceLoaderDelegate? + private let playData: PlayerDetailData + + init(detailData: PlayerDetailData) { + playData = detailData + } + + func playerDidLoad(playerVC: AVPlayerViewController) { + self.playerVC = playerVC + Task { + try? await playmedia(urlInfo: playData.videoPlayURLInfo, playerInfo: playData.playerInfo) + } + } + + private func updatePlayerInfoView(aid: Int) async { +// if data == nil { +// data = try? await WebRequest.requestDetailVideo(aid: aid) +// } +// setPlayerInfo(title: data?.title, subTitle: data?.ownerName, desp: data?.View.desc, pic: data?.pic) + } + + private func setupDanmuMask() { + // if Settings.danmuMask { + // if let mask = info?.dm_mask, + // let video = playData.dash.video.first, + // let fps = info?.dm_mask?.fps, fps > 0 + // { + // maskProvider = BMaskProvider(info: mask, videoSize: CGSize(width: video.width ?? 0, height: video.height ?? 0)) + // } else if Settings.vnMask { + // maskProvider = VMaskProvider() + // } + // setupMask() + // } + } + + @MainActor + private func playmedia(urlInfo: VideoPlayURLInfo, playerInfo: PlayerInfo?) async throws { + let playURL = URL(string: BilibiliVideoResourceLoaderDelegate.URLs.play)! + let headers: [String: String] = [ + "User-Agent": Keys.userAgent, + "Referer": Keys.referer(for: playData.aid), + ] + let asset = AVURLAsset(url: playURL, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) + playerDelegate = BilibiliVideoResourceLoaderDelegate() + playerDelegate?.setBilibili(info: urlInfo, subtitles: playerInfo?.subtitle?.subtitles ?? [], aid: playData.aid) + if Settings.contentMatchOnlyInHDR { + if playerDelegate?.isHDR != true { + playerVC?.appliesPreferredDisplayCriteriaAutomatically = false + } + } + asset.resourceLoader.setDelegate(playerDelegate, queue: DispatchQueue(label: "loader")) + let playable = try await asset.load(.isPlayable) + if !playable { + throw "加载资源失败" + } + await prepare(toPlay: asset) + } + + @MainActor + func prepare(toPlay asset: AVURLAsset) async { + let playerItem = AVPlayerItem(asset: asset) + let player = AVPlayer(playerItem: playerItem) +// player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), queue: .main) { [weak self] time in +// guard let self else { return } + //// if self.danMuView.isHidden { return } +// let seconds = time.seconds + //// self.danmuProvider.playerTimeChange(time: seconds) +// +// if let duration = self.data?.View.duration { +// BiliBiliUpnpDMR.shared.sendProgress(duration: duration, current: Int(seconds)) +// } +// +// if let clipInfos = self.clipInfos { +// var matched = false +// for clip in clipInfos { +// if seconds > clip.start, seconds < clip.end { +// let action = { +// clip.skipped = true +// self.player?.seek(to: CMTime(seconds: Double(clip.end), preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) +// } +// if !(clip.skipped ?? false), Settings.autoSkip { +// action() +// self.skipAction = nil +// } else if self.skipAction?.accessibilityLabel != clip.a11Tag { +// self.skipAction = UIAction(title: clip.customText) { _ in +// action() +// } +// self.skipAction?.accessibilityLabel = clip.a11Tag +// } +// +// self.contextualActions = [self.skipAction].compactMap { $0 } +// matched = true +// break +// } +// } +// if !matched { +// self.contextualActions = [] +// } +// } +// } + + if let defaultRate = playerVC?.player?.defaultRate, + let speed = PlaySpeed.blDefaults.first(where: { $0.value == defaultRate }) + { + playerVC?.player = player + playerVC?.selectSpeed(AVPlaybackSpeed(rate: speed.value, localizedName: speed.name)) + } else { + playerVC?.player = player + } + } +} diff --git a/BilibiliLive/Component/Video/NewVideoPlayerViewController.swift b/BilibiliLive/Component/Video/NewVideoPlayerViewController.swift new file mode 100644 index 00000000..d74ff509 --- /dev/null +++ b/BilibiliLive/Component/Video/NewVideoPlayerViewController.swift @@ -0,0 +1,46 @@ +// +// NewVideoPlayerViewController.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/23. +// + +import AVKit +import Combine +import UIKit + +class NewVideoPlayerViewController: NewCommonPlayerViewController { + var data: VideoDetail? + var nextProvider: VideoNextProvider? + + init(playInfo: PlayInfo) { + viewModel = NewVideoPlayerViewModel(playInfo: playInfo) + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private let viewModel: NewVideoPlayerViewModel + private var cancelable = Set() + + override func viewDidLoad() { + super.viewDidLoad() + viewModel.onPluginReady.receive(on: DispatchQueue.main).sink { [weak self] completion in + switch completion { + case let .failure(err): + self?.showErrorAlertAndExit(message: err) + default: + break + } + } receiveValue: { [weak self] plugins in + plugins.forEach { self?.addPlugin(plugin: $0) } + }.store(in: &cancelable) + + Task { + await viewModel.load() + } + } +} diff --git a/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift b/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift new file mode 100644 index 00000000..b8e5e86c --- /dev/null +++ b/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift @@ -0,0 +1,105 @@ +// +// NewVideoPlayerViewModel.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/23. +// + +import Combine +import UIKit + +struct PlayerDetailData { + let aid: Int + let cid: Int + let epid: Int? // 港澳台解锁需要 + let isBangumi: Bool + + var playerStartPos: Int? + var detail: VideoDetail? + var clips: [VideoPlayURLInfo.ClipInfo]? + var playerInfo: PlayerInfo? + var videoPlayURLInfo: VideoPlayURLInfo +} + +class NewVideoPlayerViewModel { + var onPluginReady = PassthroughSubject<[CommonPlayerPlugin], String>() + + private var playInfo: PlayInfo + private let danmuProvider = VideoDanmuProvider() + + init(playInfo: PlayInfo) { + self.playInfo = playInfo + } + + func load() async { + do { + try await initPlayInfo() + let data = try await fetchVideoData() + await danmuProvider.initVideo(cid: data.cid, startPos: data.playerStartPos ?? 0) + let plugin = await generatePlayerPlugin(data) + onPluginReady.send(plugin) + + } catch let err { + onPluginReady.send(completion: .failure(err.localizedDescription)) + } + } + + private func initPlayInfo() async throws { + if !playInfo.isCidVaild { + playInfo.cid = try await WebRequest.requestCid(aid: playInfo.aid) + } + BiliBiliUpnpDMR.shared.sendVideoSwitch(aid: playInfo.aid, cid: playInfo.cid ?? 0) + } + + private func fetchVideoData() async throws -> PlayerDetailData { + assert(playInfo.isCidVaild) + let aid = playInfo.aid + let cid = playInfo.cid! + let info = try? await WebRequest.requestPlayerInfo(aid: aid, cid: cid) + do { + let playData: VideoPlayURLInfo + var clipInfos: [VideoPlayURLInfo.ClipInfo]? + if playInfo.isBangumi { + playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid) + clipInfos = playData.clip_info_list + } else { + playData = try await WebRequest.requestPlayUrl(aid: aid, cid: cid) + } + + var detail = PlayerDetailData(aid: playInfo.aid, cid: playInfo.cid!, epid: playInfo.epid, isBangumi: playInfo.isBangumi, clips: clipInfos, playerInfo: info, videoPlayURLInfo: playData) + + if let info, info.last_play_cid == cid, playData.dash.duration - info.playTimeInSecond > 5, Settings.continuePlay { + detail.playerStartPos = info.playTimeInSecond + } + + return detail + + // updatePlayerCharpter(playerInfo: playerInfo) + + } catch let err { + if case let .statusFail(code, message) = err as? RequestError { + if code == -404 || code == -10403 { +// 解锁港澳台番剧处理 +// do { +// if let ok = try await fetchAreaLimitVideoData(), ok { +// return +// } +// } catch let err { +// } + } + throw "\(code) \(message),可能需要大会员" + } else if info?.is_upower_exclusive == true { + throw "该视频为充电专属视频 \(err)" + } else { + throw err + } + } + } + + @MainActor private func generatePlayerPlugin(_ data: PlayerDetailData) async -> [CommonPlayerPlugin] { + let player = BVideoPlayPlugin(detailData: data) + let danmu = DanmuViewPlugin(provider: danmuProvider) + + return [player, danmu] + } +} diff --git a/BilibiliLive/Component/Video/VideoDanmuProvider.swift b/BilibiliLive/Component/Video/VideoDanmuProvider.swift index 1ef4b9e3..990efc45 100644 --- a/BilibiliLive/Component/Video/VideoDanmuProvider.swift +++ b/BilibiliLive/Component/Video/VideoDanmuProvider.swift @@ -6,6 +6,7 @@ // import Alamofire +import Combine import Foundation import SwiftyXMLParser import UIKit @@ -35,11 +36,14 @@ struct Danmu: Codable { } } -class VideoDanmuProvider { +class VideoDanmuProvider: DanmuProviderProtocol { var cid: Int! private var allDanmus = [Danmu]() private var playingDanmus = [Danmu]() + let observerPlayerTime: Bool = true + let onSendTextModel = PassthroughSubject() + var onShowDanmu: ((DanmakuTextCellModel) -> Void)? private var upDanmus = [Danmu]() @@ -55,7 +59,7 @@ class VideoDanmuProvider { private let segmentDuration = 60 * 6 private func getSegmentIdx(time: TimeInterval) -> Int { Int(time) / segmentDuration + 1 } - func initVideo(cid id: Int?, startPos: Int) async { + func initVideo(cid id: Int, startPos: Int) async { cid = id upDanmus.removeAll() segmentDanmus.removeAll(keepingCapacity: true) @@ -160,7 +164,9 @@ class VideoDanmuProvider { let dm = upDanmus[upDanmuIdx] guard dm.time < time else { break } upDanmuIdx += 1 - onShowDanmu?(DanmakuTextCellModel(dm: dm)) + let model = DanmakuTextCellModel(dm: dm) + onShowDanmu?(model) + onSendTextModel.send(model) } while danmuIdx < dms.count { @@ -168,7 +174,9 @@ class VideoDanmuProvider { guard dm.time < time else { break } danmuIdx += 1 if dm.aiLevel < Settings.danmuAILevel { continue } - onShowDanmu?(DanmakuTextCellModel(dm: dm)) + let model = DanmakuTextCellModel(dm: dm) + onShowDanmu?(model) + onSendTextModel.send(model) } } } diff --git a/BilibiliLive/Component/Video/VideoDetailViewController.swift b/BilibiliLive/Component/Video/VideoDetailViewController.swift index f9ef9a3f..84a91097 100644 --- a/BilibiliLive/Component/Video/VideoDetailViewController.swift +++ b/BilibiliLive/Component/Video/VideoDetailViewController.swift @@ -147,7 +147,7 @@ class VideoDetailViewController: UIViewController { } else { vc.present(self, animated: false) { [weak self] in guard let self else { return } - let player = VideoPlayerViewController(playInfo: PlayInfo(aid: self.aid, cid: self.cid, epid: self.epid, isBangumi: self.isBangumi)) + let player = NewVideoPlayerViewController(playInfo: PlayInfo(aid: self.aid, cid: self.cid, epid: self.epid, isBangumi: self.isBangumi)) self.present(player, animated: true) } } @@ -354,7 +354,7 @@ class VideoDetailViewController: UIViewController { } @IBAction func actionPlay(_ sender: Any) { - let player = VideoPlayerViewController(playInfo: PlayInfo(aid: aid, cid: cid, epid: epid, isBangumi: isBangumi)) + let player = NewVideoPlayerViewController(playInfo: PlayInfo(aid: aid, cid: cid, epid: epid, isBangumi: isBangumi)) player.data = data if pages.count > 0, let index = pages.firstIndex(where: { $0.cid == cid }) { let seq = pages.dropFirst(index).map({ PlayInfo(aid: aid, cid: $0.cid, epid: $0.epid, isBangumi: isBangumi) }) diff --git a/BilibiliLive/Component/Video/VideoPlayerViewController.swift b/BilibiliLive/Component/Video/VideoPlayerViewController.swift index 18f1510a..a6e94b5f 100644 --- a/BilibiliLive/Component/Video/VideoPlayerViewController.swift +++ b/BilibiliLive/Component/Video/VideoPlayerViewController.swift @@ -65,6 +65,7 @@ class VideoPlayerViewController: CommonPlayerViewController { private let danmuProvider = VideoDanmuProvider() private var clipInfos: [VideoPlayURLInfo.ClipInfo]? private var skipAction: UIAction? + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) guard let currentTime = player?.currentTime().seconds, currentTime > 0 else { return } @@ -95,13 +96,13 @@ class VideoPlayerViewController: CommonPlayerViewController { } } await fetchVideoData() - await danmuProvider.initVideo(cid: playInfo.cid, startPos: playerStartPos ?? 0) + await danmuProvider.initVideo(cid: playInfo.cid!, startPos: playerStartPos ?? 0) } private func playmedia(urlInfo: VideoPlayURLInfo, playerInfo: PlayerInfo?) async { let playURL = URL(string: BilibiliVideoResourceLoaderDelegate.URLs.play)! let headers: [String: String] = [ - "User-Agent": "Bilibili/APPLE TV", + "User-Agent": Keys.userAgent, "Referer": "https://www.bilibili.com/video/av\(playInfo.aid)", ] let asset = AVURLAsset(url: playURL, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) @@ -160,13 +161,13 @@ class VideoPlayerViewController: CommonPlayerViewController { } } - override func playerRateDidChange(player: AVPlayer) { - if player.rate > 0, danMuView.status == .pause { - danMuView.play() - } else if player.rate == 0, danMuView.status == .play { - danMuView.pause() - } - } +// override func playerRateDidChange(player: AVPlayer) { +// if player.rate > 0, danMuView.status == .pause { +// danMuView.play() +// } else if player.rate == 0, danMuView.status == .play { +// danMuView.pause() +// } +// } func playNext() -> Bool { if let next = nextProvider?.getNext() { diff --git a/BilibiliLive/Extensions/String+Error.swift b/BilibiliLive/Extensions/String+Error.swift new file mode 100644 index 00000000..7754b844 --- /dev/null +++ b/BilibiliLive/Extensions/String+Error.swift @@ -0,0 +1,12 @@ +// +// String+Error.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/24. +// + +import Foundation + +extension String: LocalizedError { + public var errorDescription: String? { return self } +} diff --git a/BilibiliLive/Keys.swift b/BilibiliLive/Keys.swift index 197799ac..ff7194b3 100644 --- a/BilibiliLive/Keys.swift +++ b/BilibiliLive/Keys.swift @@ -10,4 +10,7 @@ enum Keys { static let userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15" static let liveReferer = "https://live.bilibili.com" static let referer = "https://www.bilibili.com" + static func referer(for aid: Int) -> String { + return "https://www.bilibili.com/video/av\(aid)" + } } diff --git a/BilibiliLive/Vendor/DanmakuKit/DanmakuView.swift b/BilibiliLive/Vendor/DanmakuKit/DanmakuView.swift index f8645b77..0e0f516a 100644 --- a/BilibiliLive/Vendor/DanmakuKit/DanmakuView.swift +++ b/BilibiliLive/Vendor/DanmakuKit/DanmakuView.swift @@ -433,6 +433,7 @@ public extension DanmakuView { private extension DanmakuView { func recaculateFloatingTracks() { + if viewHeight == 0 { return } let trackCount = Int(floorf(Float((viewHeight - paddingTop - paddingBottom) / trackHeight))) let offsetY = max(0, (viewHeight - CGFloat(trackCount) * trackHeight) / 2.0) let diffFloatingTrackCount = trackCount - floatingTracks.count @@ -458,6 +459,7 @@ private extension DanmakuView { } func recaculateTopTracks() { + if viewHeight == 0 { return } let trackCount = Int(floorf(Float((viewHeight - paddingTop - paddingBottom) / trackHeight))) let offsetY = max(0, (viewHeight - CGFloat(trackCount) * trackHeight) / 2.0) let diffFloatingTrackCount = trackCount - topTracks.count @@ -483,6 +485,7 @@ private extension DanmakuView { } func recaculateBottomTracks() { + if viewHeight == 0 { return } let trackCount = Int(floorf(Float((viewHeight - paddingTop - paddingBottom) / trackHeight))) let offsetY = max(0, (viewHeight - CGFloat(trackCount) * trackHeight) / 2.0) let diffFloatingTrackCount = trackCount - bottomTracks.count From b349c16ad0a271a273f4de1e74f64832a4cc9778 Mon Sep 17 00:00:00 2001 From: yicheng <11733500+yichengchen@users.noreply.github.com> Date: Sat, 25 May 2024 15:43:12 +0800 Subject: [PATCH 2/9] feat: add plugins --- BilibiliLive.xcodeproj/project.pbxproj | 24 ++++ .../Component/Player/DanmuViewPlugin.swift | 3 +- .../Component/Player/DebugPlugin.swift | 124 ++++++++++++++++++ .../Component/Player/MaskViewPugin.swift | 62 +++++++++ .../NewCommonPlayerViewController.swift | 53 +++++++- .../Component/Player/SpeedChangerPlugin.swift | 10 ++ .../Component/Video/BUpnpPlugin.swift | 55 ++++++++ .../Component/Video/BVideoClipsPlugin.swift | 56 ++++++++ .../Component/Video/BVideoInfoPlugin.swift | 122 +++++++++++++++++ .../Component/Video/BVideoPlayPlugin.swift | 63 ++------- .../Video/NewVideoPlayerViewModel.swift | 49 ++++++- BilibiliLive/Request/WebRequest.swift | 8 +- 12 files changed, 562 insertions(+), 67 deletions(-) create mode 100644 BilibiliLive/Component/Player/DebugPlugin.swift create mode 100644 BilibiliLive/Component/Player/MaskViewPugin.swift create mode 100644 BilibiliLive/Component/Player/SpeedChangerPlugin.swift create mode 100644 BilibiliLive/Component/Video/BUpnpPlugin.swift create mode 100644 BilibiliLive/Component/Video/BVideoClipsPlugin.swift create mode 100644 BilibiliLive/Component/Video/BVideoInfoPlugin.swift diff --git a/BilibiliLive.xcodeproj/project.pbxproj b/BilibiliLive.xcodeproj/project.pbxproj index 38b7455b..b041d6c1 100644 --- a/BilibiliLive.xcodeproj/project.pbxproj +++ b/BilibiliLive.xcodeproj/project.pbxproj @@ -38,6 +38,12 @@ 49508E0F2943420100D26812 /* CocoaLumberjack in Frameworks */ = {isa = PBXBuildFile; productRef = 49508E0E2943420100D26812 /* CocoaLumberjack */; }; 49508E112943420100D26812 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 49508E102943420100D26812 /* CocoaLumberjackSwift */; }; 496400D32943431E0098ACA6 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496400D22943431E0098ACA6 /* Logger.swift */; }; + 496E5A4D2C018F150062951B /* BVideoClipsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */; }; + 496E5A4F2C0194720062951B /* MaskViewPugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A4E2C0194720062951B /* MaskViewPugin.swift */; }; + 496E5A512C0194CD0062951B /* BUpnpPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A502C0194CD0062951B /* BUpnpPlugin.swift */; }; + 496E5A532C01B1CA0062951B /* BVideoInfoPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */; }; + 496E5A552C01CDBB0062951B /* DebugPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A542C01CDBB0062951B /* DebugPlugin.swift */; }; + 496E5A572C01CDCA0062951B /* SpeedChangerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */; }; 4973502B29161B770045C26B /* WeeklyWatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4973502A29161B770045C26B /* WeeklyWatchViewController.swift */; }; 4973502D29162A6D0045C26B /* StandardVideoCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4973502C29162A6D0045C26B /* StandardVideoCollectionViewController.swift */; }; 497361082BF1A16600ED213F /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497361072BF1A16600ED213F /* Keys.swift */; }; @@ -169,6 +175,12 @@ 49474212290509F6005D6885 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 4947423A2906B308005D6885 /* BLTextOnlyCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLTextOnlyCollectionViewCell.swift; sourceTree = ""; }; 496400D22943431E0098ACA6 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BVideoClipsPlugin.swift; sourceTree = ""; }; + 496E5A4E2C0194720062951B /* MaskViewPugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskViewPugin.swift; sourceTree = ""; }; + 496E5A502C0194CD0062951B /* BUpnpPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BUpnpPlugin.swift; sourceTree = ""; }; + 496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BVideoInfoPlugin.swift; sourceTree = ""; }; + 496E5A542C01CDBB0062951B /* DebugPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugPlugin.swift; sourceTree = ""; }; + 496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedChangerPlugin.swift; sourceTree = ""; }; 4973502A29161B770045C26B /* WeeklyWatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeeklyWatchViewController.swift; sourceTree = ""; }; 4973502C29162A6D0045C26B /* StandardVideoCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardVideoCollectionViewController.swift; sourceTree = ""; }; 497361072BF1A16600ED213F /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = ""; }; @@ -382,6 +394,9 @@ 492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */, 492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */, 492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */, + 496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */, + 496E5A502C0194CD0062951B /* BUpnpPlugin.swift */, + 496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */, ); path = Video; sourceTree = ""; @@ -644,6 +659,9 @@ 49FB8EE829208EBE0045D5DE /* SidxParseUtil.swift */, 492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */, 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */, + 496E5A4E2C0194720062951B /* MaskViewPugin.swift */, + 496E5A542C01CDBB0062951B /* DebugPlugin.swift */, + 496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */, ); path = Player; sourceTree = ""; @@ -814,7 +832,9 @@ files = ( F9B9EAED261B25E40045C2C6 /* ToViewViewController.swift in Sources */, 4973502B29161B770045C26B /* WeeklyWatchViewController.swift in Sources */, + 496E5A4D2C018F150062951B /* BVideoClipsPlugin.swift in Sources */, AE7A3B20290298BE006FEBB0 /* Colors.swift in Sources */, + 496E5A4F2C0194720062951B /* MaskViewPugin.swift in Sources */, 49FB8EC0291F4C520045D5DE /* VMaskProvider.swift in Sources */, 497361082BF1A16600ED213F /* Keys.swift in Sources */, 49E5F85028AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift in Sources */, @@ -867,7 +887,9 @@ 499C760F2930E068003160FB /* NVASocket.swift in Sources */, 498CF2912B63AABE0009793E /* backward_references_hq.c in Sources */, 494742112905053E005D6885 /* BLMotionCollectionViewCell.swift in Sources */, + 496E5A512C0194CD0062951B /* BUpnpPlugin.swift in Sources */, 494741E7290391A7005D6885 /* BLButton.swift in Sources */, + 496E5A572C01CDCA0062951B /* SpeedChangerPlugin.swift in Sources */, F927ED992610AD8D00EAB8E3 /* LiveViewController.swift in Sources */, 492AD7132C001CA7007221C8 /* String+Error.swift in Sources */, 490EC3E7290CC8F8001E00B6 /* RankingViewController.swift in Sources */, @@ -882,6 +904,8 @@ AEA6AB1928FFF3DD007CE72E /* Settings.swift in Sources */, 498CF2962B63AABE0009793E /* block_splitter.c in Sources */, 494741C6290177BB005D6885 /* UpSpaceViewController.swift in Sources */, + 496E5A532C01B1CA0062951B /* BVideoInfoPlugin.swift in Sources */, + 496E5A552C01CDBB0062951B /* DebugPlugin.swift in Sources */, 49DA01A0296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift in Sources */, 492731EE29096677005F5B0A /* HotViewController.swift in Sources */, F927ED8926103CFB00EAB8E3 /* DanmakuTextCellModel.swift in Sources */, diff --git a/BilibiliLive/Component/Player/DanmuViewPlugin.swift b/BilibiliLive/Component/Player/DanmuViewPlugin.swift index a6e6916d..ad285659 100644 --- a/BilibiliLive/Component/Player/DanmuViewPlugin.swift +++ b/BilibiliLive/Component/Player/DanmuViewPlugin.swift @@ -20,6 +20,8 @@ class DanmuViewPlugin { didSet { danMuView.isHidden = !showDanmu } } + let danMuView = DanmakuView() + init(provider: DanmuProviderProtocol) { danmuProvider = provider provider.onSendTextModel @@ -30,7 +32,6 @@ class DanmuViewPlugin { } private let danmuProvider: DanmuProviderProtocol - private let danMuView = DanmakuView() private var timeObserver: Any? private var cancellable = Set() diff --git a/BilibiliLive/Component/Player/DebugPlugin.swift b/BilibiliLive/Component/Player/DebugPlugin.swift new file mode 100644 index 00000000..c7969dd7 --- /dev/null +++ b/BilibiliLive/Component/Player/DebugPlugin.swift @@ -0,0 +1,124 @@ +// +// DebugPlugin.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/25. +// + +import AVKit +import UIKit + +class DebugPlugin: CommonPlayerPlugin { + private var debugView: UILabel? + private weak var containerView: UIView? + private var debugTimer: Timer? + private weak var player: AVPlayer? + private var debugEnable: Bool { debugTimer?.isValid ?? false } + + var customInfo: String = "" + var additionDebugInfo: (() -> String)? + + func addViewToPlayerOverlay(container: UIView) { + containerView = container + } + + func playerDidChange(player: AVPlayer) { + self.player = player + } + + func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] { + let debugEnableImage = UIImage(systemName: "terminal.fill") + let debugDisableImage = UIImage(systemName: "terminal") + let debugAction = UIAction(title: "Debug", image: debugEnable ? debugEnableImage : debugDisableImage) { + [weak self] action in + guard let self = self else { return } + if self.debugEnable { + self.stopDebug() + action.image = debugDisableImage + } else { + action.image = debugEnableImage + self.startDebug() + } + } + if let setting = current.compactMap({ $0 as? UIMenu }) + .first(where: { $0.identifier == UIMenu.Identifier(rawValue: "setting") }) + { + var child = setting.children + child.append(debugAction) + setting.replacingChildren(child) + return [] + } + return [debugAction] + } + + deinit { + debugTimer?.invalidate() + } + + private func startDebug() { + if debugView == nil { + debugView = UILabel() + debugView?.backgroundColor = UIColor.black.withAlphaComponent(0.8) + debugView?.textColor = UIColor.white + containerView?.addSubview(debugView!) + debugView?.numberOfLines = 0 + debugView?.font = UIFont.systemFont(ofSize: 26) + debugView?.snp.makeConstraints { make in + make.top.equalToSuperview().offset(12) + make.right.equalToSuperview().offset(-12) + make.width.equalTo(800) + } + } + debugView?.isHidden = false + debugTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + let info = self?.fetchDebugInfo() + self?.debugView?.text = info + } + } + + private func stopDebug() { + debugTimer?.invalidate() + debugTimer = nil + debugView?.isHidden = true + } + + private func fetchDebugInfo() -> String { + let bitrateStr: (Double) -> String = { + bit in + String(format: "%.2fMbps", bit / 1024.0 / 1024.0) + } + guard let player else { return "Player no init" } + + var logs = """ + time control status: \(player.timeControlStatus.rawValue) \(player.reasonForWaitingToPlay?.rawValue ?? "") + player status:\(player.status.rawValue) + """ + + guard let log = player.currentItem?.accessLog() else { return logs } + guard let item = log.events.last else { return logs } + let uri = item.uri ?? "" + let addr = item.serverAddress ?? "" + let changes = item.numberOfServerAddressChanges + let dropped = item.numberOfDroppedVideoFrames + let stalls = item.numberOfStalls + let averageAudioBitrate = item.averageAudioBitrate + let averageVideoBitrate = item.averageVideoBitrate + let indicatedBitrate = item.indicatedBitrate + let observedBitrate = item.observedBitrate + logs += """ + uri:\(uri), ip:\(addr), change:\(changes) + drop:\(dropped) stalls:\(stalls) + bitrate audio:\(bitrateStr(averageAudioBitrate)), video: \(bitrateStr(averageVideoBitrate)) + observedBitrate:\(bitrateStr(observedBitrate)) + indicatedAverageBitrate:\(bitrateStr(indicatedBitrate)) + """ + + if let additionDebugInfo = additionDebugInfo?() { + logs = additionDebugInfo + "\n" + logs + } + if customInfo.isEmpty == false { + logs = logs + "\n" + customInfo + } + return logs + } +} diff --git a/BilibiliLive/Component/Player/MaskViewPugin.swift b/BilibiliLive/Component/Player/MaskViewPugin.swift new file mode 100644 index 00000000..ccaeb4b9 --- /dev/null +++ b/BilibiliLive/Component/Player/MaskViewPugin.swift @@ -0,0 +1,62 @@ +// +// MaskViewPugin.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/25. +// + +import AVKit +import UIKit + +class MaskViewPugin: CommonPlayerPlugin { + weak var maskView: UIView? + var maskProvider: MaskProvider + private var observer: Any? + private let queue = DispatchQueue(label: "plugin.mask") + private var videoOutput: AVPlayerItemVideoOutput? + + init(maskView: UIView, maskProvider: MaskProvider) { + self.maskView = maskView + self.maskProvider = maskProvider + } + + func playerDidChange(player: AVPlayer) { + if maskProvider.needVideoOutput() { + setUpOutput(player: player) + } + + let timePerFrame = CMTime(value: 1, timescale: CMTimeScale(maskProvider.preferFPS())) + + observer = player.addPeriodicTimeObserver(forInterval: timePerFrame, queue: queue) { + [weak self] time in + guard let self, let maskView, !maskView.isHidden else { return } + maskProvider.getMask(for: time, frame: maskView.frame) { + [weak maskView] maskLayer in + if Thread.isMainThread { + maskView?.layer.mask = maskLayer + } else { + DispatchQueue.main.async { + maskView?.layer.mask = maskLayer + } + } + } + } + } + + func playerDidCleanUp(player: AVPlayer) { + if let observer { + player.removeTimeObserver(observer) + } + } + + private func setUpOutput(player: AVPlayer) { + guard videoOutput == nil, let videoItem = player.currentItem else { return } + let pixelBuffAttributes = [ + kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + ] + let videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: pixelBuffAttributes) + videoItem.add(videoOutput) + self.videoOutput = videoOutput + maskProvider.setVideoOutout(ouput: videoOutput) + } +} diff --git a/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift b/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift index 87f6d3f8..ccede833 100644 --- a/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift +++ b/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift @@ -13,6 +13,7 @@ protocol CommonPlayerPlugin { func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] func playerDidLoad(playerVC: AVPlayerViewController) + func playerDidDismiss(playerVC: AVPlayerViewController) func playerDidChange(player: AVPlayer) func playerItemDidChange(playerItem: AVPlayerItem) @@ -36,6 +37,7 @@ extension CommonPlayerPlugin { func playerDidCleanUp(player: AVPlayer) {} func playerDidLoad(playerVC: AVPlayerViewController) {} + func playerDidDismiss(playerVC: AVPlayerViewController) {} func playerDidChange(player: AVPlayer) {} func playerItemDidChange(playerItem: AVPlayerItem) {} } @@ -46,13 +48,16 @@ class NewCommonPlayerViewController: UIViewController { private var observations = Set() private var rateObserver: NSKeyValueObservation? private var statusObserver: NSKeyValueObservation? - + private var isEnd = false override func viewDidLoad() { super.viewDidLoad() addChild(playerVC) view.addSubview(playerVC.view) playerVC.didMove(toParent: self) playerVC.view.snp.makeConstraints { $0.edges.equalToSuperview() } + playerVC.allowsPictureInPicturePlayback = true + playerVC.delegate = self + let playerObservation = playerVC.observe(\.player) { [weak self] vc, obs in if let oldPlayer = obs.oldValue, let oldPlayer { self?.activePlugins.forEach { $0.playerDidCleanUp(player: oldPlayer) } @@ -63,6 +68,11 @@ class NewCommonPlayerViewController: UIViewController { activePlugins.forEach { $0.playerDidLoad(playerVC: playerVC) } } + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + activePlugins.forEach { $0.playerDidDismiss(playerVC: playerVC) } + } + override var preferredFocusEnvironments: [UIFocusEnvironment] { return [playerVC.view] } @@ -112,7 +122,9 @@ extension NewCommonPlayerViewController { if player.rate > 0 { activePlugins.forEach { $0.playerDidStart(player: player) } } else if player.rate == 0 { - activePlugins.forEach { $0.playerDidPause(player: player) } + if !isEnd { + activePlugins.forEach { $0.playerDidPause(player: player) } + } } } @@ -122,6 +134,7 @@ extension NewCommonPlayerViewController { guard let self, let player = playerVC.player else { return } switch item.status { case .readyToPlay: + isEnd = false activePlugins.forEach { $0.playerWillStart(player: player) } player.play() case .failed: @@ -133,7 +146,41 @@ extension NewCommonPlayerViewController { NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: .main) { [weak self] note in guard let self, let player = playerVC.player else { return } - self.activePlugins.forEach { $0.playerDidEnd(player: player) } + isEnd = true + activePlugins.forEach { $0.playerDidEnd(player: player) } + } + } +} + + +extension NewCommonPlayerViewController: AVPlayerViewControllerDelegate { + @objc func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool { + if let presentedViewController = UIViewController.topMostViewController() as? AVPlayerViewController, + presentedViewController == playerViewController + { + return true + } + return false + } + + @objc func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool { + return true + } + + @objc func playerViewController(_ playerViewController: AVPlayerViewController, + restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) + { + let presentedViewController = UIViewController.topMostViewController() + if presentedViewController is AVPlayerViewController { + let parent = presentedViewController.presentingViewController + presentedViewController.dismiss(animated: false) { + parent?.present(playerViewController, animated: false) + completionHandler(true) + } + } else { + presentedViewController.present(playerViewController, animated: false) { + completionHandler(true) + } } } } diff --git a/BilibiliLive/Component/Player/SpeedChangerPlugin.swift b/BilibiliLive/Component/Player/SpeedChangerPlugin.swift new file mode 100644 index 00000000..add4ce96 --- /dev/null +++ b/BilibiliLive/Component/Player/SpeedChangerPlugin.swift @@ -0,0 +1,10 @@ +// +// SpeedChangerPlugin.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/25. +// + +import Foundation + +class SpeedChangerPlugin: CommonPlayerPlugin {} diff --git a/BilibiliLive/Component/Video/BUpnpPlugin.swift b/BilibiliLive/Component/Video/BUpnpPlugin.swift new file mode 100644 index 00000000..33a9a277 --- /dev/null +++ b/BilibiliLive/Component/Video/BUpnpPlugin.swift @@ -0,0 +1,55 @@ +// +// BUpnpPlugin.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/25. +// + +import AVFoundation +import Foundation +class BUpnpPlugin: CommonPlayerPlugin { + let duration: Int? + + init(duration: Int?) { + self.duration = duration + } + + func playerWillStart(player: AVPlayer) { + guard let duration else { return } + player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 5, preferredTimescale: 1), queue: .global()) { time in + DispatchQueue.main.async { + BiliBiliUpnpDMR.shared.sendProgress(duration: duration, current: Int(time.seconds)) + } + } + } + + func playerDidStart(player: AVPlayer) { + DispatchQueue.main.async { + BiliBiliUpnpDMR.shared.sendStatus(status: .playing) + } + } + + func playerDidPause(player: AVPlayer) { + DispatchQueue.main.async { + BiliBiliUpnpDMR.shared.sendStatus(status: .paused) + } + } + + func playerDidEnd(player: AVPlayer) { + DispatchQueue.main.async { + BiliBiliUpnpDMR.shared.sendStatus(status: .end) + } + } + + func playerDidFail(player: AVPlayer) { + DispatchQueue.main.async { + BiliBiliUpnpDMR.shared.sendStatus(status: .stop) + } + } + + func playerDidCleanUp(player: AVPlayer) { + DispatchQueue.main.async { + BiliBiliUpnpDMR.shared.sendStatus(status: .stop) + } + } +} diff --git a/BilibiliLive/Component/Video/BVideoClipsPlugin.swift b/BilibiliLive/Component/Video/BVideoClipsPlugin.swift new file mode 100644 index 00000000..b1c397e2 --- /dev/null +++ b/BilibiliLive/Component/Video/BVideoClipsPlugin.swift @@ -0,0 +1,56 @@ +// +// BVideoClipsPlugin.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/25. +// + +import AVKit + +class BVideoClipsPlugin: CommonPlayerPlugin { + let clipInfos: [VideoPlayURLInfo.ClipInfo] + + private var observers = [Any]() + private weak var playerVC: AVPlayerViewController? + + init(clipInfos: [VideoPlayURLInfo.ClipInfo]) { + self.clipInfos = clipInfos + } + + func playerDidLoad(playerVC: AVPlayerViewController) { + self.playerVC = playerVC + } + + func playerWillStart(player: AVPlayer) { + for clip in clipInfos { + let start = CMTime(seconds: clip.start, preferredTimescale: 1) + let end = CMTime(seconds: clip.end, preferredTimescale: 1) + let startObserver = player.addBoundaryTimeObserver(forTimes: [NSValue(time: start)], queue: .main) { + [weak player, weak self] in + let action = { + clip.skipped = true + player?.seek(to: CMTime(seconds: Double(clip.end), preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) + } + if clip.skipped == true, Settings.autoSkip { + action() + } else { + let action = UIAction(title: clip.customText) { _ in action() } + self?.playerVC?.contextualActions = [action] + } + } + observers.append(startObserver) + + let endObserver = player.addBoundaryTimeObserver(forTimes: [NSValue(time: end)], queue: .main) { + [weak self] in + self?.playerVC?.contextualActions = [] + } + observers.append(endObserver) + } + } + + func playerDidCleanUp(player: AVPlayer) { + for observer in observers { + player.removeTimeObserver(observer) + } + } +} diff --git a/BilibiliLive/Component/Video/BVideoInfoPlugin.swift b/BilibiliLive/Component/Video/BVideoInfoPlugin.swift new file mode 100644 index 00000000..c819f2a1 --- /dev/null +++ b/BilibiliLive/Component/Video/BVideoInfoPlugin.swift @@ -0,0 +1,122 @@ +// +// BVideoInfoPlugin.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/25. +// + +import AVKit +import Kingfisher + +class BVideoInfoPlugin: CommonPlayerPlugin { + let title: String? + let subTitle: String? + let desp: String? + let pic: URL? + let viewPoints: [PlayerInfo.ViewPoint]? + + init(title: String?, subTitle: String?, desp: String?, pic: URL?, viewPoints: [PlayerInfo.ViewPoint]?) { + self.title = title + self.subTitle = subTitle + self.desp = desp + self.pic = pic + self.viewPoints = viewPoints + } + + func playerWillStart(player: AVPlayer) { + Task { + async let info: () = setPlayerInfo(title: title, subTitle: subTitle, desp: desp, pic: pic, player: player) + if let viewPoints { + async let vp: () = updatePlayerCharpter(viewPoints: viewPoints, player: player) + await vp + } + await info + } + } + + private func updatePlayerCharpter(viewPoints: [PlayerInfo.ViewPoint], player: AVPlayer) async { + _ = await withTaskGroup(of: Void.self) { group in + for viewPoint in viewPoints { + group.addTask { + if let pic = viewPoint.imgUrl?.addSchemeIfNeed(), + let result = try? await KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: pic)), + let data = result.image.pngData() + { + viewPoint.imageData = data + } + } + } + return group + } + + let metas = viewPoints.compactMap { convertTimedMetadataGroup(viewPoint: $0) } + + player.currentItem?.navigationMarkerGroups = [AVNavigationMarkersGroup(title: nil, timedNavigationMarkers: metas)] + } + + private func setPlayerInfo(title: String?, subTitle: String?, desp: String?, pic: URL?, player: AVPlayer) async { + let desp = desp?.components(separatedBy: "\n").joined(separator: " ") + let mapping: [AVMetadataIdentifier: Any?] = [ + .commonIdentifierTitle: title, + .iTunesMetadataTrackSubTitle: subTitle, + .commonIdentifierDescription: desp, + ] + var metas = mapping.compactMap { createMetadataItem(for: $0, value: $1) } + + player.currentItem?.externalMetadata = metas + + if let pic = pic, + let resource = try? await KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: pic)), + let data = resource.image.pngData(), + let item = createMetadataItem(for: .commonIdentifierArtwork, value: data) + { + metas.append(item) + player.currentItem?.externalMetadata = metas + } + } + + private func createMetadataItem(for identifier: AVMetadataIdentifier, value: Any?) -> AVMetadataItem? { + if value == nil { return nil } + let item = AVMutableMetadataItem() + item.identifier = identifier + item.value = value as? NSCopying & NSObjectProtocol + // Specify "und" to indicate an undefined language. + item.extendedLanguageTag = "und" + return item.copy() as? AVMetadataItem + } + + private func convertTimedMetadataGroup(viewPoint: PlayerInfo.ViewPoint) -> AVTimedMetadataGroup { + let mapping: [AVMetadataIdentifier: Any?] = [ + .commonIdentifierTitle: viewPoint.content, + ] + var metadatas = mapping.compactMap { createMetadataItem(for: $0, value: $1) } + let timescale: Int32 = 600 + let cmStartTime = CMTimeMakeWithSeconds(viewPoint.from, preferredTimescale: timescale) + let cmEndTime = CMTimeMakeWithSeconds(viewPoint.to, preferredTimescale: timescale) + let timeRange = CMTimeRangeFromTimeToTime(start: cmStartTime, end: cmEndTime) + if let imageData = viewPoint.imageData, + let item = createMetadataItem(for: .commonIdentifierArtwork, value: imageData) + { + metadatas.append(item) + } + + return AVTimedMetadataGroup(items: metadatas, timeRange: timeRange) + } +} + +extension KingfisherManager { + func retrieveImage(with resource: Resource, + options: KingfisherOptionsInfo? = nil) async throws -> RetrieveImageResult + { + try await withCheckedThrowingContinuation { conf in + retrieveImage(with: resource, options: options) { result in + switch result { + case let .success(result): + conf.resume(returning: result) + case let .failure(err): + conf.resume(throwing: err) + } + } + } + } +} diff --git a/BilibiliLive/Component/Video/BVideoPlayPlugin.swift b/BilibiliLive/Component/Video/BVideoPlayPlugin.swift index ae47c572..27d38e6e 100644 --- a/BilibiliLive/Component/Video/BVideoPlayPlugin.swift +++ b/BilibiliLive/Component/Video/BVideoPlayPlugin.swift @@ -18,30 +18,21 @@ class BVideoPlayPlugin: CommonPlayerPlugin { func playerDidLoad(playerVC: AVPlayerViewController) { self.playerVC = playerVC + playerVC.appliesPreferredDisplayCriteriaAutomatically = Settings.contentMatch Task { try? await playmedia(urlInfo: playData.videoPlayURLInfo, playerInfo: playData.playerInfo) } } - private func updatePlayerInfoView(aid: Int) async { -// if data == nil { -// data = try? await WebRequest.requestDetailVideo(aid: aid) -// } -// setPlayerInfo(title: data?.title, subTitle: data?.ownerName, desp: data?.View.desc, pic: data?.pic) + func playerWillStart(player: AVPlayer) { + if let playerStartPos = playData.playerStartPos { + player.seek(to: CMTime(seconds: Double(playerStartPos), preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) + } } - private func setupDanmuMask() { - // if Settings.danmuMask { - // if let mask = info?.dm_mask, - // let video = playData.dash.video.first, - // let fps = info?.dm_mask?.fps, fps > 0 - // { - // maskProvider = BMaskProvider(info: mask, videoSize: CGSize(width: video.width ?? 0, height: video.height ?? 0)) - // } else if Settings.vnMask { - // maskProvider = VMaskProvider() - // } - // setupMask() - // } + func playerDidDismiss(playerVC: AVPlayerViewController) { + guard let currentTime = playerVC.player?.currentTime().seconds, currentTime > 0 else { return } + WebRequest.reportWatchHistory(aid: playData.cid, cid: playData.cid, currentTime: Int(currentTime)) } @MainActor @@ -71,44 +62,6 @@ class BVideoPlayPlugin: CommonPlayerPlugin { func prepare(toPlay asset: AVURLAsset) async { let playerItem = AVPlayerItem(asset: asset) let player = AVPlayer(playerItem: playerItem) -// player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), queue: .main) { [weak self] time in -// guard let self else { return } - //// if self.danMuView.isHidden { return } -// let seconds = time.seconds - //// self.danmuProvider.playerTimeChange(time: seconds) -// -// if let duration = self.data?.View.duration { -// BiliBiliUpnpDMR.shared.sendProgress(duration: duration, current: Int(seconds)) -// } -// -// if let clipInfos = self.clipInfos { -// var matched = false -// for clip in clipInfos { -// if seconds > clip.start, seconds < clip.end { -// let action = { -// clip.skipped = true -// self.player?.seek(to: CMTime(seconds: Double(clip.end), preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) -// } -// if !(clip.skipped ?? false), Settings.autoSkip { -// action() -// self.skipAction = nil -// } else if self.skipAction?.accessibilityLabel != clip.a11Tag { -// self.skipAction = UIAction(title: clip.customText) { _ in -// action() -// } -// self.skipAction?.accessibilityLabel = clip.a11Tag -// } -// -// self.contextualActions = [self.skipAction].compactMap { $0 } -// matched = true -// break -// } -// } -// if !matched { -// self.contextualActions = [] -// } -// } -// } if let defaultRate = playerVC?.player?.defaultRate, let speed = PlaySpeed.blDefaults.first(where: { $0.value == defaultRate }) diff --git a/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift b/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift index b8e5e86c..1526d0c3 100644 --- a/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift +++ b/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift @@ -26,7 +26,7 @@ class NewVideoPlayerViewModel { private var playInfo: PlayInfo private let danmuProvider = VideoDanmuProvider() - + private var videoDetail: VideoDetail? init(playInfo: PlayInfo) { self.playInfo = playInfo } @@ -51,14 +51,22 @@ class NewVideoPlayerViewModel { BiliBiliUpnpDMR.shared.sendVideoSwitch(aid: playInfo.aid, cid: playInfo.cid ?? 0) } + private func updateVideoDetailIfNeeded() async { + if videoDetail == nil { + videoDetail = try? await WebRequest.requestDetailVideo(aid: playInfo.aid) + } + } + private func fetchVideoData() async throws -> PlayerDetailData { assert(playInfo.isCidVaild) let aid = playInfo.aid let cid = playInfo.cid! - let info = try? await WebRequest.requestPlayerInfo(aid: aid, cid: cid) + async let infoReq = try? WebRequest.requestPlayerInfo(aid: aid, cid: cid) + async let detailUpdate: () = updateVideoDetailIfNeeded() do { let playData: VideoPlayURLInfo var clipInfos: [VideoPlayURLInfo.ClipInfo]? + if playInfo.isBangumi { playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid) clipInfos = playData.clip_info_list @@ -66,7 +74,10 @@ class NewVideoPlayerViewModel { playData = try await WebRequest.requestPlayUrl(aid: aid, cid: cid) } - var detail = PlayerDetailData(aid: playInfo.aid, cid: playInfo.cid!, epid: playInfo.epid, isBangumi: playInfo.isBangumi, clips: clipInfos, playerInfo: info, videoPlayURLInfo: playData) + let info = await infoReq + _ = await detailUpdate + + var detail = PlayerDetailData(aid: playInfo.aid, cid: playInfo.cid!, epid: playInfo.epid, isBangumi: playInfo.isBangumi, detail: videoDetail, clips: clipInfos, playerInfo: info, videoPlayURLInfo: playData) if let info, info.last_play_cid == cid, playData.dash.duration - info.playTimeInSecond > 5, Settings.continuePlay { detail.playerStartPos = info.playTimeInSecond @@ -74,8 +85,6 @@ class NewVideoPlayerViewModel { return detail - // updatePlayerCharpter(playerInfo: playerInfo) - } catch let err { if case let .statusFail(code, message) = err as? RequestError { if code == -404 || code == -10403 { @@ -88,7 +97,7 @@ class NewVideoPlayerViewModel { // } } throw "\(code) \(message),可能需要大会员" - } else if info?.is_upower_exclusive == true { + } else if await infoReq?.is_upower_exclusive == true { throw "该视频为充电专属视频 \(err)" } else { throw err @@ -99,7 +108,33 @@ class NewVideoPlayerViewModel { @MainActor private func generatePlayerPlugin(_ data: PlayerDetailData) async -> [CommonPlayerPlugin] { let player = BVideoPlayPlugin(detailData: data) let danmu = DanmuViewPlugin(provider: danmuProvider) + let upnp = BUpnpPlugin(duration: data.detail?.View.duration) + let debug = DebugPlugin() + var plugins: [CommonPlayerPlugin] = [player, danmu, upnp, debug] + + if let clips = data.clips { + let clip = BVideoClipsPlugin(clipInfos: clips) + plugins.append(clip) + } + + if Settings.danmuMask { + if let mask = data.playerInfo?.dm_mask, + let video = data.videoPlayURLInfo.dash.video.first, + mask.fps > 0 + { + let maskProvider = BMaskProvider(info: mask, videoSize: CGSize(width: video.width ?? 0, height: video.height ?? 0)) + plugins.append(MaskViewPugin(maskView: danmu.danMuView, maskProvider: maskProvider)) + } else if Settings.vnMask { + let maskProvider = VMaskProvider() + plugins.append(MaskViewPugin(maskView: danmu.danMuView, maskProvider: maskProvider)) + } + } + + if let detail = data.detail { + let info = BVideoInfoPlugin(title: detail.title, subTitle: detail.ownerName, desp: detail.View.desc, pic: detail.pic, viewPoints: data.playerInfo?.view_points) + plugins.append(info) + } - return [player, danmu] + return plugins } } diff --git a/BilibiliLive/Request/WebRequest.swift b/BilibiliLive/Request/WebRequest.swift index 431b67ec..67e2e463 100644 --- a/BilibiliLive/Request/WebRequest.swift +++ b/BilibiliLive/Request/WebRequest.swift @@ -842,12 +842,18 @@ struct PlayerInfo: Codable { last_play_time / 1000 } - struct ViewPoint: Codable { + class ViewPoint: Codable { let type: Int let from: TimeInterval let to: TimeInterval let content: String let imgUrl: URL? + + var imageData: Data? + + enum CodingKeys: String, CodingKey { + case type, from, to, content, imgUrl + } } struct MaskInfo: Codable { From 75e6540c3ba980732c512e5a6c71ce9486fa9789 Mon Sep 17 00:00:00 2001 From: yicheng <11733500+yichengchen@users.noreply.github.com> Date: Sat, 25 May 2024 19:22:28 +0800 Subject: [PATCH 3/9] feat: support pip --- BilibiliLive.xcodeproj/project.pbxproj | 4 ++ .../Component/Player/CommonPlayerPlugin.swift | 43 ++++++++++++ .../NewCommonPlayerViewController.swift | 67 +++++++------------ .../Component/Video/BVideoPlayPlugin.swift | 2 +- 4 files changed, 73 insertions(+), 43 deletions(-) create mode 100644 BilibiliLive/Component/Player/CommonPlayerPlugin.swift diff --git a/BilibiliLive.xcodeproj/project.pbxproj b/BilibiliLive.xcodeproj/project.pbxproj index b041d6c1..4c0c218c 100644 --- a/BilibiliLive.xcodeproj/project.pbxproj +++ b/BilibiliLive.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ 499C76162931A7AE003160FB /* SwiftyXMLParser in Frameworks */ = {isa = PBXBuildFile; productRef = 499C76152931A7AE003160FB /* SwiftyXMLParser */; }; 49A441CD293F6DFD0007606C /* FollowUpsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A441CC293F6DFD0007606C /* FollowUpsViewController.swift */; }; 49D39F28263AD40000F14497 /* WebRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D39F27263AD40000F14497 /* WebRequest.swift */; }; + 49D6A7122C0200ED0084A5A7 /* CommonPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */; }; 49DA01A0296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */; }; 49E5F85028AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */; }; 49F9186E2931E3C9001D3EC3 /* DLNAInfo.xml in Resources */ = {isa = PBXBuildFile; fileRef = 49F9186D2931E3C9001D3EC3 /* DLNAInfo.xml */; }; @@ -274,6 +275,7 @@ 499C760E2930E068003160FB /* NVASocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NVASocket.swift; sourceTree = ""; }; 49A441CC293F6DFD0007606C /* FollowUpsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowUpsViewController.swift; sourceTree = ""; }; 49D39F27263AD40000F14497 /* WebRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRequest.swift; sourceTree = ""; }; + 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerPlugin.swift; sourceTree = ""; }; 49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVInfoPanelCollectionViewThumbnailCell+Hook.swift"; sourceTree = ""; }; 49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BilibiliVideoResourceLoaderDelegate.swift; sourceTree = ""; }; 49F9186D2931E3C9001D3EC3 /* DLNAInfo.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = DLNAInfo.xml; sourceTree = ""; }; @@ -662,6 +664,7 @@ 496E5A4E2C0194720062951B /* MaskViewPugin.swift */, 496E5A542C01CDBB0062951B /* DebugPlugin.swift */, 496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */, + 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */, ); path = Player; sourceTree = ""; @@ -868,6 +871,7 @@ F927ED8D2610A5A800EAB8E3 /* LoginViewController.swift in Sources */, F9B9EAEA261B15020045C2C6 /* FeedCollectionViewController.swift in Sources */, 0A41EE1D2A63102B0066444C /* dmView.pb.swift in Sources */, + 49D6A7122C0200ED0084A5A7 /* CommonPlayerPlugin.swift in Sources */, 490425F729AB54B200CDBC60 /* CategoryViewController.swift in Sources */, 4973502D29162A6D0045C26B /* StandardVideoCollectionViewController.swift in Sources */, F927ED752610395300EAB8E3 /* DanmakuAsyncLayer.swift in Sources */, diff --git a/BilibiliLive/Component/Player/CommonPlayerPlugin.swift b/BilibiliLive/Component/Player/CommonPlayerPlugin.swift new file mode 100644 index 00000000..8c1cffb7 --- /dev/null +++ b/BilibiliLive/Component/Player/CommonPlayerPlugin.swift @@ -0,0 +1,43 @@ +// +// CommonPlayerPlugin.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/25. +// + +import AVKit +import UIKit + +protocol CommonPlayerPlugin { + func addViewToPlayerOverlay(container: UIView) + func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] + + func playerDidLoad(playerVC: AVPlayerViewController) + func playerDidDismiss(playerVC: AVPlayerViewController) + func playerDidChange(player: AVPlayer) + func playerItemDidChange(playerItem: AVPlayerItem) + + func playerWillStart(player: AVPlayer) + func playerDidStart(player: AVPlayer) + func playerDidPause(player: AVPlayer) + func playerDidEnd(player: AVPlayer) + func playerDidFail(player: AVPlayer) + func playerDidCleanUp(player: AVPlayer) +} + +extension CommonPlayerPlugin { + func addViewToPlayerOverlay(container: UIView) {} + func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] { return [] } + + func playerWillStart(player: AVPlayer) {} + func playerDidStart(player: AVPlayer) {} + func playerDidPause(player: AVPlayer) {} + func playerDidEnd(player: AVPlayer) {} + func playerDidFail(player: AVPlayer) {} + func playerDidCleanUp(player: AVPlayer) {} + + func playerDidLoad(playerVC: AVPlayerViewController) {} + func playerDidDismiss(playerVC: AVPlayerViewController) {} + func playerDidChange(player: AVPlayer) {} + func playerItemDidChange(playerItem: AVPlayerItem) {} +} diff --git a/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift b/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift index ccede833..cebbe6e4 100644 --- a/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift +++ b/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift @@ -8,40 +8,6 @@ import AVKit import UIKit -protocol CommonPlayerPlugin { - func addViewToPlayerOverlay(container: UIView) - func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] - - func playerDidLoad(playerVC: AVPlayerViewController) - func playerDidDismiss(playerVC: AVPlayerViewController) - func playerDidChange(player: AVPlayer) - func playerItemDidChange(playerItem: AVPlayerItem) - - func playerWillStart(player: AVPlayer) - func playerDidStart(player: AVPlayer) - func playerDidPause(player: AVPlayer) - func playerDidEnd(player: AVPlayer) - func playerDidFail(player: AVPlayer) - func playerDidCleanUp(player: AVPlayer) -} - -extension CommonPlayerPlugin { - func addViewToPlayerOverlay(container: UIView) {} - func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] { return [] } - - func playerWillStart(player: AVPlayer) {} - func playerDidStart(player: AVPlayer) {} - func playerDidPause(player: AVPlayer) {} - func playerDidEnd(player: AVPlayer) {} - func playerDidFail(player: AVPlayer) {} - func playerDidCleanUp(player: AVPlayer) {} - - func playerDidLoad(playerVC: AVPlayerViewController) {} - func playerDidDismiss(playerVC: AVPlayerViewController) {} - func playerDidChange(player: AVPlayer) {} - func playerItemDidChange(playerItem: AVPlayerItem) {} -} - class NewCommonPlayerViewController: UIViewController { private let playerVC = AVPlayerViewController() private var activePlugins = [CommonPlayerPlugin]() @@ -57,7 +23,7 @@ class NewCommonPlayerViewController: UIViewController { playerVC.view.snp.makeConstraints { $0.edges.equalToSuperview() } playerVC.allowsPictureInPicturePlayback = true playerVC.delegate = self - + let playerObservation = playerVC.observe(\.player) { [weak self] vc, obs in if let oldPlayer = obs.oldValue, let oldPlayer { self?.activePlugins.forEach { $0.playerDidCleanUp(player: oldPlayer) } @@ -152,13 +118,13 @@ extension NewCommonPlayerViewController { } } - extension NewCommonPlayerViewController: AVPlayerViewControllerDelegate { @objc func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool { - if let presentedViewController = UIViewController.topMostViewController() as? AVPlayerViewController, - presentedViewController == playerViewController + if let presentedViewController = UIViewController.topMostViewController() as? NewCommonPlayerViewController, + presentedViewController.playerVC == playerViewController { - return true + dismiss(animated: true) + return false } return false } @@ -167,20 +133,37 @@ extension NewCommonPlayerViewController: AVPlayerViewControllerDelegate { return true } + func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) { + PipRecorder.shared.playingPipViewController.append(self) + } + + func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { + PipRecorder.shared.playingPipViewController.removeAll { $0.playerVC == playerViewController } + } + @objc func playerViewController(_ playerViewController: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { let presentedViewController = UIViewController.topMostViewController() - if presentedViewController is AVPlayerViewController { + guard let containerPlayer = PipRecorder.shared.playingPipViewController.first(where: { $0.playerVC == playerViewController }) else { + completionHandler(false) + return + } + if presentedViewController is NewCommonPlayerViewController { let parent = presentedViewController.presentingViewController presentedViewController.dismiss(animated: false) { - parent?.present(playerViewController, animated: false) + parent?.present(containerPlayer, animated: false) completionHandler(true) } } else { - presentedViewController.present(playerViewController, animated: false) { + presentedViewController.present(containerPlayer, animated: false) { completionHandler(true) } } } + + class PipRecorder { + static let shared = PipRecorder() + var playingPipViewController = [NewCommonPlayerViewController]() + } } diff --git a/BilibiliLive/Component/Video/BVideoPlayPlugin.swift b/BilibiliLive/Component/Video/BVideoPlayPlugin.swift index 27d38e6e..e44b6a17 100644 --- a/BilibiliLive/Component/Video/BVideoPlayPlugin.swift +++ b/BilibiliLive/Component/Video/BVideoPlayPlugin.swift @@ -32,7 +32,7 @@ class BVideoPlayPlugin: CommonPlayerPlugin { func playerDidDismiss(playerVC: AVPlayerViewController) { guard let currentTime = playerVC.player?.currentTime().seconds, currentTime > 0 else { return } - WebRequest.reportWatchHistory(aid: playData.cid, cid: playData.cid, currentTime: Int(currentTime)) + WebRequest.reportWatchHistory(aid: playData.aid, cid: playData.cid, currentTime: Int(currentTime)) } @MainActor From 0c0a9f3296e78a3d93ed769d612c821d13a2cc2a Mon Sep 17 00:00:00 2001 From: yicheng <11733500+yichengchen@users.noreply.github.com> Date: Sun, 26 May 2024 21:31:20 +0800 Subject: [PATCH 4/9] feat: add playlist and speed changer plugin --- BilibiliLive.xcodeproj/project.pbxproj | 4 ++ .../Component/Player/CommonPlayerPlugin.swift | 6 +- .../Player/CommonPlayerViewController.swift | 4 +- .../Component/Player/DanmuViewPlugin.swift | 5 +- .../Component/Player/DebugPlugin.swift | 8 ++- .../Component/Player/MaskViewPugin.swift | 2 +- .../NewCommonPlayerViewController.swift | 10 ++- .../Component/Player/SpeedChangerPlugin.swift | 38 +++++++++++- .../Component/Video/BUpnpPlugin.swift | 2 +- .../Component/Video/BVideoClipsPlugin.swift | 2 +- .../Component/Video/BVideoInfoPlugin.swift | 2 +- .../Component/Video/BVideoPlayPlugin.swift | 13 +--- .../Video/NewVideoPlayerViewController.swift | 8 ++- .../Video/NewVideoPlayerViewModel.swift | 53 ++++++++++++++-- .../Component/Video/VideoPlayListPlugin.swift | 61 +++++++++++++++++++ 15 files changed, 186 insertions(+), 32 deletions(-) create mode 100644 BilibiliLive/Component/Video/VideoPlayListPlugin.swift diff --git a/BilibiliLive.xcodeproj/project.pbxproj b/BilibiliLive.xcodeproj/project.pbxproj index 4c0c218c..b268b33b 100644 --- a/BilibiliLive.xcodeproj/project.pbxproj +++ b/BilibiliLive.xcodeproj/project.pbxproj @@ -83,6 +83,7 @@ 49A441CD293F6DFD0007606C /* FollowUpsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A441CC293F6DFD0007606C /* FollowUpsViewController.swift */; }; 49D39F28263AD40000F14497 /* WebRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D39F27263AD40000F14497 /* WebRequest.swift */; }; 49D6A7122C0200ED0084A5A7 /* CommonPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */; }; + 49D6A7142C0363F10084A5A7 /* VideoPlayListPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */; }; 49DA01A0296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */; }; 49E5F85028AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */; }; 49F9186E2931E3C9001D3EC3 /* DLNAInfo.xml in Resources */ = {isa = PBXBuildFile; fileRef = 49F9186D2931E3C9001D3EC3 /* DLNAInfo.xml */; }; @@ -276,6 +277,7 @@ 49A441CC293F6DFD0007606C /* FollowUpsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowUpsViewController.swift; sourceTree = ""; }; 49D39F27263AD40000F14497 /* WebRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRequest.swift; sourceTree = ""; }; 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerPlugin.swift; sourceTree = ""; }; + 49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayListPlugin.swift; sourceTree = ""; }; 49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVInfoPanelCollectionViewThumbnailCell+Hook.swift"; sourceTree = ""; }; 49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BilibiliVideoResourceLoaderDelegate.swift; sourceTree = ""; }; 49F9186D2931E3C9001D3EC3 /* DLNAInfo.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = DLNAInfo.xml; sourceTree = ""; }; @@ -399,6 +401,7 @@ 496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */, 496E5A502C0194CD0062951B /* BUpnpPlugin.swift */, 496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */, + 49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */, ); path = Video; sourceTree = ""; @@ -903,6 +906,7 @@ 49389D8C28B0A84500B9DAFD /* PersonalViewController.swift in Sources */, 498CF2992B63AABE0009793E /* cluster.c in Sources */, 498CF2952B63AABE0009793E /* compress_fragment_two_pass.c in Sources */, + 49D6A7142C0363F10084A5A7 /* VideoPlayListPlugin.swift in Sources */, F927ED772610395300EAB8E3 /* DanmakuQueuePool.swift in Sources */, 49A441CD293F6DFD0007606C /* FollowUpsViewController.swift in Sources */, AEA6AB1928FFF3DD007CE72E /* Settings.swift in Sources */, diff --git a/BilibiliLive/Component/Player/CommonPlayerPlugin.swift b/BilibiliLive/Component/Player/CommonPlayerPlugin.swift index 8c1cffb7..4841d860 100644 --- a/BilibiliLive/Component/Player/CommonPlayerPlugin.swift +++ b/BilibiliLive/Component/Player/CommonPlayerPlugin.swift @@ -8,9 +8,9 @@ import AVKit import UIKit -protocol CommonPlayerPlugin { +protocol CommonPlayerPlugin: NSObject { func addViewToPlayerOverlay(container: UIView) - func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] + func addMenuItems(current: inout [UIMenuElement]) -> [UIMenuElement] func playerDidLoad(playerVC: AVPlayerViewController) func playerDidDismiss(playerVC: AVPlayerViewController) @@ -27,7 +27,7 @@ protocol CommonPlayerPlugin { extension CommonPlayerPlugin { func addViewToPlayerOverlay(container: UIView) {} - func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] { return [] } + func addMenuItems(current: inout [UIMenuElement]) -> [UIMenuElement] { return [] } func playerWillStart(player: AVPlayer) {} func playerDidStart(player: AVPlayer) {} diff --git a/BilibiliLive/Component/Player/CommonPlayerViewController.swift b/BilibiliLive/Component/Player/CommonPlayerViewController.swift index 786097ea..da9ebb62 100644 --- a/BilibiliLive/Component/Player/CommonPlayerViewController.swift +++ b/BilibiliLive/Component/Player/CommonPlayerViewController.swift @@ -409,7 +409,9 @@ struct PlaySpeed { var value: Float } -extension PlaySpeed { +extension PlaySpeed: Equatable { + static let `default` = PlaySpeed(name: "1X", value: 1) + static let blDefaults = [ PlaySpeed(name: "0.5X", value: 0.5), PlaySpeed(name: "0.75X", value: 0.75), diff --git a/BilibiliLive/Component/Player/DanmuViewPlugin.swift b/BilibiliLive/Component/Player/DanmuViewPlugin.swift index ad285659..d1289ab3 100644 --- a/BilibiliLive/Component/Player/DanmuViewPlugin.swift +++ b/BilibiliLive/Component/Player/DanmuViewPlugin.swift @@ -15,7 +15,7 @@ protocol DanmuProviderProtocol { func playerTimeChange(time: TimeInterval) } -class DanmuViewPlugin { +class DanmuViewPlugin: NSObject { var showDanmu = Settings.defaultDanmuStatus { didSet { danMuView.isHidden = !showDanmu } } @@ -24,6 +24,7 @@ class DanmuViewPlugin { init(provider: DanmuProviderProtocol) { danmuProvider = provider + super.init() provider.onSendTextModel .receive(on: DispatchQueue.main) .sink { [weak self] in @@ -80,7 +81,7 @@ extension DanmuViewPlugin: CommonPlayerPlugin { danMuView.pause() } - func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] { + func addMenuItems(current: inout [UIMenuElement]) -> [UIMenuElement] { let danmuImage = UIImage(systemName: "list.bullet.rectangle.fill") let danmuImageDisable = UIImage(systemName: "list.bullet.rectangle") let danmuAction = UIAction(title: "Show Danmu", image: danMuView.isHidden ? danmuImageDisable : danmuImage) { diff --git a/BilibiliLive/Component/Player/DebugPlugin.swift b/BilibiliLive/Component/Player/DebugPlugin.swift index c7969dd7..27880059 100644 --- a/BilibiliLive/Component/Player/DebugPlugin.swift +++ b/BilibiliLive/Component/Player/DebugPlugin.swift @@ -8,7 +8,7 @@ import AVKit import UIKit -class DebugPlugin: CommonPlayerPlugin { +class DebugPlugin: NSObject, CommonPlayerPlugin { private var debugView: UILabel? private weak var containerView: UIView? private var debugTimer: Timer? @@ -26,7 +26,7 @@ class DebugPlugin: CommonPlayerPlugin { self.player = player } - func addMenuItems(current: [UIMenuElement]) -> [UIMenuElement] { + func addMenuItems(current: inout [UIMenuElement]) -> [UIMenuElement] { let debugEnableImage = UIImage(systemName: "terminal.fill") let debugDisableImage = UIImage(systemName: "terminal") let debugAction = UIAction(title: "Debug", image: debugEnable ? debugEnableImage : debugDisableImage) { @@ -45,7 +45,9 @@ class DebugPlugin: CommonPlayerPlugin { { var child = setting.children child.append(debugAction) - setting.replacingChildren(child) + if let index = current.firstIndex(of: setting) { + current[index] = setting.replacingChildren(child) + } return [] } return [debugAction] diff --git a/BilibiliLive/Component/Player/MaskViewPugin.swift b/BilibiliLive/Component/Player/MaskViewPugin.swift index ccaeb4b9..3c8181ce 100644 --- a/BilibiliLive/Component/Player/MaskViewPugin.swift +++ b/BilibiliLive/Component/Player/MaskViewPugin.swift @@ -8,7 +8,7 @@ import AVKit import UIKit -class MaskViewPugin: CommonPlayerPlugin { +class MaskViewPugin: NSObject, CommonPlayerPlugin { weak var maskView: UIView? var maskProvider: MaskProvider private var observer: Any? diff --git a/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift b/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift index cebbe6e4..72bdaed4 100644 --- a/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift +++ b/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift @@ -15,6 +15,7 @@ class NewCommonPlayerViewController: UIViewController { private var rateObserver: NSKeyValueObservation? private var statusObserver: NSKeyValueObservation? private var isEnd = false + override func viewDidLoad() { super.viewDidLoad() addChild(playerVC) @@ -49,6 +50,12 @@ class NewCommonPlayerViewController: UIViewController { plugin.playerDidLoad(playerVC: playerVC) } + func removePlugin(plugin: CommonPlayerPlugin) { + activePlugins.removeAll { $0 == plugin } + } + + func playerDidEnd(player: AVPlayer) {} + func showErrorAlertAndExit(title: String = "播放失败", message: String = "未知错误") { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let actionOk = UIAlertAction(title: "OK", style: .default) { @@ -75,7 +82,7 @@ extension NewCommonPlayerViewController { } var menus = [UIMenuElement]() activePlugins.forEach { - let newMenus = $0.addMenuItems(current: menus) + let newMenus = $0.addMenuItems(current: &menus) menus.append(contentsOf: newMenus) } playerVC.transportBarCustomMenuItems = menus @@ -114,6 +121,7 @@ extension NewCommonPlayerViewController { guard let self, let player = playerVC.player else { return } isEnd = true activePlugins.forEach { $0.playerDidEnd(player: player) } + playerDidEnd(player: player) } } } diff --git a/BilibiliLive/Component/Player/SpeedChangerPlugin.swift b/BilibiliLive/Component/Player/SpeedChangerPlugin.swift index add4ce96..7b7a7ec6 100644 --- a/BilibiliLive/Component/Player/SpeedChangerPlugin.swift +++ b/BilibiliLive/Component/Player/SpeedChangerPlugin.swift @@ -5,6 +5,40 @@ // Created by yicheng on 2024/5/25. // -import Foundation +import AVKit -class SpeedChangerPlugin: CommonPlayerPlugin {} +class SpeedChangerPlugin: NSObject, CommonPlayerPlugin { + private weak var player: AVPlayer? + private weak var playerVC: AVPlayerViewController? + + @Published private(set) var currentPlaySpeed: PlaySpeed = .default + + func playerDidLoad(playerVC: AVPlayerViewController) { + self.playerVC = playerVC + } + + func playerDidChange(player: AVPlayer) { + self.player = player + } + + func playerWillStart(player: AVPlayer) { + playerVC?.selectSpeed(AVPlaybackSpeed(rate: currentPlaySpeed.value, localizedName: currentPlaySpeed.name)) + } + + func addMenuItems(current: inout [UIMenuElement]) -> [UIMenuElement] { + let gearImage = UIImage(systemName: "gearshape") + + let speedActions = PlaySpeed.blDefaults.map { playSpeed in + UIAction(title: playSpeed.name, state: currentPlaySpeed == playSpeed ? .on : .off) { + [weak self] _ in + guard let self else { return } + player?.currentItem?.audioTimePitchAlgorithm = .timeDomain + playerVC?.selectSpeed(AVPlaybackSpeed(rate: playSpeed.value, localizedName: playSpeed.name)) + currentPlaySpeed = playSpeed + } + } + let playSpeedMenu = UIMenu(title: "播放速度", options: [.displayInline, .singleSelection], children: speedActions) + let menu = UIMenu(title: "播放设置", image: gearImage, identifier: UIMenu.Identifier(rawValue: "setting"), children: [playSpeedMenu]) + return [menu] + } +} diff --git a/BilibiliLive/Component/Video/BUpnpPlugin.swift b/BilibiliLive/Component/Video/BUpnpPlugin.swift index 33a9a277..d1b0e127 100644 --- a/BilibiliLive/Component/Video/BUpnpPlugin.swift +++ b/BilibiliLive/Component/Video/BUpnpPlugin.swift @@ -7,7 +7,7 @@ import AVFoundation import Foundation -class BUpnpPlugin: CommonPlayerPlugin { +class BUpnpPlugin: NSObject, CommonPlayerPlugin { let duration: Int? init(duration: Int?) { diff --git a/BilibiliLive/Component/Video/BVideoClipsPlugin.swift b/BilibiliLive/Component/Video/BVideoClipsPlugin.swift index b1c397e2..112315ac 100644 --- a/BilibiliLive/Component/Video/BVideoClipsPlugin.swift +++ b/BilibiliLive/Component/Video/BVideoClipsPlugin.swift @@ -7,7 +7,7 @@ import AVKit -class BVideoClipsPlugin: CommonPlayerPlugin { +class BVideoClipsPlugin: NSObject, CommonPlayerPlugin { let clipInfos: [VideoPlayURLInfo.ClipInfo] private var observers = [Any]() diff --git a/BilibiliLive/Component/Video/BVideoInfoPlugin.swift b/BilibiliLive/Component/Video/BVideoInfoPlugin.swift index c819f2a1..300b6b37 100644 --- a/BilibiliLive/Component/Video/BVideoInfoPlugin.swift +++ b/BilibiliLive/Component/Video/BVideoInfoPlugin.swift @@ -8,7 +8,7 @@ import AVKit import Kingfisher -class BVideoInfoPlugin: CommonPlayerPlugin { +class BVideoInfoPlugin: NSObject, CommonPlayerPlugin { let title: String? let subTitle: String? let desp: String? diff --git a/BilibiliLive/Component/Video/BVideoPlayPlugin.swift b/BilibiliLive/Component/Video/BVideoPlayPlugin.swift index e44b6a17..92f33948 100644 --- a/BilibiliLive/Component/Video/BVideoPlayPlugin.swift +++ b/BilibiliLive/Component/Video/BVideoPlayPlugin.swift @@ -7,7 +7,7 @@ import AVKit -class BVideoPlayPlugin: CommonPlayerPlugin { +class BVideoPlayPlugin: NSObject, CommonPlayerPlugin { private weak var playerVC: AVPlayerViewController? private var playerDelegate: BilibiliVideoResourceLoaderDelegate? private let playData: PlayerDetailData @@ -18,6 +18,7 @@ class BVideoPlayPlugin: CommonPlayerPlugin { func playerDidLoad(playerVC: AVPlayerViewController) { self.playerVC = playerVC + playerVC.player = nil playerVC.appliesPreferredDisplayCriteriaAutomatically = Settings.contentMatch Task { try? await playmedia(urlInfo: playData.videoPlayURLInfo, playerInfo: playData.playerInfo) @@ -62,14 +63,6 @@ class BVideoPlayPlugin: CommonPlayerPlugin { func prepare(toPlay asset: AVURLAsset) async { let playerItem = AVPlayerItem(asset: asset) let player = AVPlayer(playerItem: playerItem) - - if let defaultRate = playerVC?.player?.defaultRate, - let speed = PlaySpeed.blDefaults.first(where: { $0.value == defaultRate }) - { - playerVC?.player = player - playerVC?.selectSpeed(AVPlaybackSpeed(rate: speed.value, localizedName: speed.name)) - } else { - playerVC?.player = player - } + playerVC?.player = player } } diff --git a/BilibiliLive/Component/Video/NewVideoPlayerViewController.swift b/BilibiliLive/Component/Video/NewVideoPlayerViewController.swift index d74ff509..48a58e40 100644 --- a/BilibiliLive/Component/Video/NewVideoPlayerViewController.swift +++ b/BilibiliLive/Component/Video/NewVideoPlayerViewController.swift @@ -28,6 +28,7 @@ class NewVideoPlayerViewController: NewCommonPlayerViewController { override func viewDidLoad() { super.viewDidLoad() + viewModel.nextProvider = nextProvider viewModel.onPluginReady.receive(on: DispatchQueue.main).sink { [weak self] completion in switch completion { case let .failure(err): @@ -38,7 +39,12 @@ class NewVideoPlayerViewController: NewCommonPlayerViewController { } receiveValue: { [weak self] plugins in plugins.forEach { self?.addPlugin(plugin: $0) } }.store(in: &cancelable) - + viewModel.onPluginRemove.sink { [weak self] in + self?.removePlugin(plugin: $0) + }.store(in: &cancelable) + viewModel.onExit = { [weak self] in + self?.dismiss(animated: true) + } Task { await viewModel.load() } diff --git a/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift b/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift index 1526d0c3..586cbfc0 100644 --- a/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift +++ b/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift @@ -23,27 +23,37 @@ struct PlayerDetailData { class NewVideoPlayerViewModel { var onPluginReady = PassthroughSubject<[CommonPlayerPlugin], String>() + var onPluginRemove = PassthroughSubject() + var onExit: (() -> Void)? + var nextProvider: VideoNextProvider? private var playInfo: PlayInfo private let danmuProvider = VideoDanmuProvider() private var videoDetail: VideoDetail? + private var cancellable = Set() + private var playPlugin: CommonPlayerPlugin? + init(playInfo: PlayInfo) { self.playInfo = playInfo } func load() async { do { - try await initPlayInfo() - let data = try await fetchVideoData() - await danmuProvider.initVideo(cid: data.cid, startPos: data.playerStartPos ?? 0) + let data = try await loadVideoInfo() let plugin = await generatePlayerPlugin(data) onPluginReady.send(plugin) - } catch let err { onPluginReady.send(completion: .failure(err.localizedDescription)) } } + private func loadVideoInfo() async throws -> PlayerDetailData { + try await initPlayInfo() + let data = try await fetchVideoData() + await danmuProvider.initVideo(cid: data.cid, startPos: data.playerStartPos ?? 0) + return data + } + private func initPlayInfo() async throws { if !playInfo.isCidVaild { playInfo.cid = try await WebRequest.requestCid(aid: playInfo.aid) @@ -105,12 +115,45 @@ class NewVideoPlayerViewModel { } } + private func playNext(newPlayInfo: PlayInfo) { + playInfo = newPlayInfo + if let playPlugin { + onPluginRemove.send(playPlugin) + } + Task { + do { + let data = try await loadVideoInfo() + let player = BVideoPlayPlugin(detailData: data) + onPluginReady.send([player]) + } catch let err { + onPluginReady.send(completion: .failure(err.localizedDescription)) + } + } + } + @MainActor private func generatePlayerPlugin(_ data: PlayerDetailData) async -> [CommonPlayerPlugin] { let player = BVideoPlayPlugin(detailData: data) let danmu = DanmuViewPlugin(provider: danmuProvider) let upnp = BUpnpPlugin(duration: data.detail?.View.duration) let debug = DebugPlugin() - var plugins: [CommonPlayerPlugin] = [player, danmu, upnp, debug] + let playSpeed = SpeedChangerPlugin() + playSpeed.$currentPlaySpeed.sink { [weak danmu] speed in + danmu?.danMuView.playingSpeed = speed.value + }.store(in: &cancellable) + + let playlist = VideoPlayListPlugin(nextProvider: nextProvider) + playlist.onPlayEnd = { [weak self] in + self?.onExit?() + } + playlist.onPlayNextWithInfo = { + [weak self] info in + guard let self else { return } + playNext(newPlayInfo: info) + } + + playPlugin = player + + var plugins: [CommonPlayerPlugin] = [player, danmu, playSpeed, upnp, debug, playlist] if let clips = data.clips { let clip = BVideoClipsPlugin(clipInfos: clips) diff --git a/BilibiliLive/Component/Video/VideoPlayListPlugin.swift b/BilibiliLive/Component/Video/VideoPlayListPlugin.swift new file mode 100644 index 00000000..8fbb5128 --- /dev/null +++ b/BilibiliLive/Component/Video/VideoPlayListPlugin.swift @@ -0,0 +1,61 @@ +// +// VideoPlayListPlugin.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/26. +// + +import AVKit + +class VideoPlayListPlugin: NSObject, CommonPlayerPlugin { + var onPlayEnd: (() -> Void)? + var onPlayNextWithInfo: ((PlayInfo) -> Void)? + + let nextProvider: VideoNextProvider? + + init(nextProvider: VideoNextProvider?) { + self.nextProvider = nextProvider + } + + func addMenuItems(current: inout [UIMenuElement]) -> [UIMenuElement] { + let loopImage = UIImage(systemName: "infinity") + let loopAction = UIAction(title: "循环播放", image: loopImage, state: Settings.loopPlay ? .on : .off) { + action in + action.state = (action.state == .off) ? .on : .off + Settings.loopPlay = action.state == .on + } + if let setting = current.compactMap({ $0 as? UIMenu }) + .first(where: { $0.identifier == UIMenu.Identifier(rawValue: "setting") }) + { + var child = setting.children + child.append(loopAction) + if let index = current.firstIndex(of: setting) { + current[index] = setting.replacingChildren(child) + } + return [] + } + return [loopAction] + } + + func playerDidEnd(player: AVPlayer) { + if !playNext() { + if Settings.loopPlay { + nextProvider?.reset() + if !playNext() { + player.currentItem?.seek(to: .zero, completionHandler: nil) + player.play() + } + return + } + onPlayEnd?() + } + } + + private func playNext() -> Bool { + if let next = nextProvider?.getNext() { + onPlayNextWithInfo?(next) + return true + } + return false + } +} From a4cd4d6adbe619716926cab7ee9b4aa9ae9a90ef Mon Sep 17 00:00:00 2001 From: yicheng <11733500+yichengchen@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:31:37 +0800 Subject: [PATCH 5/9] feat: live use new player --- BilibiliLive.xcodeproj/project.pbxproj | 8 +++ .../Component/Player/AVPlayerMetaUtils.swift | 42 ++++++++++++ .../Component/Video/BVideoInfoPlugin.swift | 37 +---------- BilibiliLive/LoginViewController.swift | 8 +-- .../Module/Live/LiveDanMuProvider.swift | 23 +++++-- .../Live/LivePlayerViewController.swift | 64 +++---------------- .../Module/Live/LivePlayerViewModel.swift | 64 ++++++++++++------- BilibiliLive/Module/Live/URLPlayPlugin.swift | 57 +++++++++++++++++ 8 files changed, 177 insertions(+), 126 deletions(-) create mode 100644 BilibiliLive/Component/Player/AVPlayerMetaUtils.swift create mode 100644 BilibiliLive/Module/Live/URLPlayPlugin.swift diff --git a/BilibiliLive.xcodeproj/project.pbxproj b/BilibiliLive.xcodeproj/project.pbxproj index b268b33b..5620c7ca 100644 --- a/BilibiliLive.xcodeproj/project.pbxproj +++ b/BilibiliLive.xcodeproj/project.pbxproj @@ -81,6 +81,8 @@ 499C76132931A781003160FB /* Swifter in Frameworks */ = {isa = PBXBuildFile; productRef = 499C76122931A781003160FB /* Swifter */; }; 499C76162931A7AE003160FB /* SwiftyXMLParser in Frameworks */ = {isa = PBXBuildFile; productRef = 499C76152931A7AE003160FB /* SwiftyXMLParser */; }; 49A441CD293F6DFD0007606C /* FollowUpsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A441CC293F6DFD0007606C /* FollowUpsViewController.swift */; }; + 49D250A02C118FA700173908 /* URLPlayPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D2509F2C118FA700173908 /* URLPlayPlugin.swift */; }; + 49D250A22C11A82B00173908 /* AVPlayerMetaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */; }; 49D39F28263AD40000F14497 /* WebRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D39F27263AD40000F14497 /* WebRequest.swift */; }; 49D6A7122C0200ED0084A5A7 /* CommonPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */; }; 49D6A7142C0363F10084A5A7 /* VideoPlayListPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */; }; @@ -275,6 +277,8 @@ 499C75ED29305A1E003160FB /* BiliBiliUpnpDMR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiliBiliUpnpDMR.swift; sourceTree = ""; }; 499C760E2930E068003160FB /* NVASocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NVASocket.swift; sourceTree = ""; }; 49A441CC293F6DFD0007606C /* FollowUpsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowUpsViewController.swift; sourceTree = ""; }; + 49D2509F2C118FA700173908 /* URLPlayPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLPlayPlugin.swift; sourceTree = ""; }; + 49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerMetaUtils.swift; sourceTree = ""; }; 49D39F27263AD40000F14497 /* WebRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRequest.swift; sourceTree = ""; }; 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerPlugin.swift; sourceTree = ""; }; 49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayListPlugin.swift; sourceTree = ""; }; @@ -383,6 +387,7 @@ F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */, F9B57355260F5F7400771ED5 /* LivePlayerViewController.swift */, F927ED672610113A00EAB8E3 /* LiveDanMuProvider.swift */, + 49D2509F2C118FA700173908 /* URLPlayPlugin.swift */, ); path = Live; sourceTree = ""; @@ -668,6 +673,7 @@ 496E5A542C01CDBB0062951B /* DebugPlugin.swift */, 496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */, 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */, + 49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */, ); path = Player; sourceTree = ""; @@ -870,6 +876,7 @@ 498CF2982B63AABE0009793E /* encoder_dict.c in Sources */, AE2B41552912706700BF2B0B /* SearchResultViewController.swift in Sources */, 499C75EE29305A1E003160FB /* BiliBiliUpnpDMR.swift in Sources */, + 49D250A22C11A82B00173908 /* AVPlayerMetaUtils.swift in Sources */, 4947423B2906B308005D6885 /* BLTextOnlyCollectionViewCell.swift in Sources */, F927ED8D2610A5A800EAB8E3 /* LoginViewController.swift in Sources */, F9B9EAEA261B15020045C2C6 /* FeedCollectionViewController.swift in Sources */, @@ -886,6 +893,7 @@ F927ED682610113A00EAB8E3 /* LiveDanMuProvider.swift in Sources */, 498CF2922B63AABE0009793E /* histogram.c in Sources */, 49474213290509F6005D6885 /* DateFormatter.swift in Sources */, + 49D250A02C118FA700173908 /* URLPlayPlugin.swift in Sources */, 492AD7112C001C7B007221C8 /* BVideoPlayPlugin.swift in Sources */, F927ED742610395300EAB8E3 /* DanmakuView.swift in Sources */, F927ED782610395300EAB8E3 /* DanmakuTrack.swift in Sources */, diff --git a/BilibiliLive/Component/Player/AVPlayerMetaUtils.swift b/BilibiliLive/Component/Player/AVPlayerMetaUtils.swift new file mode 100644 index 00000000..a8d85b63 --- /dev/null +++ b/BilibiliLive/Component/Player/AVPlayerMetaUtils.swift @@ -0,0 +1,42 @@ +// +// AVPlayerMetaUtils.swift +// BilibiliLive +// +// Created by yicheng on 2024/6/6. +// + +import AVKit +import Kingfisher + +enum AVPlayerMetaUtils { + static func setPlayerInfo(title: String?, subTitle: String?, desp: String?, pic: URL?, player: AVPlayer) async { + let desp = desp?.components(separatedBy: "\n").joined(separator: " ") + let mapping: [AVMetadataIdentifier: Any?] = [ + .commonIdentifierTitle: title, + .iTunesMetadataTrackSubTitle: subTitle, + .commonIdentifierDescription: desp, + ] + var metas = mapping.compactMap { createMetadataItem(for: $0, value: $1) } + + player.currentItem?.externalMetadata = metas + + if let pic = pic, + let resource = try? await KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: pic)), + let data = resource.image.pngData(), + let item = createMetadataItem(for: .commonIdentifierArtwork, value: data) + { + metas.append(item) + player.currentItem?.externalMetadata = metas + } + } + + static func createMetadataItem(for identifier: AVMetadataIdentifier, value: Any?) -> AVMetadataItem? { + if value == nil { return nil } + let item = AVMutableMetadataItem() + item.identifier = identifier + item.value = value as? NSCopying & NSObjectProtocol + // Specify "und" to indicate an undefined language. + item.extendedLanguageTag = "und" + return item.copy() as? AVMetadataItem + } +} diff --git a/BilibiliLive/Component/Video/BVideoInfoPlugin.swift b/BilibiliLive/Component/Video/BVideoInfoPlugin.swift index 300b6b37..0003bbbf 100644 --- a/BilibiliLive/Component/Video/BVideoInfoPlugin.swift +++ b/BilibiliLive/Component/Video/BVideoInfoPlugin.swift @@ -25,7 +25,7 @@ class BVideoInfoPlugin: NSObject, CommonPlayerPlugin { func playerWillStart(player: AVPlayer) { Task { - async let info: () = setPlayerInfo(title: title, subTitle: subTitle, desp: desp, pic: pic, player: player) + async let info: () = AVPlayerMetaUtils.setPlayerInfo(title: title, subTitle: subTitle, desp: desp, pic: pic, player: player) if let viewPoints { async let vp: () = updatePlayerCharpter(viewPoints: viewPoints, player: player) await vp @@ -54,48 +54,17 @@ class BVideoInfoPlugin: NSObject, CommonPlayerPlugin { player.currentItem?.navigationMarkerGroups = [AVNavigationMarkersGroup(title: nil, timedNavigationMarkers: metas)] } - private func setPlayerInfo(title: String?, subTitle: String?, desp: String?, pic: URL?, player: AVPlayer) async { - let desp = desp?.components(separatedBy: "\n").joined(separator: " ") - let mapping: [AVMetadataIdentifier: Any?] = [ - .commonIdentifierTitle: title, - .iTunesMetadataTrackSubTitle: subTitle, - .commonIdentifierDescription: desp, - ] - var metas = mapping.compactMap { createMetadataItem(for: $0, value: $1) } - - player.currentItem?.externalMetadata = metas - - if let pic = pic, - let resource = try? await KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: pic)), - let data = resource.image.pngData(), - let item = createMetadataItem(for: .commonIdentifierArtwork, value: data) - { - metas.append(item) - player.currentItem?.externalMetadata = metas - } - } - - private func createMetadataItem(for identifier: AVMetadataIdentifier, value: Any?) -> AVMetadataItem? { - if value == nil { return nil } - let item = AVMutableMetadataItem() - item.identifier = identifier - item.value = value as? NSCopying & NSObjectProtocol - // Specify "und" to indicate an undefined language. - item.extendedLanguageTag = "und" - return item.copy() as? AVMetadataItem - } - private func convertTimedMetadataGroup(viewPoint: PlayerInfo.ViewPoint) -> AVTimedMetadataGroup { let mapping: [AVMetadataIdentifier: Any?] = [ .commonIdentifierTitle: viewPoint.content, ] - var metadatas = mapping.compactMap { createMetadataItem(for: $0, value: $1) } + var metadatas = mapping.compactMap { AVPlayerMetaUtils.createMetadataItem(for: $0, value: $1) } let timescale: Int32 = 600 let cmStartTime = CMTimeMakeWithSeconds(viewPoint.from, preferredTimescale: timescale) let cmEndTime = CMTimeMakeWithSeconds(viewPoint.to, preferredTimescale: timescale) let timeRange = CMTimeRangeFromTimeToTime(start: cmStartTime, end: cmEndTime) if let imageData = viewPoint.imageData, - let item = createMetadataItem(for: .commonIdentifierArtwork, value: imageData) + let item = AVPlayerMetaUtils.createMetadataItem(for: .commonIdentifierArtwork, value: imageData) { metadatas.append(item) } diff --git a/BilibiliLive/LoginViewController.swift b/BilibiliLive/LoginViewController.swift index 36a39c7f..1da55fde 100644 --- a/BilibiliLive/LoginViewController.swift +++ b/BilibiliLive/LoginViewController.swift @@ -79,12 +79,8 @@ class LoginViewController: UIViewController { func didValidationSuccess() { qrcodeImageView.image = nil - let alert = UIAlertController() - alert.addAction(UIAlertAction(title: "Success", style: .default, handler: { [weak self] _ in - self?.dismiss(animated: true, completion: nil) - AppDelegate.shared.showTabBar() - })) - present(alert, animated: true, completion: nil) + AppDelegate.shared.showTabBar() + stopValidationTimer() } func loopValidation() { diff --git a/BilibiliLive/Module/Live/LiveDanMuProvider.swift b/BilibiliLive/Module/Live/LiveDanMuProvider.swift index 08b5ce2a..e787d0a9 100644 --- a/BilibiliLive/Module/Live/LiveDanMuProvider.swift +++ b/BilibiliLive/Module/Live/LiveDanMuProvider.swift @@ -5,18 +5,20 @@ // Created by Etan on 2021/3/28. // +import Combine import Foundation @_spi(WebSocket) import Alamofire import Gzip import SwiftyJSON -class LiveDanMuProvider { +class LiveDanMuProvider: DanmuProviderProtocol { + let observerPlayerTime = false + var onSendTextModel = PassthroughSubject() + private var websocket: WebSocketRequest? private var heartBeatTimer: Timer? private let roomID: Int private var token = "" - var onDanmu: ((String) -> Void)? - var onSC: ((String) -> Void)? init(roomID: Int) { self.roomID = roomID @@ -26,6 +28,8 @@ class LiveDanMuProvider { stop() } + func playerTimeChange(time: TimeInterval) {} + func start() async throws { let info = try await WebRequest.requestDanmuServerInfo(roomID: roomID) guard let server = info.host_list.first else { @@ -117,19 +121,24 @@ extension LiveDanMuProvider { switch cmd { case "DANMU_MSG": if let str = json["info"][1].string { - onDanmu?(str) + let model = DanmakuTextCellModel(str: str) + onSendTextModel.send(model) } case "DM_INTERACTION": guard let data = json["data"]["data"].string else { return } let comboArr = JSON(parseJSON: data)["combo"] for combo in comboArr.arrayValue { if let str = combo["content"].string { - // let cnt = combo["cnt"].int - onDanmu?("\(str)") + let model = DanmakuTextCellModel(str: str) + onSendTextModel.send(model) } } case "SUPER_CHAT_MESSAGE": - if let str = json["data"]["message"].string { onSC?(str) } + if let str = json["data"]["message"].string { + let model = DanmakuTextCellModel(str: str) + model.type = .top + model.displayTime = 60 + } default: break } diff --git a/BilibiliLive/Module/Live/LivePlayerViewController.swift b/BilibiliLive/Module/Live/LivePlayerViewController.swift index 1c35c576..a7c32fcc 100644 --- a/BilibiliLive/Module/Live/LivePlayerViewController.swift +++ b/BilibiliLive/Module/Live/LivePlayerViewController.swift @@ -11,76 +11,28 @@ import Foundation import SwiftyJSON import UIKit -class LivePlayerViewController: CommonPlayerViewController { - var room: LiveRoom? { - didSet { - roomID = room?.room_id ?? 0 - } - } +class LivePlayerViewController: NewCommonPlayerViewController { + var room: LiveRoom? - private var roomID: Int = 0 - private var failCount = 0 private var viewModel: LivePlayerViewModel? deinit { Logger.debug("deinit live player") } override func viewDidLoad() { - allowChangeSpeed = false - requiresLinearPlayback = true super.viewDidLoad() - viewModel = LivePlayerViewModel(roomID: roomID) - viewModel?.onShootDanmu = { [weak self] in - self?.danMuView.shoot(danmaku: $0) - } - viewModel?.onPlayUrlStr = { [weak self] in - guard let self else { return } - let headers: [String: String] = [ - "User-Agent": Keys.userAgent, - "Referer": Keys.liveReferer, - ] - let asset = AVURLAsset(url: URL(string: $0)!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) - playerItem = AVPlayerItem(asset: asset) - player = AVPlayer(playerItem: playerItem) - player?.automaticallyWaitsToMinimizeStalling = false + viewModel = LivePlayerViewModel(room: room!) + viewModel?.onPluginReady = { [weak self] plugins in + DispatchQueue.main.async { + plugins.forEach { self?.addPlugin(plugin: $0) } + } } + viewModel?.onError = { [weak self] in self?.showErrorAlertAndExit(message: $0) } viewModel?.start() - - if Settings.danmuMask, Settings.vnMask { - maskProvider = VMaskProvider() - setupMask() - } - - Task { - if let info = await viewModel?.fetchDespInfo() { - let subtitle = "\(room?.ownerName ?? "")·\(info.parent_area_name) \(info.area_name)" - let desp = "\(info.description)\nTags:\(info.tags ?? "")" - setPlayerInfo(title: info.title, subTitle: subtitle, desp: desp, pic: room?.pic) - } else { - setPlayerInfo(title: room?.title, subTitle: "", desp: room?.ownerName, pic: room?.pic) - } - } - } - - override func retryPlay() -> Bool { - Logger.warn("play fail, retry") - viewModel?.playerDidFailToPlay() - return true - } - - override func playerRateDidChange(player: AVPlayer) { - Logger.info("play speed change to", player.rate) - if player.rate == 0 { - viewModel?.playerDidFailToPlay() - } - } - - override func additionDebugInfo() -> String { - return viewModel?.debugInfo() ?? "" } } diff --git a/BilibiliLive/Module/Live/LivePlayerViewModel.swift b/BilibiliLive/Module/Live/LivePlayerViewModel.swift index 05619d5e..a8399759 100644 --- a/BilibiliLive/Module/Live/LivePlayerViewModel.swift +++ b/BilibiliLive/Module/Live/LivePlayerViewModel.swift @@ -19,24 +19,48 @@ enum LiveError: String, LocalizedError { } class LivePlayerViewModel { - init(roomID: Int) { - self.roomID = roomID + init(room: LiveRoom) { + self.room = room + roomID = room.room_id } deinit { danMuProvider?.stop() } - var onShootDanmu: ((DanmakuTextCellModel) -> Void)? - var onPlayUrlStr: ((String) -> Void)? + var onPluginReady: (([CommonPlayerPlugin]) -> Void)? var onError: ((String) -> Void)? + private let playPlugin = URLPlayPlugin(referer: Keys.liveReferer, isLive: true) + private let debugPlugin = DebugPlugin() + func start() { + playPlugin.onPlayFail = { [weak self] in + self?.playerDidFailToPlay() + } + + debugPlugin.additionDebugInfo = { [weak self] in + self?.debugInfo() ?? "" + } + + onPluginReady?([playPlugin, debugPlugin]) Task { do { try await refreshRoomsID() try await initPlayer() - await initDanmu() + + let danmu = await initDanmu() + self.onPluginReady?(danmu) + + if let info = await fetchDespInfo() { + let subtitle = "\(room.ownerName)·\(info.parent_area_name) \(info.area_name)" + let desp = "\(info.description)\nTags:\(info.tags ?? "")" + let infoPlugin = BVideoInfoPlugin(title: info.title, subTitle: subtitle, desp: desp, pic: room.pic, viewPoints: nil) + self.onPluginReady?([infoPlugin]) + } else { + let infoPlugin = BVideoInfoPlugin(title: room.title, subTitle: nil, desp: nil, pic: room.pic, viewPoints: nil) + self.onPluginReady?([infoPlugin]) + } } catch let err { await MainActor.run { onError?(String(describing: err)) @@ -76,6 +100,7 @@ class LivePlayerViewModel { private var allPlayInfos = [LivePlayUrlInfo]() private var playInfos = [LivePlayUrlInfo]() private var roomID: Int + private let room: LiveRoom private var danMuProvider: LiveDanMuProvider? private var retryCount = 0 @@ -127,32 +152,25 @@ class LivePlayerViewModel { if let info = playInfos.first { Logger.debug("play =>", playInfos) await MainActor.run { - onPlayUrlStr?(info.url) + playPlugin.play(urlString: info.url) } } else { throw LiveError.noPlaybackUrl } } - private func initDanmu() async { + @MainActor private func initDanmu() async -> [CommonPlayerPlugin] { danMuProvider = LiveDanMuProvider(roomID: roomID) - danMuProvider?.onDanmu = { - [weak self] string in - let model = DanmakuTextCellModel(str: string) - DispatchQueue.main.async { [weak self] in - self?.onShootDanmu?(model) - } - } - danMuProvider?.onSC = { - [weak self] string in - let model = DanmakuTextCellModel(str: string) - model.type = .top - model.displayTime = 60 - DispatchQueue.main.async { [weak self] in - self?.onShootDanmu?(model) - } - } + let danmuPlugin = DanmuViewPlugin(provider: danMuProvider!) + try? await danMuProvider?.start() + var plugins: [CommonPlayerPlugin] = [danmuPlugin] + if Settings.danmuMask, Settings.vnMask { + let plugin = MaskViewPugin(maskView: danmuPlugin.danMuView, maskProvider: VMaskProvider()) + plugins.append(plugin) + } + + return plugins } } diff --git a/BilibiliLive/Module/Live/URLPlayPlugin.swift b/BilibiliLive/Module/Live/URLPlayPlugin.swift new file mode 100644 index 00000000..6381a3c8 --- /dev/null +++ b/BilibiliLive/Module/Live/URLPlayPlugin.swift @@ -0,0 +1,57 @@ +// +// LivePlayPlugin.swift +// BilibiliLive +// +// Created by yicheng on 2024/6/6. +// + +import AVKit +import Foundation + +class URLPlayPlugin: NSObject { + var onPlayFail: (() -> Void)? + + private weak var playerVC: AVPlayerViewController? + private let referer: String + private let isLive: Bool + private var currentUrl: String? + + init(referer: String = "", isLive: Bool = false) { + self.referer = referer + self.isLive = isLive + } + + func play(urlString: String) { + currentUrl = urlString + let headers: [String: String] = [ + "User-Agent": Keys.userAgent, + "Referer": referer, + ] + let asset = AVURLAsset(url: URL(string: urlString)!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) + let playerItem = AVPlayerItem(asset: asset) + let player = AVPlayer(playerItem: playerItem) + player.automaticallyWaitsToMinimizeStalling = !isLive + playerVC?.player = player + } +} + +extension URLPlayPlugin: CommonPlayerPlugin { + func playerDidLoad(playerVC: AVPlayerViewController) { + self.playerVC = playerVC + playerVC.requiresLinearPlayback = isLive + playerVC.player = nil + if let currentUrl { + play(urlString: currentUrl) + } + } + + func playerDidFail(player: AVPlayer) { + onPlayFail?() + } + + func playerDidPause(player: AVPlayer) { + if isLive { + onPlayFail?() + } + } +} From 93a4c03ca787680546ac330c98c4a50c42ecfc29 Mon Sep 17 00:00:00 2001 From: yicheng <11733500+yichengchen@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:20:25 +0800 Subject: [PATCH 6/9] remove old commonPlayer --- BilibiliLive.xcodeproj/project.pbxproj | 88 ++-- .../xcshareddata/swiftpm/Package.resolved | 3 +- .../Player/CommonPlayerViewController.swift | 412 ----------------- .../NewCommonPlayerViewController.swift | 12 +- .../{ => Plugins}/CommonPlayerPlugin.swift | 0 .../{ => Plugins}/DanmuViewPlugin.swift | 22 +- .../Player/{ => Plugins}/DebugPlugin.swift | 0 .../Player/{ => Plugins}/MaskViewPugin.swift | 0 .../{ => Plugins}/SpeedChangerPlugin.swift | 18 + .../Player/Plugins}/URLPlayPlugin.swift | 0 BilibiliLive/Component/Settings.swift | 12 +- .../{ => MaskProvider}/BMaskProvider.swift | 0 .../Video/MaskProvider/MaskProvider.swift | 14 + .../{ => MaskProvider}/VMaskProvider.swift | 0 .../Video/NewVideoPlayerViewController.swift | 52 --- .../Video/NewVideoPlayerViewModel.swift | 2 +- .../Video/{ => Plugins}/BUpnpPlugin.swift | 14 + .../{ => Plugins}/BVideoClipsPlugin.swift | 0 .../{ => Plugins}/BVideoInfoPlugin.swift | 0 .../{ => Plugins}/BVideoPlayPlugin.swift | 0 .../{ => Plugins}/VideoPlayListPlugin.swift | 0 .../Video/VideoDetailViewController.swift | 4 +- .../Video/VideoPlayerViewController.swift | 415 ++---------------- .../Video/VideoPlayerViewModel.swift | 183 ++++++++ BilibiliLive/Extensions/Published+..swift | 20 + .../Module/DLNA/BiliBiliUpnpDMR.swift | 11 +- .../Live/LivePlayerViewController.swift | 2 +- 27 files changed, 373 insertions(+), 911 deletions(-) rename BilibiliLive/Component/Player/{ => Plugins}/CommonPlayerPlugin.swift (100%) rename BilibiliLive/Component/Player/{ => Plugins}/DanmuViewPlugin.swift (87%) rename BilibiliLive/Component/Player/{ => Plugins}/DebugPlugin.swift (100%) rename BilibiliLive/Component/Player/{ => Plugins}/MaskViewPugin.swift (100%) rename BilibiliLive/Component/Player/{ => Plugins}/SpeedChangerPlugin.swift (77%) rename BilibiliLive/{Module/Live => Component/Player/Plugins}/URLPlayPlugin.swift (100%) rename BilibiliLive/Component/Video/{ => MaskProvider}/BMaskProvider.swift (100%) create mode 100644 BilibiliLive/Component/Video/MaskProvider/MaskProvider.swift rename BilibiliLive/Component/Video/{ => MaskProvider}/VMaskProvider.swift (100%) delete mode 100644 BilibiliLive/Component/Video/NewVideoPlayerViewController.swift rename BilibiliLive/Component/Video/{ => Plugins}/BUpnpPlugin.swift (81%) rename BilibiliLive/Component/Video/{ => Plugins}/BVideoClipsPlugin.swift (100%) rename BilibiliLive/Component/Video/{ => Plugins}/BVideoInfoPlugin.swift (100%) rename BilibiliLive/Component/Video/{ => Plugins}/BVideoPlayPlugin.swift (100%) rename BilibiliLive/Component/Video/{ => Plugins}/VideoPlayListPlugin.swift (100%) create mode 100644 BilibiliLive/Component/Video/VideoPlayerViewModel.swift create mode 100644 BilibiliLive/Extensions/Published+..swift diff --git a/BilibiliLive.xcodeproj/project.pbxproj b/BilibiliLive.xcodeproj/project.pbxproj index 5620c7ca..4d4509f9 100644 --- a/BilibiliLive.xcodeproj/project.pbxproj +++ b/BilibiliLive.xcodeproj/project.pbxproj @@ -19,7 +19,7 @@ 492731EE29096677005F5B0A /* HotViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492731ED29096677005F5B0A /* HotViewController.swift */; }; 492AD7092BFF1E6C007221C8 /* NewCommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */; }; 492AD70B2BFF23B1007221C8 /* DanmuViewPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */; }; - 492AD70D2BFF33DF007221C8 /* NewVideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */; }; + 492AD70D2BFF33DF007221C8 /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70C2BFF33DF007221C8 /* VideoPlayerViewController.swift */; }; 492AD70F2BFF6761007221C8 /* NewVideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */; }; 492AD7112C001C7B007221C8 /* BVideoPlayPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */; }; 492AD7132C001CA7007221C8 /* String+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7122C001CA7007221C8 /* String+Error.swift */; }; @@ -38,15 +38,18 @@ 49508E0F2943420100D26812 /* CocoaLumberjack in Frameworks */ = {isa = PBXBuildFile; productRef = 49508E0E2943420100D26812 /* CocoaLumberjack */; }; 49508E112943420100D26812 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 49508E102943420100D26812 /* CocoaLumberjackSwift */; }; 496400D32943431E0098ACA6 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496400D22943431E0098ACA6 /* Logger.swift */; }; - 496E5A4D2C018F150062951B /* BVideoClipsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */; }; 496E5A4F2C0194720062951B /* MaskViewPugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A4E2C0194720062951B /* MaskViewPugin.swift */; }; 496E5A512C0194CD0062951B /* BUpnpPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A502C0194CD0062951B /* BUpnpPlugin.swift */; }; - 496E5A532C01B1CA0062951B /* BVideoInfoPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */; }; 496E5A552C01CDBB0062951B /* DebugPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A542C01CDBB0062951B /* DebugPlugin.swift */; }; 496E5A572C01CDCA0062951B /* SpeedChangerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */; }; 4973502B29161B770045C26B /* WeeklyWatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4973502A29161B770045C26B /* WeeklyWatchViewController.swift */; }; 4973502D29162A6D0045C26B /* StandardVideoCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4973502C29162A6D0045C26B /* StandardVideoCollectionViewController.swift */; }; 497361082BF1A16600ED213F /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497361072BF1A16600ED213F /* Keys.swift */; }; + 497CF22F2C16EBA4006E1488 /* Published+..swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF22E2C16EBA4006E1488 /* Published+..swift */; }; + 497CF2312C16ED45006E1488 /* MaskProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF2302C16ED45006E1488 /* MaskProvider.swift */; }; + 497CF2372C16EDE5006E1488 /* BVideoClipsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF2342C16EDE5006E1488 /* BVideoClipsPlugin.swift */; }; + 497CF2382C16EDE5006E1488 /* BVideoInfoPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF2352C16EDE5006E1488 /* BVideoInfoPlugin.swift */; }; + 497CF2392C16EDE5006E1488 /* VideoPlayListPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497CF2362C16EDE5006E1488 /* VideoPlayListPlugin.swift */; }; 498CF2902B63AABE0009793E /* dictionary_hash.c in Sources */ = {isa = PBXBuildFile; fileRef = 498CF2422B63AABE0009793E /* dictionary_hash.c */; }; 498CF2912B63AABE0009793E /* backward_references_hq.c in Sources */ = {isa = PBXBuildFile; fileRef = 498CF2432B63AABE0009793E /* backward_references_hq.c */; }; 498CF2922B63AABE0009793E /* histogram.c in Sources */ = {isa = PBXBuildFile; fileRef = 498CF2462B63AABE0009793E /* histogram.c */; }; @@ -85,7 +88,6 @@ 49D250A22C11A82B00173908 /* AVPlayerMetaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */; }; 49D39F28263AD40000F14497 /* WebRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D39F27263AD40000F14497 /* WebRequest.swift */; }; 49D6A7122C0200ED0084A5A7 /* CommonPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */; }; - 49D6A7142C0363F10084A5A7 /* VideoPlayListPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */; }; 49DA01A0296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */; }; 49E5F85028AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */; }; 49F9186E2931E3C9001D3EC3 /* DLNAInfo.xml in Resources */ = {isa = PBXBuildFile; fileRef = 49F9186D2931E3C9001D3EC3 /* DLNAInfo.xml */; }; @@ -119,7 +121,6 @@ F927ED902610A5E900EAB8E3 /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F927ED8F2610A5E900EAB8E3 /* CookieManager.swift */; }; F927ED992610AD8D00EAB8E3 /* LiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */; }; F927ED9F2610B5C300EAB8E3 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = F927ED9E2610B5C300EAB8E3 /* Kingfisher */; }; - F9562C92261A0D2200573B74 /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9562C91261A0D2200573B74 /* VideoPlayerViewController.swift */; }; F99D28E12619591300F8E66A /* CommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */; }; F99D28EA26195EC200F8E66A /* UIView+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99D28E926195EC200F8E66A /* UIView+Layout.swift */; }; F99D28F72619F5F000F8E66A /* FollowsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99D28F62619F5F000F8E66A /* FollowsViewController.swift */; }; @@ -161,7 +162,7 @@ 492731ED29096677005F5B0A /* HotViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotViewController.swift; sourceTree = ""; }; 492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCommonPlayerViewController.swift; sourceTree = ""; }; 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DanmuViewPlugin.swift; sourceTree = ""; }; - 492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewVideoPlayerViewController.swift; sourceTree = ""; }; + 492AD70C2BFF33DF007221C8 /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = ""; }; 492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewVideoPlayerViewModel.swift; sourceTree = ""; }; 492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BVideoPlayPlugin.swift; sourceTree = ""; }; 492AD7122C001CA7007221C8 /* String+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Error.swift"; sourceTree = ""; }; @@ -179,15 +180,18 @@ 49474212290509F6005D6885 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 4947423A2906B308005D6885 /* BLTextOnlyCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLTextOnlyCollectionViewCell.swift; sourceTree = ""; }; 496400D22943431E0098ACA6 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; - 496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BVideoClipsPlugin.swift; sourceTree = ""; }; 496E5A4E2C0194720062951B /* MaskViewPugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskViewPugin.swift; sourceTree = ""; }; 496E5A502C0194CD0062951B /* BUpnpPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BUpnpPlugin.swift; sourceTree = ""; }; - 496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BVideoInfoPlugin.swift; sourceTree = ""; }; 496E5A542C01CDBB0062951B /* DebugPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugPlugin.swift; sourceTree = ""; }; 496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedChangerPlugin.swift; sourceTree = ""; }; 4973502A29161B770045C26B /* WeeklyWatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeeklyWatchViewController.swift; sourceTree = ""; }; 4973502C29162A6D0045C26B /* StandardVideoCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardVideoCollectionViewController.swift; sourceTree = ""; }; 497361072BF1A16600ED213F /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = ""; }; + 497CF22E2C16EBA4006E1488 /* Published+..swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Published+..swift"; sourceTree = ""; }; + 497CF2302C16ED45006E1488 /* MaskProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskProvider.swift; sourceTree = ""; }; + 497CF2342C16EDE5006E1488 /* BVideoClipsPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BVideoClipsPlugin.swift; sourceTree = ""; }; + 497CF2352C16EDE5006E1488 /* BVideoInfoPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BVideoInfoPlugin.swift; sourceTree = ""; }; + 497CF2362C16EDE5006E1488 /* VideoPlayListPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayListPlugin.swift; sourceTree = ""; }; 498CF2352B63AABE0009793E /* NSData+BrotliCompression.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+BrotliCompression.h"; sourceTree = ""; }; 498CF2362B63AABE0009793E /* LMBrotliCompression.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LMBrotliCompression.h; sourceTree = ""; }; 498CF2372B63AABE0009793E /* BrotliKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BrotliKit.h; sourceTree = ""; }; @@ -281,7 +285,6 @@ 49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerMetaUtils.swift; sourceTree = ""; }; 49D39F27263AD40000F14497 /* WebRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRequest.swift; sourceTree = ""; }; 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerPlugin.swift; sourceTree = ""; }; - 49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayListPlugin.swift; sourceTree = ""; }; 49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVInfoPanelCollectionViewThumbnailCell+Hook.swift"; sourceTree = ""; }; 49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BilibiliVideoResourceLoaderDelegate.swift; sourceTree = ""; }; 49F9186D2931E3C9001D3EC3 /* DLNAInfo.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = DLNAInfo.xml; sourceTree = ""; }; @@ -312,7 +315,6 @@ F927ED8C2610A5A800EAB8E3 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; F927ED8F2610A5E900EAB8E3 /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = ""; }; F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveViewController.swift; sourceTree = ""; }; - F9562C91261A0D2200573B74 /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = ""; }; F99D28DA2618A55900F8E66A /* BilibiliLive-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BilibiliLive-Bridging-Header.h"; sourceTree = ""; }; F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerViewController.swift; sourceTree = ""; }; F99D28E926195EC200F8E66A /* UIView+Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Layout.swift"; sourceTree = ""; }; @@ -387,7 +389,6 @@ F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */, F9B57355260F5F7400771ED5 /* LivePlayerViewController.swift */, F927ED672610113A00EAB8E3 /* LiveDanMuProvider.swift */, - 49D2509F2C118FA700173908 /* URLPlayPlugin.swift */, ); path = Live; sourceTree = ""; @@ -395,18 +396,12 @@ 49389D5D28AFE6DC00B9DAFD /* Video */ = { isa = PBXGroup; children = ( - F9562C91261A0D2200573B74 /* VideoPlayerViewController.swift */, F9EDADD2262AA421007CB99F /* VideoDetailViewController.swift */, 49389D6128AFEA2900B9DAFD /* VideoDanmuProvider.swift */, - 498DB1DE291BC24700F95607 /* BMaskProvider.swift */, - 49FB8EBF291F4C520045D5DE /* VMaskProvider.swift */, - 492AD70C2BFF33DF007221C8 /* NewVideoPlayerViewController.swift */, + 497CF2322C16EDAC006E1488 /* MaskProvider */, + 492AD70C2BFF33DF007221C8 /* VideoPlayerViewController.swift */, 492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */, - 492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */, - 496E5A4C2C018F150062951B /* BVideoClipsPlugin.swift */, - 496E5A502C0194CD0062951B /* BUpnpPlugin.swift */, - 496E5A522C01B1CA0062951B /* BVideoInfoPlugin.swift */, - 49D6A7132C0363F10084A5A7 /* VideoPlayListPlugin.swift */, + 497CF2332C16EDC0006E1488 /* Plugins */, ); path = Video; sourceTree = ""; @@ -425,6 +420,7 @@ AE2B41562914C02000BF2B0B /* Int.swift */, 49DA019F296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift */, 492AD7122C001CA7007221C8 /* String+Error.swift */, + 497CF22E2C16EBA4006E1488 /* Published+..swift */, ); path = Extensions; sourceTree = ""; @@ -440,6 +436,41 @@ path = View; sourceTree = ""; }; + 497CF2322C16EDAC006E1488 /* MaskProvider */ = { + isa = PBXGroup; + children = ( + 498DB1DE291BC24700F95607 /* BMaskProvider.swift */, + 49FB8EBF291F4C520045D5DE /* VMaskProvider.swift */, + 497CF2302C16ED45006E1488 /* MaskProvider.swift */, + ); + path = MaskProvider; + sourceTree = ""; + }; + 497CF2332C16EDC0006E1488 /* Plugins */ = { + isa = PBXGroup; + children = ( + 497CF2342C16EDE5006E1488 /* BVideoClipsPlugin.swift */, + 497CF2352C16EDE5006E1488 /* BVideoInfoPlugin.swift */, + 497CF2362C16EDE5006E1488 /* VideoPlayListPlugin.swift */, + 492AD7102C001C7B007221C8 /* BVideoPlayPlugin.swift */, + 496E5A502C0194CD0062951B /* BUpnpPlugin.swift */, + ); + path = Plugins; + sourceTree = ""; + }; + 497CF23A2C16EE04006E1488 /* Plugins */ = { + isa = PBXGroup; + children = ( + 49D2509F2C118FA700173908 /* URLPlayPlugin.swift */, + 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */, + 496E5A4E2C0194720062951B /* MaskViewPugin.swift */, + 496E5A542C01CDBB0062951B /* DebugPlugin.swift */, + 496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */, + 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */, + ); + path = Plugins; + sourceTree = ""; + }; 498CF2342B63AABE0009793E /* BrotliKit */ = { isa = PBXGroup; children = ( @@ -668,11 +699,7 @@ F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */, 49FB8EE829208EBE0045D5DE /* SidxParseUtil.swift */, 492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */, - 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */, - 496E5A4E2C0194720062951B /* MaskViewPugin.swift */, - 496E5A542C01CDBB0062951B /* DebugPlugin.swift */, - 496E5A562C01CDCA0062951B /* SpeedChangerPlugin.swift */, - 49D6A7112C0200ED0084A5A7 /* CommonPlayerPlugin.swift */, + 497CF23A2C16EE04006E1488 /* Plugins */, 49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */, ); path = Player; @@ -844,7 +871,6 @@ files = ( F9B9EAED261B25E40045C2C6 /* ToViewViewController.swift in Sources */, 4973502B29161B770045C26B /* WeeklyWatchViewController.swift in Sources */, - 496E5A4D2C018F150062951B /* BVideoClipsPlugin.swift in Sources */, AE7A3B20290298BE006FEBB0 /* Colors.swift in Sources */, 496E5A4F2C0194720062951B /* MaskViewPugin.swift in Sources */, 49FB8EC0291F4C520045D5DE /* VMaskProvider.swift in Sources */, @@ -855,6 +881,7 @@ 498CF29E2B63AABE0009793E /* bit_cost.c in Sources */, 498CF29B2B63AABE0009793E /* utf8_util.c in Sources */, 498CF2AA2B63AABE0009793E /* LMBrotliCompressor.m in Sources */, + 497CF22F2C16EBA4006E1488 /* Published+..swift in Sources */, AEA6AB1628FFE951007CE72E /* SettingsViewController.swift in Sources */, 49389D8928B0A1B700B9DAFD /* UIViewController+Ext.swift in Sources */, AE2B41572914C02000BF2B0B /* Int.swift in Sources */, @@ -862,10 +889,9 @@ 498CF2A72B63AABE0009793E /* decode.c in Sources */, F90AAE04265549B5008DE7C2 /* FeedViewController.swift in Sources */, 2DBE4C4D2628818F00D20413 /* HistoryViewController.swift in Sources */, - 492AD70D2BFF33DF007221C8 /* NewVideoPlayerViewController.swift in Sources */, + 492AD70D2BFF33DF007221C8 /* VideoPlayerViewController.swift in Sources */, F9171D6629026AC5002868C7 /* TitleSupplementaryView.swift in Sources */, F9B9EAE7261AC6F80045C2C6 /* BLTabBarViewController.swift in Sources */, - F9562C92261A0D2200573B74 /* VideoPlayerViewController.swift in Sources */, 498CF2A12B63AABE0009793E /* metablock.c in Sources */, F99D28EA26195EC200F8E66A /* UIView+Layout.swift in Sources */, 498CF29D2B63AABE0009793E /* brotli_bit_stream.c in Sources */, @@ -874,6 +900,7 @@ 49FB8EE929208EBE0045D5DE /* SidxParseUtil.swift in Sources */, F9171D6129010429002868C7 /* UICollectionView+..swift in Sources */, 498CF2982B63AABE0009793E /* encoder_dict.c in Sources */, + 497CF2312C16ED45006E1488 /* MaskProvider.swift in Sources */, AE2B41552912706700BF2B0B /* SearchResultViewController.swift in Sources */, 499C75EE29305A1E003160FB /* BiliBiliUpnpDMR.swift in Sources */, 49D250A22C11A82B00173908 /* AVPlayerMetaUtils.swift in Sources */, @@ -909,18 +936,18 @@ 492AD7132C001CA7007221C8 /* String+Error.swift in Sources */, 490EC3E7290CC8F8001E00B6 /* RankingViewController.swift in Sources */, F927ED732610395300EAB8E3 /* DanmakuCell.swift in Sources */, + 497CF2392C16EDE5006E1488 /* VideoPlayListPlugin.swift in Sources */, 498CF2A92B63AABE0009793E /* bit_reader.c in Sources */, 498CF2972B63AABE0009793E /* encode.c in Sources */, 49389D8C28B0A84500B9DAFD /* PersonalViewController.swift in Sources */, 498CF2992B63AABE0009793E /* cluster.c in Sources */, 498CF2952B63AABE0009793E /* compress_fragment_two_pass.c in Sources */, - 49D6A7142C0363F10084A5A7 /* VideoPlayListPlugin.swift in Sources */, F927ED772610395300EAB8E3 /* DanmakuQueuePool.swift in Sources */, + 497CF2382C16EDE5006E1488 /* BVideoInfoPlugin.swift in Sources */, 49A441CD293F6DFD0007606C /* FollowUpsViewController.swift in Sources */, AEA6AB1928FFF3DD007CE72E /* Settings.swift in Sources */, 498CF2962B63AABE0009793E /* block_splitter.c in Sources */, 494741C6290177BB005D6885 /* UpSpaceViewController.swift in Sources */, - 496E5A532C01B1CA0062951B /* BVideoInfoPlugin.swift in Sources */, 496E5A552C01CDBB0062951B /* DebugPlugin.swift in Sources */, 49DA01A0296C466C00EEAE15 /* AVInfoPanelCollectionViewThumbnailCell+Hook.swift in Sources */, 492731EE29096677005F5B0A /* HotViewController.swift in Sources */, @@ -950,6 +977,7 @@ F9171D6429010DF1002868C7 /* FeedCollectionViewCell.swift in Sources */, F927ED902610A5E900EAB8E3 /* CookieManager.swift in Sources */, F927ED792610395400EAB8E3 /* DanmakuCellModel.swift in Sources */, + 497CF2372C16EDE5006E1488 /* BVideoClipsPlugin.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/BilibiliLive.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/BilibiliLive.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b805fee4..a31a7896 100644 --- a/BilibiliLive.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/BilibiliLive.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "0cfbbd8563ddd7bcc668337eb64d08748e8930f841fe447062f4e60b1fe5f8cb", "pins" : [ { "identity" : "alamofire", @@ -118,5 +119,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/BilibiliLive/Component/Player/CommonPlayerViewController.swift b/BilibiliLive/Component/Player/CommonPlayerViewController.swift index da9ebb62..6abc18bd 100644 --- a/BilibiliLive/Component/Player/CommonPlayerViewController.swift +++ b/BilibiliLive/Component/Player/CommonPlayerViewController.swift @@ -9,415 +9,3 @@ import AVKit import Kingfisher import UIKit import Vision - -protocol MaskProvider: AnyObject { - func getMask(for time: CMTime, frame: CGRect, onGet: @escaping (CALayer) -> Void) - func needVideoOutput() -> Bool - func setVideoOutout(ouput: AVPlayerItemVideoOutput) - func preferFPS() -> Int -} - -class CommonPlayerViewController: AVPlayerViewController { - let danMuView = DanmakuView() - var allowChangeSpeed = true - var playerStartPos: Int? - private var retryCount = 0 - private let maxRetryCount = 3 - private var observer: NSKeyValueObservation? - private var rateObserver: NSKeyValueObservation? - private var debugView: UILabel? - var maskProvider: MaskProvider? - - var playerItem: AVPlayerItem? { - didSet { - if let playerItem = playerItem { - removeObservarPlayerItem() - observePlayerItem(playerItem) - if let playerInfo = playerInfo { - playerItem.externalMetadata = playerInfo - } - } - } - } - - override var player: AVPlayer? { - didSet { - if let player = player { - rateObserver = player.observe(\.rate, options: [.old, .new]) { - [weak self] player, _ in - guard let self = self else { return } - playerRateDidChange(player: player) - } - danMuView.play() - } else { - rateObserver = nil - } - } - } - - var videoOutput: AVPlayerItemVideoOutput? - - private var playerInfo: [AVMetadataItem]? - - deinit { - stopDebug() - } - - override func viewDidLoad() { - super.viewDidLoad() - appliesPreferredDisplayCriteriaAutomatically = Settings.contentMatch - allowsPictureInPicturePlayback = true - delegate = self - initDanmuView() - setupPlayerMenu() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - danMuView.recaculateTracks() - danMuView.paddingTop = 5 - danMuView.trackHeight = 50 - danMuView.displayArea = Settings.danmuArea.percent - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillAppear(animated) - danMuView.stop() - } - - func extraInfoForPlayerError() -> String { - return "" - } - - func playerStatusDidChange() { - Logger.debug("player status: \(player?.currentItem?.status.rawValue ?? -1)") - switch player?.currentItem?.status { - case .readyToPlay: - if maskProvider?.needVideoOutput() == true { - setUpOutput() - } - startPlay() - case .failed: - removeObservarPlayerItem() - Logger.debug(player?.currentItem?.error ?? "no error") - Logger.debug(player?.currentItem?.errorLog() ?? "no error log") - if retryCount < maxRetryCount, !retryPlay() { - let log = playerItem?.errorLog() - let errorLogData = log?.extendedLogData() ?? Data() - var str = String(data: errorLogData, encoding: .utf8) ?? "" - str = str.split(separator: "\n").dropFirst(4).joined() - showErrorAlertAndExit(title: "播放器失败", message: str + extraInfoForPlayerError()) - } - retryCount += 1 - default: - break - } - } - - func playerRateDidChange(player: AVPlayer) {} - - @MainActor func setPlayerInfo(title: String?, subTitle: String?, desp: String?, pic: URL?) { - let desp = desp?.components(separatedBy: "\n").joined(separator: " ") - let mapping: [AVMetadataIdentifier: Any?] = [ - .commonIdentifierTitle: title, - .iTunesMetadataTrackSubTitle: subTitle, - .commonIdentifierDescription: desp, - ] - let meta = mapping.compactMap { createMetadataItem(for: $0, value: $1) } - playerInfo = meta - playerItem?.externalMetadata = meta - - if let pic = pic { - let resource = Kingfisher.ImageResource(downloadURL: pic) - KingfisherManager.shared.retrieveImage(with: resource) { - [weak self] result in - guard let self = self, - let data = try? result.get().image.pngData(), - let item = self.createMetadataItem(for: .commonIdentifierArtwork, value: data) - else { return } - - self.playerInfo?.removeAll { $0.identifier == .commonIdentifierArtwork } - self.playerInfo?.append(item) - self.playerItem?.externalMetadata = self.playerInfo ?? [] - } - } - } - - func createMetadataItem(for identifier: AVMetadataIdentifier, - value: Any?) -> AVMetadataItem? - { - if value == nil { return nil } - let item = AVMutableMetadataItem() - item.identifier = identifier - item.value = value as? NSCopying & NSObjectProtocol - // Specify "und" to indicate an undefined language. - item.extendedLanguageTag = "und" - return item.copy() as? AVMetadataItem - } - - private func setupPlayerMenu() { - var menus = [UIMenuElement]() - let danmuImage = UIImage(systemName: "list.bullet.rectangle.fill") - let danmuImageDisable = UIImage(systemName: "list.bullet.rectangle") - let danmuAction = UIAction(title: "Show Danmu", image: danMuView.isHidden ? danmuImageDisable : danmuImage) { - [weak self] action in - guard let self = self else { return } - Settings.defaultDanmuStatus.toggle() - self.danMuView.isHidden.toggle() - action.image = self.danMuView.isHidden ? danmuImageDisable : danmuImage - } - menus.append(danmuAction) - - let danmuDurationMenu = UIMenu(title: "弹幕展示时长", options: [.displayInline, .singleSelection], children: [4, 6, 8].map { dur in - UIAction(title: "\(dur) 秒", state: dur == Settings.danmuDuration ? .on : .off) { _ in Settings.danmuDuration = dur } - }) - let danmuAILevelMenu = UIMenu(title: "弹幕屏蔽等级", options: [.displayInline, .singleSelection], children: [Int32](1...10).map { level in - UIAction(title: "\(level)", state: level == Settings.danmuAILevel ? .on : .off) { _ in Settings.danmuAILevel = level } - }) - let danmuSettingMenu = UIMenu(title: "弹幕设置", image: UIImage(systemName: "keyboard.badge.ellipsis"), children: [danmuDurationMenu, danmuAILevelMenu]) - menus.append(danmuSettingMenu) - - let debugEnableImage = UIImage(systemName: "terminal.fill") - let debugDisableImage = UIImage(systemName: "terminal") - let debugAction = UIAction(title: "Debug", image: debugEnable ? debugEnableImage : debugDisableImage) { - [weak self] action in - guard let self = self else { return } - if self.debugEnable { - self.stopDebug() - action.image = debugDisableImage - } else { - action.image = debugEnableImage - self.startDebug() - } - } - - if allowChangeSpeed { - // Create ∞ and ⚙ images. - let loopImage = UIImage(systemName: "infinity") - let gearImage = UIImage(systemName: "gearshape") - - // Create an action to enable looping playback. - let loopAction = UIAction(title: "循环播放", image: loopImage, state: Settings.loopPlay ? .on : .off) { - action in - action.state = (action.state == .off) ? .on : .off - Settings.loopPlay = action.state == .on - } - - let speedActions = PlaySpeed.blDefaults.map { playSpeed in - UIAction(title: playSpeed.name, state: player?.rate ?? 1 == playSpeed.value ? .on : .off) { [weak self] _ in - self?.player?.currentItem?.audioTimePitchAlgorithm = .timeDomain - self?.selectSpeed(AVPlaybackSpeed(rate: playSpeed.value, localizedName: playSpeed.name)) - self?.danMuView.playingSpeed = playSpeed.value - } - } - let playSpeedMenu = UIMenu(title: "播放速度", options: [.displayInline, .singleSelection], children: speedActions) - let menu = UIMenu(title: "播放设置", image: gearImage, children: [playSpeedMenu, loopAction, debugAction]) - menus.append(menu) - } else { - menus.append(debugAction) - } - - transportBarCustomMenuItems = menus - } - - private func removeObservarPlayerItem() { - NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) - } - - private func observePlayerItem(_ playerItem: AVPlayerItem) { - observer = playerItem.observe(\.status, options: [.new, .old]) { - [weak self] _, _ in - self?.playerStatusDidChange() - } - NotificationCenter.default.addObserver(self, - selector: #selector(playerDidFinishPlaying), - name: .AVPlayerItemDidPlayToEndTime, - object: playerItem) - } - - func setupMask() { - guard let maskProvider else { return } -// danMuView.backgroundColor = UIColor.red.withAlphaComponent(0.5) - Logger.info("mask provider is \(maskProvider)") - let interval = CMTime(seconds: 1.0 / CGFloat(maskProvider.preferFPS()), - preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - player?.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { - [weak self, weak maskProvider] time in - guard let self else { return } - guard self.danMuView.isHidden == false else { return } - maskProvider?.getMask(for: time, frame: self.danMuView.frame) { - maskLayer in - self.danMuView.layer.mask = maskLayer - } - }) - } - - func retryPlay() -> Bool { - return false - } - - @objc private func playerDidFinishPlaying() { - playDidEnd() - } - - func playDidEnd() {} - - func showErrorAlertAndExit(title: String = "播放失败", message: String = "未知错误") { - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let actionOk = UIAlertAction(title: "OK", style: .default) { - [weak self] _ in - self?.dismiss(animated: true, completion: nil) - } - alertController.addAction(actionOk) - present(alertController, animated: true, completion: nil) - } - - private func startPlay() { - guard player?.rate == 0 && player?.error == nil else { return } - if let playerStartPos = playerStartPos { - player?.seek(to: CMTime(seconds: Double(playerStartPos), preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) - } - player?.play() - } - - private func fetchDebugInfo() -> String { - let bitrateStr: (Double) -> String = { - bit in - String(format: "%.2fMbps", bit / 1024.0 / 1024.0) - } - guard let player else { return "Player no init" } - - var logs = """ - \(additionDebugInfo()) - time control status: \(player.timeControlStatus.rawValue) \(player.reasonForWaitingToPlay?.rawValue ?? "") - player status:\(player.status.rawValue) - """ - - guard let log = player.currentItem?.accessLog() else { return logs } - guard let item = log.events.last else { return logs } - let uri = item.uri ?? "" - let addr = item.serverAddress ?? "" - let changes = item.numberOfServerAddressChanges - let dropped = item.numberOfDroppedVideoFrames - let stalls = item.numberOfStalls - let averageAudioBitrate = item.averageAudioBitrate - let averageVideoBitrate = item.averageVideoBitrate - let indicatedBitrate = item.indicatedBitrate - let observedBitrate = item.observedBitrate - logs += """ - uri:\(uri), ip:\(addr), change:\(changes) - drop:\(dropped) stalls:\(stalls) - bitrate audio:\(bitrateStr(averageAudioBitrate)), video: \(bitrateStr(averageVideoBitrate)) - observedBitrate:\(bitrateStr(observedBitrate)) - indicatedAverageBitrate:\(bitrateStr(indicatedBitrate)) - maskProvider: \(String(describing: maskProvider)) - """ - return logs - } - - func additionDebugInfo() -> String { return "" } - - var debugTimer: Timer? - var debugEnable: Bool { debugTimer?.isValid ?? false } - private func startDebug() { - if debugView == nil { - debugView = UILabel() - debugView?.backgroundColor = UIColor.black.withAlphaComponent(0.8) - debugView?.textColor = UIColor.white - view.addSubview(debugView!) - debugView?.numberOfLines = 0 - debugView?.font = UIFont.systemFont(ofSize: 26) - debugView?.snp.makeConstraints { make in - make.top.equalToSuperview().offset(12) - make.right.equalToSuperview().offset(-12) - make.width.equalTo(800) - } - } - debugView?.isHidden = false - debugTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in - let info = self?.fetchDebugInfo() - self?.debugView?.text = info - } - } - - private func stopDebug() { - debugTimer?.invalidate() - debugTimer = nil - debugView?.isHidden = true - } - - private func initDanmuView() { - view.addSubview(danMuView) - danMuView.accessibilityLabel = "danmuView" - danMuView.makeConstraintsToBindToSuperview() - danMuView.isHidden = !Settings.defaultDanmuStatus - } - - func setUpOutput() { - guard videoOutput == nil, let videoItem = player?.currentItem else { return } - let pixelBuffAttributes = [ - kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, - ] - let videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: pixelBuffAttributes) - videoItem.add(videoOutput) - self.videoOutput = videoOutput - maskProvider?.setVideoOutout(ouput: videoOutput) - } - - func ensureDanmuViewFront() { - view.bringSubviewToFront(danMuView) - danMuView.play() - } -} - -extension CommonPlayerViewController: AVPlayerViewControllerDelegate { - @objc func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool { - if let presentedViewController = UIViewController.topMostViewController() as? AVPlayerViewController, - presentedViewController == playerViewController - { - return true - } - return false - } - - @objc func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool { - return true - } - - @objc func playerViewController(_ playerViewController: AVPlayerViewController, - restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) - { - let presentedViewController = UIViewController.topMostViewController() - if presentedViewController is AVPlayerViewController { - let parent = presentedViewController.presentingViewController - presentedViewController.dismiss(animated: false) { - parent?.present(playerViewController, animated: false) - completionHandler(true) - (playerViewController as? CommonPlayerViewController)?.ensureDanmuViewFront() - } - } else { - presentedViewController.present(playerViewController, animated: false) { - completionHandler(true) - (playerViewController as? CommonPlayerViewController)?.ensureDanmuViewFront() - } - } - } -} - -struct PlaySpeed { - var name: String - var value: Float -} - -extension PlaySpeed: Equatable { - static let `default` = PlaySpeed(name: "1X", value: 1) - - static let blDefaults = [ - PlaySpeed(name: "0.5X", value: 0.5), - PlaySpeed(name: "0.75X", value: 0.75), - PlaySpeed(name: "1X", value: 1), - PlaySpeed(name: "1.25X", value: 1.25), - PlaySpeed(name: "1.5X", value: 1.5), - PlaySpeed(name: "2X", value: 2), - ] -} diff --git a/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift b/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift index 72bdaed4..f5654d8a 100644 --- a/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift +++ b/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift @@ -8,7 +8,7 @@ import AVKit import UIKit -class NewCommonPlayerViewController: UIViewController { +class CommonPlayerViewController: UIViewController { private let playerVC = AVPlayerViewController() private var activePlugins = [CommonPlayerPlugin]() private var observations = Set() @@ -67,7 +67,7 @@ class NewCommonPlayerViewController: UIViewController { } } -extension NewCommonPlayerViewController { +extension CommonPlayerViewController { private func playerDidChange(player: AVPlayer?) { if let player { activePlugins.forEach { $0.playerDidChange(player: player) } @@ -126,9 +126,9 @@ extension NewCommonPlayerViewController { } } -extension NewCommonPlayerViewController: AVPlayerViewControllerDelegate { +extension CommonPlayerViewController: AVPlayerViewControllerDelegate { @objc func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool { - if let presentedViewController = UIViewController.topMostViewController() as? NewCommonPlayerViewController, + if let presentedViewController = UIViewController.topMostViewController() as? CommonPlayerViewController, presentedViewController.playerVC == playerViewController { dismiss(animated: true) @@ -157,7 +157,7 @@ extension NewCommonPlayerViewController: AVPlayerViewControllerDelegate { completionHandler(false) return } - if presentedViewController is NewCommonPlayerViewController { + if presentedViewController is CommonPlayerViewController { let parent = presentedViewController.presentingViewController presentedViewController.dismiss(animated: false) { parent?.present(containerPlayer, animated: false) @@ -172,6 +172,6 @@ extension NewCommonPlayerViewController: AVPlayerViewControllerDelegate { class PipRecorder { static let shared = PipRecorder() - var playingPipViewController = [NewCommonPlayerViewController]() + var playingPipViewController = [CommonPlayerViewController]() } } diff --git a/BilibiliLive/Component/Player/CommonPlayerPlugin.swift b/BilibiliLive/Component/Player/Plugins/CommonPlayerPlugin.swift similarity index 100% rename from BilibiliLive/Component/Player/CommonPlayerPlugin.swift rename to BilibiliLive/Component/Player/Plugins/CommonPlayerPlugin.swift diff --git a/BilibiliLive/Component/Player/DanmuViewPlugin.swift b/BilibiliLive/Component/Player/Plugins/DanmuViewPlugin.swift similarity index 87% rename from BilibiliLive/Component/Player/DanmuViewPlugin.swift rename to BilibiliLive/Component/Player/Plugins/DanmuViewPlugin.swift index d1289ab3..8e2e1758 100644 --- a/BilibiliLive/Component/Player/DanmuViewPlugin.swift +++ b/BilibiliLive/Component/Player/Plugins/DanmuViewPlugin.swift @@ -16,10 +16,6 @@ protocol DanmuProviderProtocol { } class DanmuViewPlugin: NSObject { - var showDanmu = Settings.defaultDanmuStatus { - didSet { danMuView.isHidden = !showDanmu } - } - let danMuView = DanmakuView() init(provider: DanmuProviderProtocol) { @@ -30,6 +26,13 @@ class DanmuViewPlugin: NSObject { .sink { [weak self] in self?.shoot($0) }.store(in: &cancellable) + + Defaults.shared.$showDanmu + .receive(on: DispatchQueue.main) + .sink { + [weak self] in + self?.danMuView.isHidden = !$0 + }.store(in: &cancellable) } private let danmuProvider: DanmuProviderProtocol @@ -49,7 +52,7 @@ extension DanmuViewPlugin: CommonPlayerPlugin { player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), queue: DispatchQueue.global()) { [weak self] time in guard let self else { return } - if danMuView.isHidden { return } + if !Defaults.shared.showDanmu { return } let seconds = time.seconds danmuProvider.playerTimeChange(time: seconds) } @@ -66,7 +69,6 @@ extension DanmuViewPlugin: CommonPlayerPlugin { danMuView.makeConstraintsToBindToSuperview() danMuView.setNeedsLayout() danMuView.layoutIfNeeded() - danMuView.isHidden = showDanmu danMuView.paddingTop = 5 danMuView.trackHeight = 50 danMuView.displayArea = Settings.danmuArea.percent @@ -85,11 +87,9 @@ extension DanmuViewPlugin: CommonPlayerPlugin { let danmuImage = UIImage(systemName: "list.bullet.rectangle.fill") let danmuImageDisable = UIImage(systemName: "list.bullet.rectangle") let danmuAction = UIAction(title: "Show Danmu", image: danMuView.isHidden ? danmuImageDisable : danmuImage) { - [weak self] action in - guard let self = self else { return } - Settings.defaultDanmuStatus.toggle() - self.danMuView.isHidden.toggle() - action.image = self.danMuView.isHidden ? danmuImageDisable : danmuImage + action in + Defaults.shared.showDanmu.toggle() + action.image = Defaults.shared.showDanmu ? danmuImage : danmuImageDisable } let danmuDurationMenu = UIMenu(title: "弹幕展示时长", options: [.displayInline, .singleSelection], children: [4, 6, 8].map { dur in UIAction(title: "\(dur) 秒", state: dur == Settings.danmuDuration ? .on : .off) { _ in Settings.danmuDuration = dur } diff --git a/BilibiliLive/Component/Player/DebugPlugin.swift b/BilibiliLive/Component/Player/Plugins/DebugPlugin.swift similarity index 100% rename from BilibiliLive/Component/Player/DebugPlugin.swift rename to BilibiliLive/Component/Player/Plugins/DebugPlugin.swift diff --git a/BilibiliLive/Component/Player/MaskViewPugin.swift b/BilibiliLive/Component/Player/Plugins/MaskViewPugin.swift similarity index 100% rename from BilibiliLive/Component/Player/MaskViewPugin.swift rename to BilibiliLive/Component/Player/Plugins/MaskViewPugin.swift diff --git a/BilibiliLive/Component/Player/SpeedChangerPlugin.swift b/BilibiliLive/Component/Player/Plugins/SpeedChangerPlugin.swift similarity index 77% rename from BilibiliLive/Component/Player/SpeedChangerPlugin.swift rename to BilibiliLive/Component/Player/Plugins/SpeedChangerPlugin.swift index 7b7a7ec6..6ba63b6e 100644 --- a/BilibiliLive/Component/Player/SpeedChangerPlugin.swift +++ b/BilibiliLive/Component/Player/Plugins/SpeedChangerPlugin.swift @@ -42,3 +42,21 @@ class SpeedChangerPlugin: NSObject, CommonPlayerPlugin { return [menu] } } + +struct PlaySpeed { + var name: String + var value: Float +} + +extension PlaySpeed: Equatable { + static let `default` = PlaySpeed(name: "1X", value: 1) + + static let blDefaults = [ + PlaySpeed(name: "0.5X", value: 0.5), + PlaySpeed(name: "0.75X", value: 0.75), + PlaySpeed(name: "1X", value: 1), + PlaySpeed(name: "1.25X", value: 1.25), + PlaySpeed(name: "1.5X", value: 1.5), + PlaySpeed(name: "2X", value: 2), + ] +} diff --git a/BilibiliLive/Module/Live/URLPlayPlugin.swift b/BilibiliLive/Component/Player/Plugins/URLPlayPlugin.swift similarity index 100% rename from BilibiliLive/Module/Live/URLPlayPlugin.swift rename to BilibiliLive/Component/Player/Plugins/URLPlayPlugin.swift diff --git a/BilibiliLive/Component/Settings.swift b/BilibiliLive/Component/Settings.swift index 073cca2d..97f4042f 100644 --- a/BilibiliLive/Component/Settings.swift +++ b/BilibiliLive/Component/Settings.swift @@ -5,7 +5,9 @@ // Created by whw on 2022/10/19. // +import Combine import Foundation +import SwiftUI enum FeedDisplayStyle: Codable, CaseIterable { case large @@ -17,6 +19,13 @@ enum FeedDisplayStyle: Codable, CaseIterable { } } +class Defaults { + static let shared = Defaults() + private init() {} + + @Published(key: "Settings.danmuStatus") var showDanmu = true +} + enum Settings { @UserDefaultCodable("Settings.displayStyle", defaultValue: .normal) static var displayStyle: FeedDisplayStyle @@ -45,9 +54,6 @@ enum Settings { @UserDefault("Settings.preferAvc", defaultValue: false) static var preferAvc: Bool - @UserDefault("Settings.defaultDanmuStatus", defaultValue: true) - static var defaultDanmuStatus: Bool - @UserDefault("Settings.danmuMask", defaultValue: true) static var danmuMask: Bool diff --git a/BilibiliLive/Component/Video/BMaskProvider.swift b/BilibiliLive/Component/Video/MaskProvider/BMaskProvider.swift similarity index 100% rename from BilibiliLive/Component/Video/BMaskProvider.swift rename to BilibiliLive/Component/Video/MaskProvider/BMaskProvider.swift diff --git a/BilibiliLive/Component/Video/MaskProvider/MaskProvider.swift b/BilibiliLive/Component/Video/MaskProvider/MaskProvider.swift new file mode 100644 index 00000000..8a249b90 --- /dev/null +++ b/BilibiliLive/Component/Video/MaskProvider/MaskProvider.swift @@ -0,0 +1,14 @@ +// +// MaskProvider.swift +// BilibiliLive +// +// Created by yicheng on 2024/6/10. +// +import AVKit + +protocol MaskProvider: AnyObject { + func getMask(for time: CMTime, frame: CGRect, onGet: @escaping (CALayer) -> Void) + func needVideoOutput() -> Bool + func setVideoOutout(ouput: AVPlayerItemVideoOutput) + func preferFPS() -> Int +} diff --git a/BilibiliLive/Component/Video/VMaskProvider.swift b/BilibiliLive/Component/Video/MaskProvider/VMaskProvider.swift similarity index 100% rename from BilibiliLive/Component/Video/VMaskProvider.swift rename to BilibiliLive/Component/Video/MaskProvider/VMaskProvider.swift diff --git a/BilibiliLive/Component/Video/NewVideoPlayerViewController.swift b/BilibiliLive/Component/Video/NewVideoPlayerViewController.swift deleted file mode 100644 index 48a58e40..00000000 --- a/BilibiliLive/Component/Video/NewVideoPlayerViewController.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// NewVideoPlayerViewController.swift -// BilibiliLive -// -// Created by yicheng on 2024/5/23. -// - -import AVKit -import Combine -import UIKit - -class NewVideoPlayerViewController: NewCommonPlayerViewController { - var data: VideoDetail? - var nextProvider: VideoNextProvider? - - init(playInfo: PlayInfo) { - viewModel = NewVideoPlayerViewModel(playInfo: playInfo) - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private let viewModel: NewVideoPlayerViewModel - private var cancelable = Set() - - override func viewDidLoad() { - super.viewDidLoad() - viewModel.nextProvider = nextProvider - viewModel.onPluginReady.receive(on: DispatchQueue.main).sink { [weak self] completion in - switch completion { - case let .failure(err): - self?.showErrorAlertAndExit(message: err) - default: - break - } - } receiveValue: { [weak self] plugins in - plugins.forEach { self?.addPlugin(plugin: $0) } - }.store(in: &cancelable) - viewModel.onPluginRemove.sink { [weak self] in - self?.removePlugin(plugin: $0) - }.store(in: &cancelable) - viewModel.onExit = { [weak self] in - self?.dismiss(animated: true) - } - Task { - await viewModel.load() - } - } -} diff --git a/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift b/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift index 586cbfc0..9a7f55c1 100644 --- a/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift +++ b/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift @@ -21,7 +21,7 @@ struct PlayerDetailData { var videoPlayURLInfo: VideoPlayURLInfo } -class NewVideoPlayerViewModel { +class VideoPlayerViewModel { var onPluginReady = PassthroughSubject<[CommonPlayerPlugin], String>() var onPluginRemove = PassthroughSubject() var onExit: (() -> Void)? diff --git a/BilibiliLive/Component/Video/BUpnpPlugin.swift b/BilibiliLive/Component/Video/Plugins/BUpnpPlugin.swift similarity index 81% rename from BilibiliLive/Component/Video/BUpnpPlugin.swift rename to BilibiliLive/Component/Video/Plugins/BUpnpPlugin.swift index d1b0e127..80e541a0 100644 --- a/BilibiliLive/Component/Video/BUpnpPlugin.swift +++ b/BilibiliLive/Component/Video/Plugins/BUpnpPlugin.swift @@ -9,12 +9,26 @@ import AVFoundation import Foundation class BUpnpPlugin: NSObject, CommonPlayerPlugin { let duration: Int? + weak var player: AVPlayer? init(duration: Int?) { self.duration = duration } + func pause() { + player?.pause() + } + + func resume() { + player?.play() + } + + func seek(to time: TimeInterval) { + player?.seek(to: CMTime(seconds: time, preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) + } + func playerWillStart(player: AVPlayer) { + self.player = player guard let duration else { return } player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 5, preferredTimescale: 1), queue: .global()) { time in DispatchQueue.main.async { diff --git a/BilibiliLive/Component/Video/BVideoClipsPlugin.swift b/BilibiliLive/Component/Video/Plugins/BVideoClipsPlugin.swift similarity index 100% rename from BilibiliLive/Component/Video/BVideoClipsPlugin.swift rename to BilibiliLive/Component/Video/Plugins/BVideoClipsPlugin.swift diff --git a/BilibiliLive/Component/Video/BVideoInfoPlugin.swift b/BilibiliLive/Component/Video/Plugins/BVideoInfoPlugin.swift similarity index 100% rename from BilibiliLive/Component/Video/BVideoInfoPlugin.swift rename to BilibiliLive/Component/Video/Plugins/BVideoInfoPlugin.swift diff --git a/BilibiliLive/Component/Video/BVideoPlayPlugin.swift b/BilibiliLive/Component/Video/Plugins/BVideoPlayPlugin.swift similarity index 100% rename from BilibiliLive/Component/Video/BVideoPlayPlugin.swift rename to BilibiliLive/Component/Video/Plugins/BVideoPlayPlugin.swift diff --git a/BilibiliLive/Component/Video/VideoPlayListPlugin.swift b/BilibiliLive/Component/Video/Plugins/VideoPlayListPlugin.swift similarity index 100% rename from BilibiliLive/Component/Video/VideoPlayListPlugin.swift rename to BilibiliLive/Component/Video/Plugins/VideoPlayListPlugin.swift diff --git a/BilibiliLive/Component/Video/VideoDetailViewController.swift b/BilibiliLive/Component/Video/VideoDetailViewController.swift index 84a91097..f9ef9a3f 100644 --- a/BilibiliLive/Component/Video/VideoDetailViewController.swift +++ b/BilibiliLive/Component/Video/VideoDetailViewController.swift @@ -147,7 +147,7 @@ class VideoDetailViewController: UIViewController { } else { vc.present(self, animated: false) { [weak self] in guard let self else { return } - let player = NewVideoPlayerViewController(playInfo: PlayInfo(aid: self.aid, cid: self.cid, epid: self.epid, isBangumi: self.isBangumi)) + let player = VideoPlayerViewController(playInfo: PlayInfo(aid: self.aid, cid: self.cid, epid: self.epid, isBangumi: self.isBangumi)) self.present(player, animated: true) } } @@ -354,7 +354,7 @@ class VideoDetailViewController: UIViewController { } @IBAction func actionPlay(_ sender: Any) { - let player = NewVideoPlayerViewController(playInfo: PlayInfo(aid: aid, cid: cid, epid: epid, isBangumi: isBangumi)) + let player = VideoPlayerViewController(playInfo: PlayInfo(aid: aid, cid: cid, epid: epid, isBangumi: isBangumi)) player.data = data if pages.count > 0, let index = pages.firstIndex(where: { $0.cid == cid }) { let seq = pages.dropFirst(index).map({ PlayInfo(aid: aid, cid: $0.cid, epid: $0.epid, isBangumi: isBangumi) }) diff --git a/BilibiliLive/Component/Video/VideoPlayerViewController.swift b/BilibiliLive/Component/Video/VideoPlayerViewController.swift index a6e94b5f..5d1968cb 100644 --- a/BilibiliLive/Component/Video/VideoPlayerViewController.swift +++ b/BilibiliLive/Component/Video/VideoPlayerViewController.swift @@ -1,16 +1,12 @@ // -// VideoPlayerViewController.swift +// NewVideoPlayerViewController.swift // BilibiliLive // -// Created by Etan Chen on 2021/4/4. +// Created by yicheng on 2024/5/23. // -import Alamofire -import AVFoundation import AVKit -import Kingfisher -import SwiftyJSON -import SwiftyXMLParser +import Combine import UIKit struct PlayInfo { @@ -46,9 +42,11 @@ class VideoNextProvider { } class VideoPlayerViewController: CommonPlayerViewController { - var playInfo: PlayInfo + var data: VideoDetail? + var nextProvider: VideoNextProvider? + init(playInfo: PlayInfo) { - self.playInfo = playInfo + viewModel = VideoPlayerViewModel(playInfo: playInfo) super.init(nibName: nil, bundle: nil) } @@ -57,389 +55,30 @@ class VideoPlayerViewController: CommonPlayerViewController { fatalError("init(coder:) has not been implemented") } - var data: VideoDetail? - var nextProvider: VideoNextProvider? - private var allDanmus = [Danmu]() - private var playingDanmus = [Danmu]() - private var playerDelegate: BilibiliVideoResourceLoaderDelegate? - private let danmuProvider = VideoDanmuProvider() - private var clipInfos: [VideoPlayURLInfo.ClipInfo]? - private var skipAction: UIAction? - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - guard let currentTime = player?.currentTime().seconds, currentTime > 0 else { return } - - if let cid = playInfo.cid, cid > 0 { - WebRequest.reportWatchHistory(aid: playInfo.aid, cid: cid, currentTime: Int(currentTime)) - } - BiliBiliUpnpDMR.shared.sendStatus(status: .stop) - } + private let viewModel: VideoPlayerViewModel + private var cancelable = Set() override func viewDidLoad() { super.viewDidLoad() - Task { - await initPlayer() - } - danmuProvider.onShowDanmu = { - [weak self] in - self?.danMuView.shoot(danmaku: $0) - } - } - - private func initPlayer() async { - if !playInfo.isCidVaild { - do { - playInfo.cid = try await WebRequest.requestCid(aid: playInfo.aid) - } catch let err { - self.showErrorAlertAndExit(message: "请求cid失败,\(err.localizedDescription)") - } - } - await fetchVideoData() - await danmuProvider.initVideo(cid: playInfo.cid!, startPos: playerStartPos ?? 0) - } - - private func playmedia(urlInfo: VideoPlayURLInfo, playerInfo: PlayerInfo?) async { - let playURL = URL(string: BilibiliVideoResourceLoaderDelegate.URLs.play)! - let headers: [String: String] = [ - "User-Agent": Keys.userAgent, - "Referer": "https://www.bilibili.com/video/av\(playInfo.aid)", - ] - let asset = AVURLAsset(url: playURL, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) - playerDelegate = BilibiliVideoResourceLoaderDelegate() - playerDelegate?.setBilibili(info: urlInfo, subtitles: playerInfo?.subtitle?.subtitles ?? [], aid: playInfo.aid) - if Settings.contentMatchOnlyInHDR { - if playerDelegate?.isHDR != true { - appliesPreferredDisplayCriteriaAutomatically = false - } - } - asset.resourceLoader.setDelegate(playerDelegate, queue: DispatchQueue(label: "loader")) - let requestedKeys = ["playable"] - await asset.loadValues(forKeys: requestedKeys) - prepare(toPlay: asset, withKeys: requestedKeys) - updatePlayerCharpter(playerInfo: playerInfo) - BiliBiliUpnpDMR.shared.sendVideoSwitch(aid: playInfo.aid, cid: playInfo.cid ?? 0) - } - - private func updatePlayerCharpter(playerInfo: PlayerInfo?) { - let group = DispatchGroup() - var metas = [AVTimedMetadataGroup]() - for viewPoint in playerInfo?.view_points ?? [] { - group.enter() - convertTimedMetadataGroup(viewPoint: viewPoint) { - metas.append($0) - group.leave() - } - } - group.notify(queue: .main) { - if metas.count > 0 { - self.playerItem?.navigationMarkerGroups = [AVNavigationMarkersGroup(title: nil, timedNavigationMarkers: metas)] - } - } - } - - override func extraInfoForPlayerError() -> String { - return playerDelegate?.infoDebugText ?? "-" - } - - override func additionDebugInfo() -> String { - if let port = playerDelegate?.httpPort.string() { - return " :" + port - } - return "" - } - - override func playerStatusDidChange() { - super.playerStatusDidChange() - switch player?.status { - case .readyToPlay: - BiliBiliUpnpDMR.shared.sendStatus(status: .playing) - case .failed: - BiliBiliUpnpDMR.shared.sendStatus(status: .stop) - default: - break - } - } - -// override func playerRateDidChange(player: AVPlayer) { -// if player.rate > 0, danMuView.status == .pause { -// danMuView.play() -// } else if player.rate == 0, danMuView.status == .play { -// danMuView.pause() -// } -// } - - func playNext() -> Bool { - if let next = nextProvider?.getNext() { - playInfo = next - playerStartPos = 0 - Task { - await initPlayer() - } - return true - } - return false - } - - override func playDidEnd() { - BiliBiliUpnpDMR.shared.sendStatus(status: .end) - if !playNext() { - if Settings.loopPlay { - nextProvider?.reset() - if !playNext() { - playerItem?.seek(to: .zero, completionHandler: nil) - player?.play() - } - return - } - dismiss(animated: true) - } - } - - private func convertTimedMetadataGroup(viewPoint: PlayerInfo.ViewPoint, onResult: ((AVTimedMetadataGroup) -> Void)? = nil) { - let mapping: [AVMetadataIdentifier: Any?] = [ - .commonIdentifierTitle: viewPoint.content, - ] - - var metadatas = mapping.compactMap { createMetadataItem(for: $0, value: $1) } - let timescale: Int32 = 600 - let cmStartTime = CMTimeMakeWithSeconds(viewPoint.from, preferredTimescale: timescale) - let cmEndTime = CMTimeMakeWithSeconds(viewPoint.to, preferredTimescale: timescale) - let timeRange = CMTimeRangeFromTimeToTime(start: cmStartTime, end: cmEndTime) - if let pic = viewPoint.imgUrl?.addSchemeIfNeed() { - let resource = Kingfisher.ImageResource(downloadURL: pic) - KingfisherManager.shared.retrieveImage(with: resource) { - [weak self] result in - guard let self = self, - let data = try? result.get().image.pngData(), - let item = self.createMetadataItem(for: .commonIdentifierArtwork, value: data) - else { - onResult?(AVTimedMetadataGroup(items: metadatas, timeRange: timeRange)) - return - } - metadatas.append(item) - onResult?(AVTimedMetadataGroup(items: metadatas, timeRange: timeRange)) - } - } else { - onResult?(AVTimedMetadataGroup(items: metadatas, timeRange: timeRange)) - } - } -} - -// MARK: - Requests - -extension VideoPlayerViewController { - func fetchVideoData() async { - assert(playInfo.isCidVaild) - let aid = playInfo.aid - let cid = playInfo.cid! - let info = try? await WebRequest.requestPlayerInfo(aid: aid, cid: cid) - do { - let playData: VideoPlayURLInfo - if playInfo.isBangumi { - playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid) - clipInfos = playData.clip_info_list - } else { - playData = try await WebRequest.requestPlayUrl(aid: aid, cid: cid) - } - if info?.last_play_cid == cid, let startTime = info?.playTimeInSecond, playData.dash.duration - startTime > 5, Settings.continuePlay { - playerStartPos = startTime - } - - await playmedia(urlInfo: playData, playerInfo: info) - - if Settings.danmuMask { - if let mask = info?.dm_mask, - let video = playData.dash.video.first, - let fps = info?.dm_mask?.fps, fps > 0 - { - maskProvider = BMaskProvider(info: mask, videoSize: CGSize(width: video.width ?? 0, height: video.height ?? 0)) - } else if Settings.vnMask { - maskProvider = VMaskProvider() - } - setupMask() - } - - if data == nil { - data = try? await WebRequest.requestDetailVideo(aid: aid) - } - setPlayerInfo(title: data?.title, subTitle: data?.ownerName, desp: data?.View.desc, pic: data?.pic) - } catch let err { - if case let .statusFail(code, message) = err as? RequestError { - if code == -404 || code == -10403 { - // 解锁港澳台番剧处理 - do { - if let ok = try await fetchAreaLimitVideoData(), ok { - return - } - } catch let err { - showErrorAlertAndExit(message: "请求失败,\(err)") - } - } - showErrorAlertAndExit(message: "请求失败\(code) \(message),可能需要大会员") - } else if info?.is_upower_exclusive == true { - showErrorAlertAndExit(message: "请求失败,该视频为充电专属视频 \(err)") - } else { - showErrorAlertAndExit(message: "请求失败,\(err)") - } - } - } - - func fetchAreaLimitVideoData() async throws -> Bool? { - guard Settings.areaLimitUnlock else { return false } - guard let epid = playInfo.epid, epid > 0 else { return false } - - let aid = playInfo.aid - let cid = playInfo.cid! - - let season = try await WebRequest.requestBangumiSeasonView(epid: epid) - let checkTitle = season.title.contains("僅") ? season.title : season.series_title - let checkAreaList = parseAreaByTitle(title: checkTitle) - guard !checkAreaList.isEmpty else { return false } - - let playData = try await requestAreaLimitPcgPlayUrl(epid: epid, cid: cid, areaList: checkAreaList) - guard let playData = playData else { return false } - - let info = try? await WebRequest.requestPlayerInfo(aid: aid, cid: cid) - if info?.last_play_cid == cid, let startTime = info?.playTimeInSecond, playData.dash.duration - startTime > 5, Settings.continuePlay { - playerStartPos = startTime - } else { - playerStartPos = 0 + viewModel.nextProvider = nextProvider + viewModel.onPluginReady.receive(on: DispatchQueue.main).sink { [weak self] completion in + switch completion { + case let .failure(err): + self?.showErrorAlertAndExit(message: err) + default: + break + } + } receiveValue: { [weak self] plugins in + plugins.forEach { self?.addPlugin(plugin: $0) } + }.store(in: &cancelable) + viewModel.onPluginRemove.sink { [weak self] in + self?.removePlugin(plugin: $0) + }.store(in: &cancelable) + viewModel.onExit = { [weak self] in + self?.dismiss(animated: true) } - - await playmedia(urlInfo: playData, playerInfo: info) - - if Settings.danmuMask { - if let mask = info?.dm_mask, - let video = playData.dash.video.first, - let fps = info?.dm_mask?.fps, fps > 0 - { - maskProvider = BMaskProvider(info: mask, videoSize: CGSize(width: video.width ?? 0, height: video.height ?? 0)) - } else if Settings.vnMask { - maskProvider = VMaskProvider() - } - setupMask() - } - - if data == nil { - if let epi = season.episodes.first(where: { $0.ep_id == epid }) { - setPlayerInfo(title: epi.index + " " + (epi.index_title ?? ""), subTitle: season.up_info.uname, desp: season.evaluate, pic: epi.cover) - } - } else { - setPlayerInfo(title: data?.title, subTitle: data?.ownerName, desp: data?.View.desc, pic: data?.pic) - } - - return true - } - - private func requestAreaLimitPcgPlayUrl(epid: Int, cid: Int, areaList: [String]) async throws -> VideoPlayURLInfo? { - for area in areaList { - do { - return try await WebRequest.requestAreaLimitPcgPlayUrl(epid: epid, cid: cid, area: area) - } catch let err { - if area == areaList.last { - throw err - } else { - print(err) - } - } - } - - return nil - } - - private func parseAreaByTitle(title: String) -> [String] { - if title.isMatch(pattern: "[仅|僅].*[东南亚|其他]") { - // TODO: 未支持 - return [] - } - - var areas: [String] = [] - if title.isMatch(pattern: "僅.*台") { - areas.append("tw") - } - if title.isMatch(pattern: "僅.*港") { - areas.append("hk") - } - - if areas.isEmpty { - // 标题没有地区限制信息,返回尝试检测的区域 - return ["tw", "hk"] - } else { - return areas - } - } -} - -// MARK: - Player - -extension VideoPlayerViewController { - @MainActor - func prepare(toPlay asset: AVURLAsset, withKeys requestedKeys: [AnyHashable]) { - for thisKey in requestedKeys { - guard let thisKey = thisKey as? String else { - continue - } - var error: NSError? - let keyStatus = asset.statusOfValue(forKey: thisKey, error: &error) - if keyStatus == .failed { - showErrorAlertAndExit(title: error?.localizedDescription ?? "", message: error?.localizedFailureReason ?? "") - return - } - } - - if !asset.isPlayable { - showErrorAlertAndExit(message: "URL解析错误") - return - } - - playerItem = AVPlayerItem(asset: asset) - let player = AVPlayer(playerItem: playerItem) - player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), queue: .main) { [weak self] time in - guard let self else { return } - if self.danMuView.isHidden { return } - let seconds = time.seconds - self.danmuProvider.playerTimeChange(time: seconds) - - if let duration = self.data?.View.duration { - BiliBiliUpnpDMR.shared.sendProgress(duration: duration, current: Int(seconds)) - } - - if let clipInfos = self.clipInfos { - var matched = false - for clip in clipInfos { - if seconds > clip.start, seconds < clip.end { - let action = { - clip.skipped = true - self.player?.seek(to: CMTime(seconds: Double(clip.end), preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) - } - if !(clip.skipped ?? false), Settings.autoSkip { - action() - self.skipAction = nil - } else if self.skipAction?.accessibilityLabel != clip.a11Tag { - self.skipAction = UIAction(title: clip.customText) { _ in - action() - } - self.skipAction?.accessibilityLabel = clip.a11Tag - } - - self.contextualActions = [self.skipAction].compactMap { $0 } - matched = true - break - } - } - if !matched { - self.contextualActions = [] - } - } - } - if let defaultRate = self.player?.defaultRate, - let speed = PlaySpeed.blDefaults.first(where: { $0.value == defaultRate }) - { - self.player = player - selectSpeed(AVPlaybackSpeed(rate: speed.value, localizedName: speed.name)) - } else { - self.player = player + Task { + await viewModel.load() } } } diff --git a/BilibiliLive/Component/Video/VideoPlayerViewModel.swift b/BilibiliLive/Component/Video/VideoPlayerViewModel.swift new file mode 100644 index 00000000..9a7f55c1 --- /dev/null +++ b/BilibiliLive/Component/Video/VideoPlayerViewModel.swift @@ -0,0 +1,183 @@ +// +// NewVideoPlayerViewModel.swift +// BilibiliLive +// +// Created by yicheng on 2024/5/23. +// + +import Combine +import UIKit + +struct PlayerDetailData { + let aid: Int + let cid: Int + let epid: Int? // 港澳台解锁需要 + let isBangumi: Bool + + var playerStartPos: Int? + var detail: VideoDetail? + var clips: [VideoPlayURLInfo.ClipInfo]? + var playerInfo: PlayerInfo? + var videoPlayURLInfo: VideoPlayURLInfo +} + +class VideoPlayerViewModel { + var onPluginReady = PassthroughSubject<[CommonPlayerPlugin], String>() + var onPluginRemove = PassthroughSubject() + var onExit: (() -> Void)? + var nextProvider: VideoNextProvider? + + private var playInfo: PlayInfo + private let danmuProvider = VideoDanmuProvider() + private var videoDetail: VideoDetail? + private var cancellable = Set() + private var playPlugin: CommonPlayerPlugin? + + init(playInfo: PlayInfo) { + self.playInfo = playInfo + } + + func load() async { + do { + let data = try await loadVideoInfo() + let plugin = await generatePlayerPlugin(data) + onPluginReady.send(plugin) + } catch let err { + onPluginReady.send(completion: .failure(err.localizedDescription)) + } + } + + private func loadVideoInfo() async throws -> PlayerDetailData { + try await initPlayInfo() + let data = try await fetchVideoData() + await danmuProvider.initVideo(cid: data.cid, startPos: data.playerStartPos ?? 0) + return data + } + + private func initPlayInfo() async throws { + if !playInfo.isCidVaild { + playInfo.cid = try await WebRequest.requestCid(aid: playInfo.aid) + } + BiliBiliUpnpDMR.shared.sendVideoSwitch(aid: playInfo.aid, cid: playInfo.cid ?? 0) + } + + private func updateVideoDetailIfNeeded() async { + if videoDetail == nil { + videoDetail = try? await WebRequest.requestDetailVideo(aid: playInfo.aid) + } + } + + private func fetchVideoData() async throws -> PlayerDetailData { + assert(playInfo.isCidVaild) + let aid = playInfo.aid + let cid = playInfo.cid! + async let infoReq = try? WebRequest.requestPlayerInfo(aid: aid, cid: cid) + async let detailUpdate: () = updateVideoDetailIfNeeded() + do { + let playData: VideoPlayURLInfo + var clipInfos: [VideoPlayURLInfo.ClipInfo]? + + if playInfo.isBangumi { + playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid) + clipInfos = playData.clip_info_list + } else { + playData = try await WebRequest.requestPlayUrl(aid: aid, cid: cid) + } + + let info = await infoReq + _ = await detailUpdate + + var detail = PlayerDetailData(aid: playInfo.aid, cid: playInfo.cid!, epid: playInfo.epid, isBangumi: playInfo.isBangumi, detail: videoDetail, clips: clipInfos, playerInfo: info, videoPlayURLInfo: playData) + + if let info, info.last_play_cid == cid, playData.dash.duration - info.playTimeInSecond > 5, Settings.continuePlay { + detail.playerStartPos = info.playTimeInSecond + } + + return detail + + } catch let err { + if case let .statusFail(code, message) = err as? RequestError { + if code == -404 || code == -10403 { +// 解锁港澳台番剧处理 +// do { +// if let ok = try await fetchAreaLimitVideoData(), ok { +// return +// } +// } catch let err { +// } + } + throw "\(code) \(message),可能需要大会员" + } else if await infoReq?.is_upower_exclusive == true { + throw "该视频为充电专属视频 \(err)" + } else { + throw err + } + } + } + + private func playNext(newPlayInfo: PlayInfo) { + playInfo = newPlayInfo + if let playPlugin { + onPluginRemove.send(playPlugin) + } + Task { + do { + let data = try await loadVideoInfo() + let player = BVideoPlayPlugin(detailData: data) + onPluginReady.send([player]) + } catch let err { + onPluginReady.send(completion: .failure(err.localizedDescription)) + } + } + } + + @MainActor private func generatePlayerPlugin(_ data: PlayerDetailData) async -> [CommonPlayerPlugin] { + let player = BVideoPlayPlugin(detailData: data) + let danmu = DanmuViewPlugin(provider: danmuProvider) + let upnp = BUpnpPlugin(duration: data.detail?.View.duration) + let debug = DebugPlugin() + let playSpeed = SpeedChangerPlugin() + playSpeed.$currentPlaySpeed.sink { [weak danmu] speed in + danmu?.danMuView.playingSpeed = speed.value + }.store(in: &cancellable) + + let playlist = VideoPlayListPlugin(nextProvider: nextProvider) + playlist.onPlayEnd = { [weak self] in + self?.onExit?() + } + playlist.onPlayNextWithInfo = { + [weak self] info in + guard let self else { return } + playNext(newPlayInfo: info) + } + + playPlugin = player + + var plugins: [CommonPlayerPlugin] = [player, danmu, playSpeed, upnp, debug, playlist] + + if let clips = data.clips { + let clip = BVideoClipsPlugin(clipInfos: clips) + plugins.append(clip) + } + + if Settings.danmuMask { + if let mask = data.playerInfo?.dm_mask, + let video = data.videoPlayURLInfo.dash.video.first, + mask.fps > 0 + { + let maskProvider = BMaskProvider(info: mask, videoSize: CGSize(width: video.width ?? 0, height: video.height ?? 0)) + plugins.append(MaskViewPugin(maskView: danmu.danMuView, maskProvider: maskProvider)) + } else if Settings.vnMask { + let maskProvider = VMaskProvider() + plugins.append(MaskViewPugin(maskView: danmu.danMuView, maskProvider: maskProvider)) + } + } + + if let detail = data.detail { + let info = BVideoInfoPlugin(title: detail.title, subTitle: detail.ownerName, desp: detail.View.desc, pic: detail.pic, viewPoints: data.playerInfo?.view_points) + plugins.append(info) + } + + return plugins + } +} diff --git a/BilibiliLive/Extensions/Published+..swift b/BilibiliLive/Extensions/Published+..swift new file mode 100644 index 00000000..424a1779 --- /dev/null +++ b/BilibiliLive/Extensions/Published+..swift @@ -0,0 +1,20 @@ +// +// Published+..swift +// BilibiliLive +// +// Created by yicheng on 2024/6/10. +// + +import Combine + +fileprivate var cancellables = [String: AnyCancellable]() + +public extension Published { + init(wrappedValue defaultValue: Value, key: String) { + let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue + self.init(initialValue: value) + cancellables[key] = projectedValue.sink { val in + UserDefaults.standard.set(val, forKey: key) + } + } +} diff --git a/BilibiliLive/Module/DLNA/BiliBiliUpnpDMR.swift b/BilibiliLive/Module/DLNA/BiliBiliUpnpDMR.swift index 05ebf1a3..031377ca 100644 --- a/BilibiliLive/Module/DLNA/BiliBiliUpnpDMR.swift +++ b/BilibiliLive/Module/DLNA/BiliBiliUpnpDMR.swift @@ -14,6 +14,9 @@ import UIKit class BiliBiliUpnpDMR: NSObject { static let shared = BiliBiliUpnpDMR() + + weak var currentPlugin: BUpnpPlugin? + private var udp: GCDAsyncUdpSocket! private var httpServer = HttpServer() private var connectedSockets = [GCDAsyncSocket]() @@ -221,18 +224,18 @@ class BiliBiliUpnpDMR: NSObject { handlePlay(json: JSON(parseJSON: frame.body)) session.sendEmpty() case "Pause": - (topMost as? CommonPlayerViewController)?.player?.pause() + currentPlugin?.pause() session.sendEmpty() case "Resume": - (topMost as? CommonPlayerViewController)?.player?.play() + currentPlugin?.resume() session.sendEmpty() case "SwitchDanmaku": let json = JSON(parseJSON: frame.body) - (topMost as? CommonPlayerViewController)?.danMuView.isHidden = !json["open"].boolValue + Defaults.shared.showDanmu = json["open"].boolValue session.sendEmpty() case "Seek": let json = JSON(parseJSON: frame.body) - (topMost as? VideoPlayerViewController)?.player?.seek(to: CMTime(seconds: json["seekTs"].doubleValue, preferredTimescale: 1), toleranceBefore: .zero, toleranceAfter: .zero) + currentPlugin?.seek(to: json["seekTs"].doubleValue) session.sendEmpty() case "Stop": (topMost as? CommonPlayerViewController)?.dismiss(animated: true) diff --git a/BilibiliLive/Module/Live/LivePlayerViewController.swift b/BilibiliLive/Module/Live/LivePlayerViewController.swift index a7c32fcc..750ef493 100644 --- a/BilibiliLive/Module/Live/LivePlayerViewController.swift +++ b/BilibiliLive/Module/Live/LivePlayerViewController.swift @@ -11,7 +11,7 @@ import Foundation import SwiftyJSON import UIKit -class LivePlayerViewController: NewCommonPlayerViewController { +class LivePlayerViewController: CommonPlayerViewController { var room: LiveRoom? private var viewModel: LivePlayerViewModel? From 68c3aff948761f65c77848336304a177a2ce6866 Mon Sep 17 00:00:00 2001 From: yicheng <11733500+yichengchen@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:44:49 +0800 Subject: [PATCH 7/9] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B8=AF=E6=BE=B3?= =?UTF-8?q?=E5=8F=B0=E8=A7=A3=E9=94=81=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Video/NewVideoPlayerViewModel.swift | 76 ++++++++++++++++--- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift b/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift index 9a7f55c1..69435430 100644 --- a/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift +++ b/BilibiliLive/Component/Video/NewVideoPlayerViewModel.swift @@ -78,7 +78,19 @@ class VideoPlayerViewModel { var clipInfos: [VideoPlayURLInfo.ClipInfo]? if playInfo.isBangumi { - playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid) + do { + playData = try await WebRequest.requestPcgPlayUrl(aid: aid, cid: cid) + } catch let err as RequestError { + if case let .statusFail(code, _) = err, + code == -404 || code == -10403, + let data = try await fetchAreaLimitPcgVideoData() + { + playData = data + } else { + throw err + } + } + clipInfos = playData.clip_info_list } else { playData = try await WebRequest.requestPlayUrl(aid: aid, cid: cid) @@ -97,15 +109,6 @@ class VideoPlayerViewModel { } catch let err { if case let .statusFail(code, message) = err as? RequestError { - if code == -404 || code == -10403 { -// 解锁港澳台番剧处理 -// do { -// if let ok = try await fetchAreaLimitVideoData(), ok { -// return -// } -// } catch let err { -// } - } throw "\(code) \(message),可能需要大会员" } else if await infoReq?.is_upower_exclusive == true { throw "该视频为充电专属视频 \(err)" @@ -181,3 +184,56 @@ class VideoPlayerViewModel { return plugins } } + +// 港澳台解锁 +extension VideoPlayerViewModel { + private func fetchAreaLimitPcgVideoData() async throws -> VideoPlayURLInfo? { + guard Settings.areaLimitUnlock else { return nil } + guard let epid = playInfo.epid, epid > 0 else { return nil } + + let season = try await WebRequest.requestBangumiSeasonView(epid: epid) + let checkTitle = season.title.contains("僅") ? season.title : season.series_title + let checkAreaList = parseAreaByTitle(title: checkTitle) + guard !checkAreaList.isEmpty else { return nil } + + let playData = try await requestAreaLimitPcgPlayUrl(epid: epid, cid: playInfo.cid!, areaList: checkAreaList) + return playData + } + + private func requestAreaLimitPcgPlayUrl(epid: Int, cid: Int, areaList: [String]) async throws -> VideoPlayURLInfo? { + for area in areaList { + do { + return try await WebRequest.requestAreaLimitPcgPlayUrl(epid: epid, cid: cid, area: area) + } catch let err { + if area == areaList.last { + throw err + } else { + print(err) + } + } + } + return nil + } + + private func parseAreaByTitle(title: String) -> [String] { + if title.isMatch(pattern: "[仅|僅].*[东南亚|其他]") { + // TODO: 未支持 + return [] + } + + var areas: [String] = [] + if title.isMatch(pattern: "僅.*台") { + areas.append("tw") + } + if title.isMatch(pattern: "僅.*港") { + areas.append("hk") + } + + if areas.isEmpty { + // 标题没有地区限制信息,返回尝试检测的区域 + return ["tw", "hk"] + } else { + return areas + } + } +} From 003a81d7034ee107dcd9a98de4d5f79ecb3d4293 Mon Sep 17 00:00:00 2001 From: yicheng <11733500+yichengchen@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:52:24 +0800 Subject: [PATCH 8/9] misc: rename NewCommonPlayerViewController.swift --- BilibiliLive.xcodeproj/project.pbxproj | 12 +- .../Player/CommonPlayerViewController.swift | 174 ++++++++++++++++- .../NewCommonPlayerViewController.swift | 177 ------------------ 3 files changed, 174 insertions(+), 189 deletions(-) delete mode 100644 BilibiliLive/Component/Player/NewCommonPlayerViewController.swift diff --git a/BilibiliLive.xcodeproj/project.pbxproj b/BilibiliLive.xcodeproj/project.pbxproj index 4d4509f9..4a799309 100644 --- a/BilibiliLive.xcodeproj/project.pbxproj +++ b/BilibiliLive.xcodeproj/project.pbxproj @@ -17,7 +17,7 @@ 490EC3E7290CC8F8001E00B6 /* RankingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490EC3E6290CC8F8001E00B6 /* RankingViewController.swift */; }; 490EC3E9290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490EC3E8290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift */; }; 492731EE29096677005F5B0A /* HotViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492731ED29096677005F5B0A /* HotViewController.swift */; }; - 492AD7092BFF1E6C007221C8 /* NewCommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */; }; + 492AD7092BFF1E6C007221C8 /* CommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD7082BFF1E6C007221C8 /* CommonPlayerViewController.swift */; }; 492AD70B2BFF23B1007221C8 /* DanmuViewPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */; }; 492AD70D2BFF33DF007221C8 /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70C2BFF33DF007221C8 /* VideoPlayerViewController.swift */; }; 492AD70F2BFF6761007221C8 /* NewVideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */; }; @@ -121,7 +121,6 @@ F927ED902610A5E900EAB8E3 /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F927ED8F2610A5E900EAB8E3 /* CookieManager.swift */; }; F927ED992610AD8D00EAB8E3 /* LiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */; }; F927ED9F2610B5C300EAB8E3 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = F927ED9E2610B5C300EAB8E3 /* Kingfisher */; }; - F99D28E12619591300F8E66A /* CommonPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */; }; F99D28EA26195EC200F8E66A /* UIView+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99D28E926195EC200F8E66A /* UIView+Layout.swift */; }; F99D28F72619F5F000F8E66A /* FollowsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99D28F62619F5F000F8E66A /* FollowsViewController.swift */; }; F9B57354260F5F7400771ED5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9B57353260F5F7400771ED5 /* AppDelegate.swift */; }; @@ -160,7 +159,7 @@ 490EC3E6290CC8F8001E00B6 /* RankingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingViewController.swift; sourceTree = ""; }; 490EC3E8290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLSettingLineCollectionViewCell.swift; sourceTree = ""; }; 492731ED29096677005F5B0A /* HotViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotViewController.swift; sourceTree = ""; }; - 492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCommonPlayerViewController.swift; sourceTree = ""; }; + 492AD7082BFF1E6C007221C8 /* CommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerViewController.swift; sourceTree = ""; }; 492AD70A2BFF23B1007221C8 /* DanmuViewPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DanmuViewPlugin.swift; sourceTree = ""; }; 492AD70C2BFF33DF007221C8 /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = ""; }; 492AD70E2BFF6761007221C8 /* NewVideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewVideoPlayerViewModel.swift; sourceTree = ""; }; @@ -316,7 +315,6 @@ F927ED8F2610A5E900EAB8E3 /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = ""; }; F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveViewController.swift; sourceTree = ""; }; F99D28DA2618A55900F8E66A /* BilibiliLive-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BilibiliLive-Bridging-Header.h"; sourceTree = ""; }; - F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPlayerViewController.swift; sourceTree = ""; }; F99D28E926195EC200F8E66A /* UIView+Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Layout.swift"; sourceTree = ""; }; F99D28F62619F5F000F8E66A /* FollowsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsViewController.swift; sourceTree = ""; }; F9B57350260F5F7400771ED5 /* BilibiliLive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BilibiliLive.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -696,9 +694,8 @@ isa = PBXGroup; children = ( 49E5F84F28AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift */, - F99D28E02619591300F8E66A /* CommonPlayerViewController.swift */, 49FB8EE829208EBE0045D5DE /* SidxParseUtil.swift */, - 492AD7082BFF1E6C007221C8 /* NewCommonPlayerViewController.swift */, + 492AD7082BFF1E6C007221C8 /* CommonPlayerViewController.swift */, 497CF23A2C16EE04006E1488 /* Plugins */, 49D250A12C11A82B00173908 /* AVPlayerMetaUtils.swift */, ); @@ -924,7 +921,7 @@ 492AD7112C001C7B007221C8 /* BVideoPlayPlugin.swift in Sources */, F927ED742610395300EAB8E3 /* DanmakuView.swift in Sources */, F927ED782610395300EAB8E3 /* DanmakuTrack.swift in Sources */, - 492AD7092BFF1E6C007221C8 /* NewCommonPlayerViewController.swift in Sources */, + 492AD7092BFF1E6C007221C8 /* CommonPlayerViewController.swift in Sources */, 498CF29C2B63AABE0009793E /* compress_fragment.c in Sources */, 499C760F2930E068003160FB /* NVASocket.swift in Sources */, 498CF2912B63AABE0009793E /* backward_references_hq.c in Sources */, @@ -959,7 +956,6 @@ 49D39F28263AD40000F14497 /* WebRequest.swift in Sources */, 498DB1DF291BC24700F95607 /* BMaskProvider.swift in Sources */, 494741C029002797005D6885 /* UserDefault+..swift in Sources */, - F99D28E12619591300F8E66A /* CommonPlayerViewController.swift in Sources */, AE4889B228FE55DA00E8C5CD /* FavoriteViewController.swift in Sources */, F9B57356260F5F7400771ED5 /* LivePlayerViewController.swift in Sources */, 498CF29F2B63AABE0009793E /* static_dict.c in Sources */, diff --git a/BilibiliLive/Component/Player/CommonPlayerViewController.swift b/BilibiliLive/Component/Player/CommonPlayerViewController.swift index 6abc18bd..f5654d8a 100644 --- a/BilibiliLive/Component/Player/CommonPlayerViewController.swift +++ b/BilibiliLive/Component/Player/CommonPlayerViewController.swift @@ -1,11 +1,177 @@ // -// CommonPlayerViewController.swift +// NewCommonPlayerViewController.swift // BilibiliLive // -// Created by Etan Chen on 2021/4/4. +// Created by yicheng on 2024/5/23. // import AVKit -import Kingfisher import UIKit -import Vision + +class CommonPlayerViewController: UIViewController { + private let playerVC = AVPlayerViewController() + private var activePlugins = [CommonPlayerPlugin]() + private var observations = Set() + private var rateObserver: NSKeyValueObservation? + private var statusObserver: NSKeyValueObservation? + private var isEnd = false + + override func viewDidLoad() { + super.viewDidLoad() + addChild(playerVC) + view.addSubview(playerVC.view) + playerVC.didMove(toParent: self) + playerVC.view.snp.makeConstraints { $0.edges.equalToSuperview() } + playerVC.allowsPictureInPicturePlayback = true + playerVC.delegate = self + + let playerObservation = playerVC.observe(\.player) { [weak self] vc, obs in + if let oldPlayer = obs.oldValue, let oldPlayer { + self?.activePlugins.forEach { $0.playerDidCleanUp(player: oldPlayer) } + } + self?.playerDidChange(player: vc.player) + } + observations.insert(playerObservation) + activePlugins.forEach { $0.playerDidLoad(playerVC: playerVC) } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + activePlugins.forEach { $0.playerDidDismiss(playerVC: playerVC) } + } + + override var preferredFocusEnvironments: [UIFocusEnvironment] { + return [playerVC.view] + } + + func addPlugin(plugin: CommonPlayerPlugin) { + plugin.addViewToPlayerOverlay(container: playerVC.contentOverlayView!) + activePlugins.append(plugin) + plugin.playerDidLoad(playerVC: playerVC) + } + + func removePlugin(plugin: CommonPlayerPlugin) { + activePlugins.removeAll { $0 == plugin } + } + + func playerDidEnd(player: AVPlayer) {} + + func showErrorAlertAndExit(title: String = "播放失败", message: String = "未知错误") { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let actionOk = UIAlertAction(title: "OK", style: .default) { + [weak self] _ in + self?.dismiss(animated: true, completion: nil) + } + alertController.addAction(actionOk) + present(alertController, animated: true, completion: nil) + } +} + +extension CommonPlayerViewController { + private func playerDidChange(player: AVPlayer?) { + if let player { + activePlugins.forEach { $0.playerDidChange(player: player) } + rateObserver = player.observe(\.rate, options: [.old, .new]) { + [weak self] _player, obs in + DispatchQueue.main.async { [weak self] in + self?.playerRateDidChange(player: player) + } + } + if let playItem = player.currentItem { + observePlayerItem(playItem) + } + var menus = [UIMenuElement]() + activePlugins.forEach { + let newMenus = $0.addMenuItems(current: &menus) + menus.append(contentsOf: newMenus) + } + playerVC.transportBarCustomMenuItems = menus + } else { + rateObserver = nil + } + } + + private func playerRateDidChange(player: AVPlayer) { + if player.rate > 0 { + activePlugins.forEach { $0.playerDidStart(player: player) } + } else if player.rate == 0 { + if !isEnd { + activePlugins.forEach { $0.playerDidPause(player: player) } + } + } + } + + private func observePlayerItem(_ playerItem: AVPlayerItem) { + statusObserver = playerItem.observe(\.status, options: [.new, .old]) { + [weak self] item, _ in + guard let self, let player = playerVC.player else { return } + switch item.status { + case .readyToPlay: + isEnd = false + activePlugins.forEach { $0.playerWillStart(player: player) } + player.play() + case .failed: + activePlugins.forEach { $0.playerDidFail(player: player) } + default: + break + } + } + NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) + NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: .main) { [weak self] note in + guard let self, let player = playerVC.player else { return } + isEnd = true + activePlugins.forEach { $0.playerDidEnd(player: player) } + playerDidEnd(player: player) + } + } +} + +extension CommonPlayerViewController: AVPlayerViewControllerDelegate { + @objc func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool { + if let presentedViewController = UIViewController.topMostViewController() as? CommonPlayerViewController, + presentedViewController.playerVC == playerViewController + { + dismiss(animated: true) + return false + } + return false + } + + @objc func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool { + return true + } + + func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) { + PipRecorder.shared.playingPipViewController.append(self) + } + + func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { + PipRecorder.shared.playingPipViewController.removeAll { $0.playerVC == playerViewController } + } + + @objc func playerViewController(_ playerViewController: AVPlayerViewController, + restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) + { + let presentedViewController = UIViewController.topMostViewController() + guard let containerPlayer = PipRecorder.shared.playingPipViewController.first(where: { $0.playerVC == playerViewController }) else { + completionHandler(false) + return + } + if presentedViewController is CommonPlayerViewController { + let parent = presentedViewController.presentingViewController + presentedViewController.dismiss(animated: false) { + parent?.present(containerPlayer, animated: false) + completionHandler(true) + } + } else { + presentedViewController.present(containerPlayer, animated: false) { + completionHandler(true) + } + } + } + + class PipRecorder { + static let shared = PipRecorder() + var playingPipViewController = [CommonPlayerViewController]() + } +} diff --git a/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift b/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift deleted file mode 100644 index f5654d8a..00000000 --- a/BilibiliLive/Component/Player/NewCommonPlayerViewController.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// NewCommonPlayerViewController.swift -// BilibiliLive -// -// Created by yicheng on 2024/5/23. -// - -import AVKit -import UIKit - -class CommonPlayerViewController: UIViewController { - private let playerVC = AVPlayerViewController() - private var activePlugins = [CommonPlayerPlugin]() - private var observations = Set() - private var rateObserver: NSKeyValueObservation? - private var statusObserver: NSKeyValueObservation? - private var isEnd = false - - override func viewDidLoad() { - super.viewDidLoad() - addChild(playerVC) - view.addSubview(playerVC.view) - playerVC.didMove(toParent: self) - playerVC.view.snp.makeConstraints { $0.edges.equalToSuperview() } - playerVC.allowsPictureInPicturePlayback = true - playerVC.delegate = self - - let playerObservation = playerVC.observe(\.player) { [weak self] vc, obs in - if let oldPlayer = obs.oldValue, let oldPlayer { - self?.activePlugins.forEach { $0.playerDidCleanUp(player: oldPlayer) } - } - self?.playerDidChange(player: vc.player) - } - observations.insert(playerObservation) - activePlugins.forEach { $0.playerDidLoad(playerVC: playerVC) } - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - activePlugins.forEach { $0.playerDidDismiss(playerVC: playerVC) } - } - - override var preferredFocusEnvironments: [UIFocusEnvironment] { - return [playerVC.view] - } - - func addPlugin(plugin: CommonPlayerPlugin) { - plugin.addViewToPlayerOverlay(container: playerVC.contentOverlayView!) - activePlugins.append(plugin) - plugin.playerDidLoad(playerVC: playerVC) - } - - func removePlugin(plugin: CommonPlayerPlugin) { - activePlugins.removeAll { $0 == plugin } - } - - func playerDidEnd(player: AVPlayer) {} - - func showErrorAlertAndExit(title: String = "播放失败", message: String = "未知错误") { - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let actionOk = UIAlertAction(title: "OK", style: .default) { - [weak self] _ in - self?.dismiss(animated: true, completion: nil) - } - alertController.addAction(actionOk) - present(alertController, animated: true, completion: nil) - } -} - -extension CommonPlayerViewController { - private func playerDidChange(player: AVPlayer?) { - if let player { - activePlugins.forEach { $0.playerDidChange(player: player) } - rateObserver = player.observe(\.rate, options: [.old, .new]) { - [weak self] _player, obs in - DispatchQueue.main.async { [weak self] in - self?.playerRateDidChange(player: player) - } - } - if let playItem = player.currentItem { - observePlayerItem(playItem) - } - var menus = [UIMenuElement]() - activePlugins.forEach { - let newMenus = $0.addMenuItems(current: &menus) - menus.append(contentsOf: newMenus) - } - playerVC.transportBarCustomMenuItems = menus - } else { - rateObserver = nil - } - } - - private func playerRateDidChange(player: AVPlayer) { - if player.rate > 0 { - activePlugins.forEach { $0.playerDidStart(player: player) } - } else if player.rate == 0 { - if !isEnd { - activePlugins.forEach { $0.playerDidPause(player: player) } - } - } - } - - private func observePlayerItem(_ playerItem: AVPlayerItem) { - statusObserver = playerItem.observe(\.status, options: [.new, .old]) { - [weak self] item, _ in - guard let self, let player = playerVC.player else { return } - switch item.status { - case .readyToPlay: - isEnd = false - activePlugins.forEach { $0.playerWillStart(player: player) } - player.play() - case .failed: - activePlugins.forEach { $0.playerDidFail(player: player) } - default: - break - } - } - NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) - NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: .main) { [weak self] note in - guard let self, let player = playerVC.player else { return } - isEnd = true - activePlugins.forEach { $0.playerDidEnd(player: player) } - playerDidEnd(player: player) - } - } -} - -extension CommonPlayerViewController: AVPlayerViewControllerDelegate { - @objc func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool { - if let presentedViewController = UIViewController.topMostViewController() as? CommonPlayerViewController, - presentedViewController.playerVC == playerViewController - { - dismiss(animated: true) - return false - } - return false - } - - @objc func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool { - return true - } - - func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) { - PipRecorder.shared.playingPipViewController.append(self) - } - - func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { - PipRecorder.shared.playingPipViewController.removeAll { $0.playerVC == playerViewController } - } - - @objc func playerViewController(_ playerViewController: AVPlayerViewController, - restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) - { - let presentedViewController = UIViewController.topMostViewController() - guard let containerPlayer = PipRecorder.shared.playingPipViewController.first(where: { $0.playerVC == playerViewController }) else { - completionHandler(false) - return - } - if presentedViewController is CommonPlayerViewController { - let parent = presentedViewController.presentingViewController - presentedViewController.dismiss(animated: false) { - parent?.present(containerPlayer, animated: false) - completionHandler(true) - } - } else { - presentedViewController.present(containerPlayer, animated: false) { - completionHandler(true) - } - } - } - - class PipRecorder { - static let shared = PipRecorder() - var playingPipViewController = [CommonPlayerViewController]() - } -} From 937eb7adcdfa1c59bd10c9b74aab5847c42b568f Mon Sep 17 00:00:00 2001 From: yicheng <11733500+yichengchen@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:25:09 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8A=95=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BilibiliLive/Component/Video/Plugins/BUpnpPlugin.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/BilibiliLive/Component/Video/Plugins/BUpnpPlugin.swift b/BilibiliLive/Component/Video/Plugins/BUpnpPlugin.swift index 80e541a0..e0bf758f 100644 --- a/BilibiliLive/Component/Video/Plugins/BUpnpPlugin.swift +++ b/BilibiliLive/Component/Video/Plugins/BUpnpPlugin.swift @@ -28,6 +28,7 @@ class BUpnpPlugin: NSObject, CommonPlayerPlugin { } func playerWillStart(player: AVPlayer) { + BiliBiliUpnpDMR.shared.currentPlugin = self self.player = player guard let duration else { return } player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 5, preferredTimescale: 1), queue: .global()) { time in