Selaa lähdekoodia

feat: 多平台转推

shuisheng 1 vuosi sitten
vanhempi
sitoutus
9d55be2cc2

+ 10 - 10
README.md

@@ -22,16 +22,16 @@ billd 直播间,目前实现了类似 [bilibili 的 Web 在线直播](https://
 
 ## 生态
 
-| 名称         | 仓库                                                                             | star & fork                                                                                                                                                                                                                                                                                                                         | 线上地址                                                                       |
-| ------------ | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
-| 直播间网页端 | [billd-live](https://github.com/galaxy-s10/billd-live)                           | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live)                                                     | [https://live.hsslive.cn](https://live.hsslive.cn)                             |
-| 远程桌面     | [billd-desk](https://github.com/galaxy-s10/billd-desk)                           | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-desk?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-desk) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-desk?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-desk)                                                     | [https://live.hsslive.cn/remoteDeskTop](https://live.hsslive.cn/remoteDeskTop) |
-| 直播间移动端 | [billd-live-react-native](https://github.com/galaxy-s10/billd-live-react-native) | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live-react-native?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live-react-native) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live-react-native?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live-react-native) |                                                                                |
-| 直播间客户端 | [billd-live-electron](https://github.com/galaxy-s10/billd-live-electron)         | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live-electron?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live-flutter) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live-electron?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live-electron)                  |                                                                                |
-| 直播间移动端 | [billd-live-flutter](https://github.com/galaxy-s10/billd-live-flutter)           | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live-flutter?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live-flutter) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live-flutter?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live-flutter)                     |                                                                                |
-| 直播间移动端 | [billd-live-kotlin](https://github.com/galaxy-s10/billd-live-kotlin)             | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live-kotlin?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live-kotlin) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live-kotlin?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live-kotlin)                         |                                                                                |
-| 直播间后台   | [billd-live-admin](https://github.com/galaxy-s10/billd-live-admin)               | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live-admin?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live-admin) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live-admin?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live-admin)                             | [https://live-admin.hsslive.cn](https://live-admin.hsslive.cn)                 |
-| 直播间后端   | [billd-live-server](https://github.com/galaxy-s10/billd-live-server)             | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live-server?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live-server) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live-server?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live-server)                         | [https://live-api.hsslive.cn](https://live-api.hsslive.cn)                     |
+| 名称         | 仓库                                                         | star & fork                                                  | 线上地址                                                     |
+| ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
+| 直播间前台   | [billd-live](https://github.com/galaxy-s10/billd-live)       | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live) | [https://live.hsslive.cn](https://live.hsslive.cn)           |
+| 直播间后端   | [billd-live-server](https://github.com/galaxy-s10/billd-live-server) | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live-server?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live-server) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live-server?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live-server) | [https://live-api.hsslive.cn](https://live-api.hsslive.cn)   |
+| 直播间后台   | [billd-live-admin](https://github.com/galaxy-s10/billd-live-admin) | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live-admin?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live-admin) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live-admin?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live-admin) | [https://live-admin.hsslive.cn](https://live-admin.hsslive.cn) |
+| 远程桌面     | [billd-desk](https://github.com/galaxy-s10/billd-desk)       | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-desk?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-desk) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-desk?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-desk) | [https://live.hsslive.cn/remoteDeskTop](https://live.hsslive.cn/remoteDeskTop) |
+| 直播间移动端 | [billd-live-react-native](https://github.com/galaxy-s10/billd-live-react-native) | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live-react-native?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live-react-native) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live-react-native?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live-react-native) | [https://live.hsslive.cn/download](https://live.hsslive.cn/download) |
+| 直播间客户端 | [billd-live-electron](https://github.com/galaxy-s10/billd-live-electron) | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live-electron?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live-flutter) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live-electron?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live-electron) | [https://live.hsslive.cn/download](https://live.hsslive.cn/download) |
+| 直播间移动端 | [billd-live-flutter](https://github.com/galaxy-s10/billd-live-flutter) | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live-flutter?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live-flutter) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live-flutter?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live-flutter) | [https://live.hsslive.cn/download](https://live.hsslive.cn/download) |
+| 直播间移动端 | [billd-live-kotlin](https://github.com/galaxy-s10/billd-live-kotlin) | [![github](https://img.shields.io/github/stars/galaxy-s10/billd-live-kotlin?label=star&logo=GitHub)](https://github.com/galaxy-s10/billd-live-kotlin) [![github](https://img.shields.io/github/forks/galaxy-s10/billd-live-kotlin?label=fork&logo=GitHub)](https://github.com/galaxy-s10/billd-live-kotlin) | [https://live.hsslive.cn/download](https://live.hsslive.cn/download) |
 
 ## 功能
 

+ 7 - 1
src/hooks/use-pull.ts

@@ -223,7 +223,7 @@ export function usePull(roomId: string) {
   }
 
   function handleFlvPlay() {
-    console.log('handleFlvPlay');
+    console.log('handleFlvPlay', flvurl.value);
     handleStopDrawing();
     videoLoading.value = true;
     appStore.setLiveLine(LiveLineEnum.flv);
@@ -252,6 +252,9 @@ export function usePull(roomId: string) {
         LiveRoomTypeEnum.obs,
         LiveRoomTypeEnum.msr,
         LiveRoomTypeEnum.pk,
+        LiveRoomTypeEnum.forward_bilibili,
+        LiveRoomTypeEnum.forward_huya,
+        LiveRoomTypeEnum.forward_all,
       ].includes(data.type!)
     ) {
       play();
@@ -279,6 +282,9 @@ export function usePull(roomId: string) {
             LiveRoomTypeEnum.obs,
             LiveRoomTypeEnum.tencent_css,
             LiveRoomTypeEnum.tencent_css_pk,
+            LiveRoomTypeEnum.forward_bilibili,
+            LiveRoomTypeEnum.forward_huya,
+            LiveRoomTypeEnum.forward_all,
           ].includes(liveRoomInfo.type!)
         ) {
           handlePlay(liveRoomInfo!);

+ 51 - 0
src/hooks/use-websocket.ts

@@ -51,6 +51,9 @@ import {
   prettierReceiveWsMsg,
 } from '@/utils/network/webSocket';
 
+import { useForwardAll } from './webrtc/forwardAll';
+import { useForwardBilibili } from './webrtc/forwardBilibili';
+import { useForwardDouyu } from './webrtc/forwardDouyu';
 import { useWebRtcRemoteDesk } from './webrtc/remoteDesk';
 
 export const useWebsocket = () => {
@@ -64,6 +67,9 @@ export const useWebsocket = () => {
     useWebRtcRemoteDesk();
   const { updateWebRtcMeetingPkConfig, webRtcMeetingPk } = useWebRtcMeetingPk();
   const { updateWebRtcSrsConfig, webRtcSrs } = useWebRtcSrs();
+  const { updateForwardBilibiliConfig, forwardBilibili } = useForwardBilibili();
+  const { updateForwardAllConfig, forwardAll } = useForwardAll();
+  const { updateForwardDouyuConfig, forwardDouyu } = useForwardDouyu();
   const { updateWebRtcTencentcloudCssConfig, webRtcTencentcloudCss } =
     useWebRtcTencentcloudCss();
   const { updateWebRtcLiveConfig, webRtcLive } = useWebRtcLive();
@@ -186,6 +192,51 @@ export const useWebsocket = () => {
         sender: mySocketId.value,
         receiver: 'srs',
       });
+    } else if (type === LiveRoomTypeEnum.forward_bilibili) {
+      updateForwardBilibiliConfig({
+        isPk: false,
+        roomId: roomId.value,
+        canvasVideoStream: canvasVideoStream.value,
+      });
+      forwardBilibili.newWebRtc({
+        sender: mySocketId.value,
+        receiver: 'srs',
+        videoEl: createNullVideo(),
+      });
+      forwardBilibili.sendOffer({
+        sender: mySocketId.value,
+        receiver: 'srs',
+      });
+    } else if (type === LiveRoomTypeEnum.forward_huya) {
+      updateForwardDouyuConfig({
+        isPk: false,
+        roomId: roomId.value,
+        canvasVideoStream: canvasVideoStream.value,
+      });
+      forwardDouyu.newWebRtc({
+        sender: mySocketId.value,
+        receiver: 'srs',
+        videoEl: createNullVideo(),
+      });
+      forwardDouyu.sendOffer({
+        sender: mySocketId.value,
+        receiver: 'srs',
+      });
+    } else if (type === LiveRoomTypeEnum.forward_all) {
+      updateForwardAllConfig({
+        isPk: false,
+        roomId: roomId.value,
+        canvasVideoStream: canvasVideoStream.value,
+      });
+      forwardAll.newWebRtc({
+        sender: mySocketId.value,
+        receiver: 'srs',
+        videoEl: createNullVideo(),
+      });
+      forwardAll.sendOffer({
+        sender: mySocketId.value,
+        receiver: 'srs',
+      });
     } else if (type === LiveRoomTypeEnum.tencent_css) {
       updateWebRtcTencentcloudCssConfig({
         isPk: false,

+ 121 - 0
src/hooks/webrtc/forwardAll.ts

@@ -0,0 +1,121 @@
+import { getRandomString } from 'billd-utils';
+import { ref } from 'vue';
+
+import { fetchRtcV1Publish } from '@/api/srs';
+import { SRS_CB_URL_PARAMS } from '@/constant';
+import { useRTCParams } from '@/hooks/use-rtcParams';
+import { useNetworkStore } from '@/store/network';
+import { useUserStore } from '@/store/user';
+import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
+import { WebRTCClass } from '@/utils/network/webRTC';
+
+export const useForwardAll = () => {
+  const userStore = useUserStore();
+  const networkStore = useNetworkStore();
+
+  const { maxBitrate, maxFramerate, resolutionRatio } = useRTCParams();
+  const currentMaxBitrate = ref(maxBitrate.value[3].value);
+  const currentMaxFramerate = ref(maxFramerate.value[2].value);
+  const currentResolutionRatio = ref(resolutionRatio.value[3].value);
+  const isPk = ref(false);
+  const roomId = ref('');
+  const canvasVideoStream = ref<MediaStream>();
+
+  function updateForwardAllConfig(data: { isPk; roomId; canvasVideoStream }) {
+    isPk.value = data.isPk;
+    roomId.value = data.roomId;
+    canvasVideoStream.value = data.canvasVideoStream;
+  }
+
+  const forwardAll = {
+    newWebRtc: (data: {
+      sender: string;
+      receiver: string;
+      videoEl: HTMLVideoElement;
+    }) => {
+      return new WebRTCClass({
+        maxBitrate: currentMaxBitrate.value,
+        maxFramerate: currentMaxFramerate.value,
+        resolutionRatio: currentResolutionRatio.value,
+        isSRS: true,
+        roomId: roomId.value,
+        videoEl: data.videoEl,
+        sender: data.sender,
+        receiver: data.receiver,
+      });
+    },
+    /**
+     * 主播发offer给观众
+     */
+    sendOffer: async ({
+      sender,
+      receiver,
+    }: {
+      sender: string;
+      receiver: string;
+    }) => {
+      console.log('开始ForwardAll的sendOffer', {
+        sender,
+        receiver,
+      });
+      try {
+        const ws = networkStore.wsMap.get(roomId.value);
+        if (!ws) return;
+        const rtc = networkStore.rtcMap.get(receiver);
+        if (rtc) {
+          canvasVideoStream.value?.getTracks().forEach((track) => {
+            if (canvasVideoStream.value) {
+              console.log(
+                'ForwardAll的canvasVideoStream插入track',
+                track.kind,
+                track
+              );
+              rtc.peerConnection?.addTrack(track, canvasVideoStream.value);
+            }
+          });
+          const offerSdp = await rtc.createOffer();
+          if (!offerSdp) {
+            console.error('ForwardAll的offerSdp为空');
+            window.$message.error('ForwardAll的offerSdp为空');
+            return;
+          }
+          await rtc.setLocalDescription(offerSdp!);
+          const liveRooms = userStore.userInfo?.live_rooms;
+          const myLiveRoom = liveRooms?.[0];
+          if (!myLiveRoom) {
+            window.$message.error('你没有开通直播间');
+            return;
+          }
+          const answerRes = await fetchRtcV1Publish({
+            api: `/rtc/v1/publish/`,
+            clientip: null,
+            sdp: offerSdp.sdp!,
+            streamurl: `${myLiveRoom.rtmp_url!}?${
+              SRS_CB_URL_PARAMS.publishKey
+            }=${myLiveRoom.key!}&${SRS_CB_URL_PARAMS.publishType}=${
+              isPk.value ? LiveRoomTypeEnum.pk : LiveRoomTypeEnum.forward_all
+            }`,
+            tid: getRandomString(10),
+          });
+          if (answerRes.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: answerRes.data.sdp,
+            })
+          );
+        } else {
+          console.error('rtc不存在');
+        }
+      } catch (error) {
+        console.error('ForwardAll的sendOffer错误');
+      }
+    },
+  };
+
+  return { updateForwardAllConfig, forwardAll };
+};

+ 127 - 0
src/hooks/webrtc/forwardBilibili.ts

@@ -0,0 +1,127 @@
+import { getRandomString } from 'billd-utils';
+import { ref } from 'vue';
+
+import { fetchRtcV1Publish } from '@/api/srs';
+import { SRS_CB_URL_PARAMS } from '@/constant';
+import { useRTCParams } from '@/hooks/use-rtcParams';
+import { useNetworkStore } from '@/store/network';
+import { useUserStore } from '@/store/user';
+import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
+import { WebRTCClass } from '@/utils/network/webRTC';
+
+export const useForwardBilibili = () => {
+  const userStore = useUserStore();
+  const networkStore = useNetworkStore();
+
+  const { maxBitrate, maxFramerate, resolutionRatio } = useRTCParams();
+  const currentMaxBitrate = ref(maxBitrate.value[3].value);
+  const currentMaxFramerate = ref(maxFramerate.value[2].value);
+  const currentResolutionRatio = ref(resolutionRatio.value[3].value);
+  const isPk = ref(false);
+  const roomId = ref('');
+  const canvasVideoStream = ref<MediaStream>();
+
+  function updateForwardBilibiliConfig(data: {
+    isPk;
+    roomId;
+    canvasVideoStream;
+  }) {
+    isPk.value = data.isPk;
+    roomId.value = data.roomId;
+    canvasVideoStream.value = data.canvasVideoStream;
+  }
+
+  const forwardBilibili = {
+    newWebRtc: (data: {
+      sender: string;
+      receiver: string;
+      videoEl: HTMLVideoElement;
+    }) => {
+      return new WebRTCClass({
+        maxBitrate: currentMaxBitrate.value,
+        maxFramerate: currentMaxFramerate.value,
+        resolutionRatio: currentResolutionRatio.value,
+        isSRS: true,
+        roomId: roomId.value,
+        videoEl: data.videoEl,
+        sender: data.sender,
+        receiver: data.receiver,
+      });
+    },
+    /**
+     * 主播发offer给观众
+     */
+    sendOffer: async ({
+      sender,
+      receiver,
+    }: {
+      sender: string;
+      receiver: string;
+    }) => {
+      console.log('开始ForwardBilibili的sendOffer', {
+        sender,
+        receiver,
+      });
+      try {
+        const ws = networkStore.wsMap.get(roomId.value);
+        if (!ws) return;
+        const rtc = networkStore.rtcMap.get(receiver);
+        if (rtc) {
+          canvasVideoStream.value?.getTracks().forEach((track) => {
+            if (canvasVideoStream.value) {
+              console.log(
+                'ForwardBilibili的canvasVideoStream插入track',
+                track.kind,
+                track
+              );
+              rtc.peerConnection?.addTrack(track, canvasVideoStream.value);
+            }
+          });
+          const offerSdp = await rtc.createOffer();
+          if (!offerSdp) {
+            console.error('ForwardBilibili的offerSdp为空');
+            window.$message.error('ForwardBilibili的offerSdp为空');
+            return;
+          }
+          await rtc.setLocalDescription(offerSdp!);
+          const liveRooms = userStore.userInfo?.live_rooms;
+          const myLiveRoom = liveRooms?.[0];
+          if (!myLiveRoom) {
+            window.$message.error('你没有开通直播间');
+            return;
+          }
+          const answerRes = await fetchRtcV1Publish({
+            api: `/rtc/v1/publish/`,
+            clientip: null,
+            sdp: offerSdp.sdp!,
+            streamurl: `${myLiveRoom.rtmp_url!}?${
+              SRS_CB_URL_PARAMS.publishKey
+            }=${myLiveRoom.key!}&${SRS_CB_URL_PARAMS.publishType}=${
+              isPk.value
+                ? LiveRoomTypeEnum.pk
+                : LiveRoomTypeEnum.forward_bilibili
+            }`,
+            tid: getRandomString(10),
+          });
+          if (answerRes.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: answerRes.data.sdp,
+            })
+          );
+        } else {
+          console.error('rtc不存在');
+        }
+      } catch (error) {
+        console.error('ForwardBilibili的sendOffer错误');
+      }
+    },
+  };
+
+  return { updateForwardBilibiliConfig, forwardBilibili };
+};

+ 121 - 0
src/hooks/webrtc/forwardDouyu.ts

@@ -0,0 +1,121 @@
+import { getRandomString } from 'billd-utils';
+import { ref } from 'vue';
+
+import { fetchRtcV1Publish } from '@/api/srs';
+import { SRS_CB_URL_PARAMS } from '@/constant';
+import { useRTCParams } from '@/hooks/use-rtcParams';
+import { useNetworkStore } from '@/store/network';
+import { useUserStore } from '@/store/user';
+import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
+import { WebRTCClass } from '@/utils/network/webRTC';
+
+export const useForwardDouyu = () => {
+  const userStore = useUserStore();
+  const networkStore = useNetworkStore();
+
+  const { maxBitrate, maxFramerate, resolutionRatio } = useRTCParams();
+  const currentMaxBitrate = ref(maxBitrate.value[3].value);
+  const currentMaxFramerate = ref(maxFramerate.value[2].value);
+  const currentResolutionRatio = ref(resolutionRatio.value[3].value);
+  const isPk = ref(false);
+  const roomId = ref('');
+  const canvasVideoStream = ref<MediaStream>();
+
+  function updateForwardDouyuConfig(data: { isPk; roomId; canvasVideoStream }) {
+    isPk.value = data.isPk;
+    roomId.value = data.roomId;
+    canvasVideoStream.value = data.canvasVideoStream;
+  }
+
+  const forwardDouyu = {
+    newWebRtc: (data: {
+      sender: string;
+      receiver: string;
+      videoEl: HTMLVideoElement;
+    }) => {
+      return new WebRTCClass({
+        maxBitrate: currentMaxBitrate.value,
+        maxFramerate: currentMaxFramerate.value,
+        resolutionRatio: currentResolutionRatio.value,
+        isSRS: true,
+        roomId: roomId.value,
+        videoEl: data.videoEl,
+        sender: data.sender,
+        receiver: data.receiver,
+      });
+    },
+    /**
+     * 主播发offer给观众
+     */
+    sendOffer: async ({
+      sender,
+      receiver,
+    }: {
+      sender: string;
+      receiver: string;
+    }) => {
+      console.log('开始ForwardDouyu的sendOffer', {
+        sender,
+        receiver,
+      });
+      try {
+        const ws = networkStore.wsMap.get(roomId.value);
+        if (!ws) return;
+        const rtc = networkStore.rtcMap.get(receiver);
+        if (rtc) {
+          canvasVideoStream.value?.getTracks().forEach((track) => {
+            if (canvasVideoStream.value) {
+              console.log(
+                'ForwardDouyu的canvasVideoStream插入track',
+                track.kind,
+                track
+              );
+              rtc.peerConnection?.addTrack(track, canvasVideoStream.value);
+            }
+          });
+          const offerSdp = await rtc.createOffer();
+          if (!offerSdp) {
+            console.error('ForwardDouyu的offerSdp为空');
+            window.$message.error('ForwardDouyu的offerSdp为空');
+            return;
+          }
+          await rtc.setLocalDescription(offerSdp!);
+          const liveRooms = userStore.userInfo?.live_rooms;
+          const myLiveRoom = liveRooms?.[0];
+          if (!myLiveRoom) {
+            window.$message.error('你没有开通直播间');
+            return;
+          }
+          const answerRes = await fetchRtcV1Publish({
+            api: `/rtc/v1/publish/`,
+            clientip: null,
+            sdp: offerSdp.sdp!,
+            streamurl: `${myLiveRoom.rtmp_url!}?${
+              SRS_CB_URL_PARAMS.publishKey
+            }=${myLiveRoom.key!}&${SRS_CB_URL_PARAMS.publishType}=${
+              isPk.value ? LiveRoomTypeEnum.pk : LiveRoomTypeEnum.forward_huya
+            }`,
+            tid: getRandomString(10),
+          });
+          if (answerRes.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: answerRes.data.sdp,
+            })
+          );
+        } else {
+          console.error('rtc不存在');
+        }
+      } catch (error) {
+        console.error('ForwardDouyu的sendOffer错误');
+      }
+    },
+  };
+
+  return { updateForwardDouyuConfig, forwardDouyu };
+};

+ 20 - 0
src/layout/pc/head/index.vue

@@ -259,6 +259,26 @@
               >
                 <div class="txt">{{ t('layout.srsLive') }}</div>
               </a>
+              <a
+                class="item"
+                @click.prevent="
+                  handleStartLive(LiveRoomTypeEnum.forward_bilibili)
+                "
+              >
+                <div class="txt">{{ t('layout.forwardBilibili') }}</div>
+              </a>
+              <a
+                class="item"
+                @click.prevent="handleStartLive(LiveRoomTypeEnum.forward_huya)"
+              >
+                <div class="txt">{{ t('layout.forwardHuya') }}</div>
+              </a>
+              <a
+                class="item"
+                @click.prevent="handleStartLive(LiveRoomTypeEnum.forward_all)"
+              >
+                <div class="txt">{{ t('layout.forwardAll') }}</div>
+              </a>
               <a
                 class="item"
                 @click.prevent="handleStartLive(LiveRoomTypeEnum.wertc_live)"

+ 4 - 0
src/locales/en/layout.ts

@@ -39,6 +39,10 @@ export default nameSpaceWrap('layout', {
   release: 'Version Release',
 
   srsLive: 'SRS Live',
+  forwardBilibili: 'forward Bilibili',
+  forwardHuya: 'forward Huya',
+  forwardDouyu: 'forward Douyu',
+  forwardAll: 'forward All',
   webrtcLive: 'WebRTC Live',
   webrtcMeeting: 'WebRTC Meeting',
   msrLive: 'Msr Live',

+ 4 - 0
src/locales/zh/layout.ts

@@ -39,6 +39,10 @@ export default nameSpaceWrap('layout', {
   release: '版本发布',
 
   srsLive: 'SRS直播(推荐)',
+  forwardBilibili: '转推b站(beta)',
+  forwardHuya: '转推虎牙(beta)',
+  forwardDouyu: '转推斗鱼(beta)',
+  forwardAll: '转推b站和斗鱼(beta)',
   webrtcLive: 'WebRTC直播(低延迟)',
   webrtcMeeting: 'WebRTC会议(低延迟)',
   msrLive: 'Msr直播(b站实现)',

+ 12 - 0
src/types/ILiveRoom.ts

@@ -31,6 +31,12 @@ export enum LiveRoomTypeEnum {
   tencent_css,
   /** 主播使用腾讯云css推流打pk */
   tencent_css_pk,
+  /** 转推b站 */
+  forward_bilibili,
+  /** 转推斗鱼 */
+  forward_huya,
+  /** 转推所有 */
+  forward_all,
 }
 
 /** 拉流是否需要鉴权 */
@@ -234,6 +240,12 @@ export interface ILiveRoom {
   push_obs_stream_key?: string;
   push_webrtc_url?: string;
   push_srt_url?: string;
+  forward_bilibili_url?: string;
+  forward_douyu_url?: string;
+  forward_huya_url?: string;
+  forward_douyin_url?: string;
+  forward_kuaishou_url?: string;
+  forward_xiaohongshu_url?: string;
   /** 直播间备注 */
   remark?: string;
 

+ 1 - 3
src/views/download/index.vue

@@ -127,9 +127,7 @@ import { onMounted, ref } from 'vue';
 
 import { fetchServerInfo } from '@/api/other';
 import { COMMON_URL } from '@/constant';
-import { BilldHtmlWebpackPluginLog, IServerInfo } from '@/interface';
-// @ts-ignore
-const billd: BilldHtmlWebpackPluginLog = process.env.BilldHtmlWebpackPlugin;
+import { IServerInfo } from '@/interface';
 
 const serverInfo = ref<IServerInfo>();
 const loading = ref(false);