LNRoomSession.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. //
  2. // LNRoomSession.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. // 公屏事件
  12. func onRoomMessageChanged(session: LNRoomSession, messages: [LNRoomMessageItem])
  13. // 麦位事件
  14. func onRoomSeatsChanged(session: LNRoomSession, changed: [LNRoomSeatItem])
  15. func onRoomSpeakingUsersChanged(session: LNRoomSession, speakings: [String])
  16. func onRoomSeatApplyChanged(session: LNRoomSession, applyCount: Int)
  17. // 用户自身事件
  18. func onRoomUserDidJoinOnSeat(session: LNRoomSession, newSeat: LNRoomSeatItem)
  19. func onRoomUserDidLeaveSeat(session: LNRoomSession)
  20. func onRoomUserDidApplySeat(session: LNRoomSession, index: Int)
  21. func onRoomUserDidCancelApplySeat(session: LNRoomSession)
  22. // 房间配置事件
  23. func onRoomInfoChanged(session: LNRoomSession, roomInfo: LNRoomInfo)
  24. // 房间关闭
  25. func onRoomClosed(session: LNRoomSession)
  26. }
  27. extension LNRoomViewModelNotify {
  28. func onRoomMessageChanged(session: LNRoomSession, messages: [LNRoomMessageItem]) { }
  29. func onRoomSeatsChanged(session: LNRoomSession, changed: [LNRoomSeatItem]) { }
  30. func onRoomSpeakingUsersChanged(session: LNRoomSession, speakings: [String]) { }
  31. func onRoomSeatApplyChanged(session: LNRoomSession, applyCount: Int) { }
  32. func onRoomUserDidJoinOnSeat(session: LNRoomSession, newSeat: LNRoomSeatItem) { }
  33. func onRoomUserDidLeaveSeat(session: LNRoomSession) { }
  34. func onRoomUserDidApplySeat(session: LNRoomSession, index: Int) { }
  35. func onRoomUserDidCancelApplySeat(session: LNRoomSession) { }
  36. func onRoomInfoChanged(session: LNRoomSession, roomInfo: LNRoomInfo) { }
  37. func onRoomClosed(session: LNRoomSession) { }
  38. }
  39. enum LNApplySeatErrCode: Int {
  40. case noPermission = 50001
  41. }
  42. class LNRoomSession: NSObject {
  43. let roomId: String
  44. private let seatStore: LiveSeatStore
  45. private let guestStore: CoGuestStore
  46. private let messageStore: BarrageStore
  47. private let audienceStore: LiveAudienceStore
  48. private let systemHandlerQueue = DispatchQueue(label: "com.gami.room.system.message", attributes: .concurrent)
  49. private(set) var seatsInfo: [LNRoomSeatItem] = []
  50. private(set) var roomInfo = LNRoomInfo()
  51. private(set) var speakingUser: [String] = []
  52. private(set) var seatApplyCount: Int = 0
  53. private var lastmessage: Barrage? = nil
  54. private var isLoadingGiftList = false
  55. private(set) var giftList: [LNGiftItemVO] = []
  56. init(roomId: String) {
  57. self.roomId = roomId
  58. seatStore = LiveSeatStore.create(liveID: roomId)
  59. guestStore = CoGuestStore.create(liveID: roomId)
  60. messageStore = BarrageStore.create(liveID: roomId)
  61. audienceStore = LiveAudienceStore.create(liveID: roomId)
  62. super.init()
  63. LNEventDeliver.addObserver(self)
  64. setupSeatObservers()
  65. setupMessageObservers()
  66. setupRoomInfoObserver()
  67. setupAudienceEventObservers()
  68. reloadGiftList()
  69. LNRoomManager.shared.updateCurRoom(self)
  70. }
  71. func closeRoom() {
  72. LNRoomManager.shared.closeRoom { success in
  73. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed(session: self) }
  74. }
  75. }
  76. func leaveRoom() {
  77. LNRoomManager.shared.leaveRoom { success in
  78. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed(session: self) }
  79. }
  80. }
  81. func handleSystemMessage(message: LNRoomPushMessage) { }
  82. }
  83. // MARK: 麦位管理 - 普通用户
  84. extension LNRoomSession {
  85. var mySeatInfo: LNRoomSeatItem? {
  86. seatsInfo.first { $0.uid.isMyUid }
  87. }
  88. private func setupSeatObservers() {
  89. seatStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
  90. guard let self else { return }
  91. let wasOnMic = mySeatInfo != nil
  92. var hasChanged = state.seatList.count != seatsInfo.count
  93. var newSeats: [LNRoomSeatItem] = []
  94. var changedSeats: [LNRoomSeatItem] = []
  95. for seat in state.seatList {
  96. let item = seatsInfo.first(where: { $0.index == seat.index })
  97. ?? LNRoomSeatItem(index: seat.index)
  98. if item.update(seat) {
  99. changedSeats.append(item)
  100. hasChanged = true
  101. }
  102. newSeats.append(item)
  103. }
  104. if hasChanged {
  105. seatsInfo = newSeats
  106. if !wasOnMic, let mySeatInfo {
  107. // 自己上麦
  108. if roomInfo.forbidAudio {
  109. DeviceStore.shared.closeLocalMicrophone()
  110. } else {
  111. DeviceStore.shared.openLocalMicrophone(completion: nil)
  112. }
  113. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomUserDidJoinOnSeat(session: self, newSeat: mySeatInfo) }
  114. }
  115. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSeatsChanged(session: self, changed: changedSeats) }
  116. }
  117. let speakings = state.speakingUsers.filter { $0.value > 0 }.map { $0.key }.sorted { $1 > $0 }
  118. if speakings != speakingUser {
  119. speakingUser = speakings
  120. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSpeakingUsersChanged(session: self, speakings: speakings) }
  121. }
  122. }.store(in: &cancellables)
  123. }
  124. // 申请上麦
  125. func applySeat(index: Int, handler: @escaping (Bool) -> Void) {
  126. LNPermissionHelper.requestMicrophoneAccess { [weak self] success in
  127. guard let self else { return }
  128. guard success else {
  129. showToast(.init(key: "A00362"))
  130. return
  131. }
  132. LNHttpManager.shared.applySeat(roomId: roomId, index: index) { [weak self] err in
  133. runOnMain {
  134. handler(err == nil)
  135. }
  136. if let err {
  137. if case .serverError(let code, _) = err,
  138. code == LNApplySeatErrCode.noPermission.rawValue {
  139. showToast(err.errorDesc, icon: .icAbout.withTintColor(.fill))
  140. } else {
  141. showToast(err.errorDesc)
  142. }
  143. } else if let self {
  144. LNEventDeliver.notifyEvent {
  145. ($0 as? LNRoomViewModelNotify)?.onRoomUserDidApplySeat(session: self, index: index)
  146. }
  147. }
  148. }
  149. }
  150. }
  151. // 取消上麦申请
  152. func cancelSeatApply(handler: @escaping (Bool) -> Void) {
  153. LNHttpManager.shared.cancelApplySeat(roomId: roomId) { [weak self] err in
  154. runOnMain {
  155. handler(err == nil)
  156. }
  157. if let err {
  158. showToast(err.errorDesc)
  159. } else if let self {
  160. LNEventDeliver.notifyEvent {
  161. ($0 as? LNRoomViewModelNotify)?.onRoomUserDidCancelApplySeat(session: self)
  162. }
  163. }
  164. }
  165. }
  166. // 主动下麦
  167. func leaveSeat(handler: @escaping (Bool) -> Void) {
  168. seatStore.leaveSeat { [weak self] result in
  169. guard let self else { return }
  170. switch result {
  171. case .success:
  172. handler(true)
  173. LNEventDeliver.notifyEvent {
  174. ($0 as? LNRoomViewModelNotify)?.onRoomUserDidLeaveSeat(session: self)
  175. }
  176. case .failure(let err):
  177. showTencentError(err)
  178. handler(false)
  179. }
  180. }
  181. }
  182. }
  183. // MARK: 麦位管理 - 管理员
  184. extension LNRoomSession {
  185. // 踢人下麦
  186. func kickUserOffSeat(uid: String, handler: @escaping (Bool) -> Void) {
  187. seatStore.kickUserOutOfSeat(userID: uid) { result in
  188. switch result {
  189. case .success:
  190. handler(true)
  191. case .failure(let err):
  192. showTencentError(err)
  193. handler(false)
  194. }
  195. }
  196. }
  197. func clearApplyList(type: LNRoomApplySeatType, handler: @escaping (Bool) -> Void) {
  198. LNHttpManager.shared.clearApplySeatList(roomId: roomId, searchType: type) { err in
  199. runOnMain {
  200. handler(err == nil)
  201. }
  202. if let err {
  203. showToast(err.errorDesc)
  204. }
  205. }
  206. }
  207. // 接受上麦申请
  208. func acceptSeatApply(applyId: String, handler: @escaping (Bool) -> Void) {
  209. LNHttpManager.shared.handleApplySeat(roomId: roomId, applyId: applyId, accept: true) { err in
  210. runOnMain {
  211. handler(err == nil)
  212. }
  213. if let err {
  214. showToast(err.errorDesc)
  215. }
  216. }
  217. }
  218. // 拒绝上麦申请
  219. func rejectSeatApply(applyId: String, handler: @escaping (Bool) -> Void) {
  220. LNHttpManager.shared.handleApplySeat(roomId: roomId, applyId: applyId, accept: false) { err in
  221. runOnMain {
  222. handler(err == nil)
  223. }
  224. if let err {
  225. showToast(err.errorDesc)
  226. }
  227. }
  228. }
  229. // 邀请上麦
  230. func inviteUserToSeat(uid: String, index: Int, handler: @escaping (Bool) -> Void) {
  231. LNHttpManager.shared.inviteUserToSeat(roomId: roomId, uid: uid, index: index) { err in
  232. runOnMain {
  233. handler(err == nil)
  234. }
  235. if let err {
  236. showToast(err.errorDesc)
  237. }
  238. }
  239. }
  240. // 关闭麦位
  241. func lockSeat(num: Int, handler: @escaping (Bool) -> Void) {
  242. seatStore.lockSeat(seatIndex: num) { result in
  243. switch result {
  244. case .success:
  245. handler(true)
  246. case .failure(let err):
  247. showTencentError(err)
  248. handler(false)
  249. }
  250. }
  251. }
  252. // 解锁麦位
  253. func unlockSeat(num: Int, handler: @escaping (Bool) -> Void) {
  254. seatStore.unlockSeat(seatIndex: num) { result in
  255. switch result {
  256. case .success:
  257. handler(true)
  258. case .failure(let err):
  259. showTencentError(err)
  260. handler(false)
  261. }
  262. }
  263. }
  264. }
  265. // MARK: 麦克风管理 - 普通用户
  266. extension LNRoomSession {
  267. // 关闭自己麦克风
  268. func muteMySeat() {
  269. seatStore.muteMicrophone()
  270. }
  271. // 打开自己麦克风
  272. func unmuteMySeat(handler: @escaping (Bool) -> Void) {
  273. let unmute = { [weak self] in
  274. guard let self else { return }
  275. seatStore.unmuteMicrophone { result in
  276. switch result {
  277. case .success:
  278. handler(true)
  279. case .failure(let err):
  280. showTencentError(err)
  281. handler(false)
  282. }
  283. }
  284. }
  285. if DeviceStore.shared.state.value.microphoneStatus == .off {
  286. DeviceStore.shared.openLocalMicrophone { result in
  287. switch result {
  288. case .success:
  289. unmute()
  290. case .failure(let err):
  291. showTencentError(err)
  292. handler(false)
  293. }
  294. }
  295. } else {
  296. unmute()
  297. }
  298. }
  299. }
  300. // MARK: 麦克风管理 - 管理员
  301. extension LNRoomSession {
  302. // 禁止某人的麦克风
  303. func muteSeat(uid: String, handler: @escaping (Bool) -> Void) {
  304. seatStore.closeRemoteMicrophone(userID: uid) { 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 unmuteSeat(uid: String, handler: @escaping (Bool) -> Void) {
  316. seatStore.openRemoteMicrophone(userID: uid, policy: .unlockOnly) { result in
  317. switch result {
  318. case .success:
  319. handler(true)
  320. case .failure(let err):
  321. showTencentError(err)
  322. handler(false)
  323. }
  324. }
  325. }
  326. }
  327. // MARK: 公屏
  328. extension LNRoomSession {
  329. private func setupMessageObservers() {
  330. messageStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
  331. guard let self else { return }
  332. var newMessage: [Barrage] = []
  333. if let lastId = lastmessage?.sequence,
  334. let index = state.messageList.lastIndex(where: { $0.sequence == lastId }) {
  335. newMessage.append(contentsOf: state.messageList[(index + 1)...])
  336. } else {
  337. newMessage.append(contentsOf: state.messageList)
  338. }
  339. lastmessage = newMessage.last
  340. systemHandlerQueue.async { [weak self] in
  341. guard let self else { return }
  342. var chatMessages: [LNRoomMessageItem] = []
  343. var systemMessages: [LNRoomPushMessage] = []
  344. newMessage.forEach {
  345. switch $0.messageType {
  346. case .text:
  347. if let messageItems = LNRoomUserMessage(info: $0)?.messageItems,
  348. !messageItems.isEmpty {
  349. chatMessages.append(contentsOf: messageItems)
  350. }
  351. case .custom:
  352. if let item = LNRoomPushMessage(info: $0) {
  353. systemMessages.append(item)
  354. if let messageItems = item.messageItems,
  355. !messageItems.isEmpty {
  356. chatMessages.append(contentsOf: messageItems)
  357. }
  358. }
  359. default:
  360. break
  361. }
  362. }
  363. if !chatMessages.isEmpty {
  364. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomMessageChanged(session: self, messages: chatMessages) }
  365. }
  366. systemMessages.forEach {
  367. self.handleSystemMessage(message: $0)
  368. }
  369. }
  370. }.store(in: &cancellables)
  371. }
  372. func sendChatMessage(text: String, handler: @escaping (Bool) -> Void) {
  373. messageStore.sendTextMessage(text: text, extensionInfo: [
  374. LNRoomChatMessageTypeKey: LNRoomChatMessageType.chat.rawValue
  375. ]) { result in
  376. switch result {
  377. case .success:
  378. handler(true)
  379. case .failure(let err):
  380. showTencentError(err)
  381. handler(false)
  382. }
  383. }
  384. }
  385. }
  386. // MARK: 房间信息
  387. extension LNRoomSession {
  388. private func setupRoomInfoObserver() {
  389. LNRoomManager.shared.liveListStore.state.subscribe().receive(on: DispatchQueue.main).sink { [weak self] state in
  390. guard let self else { return }
  391. if state.currentLive.liveID.isEmpty { return }
  392. if !state.currentLive.roomType.contains(.playmate) {
  393. showToast(.init(key: "A00389"))
  394. leaveRoom()
  395. return
  396. }
  397. if roomInfo.update(state.currentLive) {
  398. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomInfoChanged(session: self, roomInfo: self.roomInfo) }
  399. }
  400. let oldCount = seatApplyCount
  401. if state.currentLive.applySeatCount != seatApplyCount {
  402. seatApplyCount = state.currentLive.applySeatCount
  403. }
  404. if oldCount != seatApplyCount {
  405. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomSeatApplyChanged(session: self, applyCount: self.seatApplyCount) }
  406. }
  407. }.store(in: &cancellables)
  408. LNRoomManager.shared.liveListStore.liveListEventPublisher.receive(on: DispatchQueue.main).sink { [weak self] event in
  409. guard let self else { return }
  410. switch event {
  411. case .onLiveEnded(let roomId, _, _):
  412. if self.roomId == roomId {
  413. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed(session: self) }
  414. }
  415. case .onKickedOutOfLive(let roomId, _, _):
  416. if self.roomId == roomId {
  417. LNEventDeliver.notifyEvent { ($0 as? LNRoomViewModelNotify)?.onRoomClosed(session: self) }
  418. }
  419. default: break
  420. }
  421. }.store(in: &cancellables)
  422. }
  423. func updateRoomInfo(name: String, cover: String, forbidAudio: Bool, handler: @escaping (Bool) -> Void) {
  424. LNHttpManager.shared.updateRoom(roomId: roomId, roomTitle: name,
  425. roomCover: cover, forbidAudio: forbidAudio) { err in
  426. runOnMain {
  427. handler(err == nil)
  428. }
  429. if let err {
  430. showToast(err.errorDesc)
  431. }
  432. }
  433. }
  434. }
  435. // MARK: 观众事件
  436. extension LNRoomSession {
  437. private func setupAudienceEventObservers() {
  438. audienceStore.liveAudienceEventPublisher.receive(on: DispatchQueue.main).sink { [weak self] event in
  439. guard let self else { return }
  440. switch event {
  441. case .onAudienceJoined(let info):
  442. messageStore.appendLocalTip(message: Barrage.welcomeMessage(userInfo: info))
  443. default:
  444. break
  445. }
  446. }.store(in: &cancellables)
  447. }
  448. }
  449. // MARK: 礼物
  450. extension LNRoomSession {
  451. func sendGift(gift: LNGiftItemVO, to: [String], count: Int, handler: @escaping (Bool) -> Void) {
  452. let param = LNSendGiftParams()
  453. param.roomId = roomId
  454. param.giftId = gift.id
  455. param.userIds = to
  456. param.quantity = count
  457. LNGiftManager.shared.sendGift(params: param) { success in
  458. handler(success)
  459. }
  460. }
  461. func reloadGiftList() {
  462. guard !isLoadingGiftList else { return }
  463. isLoadingGiftList = true
  464. LNGiftManager.shared.fetchGiftList(roomId: roomId) { [weak self] list in
  465. guard let self else { return }
  466. if let list, !list.isEmpty {
  467. giftList = list
  468. }
  469. isLoadingGiftList = false
  470. }
  471. }
  472. }