浏览代码

fix: 优化

shuisheng 2 年之前
父节点
当前提交
db20e339dc

+ 1 - 1
src/components/VideoControls/index.vue

@@ -81,7 +81,7 @@ function changeLiveLine(item) {
   position: absolute;
   bottom: 0;
   left: 0;
-  z-index: 1;
+  z-index: 10;
   display: flex;
   align-items: center;
   justify-content: space-between;

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

@@ -298,6 +298,7 @@ export function usePush() {
   }
 
   async function startLive({ type, receiver }) {
+    console.log('startLive');
     if (!loginTip()) return;
     const flag = await userHasLiveRoom();
     if (!flag) {

+ 9 - 2
src/hooks/use-srs-ws.ts

@@ -138,6 +138,7 @@ export const useSrsWs = () => {
     receiver: string;
     videoEl?: HTMLVideoElement;
   }) {
+    console.log('handleStartLivehandleStartLive', receiver);
     networkStore.wsMap.get(roomId.value)?.send<WsStartLiveType['data']>({
       msgType: WsMsgTypeEnum.startLive,
       data: {
@@ -177,7 +178,11 @@ export const useSrsWs = () => {
     receiver: string;
     videoEl: HTMLVideoElement;
   }) {
-    console.warn('22开始new WebRTCClass', `${roomId.value}___${receiver!}`);
+    console.warn(
+      '22开始new WebRTCClass',
+      receiver,
+      `${roomId.value}___${receiver!}`
+    );
     new WebRTCClass({
       maxBitrate: currentMaxBitrate.value,
       maxFramerate: currentMaxFramerate.value,
@@ -278,7 +283,9 @@ export const useSrsWs = () => {
       (data: WsRoomLivingType['data']) => {
         prettierReceiveWsMsg(WsMsgTypeEnum.roomLiving, data);
         roomLiving.value = true;
-        anchorSocketId.value = data.anchor_socket_id;
+        if (data.anchor_socket_id) {
+          anchorSocketId.value = data.anchor_socket_id;
+        }
       }
     );
 

+ 1634 - 7
src/views/push/index.vue

@@ -1,20 +1,1647 @@
 <template>
-  <div>
-    <SrsCpt v-if="liveType === LiveRoomTypeEnum.user_srs"></SrsCpt>
-    <RtcCpt v-else-if="liveType === LiveRoomTypeEnum.user_wertc"></RtcCpt>
+  <div class="push-wrap">
+    <div
+      ref="topRef"
+      class="left"
+    >
+      <div
+        ref="containerRef"
+        class="container"
+      >
+        <canvas
+          id="pushCanvasRef"
+          ref="pushCanvasRef"
+        ></canvas>
+        <div
+          v-if="appStore.allTrack.filter((item) => !item.hidden).length <= 0"
+          class="add-wrap"
+        >
+          <n-space>
+            <n-button
+              v-for="(item, index) in allMediaTypeList"
+              :key="index"
+              class="item"
+              @click="handleStartMedia(item)"
+            >
+              {{ item.txt }}
+            </n-button>
+          </n-space>
+        </div>
+      </div>
+
+      <div
+        ref="bottomRef"
+        class="room-control"
+      >
+        <div class="info">
+          <div
+            class="avatar"
+            :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
+          ></div>
+          <div class="detail">
+            <div class="top">
+              <n-input-group>
+                <n-input
+                  v-model:value="roomName"
+                  size="small"
+                  placeholder="输入房间名"
+                  :style="{ width: '50%' }"
+                />
+                <n-button
+                  size="small"
+                  type="primary"
+                  @click="confirmRoomName"
+                >
+                  确定
+                </n-button>
+              </n-input-group>
+            </div>
+            <div class="bottom">
+              <span v-if="NODE_ENV === 'development'">
+                {{ mySocketId }}
+              </span>
+            </div>
+          </div>
+        </div>
+        <div class="rtc">
+          <div class="item">
+            <div class="txt">码率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxBitrate"
+                :options="maxBitrate"
+              />
+            </div>
+          </div>
+          <div class="item">
+            <div class="txt">帧率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxFramerate"
+                :options="maxFramerate"
+              />
+            </div>
+          </div>
+          <div class="item">
+            <div class="txt">分辨率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentResolutionRatio"
+                :options="resolutionRatio"
+              />
+            </div>
+          </div>
+        </div>
+        <div class="other">
+          <div class="top">
+            <span class="item">
+              <i class="ico"></i>
+              <span>
+                正在观看:
+                {{
+                  liveUserList.filter((item) => item.id !== mySocketId).length
+                }}
+              </span>
+            </span>
+          </div>
+          <div class="bottom">
+            <n-button
+              v-if="!roomLiving"
+              type="info"
+              size="small"
+              @click="handleStartLive"
+            >
+              开始直播
+            </n-button>
+            <n-button
+              v-else
+              type="error"
+              size="small"
+              @click="endLive"
+            >
+              结束直播
+            </n-button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="right">
+      <div class="resource-card">
+        <div class="title">素材列表</div>
+        <div class="list">
+          <div
+            v-for="(item, index) in appStore.allTrack.filter(
+              (item) => !item.hidden
+            )"
+            :key="index"
+            class="item"
+          >
+            <span class="name">
+              ({{ mediaTypeEnumMap[item.type] }}){{ item.mediaName }}
+            </span>
+            <div class="control">
+              <div
+                v-if="item.audio === 1"
+                class="control-item"
+                @click="handleChangeMuted(item)"
+              >
+                <n-icon size="16">
+                  <VolumeMuteOutline v-if="item.muted"></VolumeMuteOutline>
+                  <VolumeHighOutline v-else></VolumeHighOutline>
+                </n-icon>
+              </div>
+              <div
+                class="control-item"
+                @click="handleEdit(item)"
+              >
+                <n-icon size="16">
+                  <CreateOutline></CreateOutline>
+                </n-icon>
+              </div>
+              <div
+                class="control-item"
+                @click="handleDel(item)"
+              >
+                <n-icon size="16">
+                  <Close></Close>
+                </n-icon>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="bottom">
+          <n-button
+            size="small"
+            type="primary"
+            @click="showSelectMediaModalCpt = true"
+          >
+            添加素材
+          </n-button>
+        </div>
+      </div>
+      <div class="danmu-card">
+        <div class="title">弹幕互动</div>
+        <div class="list-wrap">
+          <div
+            ref="danmuListRef"
+            class="list"
+          >
+            <div
+              v-for="(item, index) in damuList"
+              :key="index"
+              class="item"
+            >
+              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+                <span class="name">
+                  {{ item.userInfo?.username || item.socket_id }}:
+                </span>
+                <span class="msg">{{ item.msg }}</span>
+              </template>
+              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+                <span class="name system">系统通知:</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>进入直播!</span>
+                </span>
+              </template>
+              <template
+                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
+              >
+                <span class="name system">系统通知:</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>离开直播!</span>
+                </span>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="send-msg">
+          <input
+            v-model="danmuStr"
+            class="ipt"
+            @keydown="keydownDanmu"
+          />
+          <n-button
+            type="info"
+            size="small"
+            @click="sendDanmu"
+          >
+            发送
+          </n-button>
+        </div>
+      </div>
+    </div>
+
+    <SelectMediaModalCpt
+      v-if="showSelectMediaModalCpt"
+      :all-media-type-list="allMediaTypeList"
+      @close="showSelectMediaModalCpt = false"
+      @ok="selectMediaOk"
+    ></SelectMediaModalCpt>
+
+    <MediaModalCpt
+      v-if="showMediaModalCpt"
+      :media-type="currentMediaType"
+      @close="showMediaModalCpt = false"
+      @ok="addMediaOk"
+    ></MediaModalCpt>
+    <OpenMicophoneTipCpt
+      v-if="showOpenMicophoneTipCpt"
+      @close="showOpenMicophoneTipCpt = false"
+    ></OpenMicophoneTipCpt>
   </div>
 </template>
 
 <script lang="ts" setup>
+import {
+  Close,
+  CreateOutline,
+  VolumeHighOutline,
+  VolumeMuteOutline,
+} from '@vicons/ionicons5';
+import { fabric } from 'fabric';
+import { UploadFileInfo } from 'naive-ui';
+import {
+  Raw,
+  markRaw,
+  onMounted,
+  onUnmounted,
+  reactive,
+  ref,
+  watch,
+} from 'vue';
 import { useRoute } from 'vue-router';
+import * as workerTimers from 'worker-timers';
 
-import { LiveRoomTypeEnum } from '@/interface';
+import { mediaTypeEnumMap } from '@/constant';
+import { usePush } from '@/hooks/use-push';
+import { useRTCParams } from '@/hooks/use-rtc-params';
+import { DanmuMsgTypeEnum, MediaTypeEnum } from '@/interface';
+import { AppRootState, useAppStore } from '@/store/app';
+import { useResourceCacheStore } from '@/store/cache';
+import { useUserStore } from '@/store/user';
+import {
+  createVideo,
+  formatDownTime,
+  generateBase64,
+  getRandomEnglishString,
+  readFile,
+  saveFile,
+} from '@/utils';
+import { NODE_ENV } from 'script/constant';
 
-import RtcCpt from './rtc/index.vue';
-import SrsCpt from './srs/index.vue';
+import MediaModalCpt from './mediaModal/index.vue';
+import OpenMicophoneTipCpt from './openMicophoneTip/index.vue';
+import SelectMediaModalCpt from './selectMediaModal/index.vue';
 
 const route = useRoute();
+const userStore = useUserStore();
+const appStore = useAppStore();
+const resourceCacheStore = useResourceCacheStore();
+const { maxBitrate, maxFramerate, resolutionRatio, allMediaTypeList } =
+  useRTCParams();
+
+const {
+  confirmRoomName,
+  startLive,
+  endLive,
+  sendDanmu,
+  keydownDanmu,
+  addTrack,
+  delTrack,
+  mySocketId,
+  lastCoverImg,
+  canvasVideoStream,
+  roomLiving,
+  currentResolutionRatio,
+  currentMaxBitrate,
+  currentMaxFramerate,
+  danmuStr,
+  roomName,
+  damuList,
+  liveUserList,
+} = usePush();
+
+const currentMediaType = ref(MediaTypeEnum.camera);
+const showOpenMicophoneTipCpt = ref(false);
+const showSelectMediaModalCpt = ref(false);
+const showMediaModalCpt = ref(false);
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const danmuListRef = ref<HTMLDivElement>();
+const containerRef = ref<HTMLDivElement>();
+const pushCanvasRef = ref<HTMLCanvasElement>();
+const webaudioVideo = ref<HTMLVideoElement>();
+const fabricCanvas = ref<fabric.Canvas>();
+const audioCtx = ref<AudioContext>();
+const startTime = ref(+new Date());
+// const startTime = ref(1692807352565); // 1693027352565
+
+const timeCanvasDom = ref<Raw<fabric.Text>[]>([]);
+const stopwatchCanvasDom = ref<Raw<fabric.Text>[]>([]);
+const wrapSize = reactive({
+  width: 0,
+  height: 0,
+});
+const workerTimerId = ref(-1);
+const bodyAppendChildElArr = ref<HTMLElement[]>([]);
 const liveType = Number(route.query.liveType);
+
+watch(
+  () => damuList.value.length,
+  () => {
+    setTimeout(() => {
+      if (danmuListRef.value) {
+        danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight;
+      }
+    }, 0);
+  }
+);
+
+onMounted(() => {
+  setTimeout(() => {
+    scrollTo(0, 0);
+  }, 100);
+  initUserMedia();
+  initCanvas();
+  handleCache();
+});
+
+onUnmounted(() => {
+  bodyAppendChildElArr.value.forEach((el) => {
+    el.remove();
+  });
+  clearFrame();
+});
+
+function initUserMedia() {
+  navigator.mediaDevices
+    .getUserMedia({
+      video: true,
+      audio: false,
+    })
+    .then(() => {
+      console.log('初始化获取摄像头成功');
+    })
+    .catch(() => {
+      console.log('初始化获取摄像头失败');
+    })
+    .finally(() => {
+      navigator.mediaDevices
+        .getUserMedia({
+          video: false,
+          audio: true,
+        })
+        .then(() => {
+          console.log('初始化获取麦克风成功');
+        })
+        .catch(() => {
+          console.log('初始化获取麦克风失败');
+          showOpenMicophoneTipCpt.value = true;
+        });
+    });
+}
+
+function renderAll() {
+  timeCanvasDom.value.forEach((item) => {
+    item.text = new Date().toLocaleString();
+  });
+  stopwatchCanvasDom.value.forEach((item) => {
+    item.text = formatDownTime({
+      endTime: +new Date(),
+      startTime: startTime.value,
+      showMs: true,
+    });
+  });
+  fabricCanvas.value?.renderAll();
+}
+
+function clearFrame() {
+  if (workerTimerId.value !== -1) {
+    workerTimers.clearInterval(workerTimerId.value);
+  }
+}
+
+function renderFrame() {
+  const delay = 1000 / 60; // 16.666666666666668
+  workerTimerId.value = workerTimers.setInterval(() => {
+    renderAll();
+  }, delay);
+  console.log('workerTimerId.value', workerTimerId.value);
+}
+
+function handleMixedAudio() {
+  console.log('handleMixedAudio');
+  const allAudioTrack = appStore.allTrack.filter((item) => item.audio === 1);
+  audioCtx.value = new AudioContext();
+  const gainNode = audioCtx.value.createGain();
+  allAudioTrack.forEach((item) => {
+    if (!audioCtx.value || !item.stream) return;
+    const audioInput = audioCtx.value.createMediaStreamSource(item.stream);
+    audioInput.connect(gainNode);
+    console.log('混流', item.stream?.id, item.stream);
+  });
+
+  const destination = audioCtx.value.createMediaStreamDestination();
+  if (canvasVideoStream.value?.getAudioTracks()[0]) {
+    canvasVideoStream.value.removeTrack(
+      canvasVideoStream.value?.getAudioTracks()[0]
+    );
+  }
+  canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
+  // streamTmp = destination.stream;
+  if (canvasVideoStream.value?.getAudioTracks()[0]) {
+    canvasVideoStream.value.removeTrack(
+      canvasVideoStream.value?.getAudioTracks()[0]
+    );
+    canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
+  } else {
+    canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
+  }
+  console.log(
+    canvasVideoStream.value?.getAudioTracks(),
+    'canvasVideoStreamcanvasVideoStream'
+  );
+  gainNode.connect(destination);
+  if (webaudioVideo.value) {
+    webaudioVideo.value.remove();
+  }
+  webaudioVideo.value = createVideo({ appendChild: true });
+  webaudioVideo.value.className = 'web-audio-video';
+  webaudioVideo.value.srcObject = destination.stream;
+}
+
+function handleStartLive() {
+  handleMixedAudio();
+  lastCoverImg.value = generateBase64(pushCanvasRef.value!);
+  startLive({ type: liveType, receiver: mySocketId.value });
+}
+
+function handleScale({ width, height }: { width: number; height: number }) {
+  const resolutionHeight =
+    currentResolutionRatio.value * window.devicePixelRatio;
+  const resolutionWidth =
+    currentResolutionRatio.value *
+    window.devicePixelRatio *
+    appStore.videoRatio;
+  let ratio = 1;
+  if (width > resolutionWidth) {
+    const r1 = resolutionWidth / width;
+    ratio = r1;
+  }
+  if (height > resolutionHeight) {
+    const r1 = resolutionHeight / height;
+    if (ratio > r1) {
+      ratio = r1;
+    }
+  }
+  return ratio;
+}
+
+function autoCreateVideo({
+  stream,
+  id,
+  rect,
+  muted,
+}: {
+  stream: MediaStream;
+  id: string;
+  rect?: { left: number; top: number };
+  muted?: boolean;
+}) {
+  console.warn('autoCreateVideo', id);
+  const videoEl = createVideo({ appendChild: true });
+  if (muted !== undefined) {
+    videoEl.muted = muted;
+  }
+  videoEl.srcObject = stream;
+  bodyAppendChildElArr.value.push(videoEl);
+  return new Promise<{
+    canvasDom: fabric.Image;
+    videoEl: HTMLVideoElement;
+    scale: number;
+  }>((resolve) => {
+    videoEl.onloadedmetadata = () => {
+      const width = stream.getVideoTracks()[0].getSettings().width!;
+      const height = stream.getVideoTracks()[0].getSettings().height!;
+      const ratio = handleScale({ width, height });
+      videoEl.width = width;
+      videoEl.height = height;
+
+      const canvasDom = markRaw(
+        new fabric.Image(videoEl, {
+          top: rect?.top || 0,
+          left: rect?.left || 0,
+          width,
+          height,
+        })
+      );
+      console.log(
+        '初始化',
+        ratio,
+        canvasDom.width,
+        canvasDom.height,
+        width * ratio,
+        height * ratio,
+        canvasDom
+      );
+      handleMoving({ canvasDom, id });
+      handleScaling({ canvasDom, id });
+      canvasDom.scale(ratio / window.devicePixelRatio);
+      fabricCanvas.value!.add(canvasDom);
+
+      resolve({ canvasDom, scale: ratio, videoEl });
+    };
+  });
+}
+
+watch(
+  () => currentResolutionRatio.value,
+  (newHeight, oldHeight) => {
+    changeCanvasAttr({ newHeight, oldHeight });
+  }
+);
+
+// 容器宽高,1280*720,即720p
+// canvas容器宽高,2560*1440,即1440p
+
+// ======
+// 容器宽高,960*540,即540p
+// dom宽高,640*480
+// canvas容器宽高,960*540,即540p
+// 将dom绘制到容器里,此时dom的大小就是640*480
+// 需求,不管切换多少分辨率,我要看到的dom都是一样大小,即
+// 960*540时,dom是640*480
+// 1280*720时,dom不能是640*480了,因为这样他就会对比上一个分辨率的dom看起来小了,960/1280=0.75,540/720=0.75,
+// 其实就是分辨率变大了,我们就要将图片也变大,即图片的宽是640/0.75=853.4,高是480/0.75=640
+// 坐标变化,960*540时,dom坐标是100,100
+// 1280*720时,dom的坐标不能再是100,100了,否则对比上一个分辨率看起来偏
+
+function changeCanvasAttr({
+  newHeight,
+  oldHeight,
+}: {
+  newHeight: number;
+  oldHeight: number;
+}) {
+  if (fabricCanvas.value) {
+    const resolutionHeight =
+      currentResolutionRatio.value / window.devicePixelRatio;
+    const resolutionWidth =
+      (currentResolutionRatio.value / window.devicePixelRatio) *
+      appStore.videoRatio;
+    fabricCanvas.value.setWidth(resolutionWidth);
+    fabricCanvas.value.setHeight(resolutionHeight);
+    appStore.allTrack.forEach((iten) => {
+      console.log('当前类型', iten.type);
+      const item = iten.canvasDom;
+
+      if (item) {
+        // 分辨率变小了,将图片变小
+        if (newHeight < oldHeight) {
+          const ratio2 = oldHeight / newHeight;
+          item.left = item.left! / ratio2;
+          item.top = item.top! / ratio2;
+        } else {
+          // 分辨率变大了,将图片变大
+          const ratio2 = oldHeight / newHeight;
+          item.left = item.left! / ratio2;
+          item.top = item.top! / ratio2;
+        }
+      }
+    });
+    appStore.allTrack.forEach((iten) => {
+      console.log('当前类型', iten.type);
+      const item = iten.canvasDom;
+
+      if (item) {
+        // 分辨率变小了,将图片变小
+        if (newHeight < oldHeight) {
+          const ratio = newHeight / oldHeight;
+          const ratio1 = (item.scaleX || 1) * ratio;
+          item.scale(ratio1);
+        } else {
+          // 分辨率变大了,将图片变大
+          const ratio = newHeight / oldHeight;
+          const ratio1 = (item.scaleX || 1) * ratio;
+          item.scale(ratio1);
+        }
+      }
+    });
+
+    changeCanvasStyle();
+  }
+}
+
+function changeCanvasStyle() {
+  // @ts-ignore
+  fabricCanvas.value.wrapperEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.wrapperEl.style.height = `${wrapSize.height}px`;
+  // @ts-ignore
+  fabricCanvas.value.lowerCanvasEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.lowerCanvasEl.style.height = `${wrapSize.height}px`;
+  // @ts-ignore
+  fabricCanvas.value.upperCanvasEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.upperCanvasEl.style.height = `${wrapSize.height}px`;
+}
+
+function initCanvas() {
+  const resolutionHeight =
+    currentResolutionRatio.value / window.devicePixelRatio;
+  const resolutionWidth =
+    (currentResolutionRatio.value / window.devicePixelRatio) *
+    appStore.videoRatio;
+  const wrapWidth = containerRef.value!.getBoundingClientRect().width;
+  // const wrapWidth = 1920;
+  const ratio = wrapWidth / resolutionWidth;
+  const wrapHeight = resolutionHeight * ratio;
+  // const wrapHeight = 1080;
+  // lower-canvas: 实际的canvas画面,也就是pushCanvasRef
+  // upper-canvas: 操作时候的canvas
+  const ins = markRaw(new fabric.Canvas(pushCanvasRef.value!));
+  ins.setWidth(resolutionWidth);
+  ins.setHeight(resolutionHeight);
+  console.log('initCanvas', { resolutionWidth, resolutionHeight });
+  ins.setBackgroundColor('black', () => {
+    console.log('setBackgroundColor回调');
+  });
+  wrapSize.width = wrapWidth;
+  wrapSize.height = wrapHeight;
+  fabricCanvas.value = ins;
+  renderFrame();
+  changeCanvasStyle();
+}
+
+/**
+ * 1: {scaleX: 1, scaleY: 1}
+ * 2: {scaleX: 0.5, scaleY: 0.5}
+ * 3: {scaleX: 0.3333333333333333, scaleY: 0.3333333333333333}
+ * 4: {scaleX: 0.25, scaleY: 0.25}
+ */
+
+/**
+ * 二倍屏即1px里面有2个像素;三倍屏1px里面有3个像素,以此类推
+ * 一个图片,宽高都是100px
+ * 一倍屏展示:100px等于100个像素,一比一展示
+ * 二倍屏展示:100px等于100个像素,二比一展示,即在二倍屏的100px看起来会比一倍屏的100px小一倍
+ * 如果需要在一杯和二倍屏幕的时候看的大小都一样:
+ * 1,在二倍屏的时候,需要将100px放大一倍,即200px;
+ * 2,在一倍屏的时候,需要将100px缩小一百,即50px;
+ */
+function handleScaling({ canvasDom, id }) {
+  canvasDom.on('scaling', () => {
+    appStore.allTrack.forEach((item) => {
+      if (id === item.id) {
+        item.scaleInfo[window.devicePixelRatio] = {
+          scaleX: canvasDom.scaleX || 1,
+          scaleY: canvasDom.scaleY || 1,
+        };
+        Object.keys(item.scaleInfo).forEach((iten) => {
+          if (window.devicePixelRatio !== Number(iten)) {
+            if (window.devicePixelRatio > Number(iten)) {
+              item.scaleInfo[iten] = {
+                scaleX:
+                  item.scaleInfo[window.devicePixelRatio].scaleX *
+                  window.devicePixelRatio,
+                scaleY:
+                  item.scaleInfo[window.devicePixelRatio].scaleY *
+                  window.devicePixelRatio,
+              };
+            } else {
+              if (window.devicePixelRatio === 1) {
+                item.scaleInfo[iten] = {
+                  scaleX: item.scaleInfo[1].scaleX / Number(iten),
+                  scaleY: item.scaleInfo[1].scaleY / Number(iten),
+                };
+              } else {
+                item.scaleInfo[iten] = {
+                  scaleX: item.scaleInfo[1].scaleX * Number(iten),
+                  scaleY: item.scaleInfo[1].scaleY * Number(iten),
+                };
+              }
+            }
+          }
+        });
+      }
+    });
+    resourceCacheStore.setList(appStore.allTrack);
+  });
+}
+function handleMoving({
+  canvasDom,
+  id,
+}: {
+  canvasDom: fabric.Image | fabric.Text;
+  id: string;
+}) {
+  canvasDom.on('moving', () => {
+    console.log(
+      'moving',
+      canvasDom.width,
+      canvasDom.height,
+      canvasDom.scaleX,
+      canvasDom.scaleY
+    );
+    appStore.allTrack.forEach((item) => {
+      if (id === item.id) {
+        item.rect = {
+          top: (canvasDom.top || 0) * window.devicePixelRatio,
+          left: (canvasDom.left || 0) * window.devicePixelRatio,
+        };
+      }
+    });
+    resourceCacheStore.setList(appStore.allTrack);
+  });
+}
+
+async function handleCache() {
+  const res: AppRootState['allTrack'] = [];
+  const queue: any[] = [];
+  resourceCacheStore.list.forEach((item) => {
+    // @ts-ignore
+    const obj: AppRootState['allTrack'][0] = {};
+    obj.audio = item.audio;
+    obj.video = item.video;
+    obj.id = item.id;
+    obj.type = item.type;
+    obj.hidden = item.hidden;
+    obj.mediaName = item.mediaName;
+    obj.muted = item.muted;
+    obj.rect = item.rect;
+    obj.scaleInfo = item.scaleInfo;
+    obj.stopwatchInfo = item.stopwatchInfo;
+
+    async function handleMediaVideo() {
+      const { code, file } = await readFile(item.id);
+      if (code === 1 && file) {
+        const url = URL.createObjectURL(file);
+        const videoEl = createVideo({
+          muted: item.muted ? item.muted : false,
+          appendChild: true,
+        });
+        videoEl.src = url;
+        bodyAppendChildElArr.value.push(videoEl);
+        await new Promise((resolve) => {
+          videoEl.onloadedmetadata = () => {
+            const stream = videoEl
+              // @ts-ignore
+              .captureStream();
+            const width = stream.getVideoTracks()[0].getSettings().width!;
+            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,
+                left: (item.rect?.left || 0) / window.devicePixelRatio,
+                width,
+                height,
+              })
+            );
+            handleMoving({ canvasDom, id: item.id });
+            handleScaling({ canvasDom, id: item.id });
+            canvasDom.scale(
+              item.scaleInfo[window.devicePixelRatio].scaleX || 1
+            );
+            fabricCanvas.value!.add(canvasDom);
+            obj.videoEl = videoEl;
+            obj.canvasDom = canvasDom;
+            resolve({ videoEl, canvasDom });
+          };
+        });
+        const stream = videoEl
+          // @ts-ignore
+          .captureStream() as MediaStream;
+        obj.stream = stream;
+        obj.streamid = stream.id;
+        obj.track = stream.getVideoTracks()[0];
+        obj.trackid = stream.getVideoTracks()[0].id;
+      } else {
+        console.error('读取文件失败');
+      }
+    }
+
+    async function handleImg() {
+      const { code, file } = await readFile(item.id);
+      if (code === 1 && file) {
+        const imgEl = await new Promise<HTMLImageElement>((resolve) => {
+          const reader = new FileReader();
+          reader.addEventListener(
+            'load',
+            function () {
+              const img = document.createElement('img');
+              img.src = reader.result as string;
+              img.onload = () => {
+                resolve(img);
+              };
+            },
+            false
+          );
+          if (file) {
+            reader.readAsDataURL(file);
+          }
+        });
+        if (fabricCanvas.value) {
+          const canvasDom = markRaw(
+            new fabric.Image(imgEl, {
+              top: (item.rect?.top || 0) / window.devicePixelRatio,
+              left: (item.rect?.left || 0) / window.devicePixelRatio,
+              width: imgEl.width,
+              height: imgEl.height,
+            })
+          );
+          handleMoving({ canvasDom, id: obj.id });
+          handleScaling({ canvasDom, id: obj.id });
+          canvasDom.scale(item.scaleInfo[window.devicePixelRatio].scaleX || 1);
+          fabricCanvas.value.add(canvasDom);
+          obj.canvasDom = canvasDom;
+        }
+      } else {
+        console.error('读取文件失败');
+      }
+    }
+    if (item.type === MediaTypeEnum.media && item.video === 1) {
+      queue.push(handleMediaVideo());
+    } else if (item.type === MediaTypeEnum.img) {
+      queue.push(handleImg());
+    } else if (item.type === MediaTypeEnum.txt) {
+      obj.txtInfo = item.txtInfo;
+      obj.scaleInfo = item.scaleInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text(item.txtInfo?.txt || '', {
+            top: (item.rect?.top || 0) / window.devicePixelRatio,
+            left: (item.rect?.left || 0) / window.devicePixelRatio,
+            fill: item.txtInfo?.color,
+          })
+        );
+        handleMoving({ canvasDom, id: obj.id });
+        handleScaling({ canvasDom, id: obj.id });
+        // fabric.Text类型不能除以分辨率
+        canvasDom.scale(item.scaleInfo[window.devicePixelRatio].scaleX);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+      }
+    } else if (item.type === MediaTypeEnum.time) {
+      obj.timeInfo = item.timeInfo;
+      obj.scaleInfo = item.scaleInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text(new Date().toLocaleString(), {
+            top: (item.rect?.top || 0) / window.devicePixelRatio,
+            left: (item.rect?.left || 0) / window.devicePixelRatio,
+            fill: item.timeInfo?.color,
+          })
+        );
+        timeCanvasDom.value.push(canvasDom);
+        handleMoving({ canvasDom, id: obj.id });
+        handleScaling({ canvasDom, id: obj.id });
+        // fabric.Text类型不能除以分辨率
+        canvasDom.scale(item.scaleInfo[window.devicePixelRatio].scaleX);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+      }
+    } else if (item.type === MediaTypeEnum.stopwatch) {
+      obj.stopwatchInfo = item.stopwatchInfo;
+      obj.scaleInfo = item.scaleInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text('00天00时00分00秒000毫秒', {
+            top: (item.rect?.top || 0) / window.devicePixelRatio,
+            left: (item.rect?.left || 0) / window.devicePixelRatio,
+            fill: item.stopwatchInfo?.color,
+          })
+        );
+        stopwatchCanvasDom.value.push(canvasDom);
+        handleMoving({ canvasDom, id: obj.id });
+        handleScaling({ canvasDom, id: obj.id });
+        // fabric.Text类型不能除以分辨率
+        canvasDom.scale(item.scaleInfo[window.devicePixelRatio].scaleX);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+      }
+    }
+    res.push(obj);
+  });
+  await Promise.all(queue);
+  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+  appStore.setAllTrack(res);
+}
+
+function selectMediaOk(val: MediaTypeEnum) {
+  showMediaModalCpt.value = true;
+  showSelectMediaModalCpt.value = false;
+  currentMediaType.value = val;
+}
+
+function setScaleInfo({ track, canvasDom }) {
+  [1, 2, 3, 4].forEach((devicePixelRatio) => {
+    track.scaleInfo[devicePixelRatio] = {
+      scaleX: 1 / devicePixelRatio,
+      scaleY: 1 / devicePixelRatio,
+    };
+  });
+  if (window.devicePixelRatio !== 1) {
+    const ratio = 1 / window.devicePixelRatio;
+    canvasDom.scale(ratio);
+    track.scaleInfo[window.devicePixelRatio] = {
+      scaleX: ratio,
+      scaleY: ratio,
+    };
+  }
+}
+
+async function addMediaOk(val: {
+  type: MediaTypeEnum;
+  deviceId: string;
+  mediaName: string;
+  txtInfo?: { txt: string; color: string };
+  timeInfo?: { color: string };
+  stopwatchInfo?: { color: string };
+  imgInfo?: UploadFileInfo[];
+  mediaInfo?: UploadFileInfo[];
+}) {
+  if (!audioCtx.value) {
+    audioCtx.value = new AudioContext();
+  }
+  showMediaModalCpt.value = false;
+  if (val.type === MediaTypeEnum.screen) {
+    const event = await navigator.mediaDevices.getDisplayMedia({
+      video: {
+        deviceId: val.deviceId,
+        // displaySurface: 'monitor', // browser默认标签页;window默认窗口;monitor默认整个屏幕
+      },
+      audio: true,
+    });
+
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.screen,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+
+    const { canvasDom, videoEl, scale } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+    });
+    setScaleInfo({ canvasDom, track: videoTrack });
+    videoTrack.videoEl = videoEl;
+    // @ts-ignore
+    videoTrack.canvasDom = canvasDom;
+
+    const audio = event.getAudioTracks();
+    if (audio.length) {
+      videoTrack.audio = 1;
+      const audioTrack: AppRootState['allTrack'][0] = {
+        id: videoTrack.id,
+        audio: 1,
+        video: 2,
+        mediaName: val.mediaName,
+        type: MediaTypeEnum.screen,
+        track: event.getAudioTracks()[0],
+        trackid: event.getAudioTracks()[0].id,
+        stream: event,
+        streamid: event.id,
+        hidden: true,
+        muted: false,
+        scaleInfo: {},
+      };
+      const res = [...appStore.allTrack, videoTrack, audioTrack];
+      appStore.setAllTrack(res);
+      resourceCacheStore.setList(res);
+      handleMixedAudio();
+      // @ts-ignore
+      addTrack(videoTrack);
+      // @ts-ignore
+      addTrack(audioTrack);
+    } else {
+      const res = [...appStore.allTrack, videoTrack];
+      appStore.setAllTrack(res);
+      resourceCacheStore.setList(res);
+      // @ts-ignore
+      addTrack(videoTrack);
+    }
+
+    console.log('获取窗口成功');
+  } else if (val.type === MediaTypeEnum.camera) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: {
+        deviceId: val.deviceId,
+      },
+      audio: false,
+    });
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.camera,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+    const { canvasDom, videoEl, scale } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+    });
+    setScaleInfo({ canvasDom, track: videoTrack });
+    videoTrack.videoEl = videoEl;
+    // @ts-ignore
+    videoTrack.canvasDom = canvasDom;
+
+    const res = [...appStore.allTrack, videoTrack];
+    appStore.setAllTrack(res);
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(videoTrack);
+    console.log('获取摄像头成功');
+  } else if (val.type === MediaTypeEnum.microphone) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: false,
+      audio: { deviceId: val.deviceId },
+    });
+    const audioTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 1,
+      video: 2,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.microphone,
+      track: event.getAudioTracks()[0],
+      trackid: event.getAudioTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+    const res = [...appStore.allTrack, audioTrack];
+    appStore.setAllTrack(res);
+    resourceCacheStore.setList(res);
+    handleMixedAudio();
+    // @ts-ignore
+    addTrack(audioTrack);
+
+    console.log('获取麦克风成功');
+  } else if (val.type === MediaTypeEnum.txt) {
+    const txtTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.txt,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+    if (fabricCanvas.value) {
+      console.log('val.txtInfo?.txt ', val.txtInfo?.txt);
+      const canvasDom = markRaw(
+        new fabric.Text(val.txtInfo?.txt || '', {
+          top: 0,
+          left: 0,
+          fill: val.txtInfo?.color,
+        })
+      );
+      handleMoving({ canvasDom, id: txtTrack.id });
+      handleScaling({ canvasDom, id: txtTrack.id });
+      txtTrack.txtInfo = val.txtInfo;
+      if (window.devicePixelRatio !== 1) {
+        const ratio = 1 / window.devicePixelRatio;
+        canvasDom.scale(ratio);
+        txtTrack.scaleInfo[window.devicePixelRatio] = {
+          scaleX: ratio,
+          scaleY: ratio,
+        };
+      } else {
+        txtTrack.scaleInfo[window.devicePixelRatio] = { scaleX: 1, scaleY: 1 };
+      }
+      // @ts-ignore
+      txtTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, txtTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(txtTrack);
+
+    console.log('获取文字成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.time) {
+    const timeTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.time,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+    if (fabricCanvas.value) {
+      const canvasDom = markRaw(
+        new fabric.Text(new Date().toLocaleString(), {
+          top: 0,
+          left: 0,
+          fill: val.timeInfo?.color,
+        })
+      );
+      setScaleInfo({ canvasDom, track: timeTrack });
+      timeCanvasDom.value.push(canvasDom);
+      handleMoving({ canvasDom, id: timeTrack.id });
+      handleScaling({ canvasDom, id: timeTrack.id });
+      timeTrack.timeInfo = val.timeInfo;
+      // @ts-ignore
+      timeTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, timeTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(timeTrack);
+
+    console.log('获取时间成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.stopwatch) {
+    const stopwatchTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.stopwatch,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+    if (fabricCanvas.value) {
+      const canvasDom = markRaw(
+        new fabric.Text('00天00时00分00秒000毫秒', {
+          top: 0,
+          left: 0,
+          fill: val.stopwatchInfo?.color,
+          // editable: true,
+        })
+      );
+      setScaleInfo({ canvasDom, track: stopwatchTrack });
+      stopwatchCanvasDom.value.push(canvasDom);
+      handleMoving({ canvasDom, id: stopwatchTrack.id });
+      handleScaling({ canvasDom, id: stopwatchTrack.id });
+      stopwatchTrack.stopwatchInfo = val.stopwatchInfo;
+      // @ts-ignore
+      stopwatchTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, stopwatchTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(stopwatchTrack);
+
+    console.log('获取秒表成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.img) {
+    const imgTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.img,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+
+    if (fabricCanvas.value) {
+      if (!val.imgInfo) return;
+      const file = val.imgInfo[0].file!;
+      const { code } = await saveFile({ file, fileName: imgTrack.id });
+      if (code !== 1) return;
+      const imgEl = await new Promise<HTMLImageElement>((resolve) => {
+        const reader = new FileReader();
+        reader.addEventListener(
+          'load',
+          function () {
+            const img = document.createElement('img');
+            img.src = reader.result as string;
+            img.onload = () => {
+              resolve(img);
+            };
+          },
+          false
+        );
+        if (file) {
+          reader.readAsDataURL(file);
+        }
+      });
+
+      const canvasDom = markRaw(
+        new fabric.Image(imgEl, {
+          top: 0,
+          left: 0,
+          width: imgEl.width,
+          height: imgEl.height,
+        })
+      );
+      const ratio = handleScale({ width: imgEl.width, height: imgEl.height });
+      setScaleInfo({ canvasDom, track: imgTrack });
+      handleMoving({ canvasDom, id: imgTrack.id });
+      handleScaling({ canvasDom, id: imgTrack.id });
+      // @ts-ignore
+      imgTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, imgTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(imgTrack);
+
+    console.log('获取图片成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.media) {
+    const mediaVideoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.media,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+    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 url = URL.createObjectURL(file);
+      const videoEl = createVideo({ muted: false, appendChild: true });
+      videoEl.src = url;
+      videoEl.muted = false;
+      bodyAppendChildElArr.value.push(videoEl);
+      const videoRes = await new Promise<HTMLVideoElement>((resolve) => {
+        videoEl.onloadedmetadata = () => {
+          resolve(videoEl);
+        };
+      });
+      // @ts-ignore
+      const stream = videoRes.captureStream();
+      const { canvasDom, scale } = await autoCreateVideo({
+        stream,
+        id: mediaVideoTrack.id,
+      });
+      setScaleInfo({ canvasDom, track: mediaVideoTrack });
+      mediaVideoTrack.videoEl = videoEl;
+      // @ts-ignore
+      mediaVideoTrack.canvasDom = canvasDom;
+      if (stream.getAudioTracks()[0]) {
+        console.log('视频有音频', stream.getAudioTracks()[0]);
+        mediaVideoTrack.audio = 1;
+        const audioTrack: AppRootState['allTrack'][0] = {
+          id: mediaVideoTrack.id,
+          audio: 1,
+          video: 2,
+          mediaName: val.mediaName,
+          type: MediaTypeEnum.media,
+          track: stream.getAudioTracks()[0],
+          trackid: stream.getAudioTracks()[0].id,
+          stream,
+          streamid: stream.id,
+          hidden: true,
+          muted: false,
+          scaleInfo: {},
+        };
+        // @ts-ignore
+        const res = [...appStore.allTrack, audioTrack];
+        appStore.setAllTrack(res);
+        resourceCacheStore.setList(res);
+        handleMixedAudio();
+        // @ts-ignore
+
+        addTrack(audioTrack);
+      }
+    }
+    const res = [...appStore.allTrack, mediaVideoTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+
+    addTrack(mediaVideoTrack);
+
+    console.log('获取视频成功', fabricCanvas.value);
+  }
+  // canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+}
+
+function handleChangeMuted(item: AppRootState['allTrack'][0]) {
+  if (item.videoEl) {
+    const res = !item.videoEl.muted;
+    item.videoEl.muted = res;
+    item.muted = res;
+    resourceCacheStore.setList(appStore.allTrack);
+  }
+}
+
+function handleEdit(item: AppRootState['allTrack'][0]) {
+  console.log('handleEdit', item);
+}
+
+function handleDel(item: AppRootState['allTrack'][0]) {
+  console.log('handleDel', item);
+  if (item.canvasDom !== undefined) {
+    // @ts-ignore
+    fabricCanvas.value?.remove(item.canvasDom);
+    item.videoEl?.remove();
+  }
+  const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
+  appStore.setAllTrack(res);
+  resourceCacheStore.setList(res);
+  delTrack(item);
+}
+
+function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
+  currentMediaType.value = item.type;
+  showMediaModalCpt.value = true;
+}
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.push-wrap {
+  display: flex;
+  justify-content: space-between;
+  margin: 15px auto 0;
+  width: $w-1250;
+  .left {
+    position: relative;
+    display: inline-block;
+    overflow: hidden;
+    box-sizing: border-box;
+    width: $w-960;
+    height: 100%;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+    vertical-align: top;
+
+    .container {
+      position: relative;
+      overflow: hidden;
+      height: 100%;
+      background-color: rgba($color: #000000, $alpha: 0.5);
+      line-height: 0;
+
+      .add-wrap {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        padding: 10px 20px;
+        width: 50%;
+        border-radius: 6px;
+        background-color: white;
+        transform: translate(-50%, -50%);
+      }
+    }
+    .room-control {
+      display: flex;
+      justify-content: space-between;
+      padding: 20px;
+      background-color: papayawhip;
+
+      .info {
+        display: flex;
+        align-items: center;
+
+        .avatar {
+          margin-right: 20px;
+          width: 55px;
+          height: 55px;
+          border-radius: 50%;
+          background-position: center center;
+          background-size: cover;
+          background-repeat: no-repeat;
+        }
+        .detail {
+          display: flex;
+          flex-direction: column;
+          flex-shrink: 0;
+          width: 200px;
+          text-align: initial;
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+          }
+          .bottom {
+            font-size: 14px;
+          }
+        }
+      }
+      .rtc {
+        display: flex;
+        align-items: center;
+        flex: 1;
+        font-size: 14px;
+        .item {
+          display: flex;
+          align-items: center;
+          flex: 1;
+          .txt {
+            flex-shrink: 0;
+            width: 80px;
+          }
+          .down {
+            width: 90px;
+
+            user-select: none;
+          }
+        }
+      }
+      .other {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        font-size: 12px;
+        .top {
+        }
+        .bottom {
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+  .right {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    box-sizing: border-box;
+    margin-left: 10px;
+    width: $w-250;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+
+    .resource-card {
+      position: relative;
+      box-sizing: border-box;
+      margin-bottom: 10px;
+      padding: 10px;
+      width: 100%;
+      height: 290px;
+      border-radius: 6px;
+      background-color: papayawhip;
+      .title {
+        text-align: initial;
+      }
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin: 5px 0;
+        font-size: 14px;
+        // &:hover {
+        //   .control {
+        //     display: flex;
+        //     align-items: center;
+        //   }
+        // }
+        .control {
+          display: flex;
+          align-items: center;
+          .control-item {
+            cursor: pointer;
+            &:not(:last-child) {
+              margin-right: 6px;
+            }
+          }
+        }
+      }
+      .bottom {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        padding: 10px;
+      }
+    }
+    .danmu-card {
+      position: relative;
+      flex: 1;
+      box-sizing: border-box;
+      padding: 10px;
+      width: 100%;
+      border-radius: 6px;
+      background-color: papayawhip;
+      text-align: initial;
+      .title {
+        margin-bottom: 10px;
+      }
+      .list {
+        overflow: scroll;
+        height: 360px;
+
+        @extend %hideScrollbar;
+
+        .item {
+          margin-bottom: 10px;
+          font-size: 12px;
+          .name {
+            color: #9499a0;
+          }
+          .msg {
+            color: #61666d;
+          }
+        }
+      }
+
+      .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;
+          margin: 0 auto;
+          margin-right: 10px;
+          padding: 10px;
+          width: 80%;
+          height: 30px;
+          outline: none;
+          border: 1px solid hsla(0, 0%, 60%, 0.2);
+          border-radius: 4px;
+          background-color: #f1f2f3;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+
+// 屏幕宽度大于1500的时候
+@media screen and (min-width: $w-1500) {
+  .push-wrap {
+    width: $w-1475;
+    .left {
+      width: $w-1150;
+    }
+    .right {
+      width: $w-300;
+    }
+  }
+}
+</style>

+ 20 - 0
src/views/待删/index copy.vue

@@ -0,0 +1,20 @@
+<template>
+  <div>
+    <SrsCpt v-if="liveType === LiveRoomTypeEnum.user_srs"></SrsCpt>
+    <RtcCpt v-else-if="liveType === LiveRoomTypeEnum.user_wertc"></RtcCpt>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { useRoute } from 'vue-router';
+
+import { LiveRoomTypeEnum } from '@/interface';
+
+import RtcCpt from './rtc/index.vue';
+import SrsCpt from './srs/index.vue';
+
+const route = useRoute();
+const liveType = Number(route.query.liveType);
+</script>
+
+<style lang="scss" scoped></style>

+ 0 - 0
src/views/push/rtc/index.vue → src/views/待删/rtc/index.vue


+ 0 - 0
src/views/push/srs/index.vue → src/views/待删/srs/index copy.vue


+ 1644 - 0
src/views/待删/srs/index.vue

@@ -0,0 +1,1644 @@
+<template>
+  <div class="push-wrap">
+    <div
+      ref="topRef"
+      class="left"
+    >
+      <div
+        ref="containerRef"
+        class="container"
+      >
+        <canvas
+          id="pushCanvasRef"
+          ref="pushCanvasRef"
+        ></canvas>
+        <div
+          v-if="appStore.allTrack.filter((item) => !item.hidden).length <= 0"
+          class="add-wrap"
+        >
+          <n-space>
+            <n-button
+              v-for="(item, index) in allMediaTypeList"
+              :key="index"
+              class="item"
+              @click="handleStartMedia(item)"
+            >
+              {{ item.txt }}
+            </n-button>
+          </n-space>
+        </div>
+      </div>
+
+      <div
+        ref="bottomRef"
+        class="room-control"
+      >
+        <div class="info">
+          <div
+            class="avatar"
+            :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
+          ></div>
+          <div class="detail">
+            <div class="top">
+              <n-input-group>
+                <n-input
+                  v-model:value="roomName"
+                  size="small"
+                  placeholder="输入房间名"
+                  :style="{ width: '50%' }"
+                />
+                <n-button
+                  size="small"
+                  type="primary"
+                  @click="confirmRoomName"
+                >
+                  确定
+                </n-button>
+              </n-input-group>
+            </div>
+            <div class="bottom">
+              <span v-if="NODE_ENV === 'development'">
+                {{ mySocketId }}
+              </span>
+            </div>
+          </div>
+        </div>
+        <div class="rtc">
+          <div class="item">
+            <div class="txt">码率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxBitrate"
+                :options="maxBitrate"
+              />
+            </div>
+          </div>
+          <div class="item">
+            <div class="txt">帧率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxFramerate"
+                :options="maxFramerate"
+              />
+            </div>
+          </div>
+          <div class="item">
+            <div class="txt">分辨率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentResolutionRatio"
+                :options="resolutionRatio"
+              />
+            </div>
+          </div>
+        </div>
+        <div class="other">
+          <div class="top">
+            <span class="item">
+              <i class="ico"></i>
+              <span>
+                正在观看:
+                {{
+                  liveUserList.filter((item) => item.id !== mySocketId).length
+                }}
+              </span>
+            </span>
+          </div>
+          <div class="bottom">
+            <n-button
+              v-if="!roomLiving"
+              type="info"
+              size="small"
+              @click="handleStartLive"
+            >
+              开始直播
+            </n-button>
+            <n-button
+              v-else
+              type="error"
+              size="small"
+              @click="endLive"
+            >
+              结束直播
+            </n-button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="right">
+      <div class="resource-card">
+        <div class="title">素材列表</div>
+        <div class="list">
+          <div
+            v-for="(item, index) in appStore.allTrack.filter(
+              (item) => !item.hidden
+            )"
+            :key="index"
+            class="item"
+          >
+            <span class="name">
+              ({{ mediaTypeEnumMap[item.type] }}){{ item.mediaName }}
+            </span>
+            <div class="control">
+              <div
+                v-if="item.audio === 1"
+                class="control-item"
+                @click="handleChangeMuted(item)"
+              >
+                <n-icon size="16">
+                  <VolumeMuteOutline v-if="item.muted"></VolumeMuteOutline>
+                  <VolumeHighOutline v-else></VolumeHighOutline>
+                </n-icon>
+              </div>
+              <div
+                class="control-item"
+                @click="handleEdit(item)"
+              >
+                <n-icon size="16">
+                  <CreateOutline></CreateOutline>
+                </n-icon>
+              </div>
+              <div
+                class="control-item"
+                @click="handleDel(item)"
+              >
+                <n-icon size="16">
+                  <Close></Close>
+                </n-icon>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="bottom">
+          <n-button
+            size="small"
+            type="primary"
+            @click="showSelectMediaModalCpt = true"
+          >
+            添加素材
+          </n-button>
+        </div>
+      </div>
+      <div class="danmu-card">
+        <div class="title">弹幕互动</div>
+        <div class="list-wrap">
+          <div
+            ref="danmuListRef"
+            class="list"
+          >
+            <div
+              v-for="(item, index) in damuList"
+              :key="index"
+              class="item"
+            >
+              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+                <span class="name">
+                  {{ item.userInfo?.username || item.socket_id }}:
+                </span>
+                <span class="msg">{{ item.msg }}</span>
+              </template>
+              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+                <span class="name system">系统通知:</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>进入直播!</span>
+                </span>
+              </template>
+              <template
+                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
+              >
+                <span class="name system">系统通知:</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>离开直播!</span>
+                </span>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="send-msg">
+          <input
+            v-model="danmuStr"
+            class="ipt"
+            @keydown="keydownDanmu"
+          />
+          <n-button
+            type="info"
+            size="small"
+            @click="sendDanmu"
+          >
+            发送
+          </n-button>
+        </div>
+      </div>
+    </div>
+
+    <SelectMediaModalCpt
+      v-if="showSelectMediaModalCpt"
+      :all-media-type-list="allMediaTypeList"
+      @close="showSelectMediaModalCpt = false"
+      @ok="selectMediaOk"
+    ></SelectMediaModalCpt>
+
+    <MediaModalCpt
+      v-if="showMediaModalCpt"
+      :media-type="currentMediaType"
+      @close="showMediaModalCpt = false"
+      @ok="addMediaOk"
+    ></MediaModalCpt>
+    <OpenMicophoneTipCpt
+      v-if="showOpenMicophoneTipCpt"
+      @close="showOpenMicophoneTipCpt = false"
+    ></OpenMicophoneTipCpt>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {
+  Close,
+  CreateOutline,
+  VolumeHighOutline,
+  VolumeMuteOutline,
+} from '@vicons/ionicons5';
+import { fabric } from 'fabric';
+import { UploadFileInfo } from 'naive-ui';
+import {
+  Raw,
+  markRaw,
+  onMounted,
+  onUnmounted,
+  reactive,
+  ref,
+  watch,
+} from 'vue';
+import * as workerTimers from 'worker-timers';
+
+import { mediaTypeEnumMap } from '@/constant';
+import { usePush } from '@/hooks/use-push';
+import { useRTCParams } from '@/hooks/use-rtc-params';
+import { DanmuMsgTypeEnum, LiveRoomTypeEnum, MediaTypeEnum } from '@/interface';
+import { AppRootState, useAppStore } from '@/store/app';
+import { useResourceCacheStore } from '@/store/cache';
+import { useUserStore } from '@/store/user';
+import {
+  createVideo,
+  formatDownTime,
+  generateBase64,
+  getRandomEnglishString,
+  readFile,
+  saveFile,
+} from '@/utils';
+import { NODE_ENV } from 'script/constant';
+
+import MediaModalCpt from '../mediaModal/index.vue';
+import OpenMicophoneTipCpt from '../openMicophoneTip/index.vue';
+import SelectMediaModalCpt from '../selectMediaModal/index.vue';
+
+const userStore = useUserStore();
+const appStore = useAppStore();
+const resourceCacheStore = useResourceCacheStore();
+const { maxBitrate, maxFramerate, resolutionRatio, allMediaTypeList } =
+  useRTCParams();
+
+const {
+  confirmRoomName,
+  startLive,
+  endLive,
+  sendDanmu,
+  keydownDanmu,
+  addTrack,
+  delTrack,
+  mySocketId,
+  lastCoverImg,
+  canvasVideoStream,
+  roomLiving,
+  currentResolutionRatio,
+  currentMaxBitrate,
+  currentMaxFramerate,
+  danmuStr,
+  roomName,
+  damuList,
+  liveUserList,
+} = usePush();
+
+const currentMediaType = ref(MediaTypeEnum.camera);
+const showOpenMicophoneTipCpt = ref(false);
+const showSelectMediaModalCpt = ref(false);
+const showMediaModalCpt = ref(false);
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const danmuListRef = ref<HTMLDivElement>();
+const containerRef = ref<HTMLDivElement>();
+const pushCanvasRef = ref<HTMLCanvasElement>();
+const webaudioVideo = ref<HTMLVideoElement>();
+const fabricCanvas = ref<fabric.Canvas>();
+const audioCtx = ref<AudioContext>();
+const startTime = ref(+new Date());
+// const startTime = ref(1692807352565); // 1693027352565
+
+const timeCanvasDom = ref<Raw<fabric.Text>[]>([]);
+const stopwatchCanvasDom = ref<Raw<fabric.Text>[]>([]);
+const wrapSize = reactive({
+  width: 0,
+  height: 0,
+});
+const workerTimerId = ref(-1);
+const bodyAppendChildElArr = ref<HTMLElement[]>([]);
+
+watch(
+  () => damuList.value.length,
+  () => {
+    setTimeout(() => {
+      if (danmuListRef.value) {
+        danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight;
+      }
+    }, 0);
+  }
+);
+
+onMounted(() => {
+  setTimeout(() => {
+    scrollTo(0, 0);
+  }, 100);
+  initUserMedia();
+  initCanvas();
+  handleCache();
+});
+
+onUnmounted(() => {
+  bodyAppendChildElArr.value.forEach((el) => {
+    el.remove();
+  });
+  clearFrame();
+});
+
+function initUserMedia() {
+  navigator.mediaDevices
+    .getUserMedia({
+      video: true,
+      audio: false,
+    })
+    .then(() => {
+      console.log('初始化获取摄像头成功');
+    })
+    .catch(() => {
+      console.log('初始化获取摄像头失败');
+    })
+    .finally(() => {
+      navigator.mediaDevices
+        .getUserMedia({
+          video: false,
+          audio: true,
+        })
+        .then(() => {
+          console.log('初始化获取麦克风成功');
+        })
+        .catch(() => {
+          console.log('初始化获取麦克风失败');
+          showOpenMicophoneTipCpt.value = true;
+        });
+    });
+}
+
+function renderAll() {
+  timeCanvasDom.value.forEach((item) => {
+    item.text = new Date().toLocaleString();
+  });
+  stopwatchCanvasDom.value.forEach((item) => {
+    item.text = formatDownTime({
+      endTime: +new Date(),
+      startTime: startTime.value,
+      showMs: true,
+    });
+  });
+  fabricCanvas.value?.renderAll();
+}
+
+function clearFrame() {
+  if (workerTimerId.value !== -1) {
+    workerTimers.clearInterval(workerTimerId.value);
+  }
+}
+
+function renderFrame() {
+  const delay = 1000 / 60; // 16.666666666666668
+  workerTimerId.value = workerTimers.setInterval(() => {
+    renderAll();
+  }, delay);
+  console.log('workerTimerId.value', workerTimerId.value);
+}
+
+function handleMixedAudio() {
+  console.log('handleMixedAudio');
+  const allAudioTrack = appStore.allTrack.filter((item) => item.audio === 1);
+  audioCtx.value = new AudioContext();
+  const gainNode = audioCtx.value.createGain();
+  allAudioTrack.forEach((item) => {
+    if (!audioCtx.value || !item.stream) return;
+    const audioInput = audioCtx.value.createMediaStreamSource(item.stream);
+    audioInput.connect(gainNode);
+    console.log('混流', item.stream?.id, item.stream);
+  });
+
+  const destination = audioCtx.value.createMediaStreamDestination();
+  if (canvasVideoStream.value?.getAudioTracks()[0]) {
+    canvasVideoStream.value.removeTrack(
+      canvasVideoStream.value?.getAudioTracks()[0]
+    );
+  }
+  canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
+  // streamTmp = destination.stream;
+  if (canvasVideoStream.value?.getAudioTracks()[0]) {
+    canvasVideoStream.value.removeTrack(
+      canvasVideoStream.value?.getAudioTracks()[0]
+    );
+    canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
+  } else {
+    canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
+  }
+  console.log(
+    canvasVideoStream.value?.getAudioTracks(),
+    'canvasVideoStreamcanvasVideoStream'
+  );
+  gainNode.connect(destination);
+  if (webaudioVideo.value) {
+    webaudioVideo.value.remove();
+  }
+  webaudioVideo.value = createVideo({ appendChild: true });
+  webaudioVideo.value.className = 'web-audio-video';
+  webaudioVideo.value.srcObject = destination.stream;
+}
+
+function handleStartLive() {
+  handleMixedAudio();
+  lastCoverImg.value = generateBase64(pushCanvasRef.value!);
+  startLive({ type: LiveRoomTypeEnum.user_srs, receiver: mySocketId });
+}
+
+function handleScale({ width, height }: { width: number; height: number }) {
+  const resolutionHeight =
+    currentResolutionRatio.value * window.devicePixelRatio;
+  const resolutionWidth =
+    currentResolutionRatio.value *
+    window.devicePixelRatio *
+    appStore.videoRatio;
+  let ratio = 1;
+  if (width > resolutionWidth) {
+    const r1 = resolutionWidth / width;
+    ratio = r1;
+  }
+  if (height > resolutionHeight) {
+    const r1 = resolutionHeight / height;
+    if (ratio > r1) {
+      ratio = r1;
+    }
+  }
+  return ratio;
+}
+
+function autoCreateVideo({
+  stream,
+  id,
+  rect,
+  muted,
+}: {
+  stream: MediaStream;
+  id: string;
+  rect?: { left: number; top: number };
+  muted?: boolean;
+}) {
+  console.warn('autoCreateVideo', id);
+  const videoEl = createVideo({ appendChild: true });
+  if (muted !== undefined) {
+    videoEl.muted = muted;
+  }
+  videoEl.srcObject = stream;
+  bodyAppendChildElArr.value.push(videoEl);
+  return new Promise<{
+    canvasDom: fabric.Image;
+    videoEl: HTMLVideoElement;
+    scale: number;
+  }>((resolve) => {
+    videoEl.onloadedmetadata = () => {
+      const width = stream.getVideoTracks()[0].getSettings().width!;
+      const height = stream.getVideoTracks()[0].getSettings().height!;
+      const ratio = handleScale({ width, height });
+      videoEl.width = width;
+      videoEl.height = height;
+
+      const canvasDom = markRaw(
+        new fabric.Image(videoEl, {
+          top: rect?.top || 0,
+          left: rect?.left || 0,
+          width,
+          height,
+        })
+      );
+      console.log(
+        '初始化',
+        ratio,
+        canvasDom.width,
+        canvasDom.height,
+        width * ratio,
+        height * ratio,
+        canvasDom
+      );
+      handleMoving({ canvasDom, id });
+      handleScaling({ canvasDom, id });
+      canvasDom.scale(ratio / window.devicePixelRatio);
+      fabricCanvas.value!.add(canvasDom);
+
+      resolve({ canvasDom, scale: ratio, videoEl });
+    };
+  });
+}
+
+watch(
+  () => currentResolutionRatio.value,
+  (newHeight, oldHeight) => {
+    changeCanvasAttr({ newHeight, oldHeight });
+  }
+);
+
+// 容器宽高,1280*720,即720p
+// canvas容器宽高,2560*1440,即1440p
+
+// ======
+// 容器宽高,960*540,即540p
+// dom宽高,640*480
+// canvas容器宽高,960*540,即540p
+// 将dom绘制到容器里,此时dom的大小就是640*480
+// 需求,不管切换多少分辨率,我要看到的dom都是一样大小,即
+// 960*540时,dom是640*480
+// 1280*720时,dom不能是640*480了,因为这样他就会对比上一个分辨率的dom看起来小了,960/1280=0.75,540/720=0.75,
+// 其实就是分辨率变大了,我们就要将图片也变大,即图片的宽是640/0.75=853.4,高是480/0.75=640
+// 坐标变化,960*540时,dom坐标是100,100
+// 1280*720时,dom的坐标不能再是100,100了,否则对比上一个分辨率看起来偏
+
+function changeCanvasAttr({
+  newHeight,
+  oldHeight,
+}: {
+  newHeight: number;
+  oldHeight: number;
+}) {
+  if (fabricCanvas.value) {
+    const resolutionHeight =
+      currentResolutionRatio.value / window.devicePixelRatio;
+    const resolutionWidth =
+      (currentResolutionRatio.value / window.devicePixelRatio) *
+      appStore.videoRatio;
+    fabricCanvas.value.setWidth(resolutionWidth);
+    fabricCanvas.value.setHeight(resolutionHeight);
+    appStore.allTrack.forEach((iten) => {
+      console.log('当前类型', iten.type);
+      const item = iten.canvasDom;
+
+      if (item) {
+        // 分辨率变小了,将图片变小
+        if (newHeight < oldHeight) {
+          const ratio2 = oldHeight / newHeight;
+          item.left = item.left! / ratio2;
+          item.top = item.top! / ratio2;
+        } else {
+          // 分辨率变大了,将图片变大
+          const ratio2 = oldHeight / newHeight;
+          item.left = item.left! / ratio2;
+          item.top = item.top! / ratio2;
+        }
+      }
+    });
+    appStore.allTrack.forEach((iten) => {
+      console.log('当前类型', iten.type);
+      const item = iten.canvasDom;
+
+      if (item) {
+        // 分辨率变小了,将图片变小
+        if (newHeight < oldHeight) {
+          const ratio = newHeight / oldHeight;
+          const ratio1 = (item.scaleX || 1) * ratio;
+          item.scale(ratio1);
+        } else {
+          // 分辨率变大了,将图片变大
+          const ratio = newHeight / oldHeight;
+          const ratio1 = (item.scaleX || 1) * ratio;
+          item.scale(ratio1);
+        }
+      }
+    });
+
+    changeCanvasStyle();
+  }
+}
+
+function changeCanvasStyle() {
+  // @ts-ignore
+  fabricCanvas.value.wrapperEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.wrapperEl.style.height = `${wrapSize.height}px`;
+  // @ts-ignore
+  fabricCanvas.value.lowerCanvasEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.lowerCanvasEl.style.height = `${wrapSize.height}px`;
+  // @ts-ignore
+  fabricCanvas.value.upperCanvasEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.upperCanvasEl.style.height = `${wrapSize.height}px`;
+}
+
+function initCanvas() {
+  const resolutionHeight =
+    currentResolutionRatio.value / window.devicePixelRatio;
+  const resolutionWidth =
+    (currentResolutionRatio.value / window.devicePixelRatio) *
+    appStore.videoRatio;
+  const wrapWidth = containerRef.value!.getBoundingClientRect().width;
+  // const wrapWidth = 1920;
+  const ratio = wrapWidth / resolutionWidth;
+  const wrapHeight = resolutionHeight * ratio;
+  // const wrapHeight = 1080;
+  // lower-canvas: 实际的canvas画面,也就是pushCanvasRef
+  // upper-canvas: 操作时候的canvas
+  const ins = markRaw(new fabric.Canvas(pushCanvasRef.value!));
+  ins.setWidth(resolutionWidth);
+  ins.setHeight(resolutionHeight);
+  console.log('initCanvas', { resolutionWidth, resolutionHeight });
+  ins.setBackgroundColor('black', () => {
+    console.log('setBackgroundColor回调');
+  });
+  wrapSize.width = wrapWidth;
+  wrapSize.height = wrapHeight;
+  fabricCanvas.value = ins;
+  renderFrame();
+  changeCanvasStyle();
+}
+
+/**
+ * 1: {scaleX: 1, scaleY: 1}
+ * 2: {scaleX: 0.5, scaleY: 0.5}
+ * 3: {scaleX: 0.3333333333333333, scaleY: 0.3333333333333333}
+ * 4: {scaleX: 0.25, scaleY: 0.25}
+ */
+
+/**
+ * 二倍屏即1px里面有2个像素;三倍屏1px里面有3个像素,以此类推
+ * 一个图片,宽高都是100px
+ * 一倍屏展示:100px等于100个像素,一比一展示
+ * 二倍屏展示:100px等于100个像素,二比一展示,即在二倍屏的100px看起来会比一倍屏的100px小一倍
+ * 如果需要在一杯和二倍屏幕的时候看的大小都一样:
+ * 1,在二倍屏的时候,需要将100px放大一倍,即200px;
+ * 2,在一倍屏的时候,需要将100px缩小一百,即50px;
+ */
+function handleScaling({ canvasDom, id }) {
+  canvasDom.on('scaling', () => {
+    appStore.allTrack.forEach((item) => {
+      if (id === item.id) {
+        item.scaleInfo[window.devicePixelRatio] = {
+          scaleX: canvasDom.scaleX || 1,
+          scaleY: canvasDom.scaleY || 1,
+        };
+        Object.keys(item.scaleInfo).forEach((iten) => {
+          if (window.devicePixelRatio !== Number(iten)) {
+            if (window.devicePixelRatio > Number(iten)) {
+              item.scaleInfo[iten] = {
+                scaleX:
+                  item.scaleInfo[window.devicePixelRatio].scaleX *
+                  window.devicePixelRatio,
+                scaleY:
+                  item.scaleInfo[window.devicePixelRatio].scaleY *
+                  window.devicePixelRatio,
+              };
+            } else {
+              if (window.devicePixelRatio === 1) {
+                item.scaleInfo[iten] = {
+                  scaleX: item.scaleInfo[1].scaleX / Number(iten),
+                  scaleY: item.scaleInfo[1].scaleY / Number(iten),
+                };
+              } else {
+                item.scaleInfo[iten] = {
+                  scaleX: item.scaleInfo[1].scaleX * Number(iten),
+                  scaleY: item.scaleInfo[1].scaleY * Number(iten),
+                };
+              }
+            }
+          }
+        });
+      }
+    });
+    resourceCacheStore.setList(appStore.allTrack);
+  });
+}
+function handleMoving({
+  canvasDom,
+  id,
+}: {
+  canvasDom: fabric.Image | fabric.Text;
+  id: string;
+}) {
+  canvasDom.on('moving', () => {
+    console.log(
+      'moving',
+      canvasDom.width,
+      canvasDom.height,
+      canvasDom.scaleX,
+      canvasDom.scaleY
+    );
+    appStore.allTrack.forEach((item) => {
+      if (id === item.id) {
+        item.rect = {
+          top: (canvasDom.top || 0) * window.devicePixelRatio,
+          left: (canvasDom.left || 0) * window.devicePixelRatio,
+        };
+      }
+    });
+    resourceCacheStore.setList(appStore.allTrack);
+  });
+}
+
+async function handleCache() {
+  const res: AppRootState['allTrack'] = [];
+  const queue: any[] = [];
+  resourceCacheStore.list.forEach((item) => {
+    // @ts-ignore
+    const obj: AppRootState['allTrack'][0] = {};
+    obj.audio = item.audio;
+    obj.video = item.video;
+    obj.id = item.id;
+    obj.type = item.type;
+    obj.hidden = item.hidden;
+    obj.mediaName = item.mediaName;
+    obj.muted = item.muted;
+    obj.rect = item.rect;
+    obj.scaleInfo = item.scaleInfo;
+    obj.stopwatchInfo = item.stopwatchInfo;
+
+    async function handleMediaVideo() {
+      const { code, file } = await readFile(item.id);
+      if (code === 1 && file) {
+        const url = URL.createObjectURL(file);
+        const videoEl = createVideo({
+          muted: item.muted ? item.muted : false,
+          appendChild: true,
+        });
+        videoEl.src = url;
+        bodyAppendChildElArr.value.push(videoEl);
+        await new Promise((resolve) => {
+          videoEl.onloadedmetadata = () => {
+            const stream = videoEl
+              // @ts-ignore
+              .captureStream();
+            const width = stream.getVideoTracks()[0].getSettings().width!;
+            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,
+                left: (item.rect?.left || 0) / window.devicePixelRatio,
+                width,
+                height,
+              })
+            );
+            handleMoving({ canvasDom, id: item.id });
+            handleScaling({ canvasDom, id: item.id });
+            canvasDom.scale(
+              item.scaleInfo[window.devicePixelRatio].scaleX || 1
+            );
+            fabricCanvas.value!.add(canvasDom);
+            obj.videoEl = videoEl;
+            obj.canvasDom = canvasDom;
+            resolve({ videoEl, canvasDom });
+          };
+        });
+        const stream = videoEl
+          // @ts-ignore
+          .captureStream() as MediaStream;
+        obj.stream = stream;
+        obj.streamid = stream.id;
+        obj.track = stream.getVideoTracks()[0];
+        obj.trackid = stream.getVideoTracks()[0].id;
+      } else {
+        console.error('读取文件失败');
+      }
+    }
+
+    async function handleImg() {
+      const { code, file } = await readFile(item.id);
+      if (code === 1 && file) {
+        const imgEl = await new Promise<HTMLImageElement>((resolve) => {
+          const reader = new FileReader();
+          reader.addEventListener(
+            'load',
+            function () {
+              const img = document.createElement('img');
+              img.src = reader.result as string;
+              img.onload = () => {
+                resolve(img);
+              };
+            },
+            false
+          );
+          if (file) {
+            reader.readAsDataURL(file);
+          }
+        });
+        if (fabricCanvas.value) {
+          const canvasDom = markRaw(
+            new fabric.Image(imgEl, {
+              top: (item.rect?.top || 0) / window.devicePixelRatio,
+              left: (item.rect?.left || 0) / window.devicePixelRatio,
+              width: imgEl.width,
+              height: imgEl.height,
+            })
+          );
+          handleMoving({ canvasDom, id: obj.id });
+          handleScaling({ canvasDom, id: obj.id });
+          canvasDom.scale(item.scaleInfo[window.devicePixelRatio].scaleX || 1);
+          fabricCanvas.value.add(canvasDom);
+          obj.canvasDom = canvasDom;
+        }
+      } else {
+        console.error('读取文件失败');
+      }
+    }
+    if (item.type === MediaTypeEnum.media && item.video === 1) {
+      queue.push(handleMediaVideo());
+    } else if (item.type === MediaTypeEnum.img) {
+      queue.push(handleImg());
+    } else if (item.type === MediaTypeEnum.txt) {
+      obj.txtInfo = item.txtInfo;
+      obj.scaleInfo = item.scaleInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text(item.txtInfo?.txt || '', {
+            top: (item.rect?.top || 0) / window.devicePixelRatio,
+            left: (item.rect?.left || 0) / window.devicePixelRatio,
+            fill: item.txtInfo?.color,
+          })
+        );
+        handleMoving({ canvasDom, id: obj.id });
+        handleScaling({ canvasDom, id: obj.id });
+        // fabric.Text类型不能除以分辨率
+        canvasDom.scale(item.scaleInfo[window.devicePixelRatio].scaleX);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+      }
+    } else if (item.type === MediaTypeEnum.time) {
+      obj.timeInfo = item.timeInfo;
+      obj.scaleInfo = item.scaleInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text(new Date().toLocaleString(), {
+            top: (item.rect?.top || 0) / window.devicePixelRatio,
+            left: (item.rect?.left || 0) / window.devicePixelRatio,
+            fill: item.timeInfo?.color,
+          })
+        );
+        timeCanvasDom.value.push(canvasDom);
+        handleMoving({ canvasDom, id: obj.id });
+        handleScaling({ canvasDom, id: obj.id });
+        // fabric.Text类型不能除以分辨率
+        canvasDom.scale(item.scaleInfo[window.devicePixelRatio].scaleX);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+      }
+    } else if (item.type === MediaTypeEnum.stopwatch) {
+      obj.stopwatchInfo = item.stopwatchInfo;
+      obj.scaleInfo = item.scaleInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text('00天00时00分00秒000毫秒', {
+            top: (item.rect?.top || 0) / window.devicePixelRatio,
+            left: (item.rect?.left || 0) / window.devicePixelRatio,
+            fill: item.stopwatchInfo?.color,
+          })
+        );
+        stopwatchCanvasDom.value.push(canvasDom);
+        handleMoving({ canvasDom, id: obj.id });
+        handleScaling({ canvasDom, id: obj.id });
+        // fabric.Text类型不能除以分辨率
+        canvasDom.scale(item.scaleInfo[window.devicePixelRatio].scaleX);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+      }
+    }
+    res.push(obj);
+  });
+  await Promise.all(queue);
+  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+  appStore.setAllTrack(res);
+}
+
+function selectMediaOk(val: MediaTypeEnum) {
+  showMediaModalCpt.value = true;
+  showSelectMediaModalCpt.value = false;
+  currentMediaType.value = val;
+}
+
+function setScaleInfo({ track, canvasDom }) {
+  [1, 2, 3, 4].forEach((devicePixelRatio) => {
+    track.scaleInfo[devicePixelRatio] = {
+      scaleX: 1 / devicePixelRatio,
+      scaleY: 1 / devicePixelRatio,
+    };
+  });
+  if (window.devicePixelRatio !== 1) {
+    const ratio = 1 / window.devicePixelRatio;
+    canvasDom.scale(ratio);
+    track.scaleInfo[window.devicePixelRatio] = {
+      scaleX: ratio,
+      scaleY: ratio,
+    };
+  }
+}
+
+async function addMediaOk(val: {
+  type: MediaTypeEnum;
+  deviceId: string;
+  mediaName: string;
+  txtInfo?: { txt: string; color: string };
+  timeInfo?: { color: string };
+  stopwatchInfo?: { color: string };
+  imgInfo?: UploadFileInfo[];
+  mediaInfo?: UploadFileInfo[];
+}) {
+  if (!audioCtx.value) {
+    audioCtx.value = new AudioContext();
+  }
+  showMediaModalCpt.value = false;
+  if (val.type === MediaTypeEnum.screen) {
+    const event = await navigator.mediaDevices.getDisplayMedia({
+      video: {
+        deviceId: val.deviceId,
+        // displaySurface: 'monitor', // browser默认标签页;window默认窗口;monitor默认整个屏幕
+      },
+      audio: true,
+    });
+
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.screen,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+
+    const { canvasDom, videoEl, scale } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+    });
+    setScaleInfo({ canvasDom, track: videoTrack });
+    videoTrack.videoEl = videoEl;
+    // @ts-ignore
+    videoTrack.canvasDom = canvasDom;
+
+    const audio = event.getAudioTracks();
+    if (audio.length) {
+      videoTrack.audio = 1;
+      const audioTrack: AppRootState['allTrack'][0] = {
+        id: videoTrack.id,
+        audio: 1,
+        video: 2,
+        mediaName: val.mediaName,
+        type: MediaTypeEnum.screen,
+        track: event.getAudioTracks()[0],
+        trackid: event.getAudioTracks()[0].id,
+        stream: event,
+        streamid: event.id,
+        hidden: true,
+        muted: false,
+        scaleInfo: {},
+      };
+      const res = [...appStore.allTrack, videoTrack, audioTrack];
+      appStore.setAllTrack(res);
+      resourceCacheStore.setList(res);
+      handleMixedAudio();
+      // @ts-ignore
+      addTrack(videoTrack);
+      // @ts-ignore
+      addTrack(audioTrack);
+    } else {
+      const res = [...appStore.allTrack, videoTrack];
+      appStore.setAllTrack(res);
+      resourceCacheStore.setList(res);
+      // @ts-ignore
+      addTrack(videoTrack);
+    }
+
+    console.log('获取窗口成功');
+  } else if (val.type === MediaTypeEnum.camera) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: {
+        deviceId: val.deviceId,
+      },
+      audio: false,
+    });
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.camera,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+    const { canvasDom, videoEl, scale } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+    });
+    setScaleInfo({ canvasDom, track: videoTrack });
+    videoTrack.videoEl = videoEl;
+    // @ts-ignore
+    videoTrack.canvasDom = canvasDom;
+
+    const res = [...appStore.allTrack, videoTrack];
+    appStore.setAllTrack(res);
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(videoTrack);
+    console.log('获取摄像头成功');
+  } else if (val.type === MediaTypeEnum.microphone) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: false,
+      audio: { deviceId: val.deviceId },
+    });
+    const audioTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 1,
+      video: 2,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.microphone,
+      track: event.getAudioTracks()[0],
+      trackid: event.getAudioTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+    const res = [...appStore.allTrack, audioTrack];
+    appStore.setAllTrack(res);
+    resourceCacheStore.setList(res);
+    handleMixedAudio();
+    // @ts-ignore
+    addTrack(audioTrack);
+
+    console.log('获取麦克风成功');
+  } else if (val.type === MediaTypeEnum.txt) {
+    const txtTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.txt,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+    if (fabricCanvas.value) {
+      console.log('val.txtInfo?.txt ', val.txtInfo?.txt);
+      const canvasDom = markRaw(
+        new fabric.Text(val.txtInfo?.txt || '', {
+          top: 0,
+          left: 0,
+          fill: val.txtInfo?.color,
+        })
+      );
+      handleMoving({ canvasDom, id: txtTrack.id });
+      handleScaling({ canvasDom, id: txtTrack.id });
+      txtTrack.txtInfo = val.txtInfo;
+      if (window.devicePixelRatio !== 1) {
+        const ratio = 1 / window.devicePixelRatio;
+        canvasDom.scale(ratio);
+        txtTrack.scaleInfo[window.devicePixelRatio] = {
+          scaleX: ratio,
+          scaleY: ratio,
+        };
+      } else {
+        txtTrack.scaleInfo[window.devicePixelRatio] = { scaleX: 1, scaleY: 1 };
+      }
+      // @ts-ignore
+      txtTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, txtTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(txtTrack);
+
+    console.log('获取文字成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.time) {
+    const timeTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.time,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+    if (fabricCanvas.value) {
+      const canvasDom = markRaw(
+        new fabric.Text(new Date().toLocaleString(), {
+          top: 0,
+          left: 0,
+          fill: val.timeInfo?.color,
+        })
+      );
+      setScaleInfo({ canvasDom, track: timeTrack });
+      timeCanvasDom.value.push(canvasDom);
+      handleMoving({ canvasDom, id: timeTrack.id });
+      handleScaling({ canvasDom, id: timeTrack.id });
+      timeTrack.timeInfo = val.timeInfo;
+      // @ts-ignore
+      timeTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, timeTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(timeTrack);
+
+    console.log('获取时间成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.stopwatch) {
+    const stopwatchTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.stopwatch,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+    if (fabricCanvas.value) {
+      const canvasDom = markRaw(
+        new fabric.Text('00天00时00分00秒000毫秒', {
+          top: 0,
+          left: 0,
+          fill: val.stopwatchInfo?.color,
+          // editable: true,
+        })
+      );
+      setScaleInfo({ canvasDom, track: stopwatchTrack });
+      stopwatchCanvasDom.value.push(canvasDom);
+      handleMoving({ canvasDom, id: stopwatchTrack.id });
+      handleScaling({ canvasDom, id: stopwatchTrack.id });
+      stopwatchTrack.stopwatchInfo = val.stopwatchInfo;
+      // @ts-ignore
+      stopwatchTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, stopwatchTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(stopwatchTrack);
+
+    console.log('获取秒表成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.img) {
+    const imgTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.img,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+
+    if (fabricCanvas.value) {
+      if (!val.imgInfo) return;
+      const file = val.imgInfo[0].file!;
+      const { code } = await saveFile({ file, fileName: imgTrack.id });
+      if (code !== 1) return;
+      const imgEl = await new Promise<HTMLImageElement>((resolve) => {
+        const reader = new FileReader();
+        reader.addEventListener(
+          'load',
+          function () {
+            const img = document.createElement('img');
+            img.src = reader.result as string;
+            img.onload = () => {
+              resolve(img);
+            };
+          },
+          false
+        );
+        if (file) {
+          reader.readAsDataURL(file);
+        }
+      });
+
+      const canvasDom = markRaw(
+        new fabric.Image(imgEl, {
+          top: 0,
+          left: 0,
+          width: imgEl.width,
+          height: imgEl.height,
+        })
+      );
+      const ratio = handleScale({ width: imgEl.width, height: imgEl.height });
+      setScaleInfo({ canvasDom, track: imgTrack });
+      handleMoving({ canvasDom, id: imgTrack.id });
+      handleScaling({ canvasDom, id: imgTrack.id });
+      // @ts-ignore
+      imgTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+    }
+
+    const res = [...appStore.allTrack, imgTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(imgTrack);
+
+    console.log('获取图片成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.media) {
+    const mediaVideoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.media,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+    };
+    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 url = URL.createObjectURL(file);
+      const videoEl = createVideo({ muted: false, appendChild: true });
+      videoEl.src = url;
+      videoEl.muted = false;
+      bodyAppendChildElArr.value.push(videoEl);
+      const videoRes = await new Promise<HTMLVideoElement>((resolve) => {
+        videoEl.onloadedmetadata = () => {
+          resolve(videoEl);
+        };
+      });
+      // @ts-ignore
+      const stream = videoRes.captureStream();
+      const { canvasDom, scale } = await autoCreateVideo({
+        stream,
+        id: mediaVideoTrack.id,
+      });
+      setScaleInfo({ canvasDom, track: mediaVideoTrack });
+      mediaVideoTrack.videoEl = videoEl;
+      // @ts-ignore
+      mediaVideoTrack.canvasDom = canvasDom;
+      if (stream.getAudioTracks()[0]) {
+        console.log('视频有音频', stream.getAudioTracks()[0]);
+        mediaVideoTrack.audio = 1;
+        const audioTrack: AppRootState['allTrack'][0] = {
+          id: mediaVideoTrack.id,
+          audio: 1,
+          video: 2,
+          mediaName: val.mediaName,
+          type: MediaTypeEnum.media,
+          track: stream.getAudioTracks()[0],
+          trackid: stream.getAudioTracks()[0].id,
+          stream,
+          streamid: stream.id,
+          hidden: true,
+          muted: false,
+          scaleInfo: {},
+        };
+        // @ts-ignore
+        const res = [...appStore.allTrack, audioTrack];
+        appStore.setAllTrack(res);
+        resourceCacheStore.setList(res);
+        handleMixedAudio();
+        // @ts-ignore
+
+        addTrack(audioTrack);
+      }
+    }
+    const res = [...appStore.allTrack, mediaVideoTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+
+    addTrack(mediaVideoTrack);
+
+    console.log('获取视频成功', fabricCanvas.value);
+  }
+  // canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+}
+
+function handleChangeMuted(item: AppRootState['allTrack'][0]) {
+  if (item.videoEl) {
+    const res = !item.videoEl.muted;
+    item.videoEl.muted = res;
+    item.muted = res;
+    resourceCacheStore.setList(appStore.allTrack);
+  }
+}
+
+function handleEdit(item: AppRootState['allTrack'][0]) {
+  console.log('handleEdit', item);
+}
+
+function handleDel(item: AppRootState['allTrack'][0]) {
+  console.log('handleDel', item);
+  if (item.canvasDom !== undefined) {
+    // @ts-ignore
+    fabricCanvas.value?.remove(item.canvasDom);
+    item.videoEl?.remove();
+  }
+  const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
+  appStore.setAllTrack(res);
+  resourceCacheStore.setList(res);
+  delTrack(item);
+}
+
+function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
+  currentMediaType.value = item.type;
+  showMediaModalCpt.value = true;
+}
+</script>
+
+<style lang="scss" scoped>
+.push-wrap {
+  display: flex;
+  justify-content: space-between;
+  margin: 15px auto 0;
+  width: $w-1250;
+  .left {
+    position: relative;
+    display: inline-block;
+    overflow: hidden;
+    box-sizing: border-box;
+    width: $w-960;
+    height: 100%;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+    vertical-align: top;
+
+    .container {
+      position: relative;
+      overflow: hidden;
+      height: 100%;
+      background-color: rgba($color: #000000, $alpha: 0.5);
+      line-height: 0;
+
+      .add-wrap {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        padding: 10px 20px;
+        width: 50%;
+        border-radius: 6px;
+        background-color: white;
+        transform: translate(-50%, -50%);
+      }
+    }
+    .room-control {
+      display: flex;
+      justify-content: space-between;
+      padding: 20px;
+      background-color: papayawhip;
+
+      .info {
+        display: flex;
+        align-items: center;
+
+        .avatar {
+          margin-right: 20px;
+          width: 55px;
+          height: 55px;
+          border-radius: 50%;
+          background-position: center center;
+          background-size: cover;
+          background-repeat: no-repeat;
+        }
+        .detail {
+          display: flex;
+          flex-direction: column;
+          flex-shrink: 0;
+          width: 200px;
+          text-align: initial;
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+          }
+          .bottom {
+            font-size: 14px;
+          }
+        }
+      }
+      .rtc {
+        display: flex;
+        align-items: center;
+        flex: 1;
+        font-size: 14px;
+        .item {
+          display: flex;
+          align-items: center;
+          flex: 1;
+          .txt {
+            flex-shrink: 0;
+            width: 80px;
+          }
+          .down {
+            width: 90px;
+
+            user-select: none;
+          }
+        }
+      }
+      .other {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        font-size: 12px;
+        .top {
+        }
+        .bottom {
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+  .right {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    box-sizing: border-box;
+    margin-left: 10px;
+    width: $w-250;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+
+    .resource-card {
+      position: relative;
+      box-sizing: border-box;
+      margin-bottom: 10px;
+      padding: 10px;
+      width: 100%;
+      height: 290px;
+      border-radius: 6px;
+      background-color: papayawhip;
+      .title {
+        text-align: initial;
+      }
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin: 5px 0;
+        font-size: 14px;
+        // &:hover {
+        //   .control {
+        //     display: flex;
+        //     align-items: center;
+        //   }
+        // }
+        .control {
+          display: flex;
+          align-items: center;
+          .control-item {
+            cursor: pointer;
+            &:not(:last-child) {
+              margin-right: 6px;
+            }
+          }
+        }
+      }
+      .bottom {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        padding: 10px;
+      }
+    }
+    .danmu-card {
+      position: relative;
+      flex: 1;
+      box-sizing: border-box;
+      padding: 10px;
+      width: 100%;
+      border-radius: 6px;
+      background-color: papayawhip;
+      text-align: initial;
+      .title {
+        margin-bottom: 10px;
+      }
+      .list {
+        overflow: scroll;
+        height: 360px;
+
+        @extend %hideScrollbar;
+
+        .item {
+          margin-bottom: 10px;
+          font-size: 12px;
+          .name {
+            color: #9499a0;
+          }
+          .msg {
+            color: #61666d;
+          }
+        }
+      }
+
+      .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;
+          margin: 0 auto;
+          margin-right: 10px;
+          padding: 10px;
+          width: 80%;
+          height: 30px;
+          outline: none;
+          border: 1px solid hsla(0, 0%, 60%, 0.2);
+          border-radius: 4px;
+          background-color: #f1f2f3;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+
+// 屏幕宽度大于1500的时候
+@media screen and (min-width: $w-1500) {
+  .push-wrap {
+    width: $w-1475;
+    .left {
+      width: $w-1150;
+    }
+    .right {
+      width: $w-300;
+    }
+  }
+}
+</style>