Skip to content

Commit

Permalink
misc: add LivePlayerViewModel
Browse files Browse the repository at this point in the history
  • Loading branch information
yichengchen committed May 13, 2024
1 parent 16cda66 commit edb4b83
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 169 deletions.
4 changes: 4 additions & 0 deletions BilibiliLive.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +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 */; };
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 */; };
49389D8C28B0A84500B9DAFD /* PersonalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49389D8B28B0A84500B9DAFD /* PersonalViewController.swift */; };
Expand Down Expand Up @@ -142,6 +143,7 @@
490EC3E6290CC8F8001E00B6 /* RankingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingViewController.swift; sourceTree = "<group>"; };
490EC3E8290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLSettingLineCollectionViewCell.swift; sourceTree = "<group>"; };
492731ED29096677005F5B0A /* HotViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotViewController.swift; sourceTree = "<group>"; };
493307FC2BF230DB003622ED /* LivePlayerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LivePlayerViewModel.swift; sourceTree = "<group>"; };
49389D6128AFEA2900B9DAFD /* VideoDanmuProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDanmuProvider.swift; sourceTree = "<group>"; };
49389D8828B0A1B700B9DAFD /* UIViewController+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Ext.swift"; sourceTree = "<group>"; };
49389D8B28B0A84500B9DAFD /* PersonalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -349,6 +351,7 @@
2D3CD4002635BE7C00191419 /* Live */ = {
isa = PBXGroup;
children = (
493307FC2BF230DB003622ED /* LivePlayerViewModel.swift */,
F927ED982610AD8D00EAB8E3 /* LiveViewController.swift */,
F9B57355260F5F7400771ED5 /* LivePlayerViewController.swift */,
F927ED672610113A00EAB8E3 /* LiveDanMuProvider.swift */,
Expand Down Expand Up @@ -876,6 +879,7 @@
490EC3E9290CE23E001E00B6 /* BLSettingLineCollectionViewCell.swift in Sources */,
F9B57354260F5F7400771ED5 /* AppDelegate.swift in Sources */,
F9EDADD3262AA421007CB99F /* VideoDetailViewController.swift in Sources */,
493307FD2BF230DB003622ED /* LivePlayerViewModel.swift in Sources */,
496400D32943431E0098ACA6 /* Logger.swift in Sources */,
F927ED8826103CFB00EAB8E3 /* DanmakuTextCell.swift in Sources */,
0A41EE1C2A63102B0066444C /* dm.pb.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class CommonPlayerViewController: AVPlayerViewController {

func playerRateDidChange(player: AVPlayer) {}

func setPlayerInfo(title: String?, subTitle: String?, desp: String?, pic: URL?) {
@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,
Expand Down Expand Up @@ -310,7 +310,7 @@ class CommonPlayerViewController: AVPlayerViewController {
bitrate audio:\(bitrateStr(averageAudioBitrate)), video: \(bitrateStr(averageVideoBitrate))
observedBitrate:\(bitrateStr(observedBitrate))
indicatedAverageBitrate:\(bitrateStr(indicatedBitrate))
maskProvider: \(String(describing: maskProvider)))
maskProvider: \(String(describing: maskProvider))
"""
return logs
}
Expand Down
6 changes: 3 additions & 3 deletions BilibiliLive/Module/Live/LiveDanMuProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ class LiveDanMuProvider {
}

private func setupHeartBeat() {
heartBeatTimer = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(sendHeartBeat), userInfo: nil, repeats: true)
heartBeatTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true, block: { [weak self] _ in
self?.sendHeartBeat()
})
sendHeartBeat()
}

Expand Down Expand Up @@ -112,11 +114,9 @@ extension LiveDanMuProvider {
.map { JSON(parseJSON: $0) }
.forEach { json in
let cmd = json["cmd"].stringValue
Logger.debug("get cmd:\(cmd)")
switch cmd {
case "DANMU_MSG":
if let str = json["info"][1].string {
Logger.debug("danmu:\(str)")
onDanmu?(str)
}
case "DM_INTERACTION":
Expand Down
194 changes: 30 additions & 164 deletions BilibiliLive/Module/Live/LivePlayerViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,15 @@ import SwiftyJSON
import UIKit

class LivePlayerViewController: CommonPlayerViewController {
enum LiveError: Error {
case noLiving
case noPlaybackUrl
case fetchApiFail
}

var room: LiveRoom? {
didSet {
roomID = room?.room_id ?? 0
}
}

private var roomID: Int = 0
private var danMuProvider: LiveDanMuProvider?
private var failCount = 0
private var playInfo = [PlayInfo]()

private var viewModel: LivePlayerViewModel?
deinit {
Logger.debug("deinit live player")
}
Expand All @@ -38,183 +30,57 @@ class LivePlayerViewController: CommonPlayerViewController {
requiresLinearPlayback = true
super.viewDidLoad()

Task {
do {
try await refreshRoomsID()
await initDataSource()
try await initPlayer()
} catch let err {
endWithError(err: err)
}
if let info = try? await WebRequest.requestLiveBaseInfo(roomID: roomID) {
let subtitle = "\(room?.ownerName ?? "")·\(info.parent_area_name) \(info.area_name)"
let desp = "\(info.description)\nTags:\(info.tags ?? "")\n Hot words:\(info.hot_words?.joined(separator: ",") ?? "")"
setPlayerInfo(title: info.title, subTitle: subtitle, desp: desp, pic: room?.pic)
} else {
setPlayerInfo(title: room?.title, subTitle: "nil", desp: room?.ownerName, pic: room?.pic)
}
}
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillAppear(animated)
danMuProvider?.stop()
}

override func retryPlay() -> Bool {
Logger.warn("play fail, retry")
failCount += 1
if playInfo.count > 0 {
playInfo = Array(playInfo.dropFirst())
viewModel = LivePlayerViewModel(roomID: roomID)
viewModel?.onShootDanmu = { [weak self] in
self?.danMuView.shoot(danmaku: $0)
}
play()
return true
}

override func playerRateDidChange(player: AVPlayer) {
Logger.info("play speed change to", player.rate)
if player.rate == 0 {
Task {
do {
try await initPlayer()
} catch let err {
endWithError(err: err)
}
}
}
}

func play() {
if let url = playInfo.first?.url {
danMuView.play()

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: url)!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
let asset = AVURLAsset(url: URL(string: $0)!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
playerItem = AVPlayerItem(asset: asset)
player = AVPlayer(playerItem: playerItem)
player?.automaticallyWaitsToMinimizeStalling = false
} else {
showErrorAlertAndExit(title: "url is nil", message: "url: \(playInfo.first?.url.count ?? 0)")
}
viewModel?.onError = { [weak self] in
self?.showErrorAlertAndExit(message: $0)
}

viewModel?.start()

if Settings.danmuMask, Settings.vnMask {
maskProvider = VMaskProvider()
setupMask()
}
}

func endWithError(err: Error) {
let alert = UIAlertController(title: "播放失败", message: "\(err)", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: {
[weak self] _ in
self?.dismiss(animated: true, completion: nil)
}))
present(alert, animated: true, completion: nil)
}

func refreshRoomsID() async throws {
let url = "https://api.live.bilibili.com/room/v1/Room/room_init?id=\(roomID)"
let resp = await AF.request(url).serializingData().result
switch resp {
case let .success(object):
let json = JSON(object)
let isLive = json["data"]["live_status"].intValue == 1
if !isLive {
throw LiveError.noLiving
}
if let newID = json["data"]["room_id"].int {
roomID = newID
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)
}
case let .failure(error):
throw error
}
}

func initDataSource() async {
danMuProvider = LiveDanMuProvider(roomID: roomID)
danMuProvider?.onDanmu = {
[weak self] string in
let model = DanmakuTextCellModel(str: string)
self?.danMuView.shoot(danmaku: model)
}
danMuProvider?.onSC = {
[weak self] string in
let model = DanmakuTextCellModel(str: string)
model.type = .top
model.displayTime = 60
self?.danMuView.shoot(danmaku: model)
}
try? await danMuProvider?.start()
}

override func additionDebugInfo() -> String {
return "\(playInfo.first?.formate ?? "") \(playInfo.first?.current_qn ?? 0) failed: \(failCount)"
}

struct PlayInfo {
let formate: String?
let url: String
let current_qn: Int?
override func retryPlay() -> Bool {
Logger.warn("play fail, retry")
viewModel?.playerDidFailToPlay()
return true
}

func initPlayer() async throws {
let requestUrl = "https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?room_id=\(roomID)&protocol=1&format=0,1,2&codec=0,1&qn=10000&platform=web&ptype=8&dolby=5&panorama=1"
guard let data = try? await AF.request(requestUrl).serializingData().result.get() else {
throw LiveError.fetchApiFail
}
var playInfos = [PlayInfo]()
let json = JSON(data)

for stream in json["data"]["playurl_info"]["playurl"]["stream"].arrayValue {
for content in stream["format"].arrayValue {
let formate = content["format_name"].stringValue
let codecs = content["codec"].arrayValue

for codec in codecs {
let qn = codec["current_qn"].intValue
let baseUrl = codec["base_url"].stringValue
for url_info in codec["url_info"].arrayValue {
let host = url_info["host"]
let extra = url_info["extra"]
let url = "\(host)\(baseUrl)\(extra)"
let playInfo = PlayInfo(formate: formate, url: url, current_qn: qn)
playInfos.append(playInfo)
}
}
}
}
Logger.debug("info arry:\(playInfos)")
let info = playInfos.filter({ $0.formate == "fmp4" })
if info.count > 0 {
Logger.debug("play =>", info)
playInfo = info
play()
} else {
if playInfos.count > 0 {
Logger.debug("no fmp4 found, play directly")
playInfo = playInfos
play()
return
}

throw LiveError.noPlaybackUrl
override func playerRateDidChange(player: AVPlayer) {
Logger.info("play speed change to", player.rate)
if player.rate == 0 {
viewModel?.playerDidFailToPlay()
}
}
}

extension WebRequest {
struct LiveRoomInfo: Codable {
let description: String
let parent_area_name: String
let title: String
let tags: String?
let area_name: String
let hot_words: [String]?
}

static func requestLiveBaseInfo(roomID: Int) async throws -> LiveRoomInfo {
return try await request(url: "https://api.live.bilibili.com/room/v1/Room/get_info", parameters: ["room_id": roomID])
override func additionDebugInfo() -> String {
return viewModel?.debugInfo() ?? ""
}
}
Loading

0 comments on commit edb4b83

Please sign in to comment.