use-srs-ws.ts 14 KB

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