Răsfoiți Sursa

fix: 优化

shuisheng 1 an în urmă
părinte
comite
ac03fa411b

+ 1 - 1
.vscode/settings.json

@@ -44,6 +44,6 @@
     "tsx",
     "vue"
   ],
-  "typescript.tsdk": "node_modules/typescript/lib",
+  // "typescript.tsdk": "node_modules/typescript/lib",
   "vue.codeActions.savingTimeLimit": 2000
 }

+ 3 - 2
package.json

@@ -39,7 +39,8 @@
   "dependencies": {
     "@vicons/ionicons5": "^0.12.0",
     "@vueuse/core": "^10.11.1",
-    "@webav/av-recorder": "^0.3.3",
+    "@webav/av-cliper": "^1.0.6",
+    "@webav/av-recorder": "^1.0.6",
     "axios": "^1.2.1",
     "billd-deploy": "^1.1.0",
     "billd-html-webpack-plugin": "^1.0.6",
@@ -138,4 +139,4 @@
     "webpackbar": "^5.0.2",
     "windicss-webpack-plugin": "^1.7.7"
   }
-}
+}

+ 29 - 45
pnpm-lock.yaml

@@ -14,9 +14,12 @@ importers:
       '@vueuse/core':
         specifier: ^10.11.1
         version: 10.11.1(vue@3.3.4)
+      '@webav/av-cliper':
+        specifier: ^1.0.6
+        version: 1.0.6
       '@webav/av-recorder':
-        specifier: ^0.3.3
-        version: 0.3.3
+        specifier: ^1.0.6
+        version: 1.0.6
       axios:
         specifier: ^1.2.1
         version: 1.3.4
@@ -1834,12 +1837,6 @@ packages:
   '@types/connect@3.4.35':
     resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
 
-  '@types/dom-mediacapture-transform@0.1.9':
-    resolution: {integrity: sha512-/K96dASG23bqF+VAftybbI5SUj9qSsdsSKZglm7Bq/sIaEve5z8I+GdClARcSQMAAVkH7bc83UI1jiH/qc5LMw==}
-
-  '@types/dom-webcodecs@0.1.11':
-    resolution: {integrity: sha512-yPEZ3z7EohrmOxbk/QTAa0yonMFkNkjnVXqbGb7D4rMr+F1dGQ8ZUFxXkyLLJuiICPejZ0AZE9Rrk9wUCczx4A==}
-
   '@types/eslint-scope@3.7.4':
     resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
 
@@ -1942,9 +1939,6 @@ packages:
   '@types/web-bluetooth@0.0.20':
     resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
 
-  '@types/wicg-file-system-access@2020.9.8':
-    resolution: {integrity: sha512-ggMz8nOygG7d/stpH40WVaNvBwuyYLnrg5Mbyf6bmsj/8+gb6Ei4ZZ9/4PNpcPNTT8th9Q8sM8wYmWGjMWLX/A==}
-
   '@types/ws@8.5.4':
     resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==}
 
@@ -2142,14 +2136,17 @@ packages:
   '@webassemblyjs/wast-printer@1.11.1':
     resolution: {integrity: sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==}
 
-  '@webav/av-cliper@0.3.3':
-    resolution: {integrity: sha512-VUIaxlfZHsJLc875eL+rGaWTavVLRqkgaVa6R8XIpdjmWJxnD7Le55mViL1SxRILKtsi0KnqWfWDL2JzNAcwOw==}
+  '@webav/av-cliper@1.0.6':
+    resolution: {integrity: sha512-uyG6FkD2XYfWT0J+lpVJiTZ7YBg4OzGzG97Fq9A97LKTvKoqP0vB8jyELV4edZOUzwKjh3HJiALFWNL9uV+IWQ==}
+
+  '@webav/av-recorder@1.0.6':
+    resolution: {integrity: sha512-I50ZGU9LSIRuW8xrF8Amqr/IjtPpkfexfjYG2M0CMt2aT6bFo9jhvc1jdwlRT7V7/1iuS52DH7rYDg11+myG7A==}
 
-  '@webav/av-recorder@0.3.3':
-    resolution: {integrity: sha512-nMA6MT8hqEYq9yjIQeTo+Ee5CNw/9zXzvbksFCsWH8+VPTKoKFlDIqbgSLMpYjOgkkT7a9RY5IpCxU9Aq7Wdkw==}
+  '@webav/internal-utils@1.0.6':
+    resolution: {integrity: sha512-rjuVwsxcaS9LGSBeBZqoFE8af7GKH9IPAKMsr2tsdd03B+M/wWrmRWH0ZtaHJChPRneOOZkDicHlMKijc2Cdqw==}
 
-  '@webav/mp4box.js@0.5.3-fenghen':
-    resolution: {integrity: sha512-jAN15I3Po1Z6Ns02iknb6KGbird9rd1h9TGldzbwsar+88ZlHd+oVjOQnFavBdNDc5vmULUnldcWGDdKc02npw==}
+  '@webav/mp4box.js@0.5.4-fenghen':
+    resolution: {integrity: sha512-1NDZyNcB4Eu52tWhrRPGRTXpXUzWzh1xJE6jA0owj/Tlh8d9Bhsu2nsl9Dyheg1IhiFm3FfFoz0aK5dkCadqow==}
 
   '@webpack-cli/configtest@1.2.0':
     resolution: {integrity: sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==}
@@ -3875,9 +3872,6 @@ packages:
     resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==}
     engines: {node: '>= 8'}
 
-  fix-webm-duration@1.0.5:
-    resolution: {integrity: sha512-b6oula3OfSknx0aWoLsxvp4DVIYbwsf+UAkr6EDAK3iuMYk/OSNKzmeSI61GXK0MmFTEuzle19BPvTxMIKjkZg==}
-
   flat-cache@3.0.4:
     resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
     engines: {node: ^10.12.0 || >=12.0.0}
@@ -5240,8 +5234,8 @@ packages:
     resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
     hasBin: true
 
-  opfs-tools@0.1.1:
-    resolution: {integrity: sha512-F+UroKcPfU2/ZzT/4/hONIEbdFHkD/oWwpzH3rkaz+EiDm0W6b2vqNAp1LrWClGrERZ19X3ceRixYL0ZdXcpjA==}
+  opfs-tools@0.6.1:
+    resolution: {integrity: sha512-UAopBwr64jzyrNUr4LWWhLGZY2dAzNIKX/CTI48wR5V88dx3aqx0DN+NdosMY9vZ0heb12ZMTPQ0XpoC4cfSOA==}
 
   optionator@0.9.1:
     resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
@@ -9144,12 +9138,6 @@ snapshots:
     dependencies:
       '@types/node': 18.15.3
 
-  '@types/dom-mediacapture-transform@0.1.9':
-    dependencies:
-      '@types/dom-webcodecs': 0.1.11
-
-  '@types/dom-webcodecs@0.1.11': {}
-
   '@types/eslint-scope@3.7.4':
     dependencies:
       '@types/eslint': 8.21.2
@@ -9251,8 +9239,6 @@ snapshots:
 
   '@types/web-bluetooth@0.0.20': {}
 
-  '@types/wicg-file-system-access@2020.9.8': {}
-
   '@types/ws@8.5.4':
     dependencies:
       '@types/node': 18.15.3
@@ -9558,23 +9544,23 @@ snapshots:
       '@webassemblyjs/ast': 1.11.1
       '@xtuc/long': 4.2.2
 
-  '@webav/av-cliper@0.3.3':
+  '@webav/av-cliper@1.0.6':
     dependencies:
-      '@types/dom-webcodecs': 0.1.11
-      '@webav/mp4box.js': 0.5.3-fenghen
-      opfs-tools: 0.1.1
+      '@webav/internal-utils': 1.0.6
+      '@webav/mp4box.js': 0.5.4-fenghen
+      opfs-tools: 0.6.1
       wave-resampler: 1.0.0
 
-  '@webav/av-recorder@0.3.3':
+  '@webav/av-recorder@1.0.6':
     dependencies:
-      '@types/dom-mediacapture-transform': 0.1.9
-      '@types/dom-webcodecs': 0.1.11
-      '@types/wicg-file-system-access': 2020.9.8
-      '@webav/av-cliper': 0.3.3
-      '@webav/mp4box.js': 0.5.3-fenghen
-      fix-webm-duration: 1.0.5
+      '@webav/av-cliper': 1.0.6
+      '@webav/internal-utils': 1.0.6
 
-  '@webav/mp4box.js@0.5.3-fenghen': {}
+  '@webav/internal-utils@1.0.6':
+    dependencies:
+      '@webav/mp4box.js': 0.5.4-fenghen
+
+  '@webav/mp4box.js@0.5.4-fenghen': {}
 
   '@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0(webpack-bundle-analyzer@4.8.0)(webpack-dev-server@4.13.1)(webpack@5.76.2))(webpack@5.76.2(@swc/core@1.3.84)(esbuild@0.15.18)(webpack-cli@4.10.0))':
     dependencies:
@@ -11651,8 +11637,6 @@ snapshots:
       micromatch: 4.0.5
       resolve-dir: 1.0.1
 
-  fix-webm-duration@1.0.5: {}
-
   flat-cache@3.0.4:
     dependencies:
       flatted: 3.2.7
@@ -13101,7 +13085,7 @@ snapshots:
 
   opener@1.5.2: {}
 
-  opfs-tools@0.1.1: {}
+  opfs-tools@0.6.1: {}
 
   optionator@0.9.1:
     dependencies:

+ 0 - 15
public/index.html

@@ -18,21 +18,6 @@
       href="<%= BASE_URL %>favicon.ico"
     />
     <title><%= htmlWebpackPlugin.options.title %></title>
-    <script>
-      var _hmt = _hmt || [];
-      (function () {
-        var hm = document.createElement('script');
-        hm.src = 'https://hm.baidu.com/hm.js?ffc088b4c034487265310b8492d3074c';
-        var s = document.getElementsByTagName('script')[0];
-        s.parentNode.insertBefore(hm, s);
-      })();
-    </script>
-    <!-- 谷歌广告 -->
-    <script
-      async
-      src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6064454674933772"
-      crossorigin="anonymous"
-    ></script>
   </head>
 
   <body>

+ 4 - 0
src/api/live.ts

@@ -16,3 +16,7 @@ export function fetchLiveRoomOnlineUser(params) {
 export function fetchLiveLiveRoomIsLive(liveRoomId: number) {
   return request.get(`/live/live_room_is_live/${liveRoomId}`);
 }
+
+export function fetchLiveCloseMyLive() {
+  return request.post(`/live/close_my_live`);
+}

+ 42 - 12
src/constant.ts

@@ -195,17 +195,43 @@ export const goodsTypeEnumMap = {
   [GoodsTypeEnum.qypShop]: '逸鹏的商品',
 };
 
-export const mediaTypeEnumMap = {
-  [MediaTypeEnum.camera]: '摄像头',
-  [MediaTypeEnum.microphone]: '麦克风',
-  [MediaTypeEnum.screen]: '窗口',
-  [MediaTypeEnum.img]: '图片',
-  [MediaTypeEnum.txt]: '文字',
-  [MediaTypeEnum.media]: '视频',
-  [MediaTypeEnum.time]: '时间',
-  [MediaTypeEnum.stopwatch]: '秒表',
-  [MediaTypeEnum.pk]: '打pk',
-  [MediaTypeEnum.metting]: '会议',
+export const allMediaTypeList = {
+  [MediaTypeEnum.camera]: {
+    type: MediaTypeEnum.camera,
+    txt: '摄像头',
+  },
+  [MediaTypeEnum.microphone]: {
+    type: MediaTypeEnum.microphone,
+    txt: '麦克风',
+  },
+  [MediaTypeEnum.screen]: {
+    type: MediaTypeEnum.screen,
+    txt: '窗口',
+  },
+  [MediaTypeEnum.txt]: {
+    type: MediaTypeEnum.txt,
+    txt: '文字',
+  },
+  [MediaTypeEnum.img]: {
+    type: MediaTypeEnum.img,
+    txt: '图片',
+  },
+  [MediaTypeEnum.media]: {
+    type: MediaTypeEnum.media,
+    txt: '视频',
+  },
+  [MediaTypeEnum.time]: {
+    type: MediaTypeEnum.time,
+    txt: '时间',
+  },
+  [MediaTypeEnum.stopwatch]: {
+    type: MediaTypeEnum.stopwatch,
+    txt: '秒表',
+  },
+  [MediaTypeEnum.removeGreenVideo]: {
+    type: MediaTypeEnum.removeGreenVideo,
+    txt: '移除绿幕',
+  },
 };
 
 export const liveRoomTypeEnumMap = {
@@ -219,9 +245,13 @@ export const liveRoomTypeEnumMap = {
   [LiveRoomTypeEnum.wertc_live]: 'webrtc直播',
   [LiveRoomTypeEnum.wertc_meeting_one]: 'webrtc会议一',
   [LiveRoomTypeEnum.wertc_meeting_two]: 'webrtc会议二',
+  [LiveRoomTypeEnum.forward_all]: '转推所有',
   [LiveRoomTypeEnum.forward_bilibili]: '转推b站',
+  [LiveRoomTypeEnum.forward_douyin]: '转推抖音',
+  [LiveRoomTypeEnum.forward_douyu]: '转推斗鱼',
   [LiveRoomTypeEnum.forward_huya]: '转推虎牙',
-  [LiveRoomTypeEnum.forward_all]: '转推所有',
+  [LiveRoomTypeEnum.forward_kuaishou]: '转推快手',
+  [LiveRoomTypeEnum.forward_xiaohongshu]: '转推小红书',
 };
 
 export const sliderList = [

+ 2 - 6
src/hooks/use-push.ts

@@ -193,7 +193,7 @@ export function usePush() {
     });
   }
 
-  async function startLive({ type, msrDelay, msrMaxDelay }) {
+  async function startLive({ type, cdn, isdev, msrDelay, msrMaxDelay }) {
     if (!loginTip()) return;
     const flag = handleUserHasLiveRoom();
     if (!flag) {
@@ -232,11 +232,7 @@ export function usePush() {
         frameRate: currentMaxFramerate.value,
       });
     }
-    handleStartLive({
-      type,
-      msrDelay,
-      msrMaxDelay,
-    });
+    handleStartLive({ cdn, isdev, type, msrDelay, msrMaxDelay });
   }
 
   /** 结束直播 */

+ 1 - 37
src/hooks/use-rtcParams.ts

@@ -1,7 +1,6 @@
 import { ref, watch } from 'vue';
 
-import { DEFAULT_AUTH_INFO } from '@/constant';
-import { MediaTypeEnum } from '@/interface';
+import { allMediaTypeList, DEFAULT_AUTH_INFO } from '@/constant';
 import { useUserStore } from '@/store/user';
 
 export const useRTCParams = () => {
@@ -192,41 +191,6 @@ export const useRTCParams = () => {
     },
     { immediate: true }
   );
-  const allMediaTypeList: Record<string, { type: MediaTypeEnum; txt: string }> =
-    {
-      [MediaTypeEnum.camera]: {
-        type: MediaTypeEnum.camera,
-        txt: '摄像头',
-      },
-      [MediaTypeEnum.microphone]: {
-        type: MediaTypeEnum.microphone,
-        txt: '麦克风',
-      },
-      [MediaTypeEnum.screen]: {
-        type: MediaTypeEnum.screen,
-        txt: '窗口',
-      },
-      [MediaTypeEnum.txt]: {
-        type: MediaTypeEnum.txt,
-        txt: '文字',
-      },
-      [MediaTypeEnum.img]: {
-        type: MediaTypeEnum.img,
-        txt: '图片',
-      },
-      [MediaTypeEnum.media]: {
-        type: MediaTypeEnum.media,
-        txt: '视频',
-      },
-      [MediaTypeEnum.time]: {
-        type: MediaTypeEnum.time,
-        txt: '时间',
-      },
-      [MediaTypeEnum.stopwatch]: {
-        type: MediaTypeEnum.stopwatch,
-        txt: '秒表',
-      },
-    };
 
   return {
     maxBitrate,

+ 27 - 45
src/hooks/use-websocket.ts

@@ -8,9 +8,7 @@ import { THEME_COLOR, URL_QUERY } from '@/constant';
 import { commentAuthTip, loginTip } from '@/hooks/use-login';
 import { useRTCParams } from '@/hooks/use-rtcParams';
 import { useTip } from '@/hooks/use-tip';
-import { useForwardAll } from '@/hooks/webrtc/forwardAll';
-import { useForwardBilibili } from '@/hooks/webrtc/forwardBilibili';
-import { useForwardHuya } from '@/hooks/webrtc/forwardHuya';
+import { useForwardThirdPartyLiveStreaming } from '@/hooks/webrtc/forwardThirdPartyLiveStreaming';
 import { useWebRtcLive } from '@/hooks/webrtc/live';
 import { useWebRtcMeetingOne } from '@/hooks/webrtc/meetingOne';
 import { useWebRtcMeetingPk } from '@/hooks/webrtc/meetingPk';
@@ -20,6 +18,7 @@ import {
   DanmuMsgTypeEnum,
   ILiveUser,
   IWsMessage,
+  SwitchEnum,
   WsMessageContentTypeEnum,
   WsMessageIsBilibiliEnum,
 } from '@/interface';
@@ -71,9 +70,10 @@ export const useWebsocket = () => {
   } = useRTCParams();
   const { updateWebRtcMeetingPkConfig, webRtcMeetingPk } = useWebRtcMeetingPk();
   const { updateWebRtcSrsConfig, webRtcSrs } = useWebRtcSrs();
-  const { updateForwardBilibiliConfig, forwardBilibili } = useForwardBilibili();
-  const { updateForwardAllConfig, forwardAll } = useForwardAll();
-  const { updateForwardHuyaConfig, forwardHuya } = useForwardHuya();
+  const {
+    updateForwardThirdPartyLiveStreamingConfig,
+    forwardThirdPartyLiveStreaming,
+  } = useForwardThirdPartyLiveStreaming();
   const { updateWebRtcTencentcloudCssConfig, webRtcTencentcloudCss } =
     useWebRtcTencentcloudCss();
   const { updateWebRtcLiveConfig, webRtcLive } = useWebRtcLive();
@@ -217,12 +217,14 @@ export const useWebsocket = () => {
   function handleHeartbeat() {}
 
   function handleStartLive({
-    name,
+    cdn,
+    isdev,
     type,
     msrDelay,
     msrMaxDelay,
   }: {
-    name?: string;
+    cdn: SwitchEnum;
+    isdev: string;
     type: LiveRoomTypeEnum;
     videoEl?: HTMLVideoElement;
     msrDelay: number;
@@ -235,7 +237,6 @@ export const useWebsocket = () => {
       requestId: getRandomString(8),
       msgType: WsMsgTypeEnum.startLive,
       data: {
-        name: name!,
         type,
         msrDelay,
         msrMaxDelay,
@@ -269,48 +270,31 @@ 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) {
-      updateForwardHuyaConfig({
-        isPk: false,
-        roomId: roomId.value,
-        canvasVideoStream: canvasVideoStream.value,
-      });
-      forwardHuya.newWebRtc({
-        sender: mySocketId.value,
-        receiver: 'srs',
-        videoEl: createNullVideo(),
-      });
-      forwardHuya.sendOffer({
-        sender: mySocketId.value,
-        receiver: 'srs',
-      });
-    } else if (type === LiveRoomTypeEnum.forward_all) {
-      updateForwardAllConfig({
+    } else if (
+      [
+        LiveRoomTypeEnum.forward_all,
+        LiveRoomTypeEnum.forward_bilibili,
+        LiveRoomTypeEnum.forward_douyin,
+        LiveRoomTypeEnum.forward_douyu,
+        LiveRoomTypeEnum.forward_huya,
+        LiveRoomTypeEnum.forward_kuaishou,
+        LiveRoomTypeEnum.forward_xiaohongshu,
+      ].includes(type)
+    ) {
+      updateForwardThirdPartyLiveStreamingConfig({
+        cdn,
+        isdev,
+        liveRoomType: type,
         isPk: false,
         roomId: roomId.value,
         canvasVideoStream: canvasVideoStream.value,
       });
-      forwardAll.newWebRtc({
+      forwardThirdPartyLiveStreaming.newWebRtc({
         sender: mySocketId.value,
         receiver: 'srs',
         videoEl: createNullVideo(),
       });
-      forwardAll.sendOffer({
+      forwardThirdPartyLiveStreaming.sendOffer({
         sender: mySocketId.value,
         receiver: 'srs',
       });
@@ -523,8 +507,6 @@ export const useWebsocket = () => {
           } else {
             console.error('不是发给我的nativeWebRtcOffer');
           }
-        } else if (data.live_room.type === LiveRoomTypeEnum.wertc_live) {
-        } else if (data.live_room.type === LiveRoomTypeEnum.wertc_meeting_one) {
         }
       }
     );

+ 153 - 0
src/hooks/webrtc/forwardThirdPartyLiveStreaming.ts

@@ -0,0 +1,153 @@
+import { ref } from 'vue';
+
+import { fetchRtcV1Publish } from '@/api/srs';
+import { fetchTencentcloudCssPush } from '@/api/tencentcloudCss';
+import { SRS_CB_URL_QUERY } from '@/constant';
+import { useRTCParams } from '@/hooks/use-rtcParams';
+import { SwitchEnum } from '@/interface';
+import { useNetworkStore } from '@/store/network';
+import { useUserStore } from '@/store/user';
+import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
+import { WebRTCClass } from '@/utils/network/webRTC';
+
+export function useForwardThirdPartyLiveStreaming() {
+  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>();
+  const cdn = ref<SwitchEnum>();
+  const isdev = ref<string>('2');
+  const liveRoomType = ref<LiveRoomTypeEnum>();
+
+  function updateForwardThirdPartyLiveStreamingConfig(data: {
+    cdn: SwitchEnum;
+    isdev: string;
+    liveRoomType: LiveRoomTypeEnum;
+    isPk;
+    roomId;
+    canvasVideoStream;
+  }) {
+    cdn.value = data.cdn;
+    isdev.value = data.isdev;
+    liveRoomType.value = data.liveRoomType;
+    isPk.value = data.isPk;
+    roomId.value = data.roomId;
+    canvasVideoStream.value = data.canvasVideoStream;
+  }
+
+  const forwardThirdPartyLiveStreaming = {
+    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('开始ForwardThirdPartyLiveStreaming的sendOffer', {
+        sender,
+        receiver,
+      });
+      try {
+        const liveRooms = userStore.userInfo?.live_rooms;
+        const myLiveRoom = liveRooms?.[0];
+        if (!myLiveRoom) {
+          window.$message.error('你没有开通直播间');
+          return;
+        }
+        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(
+                'ForwardThirdPartyLiveStreaming的canvasVideoStream插入track',
+                track.kind,
+                track
+              );
+              rtc.peerConnection?.addTrack(track, canvasVideoStream.value);
+            }
+          });
+          const offerSdp = await rtc.createOffer();
+          if (!offerSdp) {
+            console.error('ForwardThirdPartyLiveStreaming的offerSdp为空');
+            window.$message.error(
+              'ForwardThirdPartyLiveStreaming的offerSdp为空'
+            );
+            return;
+          }
+          await rtc.setLocalDescription(offerSdp!);
+          if (cdn.value === SwitchEnum.no) {
+            const answerRes = await fetchRtcV1Publish({
+              sdp: offerSdp.sdp!,
+              streamurl: `${myLiveRoom.pull_rtmp_url!}?${
+                SRS_CB_URL_QUERY.publishKey
+              }=${myLiveRoom.key!}&${SRS_CB_URL_QUERY.publishType}=${
+                isPk.value ? LiveRoomTypeEnum.pk : liveRoomType.value!
+              }&${SRS_CB_URL_QUERY.userId}=${userStore.userInfo?.id!}&${
+                SRS_CB_URL_QUERY.isdev
+              }=${isdev.value}`,
+            });
+            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 {
+            const res = await fetchTencentcloudCssPush(myLiveRoom.id!);
+            if (res.code === 200) {
+              const livePusher = new window.TXLivePusher();
+              // https://cloud.tencent.com/document/product/267/92713#1a9164cf-9f99-47d5-9667-ea558886cb9f
+              // 使用用户自定义的音视频流。
+              await livePusher.startCustomCapture(canvasVideoStream.value);
+              const pushurl = res.data.webrtc_url
+                ?.replace(/&isdev=\w+/g, `&isdev=${isdev.value}`)
+                .replace(/&pushtype=\w+/g, `&pushtype=${liveRoomType.value!}`);
+              livePusher.startPush(pushurl);
+            }
+          }
+        } else {
+          console.error('rtc不存在');
+        }
+      } catch (error) {
+        console.error('ForwardThirdPartyLiveStreaming的sendOffer错误');
+        console.log(error);
+      }
+    },
+  };
+
+  return {
+    updateForwardThirdPartyLiveStreamingConfig,
+    forwardThirdPartyLiveStreaming,
+  };
+}

+ 1 - 0
src/interface.ts

@@ -729,6 +729,7 @@ export enum MediaTypeEnum {
   webAudio,
   pk,
   metting,
+  removeGreenVideo,
 }
 
 export enum DanmuMsgTypeEnum {

+ 0 - 2
src/router/index.ts

@@ -337,7 +337,6 @@ router.beforeEach((to, from, next) => {
     console.log('当前是移动端');
     if (!Object.keys(mobileRouterName).includes(to.name as string)) {
       // 当前移动端,但是跳转了非移动端路由
-      console.log('当前移动端,但是跳转了非移动端路由', to, from);
       if (to.name === routerName.pull) {
         return next({
           name: mobileRouterName.h5Room,
@@ -375,7 +374,6 @@ router.beforeEach((to, from, next) => {
       return next();
     }
   } else {
-    console.log('当前是电脑/ipad端');
     if (Object.keys(mobileRouterName).includes(to.name as string)) {
       // 当前非移动端,但是跳转了移动端路由
       console.log('当前非移动端,但是跳转了移动端路由');

+ 0 - 1
src/store/app/index.ts

@@ -34,7 +34,6 @@ export type AppRootState = {
     stream?: MediaStream;
     streamid?: string;
     trackid?: string;
-    // canvasDom?: fabric.Image | fabric.Text;
     canvasDom?: any;
     hidden?: boolean;
     muted?: boolean;

+ 0 - 1
src/types/websocket.ts

@@ -191,7 +191,6 @@ export type WsOtherJoinType = IResWsFormat<{
 
 /** 开始直播 */
 export type WsStartLiveType = IReqWsFormat<{
-  name: string;
   type: LiveRoomTypeEnum;
   /** 单位:毫秒 */
   msrDelay: number;

+ 90 - 6
src/utils/index.ts

@@ -1,7 +1,69 @@
 // TIP: ctrl+cmd+t,生成函数注释
+import { createChromakey } from '@webav/av-cliper';
 import { computeBox, getRangeRandom, judgeType } from 'billd-utils';
 import sparkMD5 from 'spark-md5';
 
+export function videoRemoveBackground(data: {
+  videoEl;
+  /** 需要扣除的背景色,若不传则取第一个像素点 */
+  keyColor?;
+  /** 背景色相似度阈值,过小可能保留背景色,过大可能扣掉更多非背景像素点 */
+  similarity?;
+  /** 平滑度;过小可能出现锯齿,过大导致整体变透明 */
+  smoothness?;
+  /** 饱和度;过小可能保留绿色混合,过大导致图片变灰度 */
+  spill?;
+}) {
+  const videoEl = data.videoEl;
+  const keyColor = data.keyColor || [31, 255, 60];
+  const similarity = data.similarity || 0.4;
+  const smoothness = data.smoothness || 0.1;
+  const spill = data.spill || 0.1;
+  return new Promise<HTMLCanvasElement>((resolve) => {
+    videoEl.style.position = 'fixed';
+    videoEl.style.bottom = '0';
+    videoEl.style.right = '0';
+    videoEl.style.width = '1px';
+    videoEl.style.height = '1px';
+    videoEl.style.opacity = '0';
+    videoEl.style.pointerEvents = 'none';
+    document.body.appendChild(videoEl);
+    videoEl.addEventListener('playing', () => {
+      // @ts-ignore
+      const stream = videoEl.captureStream() as MediaStream;
+      const { width, height } = stream.getVideoTracks()[0].getSettings();
+      if (width && height) {
+        const canvasEl = document.createElement('canvas');
+        videoEl.width = width;
+        videoEl.height = height;
+        canvasEl.width = width;
+        canvasEl.height = height;
+        const ctx = canvasEl.getContext('2d', { alpha: true });
+        if (ctx) {
+          const chromakey = createChromakey({
+            keyColor,
+            similarity,
+            smoothness,
+            spill,
+          });
+          const render = async () => {
+            ctx.drawImage(await chromakey(videoEl), 0, 0, width, height);
+            requestAnimationFrame(render);
+          };
+          render();
+        }
+        // const video = document.createElement('video');
+        // video.srcObject = canvasEl.captureStream();
+        // video.autoplay = true;
+        // video.loop = true;
+        // document.body.appendChild(video);
+        // resolve(video);
+        resolve(canvasEl);
+      }
+    });
+  });
+}
+
 export function isMSESupported() {
   return !!window.MediaSource;
 }
@@ -141,10 +203,11 @@ export function formatMoney(money: number, hideZeroCent?: boolean) {
   }
 }
 
+export function addZero(num: number) {
+  return num < 10 ? `0${num}` : `${num}`;
+}
+
 export const formatTimeHour = (time: number | string | Date) => {
-  function addZero(num: number) {
-    return num < 10 ? `0${num}` : num;
-  }
   let time2 = time;
   if (judgeType(time) === 'string') {
     time2 = iosTimestamp(time as string);
@@ -162,9 +225,6 @@ export const formatTimeHour = (time: number | string | Date) => {
 };
 
 export const formatTime = (timestamp: number) => {
-  function addZero(num: number) {
-    return num < 10 ? `0${num}` : num;
-  }
   const date = new Date(timestamp);
 
   // 获取年份
@@ -191,6 +251,30 @@ export const formatTime = (timestamp: number) => {
   )}:${addZero(seconds)}`;
 };
 
+export const formatTime3 = (timestamp: number) => {
+  const date = new Date(timestamp);
+
+  // 获取年份
+  const year = date.getFullYear();
+
+  // 获取月份(注意月份是从0开始的,所以要加1)
+  const month = date.getMonth() + 1;
+
+  // 获取日期
+  const day = date.getDate();
+
+  // 获取小时
+  const hours = date.getHours();
+
+  // 获取分钟
+  const minutes = date.getMinutes();
+
+  // 获取秒数
+  const seconds = date.getSeconds();
+
+  return { year, month, day, hours, minutes, seconds };
+};
+
 export const getLiveRoomPageUrl = (liveRoomId: number) => {
   return `${getHostnameUrl()}/pull/${liveRoomId}`;
 };

+ 280 - 96
src/views/push/index.vue

@@ -9,7 +9,7 @@
         class="container"
       >
         <div
-          class="screenshot"
+          class="screenshot-ico"
           @click="handleScreenshot"
         >
           <n-popover
@@ -85,7 +85,7 @@
           class="debug-info"
         >
           <span>{{
-            liveRoomTypeEnumMap[appStore.liveRoomInfo?.type + '']
+            liveRoomTypeEnumMap[Number(route.query[URL_QUERY.liveType])]
           }}</span>
           <span>:</span>
           <span>{{ mySocketId }}</span>
@@ -97,56 +97,106 @@
           ></div>
           <div class="detail">
             <div class="top">
-              <div
-                class="name"
-                v-if="appStore.liveRoomInfo"
-              >
-                名称:
-                <div class="val">
-                  <n-input-group>
-                    <n-input
-                      v-model:value="appStore.liveRoomInfo.name"
-                      size="small"
-                      placeholder="请输入房间名"
-                    />
-                    <n-button
-                      size="small"
-                      type="primary"
-                      @click="changeLiveRoomName"
-                    >
-                      确定
-                    </n-button>
-                  </n-input-group>
+              <div class="top-config">
+                <div
+                  class="name"
+                  v-if="appStore.liveRoomInfo"
+                >
+                  名称:
+                  <div class="val">
+                    <n-input-group>
+                      <n-input
+                        v-model:value="appStore.liveRoomInfo.name"
+                        size="small"
+                        placeholder="请输入房间名"
+                      />
+                      <n-button
+                        size="small"
+                        type="primary"
+                        @click="changeLiveRoomName"
+                      >
+                        确定
+                      </n-button>
+                    </n-input-group>
+                  </div>
                 </div>
-              </div>
-              <div class="area">
-                分区:
-                <div class="val">
-                  <n-input-group>
-                    <n-select
-                      v-model:value="currentArea"
-                      :options="areaList"
-                      size="small"
-                      placeholder="请选择分区"
-                    />
+                <div class="area">
+                  分区:
+                  <div class="val">
+                    <n-input-group>
+                      <n-select
+                        v-model:value="currentArea"
+                        :options="areaList"
+                        size="small"
+                        placeholder="请选择分区"
+                      />
 
-                    <n-button
-                      size="small"
-                      type="primary"
-                      @click="changeLiveRoomArea"
-                    >
-                      确定
-                    </n-button>
-                  </n-input-group>
+                      <n-button
+                        size="small"
+                        type="primary"
+                        @click="changeLiveRoomArea"
+                      >
+                        确定
+                      </n-button>
+                    </n-input-group>
+                  </div>
+                </div>
+                <div class="cdn">
+                  CDN:
+                  <div class="val">
+                    <n-input-group>
+                      <n-select
+                        v-model:value="currentCdn"
+                        :options="cdnList"
+                        size="small"
+                        placeholder=""
+                      />
+
+                      <n-button
+                        size="small"
+                        type="primary"
+                        @click="changeLiveRoomCdn"
+                      >
+                        确定
+                      </n-button>
+                    </n-input-group>
+                  </div>
+                </div>
+                <div class="dev">
+                  DEV:
+                  <div class="val">
+                    <n-input-group>
+                      <n-select
+                        v-model:value="currentDev"
+                        :options="devList"
+                        size="small"
+                        placeholder=""
+                      />
+
+                      <n-button
+                        size="small"
+                        type="primary"
+                        @click="changeLiveRoomCdn"
+                      >
+                        确定
+                      </n-button>
+                    </n-input-group>
+                  </div>
+                </div>
+                <div
+                  class="rtc-info"
+                  v-if="0"
+                >
+                  <div class="item">延迟:{{ rtcRtt || '-' }}</div>
+                  <div class="item">丢包:{{ rtcLoss || '-' }}</div>
+                  <div class="item">帧率:{{ rtcFps || '-' }}</div>
+                  <div class="item">发送码率:{{ rtcBytesSent || '-' }}</div>
+                  <div class="item">
+                    接收码率:{{ rtcBytesReceived || '-' }}
+                  </div>
                 </div>
               </div>
-              <div class="rtc-info">
-                <div class="item">延迟:{{ rtcRtt || '-' }}</div>
-                <div class="item">丢包:{{ rtcLoss || '-' }}</div>
-                <div class="item">帧率:{{ rtcFps || '-' }}</div>
-                <div class="item">发送码率:{{ rtcBytesSent || '-' }}</div>
-                <div class="item">接收码率:{{ rtcBytesReceived || '-' }}</div>
-              </div>
+
               <div class="other">
                 <span
                   class="item share"
@@ -154,10 +204,6 @@
                 >
                   分享直播间
                 </span>
-                <span class="item">
-                  正在观看:
-                  {{ liveUserList.length }}
-                </span>
               </div>
             </div>
             <div class="bottom">
@@ -268,7 +314,7 @@
               </div>
               <div class="name">
                 {{ NODE_ENV === 'development' ? item.id : '' }}({{
-                  mediaTypeEnumMap[item.type]
+                  allMediaTypeList[item.type].txt
                 }}){{ item.mediaName }}
               </div>
             </div>
@@ -329,7 +375,9 @@
         </div>
       </div>
       <div class="danmu-card">
-        <div class="title">弹幕互动</div>
+        <div class="title">
+          弹幕互动{{ liveUserList.length ? `(${liveUserList.length})` : '' }}
+        </div>
         <div class="list-wrap">
           <div
             ref="danmuListRef"
@@ -486,15 +534,10 @@ import {
 } from 'vue';
 import { useRoute } from 'vue-router';
 
-import { fetchLiveRoomOnlineUser } from '@/api/live';
+import { fetchLiveCloseMyLive, fetchLiveRoomOnlineUser } from '@/api/live';
 import { fetchUpdateMyLiveRoom } from '@/api/liveRoom';
 import { fetchGetWsMessageList } from '@/api/wsMessage';
-import {
-  THEME_COLOR,
-  URL_QUERY,
-  liveRoomTypeEnumMap,
-  mediaTypeEnumMap,
-} from '@/constant';
+import { THEME_COLOR, URL_QUERY, liveRoomTypeEnumMap } from '@/constant';
 import { emojiArray } from '@/emoji';
 import { commentAuthTip, loginTip } from '@/hooks/use-login';
 import { usePush } from '@/hooks/use-push';
@@ -505,6 +548,7 @@ import { useWebsocket } from '@/hooks/use-websocket';
 import {
   DanmuMsgTypeEnum,
   MediaTypeEnum,
+  SwitchEnum,
   WsMessageContentTypeEnum,
   WsMessageIsFileEnum,
   WsMessageIsShowEnum,
@@ -517,9 +561,11 @@ import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
 import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
 import {
+  addZero,
   base64ToFile,
   createVideo,
   formatDownTime2,
+  formatTime3,
   formatTimeHour,
   generateBase64,
   getLiveRoomPageUrl,
@@ -529,6 +575,7 @@ import {
   saveFile,
   setAudioTrackContentHints,
   setVideoTrackContentHints,
+  videoRemoveBackground,
 } from '@/utils';
 import { NODE_ENV } from 'script/constant';
 
@@ -613,6 +660,16 @@ const suggestedName = ref('');
 const recordVideoTimer = ref();
 const areaList = ref<{ label: string; value: number }[]>([]);
 const currentArea = ref(-1);
+const cdnList = ref<{ label: string; value: number }[]>([
+  { label: '是', value: SwitchEnum.yes },
+  { label: '否', value: SwitchEnum.no },
+]);
+const currentCdn = ref(SwitchEnum.no);
+const devList = ref<{ label: string; value: string }[]>([
+  { label: '是', value: '1' },
+  { label: '否', value: '2' },
+]);
+const currentDev = ref('1');
 const recordVideoTime = ref('00:00:00');
 let avRecorder: AVRecorder | null = null;
 const loopGetLiveUserTimer = ref();
@@ -811,6 +868,17 @@ async function changeLiveRoomArea() {
     }
   }
 }
+async function changeLiveRoomCdn() {
+  if (appStore.liveRoomInfo) {
+    // @ts-ignore
+    const res = await fetchUpdateMyLiveRoom({
+      cdn: currentCdn.value,
+    });
+    if (res.code === 200) {
+      window.$message.success('修改成功!');
+    }
+  }
+}
 
 function handleSendDanmu() {
   sendDanmuTxt(danmuStr.value);
@@ -981,6 +1049,9 @@ watch(
       if (area) {
         currentArea.value = area.id!;
       }
+      if (newval.cdn !== undefined) {
+        currentCdn.value = newval.cdn;
+      }
     }
   },
   {
@@ -1201,6 +1272,7 @@ function handleEndLive() {
   clearLoop();
   endLive();
   sendRoomNoLive();
+  fetchLiveCloseMyLive();
 }
 
 function clearLoop() {
@@ -1245,16 +1317,27 @@ async function handleHistoryMsg() {
   }
 }
 
+function formatCurrentTime() {
+  const res = formatTime3(+new Date());
+  const name = `${res.year}年${addZero(res.month)}月${addZero(
+    res.day
+  )}日${addZero(res.hours)}时${addZero(res.minutes)}分${addZero(
+    res.seconds
+  )}秒`;
+  return name;
+}
+
 function handleScreenshot() {
   const url = generateBase64(pushCanvasRef.value!);
   const a = document.createElement('a');
   const event = new MouseEvent('click');
-  a.download = `${+new Date()}截屏`;
+  a.download = `${formatCurrentTime()}截屏`;
   a.href = url;
   a.dispatchEvent(event);
 }
 
 async function handleRecordVideo() {
+  // @ts-ignore
   if (!window.VideoDecoder || !window.AudioEncoder) {
     window.$message.warning(`当前环境不支持录制视频`);
     return;
@@ -1262,13 +1345,14 @@ async function handleRecordVideo() {
   initAudio();
   try {
     if (!recording.value) {
-      suggestedName.value = `billd直播录制-${+new Date()}.mp4`;
+      suggestedName.value = `billd直播录制-${formatCurrentTime()}.mp4`;
+      // @ts-ignore
       const fileHandle = await window.showSaveFilePicker({
         suggestedName: suggestedName.value,
       });
       const writer = await fileHandle.createWritable();
-      avRecorder = new AVRecorder(canvasVideoStream.value!.clone(), {});
-      await avRecorder.start();
+      avRecorder = new AVRecorder(canvasVideoStream.value!.clone());
+
       const startTime = +new Date();
       recordVideoTimer.value = setInterval(() => {
         const res = formatDownTime2({
@@ -1283,15 +1367,23 @@ async function handleRecordVideo() {
           recordVideoTime.value = `${res.h}:${res.m}:${res.s}`;
         }
       }, 1000);
-      avRecorder.outputStream?.pipeTo(writer).catch(console.error);
+      recording.value = true;
+      avRecorder
+        .start()
+        .pipeTo(writer)
+        .catch((error) => {
+          recording.value = false;
+          console.log('录制错误', error);
+          window.$message.error('录制错误');
+        });
     } else {
       clearInterval(recordVideoTimer.value);
       recordVideoTime.value = '00:00:00';
+      recording.value = false;
       await avRecorder?.stop();
       window.$message.success(`录制文件: ${suggestedName.value} 已保存到本地`);
       avRecorder = null;
     }
-    recording.value = !recording.value;
   } catch (error) {
     console.log(error);
     recording.value = false;
@@ -1323,7 +1415,7 @@ function handleShare() {
     title: '分享',
     confirmButtonText: '复制',
     hiddenCancel: true,
-    maskClosable: false,
+    maskClosable: true,
   })
     .then(() => {
       copyToClipBoard(getLiveRoomPageUrl(+roomId.value));
@@ -1339,6 +1431,8 @@ function handleStartLive() {
   }
   initAudio();
   startLive({
+    cdn: currentCdn.value,
+    isdev: currentDev.value,
     type: liveType,
     msrDelay: msrDelay.value,
     msrMaxDelay: 5000,
@@ -1376,6 +1470,7 @@ function autoCreateVideo(data: {
   rect?: { left: number; top: number };
   scaleInfo?: Record<number, { scaleX: number; scaleY: number }>;
   muted?: boolean;
+  removeGreen?: boolean;
 }) {
   const { file, id, rect, scaleInfo, muted } = data;
   let stream = data.stream;
@@ -1406,7 +1501,7 @@ function autoCreateVideo(data: {
     videoEl.onloadedmetadata = () => {
       let canvasDom: Raw<fabric.Image>;
       let ratio;
-      function main() {
+      async function main() {
         const width =
           stream?.getVideoTracks()[0].getSettings().width! ||
           videoEl.videoWidth;
@@ -1417,10 +1512,15 @@ function autoCreateVideo(data: {
         videoEl.width = width;
         videoEl.height = height;
         const old = appStore.allTrack.find((item) => item.id === id);
+        let removeGreenCanvas: any = videoEl;
+        if (data.removeGreen) {
+          removeGreenCanvas = await videoRemoveBackground({ videoEl });
+          console.log('removeGreenCanvas');
+        }
         if (canvasDom) {
           fabricCanvas.value?.remove(canvasDom);
           canvasDom = markRaw(
-            new fabric.Image(videoEl, {
+            new fabric.Image(removeGreenCanvas, {
               top: (old?.rect?.top || rect?.top || 0) / window.devicePixelRatio,
               left:
                 (old?.rect?.left || rect?.left || 0) / window.devicePixelRatio,
@@ -1430,7 +1530,7 @@ function autoCreateVideo(data: {
           );
         } else {
           canvasDom = markRaw(
-            new fabric.Image(videoEl, {
+            new fabric.Image(removeGreenCanvas, {
               top: (old?.rect?.top || rect?.top || 0) / window.devicePixelRatio,
               left:
                 (old?.rect?.left || rect?.left || 0) / window.devicePixelRatio,
@@ -1560,9 +1660,7 @@ function initCanvas() {
   const ins = markRaw(new fabric.Canvas(pushCanvasRef.value!));
   ins.setWidth(resolutionWidth);
   ins.setHeight(resolutionHeight);
-  ins.setBackgroundColor('black', () => {
-    console.log('setBackgroundColor回调');
-  });
+  ins.setBackgroundColor('black', () => {});
   wrapSize.width = wrapWidth;
   wrapSize.height = wrapHeight;
   fabricCanvas.value = ins;
@@ -1678,7 +1776,7 @@ async function handleCache() {
     obj.scaleInfo = item.scaleInfo;
     obj.stopwatchInfo = item.stopwatchInfo;
 
-    async function handleMediaVideo() {
+    async function handleMediaVideo(removeGreen?: boolean) {
       const { code, file } = await readFile(item.id);
       if (code === 1 && file) {
         const { videoEl, stream, canvasDom } = await autoCreateVideo({
@@ -1687,6 +1785,7 @@ async function handleCache() {
           muted: true,
           rect: item.rect,
           scaleInfo: item.scaleInfo,
+          removeGreen,
         });
         if (obj.volume !== undefined) {
           videoEl.volume = obj.volume / 100;
@@ -1765,7 +1864,6 @@ async function handleCache() {
             const height = stream.getVideoTracks()[0].getSettings().height!;
             videoEl.width = width;
             videoEl.height = height;
-
             const canvasDom = markRaw(
               new fabric.Image(videoEl, {
                 top: (item.rect?.top || 0) / window.devicePixelRatio,
@@ -1831,7 +1929,6 @@ async function handleCache() {
           const height = stream.getVideoTracks()[0].getSettings().height!;
           videoEl.width = width;
           videoEl.height = height;
-
           const canvasDom = markRaw(
             new fabric.Image(videoEl, {
               top: (item.rect?.top || 0) / window.devicePixelRatio,
@@ -1927,6 +2024,11 @@ async function handleCache() {
         fabricCanvas.value.add(canvasDom);
         obj.canvasDom = canvasDom;
       }
+    } else if (
+      item.type === MediaTypeEnum.removeGreenVideo &&
+      item.video === 1
+    ) {
+      queue.push(handleMediaVideo(true));
     }
     res.push(obj);
   });
@@ -2444,6 +2546,76 @@ async function addMediaOk(val: AppRootState['allTrack'][0]) {
     // @ts-ignore
     cacheStore.setResourceList(res);
     console.log('获取视频成功');
+  } else if (val.type === MediaTypeEnum.removeGreenVideo) {
+    const mediaVideoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(6),
+      openEye: true,
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.removeGreenVideo,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+      rect: { left: 0, top: 0 },
+    };
+    if (fabricCanvas.value) {
+      if (!val.mediaInfo) return;
+      const file = val.mediaInfo[0].file!;
+      const { code } = await saveFile({ file, fileName: mediaVideoTrack.id });
+      if (code !== 1) return;
+      const { videoEl, canvasDom, scale, stream } = await autoCreateVideo({
+        file,
+        id: mediaVideoTrack.id,
+        muted: mediaVideoTrack.muted,
+        rect: mediaVideoTrack.rect,
+        scaleInfo: mediaVideoTrack.scaleInfo,
+        removeGreen: true,
+      });
+      setScaleInfo({ canvasDom, track: mediaVideoTrack, scale });
+      mediaVideoTrack.videoEl = videoEl;
+      mediaVideoTrack.canvasDom = canvasDom;
+      mediaVideoTrack.stream = stream;
+      mediaVideoTrack.streamid = stream.id;
+      mediaVideoTrack.track = stream.getVideoTracks()[0];
+      mediaVideoTrack.trackid = stream.getVideoTracks()[0].id;
+
+      if (stream.getAudioTracks()[0]) {
+        console.log('视频有音频');
+        mediaVideoTrack.audio = 1;
+        mediaVideoTrack.volume = appStore.normalVolume;
+        const audioTrack: AppRootState['allTrack'][0] = {
+          id: mediaVideoTrack.id,
+          openEye: true,
+          audio: 1,
+          video: 2,
+          mediaName: val.mediaName,
+          type: MediaTypeEnum.removeGreenVideo,
+          track: stream.getAudioTracks()[0],
+          trackid: stream.getAudioTracks()[0].id,
+          stream,
+          streamid: stream.id,
+          hidden: true,
+          muted: false,
+          volume: mediaVideoTrack.volume,
+          scaleInfo: {},
+        };
+        const res = [...appStore.allTrack, audioTrack];
+        appStore.setAllTrack(res);
+        cacheStore.setResourceList(res);
+        handleMixedAudio();
+      }
+    }
+    const res = [...appStore.allTrack, mediaVideoTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    cacheStore.setResourceList(res);
+    console.log('获取视频成功');
   }
 }
 
@@ -2610,7 +2782,6 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
     background-color: white;
     color: #9499a0;
     vertical-align: top;
-
     .container {
       position: relative;
       height: 100%;
@@ -2620,7 +2791,7 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
         position: absolute;
         top: 5px;
         left: 5px;
-        z-index: 100;
+        z-index: 1;
         color: red;
         font-size: 12px;
         line-height: 1;
@@ -2632,7 +2803,7 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
         cursor: pointer;
         transform: translateX(-100%);
       }
-      .screenshot {
+      .screenshot-ico {
         position: absolute;
         top: 30px;
         left: -10px;
@@ -2643,7 +2814,6 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
         position: absolute;
         top: 5px;
         left: 5px;
-        z-index: 100;
         color: red;
         font-size: 12px;
         line-height: 1;
@@ -2698,26 +2868,40 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
             align-items: center;
             justify-content: space-between;
             color: #18191c;
-            .name {
+            .top-config {
               display: flex;
               align-items: center;
-              margin-right: 15px;
-              .val {
-                width: 180px;
+              .name {
+                display: flex;
+                align-items: center;
+                margin-right: 15px;
+                .val {
+                  width: 180px;
+                }
               }
-            }
-            .rtc-info {
-              display: flex;
-              flex: 1;
-            }
-            .area {
-              display: flex;
-              align-items: center;
-              margin-right: 15px;
-              .val {
-                width: 130px;
+              .rtc-info {
+                display: flex;
+                flex: 1;
+              }
+              .area {
+                display: flex;
+                align-items: center;
+                margin-right: 15px;
+                .val {
+                  width: 130px;
+                }
+              }
+              .cdn,
+              .dev {
+                display: flex;
+                align-items: center;
+                margin-right: 15px;
+                .val {
+                  width: 110px;
+                }
               }
             }
+
             .other {
               .item {
                 margin-right: 10px;

+ 25 - 0
src/views/push/mediaModal/index.vue

@@ -90,6 +90,20 @@
             </div>
           </div>
         </template>
+        <template v-if="props.mediaType === MediaTypeEnum.removeGreenVideo">
+          <div class="item">
+            <div class="label">视频(移除绿幕)</div>
+            <div class="value">
+              <n-upload
+                :max="1"
+                :accept="'video/mp4, video/quicktime'"
+                :on-update:file-list="changMedia"
+              >
+                <n-button :disabled="isEdit">选择文件</n-button>
+              </n-upload>
+            </div>
+          </div>
+        </template>
       </div>
 
       <template #footer>
@@ -313,6 +327,17 @@ async function init() {
         .filter((item) => item.type === MediaTypeEnum.media)
         .filter((item) => !item.hidden).length + 1
     }`;
+  } else if (props.mediaType === MediaTypeEnum.removeGreenVideo) {
+    currentInput.value = {
+      ...currentInput.value,
+      type: MediaTypeEnum.removeGreenVideo,
+    };
+    mediaInfo.value = [];
+    mediaName.value = `视频(移除绿幕)-${
+      appStore.allTrack
+        .filter((item) => item.type === MediaTypeEnum.removeGreenVideo)
+        .filter((item) => !item.hidden).length + 1
+    }`;
   }
   if (props.initData) {
     if (

+ 2 - 62
test/App.vue

@@ -1,67 +1,7 @@
 <template>
-  <div>
-    <video
-      ref="videoRef"
-      controls
-    ></video>
-    <button @click="load">load</button>
-  </div>
+  <div>1212</div>
 </template>
 
-<script lang="ts" setup>
-import { FFmpeg } from '@ffmpeg/ffmpeg';
-import { fetchFile, toBlobURL } from '@ffmpeg/util';
-import { ref } from 'vue';
-
-const ffmpegRef = new FFmpeg();
-const videoRef = ref();
-
-async function load() {
-  const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd';
-  const ffmpeg = ffmpegRef;
-  ffmpeg.on('log', ({ message }) => {
-    console.log(message);
-  });
-  // toBlobURL is used to bypass CORS issue, urls with the same
-  // domain can be used directly.
-  await ffmpeg.load({
-    coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
-    wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
-    workerURL: await toBlobURL(
-      `${baseURL}/ffmpeg-core.worker.js`,
-      'text/javascript'
-    ),
-  });
-  transcode();
-}
-
-async function transcode() {
-  await ffmpegRef.writeFile(
-    'input.mp4',
-    // await fetchFile('/mini-video.mp4')
-    // await fetchFile('/2024-02-25.mp4')
-    await fetchFile('/2024-02-25-10s.mp4')
-    // await fetchFile(
-    //   'https://raw.githubusercontent.com/ffmpegwasm/testdata/master/Big_Buck_Bunny_180_10s.webm'
-    // )
-  );
-  await ffmpegRef.exec([
-    '-i',
-    'input.mp4',
-    '-vf',
-    'scale=-1:1280',
-    '-r',
-    '20',
-    '-crf',
-    '23',
-    'output.mp4',
-  ]);
-  const data = await ffmpegRef.readFile('output.mp4');
-  console.log(data, 332322);
-  videoRef.value.src = URL.createObjectURL(
-    new Blob([data.buffer], { type: 'video/mp4' })
-  );
-}
-</script>
+<script lang="ts" setup></script>
 
 <style lang="scss" scoped></style>

+ 55 - 96
test/test.html

@@ -1,108 +1,67 @@
 <!doctype html>
 <html lang="en">
   <head>
-    <meta
-      name="renderer"
-      content="webkit"
-    />
-    <meta
-      http-equiv="X-UA-Compatible"
-      content="IE=Edge"
-    />
-    <meta charset="utf-8" />
+    <meta charset="UTF-8" />
     <meta
       name="viewport"
-      content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
-    />
-    <meta
-      name="description"
-      content=""
-    />
-    <meta
-      name="author"
-      content="ThemeBucket"
-    />
-    <link
-      rel="shortcut icon"
-      href="#"
-      type="image/png"
+      content="width=device-width, initial-scale=1.0"
     />
+    <title>Video to ReadableStream</title>
+  </head>
+  <body>
+    <video
+      id="video"
+      width="640"
+      height="480"
+      autoplay
+      loop
+      muted
+    >
+      <source
+        src="your-video.mp4"
+        type="video/mp4"
+      />
+    </video>
+    <script>
+      const video = document.getElementById('video');
 
-    <title>管理后台登陆</title>
-    <link
-      rel="stylesheet"
-      type="text/css"
-      href="/common/js/bootstrap/css/bootstrap.min.css"
-    />
-    <link
-      rel="stylesheet"
-      type="text/css"
-      href="/common/fonts/css/font-awesome.min.css"
-    />
-    <link
-      href="/assets/backend/css/style.css?v=2"
-      rel="stylesheet"
-    />
-    <link
-      href="/assets/backend/css/style-responsive.css"
-      rel="stylesheet"
-    />
+      async function videoToStream(videoElement) {
+        // 创建 MediaStream
+        const stream = videoElement.captureStream();
+        const reader = new MediaRecorder(stream);
 
-    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
-    <!--[if lt IE 9]>
-      <script src="/common/js/html5shiv.js"></script>
-      <script src="/common/js/respond.min.js"></script>
-    <![endif]-->
-  </head>
+        // 创建一个可读流
+        const readableStream = new ReadableStream({
+          start(controller) {
+            reader.ondataavailable = (event) => {
+              if (event.data.size > 0) {
+                const chunk = new Uint8Array();
+                const reader = new FileReader();
+                reader.onload = () => {
+                  // 将数据块添加到可读流
+                  controller.enqueue(new Uint8Array(reader.result));
+                };
+                reader.readAsArrayBuffer(event.data);
+              }
+            };
+            reader.onstop = () => {
+              controller.close();
+            };
+            // 开始录制
+            reader.start();
+          },
+          cancel() {
+            reader.stop();
+          },
+        });
 
-  <body class="login-body">
-    <div class="container">
-      <form
-        class="form-signin"
-        action="/admin/auth/login"
-        method="POST"
-      >
-        <div class="form-signin-heading text-center">
-          <img
-            style="max-height: 45px"
-            src="/assets/backend/images/logo.png?v=4"
-            alt=""
-          />
-        </div>
-        <div class="login-wrap">
-          <input
-            type="hidden"
-            name="_token"
-            value="k32pH8wdW5qsnnUo6nMYAk9eQWI9tq29OCw7e30b"
-          />
-          <input
-            type="hidden"
-            name="back"
-            value="https://vip.cneiie9.com/admin/user/ajaxuserlist?page=1"
-          />
-          <input
-            type="text"
-            name="login"
-            class="form-control"
-            placeholder="用户名"
-            value=""
-            autofocus
-          />
-          <input
-            type="password"
-            name="password"
-            class="form-control"
-            placeholder="密码"
-          />
+        return readableStream;
+      }
 
-          <button
-            class="btn btn-lg btn-login btn-block"
-            type="submit"
-          >
-            <i class="fa fa-check"></i>
-          </button>
-        </div>
-      </form>
-    </div>
+      video.addEventListener('loadeddata', async () => {
+        const stream = await videoToStream(video);
+        console.log(stream); // 你可以在这里使用这个流
+      });
+    </script>
   </body>
 </html>

+ 15 - 21
test/test.ts

@@ -1,21 +1,15 @@
-let a = {
-  srsPushRes: {
-    rtmp_url:
-      'rtmp://srs-push.hsslive.cn/livestream/roomId___101?roomid=101&pushtype=8&pushkey=iU2c3WWDfOgGFRrHT0xJSZlitgHZE1&userid=101',
-    obs_server: 'rtmp://srs-push.hsslive.cn/livestream/roomId___101',
-    obs_stream_key:
-      '?roomid=101&pushtype=2&pushkey=iU2c3WWDfOgGFRrHT0xJSZlitgHZE1&userid=101',
-    webrtc_url: '',
-    srt_url: '',
-  },
-  cdnPushRes: {
-    rtmp_url:
-      'rtmp://push.hsslive.cn/livestream/roomId___101?txSecret=d91001aded456341d48e08a70032e3bd&txTime=674E7079&roomid=101&pushtype=8&pushkey=iU2c3WWDfOgGFRrHT0xJSZlitgHZE1&userid=101',
-    obs_server: 'rtmp://push.hsslive.cn/livestream/',
-    obs_stream_key:
-      'roomId___101?txSecret=d91001aded456341d48e08a70032e3bd&txTime=674E7079&roomid=101&pushtype=8&pushkey=iU2c3WWDfOgGFRrHT0xJSZlitgHZE1&userid=101',
-    webrtc_url:
-      'webrtc://push.hsslive.cn/livestream/roomId___101?txSecret=d91001aded456341d48e08a70032e3bd&txTime=674E7079&roomid=101&pushtype=8&pushkey=iU2c3WWDfOgGFRrHT0xJSZlitgHZE1&userid=101',
-    srt_url: '',
-  },
-};
+enum pet {
+  a = 1,
+  b,
+}
+
+function test(d: pet) {
+  console.log(d.a, d['a'], 'sdsd');
+  if (1 === d.a) {
+    console.log('111');
+  } else {
+    console.log('222');
+  }
+}
+
+test(pet.a);