Jelajahi Sumber

feat: 完善canvas混流

shuisheng 2 tahun lalu
induk
melakukan
d094650a46

+ 17 - 0
remark.md

@@ -0,0 +1,17 @@
+```html
+<!-- x-webkit-airplay这个属性应该是使此视频支持ios的AirPlay功能 -->
+<!-- playsinline、 webkit-playsinline IOS微信浏览器支持小窗内播放 -->
+<!-- x5-video-player-type 启用H5播放器,是wechat安卓版特性 -->
+<!-- x5-video-player-fullscreen 全屏设置 -->
+<!-- x5-video-orientation 声明播放器支持的方向,可选值landscape横屏,portraint竖屏。默认值portraint。 -->
+<video
+  autoplay
+  webkit-playsinline="true"
+  playsinline
+  x-webkit-airplay="allow"
+  x5-video-player-type="h5"
+  x5-video-player-fullscreen="true"
+  x5-video-orientation="portraint"
+  muted
+></video>
+```

+ 2 - 2
src/assets/constant.scss

@@ -18,10 +18,10 @@ $w-1300: 1300px;
 $w-1275: 1275px;
 $w-1250: 1250px;
 $w-1200: 1200px;
-$w-1150: 1150px;
+$w-1152: 1152px;
 $w-1100: 1100px;
 $w-1000: 1000px;
-$w-950: 950px;
+$w-960: 960px;
 
 $w-350: 350px;
 $w-300: 300px;

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

@@ -16,7 +16,7 @@ import { WsMsgTypeEnum } from '@/network/webSocket';
 import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
-import { createVideo } from '@/utils';
+import { createVideo, generateBase64 } from '@/utils';
 
 import { loginTip } from './use-login';
 import { useTip } from './use-tip';
@@ -63,7 +63,6 @@ export function usePush({
   const {
     getSocketId,
     initWs,
-    fabricCanvasEl,
     canvasVideoStream,
     lastCoverImg,
     heartbeatTimer,
@@ -133,20 +132,6 @@ export function usePush({
     closeRtc();
   });
 
-  function handleCoverImg(dom: HTMLVideoElement) {
-    const canvas = document.createElement('canvas');
-    const { width, height } = dom.getBoundingClientRect();
-    const rate = width / height;
-    const coverWidth = width * 0.5;
-    const coverHeight = coverWidth / rate;
-    canvas.width = coverWidth;
-    canvas.height = coverHeight;
-    canvas.getContext('2d')!.drawImage(dom, 0, 0, coverWidth, coverHeight);
-    // webp比png的体积小非常多!因此coverWidth就可以不用压缩太夸张
-    const dataURL = canvas.toDataURL('image/webp');
-    return dataURL;
-  }
-
   function closeWs() {
     const instance = networkStore.wsMap.get(roomId.value);
     instance?.close();
@@ -207,7 +192,10 @@ export function usePush({
         (item) => item.getAttribute('track-id') === el.track.id
       );
       if (res1) {
-        lastCoverImg.value = handleCoverImg(res1);
+        // canvas推流的话,不需要再设置预览图了
+        if (!canvasVideoStream.value) {
+          lastCoverImg.value = generateBase64(res1);
+        }
       }
     }
     initWs({
@@ -300,8 +288,8 @@ export function usePush({
     endLive,
     sendDanmu,
     keydownDanmu,
+    lastCoverImg,
     localStream,
-    fabricCanvasEl,
     canvasVideoStream,
     isLiving,
     allMediaTypeList,

+ 31 - 62
src/hooks/use-ws.ts

@@ -49,7 +49,6 @@ export const useWs = () => {
   const trackInfo = reactive({ track_audio: 1, track_video: 1 });
   const localVideo = ref<HTMLVideoElement>(document.createElement('video'));
   const localStream = ref<MediaStream>();
-  const fabricCanvasEl = ref<HTMLVideoElement>();
   const canvasVideoStream = ref<MediaStream>();
   const lastCoverImg = ref('');
   const maxBitrate = ref([
@@ -61,10 +60,6 @@ export const useWs = () => {
       label: '10',
       value: 10,
     },
-    {
-      label: '50',
-      value: 50,
-    },
     {
       label: '1000',
       value: 1000,
@@ -119,10 +114,6 @@ export const useWs = () => {
       label: '20帧',
       value: 20,
     },
-    {
-      label: '24帧',
-      value: 24,
-    },
     {
       label: '30帧',
       value: 30,
@@ -133,14 +124,6 @@ export const useWs = () => {
     },
   ]);
   const resolutionRatio = ref([
-    {
-      label: '160P',
-      value: 160,
-    },
-    {
-      label: '240P',
-      value: 240,
-    },
     {
       label: '360P',
       value: 360,
@@ -153,9 +136,13 @@ export const useWs = () => {
       label: '1080P',
       value: 1080,
     },
+    // {
+    //   label: '1440P',
+    //   value: 1440,
+    // },
   ]);
-  const currentMaxBitrate = ref(maxBitrate.value[4].value);
-  const currentResolutionRatio = ref(resolutionRatio.value[4].value);
+  const currentMaxBitrate = ref(maxBitrate.value[2].value);
+  const currentResolutionRatio = ref(resolutionRatio.value[2].value);
   const currentMaxFramerate = ref(maxFramerate.value[2].value);
 
   const damuList = ref<IDanmu[]>([]);
@@ -164,21 +151,15 @@ export const useWs = () => {
     () => appStore.allTrack,
     (newTrack, oldTrack) => {
       console.log('appStore.allTrack变了');
-      if (fabricCanvasEl.value) {
-        // @ts-ignore
-        const mixedStream = fabricCanvasEl.value.captureStream();
-        localStream.value = mixedStream;
-      } else {
-        const mixedStream = new MediaStream();
-        newTrack.forEach((item) => {
-          mixedStream.addTrack(item.track);
-        });
-        console.log('新的allTrack音频轨', mixedStream.getAudioTracks());
-        console.log('新的allTrack视频轨', mixedStream.getVideoTracks());
-        console.log('旧的allTrack音频轨', localStream.value?.getAudioTracks());
-        console.log('旧的allTrack视频轨', localStream.value?.getVideoTracks());
-        localStream.value = mixedStream;
-      }
+      const mixedStream = new MediaStream();
+      newTrack.forEach((item) => {
+        mixedStream.addTrack(item.track);
+      });
+      console.log('新的allTrack音频轨', mixedStream.getAudioTracks());
+      console.log('新的allTrack视频轨', mixedStream.getVideoTracks());
+      console.log('旧的allTrack音频轨', localStream.value?.getAudioTracks());
+      console.log('旧的allTrack视频轨', localStream.value?.getVideoTracks());
+      localStream.value = mixedStream;
       if (isSRS.value) {
         if (!isPull.value) {
           networkStore.rtcMap.forEach((rtc) => {
@@ -197,22 +178,21 @@ export const useWs = () => {
   watch(
     () => currentResolutionRatio.value,
     (newVal) => {
-      // if (canvasVideoStream.value) {
-      // canvasVideoStream.value.getVideoTracks().forEach((track) => {
-      //   console.log('23ds1', track);
-      //   track.applyConstraints({
-      //     frameRate: { max: currentMaxFramerate.value },
-      //     height: newVal,
-      //   });
-      // });
-      // } else {
-      appStore.allTrack.forEach((info) => {
-        info.track.applyConstraints({
-          frameRate: { max: currentMaxFramerate.value },
-          height: newVal,
+      if (canvasVideoStream.value) {
+        canvasVideoStream.value.getVideoTracks().forEach((track) => {
+          track.applyConstraints({
+            frameRate: { max: currentMaxFramerate.value },
+            height: newVal,
+          });
         });
-      });
-      // }
+      } else {
+        appStore.allTrack.forEach((info) => {
+          info.track.applyConstraints({
+            frameRate: { max: currentMaxFramerate.value },
+            height: newVal,
+          });
+        });
+      }
 
       networkStore.rtcMap.forEach(async (rtc) => {
         const res = await rtc.setResolutionRatio(newVal);
@@ -566,18 +546,8 @@ export const useWs = () => {
       //   roomId: `${roomId.value}___${receiver}`,
       //   isSRS: true,
       // });
-      if (fabricCanvasEl.value) {
-        // @ts-ignore
-        const mixedStream = fabricCanvasEl.value.captureStream();
-        localStream.value?.getTracks().forEach((track) => {
-          console.log(track.id, 322312112);
-        });
-        localStream.value = mixedStream;
-        setInterval(() => {
-          localStream.value?.getTracks().forEach((track) => {
-            console.log(track.id, 322312112);
-          });
-        }, 1000);
+      if (canvasVideoStream.value) {
+        localStream.value = canvasVideoStream.value;
       }
       rtc.localStream = localStream.value;
       localStream.value?.getTracks().forEach((track) => {
@@ -895,7 +865,6 @@ export const useWs = () => {
     initWs,
     addTrack,
     delTrack,
-    fabricCanvasEl,
     canvasVideoStream,
     lastCoverImg,
     roomLiveing,

+ 2 - 2
src/layout/pc/sidebar/index.vue

@@ -34,7 +34,7 @@ import router, { routerName } from '@/router';
   top: 50%;
   right: 0;
   padding: 15px 10px;
-  width: 40px;
+  width: 50px;
   border-radius: 20px 0 0 20px;
   background-color: white;
   box-shadow: 0 0 20px 1px rgba($theme-color-gold, 0.15);
@@ -42,10 +42,10 @@ import router, { routerName } from '@/router';
   text-align: center;
   transform: translateY(-50%);
   .item {
+    cursor: pointer;
     &:not(:last-child) {
       margin-bottom: 10px;
     }
-    cursor: pointer;
     .ico {
       margin: 0 auto;
       width: 20px;

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

@@ -18,6 +18,7 @@ export type AppRootState = {
     stream: MediaStream;
     streamid: string;
     trackid: string;
+    canvasDom?: any;
   }[];
 };
 

+ 23 - 0
src/utils/index.ts

@@ -2,6 +2,29 @@
 
 import { getRangeRandom } from 'billd-utils';
 
+export function generateBase64(dom: CanvasImageSource) {
+  const canvas = document.createElement('canvas');
+  // @ts-ignore
+  const { width, height } = dom.getBoundingClientRect();
+  const rate = width / height;
+  let ratio = 0.5;
+  function geturl() {
+    const coverWidth = width * ratio;
+    const coverHeight = coverWidth / rate;
+    canvas.width = coverWidth;
+    canvas.height = coverHeight;
+    canvas.getContext('2d')!.drawImage(dom, 0, 0, coverWidth, coverHeight);
+    // webp比png的体积小非常多!因此coverWidth就可以不用压缩太夸张
+    return canvas.toDataURL('image/webp');
+  }
+  let dataURL = geturl();
+  while (dataURL.length > 1000 * 20) {
+    ratio = ratio * 0.8;
+    dataURL = geturl();
+  }
+  return dataURL;
+}
+
 /**
  * @description 获取随机字符串(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz)
  * @example: getRandomString(4) ===> abd3

+ 1 - 1
src/views/area/index.vue

@@ -97,7 +97,7 @@ async function getData() {
 
 <style lang="scss" scoped>
 .area-wrap {
-  padding: 20px;
+  padding: 15px 20px;
   .title {
     margin-bottom: 10px;
   }

+ 294 - 287
src/views/home/index.vue

@@ -2,111 +2,116 @@
   <div class="home-wrap">
     <div class="banner"></div>
     <div class="play-container">
-      <div class="left">
-        <div
-          v-if="currentLiveRoom?.live_room?.cdn === 1"
-          class="cdn-ico"
-        >
-          <div class="txt">CDN</div>
-        </div>
-        <div
-          class="cover"
-          :style="{
-            backgroundImage: `url(${
-              currentLiveRoom?.live_room?.cover_img ||
-              currentLiveRoom?.user?.avatar
-            })`,
-          }"
-        ></div>
-        <div
-          v-if="currentLiveRoom?.live_room?.flv_url"
-          ref="canvasRef"
-        ></div>
-        <template v-if="currentLiveRoom">
-          <VideoControls></VideoControls>
+      <div class="container">
+        <div class="left">
           <div
-            class="join-btn"
-            :style="{
-              display: !isMobile() ? 'none' : showControls ? 'block' : 'none',
-            }"
+            v-if="currentLiveRoom?.live_room?.cdn === 1"
+            class="cdn-ico"
           >
-            <div
-              v-if="
-                currentLiveRoom.live_room?.type === LiveRoomTypeEnum.user_wertc
-              "
-              class="btn webrtc"
-              @click="joinRoom()"
-            >
-              进入直播(webrtc)
-            </div>
-            <div
-              v-if="
-                currentLiveRoom.live_room?.type === LiveRoomTypeEnum.user_srs
-              "
-              class="btn webrtc"
-              @click="joinRoom()"
-            >
-              进入直播(srs-webrtc)
-            </div>
-            <div
-              v-if="
-                currentLiveRoom.live_room?.type !== LiveRoomTypeEnum.user_wertc
-              "
-              class="btn flv"
-              @click="joinFlvRoom()"
-            >
-              进入直播(flv)
-            </div>
-            <div
-              v-if="
-                currentLiveRoom.live_room?.type !== LiveRoomTypeEnum.user_wertc
-              "
-              class="btn hls"
-              @click="joinHlsRoom()"
-            >
-              进入直播(hls)
-            </div>
+            <div class="txt">CDN</div>
           </div>
-        </template>
-      </div>
-      <div class="right">
-        <div
-          v-if="topLiveRoomList.length"
-          class="list"
-        >
           <div
-            v-for="(item, index) in topLiveRoomList"
-            :key="index"
-            :class="{
-              item: 1,
-              active: item.live_room_id === currentLiveRoom?.live_room_id,
-            }"
+            class="cover"
             :style="{
               backgroundImage: `url(${
-                item.live_room?.cover_img || item?.user?.avatar
+                currentLiveRoom?.live_room?.cover_img ||
+                currentLiveRoom?.user?.avatar
               })`,
             }"
-            @click="changeLiveRoom(item)"
-          >
+          ></div>
+          <div
+            v-if="currentLiveRoom?.live_room?.flv_url"
+            ref="canvasRef"
+          ></div>
+          <template v-if="currentLiveRoom">
+            <VideoControls></VideoControls>
             <div
-              class="border"
+              class="join-btn"
               :style="{
-                opacity:
-                  item.live_room_id === currentLiveRoom?.live_room_id ? 1 : 0,
+                display: !isMobile() ? 'none' : showControls ? 'block' : 'none',
               }"
-            ></div>
+            >
+              <div
+                v-if="
+                  currentLiveRoom.live_room?.type ===
+                  LiveRoomTypeEnum.user_wertc
+                "
+                class="btn webrtc"
+                @click="joinRoom()"
+              >
+                进入直播(webrtc)
+              </div>
+              <div
+                v-if="
+                  currentLiveRoom.live_room?.type === LiveRoomTypeEnum.user_srs
+                "
+                class="btn webrtc"
+                @click="joinRoom()"
+              >
+                进入直播(srs-webrtc)
+              </div>
+              <div
+                v-if="
+                  currentLiveRoom.live_room?.type !==
+                  LiveRoomTypeEnum.user_wertc
+                "
+                class="btn flv"
+                @click="joinFlvRoom()"
+              >
+                进入直播(flv)
+              </div>
+              <div
+                v-if="
+                  currentLiveRoom.live_room?.type !==
+                  LiveRoomTypeEnum.user_wertc
+                "
+                class="btn hls"
+                @click="joinHlsRoom()"
+              >
+                进入直播(hls)
+              </div>
+            </div>
+          </template>
+        </div>
+        <div class="right">
+          <div
+            v-if="topLiveRoomList.length"
+            class="list"
+          >
             <div
-              v-if="item.live_room_id === currentLiveRoom?.live_room_id"
-              class="triangle"
-            ></div>
-            <div class="txt">{{ item.live_room?.name }}</div>
+              v-for="(item, index) in topLiveRoomList"
+              :key="index"
+              :class="{
+                item: 1,
+                active: item.live_room_id === currentLiveRoom?.live_room_id,
+              }"
+              :style="{
+                backgroundImage: `url(${
+                  item.live_room?.cover_img || item?.user?.avatar
+                })`,
+              }"
+              @click="changeLiveRoom(item)"
+            >
+              <div
+                class="border"
+                :style="{
+                  opacity:
+                    item.live_room_id === currentLiveRoom?.live_room_id ? 1 : 0,
+                }"
+              ></div>
+              <div
+                v-if="item.live_room_id === currentLiveRoom?.live_room_id"
+                class="triangle"
+              ></div>
+              <div class="txt">{{ item.live_room?.name }}</div>
+            </div>
+          </div>
+          <div
+            v-else
+            class="none"
+          >
+            当前没有在线的直播间
           </div>
-        </div>
-        <div
-          v-else
-          class="none"
-        >
-          当前没有在线的直播间
         </div>
       </div>
     </div>
@@ -324,217 +329,218 @@ function joinHlsRoom() {
 
 <style lang="scss" scoped>
 .home-wrap {
-  background-color: papayawhip;
-
   .play-container {
-    padding: 20px 0;
-    width: $w-1475;
-    margin: 0 auto;
-    text-align: center;
-    white-space: nowrap;
-    &.area {
-      text-align: initial;
-    }
-    .left {
-      position: relative;
-      display: inline-block;
-      overflow: hidden;
-      box-sizing: border-box;
-      width: $w-1200;
-      height: 610px;
-      border-radius: 4px;
-      background-color: rgba($color: #000000, $alpha: 0.3);
-      vertical-align: top;
-
-      @extend %coverBg;
-
-      .cdn-ico {
-        position: absolute;
-        top: -9px;
-        right: -10px;
-        z-index: 2;
-        width: 70px;
-        height: 32px;
-        background-color: #f87c48;
-        color: white;
-        transform: rotate(45deg);
-        transform-origin: bottom;
-        .txt {
-          margin-top: 11px;
-          margin-left: 2px;
-          background-image: initial !important;
-          font-size: 14px;
+    background-color: papayawhip;
+    .container {
+      display: flex;
+      justify-content: space-between;
+      margin: 0 auto;
+      padding: 15px 0;
+      width: $w-1350;
+      .left {
+        position: relative;
+        display: inline-block;
+        overflow: hidden;
+        box-sizing: border-box;
+        width: $w-1100;
+        height: 618px;
+        border-radius: 4px;
+        background-color: rgba($color: #000000, $alpha: 0.3);
+        vertical-align: top;
+
+        @extend %coverBg;
+
+        .cdn-ico {
+          position: absolute;
+          top: -9px;
+          right: -10px;
+          z-index: 2;
+          width: 70px;
+          height: 32px;
+          background-color: #f87c48;
+          color: white;
+          transform: rotate(45deg);
+          transform-origin: bottom;
+          .txt {
+            margin-top: 11px;
+            margin-left: 2px;
+            background-image: initial !important;
+            font-size: 14px;
+          }
         }
-      }
 
-      .cover {
-        position: absolute;
-        background-position: center center;
-        background-size: cover;
-        filter: blur(10px);
-
-        inset: 0;
-      }
-      :deep(canvas) {
-        position: absolute;
-        top: 0;
-        left: 50%;
-        height: 100%;
-        transform: translate(-50%);
+        .cover {
+          position: absolute;
+          background-position: center center;
+          background-size: cover;
+          filter: blur(10px);
 
-        user-select: none;
-      }
-      :deep(video) {
-        position: absolute;
-        top: 0;
-        left: 50%;
-        height: 100%;
-        transform: translate(-50%);
+          inset: 0;
+        }
+        :deep(canvas) {
+          position: absolute;
+          top: 0;
+          left: 50%;
+          width: 100%;
+          height: 100%;
+          transform: translate(-50%);
 
-        user-select: none;
-      }
-      .controls {
-        display: none;
-      }
+          user-select: none;
+        }
+        :deep(video) {
+          position: absolute;
+          top: 0;
+          left: 50%;
+          width: 100%;
+          height: 100%;
+          transform: translate(-50%);
 
-      &:hover {
-        .join-btn {
-          display: inline-flex !important;
+          user-select: none;
+        }
+        .controls {
+          display: none;
         }
-      }
-      .join-btn {
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        z-index: 1;
-        display: none;
-        align-items: center;
-        align-items: center;
-        justify-content: center;
-        box-sizing: border-box;
-        width: 80%;
-        transform: translate(-50%, -50%);
 
-        .btn {
-          padding: 14px 26px;
-          border: 2px solid rgba($color: papayawhip, $alpha: 0.5);
-          border-radius: 6px;
-          background-color: rgba(0, 0, 0, 0.3);
-          color: papayawhip;
-          font-size: 16px;
-          cursor: pointer;
-          &:hover {
-            background-color: rgba($color: papayawhip, $alpha: 0.5);
-            color: white;
-          }
-          &.webrtc {
-            margin-right: 10px;
+        &:hover {
+          .join-btn {
+            display: inline-flex !important;
           }
-          &.flv {
-            margin-right: 10px;
+        }
+        .join-btn {
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          z-index: 1;
+          display: none;
+          align-items: center;
+          align-items: center;
+          justify-content: center;
+          box-sizing: border-box;
+          width: 80%;
+          transform: translate(-50%, -50%);
+
+          .btn {
+            padding: 14px 26px;
+            border: 2px solid rgba($color: papayawhip, $alpha: 0.5);
+            border-radius: 6px;
+            background-color: rgba(0, 0, 0, 0.3);
+            color: papayawhip;
+            font-size: 16px;
+            cursor: pointer;
+            &:hover {
+              background-color: rgba($color: papayawhip, $alpha: 0.5);
+              color: white;
+            }
+            &.webrtc {
+              margin-right: 10px;
+            }
+            &.flv {
+              margin-right: 10px;
+            }
           }
         }
       }
-    }
-    .right {
-      display: inline-block;
-      overflow: scroll;
-      box-sizing: border-box;
-      margin-left: 10px;
-      padding: 12px;
-      height: 610px;
-      border-radius: 4px;
-      background-color: rgba($color: #000000, $alpha: 0.3);
-      vertical-align: top;
-
-      @extend %hideScrollbar;
-
-      .list {
-        .item {
-          position: relative;
-          box-sizing: border-box;
-          margin-bottom: 10px;
-          width: 200px;
-          height: 110px;
-          border-radius: 4px;
-          background-color: rgba($color: #000000, $alpha: 0.3);
-          cursor: pointer;
+      .right {
+        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;
+
+        .list {
+          .item {
+            position: relative;
+            box-sizing: border-box;
+            margin-bottom: 10px;
+            width: 200px;
+            height: 110px;
+            border-radius: 4px;
+            background-color: rgba($color: #000000, $alpha: 0.3);
+            cursor: pointer;
 
-          @extend %coverBg;
+            @extend %coverBg;
 
-          &:last-child {
-            margin-bottom: 0;
-          }
-          .border {
-            position: absolute;
-            top: 0;
-            right: 0;
-            bottom: 0;
-            left: 0;
-            z-index: 1;
-            border: 2px solid papayawhip;
-            border-radius: 4px;
-          }
-          .triangle {
-            position: absolute;
-            top: 50%;
-            left: 0;
-            display: inline-block;
-            border: 5px solid transparent;
-            border-right-color: papayawhip;
-            transform: translate(-100%, -50%);
-          }
-          &.active {
-            &::before {
-              background-color: transparent;
+            &:last-child {
+              margin-bottom: 0;
+            }
+            .border {
+              position: absolute;
+              top: 0;
+              right: 0;
+              bottom: 0;
+              left: 0;
+              z-index: 1;
+              border: 2px solid papayawhip;
+              border-radius: 4px;
+            }
+            .triangle {
+              position: absolute;
+              top: 50%;
+              left: 0;
+              display: inline-block;
+              border: 5px solid transparent;
+              border-right-color: papayawhip;
+              transform: translate(-100%, -50%);
+            }
+            &.active {
+              &::before {
+                background-color: transparent;
+              }
+            }
+            &:hover {
+              &::before {
+                background-color: transparent;
+              }
             }
-          }
-          &:hover {
             &::before {
-              background-color: transparent;
+              position: absolute;
+              display: block;
+              width: 100%;
+              height: 100%;
+              border-radius: 4px;
+              background-color: rgba(0, 0, 0, 0.4);
+              content: '';
+              transition: all cubic-bezier(0.22, 0.58, 0.12, 0.98) 0.4s;
             }
-          }
-          &::before {
-            position: absolute;
-            display: block;
-            width: 100%;
-            height: 100%;
-            border-radius: 4px;
-            background-color: rgba(0, 0, 0, 0.4);
-            content: '';
-            transition: all cubic-bezier(0.22, 0.58, 0.12, 0.98) 0.4s;
-          }
-          .txt {
-            position: absolute;
-            bottom: 0;
-            left: 0;
-            box-sizing: border-box;
-            padding: 4px 8px;
-            width: 100%;
-            border-radius: 0 0 4px 4px;
-            background-image: linear-gradient(
-              -180deg,
-              rgba(0, 0, 0, 0),
-              rgba(0, 0, 0, 0.6)
-            );
-            color: white;
-            text-align: initial;
-            font-size: 13px;
+            .txt {
+              position: absolute;
+              bottom: 0;
+              left: 0;
+              box-sizing: border-box;
+              padding: 4px 8px;
+              width: 100%;
+              border-radius: 0 0 4px 4px;
+              background-image: linear-gradient(
+                -180deg,
+                rgba(0, 0, 0, 0),
+                rgba(0, 0, 0, 0.6)
+              );
+              color: white;
+              text-align: initial;
+              font-size: 13px;
 
-            @extend %singleEllipsis;
+              @extend %singleEllipsis;
+            }
           }
         }
-      }
-      .none {
-        width: 200px;
-        color: white;
-        font-size: 14px;
+        .none {
+          width: 200px;
+          color: white;
+          text-align: center;
+          font-size: 14px;
+        }
       }
     }
   }
   .area-container {
     margin: 10px auto;
-    width: $w-1475;
+    width: $w-1350;
     .area-item {
       .title {
         padding: 10px 0;
@@ -612,18 +618,19 @@ function joinHlsRoom() {
 @media screen and (min-width: $w-1500) {
   .home-wrap {
     .play-container {
-      width: $w-1475;
-
-      .left {
-        width: $w-1200;
-        height: 460px;
-      }
-      .right {
-        height: 460px;
+      .container {
+        width: $w-1350;
+        .left {
+          width: $w-1100;
+          height: 618px;
+        }
+        .right {
+          height: 618px;
+        }
       }
     }
     .area-container {
-      width: $w-1475;
+      width: $w-1350;
     }
   }
 }

+ 16 - 58
src/views/pull/index.vue

@@ -28,7 +28,6 @@
         <div
           ref="containerRef"
           class="container"
-          :style="{ height: height + 'px' }"
         >
           <div
             v-loading="videoLoading"
@@ -48,48 +47,10 @@
               ref="canvasRef"
               class="media-list"
               :class="{ item: appStore.allTrack.length > 1 }"
-              :style="{ height: height + 'px' }"
             ></div>
             <AudioRoomTip></AudioRoomTip>
             <VideoControls></VideoControls>
           </div>
-
-          <div
-            v-if="showSidebar"
-            class="sidebar"
-          >
-            <div
-              v-for="(item, index) in sidebarList"
-              :key="index"
-              class="item"
-            >
-              <!-- x-webkit-airplay这个属性应该是使此视频支持ios的AirPlay功能 -->
-              <!-- playsinline、 webkit-playsinline IOS微信浏览器支持小窗内播放 -->
-              <!-- x5-video-player-type 启用H5播放器,是wechat安卓版特性 -->
-              <!-- x5-video-player-fullscreen 全屏设置 -->
-              <!-- x5-video-orientation 声明播放器支持的方向,可选值landscape横屏,portraint竖屏。默认值portraint。 -->
-              <video
-                :ref="(el) => (localVideoRef[item.socketId] = el)"
-                autoplay
-                webkit-playsinline="true"
-                playsinline
-                x-webkit-airplay="allow"
-                x5-video-player-type="h5"
-                x5-video-player-fullscreen="true"
-                x5-video-orientation="portraint"
-                muted
-              ></video>
-              <div class="name">{{ item.socketId }}</div>
-            </div>
-
-            <div
-              v-if="showJoin"
-              class="join"
-              @click="handleJoin()"
-            >
-              加入
-            </div>
-          </div>
         </div>
 
         <div
@@ -350,9 +311,10 @@ onMounted(() => {
 
 <style lang="scss" scoped>
 .pull-wrap {
-  margin: 20px auto 0;
+  display: flex;
+  justify-content: space-around;
+  margin: 15px auto 0;
   width: $w-1275;
-  height: 700px;
   .left {
     position: relative;
     display: inline-block;
@@ -376,8 +338,8 @@ onMounted(() => {
 
         .avatar {
           margin-right: 20px;
-          width: 64px;
-          height: 64px;
+          width: 50px;
+          height: 50px;
           border-radius: 50%;
 
           @extend %containBg;
@@ -422,6 +384,7 @@ onMounted(() => {
       display: flex;
       align-items: center;
       justify-content: space-between;
+      height: 562px;
       .video-wrap {
         position: relative;
         overflow: hidden;
@@ -502,23 +465,20 @@ onMounted(() => {
     }
 
     .gift-list {
-      position: absolute;
-      right: 0;
-      bottom: 0;
-      left: 0;
       display: flex;
       align-items: center;
       justify-content: space-around;
       box-sizing: border-box;
-      height: 120px;
+      height: 100px;
+      margin: 5px 0;
       .item {
         display: flex;
         align-items: center;
+        width: 100px;
+        height: 100px;
         flex-direction: column;
         justify-content: center;
         box-sizing: border-box;
-        width: 110px;
-        height: 110px;
         text-align: center;
         cursor: pointer;
         &:hover {
@@ -526,8 +486,8 @@ onMounted(() => {
         }
         .ico {
           position: relative;
-          width: 50px;
-          height: 50px;
+          width: 45px;
+          height: 45px;
           background-position: center center;
           background-size: cover;
           background-repeat: no-repeat;
@@ -568,9 +528,7 @@ onMounted(() => {
     position: relative;
     display: inline-block;
     box-sizing: border-box;
-    margin-left: 10px;
     width: $w-250;
-    height: 100%;
     border-radius: 6px;
     background-color: papayawhip;
     color: #9499a0;
@@ -614,7 +572,7 @@ onMounted(() => {
     .danmu-list {
       overflow-y: scroll;
       padding: 0 15px;
-      height: 450px;
+      height: 480px;
       text-align: initial;
 
       @extend %hideScrollbar;
@@ -671,13 +629,13 @@ onMounted(() => {
 // 屏幕宽度大于1500的时候
 @media screen and (min-width: $w-1500) {
   .pull-wrap {
-    width: $w-1475;
+    width: $w-1350;
 
     .left {
-      width: $w-1200;
+      width: $w-1000;
     }
     .right {
-      width: $w-250;
+      width: $w-300;
     }
   }
 }

+ 91 - 103
src/views/pushByCanvas/index.vue

@@ -9,7 +9,6 @@
         class="container"
       >
         <AudioRoomTip></AudioRoomTip>
-        <!-- <div id="canvasRef"></div> -->
         <canvas
           id="pushCanvasRef"
           ref="pushCanvasRef"
@@ -60,7 +59,7 @@
             </div>
             <div class="bottom">
               <span v-if="NODE_ENV === 'development'">
-                socketId:{{ getSocketId() }}
+                {{ getSocketId() }}
               </span>
             </div>
           </div>
@@ -112,7 +111,7 @@
               v-if="!isLiving"
               type="info"
               size="small"
-              @click="startLive"
+              @click="handleStartLive"
             >
               开始直播
             </n-button>
@@ -232,14 +231,14 @@
 <script lang="ts" setup>
 import { fabric } from 'fabric';
 import { NODE_ENV } from 'script/constant';
-import { markRaw, nextTick, onMounted, reactive, ref, watch } from 'vue';
+import { markRaw, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { usePush } from '@/hooks/use-push';
 import { DanmuMsgTypeEnum, MediaTypeEnum, liveTypeEnum } from '@/interface';
 import { AppRootState, useAppStore } from '@/store/app';
 import { useUserStore } from '@/store/user';
-import { createVideo, getRandomEnglishString } from '@/utils';
+import { createVideo, generateBase64, getRandomEnglishString } from '@/utils';
 
 import MediaModalCpt from './mediaModal/index.vue';
 import SelectMediaModalCpt from './selectMediaModal/index.vue';
@@ -265,7 +264,6 @@ const wrapSize = reactive({
 });
 const scaleRatio = ref(0);
 const videoRatio = ref(16 / 9);
-const canvasVideo = ref<HTMLVideoElement>();
 const {
   confirmRoomName,
   getSocketId,
@@ -273,8 +271,7 @@ const {
   endLive,
   sendDanmu,
   keydownDanmu,
-  localStream,
-  fabricCanvasEl,
+  lastCoverImg,
   canvasVideoStream,
   isLiving,
   allMediaTypeList,
@@ -307,7 +304,12 @@ watch(
   }
 );
 
-function createAutoVideo({ stream, id }: { stream: MediaStream; id }) {
+function handleStartLive() {
+  lastCoverImg.value = generateBase64(pushCanvasRef.value!);
+  startLive();
+}
+
+function autoCreateVideo({ stream }: { stream: MediaStream }) {
   const video = createVideo({});
   video.srcObject = stream;
   video.style.width = `1px`;
@@ -318,55 +320,44 @@ function createAutoVideo({ stream, id }: { stream: MediaStream; id }) {
   video.style.opacity = '0';
   video.style.pointerEvents = 'none';
   document.body.appendChild(video);
+  return new Promise<any>((resolve) => {
+    video.onloadedmetadata = () => {
+      const width = stream.getVideoTracks()[0].getSettings().width!;
+      const height = stream.getVideoTracks()[0].getSettings().height!;
+      let ratio = 1;
+      if (width > wrapSize.width) {
+        const r1 = wrapSize.width / width;
+        ratio = r1;
+      }
+      if (height > wrapSize.height) {
+        const r1 = wrapSize.height / height;
+        if (ratio > r1) {
+          ratio = r1;
+        }
+      }
 
-  const w = stream.getVideoTracks()[0].getSettings().width;
-  const h = stream.getVideoTracks()[0].getSettings().height;
-  console.log('摄像头的流', { w, h }, stream.getVideoTracks()[0].id);
-  video.width = w!;
-  video.height = h!;
-  // const dom = new fabric.Rect({
-  //   left: 100,
-  //   top: 50,
-  //   fill: 'yellow',
-  //   width: 200,
-  //   height: 100,
-  //   objectCaching: false,
-  //   stroke: 'lightgreen',
-  //   strokeWidth: 4,
-  // });
-  const dom = new fabric.Image(video, {
-    top: 0,
-    left: 0,
-  });
-  // dom.scale(scaleRatio.value);
-  fabricCanvas.value!.add(dom);
-  fabric.util.requestAnimFrame(function render() {
-    fabricCanvas.value?.renderAll();
-    fabric.util.requestAnimFrame(render);
-  });
-
-  // canvasVideoStream.value = document
-  //   .querySelector('#pushCanvasRef')!
-  //   .captureStream();
-  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
-
-  setTimeout(() => {
-    canvasVideoStream.value
-      ?.getVideoTracks()[0]
-      .applyConstraints({
-        width: { ideal: 640 },
-        height: { ideal: 480 },
-        // frameRate: { ideal: 30 },
-      })
-      .then(() => {
-        console.log('修改成功');
-        canvasVideo.value!.srcObject = canvasVideoStream.value!;
-      })
-      .catch((e) => {
-        console.error('修改错误', e);
+      video.width = width;
+      video.height = height;
+
+      const dom = markRaw(
+        new fabric.Image(video, {
+          top: 0,
+          left: 0,
+          width,
+          height,
+        })
+      );
+      dom.scale(ratio);
+      fabricCanvas.value!.add(dom);
+      fabric.util.requestAnimFrame(function render() {
+        fabricCanvas.value?.renderAll();
+        fabric.util.requestAnimFrame(render);
       });
-  }, 1000);
-  canvasVideo.value!.srcObject = canvasVideoStream.value!;
+
+      canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+      resolve(dom);
+    };
+  });
 }
 
 function initCanvas() {
@@ -376,30 +367,15 @@ function initCanvas() {
   const ratio = wrapWidth / resolutionWidth;
   scaleRatio.value = ratio;
   const wrapHeight = resolutionHeight * ratio;
-  console.log(
-    resolutionWidth,
-    resolutionHeight,
-    'xxxxxx',
-    ratio,
-    wrapHeight,
-    wrapWidth
-  );
   // lower-canvas: 实际的canvas画面,也就是pushCanvasRef
   // upper-canvas: 操作时候的canvas
   const ins = markRaw(new fabric.Canvas(pushCanvasRef.value!));
-  // ins.setWidth(resolutionWidth);
-  // ins.setHeight(resolutionHeight);
   ins.setWidth(wrapWidth);
   ins.setHeight(wrapHeight);
-  console.log({ resolutionWidth, resolutionHeight });
-  ins.setZoom(1);
+  ins.setBackgroundColor('black', () => {});
   wrapSize.width = wrapWidth;
   wrapSize.height = wrapHeight;
   fabricCanvas.value = ins;
-  const video = createVideo({});
-  fabricCanvasEl.value = video;
-  canvasVideo.value = video;
-  document.body.appendChild(video);
 }
 
 onMounted(() => {
@@ -422,11 +398,11 @@ async function addMediaOk(val: {
     const event = await navigator.mediaDevices.getDisplayMedia({
       video: {
         deviceId: val.deviceId,
-        height: currentResolutionRatio.value,
-        frameRate: { max: currentMaxFramerate.value },
+        // displaySurface: 'monitor', // browser默认标签页;window默认窗口;monitor默认整个屏幕
       },
       audio: true,
     });
+
     const videoTrack = {
       id: getRandomEnglishString(8),
       audio: 2,
@@ -437,7 +413,14 @@ async function addMediaOk(val: {
       stream: event,
       streamid: event.id,
       trackid: event.getVideoTracks()[0].id,
+      canvasDom: undefined,
     };
+
+    const canvasDom = await autoCreateVideo({
+      stream: event,
+    });
+    videoTrack.canvasDom = canvasDom;
+
     const audio = event.getAudioTracks();
     if (audio.length) {
       const audioTrack = {
@@ -452,30 +435,22 @@ async function addMediaOk(val: {
         trackid: event.getAudioTracks()[0].id,
       };
       appStore.setAllTrack([...appStore.allTrack, videoTrack, audioTrack]);
-      // addTrack(videoTrack);
+      addTrack(videoTrack);
       addTrack(audioTrack);
     } else {
       appStore.setAllTrack([...appStore.allTrack, videoTrack]);
-      // addTrack(videoTrack);
+      addTrack(videoTrack);
     }
-    nextTick(() => {
-      createAutoVideo({
-        stream: event,
-        id: videoTrack.id,
-      });
-    });
 
     console.log('获取窗口成功');
   } else if (val.type === MediaTypeEnum.camera) {
     const event = await navigator.mediaDevices.getUserMedia({
       video: {
         deviceId: val.deviceId,
-        height: currentResolutionRatio.value,
-        frameRate: { max: currentMaxFramerate.value },
       },
       audio: false,
     });
-    const track = {
+    const videoTrack = {
       id: getRandomEnglishString(8),
       audio: 2,
       video: 1,
@@ -485,12 +460,17 @@ async function addMediaOk(val: {
       stream: event,
       streamid: event.id,
       trackid: event.getVideoTracks()[0].id,
+      canvasDom: undefined,
     };
-    appStore.setAllTrack([...appStore.allTrack, track]);
-    // addTrack(track);
-    nextTick(() => {
-      createAutoVideo({ stream: event, id: track.id });
+    const video = createVideo({});
+    video.srcObject = event;
+    const canvasDom = await autoCreateVideo({
+      stream: event,
     });
+    videoTrack.canvasDom = canvasDom;
+
+    appStore.setAllTrack([...appStore.allTrack, videoTrack]);
+    addTrack(videoTrack);
     console.log('获取摄像头成功');
   } else if (val.type === MediaTypeEnum.microphone) {
     const event = await navigator.mediaDevices.getUserMedia({
@@ -523,6 +503,10 @@ async function addMediaOk(val: {
 
 function handleDelTrack(item: AppRootState['allTrack'][0]) {
   console.log('handleDelTrack', item);
+  if (item.canvasDom !== undefined) {
+    // @ts-ignore
+    fabricCanvas.value?.remove(item.canvasDom);
+  }
   const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
   appStore.setAllTrack(res);
   delTrack(item);
@@ -538,14 +522,14 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
 .push-wrap {
   display: flex;
   justify-content: space-between;
-  margin: 20px auto 0;
-  width: $w-1275;
+  margin: 15px auto 0;
+  width: $w-1250;
   .left {
     position: relative;
     display: inline-block;
     overflow: hidden;
     box-sizing: border-box;
-    width: $w-950;
+    width: $w-960;
     height: 100%;
     border-radius: 6px;
     background-color: white;
@@ -572,7 +556,7 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
         justify-content: space-around;
         padding: 0 20px;
         height: 50px;
-        border-radius: 5px;
+        border-radius: 6px;
         background-color: white;
         transform: translate(-50%, -50%);
       }
@@ -589,8 +573,8 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
 
         .avatar {
           margin-right: 20px;
-          width: 64px;
-          height: 64px;
+          width: 55px;
+          height: 55px;
           border-radius: 50%;
           background-position: center center;
           background-size: cover;
@@ -645,12 +629,12 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
     }
   }
   .right {
-    position: relative;
-    display: inline-block;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
     box-sizing: border-box;
     margin-left: 10px;
     width: $w-250;
-    height: 100%;
     border-radius: 6px;
     background-color: white;
     color: #9499a0;
@@ -658,7 +642,6 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
     .resource-card {
       position: relative;
       box-sizing: border-box;
-      margin-bottom: 5%;
       margin-bottom: 10px;
       padding: 10px;
       width: 100%;
@@ -692,10 +675,11 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
       }
     }
     .danmu-card {
+      position: relative;
+      flex: 1;
       box-sizing: border-box;
       padding: 10px;
       width: 100%;
-      height: 400px;
       border-radius: 6px;
       background-color: papayawhip;
       text-align: initial;
@@ -704,8 +688,7 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
       }
       .list {
         overflow: scroll;
-        margin-bottom: 10px;
-        height: 300px;
+        height: 360px;
 
         @extend %hideScrollbar;
 
@@ -722,9 +705,14 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
       }
 
       .send-msg {
+        position: absolute;
+        bottom: 10px;
+        left: 50%;
         display: flex;
         align-items: center;
         box-sizing: border-box;
+        width: calc(100% - 20px);
+        transform: translateX(-50%);
         .ipt {
           display: block;
           box-sizing: border-box;
@@ -749,7 +737,7 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
   .push-wrap {
     width: $w-1475;
     .left {
-      width: $w-1150;
+      width: $w-1152;
     }
     .right {
       width: $w-300;