Prechádzať zdrojové kódy

refactor: 重构推流方式

shuisheng 2 rokov pred
rodič
commit
2d9fbb949e

+ 1 - 0
src/hooks/use-push.ts

@@ -218,6 +218,7 @@ export function usePush() {
   }
 
   async function startLive({ type, receiver, chunkDelay }) {
+    console.log('startLive', { type, receiver, chunkDelay });
     if (!loginTip()) return;
     const flag = await userHasLiveRoom();
     if (!flag) {

+ 567 - 0
src/hooks/use-rtc-ws.ts

@@ -0,0 +1,567 @@
+import { getRandomString } from 'billd-utils';
+import { computed, onUnmounted, ref } from 'vue';
+
+import { fetchRtcV1Publish } from '@/api/srs';
+import { SRS_CB_URL_PARAMS, WEBSOCKET_URL } from '@/constant';
+import {
+  DanmuMsgTypeEnum,
+  IDanmu,
+  ILiveUser,
+  IUser,
+  LiveRoomTypeEnum,
+} from '@/interface';
+import {
+  WSGetRoomAllUserType,
+  WsAnswerType,
+  WsCandidateType,
+  WsConnectStatusEnum,
+  WsDisableSpeakingType,
+  WsGetLiveUserType,
+  WsHeartbeatType,
+  WsJoinType,
+  WsLeavedType,
+  WsMessageType,
+  WsMsgTypeEnum,
+  WsOfferType,
+  WsOtherJoinType,
+  WsRoomLivingType,
+  WsStartLiveType,
+  WsUpdateJoinInfoType,
+} from '@/interface-ws';
+import { WebRTCClass } from '@/network/webRTC';
+import { WebSocketClass, prettierReceiveWsMsg } from '@/network/webSocket';
+import { useAppStore } from '@/store/app';
+import { useNetworkStore } from '@/store/network';
+import { useUserStore } from '@/store/user';
+import { createVideo, formatDownTime } from '@/utils';
+
+import { useRTCParams } from './use-rtcParams';
+
+export const useSrsWs = () => {
+  const appStore = useAppStore();
+  const userStore = useUserStore();
+  const networkStore = useNetworkStore();
+  const { maxBitrate, maxFramerate, resolutionRatio } = useRTCParams();
+
+  const loopHeartbeatTimer = ref();
+  const liveUserList = ref<ILiveUser[]>([]);
+  const roomId = ref('');
+  const isPull = ref(false);
+  const roomLiving = ref(false);
+  const isAnchor = ref(false);
+  const isSRS = ref(false);
+  const anchorInfo = ref<IUser>();
+  const anchorSocketId = ref('');
+  const canvasVideoStream = ref<MediaStream>();
+  const lastCoverImg = ref('');
+  const currentMaxBitrate = ref(maxBitrate.value[3].value);
+  const currentMaxFramerate = ref(maxFramerate.value[2].value);
+  const currentResolutionRatio = ref(resolutionRatio.value[3].value);
+  const timerObj = ref({});
+  const damuList = ref<IDanmu[]>([]);
+
+  onUnmounted(() => {
+    clearInterval(loopHeartbeatTimer.value);
+  });
+
+  const mySocketId = computed(() => {
+    return networkStore.wsMap.get(roomId.value)?.socketIo?.id || '-1';
+  });
+
+  function handleHeartbeat(socketId: string) {
+    loopHeartbeatTimer.value = setInterval(() => {
+      const ws = networkStore.wsMap.get(roomId.value);
+      if (!ws) return;
+      ws.send<WsHeartbeatType['data']>({
+        requestId: getRandomString(8),
+        msgType: WsMsgTypeEnum.heartbeat,
+        data: {
+          socket_id: socketId,
+        },
+      });
+    }, 1000 * 5);
+  }
+
+  async function handleSendOffer({ receiver }: { receiver: string }) {
+    console.log('开始handleSendOffer', receiver);
+    const ws = networkStore.wsMap.get(roomId.value);
+    if (!ws) return;
+    const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
+    if (!rtc) return;
+    canvasVideoStream.value?.getTracks().forEach((track) => {
+      if (rtc && canvasVideoStream.value) {
+        console.log('11canvasVideoStream插入track', track.kind, track);
+        rtc.peerConnection?.addTrack(track, canvasVideoStream.value);
+      }
+    });
+    const sdp = await rtc.createOffer();
+    await rtc.setLocalDescription(sdp!);
+    const myLiveRoom = userStore.userInfo!.live_rooms![0];
+    const res = await fetchRtcV1Publish({
+      api: `/rtc/v1/publish/`,
+      clientip: null,
+      sdp: sdp!.sdp!,
+      streamurl: `${myLiveRoom.rtmp_url!}?${
+        SRS_CB_URL_PARAMS.publishKey
+      }=${myLiveRoom.key!}&${SRS_CB_URL_PARAMS.publishType}=${
+        LiveRoomTypeEnum.user_srs
+      }`,
+      tid: getRandomString(10),
+    });
+    networkStore.wsMap.get(roomId.value)?.send<WsUpdateJoinInfoType['data']>({
+      requestId: getRandomString(8),
+      msgType: WsMsgTypeEnum.updateJoinInfo,
+      data: {
+        live_room_id: Number(roomId.value),
+        track: {
+          audio: 1,
+          video: 1,
+        },
+      },
+    });
+    if (res.data.code !== 0) {
+      console.error('/rtc/v1/publish/拿不到sdp');
+      window.$message.error('/rtc/v1/publish/拿不到sdp');
+      return;
+    }
+    await rtc.setRemoteDescription(
+      new RTCSessionDescription({ type: 'answer', sdp: res.data.sdp })
+    );
+  }
+
+  function handleStartLive({
+    coverImg,
+    name,
+    type,
+    receiver,
+    chunkDelay,
+  }: {
+    coverImg?: string;
+    name?: string;
+    type: LiveRoomTypeEnum;
+    receiver: string;
+    videoEl?: HTMLVideoElement;
+    chunkDelay: number;
+  }) {
+    console.log('handleStartLivehandleStartLive', receiver);
+    networkStore.wsMap.get(roomId.value)?.send<WsStartLiveType['data']>({
+      requestId: getRandomString(8),
+      msgType: WsMsgTypeEnum.startLive,
+      data: {
+        cover_img: coverImg!,
+        name: name!,
+        type,
+        chunkDelay,
+      },
+    });
+    if (type === LiveRoomTypeEnum.user_msr) {
+      return;
+    }
+    isSRS.value = true;
+    if (type !== LiveRoomTypeEnum.user_wertc) {
+      startNewWebRtc({
+        videoEl: createVideo({}),
+        receiver,
+      });
+    }
+  }
+
+  function sendJoin() {
+    const instance = networkStore.wsMap.get(roomId.value);
+    if (!instance) return;
+    instance.send<WsJoinType['data']>({
+      requestId: getRandomString(8),
+      msgType: WsMsgTypeEnum.join,
+      data: {
+        socket_id: mySocketId.value,
+        live_room: {
+          id: Number(roomId.value),
+        },
+        user_info: userStore.userInfo,
+      },
+    });
+  }
+
+  /** 原生的webrtc时,receiver必传 */
+  function startNewWebRtc({
+    receiver,
+    videoEl,
+  }: {
+    receiver: string;
+    videoEl: HTMLVideoElement;
+  }) {
+    console.warn(
+      '22开始new WebRTCClass',
+      receiver,
+      `${roomId.value}___${receiver!}`,
+      isSRS.value,
+      canvasVideoStream.value
+    );
+    new WebRTCClass({
+      maxBitrate: currentMaxBitrate.value,
+      maxFramerate: currentMaxFramerate.value,
+      resolutionRatio: currentResolutionRatio.value,
+      roomId: `${roomId.value}___${receiver!}`,
+      videoEl,
+      isSRS: true,
+      receiver,
+      localStream: canvasVideoStream.value,
+    });
+    // isSRS.value = true;
+    handleSendOffer({
+      receiver,
+    });
+  }
+
+  function initReceive() {
+    const ws = networkStore.wsMap.get(roomId.value);
+    if (!ws?.socketIo) return;
+    // websocket连接成功
+    ws.socketIo.on(WsConnectStatusEnum.connect, () => {
+      prettierReceiveWsMsg(WsConnectStatusEnum.connect, ws.socketIo);
+      handleHeartbeat(ws.socketIo!.id);
+      if (!ws) return;
+      ws.status = WsConnectStatusEnum.connect;
+      ws.update();
+      sendJoin();
+    });
+
+    // websocket连接断开
+    ws.socketIo.on(WsConnectStatusEnum.disconnect, (err) => {
+      prettierReceiveWsMsg(WsConnectStatusEnum.disconnect, ws);
+      console.log('websocket连接断开', err);
+      if (!ws) return;
+      ws.status = WsConnectStatusEnum.disconnect;
+      ws.update();
+    });
+
+    // 收到offer
+    ws.socketIo.on(WsMsgTypeEnum.offer, async (data: WsOfferType['data']) => {
+      console.log('收到offer', data);
+      if (data.receiver === mySocketId.value) {
+        console.warn('是发给我的offer');
+        console.warn(
+          '33开始new WebRTCClass',
+          `${roomId.value}___${data.sender}`
+        );
+        const videoEl = createVideo({ appendChild: true });
+        const rtc = new WebRTCClass({
+          maxBitrate: currentMaxBitrate.value,
+          maxFramerate: currentMaxFramerate.value,
+          resolutionRatio: currentResolutionRatio.value,
+          roomId: `${roomId.value}___${data.sender}`,
+          videoEl,
+          isSRS: true,
+          receiver: data.receiver,
+        });
+        isSRS.value = true;
+        await rtc.setRemoteDescription(data.sdp);
+        const answer = await rtc.createAnswer();
+        if (answer) {
+          await rtc.setLocalDescription(answer);
+          ws.send<WsAnswerType['data']>({
+            requestId: getRandomString(8),
+            msgType: WsMsgTypeEnum.answer,
+            data: {
+              live_room_id: Number(roomId.value),
+              sdp: answer,
+              receiver: data.sender,
+              sender: mySocketId.value,
+            },
+          });
+        } else {
+          console.error('没有answer');
+        }
+      } else {
+        console.error('不是发给我的offer');
+      }
+    });
+
+    // 收到answer
+    ws.socketIo.on(WsMsgTypeEnum.answer, (data: WsAnswerType['data']) => {
+      console.log('收到answer', data);
+      if (data.receiver === mySocketId.value) {
+        console.warn('是发给我的answer', `${roomId.value}___${data.receiver}`);
+        const rtc = networkStore.getRtcMap(`${roomId.value}___${data.sender}`)!;
+        rtc.setRemoteDescription(data.sdp);
+      } else {
+        console.error('不是发给我的answer');
+      }
+    });
+
+    // 收到candidate
+    ws.socketIo.on(WsMsgTypeEnum.candidate, (data: WsCandidateType['data']) => {
+      console.log('收到candidate', data);
+      if (data.receiver === mySocketId.value) {
+        console.warn('是发给我的candidate');
+        const rtc = networkStore.getRtcMap(`${roomId.value}___${data.sender}`)!;
+        rtc.addIceCandidate(data.candidate);
+      } else {
+        console.error('不是发给我的candidate');
+      }
+    });
+
+    // 主播正在直播
+    ws.socketIo.on(
+      WsMsgTypeEnum.roomLiving,
+      (data: WsRoomLivingType['data']) => {
+        prettierReceiveWsMsg(WsMsgTypeEnum.roomLiving, data);
+        roomLiving.value = true;
+        if (data.anchor_socket_id) {
+          anchorSocketId.value = data.anchor_socket_id;
+        }
+        isSRS.value = true;
+        if (data.live_room.type === LiveRoomTypeEnum.user_wertc) {
+          isSRS.value = false;
+          startNewWebRtc({
+            videoEl: createVideo({}),
+            receiver: data.anchor_socket_id,
+          });
+        }
+      }
+    );
+
+    // 主播不在直播
+    ws.socketIo.on(WsMsgTypeEnum.roomNoLive, (data) => {
+      prettierReceiveWsMsg(WsMsgTypeEnum.roomNoLive, data);
+      roomLiving.value = false;
+    });
+
+    // 当前所有在线用户
+    ws.socketIo.on(
+      WsMsgTypeEnum.liveUser,
+      (data: WSGetRoomAllUserType['data']) => {
+        console.log('当前所有在线用户当前所有在线用户', data.liveUser.length);
+        prettierReceiveWsMsg(WsMsgTypeEnum.liveUser, data);
+        liveUserList.value = data.liveUser;
+      }
+    );
+
+    // 收到用户发送消息
+    ws.socketIo.on(WsMsgTypeEnum.message, (data: WsMessageType) => {
+      prettierReceiveWsMsg(WsMsgTypeEnum.message, data);
+      damuList.value.push({
+        request_id: data.request_id,
+        socket_id: data.socket_id,
+        msgType: DanmuMsgTypeEnum.danmu,
+        msg: data.data.msg,
+        userInfo: data.user_info,
+        msgIsFile: data.data.msgIsFile,
+        sendMsgTime: data.data.sendMsgTime,
+      });
+    });
+
+    // 收到disableSpeaking
+    ws.socketIo.on(
+      WsMsgTypeEnum.disableSpeaking,
+      (data: WsDisableSpeakingType['data']) => {
+        prettierReceiveWsMsg(WsMsgTypeEnum.disableSpeaking, data);
+        if (data.is_disable_speaking) {
+          window.$message.error('你已被禁言!');
+          appStore.disableSpeaking.set(data.live_room_id, {
+            exp: data.disable_expired_at,
+            label: formatDownTime({
+              startTime: +new Date(),
+              endTime: data.disable_expired_at,
+            }),
+          });
+          clearTimeout(timerObj.value[data.live_room_id]);
+          timerObj.value[data.live_room_id] = setInterval(() => {
+            if (
+              data.disable_expired_at &&
+              +new Date() > data.disable_expired_at
+            ) {
+              clearTimeout(timerObj.value[data.live_room_id]);
+            }
+            appStore.disableSpeaking.set(data.live_room_id, {
+              exp: data.disable_expired_at!,
+              label: formatDownTime({
+                startTime: +new Date(),
+                endTime: data.disable_expired_at!,
+              }),
+            });
+          }, 1000);
+          damuList.value = damuList.value.filter(
+            (v) => v.request_id !== data.request_id
+          );
+        }
+        if (data.user_id !== userStore.userInfo?.id && data.disable_ok) {
+          window.$message.success('禁言成功!');
+        }
+        if (
+          data.user_id !== userStore.userInfo?.id &&
+          data.restore_disable_ok
+        ) {
+          window.$message.success('解除禁言成功!');
+        }
+        if (
+          data.user_id === userStore.userInfo?.id &&
+          data.restore_disable_ok
+        ) {
+          window.$message.success('禁言接触了!');
+          clearTimeout(timerObj.value[data.live_room_id]);
+          appStore.disableSpeaking.delete(data.live_room_id);
+        }
+      }
+    );
+
+    // 用户加入房间完成
+    ws.socketIo.on(WsMsgTypeEnum.joined, (data: WsJoinType['data']) => {
+      prettierReceiveWsMsg(WsMsgTypeEnum.joined, data);
+      // liveUserList.value.push({
+      //   id: data.socket_id,
+      //   userInfo: data.user_info,
+      // });
+      appStore.setLiveRoomInfo(data.live_room);
+      anchorInfo.value = data.anchor_info;
+      ws.send<WsGetLiveUserType['data']>({
+        requestId: getRandomString(8),
+        msgType: WsMsgTypeEnum.getLiveUser,
+        data: {
+          live_room_id: data.live_room.id!,
+        },
+      });
+    });
+
+    // 其他用户加入房间
+    ws.socketIo.on(WsMsgTypeEnum.otherJoin, (data: WsOtherJoinType['data']) => {
+      prettierReceiveWsMsg(WsMsgTypeEnum.otherJoin, data);
+      // liveUserList.value.push({
+      //   id: data.join_socket_id,
+      //   userInfo: data.join_user_info,
+      // });
+      const requestId = getRandomString(8);
+      const danmu: IDanmu = {
+        request_id: requestId,
+        msgType: DanmuMsgTypeEnum.otherJoin,
+        socket_id: data.join_socket_id,
+        userInfo: data.join_user_info,
+        msgIsFile: false,
+        msg: '',
+        sendMsgTime: +new Date(),
+      };
+      damuList.value.push(danmu);
+      ws.send<WsGetLiveUserType['data']>({
+        requestId,
+        msgType: WsMsgTypeEnum.getLiveUser,
+        data: {
+          live_room_id: data.live_room.id!,
+        },
+      });
+      if (!isPull.value && !isSRS.value) {
+        if (!roomLiving.value) return;
+        // liveUserList.value.forEach(async (item) => {
+        //   const receiver = item.id;
+        //   if (
+        //     receiver === mySocketId.value ||
+        //     networkStore.getRtcMap(`${roomId.value}___${receiver!}`)
+        //   )
+        //     return;
+        //   console.warn(
+        //     '11开始new WebRTCClass',
+        //     `${roomId.value}___${receiver!}`
+        //   );
+        //   const rtc = new WebRTCClass({
+        //     maxBitrate: currentMaxBitrate.value,
+        //     maxFramerate: currentMaxFramerate.value,
+        //     resolutionRatio: currentResolutionRatio.value,
+        //     roomId: `${roomId.value}___${receiver!}`,
+        //     videoEl: createVideo({}),
+        //     isSRS: false,
+        //     receiver,
+        //   });
+        //   networkStore.updateRtcMap(`${roomId.value}___${receiver!}`, rtc);
+        //   canvasVideoStream.value?.getTracks().forEach((track) => {
+        //     if (rtc && canvasVideoStream.value) {
+        //       console.log('22canvasVideoStream插入track', track.kind, track);
+        //       rtc.peerConnection?.addTrack(track, canvasVideoStream.value);
+        //     }
+        //   });
+        //   const ws = networkStore.wsMap.get(roomId.value)!;
+        //   const offer = await rtc.createOffer();
+        //   await rtc.setLocalDescription(offer!);
+        //   ws.send<WsOfferType['data']>({
+        //     msgType: WsMsgTypeEnum.offer,
+        //     data: {
+        //       sdp: offer,
+        //       live_room_id: Number(roomId.value),
+        //       sender: mySocketId.value,
+        //       receiver,
+        //     },
+        //   });
+        // });
+      }
+    });
+
+    // 用户离开房间
+    ws.socketIo.on(WsMsgTypeEnum.leave, (data) => {
+      prettierReceiveWsMsg(WsMsgTypeEnum.leave, data);
+    });
+
+    // 用户离开房间完成
+    ws.socketIo.on(WsMsgTypeEnum.leaved, (data: WsLeavedType['data']) => {
+      prettierReceiveWsMsg(WsMsgTypeEnum.leaved, data);
+      if (anchorSocketId.value === data.socket_id) {
+        roomLiving.value = false;
+      }
+      networkStore.rtcMap
+        .get(`${roomId.value}___${data.socket_id as string}`)
+        ?.close();
+      networkStore.removeRtc(`${roomId.value}___${data.socket_id as string}`);
+      // const res = liveUserList.value.filter(
+      //   (item) => item.id !== data.socket_id
+      // );
+      // liveUserList.value = res;
+      damuList.value.push({
+        socket_id: data.socket_id,
+        msgType: DanmuMsgTypeEnum.userLeaved,
+        msgIsFile: false,
+        userInfo: data.user_info,
+        msg: '',
+        sendMsgTime: +new Date(),
+      });
+    });
+  }
+
+  function initSrsWs(data: {
+    isAnchor: boolean;
+    roomId: string;
+    currentResolutionRatio?: number;
+    currentMaxFramerate?: number;
+    currentMaxBitrate?: number;
+  }) {
+    roomId.value = data.roomId;
+    isAnchor.value = data.isAnchor;
+    if (data.currentMaxBitrate) {
+      currentMaxBitrate.value = data.currentMaxBitrate;
+    }
+    if (data.currentMaxFramerate) {
+      currentMaxFramerate.value = data.currentMaxFramerate;
+    }
+    if (data.currentResolutionRatio) {
+      currentResolutionRatio.value = data.currentResolutionRatio;
+    }
+    new WebSocketClass({
+      roomId: roomId.value,
+      url: WEBSOCKET_URL,
+      isAnchor: data.isAnchor,
+    });
+    initReceive();
+  }
+
+  return {
+    isPull,
+    initSrsWs,
+    handleStartLive,
+    mySocketId,
+    canvasVideoStream,
+    lastCoverImg,
+    roomLiving,
+    anchorInfo,
+    liveUserList,
+    damuList,
+    currentMaxFramerate,
+    currentMaxBitrate,
+    currentResolutionRatio,
+  };
+};

+ 12 - 2
src/hooks/use-srs-ws.ts

@@ -83,7 +83,7 @@ export const useSrsWs = () => {
   }
 
   async function handleSendOffer({ receiver }: { receiver: string }) {
-    console.log('开始handleSendOffer');
+    console.log('开始handleSendOffer', receiver);
     const ws = networkStore.wsMap.get(roomId.value);
     if (!ws) return;
     const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
@@ -157,6 +157,7 @@ export const useSrsWs = () => {
     if (type === LiveRoomTypeEnum.user_msr) {
       return;
     }
+    isSRS.value = true;
     if (type !== LiveRoomTypeEnum.user_wertc) {
       startNewWebRtc({
         videoEl: createVideo({}),
@@ -206,7 +207,7 @@ export const useSrsWs = () => {
       receiver,
       localStream: canvasVideoStream.value,
     });
-    isSRS.value = true;
+    // isSRS.value = true;
     handleSendOffer({
       receiver,
     });
@@ -309,6 +310,14 @@ export const useSrsWs = () => {
         if (data.anchor_socket_id) {
           anchorSocketId.value = data.anchor_socket_id;
         }
+        isSRS.value = true;
+        if (data.live_room.type === LiveRoomTypeEnum.user_wertc) {
+          isSRS.value = false;
+          startNewWebRtc({
+            videoEl: createVideo({}),
+            receiver: data.anchor_socket_id,
+          });
+        }
       }
     );
 
@@ -509,6 +518,7 @@ export const useSrsWs = () => {
         msgIsFile: false,
         userInfo: data.user_info,
         msg: '',
+        sendMsgTime: +new Date(),
       });
     });
   }

+ 4 - 0
src/interface-ws.ts

@@ -58,6 +58,10 @@ export enum WsMsgTypeEnum {
   answer = 'answer',
   candidate = 'candidate',
 
+  nativeWebRtcOffer = 'nativeWebRtcOffer',
+  nativeWebRtcAnswer = 'nativeWebRtcAnswer',
+  nativeWebRtcCandidate = 'nativeWebRtcCandidate',
+
   msrBlob = 'msrBlob',
 }
 

+ 2 - 2
src/views/pull/index.vue

@@ -145,7 +145,7 @@
         >
           <div
             class="info"
-            v-if="item.value.userInfo"
+            v-if="item.value?.userInfo"
             @click="jumpProfile(item.value.userInfo.id!)"
           >
             <div
@@ -164,7 +164,7 @@
           >
             <div class="avatar"></div>
             <div class="username">
-              {{ item.value.socketId }}
+              {{ item.value?.socketId }}
             </div>
           </div>
         </div>