shuisheng 2 vuotta sitten
vanhempi
sitoutus
98da3a23a4

+ 1 - 9
src/hooks/use-play.ts

@@ -124,21 +124,13 @@ export function useHlsPlay() {
   );
 
   function startHlsPlay(data: { hlsurl: string }) {
-    console.error('startHlsPlay');
     destroyHls();
     const videoEl = document.createElement('video');
     videoEl.autoplay = true;
-    videoEl.muted = true;
-    // videoEl.muted = appStore.muted;
+    videoEl.muted = appStore.muted;
     videoEl.playsInline = true;
     videoEl.setAttribute('webkit-playsinline', 'true');
     hlsVideoEl.value = videoEl;
-    videoEl.onplay = () => {
-      console.log('onplayonplay');
-    };
-    videoEl.onplaying = () => {
-      console.log('onplayingonplaying');
-    };
     document.body.appendChild(videoEl);
     return new Promise<{ width: number; height: number }>((resolve) => {
       hlsPlayer.value = videoJs(

+ 10 - 13
src/hooks/use-pull.ts

@@ -21,8 +21,7 @@ import {
   MediaTypeEnum,
   liveTypeEnum,
 } from '@/interface';
-import { SRSWebRTCClass } from '@/network/srsWebRtc';
-import { WebRTCClass } from '@/network/webRtc';
+import { WebRTCClass } from '@/network/webRTC';
 import {
   WebSocketClass,
   WsConnectStatusEnum,
@@ -209,7 +208,6 @@ export function usePull({
       console.warn('视频流-loadstart');
       const rtc = networkStore.getRtcMap(roomId.value);
       if (!rtc) return;
-      rtc.rtcStatus.loadstart = true;
       rtc.update();
     });
 
@@ -224,7 +222,6 @@ export function usePull({
       videoLoading.value = false;
       const rtc = networkStore.getRtcMap(roomId.value);
       if (!rtc) return;
-      rtc.rtcStatus.loadedmetadata = true;
       rtc.update();
     });
   }
@@ -298,11 +295,9 @@ export function usePull({
     console.log(instance, roomId.value);
     if (!instance) return;
     const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
-    console.log(rtc, `${roomId.value}___${receiver}`, 222);
-
     if (!rtc) return;
     const sdp = await rtc.createOffer();
-    await rtc.setLocalDescription(sdp);
+    await rtc.setLocalDescription(sdp!);
     const offerData = {
       sdp,
       sender,
@@ -372,11 +367,11 @@ export function usePull({
     if (isSRS) {
       if (!autoplayVal.value) return;
       console.warn('开始new SRSWebRTCClass', getSocketId());
-      const rtc = new SRSWebRTCClass({
+      const rtc = new WebRTCClass({
         roomId: `${roomId.value}___${getSocketId()}`,
         videoEl,
+        isSRS: true,
       });
-      rtc.rtcStatus.joined = true;
       rtc.update();
       if (track.video === 1) {
         rtc.peerConnection?.addTransceiver('video', { direction: 'recvonly' });
@@ -395,7 +390,9 @@ export function usePull({
           streamurl: streamurl.value,
           tid: getRandomString(10),
         });
-        await rtc.setRemoteDescription(res.data.sdp);
+        await rtc.setRemoteDescription(
+          new RTCSessionDescription({ type: 'answer', sdp: res.data.sdp })
+        );
       } catch (error) {
         console.log(error);
       }
@@ -405,6 +402,7 @@ export function usePull({
       const rtc = new WebRTCClass({
         roomId: `${roomId.value}___${receiver!}`,
         videoEl,
+        isSRS: false,
       });
       return rtc;
     }
@@ -560,7 +558,7 @@ export function usePull({
           sidebarList.value.push({ socketId: data.data.sender });
         }
         await nextTick(async () => {
-          console.log('收到offer,这个offer是发给我的');
+          console.log('收到offer,这个offer是发给我的', data);
           sender.value = data.data.sender;
           const rtc = await startNewWebRtc({
             receiver: data.data.sender,
@@ -571,7 +569,7 @@ export function usePull({
           if (rtc) {
             await rtc.setRemoteDescription(data.data.sdp);
             const sdp = await rtc.createAnswer();
-            await rtc.setLocalDescription(sdp);
+            await rtc.setLocalDescription(sdp!);
             const answerData: IAnswer = {
               sdp,
               sender: getSocketId(),
@@ -600,7 +598,6 @@ export function usePull({
       if (!instance) return;
       const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socket_id}`);
       if (!rtc) return;
-      rtc.rtcStatus.answer = true;
       rtc.update();
       if (data.data.receiver === getSocketId()) {
         console.log('收到answer,这个answer是发给我的');

+ 60 - 26
src/hooks/use-push.ts

@@ -30,8 +30,7 @@ import {
   LiveRoomTypeEnum,
   MediaTypeEnum,
 } from '@/interface';
-import { SRSWebRTCClass } from '@/network/srsWebRtc';
-import { WebRTCClass } from '@/network/webRtc';
+import { WebRTCClass } from '@/network/webRTC';
 import {
   WebSocketClass,
   WsConnectStatusEnum,
@@ -64,9 +63,9 @@ export function usePush({
   const isLiving = ref(false);
   const isDone = ref(false);
   const joined = ref(false);
-  const localStream = ref();
+  const localStream = ref<MediaStream>();
   const offerSended = ref(new Set());
-  const webRTC = ref<WebRTCClass | SRSWebRTCClass>();
+  const webRTC = ref<WebRTCClass>();
   const maxBitrate = ref([
     {
       label: '1000',
@@ -131,6 +130,26 @@ export function usePush({
   ]);
   const currentResolutionRatio = ref(resolutionRatio.value[1].value);
 
+  const maxFramerate = ref([
+    {
+      label: '10帧',
+      value: 10,
+    },
+    {
+      label: '24帧',
+      value: 24,
+    },
+    {
+      label: '30帧',
+      value: 30,
+    },
+    {
+      label: '60帧',
+      value: 60,
+    },
+  ]);
+  const currentMaxFramerate = ref(maxFramerate.value[1].value);
+
   const track = reactive({
     audio: 1,
     video: 1,
@@ -161,13 +180,27 @@ export function usePush({
     txt: string;
   }>();
 
+  watch(
+    () => currentMaxFramerate.value,
+    async (newVal) => {
+      if (!webRTC.value) {
+        return;
+      }
+      const res = await webRTC.value.setMaxFramerate(newVal);
+      if (res === 1) {
+        window.$message.success('切换帧率成功!');
+      } else {
+        window.$message.success('切换帧率失败!');
+      }
+    }
+  );
   watch(
     () => currentMaxBitrate.value,
     async (newVal) => {
       if (!webRTC.value) {
         return;
       }
-      const res = await webRTC.value?.setMaxBitrate(newVal);
+      const res = await webRTC.value.setMaxBitrate(newVal);
       if (res === 1) {
         window.$message.success('切换码率成功!');
       } else {
@@ -285,7 +318,7 @@ export function usePush({
   function endLive() {
     isLiving.value = false;
     currMediaTypeList.value = [];
-    localStream.value = null;
+    localStream.value = undefined;
     localVideoRef.value!.srcObject = null;
     clearInterval(heartbeatTimer.value);
     const instance = networkStore.wsMap.get(roomId.value);
@@ -311,18 +344,17 @@ export function usePush({
   }) {
     if (isSRS) {
       console.warn('开始new SRSWebRTCClass', `${roomId.value}___${receiver!}`);
-      const rtc = new SRSWebRTCClass({
-        roomId: `${roomId.value}___${getSocketId()}`,
-        videoEl,
+      const rtc = new WebRTCClass({
         maxBitrate: currentMaxBitrate.value,
+        maxFramerate: currentMaxFramerate.value,
         resolutionRatio: currentResolutionRatio.value,
+        roomId: `${roomId.value}___${getSocketId()}`,
+        videoEl,
+        isSRS: true,
       });
       webRTC.value = rtc;
-      localStream.value.getTracks().forEach((track) => {
-        rtc.addTrack({
-          track,
-          stream: localStream.value,
-        });
+      localStream.value?.getTracks().forEach((track) => {
+        rtc.addTrack(track, localStream.value);
       });
       try {
         const offer = await rtc.createOffer();
@@ -338,15 +370,21 @@ export function usePush({
           ),
           tid: getRandomString(10),
         });
-        await rtc.setRemoteDescription(res.data.sdp);
+        await rtc.setRemoteDescription(
+          new RTCSessionDescription({ type: 'answer', sdp: res.data.sdp })
+        );
       } catch (error) {
         console.log(error);
       }
     } else {
       console.warn('开始new WebRTCClass', `${roomId.value}___${receiver!}`);
       const rtc = new WebRTCClass({
+        maxBitrate: currentMaxBitrate.value,
+        maxFramerate: currentMaxFramerate.value,
+        resolutionRatio: currentResolutionRatio.value,
         roomId: `${roomId.value}___${receiver!}`,
         videoEl,
+        isSRS: false,
       });
       webRTC.value = rtc;
       return rtc;
@@ -388,9 +426,8 @@ export function usePush({
     if (!localStream.value) return;
     liveUserList.value.forEach((item) => {
       if (item.id !== getSocketId()) {
-        localStream.value.getTracks().forEach((track) => {
+        localStream.value?.getTracks().forEach((track) => {
           const rtc = networkStore.getRtcMap(`${roomId.value}___${item.id}`);
-          // rtc?.addTransceiver(track, localStream.value);
           rtc?.addTrack(track, localStream.value);
         });
       }
@@ -428,7 +465,7 @@ export function usePush({
     const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
     if (!rtc) return;
     const sdp = await rtc.createOffer();
-    await rtc.setLocalDescription(sdp);
+    await rtc.setLocalDescription(sdp!);
     instance.send({
       msgType: WsMsgTypeEnum.offer,
       data: {
@@ -506,7 +543,7 @@ export function usePush({
           if (rtc) {
             await rtc.setRemoteDescription(data.data.sdp);
             const sdp = await rtc.createAnswer();
-            await rtc.setLocalDescription(sdp);
+            await rtc.setLocalDescription(sdp!);
             const answerData: IAnswer = {
               sdp,
               sender: getSocketId(),
@@ -536,7 +573,6 @@ export function usePush({
       if (!instance) return;
       const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socket_id}`);
       if (!rtc) return;
-      rtc.rtcStatus.answer = true;
       rtc.update();
       if (data.data.receiver === getSocketId()) {
         console.log('收到answer,这个answer是发给我的');
@@ -683,9 +719,8 @@ export function usePush({
       const event = await navigator.mediaDevices.getUserMedia({
         video: {
           height: currentResolutionRatio.value,
-          // frameRate: 30,
+          frameRate: { ideal: currentMaxFramerate.value, max: 90 },
         },
-        // video: true,
         audio: true,
       });
       const audio = event.getAudioTracks();
@@ -708,9 +743,8 @@ export function usePush({
       const event = await navigator.mediaDevices.getDisplayMedia({
         video: {
           height: currentResolutionRatio.value,
-          // frameRate: 30,
+          frameRate: { ideal: currentMaxFramerate.value, max: 90 },
         },
-        // video: true,
         audio: true,
       });
       const audio = event.getAudioTracks();
@@ -787,7 +821,6 @@ export function usePush({
       console.warn('视频流-loadstart');
       const rtc = networkStore.getRtcMap(roomId.value);
       if (!rtc) return;
-      rtc.rtcStatus.loadstart = true;
       rtc.update();
     });
 
@@ -795,7 +828,6 @@ export function usePush({
       console.warn('视频流-loadedmetadata');
       const rtc = networkStore.getRtcMap(roomId.value);
       if (!rtc) return;
-      rtc.rtcStatus.loadedmetadata = true;
       rtc.update();
       if (isSRS) return;
       if (joined.value) {
@@ -817,8 +849,10 @@ export function usePush({
     keydownDanmu,
     currentResolutionRatio,
     currentMaxBitrate,
+    currentMaxFramerate,
     resolutionRatio,
     maxBitrate,
+    maxFramerate,
     danmuStr,
     roomName,
     damuList,

+ 0 - 506
src/network/srsWebRtc.ts

@@ -1,506 +0,0 @@
-import browserTool from 'browser-tool';
-
-import { useNetworkStore } from '@/store/network';
-
-export class SRSWebRTCClass {
-  roomId = '-1';
-  videoEl: HTMLVideoElement;
-  peerConnection: RTCPeerConnection | null = null;
-  dataChannel: RTCDataChannel | null = null;
-
-  getStatsSetIntervalDelay = 1000;
-  getStatsSetIntervalTimer;
-
-  // getStatsSetIntervalDelay是1秒的话,forceINumsMax是3,就代表一直卡了3秒。
-  forceINums = 0; // 发送forceI次数
-  forceINumsMax = 3; // 最多发送几次forceI
-
-  preFramesDecoded = -1; // 上一帧
-
-  maxBitrate: number; // 当前码率
-  resolutionRatio: number; // 当前分辨率
-
-  localStream?: MediaStream;
-
-  browser: {
-    device: string;
-    language: string;
-    engine: string;
-    browser: string;
-    system: string;
-    systemVersion: string;
-    platform: string;
-    isWebview: boolean;
-    isBot: boolean;
-    version: string;
-  };
-
-  rtcStatus = {
-    joined: false, // true代表成功,false代表失败
-    icecandidate: false, // true代表成功,false代表失败
-    createOffer: false, // true代表成功,false代表失败
-    setLocalDescription: false, // true代表成功,false代表失败
-    answer: false, // true代表成功,false代表失败
-    setRemoteDescription: false, // true代表成功,false代表失败
-    addStream: false, // true代表成功,false代表失败
-    loadstart: false, // true代表成功,false代表失败
-    loadedmetadata: false, // true代表成功,false代表失败
-  };
-
-  constructor({
-    roomId,
-    videoEl,
-    maxBitrate,
-    resolutionRatio,
-  }: {
-    roomId: string;
-    videoEl: HTMLVideoElement;
-    maxBitrate?: number;
-    resolutionRatio?: number;
-  }) {
-    this.roomId = roomId;
-    this.videoEl = videoEl;
-    this.maxBitrate = maxBitrate || 1000;
-    this.resolutionRatio = resolutionRatio || 1080;
-    this.browser = browserTool();
-    this.createPeerConnection();
-    this.update();
-  }
-
-  prettierLog = (msg: string, type?: 'log' | 'warn' | 'error', ...args) => {
-    console[type || 'log'](
-      `${new Date().toLocaleString()},${this.roomId},${
-        this.browser.browser
-      }浏览器,${msg}`,
-      ...args
-    );
-  };
-
-  addTrack = ({
-    track,
-    stream,
-  }: {
-    track: MediaStreamTrack;
-    stream: MediaStream;
-  }) => {
-    console.warn('开始addTrack', track, stream);
-    this.localStream = stream;
-    this.peerConnection?.addTrack(track, stream);
-    this.setMaxBitrate(this.maxBitrate);
-    this.setResolutionRatio(this.resolutionRatio);
-  };
-
-  setResolutionRatio = (height: number) => {
-    console.log('开始设置分辨率', height);
-    return new Promise((resolve) => {
-      this.localStream?.getTracks().forEach((track) => {
-        if (track.kind === 'video') {
-          track
-            .applyConstraints({
-              height,
-            })
-            .then(() => {
-              console.log('设置分辨率成功');
-              resolve(1);
-            })
-            .catch((error) => {
-              console.error('设置分辨率失败', error);
-              resolve(0);
-            });
-        }
-      });
-    });
-  };
-
-  setMaxBitrate = (maxBitrate: number) => {
-    console.log('开始设置码率', maxBitrate);
-    return new Promise<number>((resolve) => {
-      this.peerConnection?.getSenders().forEach((sender) => {
-        if (sender.track?.kind === 'video') {
-          const parameters = { ...sender.getParameters() };
-          if (parameters.encodings[0]) {
-            parameters.encodings[0].maxBitrate = 1000 * maxBitrate;
-            sender
-              .setParameters(parameters)
-              .then(() => {
-                console.log('设置码率成功');
-                resolve(1);
-              })
-              .catch((error) => {
-                console.error('设置码率失败', error);
-                resolve(0);
-              });
-          }
-        }
-      });
-    });
-  };
-
-  addStream = (stream: MediaProvider) => {
-    console.warn('开始addStream', stream);
-    if (!this.peerConnection) return;
-    this.rtcStatus.addStream = true;
-    this.update();
-    this.videoEl.srcObject = stream;
-  };
-
-  initStreamEvent = () => {
-    console.warn(`${this.roomId},开始监听pc的addstream`);
-    this.peerConnection?.addEventListener('addstream', (event: any) => {
-      console.warn(`${this.roomId},pc收到addstream事件`, event);
-      this.addStream(event.stream);
-    });
-
-    console.warn(`${this.roomId},开始监听pc的ontrack`);
-    this.peerConnection?.addEventListener('ontrack', (event: any) => {
-      console.warn(`${this.roomId},pc收到ontrack事件`, event);
-      this.addStream(event.streams[0]);
-    });
-
-    console.warn(`${this.roomId},开始监听pc的addtrack`);
-    this.peerConnection?.addEventListener('addtrack', (event: any) => {
-      console.warn(`${this.roomId},pc收到addtrack事件`, event);
-    });
-
-    console.warn(`${this.roomId},开始监听pc的track`);
-    this.peerConnection?.addEventListener('track', (event: any) => {
-      console.warn(`${this.roomId},pc收到track事件`, event);
-      this.addStream(event.streams[0]);
-    });
-  };
-
-  // 创建offer
-  createOffer = async () => {
-    if (!this.peerConnection) return;
-    this.prettierLog('开始createOffer', 'warn');
-    try {
-      const sdp = await this.peerConnection.createOffer();
-      this.rtcStatus.createOffer = true;
-      this.update();
-      this.prettierLog('createOffer成功', 'warn');
-      return sdp;
-    } catch (error) {
-      this.prettierLog('createOffer失败', 'error');
-      console.error(error);
-    }
-  };
-
-  // 设置本地描述
-  setLocalDescription = async (sdp: RTCLocalSessionDescriptionInit) => {
-    if (!this.peerConnection) return;
-    this.prettierLog('开始setLocalDescription', 'warn');
-    try {
-      await this.peerConnection.setLocalDescription(sdp);
-      this.rtcStatus.setLocalDescription = true;
-      this.update();
-      this.prettierLog('setLocalDescription成功', 'warn');
-    } catch (error) {
-      this.prettierLog('setLocalDescription失败', 'error');
-      console.error(error, sdp);
-    }
-  };
-
-  // 设置远端描述
-  setRemoteDescription = async (sdp: string) => {
-    if (!this.peerConnection) return;
-    this.prettierLog(`开始setRemoteDescription`, 'warn');
-    try {
-      await this.peerConnection.setRemoteDescription(
-        new RTCSessionDescription({ type: 'answer', sdp })
-      );
-      this.rtcStatus.setRemoteDescription = true;
-      this.update();
-      this.prettierLog('setRemoteDescription成功', 'warn');
-    } catch (error) {
-      this.prettierLog('setRemoteDescription失败', 'error');
-      console.error(error, sdp);
-    }
-  };
-
-  // 创建连接
-  startConnect = () => {
-    if (!this.peerConnection) return;
-    this.initStreamEvent();
-
-    // icecandidate
-    this.peerConnection.addEventListener('icecandidate', (event) => {
-      console.log('pc收到icecandidate', event);
-    });
-
-    // iceconnectionstatechange
-    this.peerConnection.addEventListener(
-      'iceconnectionstatechange',
-      (event: any) => {
-        // https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/connectionState
-        const iceConnectionState = event.currentTarget.iceConnectionState;
-        console.log(
-          this.roomId,
-          'pc收到iceconnectionstatechange',
-          // eslint-disable-next-line
-          `iceConnectionState:${iceConnectionState}`,
-          event
-        );
-        if (iceConnectionState === 'connected') {
-          // ICE 代理至少对每个候选发现了一个可用的连接,此时仍然会继续测试远程候选以便发现更优的连接。同时可能在继续收集候选。
-          console.warn(this.roomId, 'iceConnectionState:connected', event);
-        }
-        if (iceConnectionState === 'completed') {
-          // ICE 代理已经发现了可用的连接,不再测试远程候选。
-          console.warn(this.roomId, 'iceConnectionState:completed', event);
-        }
-        if (iceConnectionState === 'failed') {
-          // ICE 候选测试了所有远程候选没有发现匹配的候选。也可能有些候选中发现了一些可用连接。
-          console.error(this.roomId, 'iceConnectionState:failed', event);
-        }
-        if (iceConnectionState === 'disconnected') {
-          // 测试不再活跃,这可能是一个暂时的状态,可以自我恢复。
-          console.error(this.roomId, 'iceConnectionState:disconnected', event);
-        }
-        if (iceConnectionState === 'closed') {
-          // ICE 代理关闭,不再应答任何请求。
-          console.error(this.roomId, 'iceConnectionState:closed', event);
-        }
-      }
-    );
-
-    // connectionstatechange
-    this.peerConnection.addEventListener(
-      'connectionstatechange',
-      (event: any) => {
-        const connectionState = event.currentTarget.connectionState;
-        console.log(
-          this.roomId,
-          'pc收到connectionstatechange',
-          // eslint-disable-next-line
-          `connectionState:${connectionState}`,
-          event
-        );
-        if (connectionState === 'connected') {
-          // 表示每一个 ICE 连接要么正在使用(connected 或 completed 状态),要么已被关闭(closed 状态);并且,至少有一个连接处于 connected 或 completed 状态。
-          console.warn(this.roomId, 'connectionState:connected');
-        }
-        if (connectionState === 'disconnected') {
-          // 表示至少有一个 ICE 连接处于 disconnected 状态,并且没有连接处于 failed、connecting 或 checking 状态。
-          console.error(this.roomId, 'connectionState:disconnected');
-        }
-        if (connectionState === 'closed') {
-          // 表示 RTCPeerConnection 已关闭。
-          console.error(this.roomId, 'connectionState:closed');
-        }
-        if (connectionState === 'failed') {
-          // 表示至少有一个 ICE 连接处于 failed 的状态。
-          console.error(this.roomId, 'connectionState:failed');
-        }
-      }
-    );
-  };
-
-  // 创建对等连接
-  createPeerConnection = () => {
-    if (!window.RTCPeerConnection) {
-      console.error('当前环境不支持RTCPeerConnection!');
-      alert('当前环境不支持RTCPeerConnection!');
-      return;
-    }
-    if (!this.peerConnection) {
-      this.peerConnection = new RTCPeerConnection();
-      this.startConnect();
-      this.update();
-    }
-  };
-
-  handleWebRtcError = () => {
-    this.getStatsSetIntervalTimer = setInterval(() => {
-      this.peerConnection
-        ?.getStats()
-        .then((res) => {
-          let isBlack = false;
-          let currFramesDecoded = -1;
-          res.forEach((report: RTCInboundRtpStreamStats) => {
-            // 不能结构report的值,不然如果卡主之后,report的值就一直都是
-            // const { type, kind, framesDecoded } = report;
-            const data = {
-              type: report.type,
-              kind: report.kind,
-              framesDecoded: report.framesDecoded,
-              decoderImplementation: report.decoderImplementation,
-              isChrome: false,
-              isSafari: false,
-              other: '',
-            };
-            if (this.browser.browser === 'safari') {
-              data.isSafari = true;
-            } else if (this.browser.browser === 'chrome') {
-              data.isChrome = true;
-            } else {
-              data.other = this.browser.browser;
-            }
-            const isStopFlag = this.getCurrentFramesDecoded(data);
-            const isBlackFlag = this.isBlackScreen(data);
-            if (isStopFlag !== false) {
-              currFramesDecoded = isStopFlag;
-            }
-            if (isBlackFlag !== false) {
-              isBlack = isBlackFlag;
-            }
-          });
-          /** 处理视频流卡主 */
-          const handleStreamStop = () => {
-            // console.error(
-            //   `上一帧:${this.preFramesDecoded},当前帧:${currFramesDecoded},forceINums:${this.forceINums}`
-            // );
-            if (this.preFramesDecoded === currFramesDecoded) {
-              if (this.forceINums >= this.forceINumsMax) {
-                this.prettierLog('超过forceI次数,提示更换网络', 'warn');
-                this.forceINums = 0;
-              } else {
-                this.forceINums += 1;
-                this.prettierLog(
-                  `当前视频流卡主了,主动刷新云手机(${this.forceINums}/${this.forceINumsMax})`,
-                  'warn'
-                );
-              }
-            } else {
-              this.forceINums = 0;
-              // console.warn('视频流没有卡主');
-            }
-            this.preFramesDecoded = currFramesDecoded;
-          };
-          /** 处理黑屏 */
-          const handleBlackScreen = () => {
-            if (isBlack) {
-              this.prettierLog('黑屏了', 'error');
-            }
-            // else {
-            //   console.warn('没有黑屏');
-            // }
-          };
-          /** 处理rtcStatus */
-          const handleRtcStatus = () => {
-            const res = this.rtcStatusIsOk();
-            const length = Object.keys(res).length;
-            if (length) {
-              this.prettierLog(
-                `rtcStatus错误:${Object.keys(res).join()}`,
-                'error'
-              );
-            }
-            //  else {
-            //   console.warn('rtcStatus正常');
-            // }
-          };
-          handleStreamStop();
-          handleBlackScreen();
-          handleRtcStatus();
-          // if (!networkStore.errorCode.length) {
-          //   networkStore.setShowErrorModal(false);
-          // }
-        })
-        .catch((err) => {
-          console.error(new Date().toLocaleString(), 'getStatsErr', err);
-          // networkStore.setErrorCode(frontendErrorCode.getStatsErr);
-          // networkStore.setShowErrorModal(true);
-        });
-    }, this.getStatsSetIntervalDelay);
-  };
-
-  /** rtcStatus是否都是true了 */
-  rtcStatusIsOk = () => {
-    const res = {};
-    const status = this.rtcStatus;
-    Object.keys(status).forEach((key) => {
-      if (!status[key]) {
-        res[key] = false;
-      }
-    });
-    return res;
-  };
-
-  /** 当前是否黑屏,true代表黑屏了,false代表没有黑屏 */
-  isBlackScreen = ({
-    type,
-    kind,
-    framesDecoded,
-    decoderImplementation,
-    isSafari = false,
-    isChrome = false,
-  }) => {
-    // https://blog.csdn.net/weixin_44523653/article/details/127414387
-    // console.warn(
-    //   // eslint-disable-next-line
-    //   `type:${type},kind:${kind},framesDecoded:${framesDecoded},decoderImplementation:${decoderImplementation}`
-    // );
-    if (isSafari) {
-      if (
-        type === 'inbound-rtp' &&
-        kind === 'video' &&
-        // framesDecoded等于0代表黑屏
-        framesDecoded === 0
-      ) {
-        return true;
-      }
-    } else if (isChrome) {
-      if (
-        (type === 'track' || type === 'inbound-rtp') &&
-        kind === 'video' &&
-        // framesDecoded等于0代表黑屏
-        framesDecoded === 0
-      ) {
-        return true;
-      }
-    } else {
-      if (
-        (type === 'track' || type === 'inbound-rtp') &&
-        kind === 'video' &&
-        // framesDecoded等于0代表黑屏
-        framesDecoded === 0
-      ) {
-        // 安卓的qq浏览器适用这个黑屏判断
-        return true;
-      }
-    }
-
-    return false;
-  };
-
-  /** 获取当前帧 */
-  getCurrentFramesDecoded = ({
-    type,
-    kind,
-    framesDecoded = this.preFramesDecoded,
-    isSafari = false,
-    isChrome = false,
-  }) => {
-    if (isSafari) {
-      if (type === 'inbound-rtp' && kind === 'video') {
-        return framesDecoded;
-      }
-    } else if (isChrome) {
-      if ((type === 'track' || type === 'inbound-rtp') && kind === 'video') {
-        return framesDecoded;
-      }
-    } else {
-      if ((type === 'track' || type === 'inbound-rtp') && kind === 'video') {
-        // 安卓的qq浏览器适用这个卡屏判断
-        return framesDecoded;
-      }
-    }
-    return false;
-  };
-
-  // 手动关闭webrtc连接
-  close = () => {
-    console.warn(`${new Date().toLocaleString()},手动关闭webrtc连接`);
-    this.peerConnection?.close();
-    this.dataChannel?.close();
-    this.peerConnection = null;
-    this.dataChannel = null;
-    this.update();
-  };
-
-  // 更新store
-  update = () => {
-    const networkStore = useNetworkStore();
-    networkStore.updateRtcMap(this.roomId, this);
-  };
-}

+ 404 - 0
src/network/webRTC.ts

@@ -0,0 +1,404 @@
+import browserTool from 'browser-tool';
+
+import { ICandidate } from '@/interface';
+import { useNetworkStore } from '@/store/network';
+
+import { WsMsgTypeEnum } from './webSocket';
+
+export class WebRTCClass {
+  roomId = '-1';
+
+  videoEl: HTMLVideoElement;
+
+  peerConnection: RTCPeerConnection | null = null;
+
+  sender?: RTCRtpTransceiver;
+
+  /** 最大码率 */
+  maxBitrate = -1;
+  /** 最大帧率 */
+  maxFramerate = -1;
+  /** 分辨率 */
+  resolutionRatio = -1;
+
+  localStream?: MediaStream;
+
+  isSRS: boolean;
+
+  browser: {
+    device: string;
+    language: string;
+    engine: string;
+    browser: string;
+    system: string;
+    systemVersion: string;
+    platform: string;
+    isWebview: boolean;
+    isBot: boolean;
+    version: string;
+  };
+
+  constructor({
+    roomId,
+    videoEl,
+    maxBitrate,
+    maxFramerate,
+    resolutionRatio,
+    isSRS,
+  }: {
+    roomId: string;
+    videoEl: HTMLVideoElement;
+    maxBitrate?: number;
+    maxFramerate?: number;
+    resolutionRatio?: number;
+    isSRS: boolean;
+  }) {
+    this.roomId = roomId;
+    this.videoEl = videoEl;
+    if (maxBitrate) {
+      this.maxBitrate = maxBitrate;
+    }
+    if (resolutionRatio) {
+      this.resolutionRatio = resolutionRatio;
+    }
+    if (maxFramerate) {
+      this.maxFramerate = maxFramerate;
+    }
+    this.isSRS = isSRS;
+    this.browser = browserTool();
+    this.createPeerConnection();
+  }
+
+  prettierLog = (msg: string, type?: 'log' | 'warn' | 'error', ...args) => {
+    console[type || 'log'](
+      `${new Date().toLocaleString()},${this.roomId},${
+        this.browser.browser
+      }浏览器,${msg}`,
+      ...args
+    );
+  };
+
+  addTrack = (track, stream) => {
+    const sender = this.peerConnection
+      ?.getSenders()
+      .find((s) => s.track === track);
+    if (!sender) {
+      console.warn('开始addTrack', this.roomId, track, stream);
+      this.peerConnection?.addTrack(track, stream);
+      this.localStream = stream;
+      this.setMaxBitrate(this.maxBitrate);
+      this.setResolutionRatio(this.resolutionRatio);
+    } else {
+      console.warn('不addTrack了', this.roomId, track, stream);
+    }
+  };
+
+  /** 设置分辨率 */
+  setResolutionRatio = (height: number) => {
+    console.log('开始设置分辨率', height);
+    return new Promise((resolve) => {
+      this.localStream?.getTracks().forEach((track) => {
+        if (track.kind === 'video') {
+          track
+            .applyConstraints({
+              height,
+            })
+            .then(() => {
+              console.log('设置分辨率成功');
+              this.resolutionRatio = height;
+              resolve(1);
+            })
+            .catch((error) => {
+              console.error('设置分辨率失败', error);
+              resolve(0);
+            });
+        }
+      });
+    });
+  };
+
+  /** 设置最大码率 */
+  setMaxBitrate = (maxBitrate: number) => {
+    console.log('开始设置最大码率', maxBitrate);
+    return new Promise<number>((resolve) => {
+      this.peerConnection?.getSenders().forEach((sender) => {
+        if (sender.track?.kind === 'video') {
+          const parameters = { ...sender.getParameters() };
+          if (parameters.encodings[0]) {
+            const val = 1000 * maxBitrate;
+            parameters.encodings[0].maxBitrate = val;
+            sender
+              .setParameters(parameters)
+              .then(() => {
+                console.log('设置最大码率成功');
+                this.maxBitrate = val;
+                resolve(1);
+              })
+              .catch((error) => {
+                console.error('设置最大码率失败', error);
+                resolve(0);
+              });
+          }
+        }
+      });
+    });
+  };
+
+  /** 设置最大帧率 */
+  setMaxFramerate = (maxFramerate: number) => {
+    console.log('开始设置最大帧率', maxFramerate);
+    return new Promise<number>((resolve) => {
+      this.peerConnection?.getSenders().forEach((sender) => {
+        if (sender.track?.kind === 'video') {
+          const parameters = { ...sender.getParameters() };
+          if (parameters.encodings[0]) {
+            parameters.encodings[0].maxFramerate = maxFramerate;
+            sender
+              .setParameters(parameters)
+              .then(() => {
+                console.log('设置最大帧率成功');
+                this.maxFramerate = maxFramerate;
+                resolve(1);
+              })
+              .catch((error) => {
+                console.error('设置最大帧率失败', error);
+                resolve(0);
+              });
+          }
+        }
+      });
+    });
+  };
+
+  // 创建offer
+  createOffer = async () => {
+    if (!this.peerConnection) return;
+    this.prettierLog('createOffer开始', 'warn');
+    try {
+      const description = await this.peerConnection.createOffer({
+        iceRestart: true,
+      });
+      this.prettierLog('createOffer成功', 'warn');
+      return description;
+    } catch (error) {
+      this.prettierLog('createOffer失败', 'error');
+      console.log(error);
+    }
+  };
+
+  // 创建answer
+  createAnswer = async () => {
+    if (!this.peerConnection) return;
+    this.prettierLog('createAnswer开始', 'warn');
+    try {
+      const description = await this.peerConnection.createAnswer();
+      this.prettierLog('createAnswer成功', 'warn');
+      return description;
+    } catch (error) {
+      this.prettierLog('createAnswer失败', 'error');
+      console.log(error);
+    }
+  };
+
+  // 设置本地描述
+  setLocalDescription = async (desc: RTCLocalSessionDescriptionInit) => {
+    if (!this.peerConnection) return;
+    this.prettierLog('setLocalDescription开始', 'warn');
+    try {
+      await this.peerConnection.setLocalDescription(desc);
+      this.prettierLog('setLocalDescription成功', 'warn');
+    } catch (error) {
+      this.prettierLog('setLocalDescription失败', 'error');
+      console.error('setLocalDescription', desc);
+      console.error(error);
+    }
+  };
+
+  // 设置远端描述
+  setRemoteDescription = async (desc: RTCSessionDescriptionInit) => {
+    if (!this.peerConnection) return;
+    this.prettierLog(`setRemoteDescription开始`, 'warn');
+    try {
+      await this.peerConnection.setRemoteDescription(desc);
+      this.prettierLog('setRemoteDescription成功', 'warn');
+    } catch (error) {
+      this.prettierLog('setRemoteDescription失败', 'error');
+      console.error('setRemoteDescription', desc);
+      console.error(error);
+    }
+  };
+
+  addStream = (stream) => {
+    if (!this.peerConnection) return;
+    console.log('开始addStream', this.videoEl, stream, this.roomId);
+    this.videoEl.srcObject = stream;
+    this.prettierLog('addStream成功', 'warn');
+  };
+
+  handleStreamEvent = () => {
+    if (!this.peerConnection) return;
+    console.warn(`${this.roomId},开始监听pc的addstream`);
+    this.peerConnection.addEventListener('addstream', (event: any) => {
+      console.warn(`${this.roomId},pc收到addstream事件`, event, event.stream);
+      this.addStream(event.stream);
+    });
+
+    console.warn(`${this.roomId},开始监听pc的ontrack`);
+    this.peerConnection.addEventListener('ontrack', (event: any) => {
+      console.warn(`${this.roomId},pc收到ontrack事件`, event);
+      this.addStream(event.streams[0]);
+    });
+
+    console.warn(`${this.roomId},开始监听pc的addtrack`);
+    this.peerConnection.addEventListener('addtrack', (event: any) => {
+      console.warn(`${this.roomId},pc收到addtrack事件`, event);
+    });
+
+    console.warn(`${this.roomId},开始监听pc的track`);
+    this.peerConnection.addEventListener('track', (event: any) => {
+      console.warn(`${this.roomId},pc收到track事件`, event);
+      this.addStream(event.streams[0]);
+    });
+  };
+
+  handleConnectionEvent = () => {
+    if (!this.peerConnection) return;
+    console.warn(`${this.roomId},开始监听pc的icecandidate`);
+    this.peerConnection.addEventListener('icecandidate', (event) => {
+      this.prettierLog('pc收到icecandidate', 'warn');
+      if (event.candidate) {
+        const networkStore = useNetworkStore();
+        console.log('准备发送candidate', event.candidate.candidate);
+        const roomId = this.roomId.split('___')[0];
+        const receiver = this.roomId.split('___')[1];
+        const data: ICandidate['data'] = {
+          candidate: event.candidate.candidate,
+          sdpMid: event.candidate.sdpMid,
+          sdpMLineIndex: event.candidate.sdpMLineIndex,
+          sender: networkStore.wsMap.get(roomId)?.socketIo?.id || '',
+          receiver,
+          live_room_id: Number(roomId),
+        };
+        networkStore.wsMap
+          .get(roomId)
+          ?.send({ msgType: WsMsgTypeEnum.candidate, data });
+      } else {
+        console.log('没有候选者了');
+      }
+    });
+
+    console.warn(`${this.roomId},开始监听pc的iceconnectionstatechange`);
+    // iceconnectionstatechange
+    this.peerConnection.addEventListener(
+      'iceconnectionstatechange',
+      (event: any) => {
+        // https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/connectionState
+        const iceConnectionState = event.currentTarget.iceConnectionState;
+        console.log(
+          this.roomId,
+          'pc收到iceconnectionstatechange',
+          // eslint-disable-next-line
+          `iceConnectionState:${iceConnectionState}`,
+          event
+        );
+        if (iceConnectionState === 'connected') {
+          // ICE 代理至少对每个候选发现了一个可用的连接,此时仍然会继续测试远程候选以便发现更优的连接。同时可能在继续收集候选。
+          console.warn(this.roomId, 'iceConnectionState:connected', event);
+        }
+        if (iceConnectionState === 'completed') {
+          // ICE 代理已经发现了可用的连接,不再测试远程候选。
+          console.warn(this.roomId, 'iceConnectionState:completed', event);
+        }
+        if (iceConnectionState === 'failed') {
+          // ICE 候选测试了所有远程候选没有发现匹配的候选。也可能有些候选中发现了一些可用连接。
+          console.error(this.roomId, 'iceConnectionState:failed', event);
+        }
+        if (iceConnectionState === 'disconnected') {
+          // 测试不再活跃,这可能是一个暂时的状态,可以自我恢复。
+          console.error(this.roomId, 'iceConnectionState:disconnected', event);
+        }
+        if (iceConnectionState === 'closed') {
+          // ICE 代理关闭,不再应答任何请求。
+          console.error(this.roomId, 'iceConnectionState:closed', event);
+        }
+      }
+    );
+
+    console.warn(`${this.roomId},开始监听pc的connectionstatechange`);
+    // connectionstatechange
+    this.peerConnection.addEventListener(
+      'connectionstatechange',
+      (event: any) => {
+        const connectionState = event.currentTarget.connectionState;
+        console.log(
+          this.roomId,
+          'pc收到connectionstatechange',
+          // eslint-disable-next-line
+          `connectionState:${connectionState}`,
+          event
+        );
+        if (connectionState === 'connected') {
+          // 表示每一个 ICE 连接要么正在使用(connected 或 completed 状态),要么已被关闭(closed 状态);并且,至少有一个连接处于 connected 或 completed 状态。
+          console.warn(this.roomId, 'connectionState:connected');
+        }
+        if (connectionState === 'disconnected') {
+          // 表示至少有一个 ICE 连接处于 disconnected 状态,并且没有连接处于 failed、connecting 或 checking 状态。
+          console.error(this.roomId, 'connectionState:disconnected');
+        }
+        if (connectionState === 'closed') {
+          // 表示 RTCPeerConnection 已关闭。
+          console.error(this.roomId, 'connectionState:closed');
+        }
+        if (connectionState === 'failed') {
+          // 表示至少有一个 ICE 连接处于 failed 的状态。
+          console.error(this.roomId, 'connectionState:failed');
+        }
+      }
+    );
+  };
+
+  // 创建对等连接
+  createPeerConnection = () => {
+    if (!window.RTCPeerConnection) {
+      console.error('当前环境不支持RTCPeerConnection!');
+      alert('当前环境不支持RTCPeerConnection!');
+      return;
+    }
+    if (!this.peerConnection) {
+      const iceServers = this.isSRS
+        ? []
+        : [
+            // {
+            //   urls: 'stun:stun.l.google.com:19302',
+            // },
+            {
+              urls: 'turn:hsslive.cn:3478',
+              username: 'hss',
+              credential: '123456',
+            },
+          ];
+      this.peerConnection = new RTCPeerConnection({
+        iceServers,
+      });
+      this.handleStreamEvent();
+      this.handleConnectionEvent();
+      this.update();
+    }
+  };
+
+  // 手动关闭webrtc连接
+  close = () => {
+    console.warn(`${new Date().toLocaleString()},手动关闭webrtc连接`);
+    if (this.sender?.sender) {
+      this.peerConnection?.removeTrack(this.sender?.sender);
+    }
+    this.peerConnection?.close();
+    this.peerConnection = null;
+  };
+
+  // 更新store
+  update = () => {
+    const networkStore = useNetworkStore();
+    networkStore.updateRtcMap(this.roomId, this);
+  };
+}

+ 97 - 329
src/network/webRtc.ts

@@ -5,70 +5,26 @@ import { useNetworkStore } from '@/store/network';
 
 import { WsMsgTypeEnum } from './webSocket';
 
-export const frontendErrorCode = {
-  rtcStatusErr: {
-    // rtcStatus没有100%
-    code: 1001,
-    msg: '连接错误,是否尝试刷新页面(不影响云手机内应用运行)',
-    refresh: true,
-  },
-  streamStop: {
-    // 视频流卡主
-    code: 1002,
-    msg: '网络似乎不稳定,建议更换网络或刷新页面(不影响云手机内应用运行)',
-    refresh: true,
-  },
-  blackScreen: {
-    // 视频流黑屏
-    code: 1003,
-    msg: '当前浏览器似乎不兼容,建议使用Google Chrome浏览器',
-    refresh: false,
-  },
-  connectionStateFailed: {
-    code: 1004,
-    msg: '连接错误,是否尝试刷新页面(不影响云手机内应用运行)',
-    refresh: true,
-  },
-  iceConnectionStateDisconnected: {
-    code: 1005,
-    msg: '连接错误,是否尝试刷新页面(不影响云手机内应用运行)',
-    refresh: true,
-  },
-  getStatsErr: {
-    code: 1006,
-    msg: '连接错误,是否尝试刷新页面(不影响云手机内应用运行)',
-    refresh: true,
-  },
-  connectLongTime: {
-    // 5秒内没有收到loadedmetadata回调
-    code: 1007,
-    msg: '等待太久,是否刷新页面重试?',
-    refresh: true,
-  },
-};
-
 export class WebRTCClass {
   roomId = '-1';
-  videoEl;
-  peerConnection: RTCPeerConnection | null = null;
-  dataChannel: RTCDataChannel | null = null;
 
-  sender?: RTCRtpTransceiver;
+  videoEl: HTMLVideoElement;
 
-  getStatsSetIntervalDelay = 1000;
-  getStatsSetIntervalTimer;
-
-  // getStatsSetIntervalDelay是1秒的话,forceINumsMax是3,就代表一直卡了3秒。
-  forceINums = 0; // 发送forceI次数
-  forceINumsMax = 3; // 最多发送几次forceI
+  peerConnection: RTCPeerConnection | null = null;
 
-  preFramesDecoded = -1; // 上一帧
+  sender?: RTCRtpTransceiver;
 
-  maxBitrate: number; // 当前码率
-  resolutionRatio: number; // 当前分辨率
+  /** 最大码率 */
+  maxBitrate = -1;
+  /** 最大帧率 */
+  maxFramerate = -1;
+  /** 分辨率 */
+  resolutionRatio = -1;
 
   localStream?: MediaStream;
 
+  isSRS: boolean;
+
   browser: {
     device: string;
     language: string;
@@ -82,39 +38,35 @@ export class WebRTCClass {
     version: string;
   };
 
-  rtcStatus = {
-    joined: false, // true代表成功,false代表失败
-    icecandidate: false, // true代表成功,false代表失败
-    createOffer: false, // true代表成功,false代表失败
-    setLocalDescription: false, // true代表成功,false代表失败
-    answer: false, // true代表成功,false代表失败
-    setRemoteDescription: false, // true代表成功,false代表失败
-    addStream: false, // true代表成功,false代表失败
-    loadstart: false, // true代表成功,false代表失败
-    loadedmetadata: false, // true代表成功,false代表失败
-  };
-
-  localDescription: any;
-
   constructor({
     roomId,
     videoEl,
     maxBitrate,
+    maxFramerate,
     resolutionRatio,
+    isSRS,
   }: {
     roomId: string;
     videoEl: HTMLVideoElement;
     maxBitrate?: number;
+    maxFramerate?: number;
     resolutionRatio?: number;
+    isSRS: boolean;
   }) {
     this.roomId = roomId;
     this.videoEl = videoEl;
-    this.maxBitrate = maxBitrate || 1000;
-    this.resolutionRatio = resolutionRatio || 1080;
+    if (maxBitrate) {
+      this.maxBitrate = maxBitrate;
+    }
+    if (resolutionRatio) {
+      this.resolutionRatio = resolutionRatio;
+    }
+    if (maxFramerate) {
+      this.maxFramerate = maxFramerate;
+    }
+    this.isSRS = isSRS;
     this.browser = browserTool();
     this.createPeerConnection();
-    this.update();
-    // this.handleWebRtcError();
   }
 
   prettierLog = (msg: string, type?: 'log' | 'warn' | 'error', ...args) => {
@@ -141,6 +93,7 @@ export class WebRTCClass {
     }
   };
 
+  /** 设置分辨率 */
   setResolutionRatio = (height: number) => {
     console.log('开始设置分辨率', height);
     return new Promise((resolve) => {
@@ -152,6 +105,7 @@ export class WebRTCClass {
             })
             .then(() => {
               console.log('设置分辨率成功');
+              this.resolutionRatio = height;
               resolve(1);
             })
             .catch((error) => {
@@ -163,22 +117,25 @@ export class WebRTCClass {
     });
   };
 
+  /** 设置最大码率 */
   setMaxBitrate = (maxBitrate: number) => {
-    console.log('开始设置码率', maxBitrate);
+    console.log('开始设置最大码率', maxBitrate);
     return new Promise<number>((resolve) => {
       this.peerConnection?.getSenders().forEach((sender) => {
         if (sender.track?.kind === 'video') {
           const parameters = { ...sender.getParameters() };
           if (parameters.encodings[0]) {
-            parameters.encodings[0].maxBitrate = 1000 * maxBitrate;
+            const val = 1000 * maxBitrate;
+            parameters.encodings[0].maxBitrate = val;
             sender
               .setParameters(parameters)
               .then(() => {
-                console.log('设置码率成功');
+                console.log('设置最大码率成功');
+                this.maxBitrate = val;
                 resolve(1);
               })
               .catch((error) => {
-                console.error('设置码率失败', error);
+                console.error('设置最大码率失败', error);
                 resolve(0);
               });
           }
@@ -187,183 +144,30 @@ export class WebRTCClass {
     });
   };
 
-  handleWebRtcError = () => {
-    this.getStatsSetIntervalTimer = setInterval(() => {
-      this.peerConnection
-        ?.getStats()
-        .then((res) => {
-          let isBlack = false;
-          let currFramesDecoded = -1;
-          res.forEach((report: RTCInboundRtpStreamStats) => {
-            // 不能结构report的值,不然如果卡主之后,report的值就一直都是
-            // const { type, kind, framesDecoded } = report;
-            const data = {
-              type: report.type,
-              kind: report.kind,
-              framesDecoded: report.framesDecoded,
-              decoderImplementation: report.decoderImplementation,
-              isChrome: false,
-              isSafari: false,
-              other: '',
-            };
-            if (this.browser.browser === 'safari') {
-              data.isSafari = true;
-            } else if (this.browser.browser === 'chrome') {
-              data.isChrome = true;
-            } else {
-              data.other = this.browser.browser;
-            }
-            const isStopFlag = this.getCurrentFramesDecoded(data);
-            const isBlackFlag = this.isBlackScreen(data);
-            if (isStopFlag !== false) {
-              currFramesDecoded = isStopFlag;
-            }
-            if (isBlackFlag !== false) {
-              isBlack = isBlackFlag;
-            }
-          });
-          /** 处理视频流卡主 */
-          const handleStreamStop = () => {
-            // console.error(
-            //   `上一帧:${this.preFramesDecoded},当前帧:${currFramesDecoded},forceINums:${this.forceINums}`
-            // );
-            if (this.preFramesDecoded === currFramesDecoded) {
-              if (this.forceINums >= this.forceINumsMax) {
-                this.prettierLog('超过forceI次数,提示更换网络', 'warn');
-                this.forceINums = 0;
-              } else {
-                this.forceINums += 1;
-                this.prettierLog(
-                  `当前视频流卡主了,主动刷新云手机(${this.forceINums}/${this.forceINumsMax})`,
-                  'warn'
-                );
-              }
-            } else {
-              this.forceINums = 0;
-              // console.warn('视频流没有卡主');
-            }
-            this.preFramesDecoded = currFramesDecoded;
-          };
-          /** 处理黑屏 */
-          const handleBlackScreen = () => {
-            if (isBlack) {
-              this.prettierLog('黑屏了', 'error');
-            }
-            // else {
-            //   console.warn('没有黑屏');
-            // }
-          };
-          /** 处理rtcStatus */
-          const handleRtcStatus = () => {
-            const res = this.rtcStatusIsOk();
-            const length = Object.keys(res).length;
-            if (length) {
-              this.prettierLog(
-                `rtcStatus错误:${Object.keys(res).join()}`,
-                'error'
-              );
-            }
-            //  else {
-            //   console.warn('rtcStatus正常');
-            // }
-          };
-          handleStreamStop();
-          handleBlackScreen();
-          handleRtcStatus();
-          // if (!networkStore.errorCode.length) {
-          //   networkStore.setShowErrorModal(false);
-          // }
-        })
-        .catch((err) => {
-          console.error(new Date().toLocaleString(), 'getStatsErr', err);
-          // networkStore.setErrorCode(frontendErrorCode.getStatsErr);
-          // networkStore.setShowErrorModal(true);
-        });
-    }, this.getStatsSetIntervalDelay);
-  };
-
-  /** rtcStatus是否都是true了 */
-  rtcStatusIsOk = () => {
-    const res = {};
-    const status = this.rtcStatus;
-    Object.keys(status).forEach((key) => {
-      if (!status[key]) {
-        res[key] = false;
-      }
+  /** 设置最大帧率 */
+  setMaxFramerate = (maxFramerate: number) => {
+    console.log('开始设置最大帧率', maxFramerate);
+    return new Promise<number>((resolve) => {
+      this.peerConnection?.getSenders().forEach((sender) => {
+        if (sender.track?.kind === 'video') {
+          const parameters = { ...sender.getParameters() };
+          if (parameters.encodings[0]) {
+            parameters.encodings[0].maxFramerate = maxFramerate;
+            sender
+              .setParameters(parameters)
+              .then(() => {
+                console.log('设置最大帧率成功');
+                this.maxFramerate = maxFramerate;
+                resolve(1);
+              })
+              .catch((error) => {
+                console.error('设置最大帧率失败', error);
+                resolve(0);
+              });
+          }
+        }
+      });
     });
-    return res;
-  };
-
-  /** 当前是否黑屏,true代表黑屏了,false代表没有黑屏 */
-  isBlackScreen = ({
-    type,
-    kind,
-    framesDecoded,
-    decoderImplementation,
-    isSafari = false,
-    isChrome = false,
-  }) => {
-    // https://blog.csdn.net/weixin_44523653/article/details/127414387
-    // console.warn(
-    //   // eslint-disable-next-line
-    //   `type:${type},kind:${kind},framesDecoded:${framesDecoded},decoderImplementation:${decoderImplementation}`
-    // );
-    if (isSafari) {
-      if (
-        type === 'inbound-rtp' &&
-        kind === 'video' &&
-        // framesDecoded等于0代表黑屏
-        framesDecoded === 0
-      ) {
-        return true;
-      }
-    } else if (isChrome) {
-      if (
-        (type === 'track' || type === 'inbound-rtp') &&
-        kind === 'video' &&
-        // framesDecoded等于0代表黑屏
-        framesDecoded === 0
-      ) {
-        return true;
-      }
-    } else {
-      if (
-        (type === 'track' || type === 'inbound-rtp') &&
-        kind === 'video' &&
-        // framesDecoded等于0代表黑屏
-        framesDecoded === 0
-      ) {
-        // 安卓的qq浏览器适用这个黑屏判断
-        return true;
-      }
-    }
-
-    return false;
-  };
-
-  /** 获取当前帧 */
-  getCurrentFramesDecoded = ({
-    type,
-    kind,
-    framesDecoded = this.preFramesDecoded,
-    isSafari = false,
-    isChrome = false,
-  }) => {
-    if (isSafari) {
-      if (type === 'inbound-rtp' && kind === 'video') {
-        return framesDecoded;
-      }
-    } else if (isChrome) {
-      if ((type === 'track' || type === 'inbound-rtp') && kind === 'video') {
-        return framesDecoded;
-      }
-    } else {
-      if ((type === 'track' || type === 'inbound-rtp') && kind === 'video') {
-        // 安卓的qq浏览器适用这个卡屏判断
-        return framesDecoded;
-      }
-    }
-    return false;
   };
 
   // 创建offer
@@ -374,16 +178,7 @@ export class WebRTCClass {
       const description = await this.peerConnection.createOffer({
         iceRestart: true,
       });
-      // const description = await this.peerConnection.createOffer({
-      //   iceRestart: true,
-      //   offerToReceiveAudio: true,
-      //   offerToReceiveVideo: true,
-      // });
-      this.localDescription = description;
-      this.rtcStatus.createOffer = true;
-      this.update();
       this.prettierLog('createOffer成功', 'warn');
-      console.log('createOffer', description);
       return description;
     } catch (error) {
       this.prettierLog('createOffer失败', 'error');
@@ -391,32 +186,13 @@ export class WebRTCClass {
     }
   };
 
-  // 设置本地描述
-  setLocalDescription = async (description) => {
-    if (!this.peerConnection) return;
-    this.prettierLog('setLocalDescription开始', 'warn');
-    try {
-      await this.peerConnection.setLocalDescription(description);
-      this.rtcStatus.setLocalDescription = true;
-      this.update();
-      this.prettierLog('setLocalDescription成功', 'warn');
-      console.log(description);
-    } catch (error) {
-      this.prettierLog('setLocalDescription失败', 'error');
-      console.log('setLocalDescription', description);
-      console.log(error);
-    }
-  };
-
   // 创建answer
   createAnswer = async () => {
     if (!this.peerConnection) return;
     this.prettierLog('createAnswer开始', 'warn');
     try {
       const description = await this.peerConnection.createAnswer();
-      this.update();
       this.prettierLog('createAnswer成功', 'warn');
-      console.log(description);
       return description;
     } catch (error) {
       this.prettierLog('createAnswer失败', 'error');
@@ -424,71 +200,74 @@ export class WebRTCClass {
     }
   };
 
+  // 设置本地描述
+  setLocalDescription = async (desc: RTCLocalSessionDescriptionInit) => {
+    if (!this.peerConnection) return;
+    this.prettierLog('setLocalDescription开始', 'warn');
+    try {
+      await this.peerConnection.setLocalDescription(desc);
+      this.prettierLog('setLocalDescription成功', 'warn');
+    } catch (error) {
+      this.prettierLog('setLocalDescription失败', 'error');
+      console.error('setLocalDescription', desc);
+      console.error(error);
+    }
+  };
+
   // 设置远端描述
-  setRemoteDescription = async (description) => {
+  setRemoteDescription = async (desc: RTCSessionDescriptionInit) => {
     if (!this.peerConnection) return;
     this.prettierLog(`setRemoteDescription开始`, 'warn');
     try {
-      await this.peerConnection.setRemoteDescription(
-        new RTCSessionDescription(description)
-      );
-      this.rtcStatus.setRemoteDescription = true;
-      this.update();
+      await this.peerConnection.setRemoteDescription(desc);
       this.prettierLog('setRemoteDescription成功', 'warn');
-      console.log(description);
     } catch (error) {
       this.prettierLog('setRemoteDescription失败', 'error');
-      console.log('setRemoteDescription', description);
-      console.log(error);
+      console.error('setRemoteDescription', desc);
+      console.error(error);
     }
   };
 
   addStream = (stream) => {
     if (!this.peerConnection) return;
-    this.rtcStatus.addStream = true;
-    this.update();
     console.log('开始addStream', this.videoEl, stream, this.roomId);
     this.videoEl.srcObject = stream;
     this.prettierLog('addStream成功', 'warn');
   };
 
-  handleStream = () => {
+  handleStreamEvent = () => {
+    if (!this.peerConnection) return;
     console.warn(`${this.roomId},开始监听pc的addstream`);
-    this.peerConnection?.addEventListener('addstream', (event: any) => {
+    this.peerConnection.addEventListener('addstream', (event: any) => {
       console.warn(`${this.roomId},pc收到addstream事件`, event, event.stream);
       this.addStream(event.stream);
     });
 
     console.warn(`${this.roomId},开始监听pc的ontrack`);
-    this.peerConnection?.addEventListener('ontrack', (event: any) => {
+    this.peerConnection.addEventListener('ontrack', (event: any) => {
       console.warn(`${this.roomId},pc收到ontrack事件`, event);
       this.addStream(event.streams[0]);
     });
 
     console.warn(`${this.roomId},开始监听pc的addtrack`);
-    this.peerConnection?.addEventListener('addtrack', (event: any) => {
+    this.peerConnection.addEventListener('addtrack', (event: any) => {
       console.warn(`${this.roomId},pc收到addtrack事件`, event);
     });
 
     console.warn(`${this.roomId},开始监听pc的track`);
-    this.peerConnection?.addEventListener('track', (event: any) => {
+    this.peerConnection.addEventListener('track', (event: any) => {
       console.warn(`${this.roomId},pc收到track事件`, event);
       this.addStream(event.streams[0]);
     });
   };
 
-  // 创建连接
-  startConnect = () => {
+  handleConnectionEvent = () => {
     if (!this.peerConnection) return;
-    this.handleStream();
     console.warn(`${this.roomId},开始监听pc的icecandidate`);
     this.peerConnection.addEventListener('icecandidate', (event) => {
       this.prettierLog('pc收到icecandidate', 'warn');
-      this.rtcStatus.icecandidate = true;
-      this.update();
       if (event.candidate) {
         const networkStore = useNetworkStore();
-        this.update();
         console.log('准备发送candidate', event.candidate.candidate);
         const roomId = this.roomId.split('___')[0];
         const receiver = this.roomId.split('___')[1];
@@ -586,31 +365,23 @@ export class WebRTCClass {
       return;
     }
     if (!this.peerConnection) {
+      const iceServers = this.isSRS
+        ? []
+        : [
+            // {
+            //   urls: 'stun:stun.l.google.com:19302',
+            // },
+            {
+              urls: 'turn:hsslive.cn:3478',
+              username: 'hss',
+              credential: '123456',
+            },
+          ];
       this.peerConnection = new RTCPeerConnection({
-        iceServers: [
-          // {
-          //   urls: 'stun:stun.l.google.com:19302',
-          // },
-          {
-            urls: 'turn:hsslive.cn:3478',
-            username: 'hss',
-            credential: '123456',
-          },
-        ],
+        iceServers,
       });
-      // this.dataChannel =
-      //   this.peerConnection.createDataChannel('MessageChannel');
-
-      // this.dataChannel.onopen = (event) => {
-      //   console.warn('dataChannel---onopen', event);
-      // };
-      // this.dataChannel.onerror = (event) => {
-      //   console.warn('dataChannel---onerror', event);
-      // };
-      // this.dataChannel.onmessage = (event) => {
-      //   console.log('dataChannel---onmessage', event);
-      // };
-      this.startConnect();
+      this.handleStreamEvent();
+      this.handleConnectionEvent();
       this.update();
     }
   };
@@ -622,10 +393,7 @@ export class WebRTCClass {
       this.peerConnection?.removeTrack(this.sender?.sender);
     }
     this.peerConnection?.close();
-    this.dataChannel?.close();
     this.peerConnection = null;
-    this.dataChannel = null;
-    this.update();
   };
 
   // 更新store

+ 1 - 1
src/store/network/index.ts

@@ -1,6 +1,6 @@
 import { defineStore } from 'pinia';
 
-import { WebRTCClass } from '@/network/webRtc';
+import { WebRTCClass } from '@/network/webRTC';
 import { WebSocketClass } from '@/network/webSocket';
 
 type NetworkRootState = {

+ 1 - 1
src/views/notFound.vue

@@ -23,7 +23,7 @@ import router from '@/router';
   font-size: 30px;
   transform: translate(-50%, -50%);
   .click {
-    color: skyblue;
+    color: $theme-color-gold;
     text-decoration: underline;
     cursor: pointer;
   }

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

@@ -190,12 +190,6 @@
             class="ipt"
             @keydown="keydownDanmu"
           ></textarea>
-          <div
-            class="btn"
-            @click="startTestPlay"
-          >
-            播放
-          </div>
           <div
             class="btn"
             @click="sendDanmu"
@@ -322,11 +316,6 @@ watch(
     }, 0);
   }
 );
-function startTestPlay() {
-  autoplayVal.value = true;
-  console.log('1111');
-  initPull(true);
-}
 
 onMounted(() => {
   getGoodsList();
@@ -347,9 +336,9 @@ onMounted(() => {
     containerRef.value.style.height = `${res}px`;
   }
   if (route.query.liveType === liveTypeEnum.srsHlsPull) {
-    initPull(false);
+    initPull();
   } else {
-    initPull(false);
+    initPull();
   }
 });
 </script>

+ 11 - 2
src/views/push/index.vue

@@ -85,12 +85,10 @@
                   size="small"
                   placeholder="输入房间名"
                   :style="{ width: '50%' }"
-                  :disabled="disabled"
                 />
                 <n-button
                   size="small"
                   type="primary"
-                  :disabled="disabled"
                   @click="confirmRoomName"
                 >
                   确定
@@ -112,6 +110,15 @@
               />
             </div>
           </div>
+          <div class="item">
+            <div class="txt">帧率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxFramerate"
+                :options="maxFramerate"
+              />
+            </div>
+          </div>
           <div class="item">
             <div class="txt">分辨率设置</div>
             <div class="down">
@@ -252,8 +259,10 @@ const {
   keydownDanmu,
   currentResolutionRatio,
   currentMaxBitrate,
+  currentMaxFramerate,
   resolutionRatio,
   maxBitrate,
+  maxFramerate,
   danmuStr,
   roomName,
   damuList,