MOLineViewModel.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. //
  2. // MOLineViewModel.swift
  3. // MiMoLive
  4. //
  5. // Created by OneeChan on 2025/9/23.
  6. //
  7. import Foundation
  8. extension MOLiveViewModel {
  9. var curLineRoomId: String? {
  10. lineViewModel?.curLineRoomId
  11. }
  12. }
  13. extension MOLiveManager {
  14. static var curLineViewModel: MOLineViewModel? {
  15. curLive?.lineViewModel
  16. }
  17. }
  18. @objc
  19. enum LineState: Int {
  20. case none
  21. case matching_line
  22. case matching_pk
  23. case lining
  24. }
  25. @objc
  26. protocol MOLineViewModelDelegate {
  27. @objc optional func onLineStateChanged(state: LineState)
  28. @objc optional func onLineSettingChanged()
  29. @objc optional func onLineJoinFailed(viewModel: MOLineViewModel)
  30. @objc optional func onLineInfoChange(viewModel: MOLineViewModel)
  31. @objc optional func onLineUserMuteChange(viewModel: MOLineViewModel, user: MOLineMember)
  32. }
  33. @objcMembers
  34. class MOLineViewModel: NSObject {
  35. let curLineRoomId: String
  36. private let agoraToken: String
  37. // 重试配置
  38. private var lineInfoRetryTime = 0
  39. private var joinLineRetryTime = 0
  40. // Line 拉取到的相关信息
  41. private var hasJoinedChannel = false
  42. private(set) var lineRoomInfo: MOLivePkLinkRoomInfoVo? = nil
  43. var curLineTime: Int {
  44. guard let info = lineRoomInfo else {
  45. return 0
  46. }
  47. let hasPass = Date().timeIntervalSince1970 - info.now
  48. let time = Int(hasPass * 1000) + info.serviceTime - info.linkBeginTime
  49. return time / 1000
  50. }
  51. var isCreator: Bool {
  52. lineRoomInfo?.initiatorId == UserDefaults.uid
  53. }
  54. private var members: [MOLineMember] = []
  55. private var campMembers: [String: [MOLineMember]] = [:] // members 的 map 化
  56. var curOwnerInfo: MOLineMember? {
  57. guard let ownerUid = MOLiveManager.curLive?.ownerUid else { return nil }
  58. return members.first { $0.userId == ownerUid }
  59. }
  60. var curPeerInfo: MOLineMember? {
  61. guard let ownerUid = MOLiveManager.curLive?.ownerUid else { return nil }
  62. if members.count == 2 { // 如果只有两个人,不需要考虑阵营的问题
  63. return members.first { $0.userId != ownerUid }
  64. } else {
  65. let ownerCampCode = members.first { $0.userId == ownerUid }?.pkCampCode
  66. return members.first { $0.pkCampCode != ownerCampCode }
  67. }
  68. }
  69. // Line 状态
  70. private(set) static var matchTime: TimeInterval = 0
  71. private(set) static var curState: LineState = .none {
  72. didSet { notifyLineStateChanged() }
  73. }
  74. // Pk 玩法
  75. private(set) var curPKViewModel: MOLinePKViewModel? = nil
  76. // Line 配置
  77. private(set) static var lineConfig: MOLivePkLinkConfigVo = MOLivePkLinkConfigVo() {
  78. didSet {
  79. notifyLineSettingChanged()
  80. }
  81. }
  82. init(lineRoomId: String, token: String) {
  83. self.curLineRoomId = lineRoomId
  84. self.agoraToken = token
  85. super.init()
  86. MOEventDeliver.addObserver(self)
  87. joinLineChannel()
  88. getLineInfo()
  89. }
  90. deinit {
  91. clear()
  92. }
  93. func clear() {
  94. guard Self.curState != .none else { return }
  95. curPKViewModel?.clear()
  96. curPKViewModel = nil
  97. MOShowAgoraKitManager.share().leavePkChannelEx(withChannelId: curLineRoomId)
  98. Self.curState = .none
  99. }
  100. func handleLinePkRtm(_ statusInfo: MORtmPkV2Status) {
  101. if statusInfo.linkClose {
  102. // line 结束
  103. clear()
  104. return
  105. }
  106. updateMembers(statusInfo)
  107. checkPkInfo(statusInfo)
  108. }
  109. func handleLinePkExpandRtm(_ info: MORtmPkV2StatusExpand) {
  110. curPKViewModel?.handleTopUsersUpdate(info)
  111. }
  112. func reloadLineRoomInfo() {
  113. guard !curLineRoomId.isEmpty, !agoraToken.isEmpty else { return }
  114. MOHttpManager.shared().getLineRoomInfo(curLineRoomId) { [weak self] roomInfo, error in
  115. guard let self else { return }
  116. guard let roomInfo, error == nil else {
  117. return
  118. }
  119. if roomInfo.roomClose {
  120. return
  121. }
  122. self.lineRoomInfo = roomInfo
  123. self.updateMembers(roomInfo)
  124. self.checkPkInfo(roomInfo)
  125. }
  126. }
  127. }
  128. // MARK: Private 私有方法
  129. extension MOLineViewModel {
  130. private func updateMembers(_ roomInfo: MOLivePkLinkRoomInfoVo) {
  131. let newUsers = roomInfo.members.map { $0.userId }
  132. self.members.removeAll { !newUsers.contains($0.userId) } // 移除不在新名单的用户信息
  133. roomInfo.members.forEach { info in
  134. if let member = self.members.first(where: { $0.userId == info.userId }) {
  135. let isMute = member.mute
  136. member.updateBy(info: info)
  137. if info.mute != isMute {
  138. notifyLineUserMuteChanged(user: member)
  139. }
  140. } else {
  141. let member = MOLineMember()
  142. member.updateBy(info: info)
  143. self.members.append(member)
  144. }
  145. }
  146. // 更新 map
  147. self.campMembers = self.members.reduce(into: [String: [MOLineMember]](), { partialResult, info in
  148. partialResult[info.pkCampCode, default: []].append(info)
  149. })
  150. notifyLineInfoChanged()
  151. }
  152. private func updateMembers(_ statusInfo: MORtmPkV2Status) {
  153. let newUsers = statusInfo.linkMemberInfos.map { $0.userId }
  154. self.members.removeAll { !newUsers.contains($0.userId) } // 移除不在新名单的用户信息
  155. statusInfo.linkMemberInfos.forEach { info in
  156. if let member = self.members.first(where: { $0.userId == info.userId }) {
  157. member.updateBy(linkInfo: info)
  158. } else {
  159. let member = MOLineMember()
  160. member.updateBy(linkInfo: info)
  161. self.members.append(member)
  162. }
  163. }
  164. // 更新 map
  165. self.campMembers = self.members.reduce(into: [String: [MOLineMember]](), { partialResult, info in
  166. partialResult[info.pkCampCode, default: []].append(info)
  167. })
  168. notifyLineInfoChanged()
  169. }
  170. private func checkPkInfo(_ roomInfo: MOLivePkLinkRoomInfoVo) {
  171. if roomInfo.type == .line {
  172. if MOLiveManager.curLive?.isOwner != true, // 自己开播不需要处理上一次的记录
  173. roomInfo.lastPkAvailable() == true {
  174. if curPKViewModel == nil {
  175. curPKViewModel = MOLinePKViewModel(pkMatchId: "")
  176. }
  177. }
  178. } else if roomInfo.type == .linePk {
  179. if curPKViewModel?.pkMatchId != roomInfo.pkMatchId {
  180. curPKViewModel?.clear()
  181. curPKViewModel = nil
  182. curPKViewModel = MOLinePKViewModel(pkMatchId: roomInfo.pkMatchId)
  183. }
  184. }
  185. curPKViewModel?.updateInfo(roomInfo: roomInfo)
  186. }
  187. private func checkPkInfo(_ statusInfo: MORtmPkV2Status) {
  188. curPKViewModel?.updateInfo(statusInfo: statusInfo)
  189. }
  190. private func preloadImageToCache() {
  191. // 提前加载图片资源
  192. DispatchQueue.global().async {
  193. let webpNames: [String] = ["icon_line_pk_start", "icon_line_pk_draw",
  194. "icon_line_pk_defeat", "icon_line_pk_vic"]
  195. let urls: [URL] = webpNames.compactMap {
  196. guard let path = Bundle.main.path(forResource: $0, ofType: "webp") else { return nil }
  197. return URL(fileURLWithPath: path)
  198. }
  199. urls.forEach {
  200. SDWebImageManager.shared.loadImage(with: $0, progress: nil)
  201. { _, _, _, type, finished, _ in
  202. }
  203. }
  204. }
  205. }
  206. }
  207. // MARK: Line 查询以及频道加入
  208. extension MOLineViewModel {
  209. @objc
  210. private func joinLineChannel() {
  211. guard !curLineRoomId.isEmpty, !agoraToken.isEmpty else { return }
  212. //将要渲染的PK对象
  213. let peerId = 0
  214. MOShowAgoraKitManager.share().joinChannel(withTargetChannelId: curLineRoomId
  215. , ownerId: peerId, token: agoraToken,
  216. delegate: nil)
  217. { [weak self] success, code in
  218. guard let self else { return }
  219. if success || code == -17 {
  220. //-17错误码代表已经进入了同一个房间,直接按成功处理即可
  221. self.hasJoinedChannel = true
  222. self.joinLineRetryTime = 0
  223. self.checkIfLineSuccess()
  224. return
  225. }
  226. // 失败
  227. if self.joinLineRetryTime > 2 {
  228. self.handleJoinLineFailed()
  229. return
  230. }
  231. self.joinLineRetryTime += 1
  232. //离开房间, 然后再次加入试试
  233. MOShowAgoraKitManager.share().leavePkChannelEx(withChannelId: self.curLineRoomId)
  234. self.perform(#selector(joinLineChannel), afterDelay: 0.3)
  235. }
  236. }
  237. @objc
  238. private func getLineInfo() {
  239. guard !curLineRoomId.isEmpty, !agoraToken.isEmpty else { return }
  240. MOHttpManager.shared().getLineRoomInfo(curLineRoomId) { [weak self] roomInfo, error in
  241. guard let self else { return }
  242. guard let roomInfo, error == nil else {
  243. // 失败
  244. if self.lineInfoRetryTime > 2 {
  245. self.handleJoinLineFailed()
  246. return
  247. }
  248. self.lineInfoRetryTime += 1
  249. self.perform(#selector(getLineInfo), afterDelay: 0.3)
  250. return
  251. }
  252. if roomInfo.roomClose {
  253. // Line 房间已经关闭,TODO: 当作失败处理(有待讨论)
  254. handleJoinLineFailed()
  255. return
  256. }
  257. self.lineRoomInfo = roomInfo
  258. self.lineInfoRetryTime = 0
  259. self.checkIfLineSuccess()
  260. }
  261. }
  262. private func checkIfLineSuccess() {
  263. guard let lineRoomInfo, hasJoinedChannel else { return }
  264. // 此时已经 joinChannel & getInfo 成功
  265. self.updateMembers(lineRoomInfo)
  266. Self.curState = .lining
  267. self.checkPkInfo(lineRoomInfo)
  268. self.preloadImageToCache()
  269. }
  270. private func handleJoinLineFailed() {
  271. MOShowAgoraKitManager.share().leavePkChannelEx(withChannelId: curLineRoomId)
  272. self.notifyLineJoinFailed()
  273. }
  274. }
  275. // MARK: Line 音频控制
  276. extension MOLineViewModel {
  277. func mutePeerHost(_ mute: Bool, _ handler: @escaping (Bool) -> Void) {
  278. guard let peer = curPeerInfo else { return }
  279. MOHttpManager.shared().updateSoundControl(curLineRoomId, userIds: [peer.userId],
  280. mute: mute, forAll: false) { err in
  281. guard err == nil else {
  282. showNetError(err: err)
  283. handler(false)
  284. return
  285. }
  286. handler(true)
  287. }
  288. }
  289. }
  290. // MARK: Line 配置
  291. extension MOLineViewModel {
  292. static func getLineConfig() {
  293. MOHttpManager.shared().getLinePkConfig { info, error in
  294. guard error == nil else {
  295. showNetError(err: error)
  296. return
  297. }
  298. guard let info else { return }
  299. self.lineConfig = info
  300. }
  301. }
  302. static func updateLinePkTime(_ duration: Int, _ handler: @escaping (Bool) -> Void) {
  303. updateLineConfig(duration: duration, handler)
  304. }
  305. static func updateAcceptPk(_ canAcceptPk: Bool, _ handler: @escaping (Bool) -> Void) {
  306. updateLineConfig(canAcceptPk: canAcceptPk, handler)
  307. }
  308. static func updateAcceptLine(_ canAcceptLink: Bool, _ handler: @escaping (Bool) -> Void) {
  309. updateLineConfig(canAcceptLink: canAcceptLink, handler)
  310. }
  311. private static func updateLineConfig(duration: Int = lineConfig.duration,
  312. canAcceptPk: Bool = lineConfig.canAcceptPk,
  313. magnifyMySelf: Bool = lineConfig.magnifyMySelf,
  314. canAcceptLink: Bool = lineConfig.canAcceptLink,
  315. _ handler: @escaping (Bool) -> Void) {
  316. MOHttpManager.shared().updateLinePkConfig(duration: duration,
  317. canAcceptPk: canAcceptPk,
  318. magnifyMySelf: magnifyMySelf,
  319. canAcceptLink: canAcceptLink)
  320. { error in
  321. guard error == nil else {
  322. showNetError(err: error)
  323. handler(false)
  324. return
  325. }
  326. self.lineConfig.duration = duration
  327. self.lineConfig.canAcceptPk = canAcceptPk
  328. self.lineConfig.magnifyMySelf = magnifyMySelf
  329. self.lineConfig.canAcceptLink = canAcceptLink
  330. handler(true)
  331. Self.notifyLineSettingChanged()
  332. }
  333. }
  334. func updateLineShowValue(showValue: Bool, _ handler: @escaping (Bool) -> Void) {
  335. guard let info = lineRoomInfo else {
  336. handler(false)
  337. return
  338. }
  339. MOHttpManager.shared().updateLineRoomConfig(info.pkLinkRoomId,
  340. showValue: showValue,
  341. allowJoin: info.allowJoin,
  342. allowInvite: info.allowInvite) { [weak self] error in
  343. guard let self else { return }
  344. guard error == nil else {
  345. showNetError(err: error)
  346. handler(false)
  347. return
  348. }
  349. self.lineRoomInfo?.showValue = showValue
  350. handler(true)
  351. }
  352. }
  353. func updateLineAllowJoin(allowJoin: Bool, _ handler: @escaping (Bool) -> Void) {
  354. guard let info = lineRoomInfo else {
  355. handler(false)
  356. return
  357. }
  358. MOHttpManager.shared().updateLineRoomConfig(info.pkLinkRoomId,
  359. showValue: info.showValue,
  360. allowJoin: allowJoin,
  361. allowInvite: info.allowInvite) { [weak self] error in
  362. guard let self else { return }
  363. guard error == nil else {
  364. showNetError(err: error)
  365. handler(false)
  366. return
  367. }
  368. self.lineRoomInfo?.allowJoin = allowJoin
  369. handler(true)
  370. }
  371. }
  372. func updateLineAllowInvite(allowInvite: Bool, _ handler: @escaping (Bool) -> Void) {
  373. guard let info = lineRoomInfo else {
  374. handler(false)
  375. return
  376. }
  377. MOHttpManager.shared().updateLineRoomConfig(info.pkLinkRoomId,
  378. showValue: info.showValue,
  379. allowJoin: info.allowJoin,
  380. allowInvite: allowInvite) { [weak self] error in
  381. guard let self else { return }
  382. guard error == nil else {
  383. showNetError(err: error)
  384. handler(false)
  385. return
  386. }
  387. self.lineRoomInfo?.allowInvite = allowInvite
  388. handler(true)
  389. }
  390. }
  391. func cleanBeanValue() {
  392. MOHttpManager.shared().cleanLineRoomBeans(curLineRoomId) { error in
  393. guard error == nil else {
  394. showNetError(err: error)
  395. return
  396. }
  397. }
  398. }
  399. }
  400. // MARK: Line 操作
  401. extension MOLineViewModel {
  402. // 随机连线
  403. static func startLineMatch(_ lineType: LineType, _ handler: ((Bool) -> Void)? = nil) {
  404. guard let roomId = MOLiveManager.curRoomId,
  405. !roomId.isEmpty,
  406. curState == .none else {
  407. handler?(false)
  408. return
  409. }
  410. matchTime = Date().timeIntervalSince1970
  411. curState = lineType == .line ? .matching_line : .matching_pk
  412. MOHttpManager.shared().matchLine(roomId, lineType) { error in
  413. guard error == nil else {
  414. showNetError(err: error)
  415. handler?(false)
  416. return
  417. }
  418. handler?(true)
  419. }
  420. }
  421. static func cancelMatch(handler: @escaping (Bool) -> Void) {
  422. guard let roomId = MOLiveManager.curRoomId,
  423. !roomId.isEmpty else {
  424. handler(false)
  425. return
  426. }
  427. guard curState != .none else {
  428. handler(false)
  429. return
  430. }
  431. let lineType: LineType?
  432. if curState == .matching_pk {
  433. lineType = .linePk
  434. } else if curState == .matching_line {
  435. lineType = .line
  436. } else {
  437. lineType = nil
  438. }
  439. guard let lineType else {
  440. handler(false)
  441. return
  442. }
  443. MOHttpManager.shared().cancelMatch(roomId, lineType) { error in
  444. guard error == nil else {
  445. showNetError(err: error)
  446. handler(false)
  447. return
  448. }
  449. curState = .none
  450. matchTime = 0
  451. handler(true)
  452. }
  453. }
  454. // 邀请连线
  455. static func inviteLineUser(_ peerRoomId: String, _ lineType: LineType, _ handler: ((Bool) -> Void)? = nil) {
  456. guard let roomId = MOLiveManager.curRoomId,
  457. !roomId.isEmpty else {
  458. handler?(false)
  459. return
  460. }
  461. MOHttpManager.shared().inviteLine(roomId, peerRoomId, lineType) { error in
  462. guard error == nil else {
  463. showNetError(err: error)
  464. handler?(false)
  465. return
  466. }
  467. handler?(true)
  468. }
  469. }
  470. static func responseLineInvite(inviteId: String, accept: Bool, handler: @escaping (Bool) -> Void) {
  471. MOHttpManager.shared().responseLineInvite(inviteId, accept) { error in
  472. guard error == nil else {
  473. showNetError(err: error)
  474. handler(false)
  475. return
  476. }
  477. handler(true)
  478. }
  479. }
  480. func closeLine(handler: @escaping (Bool) -> Void) {
  481. MOHttpManager.shared().closeLine(self.curLineRoomId) { [weak self] error in
  482. guard let self else { return }
  483. guard error == nil else {
  484. showNetError(err: error)
  485. handler(false)
  486. return
  487. }
  488. handler(true)
  489. self.clear()
  490. }
  491. }
  492. }
  493. // MARK: PK 相关
  494. extension MOLineViewModel {
  495. func startPk(_ handler: ((Bool) -> Void)? = nil) {
  496. MOHttpManager.shared().startPk(curLineRoomId) { error in
  497. if error != nil {
  498. showNetError(err: error)
  499. handler?(false)
  500. return
  501. }
  502. handler?(true)
  503. }
  504. }
  505. }
  506. // MARK: Line 列表
  507. extension MOLineViewModel {
  508. static func loadLineMatchUsers(_ next: String? = nil, _ key: String = "", _ handler: @escaping (MONextVOLivePkLinkInviteVO?, Bool) -> Void) {
  509. MOHttpManager.shared().getLineInviteList(next: next ?? "", size: 50, searchKey: key) { list, error in
  510. if error != nil || list == nil {
  511. showNetError(err: error)
  512. }
  513. handler(list, list?.list.count ?? 0 >= 50)
  514. }
  515. }
  516. static func loadLinePkMatchUsers(_ next: String? = nil, _ key: String = "", _ handler: @escaping (MONextVOLivePkLinkInviteVO?, Bool) -> Void) {
  517. MOHttpManager.shared().getLinePkInviteList(next: next ?? "", size: 50, searchKey: key) { list, error in
  518. if error != nil || list == nil {
  519. showNetError(err: error)
  520. }
  521. handler(list, list?.list.count ?? 0 >= 50)
  522. }
  523. }
  524. static func searchLineMatchUser(_ keywork: String, _ handler: @escaping (MONextVOLivePkLinkInviteVO?) -> Void) {
  525. handler(nil)
  526. }
  527. static func loadPkHistory(_ category: LinePkHistoryType, next: String? = nil, _ handler: @escaping (MONextVOLivePkV2RecordVo?, Bool) -> Void) {
  528. MOHttpManager.shared().getLinePkHistory(historyType: category, next: next ?? "", size: 50) { list, error in
  529. if error != nil || list == nil {
  530. showNetError(err: error)
  531. }
  532. handler(list, list?.list.count ?? 0 >= 50)
  533. }
  534. }
  535. }
  536. extension MOLineViewModel: MOLinePKViewModelDelegate {
  537. func onLinePkStateChanged(viewModel: MOLinePKViewModel, state: LinePKState) {
  538. if state == .none {
  539. curPKViewModel?.clear()
  540. curPKViewModel = nil
  541. }
  542. }
  543. }
  544. // MARK: 通知发送
  545. extension MOLineViewModel {
  546. private static func notifyLineStateChanged() {
  547. MOEventDeliver.notifyEvent { $0.onLineStateChanged?(state: curState) }
  548. }
  549. private func notifyLineJoinFailed() {
  550. MOEventDeliver.notifyEvent { $0.onLineJoinFailed?(viewModel: self) }
  551. }
  552. private static func notifyLineSettingChanged() {
  553. MOEventDeliver.notifyEvent { $0.onLineSettingChanged?() }
  554. }
  555. private func notifyLineInfoChanged() {
  556. MOEventDeliver.notifyEvent { $0.onLineInfoChange?(viewModel: self) }
  557. }
  558. private func notifyLineUserMuteChanged(user: MOLineMember) {
  559. MOEventDeliver.notifyEvent { $0.onLineUserMuteChange?(viewModel: self, user: user) }
  560. }
  561. }