shuisheng vor 1 Jahr
Ursprung
Commit
4d1bdcb894
6 geänderte Dateien mit 272 neuen und 36 gelöschten Zeilen
  1. 11 0
      doc/备忘.md
  2. 205 26
      src/hooks/use-websocket.ts
  3. 0 1
      src/network/webRTC.ts
  4. 37 0
      src/utils/index.ts
  5. 17 8
      src/views/pull/index.vue
  6. 2 1
      src/views/team/index.vue

+ 11 - 0
doc/备忘.md

@@ -1,3 +1,14 @@
+## 不同开播
+
+- srs直播
+  > todo
+- webrtc直播
+  > 主播给除自己以外的所有人发offer
+- webrtc会议
+  > 观众进入直播间,观众给除自己以外的所有人发offer
+- 打pk
+  > 主播1和主播2先webrtc视频通话,再由任意一方转推srs。
+
 ## 测试
 
 播放错误,重连测试:

+ 205 - 26
src/hooks/use-websocket.ts

@@ -77,6 +77,19 @@ export const useWebsocket = () => {
     clearInterval(loopGetLiveUserTimer.value);
   });
 
+  function createVideo2() {
+    const videoEl = document.createElement('video');
+    videoEl.autoplay = true;
+    videoEl.muted = true;
+    videoEl.playsInline = true;
+    videoEl.loop = true;
+    videoEl.setAttribute('webkit-playsinline', 'true');
+    videoEl.setAttribute('x5-video-player-type', 'h5');
+    videoEl.setAttribute('x5-video-player-fullscreen', 'true');
+    videoEl.setAttribute('x5-video-orientation', 'portraint');
+    return videoEl;
+  }
+
   watch(
     () => appStore.pkStream,
     (newval) => {
@@ -207,6 +220,150 @@ export const useWebsocket = () => {
   }
 
   const nativeWebRtc = {
+    newWebrtc: ({
+      isAnchor,
+      sender,
+      receiver,
+      videoEl,
+    }: {
+      isAnchor: boolean;
+      sender: string;
+      receiver: string;
+      videoEl: HTMLVideoElement;
+    }) => {
+      return new WebRTCClass({
+        maxBitrate: currentMaxBitrate.value,
+        maxFramerate: currentMaxFramerate.value,
+        resolutionRatio: currentResolutionRatio.value,
+        isSRS: false,
+        roomId: roomId.value,
+        isAnchor,
+        videoEl,
+        sender,
+        receiver,
+        localStream: canvasVideoStream.value,
+      });
+    },
+    /**
+     * 房主发offer给用户
+     */
+    sendOffer: async ({
+      sender,
+      receiver,
+    }: {
+      sender: string;
+      receiver: string;
+    }) => {
+      console.log('开始nativeWebRtc的sendOffer', { sender, receiver });
+      try {
+        const ws = networkStore.wsMap.get(roomId.value);
+        if (!ws) return;
+        if (networkStore.rtcMap.get(receiver)) {
+          return;
+        }
+        const rtc = nativeWebRtc.newWebrtc({
+          isAnchor: true,
+          sender,
+          receiver,
+          videoEl: createVideo({
+            appendChild: true,
+          }),
+        });
+        canvasVideoStream.value?.getTracks().forEach((track) => {
+          if (canvasVideoStream.value) {
+            console.log(
+              'nativeWebRtc的canvasVideoStream插入track',
+              track.kind,
+              track
+            );
+            rtc.peerConnection?.addTrack(track, canvasVideoStream.value);
+          }
+        });
+        const offerSdp = await rtc.createOffer();
+        if (!offerSdp) {
+          console.error('nativeWebRtc的offerSdp为空');
+          return;
+        }
+        await rtc.setLocalDescription(offerSdp!);
+        networkStore.wsMap.get(roomId.value)?.send<WsOfferType['data']>({
+          requestId: getRandomString(8),
+          msgType: WsMsgTypeEnum.nativeWebRtcOffer,
+          data: {
+            live_room: appStore.liveRoomInfo!,
+            live_room_id: appStore.liveRoomInfo!.id!,
+            sender,
+            receiver,
+            sdp: offerSdp,
+          },
+        });
+      } catch (error) {
+        console.error('nativeWebRtc的sendOffer错误');
+      }
+    },
+    /**
+     * 原生webrtc视频通话
+     * 用户收到房主的offer,用户回复房主answer
+     */
+    sendAnswer: async ({
+      isPk,
+      sdp,
+      sender,
+      receiver,
+    }: {
+      isPk: boolean;
+      sdp: RTCSessionDescriptionInit;
+      sender: string;
+      receiver: string;
+    }) => {
+      console.log('开始nativeWebRtc的sendAnswer', { sender, receiver, sdp });
+      try {
+        const ws = networkStore.wsMap.get(roomId.value);
+        if (!ws) return;
+        const rtc = networkStore.rtcMap.get(receiver);
+        if (rtc) {
+          await rtc.setRemoteDescription(sdp);
+          if (isPk) {
+            if (!isAnchor.value) {
+              const stream = await handleUserMedia({
+                video: true,
+                audio: true,
+              });
+              if (rtc?.peerConnection) {
+                rtc.peerConnection.onnegotiationneeded = (event) => {
+                  console.log('onnegotiationneeded', event);
+                };
+                stream?.getTracks().forEach((track) => {
+                  rtc.peerConnection?.addTrack(track, stream);
+                });
+              }
+            }
+          }
+          const answerSdp = await rtc.createAnswer();
+          if (!answerSdp) {
+            console.error('nativeWebRtc的answerSdp为空');
+            return;
+          }
+          await rtc.setLocalDescription(answerSdp);
+          networkStore.wsMap.get(roomId.value)?.send<WsAnswerType['data']>({
+            requestId: getRandomString(8),
+            msgType: WsMsgTypeEnum.nativeWebRtcAnswer,
+            data: {
+              live_room_id: Number(roomId.value),
+              sender,
+              receiver,
+              sdp: answerSdp,
+            },
+          });
+        } else {
+          console.error('rtc不存在');
+        }
+      } catch (error) {
+        console.error('nativeWebRtc的sendAnswer错误');
+      }
+    },
+  };
+
+  const nativeWebRtcMeeting = {
     newWebrtc: ({
       isAnchor,
       sender,
@@ -495,7 +652,7 @@ export const useWebsocket = () => {
         console.log('收到srsOffer', data);
         if (data.receiver === mySocketId.value) {
           console.warn('是发给我的srsOffer');
-          const videoEl = createVideo({ appendChild: true });
+          const videoEl = createVideo2();
           const rtc = new WebRTCClass({
             isAnchor: true,
             maxBitrate: currentMaxBitrate.value,
@@ -614,9 +771,7 @@ export const useWebsocket = () => {
                 // 但是这里的nativeWebRtc的sender,得是自己,不能是data.data.sender,不要混淆
                 sender: mySocketId.value,
                 receiver: data.sender,
-                videoEl: createVideo({
-                  appendChild: true,
-                }),
+                videoEl: createVideo2(),
               });
               nativeWebRtc.sendAnswer({
                 isPk: true,
@@ -638,9 +793,7 @@ export const useWebsocket = () => {
               // 但是这里的nativeWebRtc的sender,得是自己,不能是data.data.sender,不要混淆
               sender: mySocketId.value,
               receiver: data.sender,
-              videoEl: createVideo({
-                appendChild: true,
-              }),
+              videoEl: createVideo2(),
             });
             await nativeWebRtc.sendAnswer({
               isPk: false,
@@ -817,30 +970,56 @@ export const useWebsocket = () => {
         send_msg_time: +new Date(),
       };
       damuList.value.push(danmu);
-      if (data.live_room.type === LiveRoomTypeEnum.user_wertc) {
-        isSRS.value = false;
-        nativeWebRtc.sendOffer({
-          sender: mySocketId.value,
-          receiver: data.join_socket_id,
-        });
-      } else {
-        data.socket_list.forEach((item) => {
-          if (item !== mySocketId.value) {
-            if (
-              [
-                LiveRoomTypeEnum.user_wertc,
-                LiveRoomTypeEnum.user_wertc_meeting,
-                LiveRoomTypeEnum.user_pk,
-              ].includes(data.live_room.type!)
-            ) {
-              isSRS.value = false;
+      if (route.name === routerName.push) {
+        // 当前是推流页面
+        if (!isAnchor.value) {
+          console.error('不是主播');
+          return;
+        }
+        if (!roomLiving.value) {
+          console.error('主播没点开始直播');
+          return;
+        }
+        if (
+          [
+            LiveRoomTypeEnum.system,
+            LiveRoomTypeEnum.user_srs,
+            LiveRoomTypeEnum.user_obs,
+          ].includes(data.live_room.type!)
+        ) {
+          return;
+        }
+        if (data.live_room.type === LiveRoomTypeEnum.user_wertc) {
+          isSRS.value = false;
+          data.socket_list.forEach((item) => {
+            if (item !== mySocketId.value) {
               nativeWebRtc.sendOffer({
                 sender: mySocketId.value,
                 receiver: item,
               });
             }
-          }
-        });
+          });
+        } else {
+          data.socket_list.forEach((item) => {
+            if (item !== mySocketId.value) {
+              if (
+                [
+                  LiveRoomTypeEnum.user_wertc,
+                  LiveRoomTypeEnum.user_wertc_meeting,
+                  LiveRoomTypeEnum.user_pk,
+                ].includes(data.live_room.type!)
+              ) {
+                isSRS.value = false;
+                nativeWebRtc.sendOffer({
+                  sender: mySocketId.value,
+                  receiver: item,
+                });
+              }
+            }
+          });
+        }
+      } else {
+        //  当前不是推流页面
       }
     });
 

+ 0 - 1
src/network/webRTC.ts

@@ -236,7 +236,6 @@ export class WebRTCClass {
 
   /** 设置最大帧率 */
   setMaxFramerate = async (maxFramerate: number) => {
-    console.log(this.localStream, maxFramerate, '设置最大帧率111');
     if (this.localStream) {
       const res = await handleMaxFramerate({
         frameRate: maxFramerate,

+ 37 - 0
src/utils/index.ts

@@ -341,6 +341,43 @@ export const createVideo = ({
   return videoEl;
 };
 
+export function videoFullBox(data: {
+  wrapSize: { width: number; height: number };
+  videoEl: HTMLVideoElement;
+  videoResize?: (data: { w: number; h: number }) => void;
+}) {
+  const { videoEl } = data;
+  if (!videoEl) {
+    throw new Error('videoEl不能为空!');
+  }
+
+  let w = videoEl.videoWidth;
+  let h = videoEl.videoHeight;
+  function handleResize() {
+    w = videoEl.videoWidth;
+    h = videoEl.videoHeight;
+    data.videoResize?.({ w, h });
+    setVideoSize({ width: w, height: h });
+  }
+  function setVideoSize({ width, height }) {
+    const res = computeBox({
+      width,
+      height,
+      maxHeight: data.wrapSize.height,
+      minHeight: data.wrapSize.height,
+      maxWidth: data.wrapSize.width,
+      minWidth: data.wrapSize.width,
+    });
+    videoEl.style.width = `${res.width as number}px`;
+    videoEl.style.height = `${res.height as number}px`;
+    videoEl.setAttribute('resolution-width', width);
+    videoEl.setAttribute('resolution-height', height);
+  }
+  setVideoSize({ width: w, height: h });
+  data.videoResize?.({ w, h });
+  videoEl.addEventListener('resize', handleResize);
+}
+
 export function videoToCanvas(data: {
   wrapSize: { width: number; height: number };
   videoEl: HTMLVideoElement;

+ 17 - 8
src/views/pull/index.vue

@@ -399,7 +399,7 @@
 
 <script lang="ts" setup>
 import { getRandomString, openToTarget } from 'billd-utils';
-import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
+import { onMounted, onUnmounted, ref, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useRoute } from 'vue-router';
 
@@ -430,7 +430,7 @@ import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
 import { WsDisableSpeakingType, WsMsgTypeEnum } from '@/types/websocket';
-import { formatMoney, formatTimeHour } from '@/utils';
+import { formatMoney, formatTimeHour, videoFullBox } from '@/utils';
 import { NODE_ENV } from 'script/constant';
 
 import RechargeCpt from './recharge/index.vue';
@@ -610,14 +610,23 @@ async function handlePk() {
 }
 
 watch(
-  () => remoteVideo.value,
+  () => networkStore.rtcMap,
   (newVal) => {
     newVal.forEach((item) => {
-      nextTick(() => {
-        setTimeout(() => {
-          remoteVideoRef.value?.appendChild(item);
-        }, 500);
-      });
+      const rect = videoWrapRef.value?.getBoundingClientRect();
+      if (rect) {
+        videoFullBox({
+          wrapSize: {
+            width: rect.width,
+            height: rect.height,
+          },
+          videoEl: item.videoEl,
+          videoResize: ({ w, h }) => {
+            videoHeight.value = `${w}x${h}`;
+          },
+        });
+        remoteVideoRef.value?.appendChild(item.videoEl);
+      }
     });
   },
   {

+ 2 - 1
src/views/team/index.vue

@@ -110,7 +110,6 @@
 </template>
 
 <script lang="ts" setup>
-import { domain } from '@/spec-config';
 import {
   CodeOutline,
   GlobeOutline,
@@ -122,6 +121,8 @@ import {
 import { openToTarget } from 'billd-utils';
 import { ref } from 'vue';
 
+import { domain } from '@/spec-config';
+
 const list = ref([
   {
     avatar: 'https://www.github.com/galaxy-s10.png',