Kaynağa Gözat

feat: 上线canvas混流

shuisheng 2 yıl önce
ebeveyn
işleme
79913b0110

+ 2 - 1
package.json

@@ -57,7 +57,8 @@
     "vue": "^3.2.31",
     "vue-demi": "^0.13.11",
     "vue-router": "^4.0.13",
-    "webrtc-adapter": "^8.2.2"
+    "webrtc-adapter": "^8.2.2",
+    "worker-timers": "^7.0.74"
   },
   "devDependencies": {
     "@babel/core": "^7.14.0",

+ 51 - 5
pnpm-lock.yaml

@@ -83,6 +83,9 @@ dependencies:
   webrtc-adapter:
     specifier: ^8.2.2
     version: 8.2.2
+  worker-timers:
+    specifier: ^7.0.74
+    version: 7.0.74
 
 devDependencies:
   '@babel/core':
@@ -1311,6 +1314,13 @@ packages:
     dependencies:
       regenerator-runtime: 0.13.11
 
+  /@babel/runtime@7.22.6:
+    resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      regenerator-runtime: 0.13.11
+    dev: false
+
   /@babel/template@7.20.7:
     resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==}
     engines: {node: '>=6.9.0'}
@@ -4619,7 +4629,7 @@ packages:
     resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
     dependencies:
       no-case: 3.0.4
-      tslib: 2.5.0
+      tslib: 2.6.1
     dev: true
 
   /dot-prop@5.3.0:
@@ -5435,6 +5445,14 @@ packages:
     resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
     dev: true
 
+  /fast-unique-numbers@8.0.7:
+    resolution: {integrity: sha512-I+VCWGlHB6HSqE0W0FxB5mgmgBHJiBs19kS9y6JJKXDp84IzuE7H24NRwpnZbuONK7T2r+7T0z1OZbehc5URxA==}
+    engines: {node: '>=16.1.0'}
+    dependencies:
+      '@babel/runtime': 7.22.6
+      tslib: 2.6.1
+    dev: false
+
   /fastest-levenshtein@1.0.16:
     resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
     engines: {node: '>= 4.9.1'}
@@ -6926,7 +6944,7 @@ packages:
   /lower-case@2.0.2:
     resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
     dependencies:
-      tslib: 2.5.0
+      tslib: 2.6.1
     dev: true
 
   /lru-cache@5.1.1:
@@ -7308,7 +7326,7 @@ packages:
     resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
     dependencies:
       lower-case: 2.0.2
-      tslib: 2.5.0
+      tslib: 2.6.1
     dev: true
 
   /node-abort-controller@3.1.1:
@@ -7643,7 +7661,7 @@ packages:
     resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
     dependencies:
       no-case: 3.0.4
-      tslib: 2.5.0
+      tslib: 2.6.1
     dev: true
 
   /path-exists@3.0.0:
@@ -7730,7 +7748,7 @@ packages:
     resolution: {integrity: sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==}
     hasBin: true
     dependencies:
-      '@babel/runtime': 7.21.0
+      '@babel/runtime': 7.22.6
     dev: false
 
   /pkg-dir@4.2.0:
@@ -9725,6 +9743,9 @@ packages:
   /tslib@2.5.0:
     resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
 
+  /tslib@2.6.1:
+    resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==}
+
   /tsutils@3.21.0(typescript@4.9.5):
     resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
     engines: {node: '>= 6'}
@@ -10512,6 +10533,31 @@ packages:
     resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
     dev: true
 
+  /worker-timers-broker@6.0.94:
+    resolution: {integrity: sha512-LYo2uktzGgJW2lB/XHq0p8M3NKeC7fCaQ20o+WpmLTx0TnS/g3IkSFituBR7Fify/q1natgrJN7GUJKcgCFVmw==}
+    dependencies:
+      '@babel/runtime': 7.22.6
+      fast-unique-numbers: 8.0.7
+      tslib: 2.6.1
+      worker-timers-worker: 7.0.58
+    dev: false
+
+  /worker-timers-worker@7.0.58:
+    resolution: {integrity: sha512-tCZI7c5YgjZAS16BZNCjd5T+W+jEC+g9M74gqKVRRlZ13Y8DW3ZJR1fiXHzd9yRy9wK1PTOZOcSdDLvxNtUb8Q==}
+    dependencies:
+      '@babel/runtime': 7.22.6
+      tslib: 2.6.1
+    dev: false
+
+  /worker-timers@7.0.74:
+    resolution: {integrity: sha512-XdxJ9gTNdvM6FkStgXmjDbF/v9reoJzpLiq1YORcA1DdMYxWh+zdEgqxuoJu3rpM05CwrUsb4d03FCDfExNiQQ==}
+    dependencies:
+      '@babel/runtime': 7.22.6
+      tslib: 2.6.1
+      worker-timers-broker: 6.0.94
+      worker-timers-worker: 7.0.58
+    dev: false
+
   /wrap-ansi@6.2.0:
     resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
     engines: {node: '>=8'}

+ 1 - 1
src/assets/constant.scss

@@ -18,7 +18,7 @@ $w-1300: 1300px;
 $w-1275: 1275px;
 $w-1250: 1250px;
 $w-1200: 1200px;
-$w-1152: 1152px;
+$w-1150: 1150px;
 $w-1100: 1100px;
 $w-1000: 1000px;
 $w-960: 960px;

+ 5 - 2
src/hooks/use-play.ts

@@ -1,5 +1,4 @@
 import '@/assets/css/videojs.scss';
-import { isSafari } from 'billd-utils';
 import flvJs from 'flv.js';
 import videoJs from 'video.js';
 import Player from 'video.js/dist/types/player';
@@ -59,7 +58,11 @@ export function useFlvPlay() {
             console.log('flv-play');
           });
           flvVideoEl.value.addEventListener('playing', () => {
-            console.log('flv-playing', isSafari());
+            console.log(
+              'flv-playing',
+              flvVideoEl.value?.videoWidth,
+              flvVideoEl.value?.videoHeight
+            );
             // setMuted(false);
             setMuted(appStore.muted);
             resolve({

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

@@ -111,7 +111,6 @@ export function usePull({
   watch(
     () => roomLiveing.value,
     (val) => {
-      console.log(val, roomLiveType.value, '-------');
       if (val) {
         flvurl.value = val.live?.live_room?.flv_url!;
         hlsurl.value = val.live?.live_room?.hls_url!;

+ 5 - 5
src/hooks/use-ws.ts

@@ -554,11 +554,13 @@ export const useWs = () => {
         console.warn(
           'srs startNewWebRtc,pc插入track',
           track.id,
-          track.getSettings().height,
-          track.getSettings().width,
           localStream.value?.id
         );
-        console.log('pc添加track-2');
+        console.log(
+          'pc添加track-2',
+          track.kind,
+          canvasVideoStream.value?.getAudioTracks()
+        );
         rtc.peerConnection?.addTrack(track, localStream.value!);
       });
 
@@ -712,11 +714,9 @@ export const useWs = () => {
     ws.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IJoin) => {
       prettierReceiveWebsocket(WsMsgTypeEnum.roomLiveing, data);
       roomLiveing.value = data.data;
-      console.log(isSRS.value, isPull.value, data, 111);
       // 如果是srs开播,则不需要等有人进来了才new webrtc,只要Websocket连上了就开始new webrtc
       if (isSRS.value) {
         if (isPull.value) {
-          console.log('llllll');
           if (roomLiveType.value === liveTypeEnum.srsWebrtcPull) {
             startNewWebRtc({
               receiver: 'srs',

+ 23 - 4
src/views/home/index.vue

@@ -1,7 +1,14 @@
 <template>
   <div class="home-wrap">
     <div class="banner"></div>
+
     <div class="play-container">
+      <div
+        class="bg"
+        :style="{
+          // backgroundImage: `url(https://resource.hsslive.cn/image/2b045c7f02febd23893244e923115535.webp)`,
+        }"
+      ></div>
       <div class="container">
         <div class="left">
           <div
@@ -329,8 +336,23 @@ function joinHlsRoom() {
 
 <style lang="scss" scoped>
 .home-wrap {
+  .banner {
+  }
+
   .play-container {
-    background-color: papayawhip;
+    position: relative;
+    .bg {
+      position: absolute;
+      top: 0;
+      right: 0;
+      left: 0;
+      z-index: -1;
+      width: 100%;
+      height: 100%;
+      background-color: papayawhip;
+      background-position: center;
+      background-repeat: no-repeat;
+    }
     .container {
       display: flex;
       justify-content: space-between;
@@ -346,7 +368,6 @@ function joinHlsRoom() {
         height: 618px;
         border-radius: 4px;
         background-color: rgba($color: #000000, $alpha: 0.3);
-        vertical-align: top;
 
         @extend %coverBg;
 
@@ -444,12 +465,10 @@ function joinHlsRoom() {
         display: inline-block;
         overflow: scroll;
         box-sizing: border-box;
-        margin-left: 10px;
         padding: 12px;
         height: 618px;
         border-radius: 4px;
         background-color: rgba($color: #000000, $alpha: 0.3);
-        vertical-align: top;
 
         @extend %hideScrollbar;
 

+ 76 - 12
src/views/pushByCanvas/index.vue

@@ -231,8 +231,9 @@
 <script lang="ts" setup>
 import { fabric } from 'fabric';
 import { NODE_ENV } from 'script/constant';
-import { markRaw, onMounted, reactive, ref, watch } from 'vue';
+import { markRaw, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
+import * as workerTimers from 'worker-timers';
 
 import { usePush } from '@/hooks/use-push';
 import { DanmuMsgTypeEnum, MediaTypeEnum, liveTypeEnum } from '@/interface';
@@ -256,13 +257,17 @@ const containerRef = ref<HTMLDivElement>();
 const pushCanvasRef = ref<HTMLCanvasElement>();
 const fabricCanvas = ref<fabric.Canvas>();
 const localVideoRef = ref<HTMLVideoElement>();
+const webAudioTrack = ref<MediaStreamTrack>();
+const audioCtx = ref<AudioContext>();
 const remoteVideoRef = ref<HTMLVideoElement[]>([]);
 const isSRS = route.query.liveType === liveTypeEnum.srsPush;
 const wrapSize = reactive({
   width: 0,
   height: 0,
 });
+
 const scaleRatio = ref(0);
+const timerId = ref(-1);
 const videoRatio = ref(16 / 9);
 const {
   confirmRoomName,
@@ -304,9 +309,54 @@ watch(
   }
 );
 
+// 处理页面显示/隐藏
+function onPageVisibility() {
+  // 注意:此属性在Page Visibility Level 2 规范中被描述为“历史” 。考虑改用该Document.visibilityState 属性。
+  // const isHidden = document.hidden;
+
+  if (document.visibilityState === 'hidden') {
+    console.log(new Date().toLocaleString(), '页面隐藏了', timerId.value);
+    if (isLiving.value) {
+      const delay = 1000 / 60; // 16.666666666666668
+      timerId.value = workerTimers.setInterval(() => {
+        fabricCanvas.value?.renderAll();
+      }, delay);
+    }
+  } else {
+    console.log(new Date().toLocaleString(), '页面显示了', timerId.value);
+    if (isLiving.value) {
+      workerTimers.clearInterval(timerId.value);
+    }
+  }
+}
+
 function handleStartLive() {
   lastCoverImg.value = generateBase64(pushCanvasRef.value!);
+  // const video = createVideo({});
+  // document.body.appendChild(video);
+  audioCtx.value = new AudioContext();
+  const gainNode = audioCtx.value.createGain();
+
+  appStore.allTrack.forEach((item) => {
+    if (item.audio === 1) {
+      if (!audioCtx.value) return;
+      // const destination = audioCtx.value.createMediaStreamDestination();
+      const audioInput = audioCtx.value.createMediaStreamSource(item.stream);
+      // gainNode.connect(destination);
+      audioInput.connect(gainNode);
+    }
+  });
+  const destination = audioCtx.value.createMediaStreamDestination();
+  // video.srcObject = destination.stream;
+  gainNode.connect(destination);
+
+  // video.onloadeddata = () => {
+  // @ts-ignore
+  // webAudioTrack.value = video.srcObject!.getAudioTracks()[0];
+  // canvasVideoStream.value?.addTrack(webAudioTrack.value!);
+  canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
   startLive();
+  // };
 }
 
 function autoCreateVideo({ stream }: { stream: MediaStream }) {
@@ -349,10 +399,12 @@ function autoCreateVideo({ stream }: { stream: MediaStream }) {
       );
       dom.scale(ratio);
       fabricCanvas.value!.add(dom);
-      fabric.util.requestAnimFrame(function render() {
+      function renderFrame() {
         fabricCanvas.value?.renderAll();
-        fabric.util.requestAnimFrame(render);
-      });
+        window.requestAnimationFrame(renderFrame);
+      }
+
+      renderFrame();
 
       canvasVideoStream.value = pushCanvasRef.value!.captureStream();
       resolve(dom);
@@ -380,8 +432,21 @@ function initCanvas() {
 
 onMounted(() => {
   initCanvas();
+  document.addEventListener('visibilitychange', onPageVisibility);
+  handleAudioTrack();
+});
+
+onUnmounted(() => {
+  document.removeEventListener('visibilitychange', onPageVisibility);
 });
 
+function handleAudioTrack() {
+  audioCtx.value = new AudioContext();
+  // const destination = audioCtx.value.createMediaStreamDestination();
+  // const gainNode = audioCtx.value.createGain();
+  // gainNode.connect(destination);
+}
+
 function selectMediaOk(val: MediaTypeEnum) {
   showMediaModalCpt.value = true;
   showSelectMediaModalCpt.value = false;
@@ -462,8 +527,6 @@ async function addMediaOk(val: {
       trackid: event.getVideoTracks()[0].id,
       canvasDom: undefined,
     };
-    const video = createVideo({});
-    video.srcObject = event;
     const canvasDom = await autoCreateVideo({
       stream: event,
     });
@@ -481,10 +544,10 @@ async function addMediaOk(val: {
       isSRS &&
       appStore.allTrack.filter((item) => item.audio === 1).length >= 1
     ) {
-      window.$message.error('srs模式最多只能有一个音频');
-      return;
+      // window.$message.error('srs模式最多只能有一个音频');
+      // return;
     }
-    const track = {
+    const audioTrack = {
       id: getRandomEnglishString(8),
       audio: 1,
       video: 2,
@@ -495,8 +558,9 @@ async function addMediaOk(val: {
       streamid: event.id,
       trackid: event.getAudioTracks()[0].id,
     };
-    appStore.setAllTrack([...appStore.allTrack, track]);
-    addTrack(track);
+    appStore.setAllTrack([...appStore.allTrack, audioTrack]);
+    addTrack(audioTrack);
+
     console.log('获取麦克风成功');
   }
 }
@@ -737,7 +801,7 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
   .push-wrap {
     width: $w-1475;
     .left {
-      width: $w-1152;
+      width: $w-1150;
     }
     .right {
       width: $w-300;