LNRoomViewModel.swift 19 KB

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