use-srs-ws.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. import { getRandomString } from 'billd-utils';
  2. import { computed, onUnmounted, ref } from 'vue';
  3. import { fetchRtcV1Publish } from '@/api/srs';
  4. import { WEBSOCKET_URL } from '@/constant';
  5. import {
  6. DanmuMsgTypeEnum,
  7. IDanmu,
  8. ILiveRoom,
  9. ILiveUser,
  10. IUser,
  11. LiveRoomTypeEnum,
  12. } from '@/interface';
  13. import {
  14. WSGetRoomAllUserType,
  15. WsAnswerType,
  16. WsCandidateType,
  17. WsGetLiveUserType,
  18. WsHeartbeatType,
  19. WsJoinType,
  20. WsLeavedType,
  21. WsMessageType,
  22. WsOfferType,
  23. WsOtherJoinType,
  24. WsRoomLivingType,
  25. WsStartLiveType,
  26. WsUpdateJoinInfoType,
  27. } from '@/interface-ws';
  28. import { WebRTCClass } from '@/network/webRTC';
  29. import {
  30. WebSocketClass,
  31. WsConnectStatusEnum,
  32. WsMsgTypeEnum,
  33. prettierReceiveWsMsg,
  34. } from '@/network/webSocket';
  35. import { useAppStore } from '@/store/app';
  36. import { useNetworkStore } from '@/store/network';
  37. import { useUserStore } from '@/store/user';
  38. import { createVideo } from '@/utils';
  39. import { useRTCParams } from './use-rtc-params';
  40. export const useSrsWs = () => {
  41. const appStore = useAppStore();
  42. const userStore = useUserStore();
  43. const networkStore = useNetworkStore();
  44. const { maxBitrate, maxFramerate, resolutionRatio } = useRTCParams();
  45. const loopHeartbeatTimer = ref();
  46. const liveUserList = ref<ILiveUser[]>([]);
  47. const roomId = ref('');
  48. const isPull = ref(false);
  49. const roomLiving = ref(false);
  50. const isAnchor = ref(false);
  51. const liveRoomInfo = ref<ILiveRoom>();
  52. const anchorInfo = ref<IUser>();
  53. const canvasVideoStream = ref<MediaStream>();
  54. const lastCoverImg = ref('');
  55. const currentMaxBitrate = ref(maxBitrate.value[2].value);
  56. const currentResolutionRatio = ref(resolutionRatio.value[3].value);
  57. const currentMaxFramerate = ref(maxFramerate.value[2].value);
  58. const damuList = ref<IDanmu[]>([]);
  59. onUnmounted(() => {
  60. clearInterval(loopHeartbeatTimer.value);
  61. });
  62. const mySocketId = computed(() => {
  63. return networkStore.wsMap.get(roomId.value)?.socketIo?.id || '-1';
  64. });
  65. function handleHeartbeat(socketId: string) {
  66. loopHeartbeatTimer.value = setInterval(() => {
  67. const ws = networkStore.wsMap.get(roomId.value);
  68. if (!ws) return;
  69. ws.send<WsHeartbeatType['data']>({
  70. msgType: WsMsgTypeEnum.heartbeat,
  71. data: {
  72. socket_id: socketId,
  73. },
  74. });
  75. }, 1000 * 5);
  76. }
  77. async function handleSendOffer({ receiver }: { receiver: string }) {
  78. console.log('开始handleSendOffer');
  79. const ws = networkStore.wsMap.get(roomId.value);
  80. if (!ws) return;
  81. const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
  82. if (!rtc) return;
  83. canvasVideoStream.value?.getTracks().forEach((track) => {
  84. if (rtc && canvasVideoStream.value) {
  85. console.log('插入track', track);
  86. rtc.peerConnection?.addTrack(track, canvasVideoStream.value);
  87. }
  88. });
  89. const sdp = await rtc.createOffer();
  90. await rtc.setLocalDescription(sdp!);
  91. const myLiveRoom = userStore.userInfo!.live_rooms![0];
  92. const res = await fetchRtcV1Publish({
  93. api: `/rtc/v1/publish/`,
  94. clientip: null,
  95. sdp: sdp!.sdp!,
  96. streamurl: `${myLiveRoom.rtmp_url!}?token=${myLiveRoom.key!}&type=${
  97. LiveRoomTypeEnum.user_srs
  98. }`,
  99. tid: getRandomString(10),
  100. });
  101. networkStore.wsMap.get(roomId.value)?.send<WsUpdateJoinInfoType['data']>({
  102. msgType: WsMsgTypeEnum.updateJoinInfo,
  103. data: {
  104. live_room_id: Number(roomId.value),
  105. track: {
  106. audio: 1,
  107. video: 1,
  108. },
  109. },
  110. });
  111. if (res.data.code !== 0) {
  112. console.error('/rtc/v1/publish/拿不到sdp');
  113. window.$message.error('/rtc/v1/publish/拿不到sdp');
  114. return;
  115. }
  116. await rtc.setRemoteDescription(
  117. new RTCSessionDescription({ type: 'answer', sdp: res.data.sdp })
  118. );
  119. }
  120. function handleStartLive({
  121. coverImg,
  122. name,
  123. type,
  124. receiver,
  125. }: {
  126. coverImg?: string;
  127. name?: string;
  128. type: LiveRoomTypeEnum;
  129. receiver: string;
  130. videoEl?: HTMLVideoElement;
  131. }) {
  132. networkStore.wsMap.get(roomId.value)?.send<WsStartLiveType['data']>({
  133. msgType: WsMsgTypeEnum.startLive,
  134. data: {
  135. cover_img: coverImg!,
  136. name: name!,
  137. type,
  138. },
  139. });
  140. if (type !== LiveRoomTypeEnum.user_wertc) {
  141. startNewWebRtc({
  142. videoEl: createVideo({}),
  143. receiver,
  144. type,
  145. });
  146. }
  147. }
  148. function sendJoin() {
  149. const instance = networkStore.wsMap.get(roomId.value);
  150. if (!instance) return;
  151. instance.send<WsJoinType['data']>({
  152. msgType: WsMsgTypeEnum.join,
  153. data: {
  154. socket_id: mySocketId.value,
  155. live_room: {
  156. id: Number(roomId.value),
  157. },
  158. user_info: userStore.userInfo,
  159. },
  160. });
  161. }
  162. /** 原生的webrtc时,receiver必传 */
  163. function startNewWebRtc({
  164. receiver,
  165. videoEl,
  166. type,
  167. }: {
  168. receiver: string;
  169. videoEl: HTMLVideoElement;
  170. type: LiveRoomTypeEnum;
  171. }) {
  172. console.warn('开始new WebRTCClass', `${roomId.value}___${receiver!}`);
  173. new WebRTCClass({
  174. maxBitrate: currentMaxBitrate.value,
  175. maxFramerate: currentMaxFramerate.value,
  176. resolutionRatio: currentResolutionRatio.value,
  177. roomId: `${roomId.value}___${receiver!}`,
  178. videoEl,
  179. isSRS: true,
  180. receiver,
  181. });
  182. handleSendOffer({
  183. receiver,
  184. });
  185. }
  186. function initReceive() {
  187. const ws = networkStore.wsMap.get(roomId.value);
  188. if (!ws?.socketIo) return;
  189. // websocket连接成功
  190. ws.socketIo.on(WsConnectStatusEnum.connect, () => {
  191. prettierReceiveWsMsg(WsConnectStatusEnum.connect, ws.socketIo);
  192. handleHeartbeat(ws.socketIo!.id);
  193. if (!ws) return;
  194. ws.status = WsConnectStatusEnum.connect;
  195. ws.update();
  196. sendJoin();
  197. });
  198. // websocket连接断开
  199. ws.socketIo.on(WsConnectStatusEnum.disconnect, () => {
  200. prettierReceiveWsMsg(WsConnectStatusEnum.disconnect, ws);
  201. if (!ws) return;
  202. ws.status = WsConnectStatusEnum.disconnect;
  203. ws.update();
  204. });
  205. ws.socketIo.on(WsMsgTypeEnum.offer, async (data: WsOfferType['data']) => {
  206. console.log('收到offer', data);
  207. if (data.receiver === mySocketId.value) {
  208. console.warn('是发给我的offer');
  209. console.warn('开始new WebRTCClass', `${roomId.value}___${data.sender}`);
  210. const videoEl = createVideo({ appendChild: true });
  211. const rtc = new WebRTCClass({
  212. maxBitrate: currentMaxBitrate.value,
  213. maxFramerate: currentMaxFramerate.value,
  214. resolutionRatio: currentResolutionRatio.value,
  215. roomId: `${roomId.value}___${data.sender}`,
  216. videoEl,
  217. isSRS: true,
  218. receiver: data.receiver,
  219. });
  220. await rtc.setRemoteDescription(data.sdp);
  221. const answer = await rtc.createAnswer();
  222. if (answer) {
  223. await rtc.setLocalDescription(answer);
  224. ws.send<WsAnswerType['data']>({
  225. msgType: WsMsgTypeEnum.answer,
  226. data: {
  227. live_room_id: Number(roomId.value),
  228. sdp: answer,
  229. receiver: data.sender,
  230. sender: mySocketId.value,
  231. },
  232. });
  233. } else {
  234. console.error('没有answer');
  235. }
  236. } else {
  237. console.error('不是发给我的offer');
  238. }
  239. });
  240. ws.socketIo.on(WsMsgTypeEnum.answer, (data: WsAnswerType['data']) => {
  241. console.log('收到answer', data);
  242. if (data.receiver === mySocketId.value) {
  243. console.warn('是发给我的answer', `${roomId.value}___${data.receiver}`);
  244. const rtc = networkStore.getRtcMap(`${roomId.value}___${data.sender}`)!;
  245. rtc.setRemoteDescription(data.sdp);
  246. } else {
  247. console.error('不是发给我的answer');
  248. }
  249. });
  250. ws.socketIo.on(WsMsgTypeEnum.candidate, (data: WsCandidateType['data']) => {
  251. console.log('收到candidate', data);
  252. if (data.receiver === mySocketId.value) {
  253. console.warn('是发给我的candidate');
  254. const rtc = networkStore.getRtcMap(`${roomId.value}___${data.sender}`)!;
  255. rtc.addIceCandidate(data.candidate);
  256. } else {
  257. console.error('不是发给我的candidate');
  258. }
  259. });
  260. // 主播正在直播
  261. ws.socketIo.on(WsMsgTypeEnum.roomLiving, (data: WsRoomLivingType) => {
  262. prettierReceiveWsMsg(WsMsgTypeEnum.roomLiving, data);
  263. roomLiving.value = true;
  264. });
  265. // 主播不在直播
  266. ws.socketIo.on(WsMsgTypeEnum.roomNoLive, (data) => {
  267. prettierReceiveWsMsg(WsMsgTypeEnum.roomNoLive, data);
  268. roomLiving.value = false;
  269. });
  270. // 当前所有在线用户
  271. ws.socketIo.on(
  272. WsMsgTypeEnum.liveUser,
  273. (data: WSGetRoomAllUserType['data']) => {
  274. prettierReceiveWsMsg(WsMsgTypeEnum.liveUser, data);
  275. const res = data.liveUser.map((item) => {
  276. return {
  277. id: item.id,
  278. // userInfo: item.id,
  279. };
  280. });
  281. liveUserList.value = res;
  282. }
  283. );
  284. // 收到用户发送消息
  285. ws.socketIo.on(WsMsgTypeEnum.message, (data: WsMessageType) => {
  286. prettierReceiveWsMsg(WsMsgTypeEnum.message, data);
  287. damuList.value.push({
  288. socket_id: data.socket_id,
  289. msgType: DanmuMsgTypeEnum.danmu,
  290. msg: data.data.msg,
  291. userInfo: data.user_info,
  292. });
  293. });
  294. // 用户加入房间完成
  295. ws.socketIo.on(WsMsgTypeEnum.joined, (data: WsJoinType['data']) => {
  296. prettierReceiveWsMsg(WsMsgTypeEnum.joined, data);
  297. liveUserList.value.push({
  298. id: data.socket_id,
  299. userInfo: data.user_info,
  300. });
  301. liveRoomInfo.value = data.live_room;
  302. anchorInfo.value = data.anchor_info;
  303. ws.send<WsGetLiveUserType['data']>({
  304. msgType: WsMsgTypeEnum.getLiveUser,
  305. data: {
  306. live_room_id: data.live_room.id!,
  307. },
  308. });
  309. });
  310. // 其他用户加入房间
  311. ws.socketIo.on(WsMsgTypeEnum.otherJoin, (data: WsOtherJoinType['data']) => {
  312. prettierReceiveWsMsg(WsMsgTypeEnum.otherJoin, data);
  313. liveUserList.value.push({
  314. id: data.join_socket_id,
  315. userInfo: data.join_user_info,
  316. });
  317. const danmu: IDanmu = {
  318. msgType: DanmuMsgTypeEnum.otherJoin,
  319. socket_id: data.join_socket_id,
  320. userInfo: data.join_user_info,
  321. msg: '',
  322. };
  323. damuList.value.push(danmu);
  324. ws.send<WsGetLiveUserType['data']>({
  325. msgType: WsMsgTypeEnum.getLiveUser,
  326. data: {
  327. live_room_id: data.live_room.id!,
  328. },
  329. });
  330. if (!isPull.value) {
  331. if (!roomLiving.value) return;
  332. liveUserList.value.forEach(async (item) => {
  333. const receiver = item.id;
  334. if (
  335. receiver === mySocketId.value ||
  336. networkStore.getRtcMap(`${roomId.value}___${receiver!}`)
  337. )
  338. return;
  339. console.warn('开始new WebRTCClass', `${roomId.value}___${receiver!}`);
  340. const rtc = new WebRTCClass({
  341. maxBitrate: currentMaxBitrate.value,
  342. maxFramerate: currentMaxFramerate.value,
  343. resolutionRatio: currentResolutionRatio.value,
  344. roomId: `${roomId.value}___${receiver!}`,
  345. videoEl: createVideo({}),
  346. isSRS: false,
  347. receiver,
  348. });
  349. networkStore.updateRtcMap(`${roomId.value}___${receiver!}`, rtc);
  350. canvasVideoStream.value?.getTracks().forEach((track) => {
  351. if (rtc && canvasVideoStream.value) {
  352. console.log('插入track', track);
  353. rtc.peerConnection?.addTrack(track, canvasVideoStream.value);
  354. }
  355. });
  356. const ws = networkStore.wsMap.get(roomId.value)!;
  357. const offer = await rtc.createOffer();
  358. await rtc.setLocalDescription(offer!);
  359. ws.send<WsOfferType['data']>({
  360. msgType: WsMsgTypeEnum.offer,
  361. data: {
  362. sdp: offer,
  363. live_room_id: Number(roomId.value),
  364. sender: mySocketId.value,
  365. receiver,
  366. },
  367. });
  368. });
  369. }
  370. });
  371. // 用户离开房间
  372. ws.socketIo.on(WsMsgTypeEnum.leave, (data) => {
  373. prettierReceiveWsMsg(WsMsgTypeEnum.leave, data);
  374. });
  375. // 用户离开房间完成
  376. ws.socketIo.on(WsMsgTypeEnum.leaved, (data: WsLeavedType['data']) => {
  377. prettierReceiveWsMsg(WsMsgTypeEnum.leaved, data);
  378. networkStore.rtcMap
  379. .get(`${roomId.value}___${data.socket_id as string}`)
  380. ?.close();
  381. networkStore.removeRtc(`${roomId.value}___${data.socket_id as string}`);
  382. const res = liveUserList.value.filter(
  383. (item) => item.id !== data.socket_id
  384. );
  385. liveUserList.value = res;
  386. damuList.value.push({
  387. socket_id: data.socket_id,
  388. msgType: DanmuMsgTypeEnum.userLeaved,
  389. userInfo: data.user_info,
  390. msg: '',
  391. });
  392. });
  393. }
  394. function initSrsWs(data: {
  395. isAnchor: boolean;
  396. roomId: string;
  397. currentResolutionRatio?: number;
  398. currentMaxFramerate?: number;
  399. currentMaxBitrate?: number;
  400. }) {
  401. roomId.value = data.roomId;
  402. isAnchor.value = data.isAnchor;
  403. if (data.currentMaxBitrate) {
  404. currentMaxBitrate.value = data.currentMaxBitrate;
  405. }
  406. if (data.currentMaxFramerate) {
  407. currentMaxFramerate.value = data.currentMaxFramerate;
  408. }
  409. if (data.currentResolutionRatio) {
  410. currentResolutionRatio.value = data.currentResolutionRatio;
  411. }
  412. new WebSocketClass({
  413. roomId: roomId.value,
  414. url: WEBSOCKET_URL,
  415. isAnchor: data.isAnchor,
  416. });
  417. initReceive();
  418. }
  419. return {
  420. isPull,
  421. initSrsWs,
  422. handleStartLive,
  423. mySocketId,
  424. canvasVideoStream,
  425. lastCoverImg,
  426. roomLiving,
  427. liveRoomInfo,
  428. anchorInfo,
  429. liveUserList,
  430. damuList,
  431. currentMaxFramerate,
  432. currentMaxBitrate,
  433. currentResolutionRatio,
  434. };
  435. };