LNRoomViewModel.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. //
  2. // LNRoomViewModel.swift
  3. // Gami
  4. //
  5. // Created by OneeChan on 2026/3/9.
  6. //
  7. import Foundation
  8. import AtomicXCore
  9. import Combine
  10. protocol LNRoomViewModelNotify {
  11. func onRoomMessageChanged(messages: [LNRoomMessageItem])
  12. func onRoomSeatsChanged()
  13. func onRoomSpeakingUsersChanged()
  14. func onRoomSeatApplyChanged()
  15. func onRoomInfoChanged()
  16. func onMySeatApplyChanged()
  17. func onRoomClosed()
  18. }
  19. extension LNRoomViewModelNotify {
  20. func onRoomMessageChanged(messages: [LNRoomMessageItem]) { }
  21. func onRoomSeatsChanged() { }
  22. func onRoomSpeakingUsersChanged() { }
  23. func onRoomSeatApplyChanged() { }
  24. func onRoomInfoChanged() { }
  25. func onMySeatApplyChanged() { }
  26. func onRoomClosed() { }
  27. }
  28. enum LNRoomSeatNum: Int, CaseIterable, Comparable {
  29. case none = -1
  30. case host = 0
  31. case guest
  32. case mic1
  33. case mic2
  34. case mic3
  35. case mic4
  36. case mic5
  37. case mic6
  38. case mic7
  39. case mic8
  40. static func < (lhs: LNRoomSeatNum, rhs: LNRoomSeatNum) -> Bool {
  41. lhs.rawValue < rhs.rawValue
  42. }
  43. var title: String {
  44. if case .host = self {
  45. .init(key: "A00328")
  46. } else if case .guest = self {
  47. .init(key: "A00329")
  48. } else {
  49. .init(key: "A00326", rawValue - 1)
  50. }
  51. }
  52. var giftHeaderTitle: String {
  53. if case .host = self {
  54. .init(key: "A00328")
  55. } else if case .guest = self {
  56. .init(key: "A00329")
  57. } else {
  58. "Mic \(rawValue - 1)"
  59. }
  60. }
  61. }
  62. enum LNApplyingSeatType: Int, Decodable {
  63. case none
  64. case guest
  65. case playmate
  66. }
  67. enum LNApplySeatErrCode: Int {
  68. case noPermission = 50001
  69. }
  70. class LNRoomViewModel: NSObject {
  71. let roomId: String
  72. private let seatStore: LiveSeatStore
  73. private let guestStore: CoGuestStore
  74. private let messageStore: BarrageStore
  75. private let audienceStore: LiveAudienceStore
  76. private(set) var seatsInfo: [LNRoomSeatItem] = []
  77. private(set) var roomInfo = LNRoomInfo()
  78. private(set) var speakingUser: [String] = []
  79. private(set) var lastmessage: Barrage? = nil
  80. private(set) var seatApplyCount: Int = 0
  81. private(set) var waitingForSeat: LNApplyingSeatType = .none {
  82. didSet {
  83. if oldValue != waitingForSeat {
  84. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onMySeatApplyChanged() }
  85. }
  86. }
  87. }
  88. private let systemHandlerQueue = DispatchQueue(label: "com.gami.room.system.message", attributes: .concurrent)
  89. private var isLoadingGiftList = false
  90. private(set) var giftList: [LNGiftItemVO] = []
  91. init(roomId: String) {
  92. self.roomId = roomId
  93. seatStore = LiveSeatStore.create(liveID: roomId)
  94. guestStore = CoGuestStore.create(liveID: roomId)
  95. messageStore = BarrageStore.create(liveID: roomId)
  96. audienceStore = LiveAudienceStore.create(liveID: roomId)
  97. super.init()
  98. setupSeatObservers()
  99. setupMessageObservers()
  100. setupRoomInfoObserver()
  101. setupAudienceEventObservers()
  102. reloadGiftList()
  103. LNRoomManager.shared.updateCurRoom(self)
  104. }
  105. func closeRoom() {
  106. LNRoomManager.shared.closeRoom { success in
  107. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed() }
  108. }
  109. }
  110. func leaveRoom() {
  111. LNRoomManager.shared.leaveRoom { success in
  112. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed() }
  113. }
  114. }
  115. }
  116. // MARK: 麦位管理 - 普通用户
  117. extension LNRoomViewModel {
  118. var mySeatInfo: LNRoomSeatItem? {
  119. seatsInfo.first { $0.uid.isMyUid }
  120. }
  121. private func setupSeatObservers() {
  122. seatStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
  123. guard let self else { return }
  124. let wasOnMic = mySeatInfo != nil
  125. let seats = state.seatList
  126. var hasChanged = seats.count != seatsInfo.count
  127. var newSeats: [LNRoomSeatItem] = []
  128. for seat in seats {
  129. let item = seatsInfo.first(where: { $0.index.rawValue == seat.index })
  130. ?? LNRoomSeatItem(index: LNRoomSeatNum(rawValue: seat.index) ?? .none)
  131. hasChanged = item.update(seat) || hasChanged
  132. newSeats.append(item)
  133. }
  134. if hasChanged {
  135. seatsInfo = newSeats
  136. if !wasOnMic, mySeatInfo != nil {
  137. // 自己上麦
  138. waitingForSeat = .none
  139. if roomInfo.forbidAudio {
  140. DeviceStore.shared.closeLocalMicrophone()
  141. } else {
  142. DeviceStore.shared.openLocalMicrophone(completion: nil)
  143. }
  144. }
  145. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSeatsChanged() }
  146. }
  147. let speakings = state.speakingUsers.filter { $0.value > 0 }.map { $0.key }.sorted { $1 > $0 }
  148. if speakings != speakingUser {
  149. speakingUser = speakings
  150. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSpeakingUsersChanged() }
  151. }
  152. }.store(in: &cancellables)
  153. }
  154. // 申请列表
  155. func getApplyList(type: LNRoomApplySeatType, next: String?, filter: String?,
  156. handler: @escaping (LNRoomMicApplyListResponse?) -> Void) {
  157. LNHttpManager.shared.getApplySeatList(roomId: roomId, searchType: type, filter: filter,
  158. size: 30, next: next ?? "")
  159. { res, err in
  160. runOnMain {
  161. handler(res)
  162. }
  163. res?.list.forEach {
  164. LNProfileManager.shared.updateProfileCache(uid: $0.user.userNo, name: $0.user.nickname, avatar: $0.user.avatar)
  165. }
  166. if let err {
  167. showToast(err.errorDesc)
  168. }
  169. }
  170. }
  171. // 申请列表的技能类别
  172. func getApplyListCategory(handler: @escaping (LNRoomGameCategoryResponse?) -> Void) {
  173. LNHttpManager.shared.getApplySeatCategory(roomId: roomId) { res, err in
  174. runOnMain {
  175. handler(res)
  176. }
  177. if let err {
  178. showToast(err.errorDesc)
  179. }
  180. }
  181. }
  182. // 申请上麦
  183. func applySeat(index: LNRoomSeatNum, handler: @escaping (Bool) -> Void) {
  184. LNPermissionHelper.requestMicrophoneAccess { [weak self] success in
  185. guard let self else { return }
  186. guard success else {
  187. showToast(.init(key: "A00362"))
  188. return
  189. }
  190. LNHttpManager.shared.applySeat(roomId: roomId, index: index.rawValue) { [weak self] err in
  191. runOnMain {
  192. handler(err == nil)
  193. }
  194. if let err {
  195. if case .serverError(let code, _) = err,
  196. code == LNApplySeatErrCode.noPermission.rawValue {
  197. showToast(err.errorDesc, icon: .icAbout.withTintColor(.fill))
  198. } else {
  199. showToast(err.errorDesc)
  200. }
  201. } else if let self {
  202. waitingForSeat = index == .guest ? .guest : index != .host ? .playmate : .none
  203. }
  204. }
  205. }
  206. }
  207. // 取消上麦申请
  208. func cancelSeatApply(handler: @escaping (Bool) -> Void) {
  209. LNHttpManager.shared.cancelApplySeat(roomId: roomId) { [weak self] err in
  210. runOnMain {
  211. handler(err == nil)
  212. }
  213. if let err {
  214. showToast(err.errorDesc)
  215. } else if let self {
  216. waitingForSeat = .none
  217. }
  218. }
  219. }
  220. // 主动下麦
  221. func leaveSeat(handler: @escaping (Bool) -> Void) {
  222. seatStore.leaveSeat { result in
  223. switch result {
  224. case .success:
  225. handler(true)
  226. case .failure(let err):
  227. showTencentError(err)
  228. handler(false)
  229. }
  230. }
  231. }
  232. }
  233. // MARK: 麦位管理 - 管理员
  234. extension LNRoomViewModel {
  235. // 踢人下麦
  236. func kickUserOffSeat(uid: String, handler: @escaping (Bool) -> Void) {
  237. seatStore.kickUserOutOfSeat(userID: uid) { result in
  238. switch result {
  239. case .success:
  240. handler(true)
  241. case .failure(let err):
  242. showTencentError(err)
  243. handler(false)
  244. }
  245. }
  246. }
  247. func clearApplyList(type: LNRoomApplySeatType, handler: @escaping (Bool) -> Void) {
  248. LNHttpManager.shared.clearApplySeatList(roomId: roomId, searchType: type) { err in
  249. runOnMain {
  250. handler(err == nil)
  251. }
  252. if let err {
  253. showToast(err.errorDesc)
  254. }
  255. }
  256. }
  257. // 接受上麦申请
  258. func acceptSeatApply(applyId: String, handler: @escaping (Bool) -> Void) {
  259. LNHttpManager.shared.handleApplySeat(roomId: roomId, applyId: applyId, accept: true) { err in
  260. runOnMain {
  261. handler(err == nil)
  262. }
  263. if let err {
  264. showToast(err.errorDesc)
  265. }
  266. }
  267. }
  268. // 拒绝上麦申请
  269. func rejectSeatApply(applyId: String, handler: @escaping (Bool) -> Void) {
  270. LNHttpManager.shared.handleApplySeat(roomId: roomId, applyId: applyId, accept: false) { err in
  271. runOnMain {
  272. handler(err == nil)
  273. }
  274. if let err {
  275. showToast(err.errorDesc)
  276. }
  277. }
  278. }
  279. // 邀请上麦
  280. func inviteUserToSeat(uid: String, index: LNRoomSeatNum, handler: @escaping (Bool) -> Void) {
  281. LNHttpManager.shared.inviteUserToSeat(roomId: roomId, uid: uid, index: index.rawValue) { err in
  282. runOnMain {
  283. handler(err == nil)
  284. }
  285. if let err {
  286. showToast(err.errorDesc)
  287. }
  288. }
  289. }
  290. // 关闭麦位
  291. func lockSeat(num: Int, handler: @escaping (Bool) -> Void) {
  292. seatStore.lockSeat(seatIndex: num) { result in
  293. switch result {
  294. case .success:
  295. handler(true)
  296. case .failure(let err):
  297. showTencentError(err)
  298. handler(false)
  299. }
  300. }
  301. }
  302. // 解锁麦位
  303. func unlockSeat(num: Int, handler: @escaping (Bool) -> Void) {
  304. seatStore.unlockSeat(seatIndex: num) { result in
  305. switch result {
  306. case .success:
  307. handler(true)
  308. case .failure(let err):
  309. showTencentError(err)
  310. handler(false)
  311. }
  312. }
  313. }
  314. // 用户列表
  315. func getRoomUserList(next: String?, playmete: Bool, filter: String?,
  316. handler: @escaping (LNRoomUserListResponse?) -> Void) {
  317. LNHttpManager.shared.getRoomUserList(
  318. roomId: roomId, size: 30, next: next ?? "",
  319. playmete: playmete, filter: filter)
  320. { res, err in
  321. runOnMain {
  322. handler(res)
  323. }
  324. res?.list.forEach {
  325. LNProfileManager.shared.updateProfileCache(uid: $0.user.userNo, name: $0.user.nickname, avatar: $0.user.avatar)
  326. }
  327. if let err {
  328. showToast(err.errorDesc)
  329. }
  330. }
  331. }
  332. // 申请列表的技能类别
  333. func getPlaymateCategory(handler: @escaping (LNRoomGameCategoryResponse?) -> Void) {
  334. LNHttpManager.shared.getRoomPlaymateCategory(roomId: roomId) { res, err in
  335. runOnMain {
  336. handler(res)
  337. }
  338. if let err {
  339. showToast(err.errorDesc)
  340. }
  341. }
  342. }
  343. }
  344. // MARK: 麦克风管理 - 普通用户
  345. extension LNRoomViewModel {
  346. // 关闭自己麦克风
  347. func muteMySeat() {
  348. seatStore.muteMicrophone()
  349. }
  350. // 打开自己麦克风
  351. func unmuteMySeat(handler: @escaping (Bool) -> Void) {
  352. let unmute = { [weak self] in
  353. guard let self else { return }
  354. seatStore.unmuteMicrophone { result in
  355. switch result {
  356. case .success:
  357. handler(true)
  358. case .failure(let err):
  359. showTencentError(err)
  360. handler(false)
  361. }
  362. }
  363. }
  364. if DeviceStore.shared.state.value.microphoneStatus == .off {
  365. DeviceStore.shared.openLocalMicrophone { result in
  366. switch result {
  367. case .success:
  368. unmute()
  369. case .failure(let err):
  370. showTencentError(err)
  371. handler(false)
  372. }
  373. }
  374. } else {
  375. unmute()
  376. }
  377. }
  378. }
  379. // MARK: 麦克风管理 - 管理员
  380. extension LNRoomViewModel {
  381. // 禁止某人的麦克风
  382. func muteSeat(uid: String, handler: @escaping (Bool) -> Void) {
  383. seatStore.closeRemoteMicrophone(userID: uid) { result in
  384. switch result {
  385. case .success:
  386. handler(true)
  387. case .failure(let err):
  388. showTencentError(err)
  389. handler(false)
  390. }
  391. }
  392. }
  393. // 解锁某人麦克风
  394. func unmuteSeat(uid: String, handler: @escaping (Bool) -> Void) {
  395. seatStore.openRemoteMicrophone(userID: uid, policy: .unlockOnly) { result in
  396. switch result {
  397. case .success:
  398. handler(true)
  399. case .failure(let err):
  400. showTencentError(err)
  401. handler(false)
  402. }
  403. }
  404. }
  405. }
  406. // MARK: 公屏
  407. extension LNRoomViewModel {
  408. private func setupMessageObservers() {
  409. messageStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
  410. guard let self else { return }
  411. var newMessage: [Barrage] = []
  412. if let lastId = lastmessage?.sequence,
  413. let index = state.messageList.lastIndex(where: { $0.sequence == lastId }) {
  414. newMessage.append(contentsOf: state.messageList[(index + 1)...])
  415. } else {
  416. newMessage.append(contentsOf: state.messageList)
  417. }
  418. lastmessage = newMessage.last
  419. systemHandlerQueue.async { [weak self] in
  420. guard let self else { return }
  421. var chatMessage: [LNRoomMessageItem] = []
  422. var systemMessage: [LNRoomPushMessage] = []
  423. newMessage.forEach {
  424. switch $0.messageType {
  425. case .text:
  426. if let messageItems = LNRoomUserMessage(info: $0)?.messageItems,
  427. !messageItems.isEmpty {
  428. chatMessage.append(contentsOf: messageItems)
  429. }
  430. case .custom:
  431. if let item = LNRoomPushMessage(info: $0) {
  432. systemMessage.append(item)
  433. if let messageItems = item.messageItems,
  434. !messageItems.isEmpty {
  435. chatMessage.append(contentsOf: messageItems)
  436. }
  437. }
  438. default:
  439. break
  440. }
  441. }
  442. if !chatMessage.isEmpty {
  443. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomMessageChanged(messages: chatMessage) }
  444. }
  445. handleSystemMessage(messages: systemMessage)
  446. }
  447. }.store(in: &cancellables)
  448. }
  449. func sendChatMessage(text: String, handler: @escaping (Bool) -> Void) {
  450. messageStore.sendTextMessage(text: text, extensionInfo: [
  451. LNRoomChatMessageTypeKey: LNRoomChatMessageType.chat.rawValue
  452. ]) { result in
  453. switch result {
  454. case .success:
  455. handler(true)
  456. case .failure(let err):
  457. showTencentError(err)
  458. handler(false)
  459. }
  460. }
  461. }
  462. }
  463. // MARK: 房间信息
  464. extension LNRoomViewModel {
  465. private func setupRoomInfoObserver() {
  466. LNRoomManager.shared.liveListStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
  467. guard let self else { return }
  468. if state.currentLive.liveID.isEmpty { return }
  469. if !state.currentLive.roomType.contains(.playmate) {
  470. showToast(.init(key: "A00389"))
  471. leaveRoom()
  472. return
  473. }
  474. if roomInfo.update(state.currentLive) {
  475. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomInfoChanged() }
  476. }
  477. let oldCount = seatApplyCount
  478. if state.currentLive.applySeatCount != seatApplyCount {
  479. seatApplyCount = state.currentLive.applySeatCount
  480. }
  481. if oldCount != seatApplyCount {
  482. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSeatApplyChanged() }
  483. }
  484. }.store(in: &cancellables)
  485. LNRoomManager.shared.liveListStore.liveListEventPublisher.receive(on: DispatchQueue.main).sink { [weak self] event in
  486. guard let self else { return }
  487. switch event {
  488. case .onLiveEnded(let roomId, _, _):
  489. if self.roomId == roomId {
  490. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed() }
  491. }
  492. case .onKickedOutOfLive(let roomId, _, _):
  493. if self.roomId == roomId {
  494. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed() }
  495. }
  496. default: break
  497. }
  498. }.store(in: &cancellables)
  499. }
  500. func updateRoomInfo(name: String, cover: String, forbidAudio: Bool, handler: @escaping (Bool) -> Void) {
  501. LNHttpManager.shared.updateRoom(roomId: roomId, roomTitle: name,
  502. roomCover: cover, forbidAudio: forbidAudio) { err in
  503. runOnMain {
  504. handler(err == nil)
  505. }
  506. if let err {
  507. showToast(err.errorDesc)
  508. }
  509. }
  510. }
  511. }
  512. // MARK: 观众事件
  513. extension LNRoomViewModel {
  514. private func setupAudienceEventObservers() {
  515. audienceStore.liveAudienceEventPublisher.receive(on: DispatchQueue.main).sink { [weak self] event in
  516. guard let self else { return }
  517. switch event {
  518. case .onAudienceJoined(let info):
  519. messageStore.appendLocalTip(message: Barrage.welcomeMessage(userInfo: info))
  520. default:
  521. break
  522. }
  523. }.store(in: &cancellables)
  524. }
  525. }
  526. // MARK: 礼物
  527. extension LNRoomViewModel {
  528. func sendGift(gift: LNGiftItemVO, to: [String], count: Int, handler: @escaping (Bool) -> Void) {
  529. let param = LNSendGiftParams()
  530. param.roomId = roomId
  531. param.giftId = gift.id
  532. param.userIds = to
  533. param.quantity = count
  534. LNGiftManager.shared.sendGift(params: param) { success in
  535. handler(success)
  536. }
  537. }
  538. func reloadGiftList() {
  539. guard !isLoadingGiftList else { return }
  540. isLoadingGiftList = true
  541. LNGiftManager.shared.fetchGiftList(roomId: roomId) { [weak self] list in
  542. guard let self else { return }
  543. if let list, !list.isEmpty {
  544. giftList = list
  545. }
  546. isLoadingGiftList = false
  547. }
  548. }
  549. }
  550. extension LNRoomViewModel {
  551. private func handleSystemMessage(messages: [LNRoomPushMessage]) {
  552. messages.forEach { message in
  553. if message.cmd == .MicClear,
  554. let cmd: LNRoomMicClearMessage = message.decodeCmdMessage(),
  555. cmd.type == self.waitingForSeat {
  556. self.waitingForSeat = .none
  557. }
  558. }
  559. }
  560. }