LNRoomViewModel.swift 18 KB

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