Ver código fonte

feat: 优化播放器

shuisheng 1 ano atrás
pai
commit
f22e00cdef

+ 18 - 6
src/components/Dropdown/index.vue

@@ -11,9 +11,9 @@
     </div>
     <div
       class="container"
-      :class="{ [props.positon]: 1 }"
+      :class="{ [props.positon]: 1, isTop: props.isTop }"
       :style="{
-        display: show ? 'block' : 'none',
+        // display: show ? 'block' : 'none',
       }"
       @click.stop="handleClick"
     >
@@ -32,11 +32,13 @@ const show = ref(false);
 const props = withDefaults(
   defineProps<{
     trigger?: 'hover' | 'click';
-    positon?: 'left' | 'right';
+    positon?: 'left' | 'right' | 'center';
+    isTop?: boolean;
   }>(),
   {
     trigger: 'hover',
     positon: 'right',
+    isTop: false,
   }
 );
 
@@ -58,8 +60,8 @@ function handleMouseLeave() {
 
 <style lang="scss" scoped>
 .dropdown-wrap {
-  display: inline-block;
   position: relative;
+  display: inline-block;
   cursor: initial;
   &.hover {
     &:hover {
@@ -71,24 +73,34 @@ function handleMouseLeave() {
 
   .btn {
     cursor: pointer;
+
     user-select: none;
   }
   .container {
     position: absolute;
     top: 100%;
-    right: 0;
     z-index: 3;
     display: none;
+
     &.right {
       right: 0;
+      left: auto;
     }
     &.left {
+      right: auto;
       left: 0;
     }
+    &.center {
+      left: 50%;
+      transform: translate(-50%, 0%);
+    }
+    &.isTop {
+      top: 0%;
+      transform: translate(-50%, -100%);
+    }
     .wrap {
       box-sizing: border-box;
       margin-top: 5px;
-      padding: 10px 0;
       border-radius: 5px;
       background-color: #fff;
       box-shadow:

+ 2 - 5
src/components/LoginModal/index.vue

@@ -89,7 +89,7 @@
           </div>
           <div
             class="logo-wrap"
-            @click="handleGithubLogin"
+            @click="handleTip"
           >
             <img
               class="logo"
@@ -106,6 +106,7 @@
 import { LockClosedOutline, PersonOutline } from '@vicons/ionicons5';
 import { onMounted, onUnmounted, ref } from 'vue';
 
+import { handleTip } from '@/hooks/use-common';
 import { useQQLogin } from '@/hooks/use-login';
 import { useAppStore } from '@/store/app';
 import { useUserStore } from '@/store/user';
@@ -132,10 +133,6 @@ onUnmounted(() => {
   clearInterval(loopTimer.value);
 });
 
-function handleGithubLogin() {
-  window.$message.warning('敬请期待!');
-}
-
 function handleQQLogin() {
   useQQLogin({ exp: 24 });
 }

+ 173 - 75
src/components/VideoControls/index.vue

@@ -3,7 +3,7 @@
     <div class="left">
       <div
         class="play"
-        @click="changePlay"
+        @click="appStore.playing = !appStore.playing"
       >
         <n-icon size="25">
           <Pause v-if="appStore.playing"></Pause>
@@ -20,7 +20,7 @@
       </div>
       <div
         class="muted"
-        @click="changeMuted"
+        @click="cacheStore.setMuted(!cacheStore.muted)"
       >
         <n-popover
           placement="top"
@@ -42,7 +42,7 @@
               :step="1"
               vertical
               :tooltip="false"
-              @update-value="changeVolume"
+              @update-value="(v) => cacheStore.setVolume(v)"
             />
           </div>
         </n-popover>
@@ -51,64 +51,131 @@
 
     <div class="right">
       <div
-        class="resolution"
-        v-if="appStore.videoFps"
+        class="item fps"
+        v-if="props.control?.fps && appStore.videoFps"
       >
         {{ appStore.videoFps }}帧
       </div>
       <div
-        class="resolution"
-        v-if="appStore.videoKBs"
+        class="item kbs"
+        v-if="props.control?.kbs && appStore.videoKBs"
       >
         {{ appStore.videoKBs }}KB/s
       </div>
       <div
-        class="resolution"
-        v-if="resolution"
+        class="item resolution"
+        v-if="props.control?.resolution && resolution"
       >
         {{ resolution }}
       </div>
-      <div class="line">
+      <div
+        class="item line"
+        v-if="props.control?.line"
+      >
+        <Dropdown
+          :positon="'center'"
+          :is-top="true"
+        >
+          <template #btn>
+            <div class="btn">线路</div>
+          </template>
+          <template #list>
+            <div class="list">
+              <div
+                class="iten"
+                :class="{ active: appStore.liveLine === item }"
+                v-for="item in LiveLineEnum"
+                :key="item"
+                @click="changeLiveLine(item)"
+              >
+                {{ item }}
+              </div>
+            </div>
+          </template>
+        </Dropdown>
+      </div>
+      <div
+        class="item speed"
+        v-if="props.control?.speed"
+      >
+        <Dropdown
+          :positon="'center'"
+          :is-top="true"
+        >
+          <template #btn>
+            <div class="btn">倍速</div>
+          </template>
+          <template #list>
+            <div
+              class="list"
+              @click="handleTip"
+            >
+              <div class="iten">2.0x</div>
+              <div class="iten">1.5x</div>
+              <div class="iten active">1.0x</div>
+              <div class="iten">0.5x</div>
+            </div>
+          </template>
+        </Dropdown>
+      </div>
+      <div
+        class="item render"
+        v-if="props.control?.renderMode"
+      >
+        <Dropdown
+          :positon="'center'"
+          :is-top="true"
+        >
+          <template #btn>
+            <div class="btn">渲染模式</div>
+          </template>
+          <template #list>
+            <div class="list">
+              <div
+                class="iten"
+                :class="{ active: appStore.videoControls?.renderMode === item }"
+                v-for="item in LiveRenderEnum"
+                :key="item"
+                @click="appStore.videoControls.renderMode = item"
+              >
+                {{ item }}
+              </div>
+            </div>
+          </template>
+        </Dropdown>
+      </div>
+      <div
+        class="item"
+        v-if="props.control?.pipMode"
+      >
         <span
           class="txt"
-          @click="showLine = !showLine"
+          @click="handlePip"
         >
-          线路
+          {{
+            !appStore.videoControlsValue.pipMode ? '开启画中画' : '退出画中画'
+          }}
         </span>
-        <div
-          class="list"
-          :class="{ show: showLine }"
-        >
-          <div
-            class="iten"
-            :class="{ active: appStore.liveLine === item }"
-            v-for="item in LiveLineEnum"
-            :key="item"
-            @click="changeLiveLine(item)"
-          >
-            {{ item }}
-          </div>
-        </div>
       </div>
-      <div class="speed">
+      <div
+        class="item"
+        v-if="props.control?.pageFullMode"
+      >
         <span
           class="txt"
-          @click="showSpeed = !showSpeed"
+          @click="handlePageFull"
         >
-          倍速
+          {{
+            !appStore.videoControlsValue.pageFullMode
+              ? '开启网页全屏'
+              : '退出网页全屏'
+          }}
         </span>
-        <div
-          class="list"
-          :class="{ show: showSpeed }"
-          @click="handleTip"
-        >
-          <div class="iten">2.0x</div>
-          <div class="iten">1.5x</div>
-          <div class="iten active">1.0x</div>
-          <div class="iten">0.5x</div>
-        </div>
       </div>
-      <div class="full">
+      <div
+        class="item"
+        v-if="props.control?.fullMode"
+      >
         <span
           class="txt"
           @click="emits('fullScreen')"
@@ -129,42 +196,62 @@ import {
   VolumeMuteOutline,
 } from '@vicons/ionicons5';
 import { debounce } from 'billd-utils';
-import { ref } from 'vue';
 
-import { LiveLineEnum } from '@/interface';
-import { useAppStore } from '@/store/app';
+import { handleTip } from '@/hooks/use-common';
+import { LiveLineEnum, LiveRenderEnum } from '@/interface';
+import { AppRootState, useAppStore } from '@/store/app';
 import { usePiniaCacheStore } from '@/store/cache';
 import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
+import { onMounted } from 'vue';
 
-withDefaults(
+const props = withDefaults(
   defineProps<{
     resolution?: string;
+    control?: AppRootState['videoControls'];
   }>(),
   {}
 );
 
-const emits = defineEmits(['refresh', 'fullScreen']);
+const emits = defineEmits([
+  'refresh',
+  'fullScreen',
+  'pageFullScreen',
+  'pictureInPicture',
+]);
+
+const cacheStore = usePiniaCacheStore();
+const appStore = useAppStore();
 
 const debounceRefresh = debounce(() => {
   emits('refresh');
 }, 500);
-const cacheStore = usePiniaCacheStore();
-const appStore = useAppStore();
-const showLine = ref(false);
-const showSpeed = ref(false);
 
-function handleTip() {
-  window.$message.info('敬请期待!');
-}
+onMounted(() => {
+  window.addEventListener('keydown', handleKeydown);
+});
 
-function changeMuted() {
-  cacheStore.setMuted(!cacheStore.muted);
+function handleKeydown(e) {
+  if (e.key === 'Escape') {
+    console.log('esc');
+    if (appStore.videoControlsValue.pageFullMode) {
+      window.$message.info('已退出网页全屏');
+      appStore.videoControlsValue.pageFullMode = false;
+    }
+  }
 }
-function changeVolume(v) {
-  cacheStore.setVolume(v);
+
+function handlePip() {
+  emits('pictureInPicture');
+  appStore.videoControlsValue.pipMode = !appStore.videoControlsValue.pipMode;
 }
-function changePlay() {
-  appStore.playing = !appStore.playing;
+
+function handlePageFull() {
+  emits('pageFullScreen');
+  if (!appStore.videoControlsValue.pageFullMode) {
+    window.$message.info('按esc可快速退出网页全屏');
+  }
+  appStore.videoControlsValue.pageFullMode =
+    !appStore.videoControlsValue.pageFullMode;
 }
 
 function changeLiveLine(item: LiveLineEnum) {
@@ -178,6 +265,16 @@ function changeLiveLine(item: LiveLineEnum) {
   ) {
     window.$message.info('不支持该线路!');
     return;
+  } else if (
+    ![
+      LiveRoomTypeEnum.wertc_live,
+      LiveRoomTypeEnum.wertc_meeting_one,
+      LiveRoomTypeEnum.wertc_meeting_two,
+    ].includes(appStore.liveRoomInfo?.type!) &&
+    item === LiveLineEnum.rtc
+  ) {
+    window.$message.info('不支持该线路!');
+    return;
   }
   appStore.setLiveLine(item);
 }
@@ -231,34 +328,34 @@ function changeLiveLine(item: LiveLineEnum) {
   .right {
     display: flex;
     align-items: center;
-    .resolution {
-      cursor: no-drop;
+    .item {
+      position: relative;
+      margin-right: 15px;
+      cursor: pointer;
+      &.fps,
+      &.kbs,
+      &.resolution {
+        cursor: no-drop;
+      }
     }
+
+    .render,
     .resolution,
     .line,
     .speed,
     .full {
-      position: relative;
-      margin-right: 15px;
       &:hover {
         .list {
           display: block;
         }
       }
-      .txt {
-        cursor: pointer;
+      :deep(.wrap) {
+        border-radius: 0px;
+        background-color: rgba($color: #000000, $alpha: 0.5);
+        color: white;
       }
       .list {
-        position: absolute;
-        top: 0;
-        left: 50%;
-        display: none;
-        background-color: rgba($color: #000000, $alpha: 0.5);
         text-align: center;
-        transform: translate(-50%, -100%);
-        &.show {
-          display: block;
-        }
         .iten {
           padding: 6px 10px;
           &.active {
@@ -274,8 +371,9 @@ function changeLiveLine(item: LiveLineEnum) {
         }
       }
     }
-    .full {
-      margin-right: 0;
+
+    & > :last-child {
+      margin-right: 0px;
     }
   }
 }

+ 6 - 1
src/hooks/use-common.ts

@@ -1,8 +1,13 @@
-import { getLastBuildDate } from '@/utils/localStorage/app';
 import { windowReload } from 'billd-utils';
 
+import { getLastBuildDate } from '@/utils/localStorage/app';
+
 import { useTip } from './use-tip';
 
+export function handleTip() {
+  window.$message.info('敬请期待!');
+}
+
 export const useCheckUpdate = () => {
   function handleHtmlCheckUpdate(data: {
     htmlUrl: string;

+ 34 - 0
src/hooks/use-play.ts

@@ -25,6 +25,36 @@ function handlePlayUrl(url: string) {
       }=${userInfo.id!}&${SRS_CB_URL_PARAMS.randomId}=${getRandomString(8)}`;
 }
 
+function closePip() {
+  const appStore = useAppStore();
+  appStore.videoControlsValue.pipMode = false;
+}
+
+export async function usePictureInPicture(el, parentEl) {
+  try {
+    if (el?.tagName?.toLowerCase() === 'video') {
+      await el.requestPictureInPicture();
+      el.addEventListener('leavepictureinpicture', closePip);
+    } else {
+      // 打开一个与播放器大小相同的画中画窗口。
+      // @ts-ignore
+      const pipWindow = await documentPictureInPicture.requestWindow({
+        width: el.clientWidth,
+        height: el.clientHeight,
+      });
+      pipWindow.document.body.append(el);
+      // 当画中画窗口关闭时,将播放器移回原位置。
+      pipWindow.addEventListener('pagehide', () => {
+        parentEl?.append(el);
+        closePip();
+      });
+    }
+  } catch (error) {
+    console.error('usePictureInPicture失败');
+    console.log(error);
+  }
+}
+
 export function useFullScreen(video) {
   if (video.requestFullscreen) {
     console.log('requestFullscreen-1');
@@ -75,6 +105,7 @@ export function useFlvPlay() {
       flvPlayer.value = undefined;
     }
     flvVideoEl.value?.remove();
+    flvVideoEl.value = undefined;
     clearInterval(retryTimer.value);
     retryMax.value = initRetryMax;
   }
@@ -149,6 +180,7 @@ export function useFlvPlay() {
           videoEl.addEventListener('playing', () => {
             console.log('flv-playing');
             flvIsPlaying.value = true;
+            appStore.playing = true;
             retry.value = 0;
             setMuted(cacheStore.muted);
             setVolume(cacheStore.volume);
@@ -225,6 +257,7 @@ export function useHlsPlay() {
       hlsPlayer.value = undefined;
     }
     hlsVideoEl.value?.remove();
+    hlsVideoEl.value = undefined;
     clearInterval(retryTimer.value);
     retryMax.value = initRetryMax;
   }
@@ -327,6 +360,7 @@ export function useHlsPlay() {
         hlsPlayer.value?.on('playing', () => {
           console.log('hls-playing');
           hlsIsPlaying.value = true;
+          appStore.playing = true;
           setMuted(cacheStore.muted);
           setVolume(cacheStore.volume);
           retry.value = 0;

+ 78 - 36
src/hooks/use-pull.ts

@@ -1,5 +1,5 @@
 import { getRandomString } from 'billd-utils';
-import { onUnmounted, ref, watch } from 'vue';
+import { nextTick, onUnmounted, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { commentAuthTip, loginTip } from '@/hooks/use-login';
@@ -8,6 +8,7 @@ import { useWebsocket } from '@/hooks/use-websocket';
 import {
   DanmuMsgTypeEnum,
   LiveLineEnum,
+  LiveRenderEnum,
   WsMessageMsgIsFileEnum,
 } from '@/interface';
 import { useAppStore } from '@/store/app';
@@ -47,6 +48,7 @@ export function usePull(roomId: string) {
   const { flvVideoEl, flvIsPlaying, startFlvPlay, destroyFlv } = useFlvPlay();
   const { hlsVideoEl, hlsIsPlaying, startHlsPlay, destroyHls } = useHlsPlay();
   const stopDrawingArr = ref<any[]>([]);
+  let changeWrapSizeFn;
 
   onUnmounted(() => {
     handleStopDrawing();
@@ -55,56 +57,93 @@ export function usePull(roomId: string) {
   function handleStopDrawing() {
     destroyFlv();
     destroyHls();
+    changeWrapSizeFn = undefined;
     stopDrawingArr.value.forEach((cb) => cb());
     stopDrawingArr.value = [];
     remoteVideo.value.forEach((el) => el.remove());
     remoteVideo.value = [];
   }
 
-  watch(hlsVideoEl, (newval) => {
+  function handleVideoWrapResize() {
+    nextTick(() => {
+      if (videoWrapRef.value) {
+        const rect = videoWrapRef.value.getBoundingClientRect();
+        changeWrapSizeFn?.({ width: rect.width, height: rect.height });
+      }
+    });
+  }
+
+  function videoPlay(videoEl: HTMLVideoElement) {
     stopDrawingArr.value = [];
     stopDrawingArr.value.forEach((cb) => cb());
-    if (newval && videoWrapRef.value) {
-      const rect = videoWrapRef.value.getBoundingClientRect();
-      const { canvas, stopDrawing } = videoToCanvas({
-        wrapSize: {
-          width: rect.width,
-          height: rect.height,
-        },
-        videoEl: newval,
-        videoResize: ({ w, h }) => {
-          videoResolution.value = `${w}x${h}`;
-        },
-      });
-      stopDrawingArr.value.push(stopDrawing);
-      remoteVideo.value.push(canvas);
-      videoElArr.value.push(newval);
-      videoLoading.value = false;
+    if (appStore.videoControls.renderMode === LiveRenderEnum.canvas) {
+      if (videoEl && videoWrapRef.value) {
+        const rect = videoWrapRef.value.getBoundingClientRect();
+        const { canvas, stopDrawing, changeWrapSize } = videoToCanvas({
+          wrapSize: {
+            width: rect.width,
+            height: rect.height,
+          },
+          videoEl,
+          videoResize: ({ w, h }) => {
+            videoResolution.value = `${w}x${h}`;
+          },
+        });
+        changeWrapSizeFn = changeWrapSize;
+        stopDrawingArr.value.push(stopDrawing);
+        remoteVideo.value.push(canvas);
+        videoElArr.value.push(videoEl);
+        videoLoading.value = false;
+      }
+    } else if (appStore.videoControls.renderMode === LiveRenderEnum.video) {
+      if (videoEl && videoWrapRef.value) {
+        const rect = videoWrapRef.value.getBoundingClientRect();
+        const { changeWrapSize } = videoFullBox({
+          wrapSize: {
+            width: rect.width,
+            height: rect.height,
+          },
+          videoEl,
+          videoResize: ({ w, h }) => {
+            videoResolution.value = `${w}x${h}`;
+          },
+        });
+        changeWrapSizeFn = changeWrapSize;
+        remoteVideo.value.push(videoEl);
+        videoElArr.value.push(videoEl);
+        videoLoading.value = false;
+      }
+    }
+  }
+
+  watch(hlsVideoEl, (newval) => {
+    if (newval) {
+      videoPlay(newval);
     }
   });
 
   watch(flvVideoEl, (newval) => {
-    stopDrawingArr.value = [];
-    stopDrawingArr.value.forEach((cb) => cb());
-    if (newval && videoWrapRef.value) {
-      const rect = videoWrapRef.value.getBoundingClientRect();
-      const { canvas, stopDrawing } = videoToCanvas({
-        wrapSize: {
-          width: rect.width,
-          height: rect.height,
-        },
-        videoEl: newval,
-        videoResize: ({ w, h }) => {
-          videoResolution.value = `${w}x${h}`;
-        },
-      });
-      stopDrawingArr.value.push(stopDrawing);
-      remoteVideo.value.push(canvas);
-      videoElArr.value.push(newval);
-      videoLoading.value = false;
+    if (newval) {
+      videoPlay(newval);
     }
   });
 
+  watch(
+    () => appStore.videoControlsValue.pageFullMode,
+    () => {
+      handleVideoWrapResize();
+    }
+  );
+
+  watch(
+    () => appStore.videoControls.renderMode,
+    () => {
+      if (appStore.liveRoomInfo) {
+        handlePlay(appStore.liveRoomInfo);
+      }
+    }
+  );
+
   watch(
     () => networkStore.rtcMap,
     (newVal) => {
@@ -181,6 +220,7 @@ export function usePull(roomId: string) {
 
   function handlePlay(data: ILiveRoom) {
     roomLiving.value = true;
+    appStore.playing = false;
     flvurl.value = data.flv_url!;
     hlsurl.value = data.hls_url!;
     function play() {
@@ -347,6 +387,7 @@ export function usePull(roomId: string) {
     const key = event.key.toLowerCase();
     if (key === 'enter') {
       event.preventDefault();
+      danmuMsgType.value = DanmuMsgTypeEnum.danmu;
       sendDanmu();
     }
   }
@@ -365,6 +406,7 @@ export function usePull(roomId: string) {
     const instance = networkStore.wsMap.get(roomId);
     if (!instance) return;
     const requestId = getRandomString(8);
+    console.log(danmuMsgType.value, 2221);
     const messageData: WsMessageType['data'] = {
       socket_id: '',
       msg: danmuStr.value,

+ 5 - 0
src/interface.ts

@@ -194,6 +194,11 @@ export enum LiveLineEnum {
   flv = 'flv',
 }
 
+export enum LiveRenderEnum {
+  video = 'video',
+  canvas = 'canvas',
+}
+
 export enum PayStatusEnum {
   wait = 'billd_status_wait',
   timeout = 'billd_status_timeout',

+ 49 - 18
src/layout/pc/head/index.vue

@@ -79,7 +79,7 @@
             <div class="list">
               <a
                 class="item"
-                @click="quickStart"
+                @click="handleTip"
               >
                 <div class="txt">{{ t('layout.guide') }}</div>
               </a>
@@ -185,7 +185,7 @@
           </template>
         </Dropdown>
 
-        <a
+        <!-- <a
           class="sponsors"
           :class="{
             active: router.currentRoute.value.name === routerName.sponsors,
@@ -194,14 +194,9 @@
           @click.prevent="router.push({ name: routerName.sponsors })"
         >
           {{ t('layout.sponsor') }}
-        </a>
+        </a> -->
         <a
           class="signin"
-          :class="{
-            active:
-              router.currentRoute.value.name ===
-              routerName.privatizationDeployment,
-          }"
           @click="handleSignin"
         >
           {{ t('layout.signin') }}
@@ -228,6 +223,20 @@
           </div>
         </a>
 
+        <a
+          class="wasm"
+          :class="{
+            active: router.currentRoute.value.name === routerName.wasm,
+          }"
+          href="/wasm"
+          @click.prevent="handleTip"
+        >
+          {{ t('layout.wasm') }}
+          <div class="badge">
+            <div class="txt">wasm</div>
+          </div>
+        </a>
+
         <a
           class="github"
           target="_blank"
@@ -381,7 +390,8 @@ import { fetchCreateSignin, fetchTodayIsSignin } from '@/api/signin';
 import Dropdown from '@/components/Dropdown/index.vue';
 import VPIconChevronDown from '@/components/icons/VPIconChevronDown.vue';
 import VPIconExternalLink from '@/components/icons/VPIconExternalLink.vue';
-import { COMMON_URL } from '@/constant';
+import { COMMON_URL, DEFAULT_AUTH_INFO } from '@/constant';
+import { handleTip } from '@/hooks/use-common';
 import { loginTip } from '@/hooks/use-login';
 import { routerName } from '@/router';
 import { useAppStore } from '@/store/app';
@@ -410,6 +420,11 @@ const about = ref([
     routerName: routerName.team,
     url: '',
   },
+  {
+    label: 'layout.sponsor',
+    routerName: routerName.sponsors,
+    url: '',
+  },
   {
     label: 'layout.officialGroup',
     routerName: routerName.group,
@@ -456,6 +471,10 @@ const plugins = ref([
     label: 'billd-cli',
     url: 'https://github.com/galaxy-s10/billd-cli',
   },
+  {
+    label: 'billd-deploy',
+    url: 'https://github.com/galaxy-s10/billd-deploy',
+  },
   {
     label: 'billd-utils',
     url: 'https://github.com/galaxy-s10/billd-utils',
@@ -515,7 +534,7 @@ function handleJump(item) {
   if (item.url) {
     openToTarget(item.url);
   } else {
-    window.$message.info('敬请期待!');
+    handleTip();
   }
 }
 
@@ -524,14 +543,19 @@ onMounted(() => {
     'https://img.shields.io/github/stars/galaxy-s10/billd-live?label=Star&logo=GitHub&labelColor=white&logoColor=black&style=social&cacheSeconds=3600';
 });
 
-function quickStart() {
-  window.$message.info('敬请期待!');
-}
-
 function handleStartLive(key: LiveRoomTypeEnum) {
   if (!loginTip()) {
     return;
   }
+  if (
+    key === LiveRoomTypeEnum.msr &&
+    !userStore.userInfo?.auths?.find(
+      (v) => v.auth_value === DEFAULT_AUTH_INFO.LIVE_PULL_SVIP.auth_value
+    )
+  ) {
+    window.$message.info('权限不足,请更换其他开播方式');
+    return;
+  }
   const url = router.resolve({
     name: routerName.push,
     query: { liveType: key },
@@ -576,7 +600,6 @@ function handleWebsiteJump() {
       color: white;
       line-height: 1;
       .txt {
-        margin-right: 0;
         transform-origin: top !important;
 
         @include minFont(10);
@@ -676,6 +699,8 @@ function handleWebsiteJump() {
 
         .list {
           width: 150px;
+          padding: 10px 0;
+
           .item {
             display: flex;
             align-items: center;
@@ -700,6 +725,8 @@ function handleWebsiteJump() {
       .ecosystem {
         .list {
           width: 225px;
+          padding: 10px 0;
+
           .title {
             margin: 10px 0 5px;
             padding: 0 15px;
@@ -715,6 +742,7 @@ function handleWebsiteJump() {
       .github,
       .sponsors,
       .privatizationDeployment,
+      .wasm,
       .signin {
         display: flex;
         align-items: center;
@@ -725,10 +753,8 @@ function handleWebsiteJump() {
         &:hover {
           color: $theme-color-gold;
         }
-        .txt {
-          margin-right: 5px;
-        }
       }
+      .wasm,
       .privatizationDeployment,
       .signin {
         position: relative;
@@ -747,6 +773,7 @@ function handleWebsiteJump() {
         .list {
           width: 180px;
           position: relative;
+          padding: 10px 0;
 
           .item {
             display: flex;
@@ -795,6 +822,8 @@ function handleWebsiteJump() {
         }
         .list {
           width: 90px;
+          padding: 10px 0;
+
           .item {
             position: relative;
             display: flex;
@@ -819,6 +848,8 @@ function handleWebsiteJump() {
         }
         .list {
           width: 80px;
+          padding: 10px 0;
+
           .item {
             display: flex;
             align-items: center;

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

@@ -12,6 +12,7 @@ export default nameSpaceWrap('layout', {
   sponsor: 'Sponsor',
   signin: 'Signin',
   deploy: 'Private Deploy',
+  wasm: 'Video Tool',
   startLive: 'Start Live',
   login: 'Login',
   logout: 'Logout',

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

@@ -12,6 +12,7 @@ export default nameSpaceWrap('layout', {
   sponsor: '赞助',
   signin: '签到',
   deploy: '私有化部署',
+  wasm: '视频工具',
   startLive: '我要开播',
   login: '登录',
   logout: '退出',

+ 1 - 0
src/router/index.ts

@@ -28,6 +28,7 @@ export const routerName = {
   rank: 'rank',
   sponsors: 'sponsors',
   privatizationDeployment: 'privatizationDeployment',
+  wasm: 'wasm',
   support: 'support',
   order: 'order',
   wallet: 'wallet',

+ 24 - 2
src/store/app/index.ts

@@ -1,7 +1,7 @@
 import { UploadFileInfo } from 'naive-ui';
 import { defineStore } from 'pinia';
 
-import { LiveLineEnum, MediaTypeEnum } from '@/interface';
+import { LiveLineEnum, LiveRenderEnum, MediaTypeEnum } from '@/interface';
 import { mobileRouterName } from '@/router';
 import { ILiveRoom } from '@/types/ILiveRoom';
 
@@ -39,6 +39,22 @@ export type AppRootState = {
     rect?: { top: number; left: number };
     scaleInfo: Record<number, { scaleX: number; scaleY: number }>;
   }[];
+  videoControls: {
+    pipMode?: boolean;
+    pageFullMode?: boolean;
+    fullMode?: boolean;
+    renderMode?: LiveRenderEnum;
+    line?: boolean;
+    speed?: boolean;
+    networkSpeed?: boolean;
+    fps?: boolean;
+    kbs?: boolean;
+    resolution?: boolean;
+  };
+  videoControlsValue: {
+    pipMode?: boolean;
+    pageFullMode?: boolean;
+  };
   liveLine: LiveLineEnum;
   liveRoomInfo?: ILiveRoom;
   showLoginModal: boolean;
@@ -49,10 +65,16 @@ export type AppRootState = {
 export const useAppStore = defineStore('app', {
   state: (): AppRootState => {
     return {
-      playing: true,
+      playing: false,
       videoKBs: undefined,
       videoFps: undefined,
       videoRatio: 16 / 9,
+      videoControls: {
+        renderMode: LiveRenderEnum.video,
+      },
+      videoControlsValue: {
+        pipMode: false,
+      },
       normalVolume: 70,
       navList: [
         { routeName: mobileRouterName.h5, name: '频道' },

+ 22 - 9
src/utils/index.ts

@@ -444,6 +444,8 @@ export function videoFullBox(data: {
   videoResize?: (data: { w: number; h: number }) => void;
 }) {
   const { videoEl } = data;
+  let wrapSize = data.wrapSize;
+
   if (!videoEl) {
     throw new Error('videoEl不能为空!');
   }
@@ -460,10 +462,10 @@ export function videoFullBox(data: {
     const res = computeBox({
       width,
       height,
-      maxHeight: data.wrapSize.height,
-      minHeight: data.wrapSize.height,
-      maxWidth: data.wrapSize.width,
-      minWidth: data.wrapSize.width,
+      maxHeight: wrapSize.height,
+      minHeight: wrapSize.height,
+      maxWidth: wrapSize.width,
+      minWidth: wrapSize.width,
     });
     videoEl.style.width = `${res.width as number}px`;
     videoEl.style.height = `${res.height as number}px`;
@@ -473,6 +475,11 @@ export function videoFullBox(data: {
   setVideoSize({ width: w, height: h });
   data.videoResize?.({ w, h });
   videoEl.addEventListener('resize', handleResize);
+  function changeWrapSize(data: { width; height }) {
+    wrapSize = data;
+    setVideoSize({ width: videoEl.videoWidth, height: videoEl.videoHeight });
+  }
+  return { changeWrapSize };
 }
 
 export function videoToCanvas(data: {
@@ -481,6 +488,7 @@ export function videoToCanvas(data: {
   videoResize?: (data: { w: number; h: number }) => void;
 }) {
   const { videoEl } = data;
+  let wrapSize = data.wrapSize;
   if (!videoEl) {
     throw new Error('videoEl不能为空!');
   }
@@ -500,10 +508,10 @@ export function videoToCanvas(data: {
     const res = computeBox({
       width,
       height,
-      maxHeight: data.wrapSize.height,
-      minHeight: data.wrapSize.height,
-      maxWidth: data.wrapSize.width,
-      minWidth: data.wrapSize.width,
+      maxHeight: wrapSize.height,
+      minHeight: wrapSize.height,
+      maxWidth: wrapSize.width,
+      minWidth: wrapSize.width,
     });
     canvas.style.width = `${res.width as number}px`;
     canvas.style.height = `${res.height as number}px`;
@@ -551,7 +559,12 @@ export function videoToCanvas(data: {
     cancelAnimationFrame(timer);
   }
 
+  function changeWrapSize(data: { width; height }) {
+    wrapSize = data;
+    setVideoSize({ width: videoEl.videoWidth, height: videoEl.videoHeight });
+  }
+
   drawCanvas();
 
-  return { drawCanvas, stopDrawing, canvas };
+  return { drawCanvas, stopDrawing, changeWrapSize, canvas };
 }

+ 2 - 1
src/views/doc/privatizationDeployment/index.vue

@@ -23,8 +23,9 @@
           <p>目前核心实现:</p>
           <ol>
             <li>服务器自建SRS直播。</li>
-            <li>服务器自建WebRTC直播。</li>
+            <li>服务器自建WebRTC会议。</li>
             <li>接入第三方腾讯云直播。</li>
+            <li>PK互动。</li>
           </ol>
         </div>
         <div class="hr"></div>

+ 19 - 42
src/views/h5/room/index.vue

@@ -24,7 +24,6 @@
     <div
       v-loading="videoLoading"
       class="video-wrap"
-      ref="videoWrapTmpRef"
       :style="{
         height: videoWrapHeight + 'px',
         '--max-height': videoWrapHeight + 'px',
@@ -57,6 +56,11 @@
         v-if="roomLiving"
         :resolution="videoResolution"
         @refresh="handleRefresh"
+        @full-screen="handleFullScreen"
+        :control="{
+          line: true,
+          fullMode: true,
+        }"
       ></VideoControls>
     </div>
     <div class="n-tab-wrap">
@@ -135,23 +139,6 @@
             </div>
           </div>
         </n-tab-pane>
-        <n-tab-pane
-          name="customerService"
-          tab="客服"
-        >
-          <div
-            class="customerService-wrap"
-            :style="{ height: containerHeight + 'px' }"
-          >
-            <img
-              class="qrcode"
-              v-if="frontendWechatQrcode !== ''"
-              :src="frontendWechatQrcode"
-              alt=""
-            />
-            <div class="tip">打开微信扫一扫添加客服</div>
-          </div>
-        </n-tab-pane>
         <n-tab-pane
           name="liveRoomInfo"
           tab="直播间信息"
@@ -160,12 +147,10 @@
             class="liveRoomInfo-wrap"
             :style="{ height: containerHeight + 'px' }"
           >
-            <div>直播间名称:{{ appStore.liveRoomInfo?.name }}</div>
-            <div>直播间简介:{{ appStore.liveRoomInfo?.desc }}</div>
+            <div>名称:{{ appStore.liveRoomInfo?.name }}</div>
+            <div>简介:{{ appStore.liveRoomInfo?.desc }}</div>
             <div>
-              直播间分区:{{
-                appStore.liveRoomInfo?.areas?.[0].name || '暂无分区'
-              }}
+              分区:{{ appStore.liveRoomInfo?.areas?.[0].name || '暂无分区' }}
             </div>
           </div>
         </n-tab-pane>
@@ -216,10 +201,10 @@
 import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
-import { fetchFindLiveConfigByKey } from '@/api/liveConfig';
 import { fetchFindLiveRoom } from '@/api/liveRoom';
 import { THEME_COLOR } from '@/constant';
 import { emojiArray } from '@/emoji';
+import { useFullScreen } from '@/hooks/use-play';
 import { usePull } from '@/hooks/use-pull';
 import { DanmuMsgTypeEnum, WsMessageMsgIsFileEnum } from '@/interface';
 import router, { mobileRouterName } from '@/router';
@@ -232,14 +217,12 @@ const route = useRoute();
 const cacheStore = usePiniaCacheStore();
 const appStore = useAppStore();
 
-const videoWrapTmpRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
 const danmuListRef = ref<HTMLDivElement>();
 const showEmoji = ref(false);
 
 const containerHeight = ref(0);
 const videoWrapHeight = ref(0);
-const frontendWechatQrcode = ref('');
 const remoteVideoRef = ref<HTMLDivElement>();
 const roomId = ref(route.params.roomId as string);
 const {
@@ -258,7 +241,6 @@ const {
   danmuStr,
   roomLiving,
   anchorInfo,
-  remoteVideo,
   videoResolution,
 } = usePull(roomId.value);
 
@@ -270,8 +252,7 @@ onUnmounted(() => {
 
 onMounted(() => {
   showPlayBtn.value = true;
-  videoWrapRef.value = videoWrapTmpRef.value;
-  getWechatQrcode();
+  videoWrapRef.value = remoteVideoRef.value;
   setTimeout(() => {
     scrollTo(0, 0);
   }, 100);
@@ -315,6 +296,13 @@ function handleRefresh() {
   }
 }
 
+function handleFullScreen() {
+  const el = remoteVideoRef.value?.childNodes[0];
+  if (el) {
+    useFullScreen(el);
+  }
+}
+
 async function getLiveRoomInfo() {
   try {
     videoLoading.value = true;
@@ -341,17 +329,6 @@ function startPull() {
   showPlayBtn.value = false;
   handlePlay(appStore.liveRoomInfo!);
 }
-
-async function getWechatQrcode() {
-  try {
-    const res = await fetchFindLiveConfigByKey('frontend_wechat_qrcode');
-    if (res.code === 200) {
-      frontendWechatQrcode.value = res.data.value;
-    }
-  } catch (error) {
-    console.log(error);
-  }
-}
 </script>
 
 <style lang="scss" scoped>
@@ -415,7 +392,7 @@ async function getWechatQrcode() {
       height: 100%;
       :deep(video) {
         position: absolute;
-        left: 50%;
+        top: 50%;
         left: 50%;
         display: block;
         margin: 0 auto;
@@ -425,7 +402,7 @@ async function getWechatQrcode() {
       }
       :deep(canvas) {
         position: absolute;
-        left: 50%;
+        top: 50%;
         left: 50%;
         display: block;
         margin: 0 auto;

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

@@ -67,6 +67,9 @@
               @click.stop
               :resolution="videoResolution"
               @refresh="handleRefresh"
+              :control="{
+                line: true,
+              }"
             ></VideoControls>
             <div
               class="join-btn"
@@ -312,7 +315,6 @@ function changeLiveRoom(item: ILive) {
   ) {
     appStore.setLiveLine(LiveLineEnum.hls);
   }
-  appStore.playing = true;
   playLive(item);
 }
 

+ 10 - 5
src/views/profile/index.vue

@@ -89,7 +89,7 @@
 
 <script lang="ts" setup>
 import { copyToClipBoard, openToTarget } from 'billd-utils';
-import { ref, watchEffect } from 'vue';
+import { ref, watch, watchEffect } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
 import { fetchUpdateLiveRoomKey } from '@/api/liveRoom';
@@ -119,6 +119,15 @@ watchEffect(() => {
   }
 });
 
+watch(
+  () => userStore.userInfo,
+  (newval) => {
+    if (newval) {
+      pushRes.value = newval.live_rooms?.[0];
+    }
+  }
+);
+
 async function handleUserInfo() {
   try {
     getUserLoading.value = true;
@@ -126,10 +135,6 @@ async function handleUserInfo() {
     if (res.code === 200) {
       userInfo.value = res.data;
     }
-    if (userStore.userInfo) {
-      const liveRoom = userStore.userInfo.live_rooms?.[0];
-      pushRes.value = liveRoom;
-    }
   } catch (error) {
     console.error(error);
   } finally {

+ 193 - 90
src/views/pull/index.vue

@@ -1,13 +1,11 @@
 <template>
-  <div class="pull-wrap">
+  <div
+    class="pull-wrap"
+    :class="{ isPageFull: appStore.videoControlsValue.pageFullMode }"
+  >
     <div class="bg-img-wrap">
-      <div
-        v-if="configBg !== ''"
-        class="bg-img"
-        :style="{ backgroundImage: `url(${configBg})` }"
-      ></div>
       <video
-        v-if="configVideo !== ''"
+        v-if="configVideo && configVideo !== ''"
         class="bg-video"
         :src="configVideo"
         muted
@@ -15,8 +13,8 @@
         loop
       ></video>
       <div
-        v-else
-        class="bg-img"
+        v-if="configBg && configBg !== ''"
+        :style="{ backgroundImage: `url(${configBg})` }"
       ></div>
     </div>
     <div class="left">
@@ -134,6 +132,8 @@
           :resolution="videoResolution"
           @refresh="handleRefresh"
           @full-screen="handleFullScreen"
+          @picture-in-picture="hanldePictureInPicture"
+          :control="appStore.videoControls"
         ></VideoControls>
       </div>
 
@@ -176,43 +176,46 @@
       </div>
     </div>
     <div class="right">
-      <div class="tab">
-        <span>在线用户</span>
-        <span> | </span>
-        <span>排行榜</span>
-      </div>
-      <div class="user-list">
-        <div
-          v-for="(item, index) in liveUserList"
-          :key="index"
-          class="item"
-        >
+      <div class="rank-wrap">
+        <div class="tab">
+          <span>在线用户</span>
+          <span> | </span>
+          <span>排行榜</span>
+        </div>
+        <div class="user-list">
           <div
-            class="info"
-            v-if="item.value?.userInfo"
-            @click="jumpProfile(item.value.userInfo.id!)"
+            v-for="(item, index) in liveUserList"
+            :key="index"
+            class="item"
           >
             <div
-              class="avatar"
-              :style="{
-                backgroundImage: `url(${item.value.userInfo.avatar})`,
-              }"
-            ></div>
-            <div class="username">
-              {{ item.value.userInfo.username }}
+              class="info"
+              v-if="item.value?.userInfo"
+              @click="jumpProfile(item.value.userInfo.id!)"
+            >
+              <div
+                class="avatar"
+                :style="{
+                  backgroundImage: `url(${item.value.userInfo.avatar})`,
+                }"
+              ></div>
+              <div class="username">
+                {{ item.value.userInfo.username }}
+              </div>
             </div>
-          </div>
-          <div
-            class="info"
-            v-else
-          >
-            <div class="avatar"></div>
-            <div class="username">
-              {{ item.value?.socketId }}
+            <div
+              class="info"
+              v-else
+            >
+              <div class="avatar"></div>
+              <div class="username">
+                {{ item.value?.socketId }}
+              </div>
             </div>
           </div>
         </div>
       </div>
+
       <div
         ref="danmuListRef"
         class="danmu-list"
@@ -383,7 +386,7 @@
         ></textarea>
         <div
           class="btn"
-          @click="sendDanmu"
+          @click="handleSendDanmu"
         >
           发送
         </div>
@@ -412,7 +415,7 @@ import { fetchGetWsMessageList } from '@/api/wsMessage';
 import { QINIU_RESOURCE, liveRoomTypeEnumMap } from '@/constant';
 import { emojiArray } from '@/emoji';
 import { commentAuthTip, loginTip } from '@/hooks/use-login';
-import { useFullScreen } from '@/hooks/use-play';
+import { useFullScreen, usePictureInPicture } from '@/hooks/use-play';
 import { usePull } from '@/hooks/use-pull';
 import { useUpload } from '@/hooks/use-upload';
 import {
@@ -421,6 +424,7 @@ import {
   GoodsTypeEnum,
   IGiftRecord,
   IGoods,
+  LiveRenderEnum,
   WsMessageMsgIsFileEnum,
   WsMessageMsgIsShowEnum,
   WsMessageMsgIsVerifyEnum,
@@ -480,12 +484,22 @@ const {
 } = usePull(roomId.value);
 
 onMounted(() => {
+  appStore.videoControls.fps = true;
+  appStore.videoControls.fullMode = true;
+  appStore.videoControls.kbs = true;
+  appStore.videoControls.line = true;
+  appStore.videoControls.networkSpeed = true;
+  appStore.videoControls.pageFullMode = true;
+  appStore.videoControls.pipMode = true;
+  appStore.videoControls.renderMode = LiveRenderEnum.video;
+  appStore.videoControls.resolution = true;
+  appStore.videoControls.speed = true;
+
   videoWrapRef.value = remoteVideoRef.value;
   setTimeout(() => {
     scrollTo(0, 0);
   }, 100);
   handleHistoryMsg();
-  appStore.playing = true;
   getGoodsList();
   if (topRef.value && bottomRef.value && remoteVideoRef.value) {
     const res =
@@ -506,6 +520,11 @@ onUnmounted(() => {
   closeRtc();
 });
 
+function handleSendDanmu() {
+  danmuMsgType.value = DanmuMsgTypeEnum.danmu;
+  sendDanmu();
+}
+
 async function getGiftGroupList() {
   const res = await fetchGiftGroupList({
     live_room_id: Number(roomId.value),
@@ -751,12 +770,22 @@ async function handlePay(item: IGoods) {
 
 function handleFullScreen() {
   const el = remoteVideoRef.value?.childNodes[0];
-  // @ts-ignore
-  if (el?.tagName?.toLowerCase() === 'video') {
+  if (el) {
     useFullScreen(el);
   }
 }
 
+async function hanldePictureInPicture() {
+  if (appStore.videoControlsValue.pipMode) {
+    document.exitPictureInPicture();
+  } else {
+    const el = remoteVideoRef.value?.childNodes[0];
+    if (el && remoteVideoRef.value) {
+      await usePictureInPicture(el, remoteVideoRef.value);
+    }
+  }
+}
+
 function handleRefresh() {
   if (appStore.liveRoomInfo) {
     handlePlay(appStore.liveRoomInfo);
@@ -826,10 +855,13 @@ function handleScrollTop() {
   }
 }
 .pull-wrap {
+  position: relative;
+  z-index: 1;
   display: flex;
   justify-content: space-around;
   margin: 15px auto 0;
-  width: $w-1275;
+  width: $w-1200;
+
   .bg-img-wrap {
     position: absolute;
     top: $layout-head-h;
@@ -869,7 +901,7 @@ function handleScrollTop() {
     display: inline-block;
     overflow: hidden;
     box-sizing: border-box;
-    width: $w-1000;
+    width: $w-900;
     height: 740px;
     border-radius: 6px;
     background-color: $theme-color-papayawhip;
@@ -969,9 +1001,8 @@ function handleScrollTop() {
       display: flex;
       overflow: hidden;
       align-items: center;
-      flex: 1;
       justify-content: space-between;
-      height: 562px;
+      height: calc(100% - 80px - 100px);
       background-color: rgba($color: #000000, $alpha: 0.5);
       .remote-video {
         position: relative;
@@ -983,10 +1014,7 @@ function handleScrollTop() {
           left: 50%;
           display: block;
           margin: 0 auto;
-          // min-width: 100%;
-          // min-height: 100%;
-          max-width: $w-1000;
-          max-height: 562px;
+          height: calc(100% - 80px - 100px);
           transform: translate(-50%, -50%);
         }
         :deep(canvas) {
@@ -995,10 +1023,7 @@ function handleScrollTop() {
           left: 50%;
           display: block;
           margin: 0 auto;
-          // min-width: 100%;
-          // min-height: 100%;
-          max-width: $w-1000;
-          max-height: 562px;
+          height: calc(100% - 80px - 100px);
           transform: translate(-50%, -50%);
         }
       }
@@ -1023,12 +1048,17 @@ function handleScrollTop() {
     }
 
     .gift-list {
-      position: relative;
+      position: absolute;
+      right: 0;
+      bottom: 0;
+      left: 0;
       display: flex;
       align-items: center;
       justify-content: space-around;
       box-sizing: border-box;
+      box-sizing: border-box;
       padding: 5px 0;
+      height: 100px;
       > :last-child {
         position: absolute;
       }
@@ -1095,49 +1125,52 @@ function handleScrollTop() {
     border-radius: 6px;
     background-color: $theme-color-papayawhip;
     color: #9499a0;
-    .tab {
-      display: flex;
-      align-items: center;
-      justify-content: space-evenly;
-      height: 27px;
-      font-size: 12px;
-    }
-    .user-list {
-      overflow-y: scroll;
-      padding: 0 15px;
-      height: 100px;
-      background-color: $theme-color-papayawhip;
-
-      @extend %customScrollbar;
-      .item {
+    .rank-wrap {
+      .tab {
         display: flex;
         align-items: center;
-        justify-content: space-between;
-        margin-bottom: 10px;
+        justify-content: space-evenly;
+        height: 30px;
         font-size: 12px;
-        .info {
+      }
+      .user-list {
+        overflow-y: scroll;
+        box-sizing: border-box;
+        padding: 0 15px;
+        height: 100px;
+
+        @extend %customScrollbar;
+        .item {
           display: flex;
           align-items: center;
-          cursor: pointer;
-          .avatar {
-            margin-right: 5px;
-            width: 25px;
-            height: 25px;
-            border-radius: 50%;
+          justify-content: space-between;
+          margin-bottom: 10px;
+          font-size: 12px;
+          .info {
+            display: flex;
+            align-items: center;
+            cursor: pointer;
+            .avatar {
+              margin-right: 5px;
+              width: 25px;
+              height: 25px;
+              border-radius: 50%;
 
-            @extend %containBg;
-          }
-          .username {
-            color: black;
+              @extend %containBg;
+            }
+            .username {
+              color: black;
+            }
           }
         }
       }
     }
+
     .danmu-list {
       overflow-y: scroll;
       box-sizing: border-box;
       padding-top: 4px;
-      height: 480px;
+      height: calc(100% - 30px - 100px - 135px);
       background-color: #f6f7f8;
       text-align: initial;
 
@@ -1188,10 +1221,13 @@ function handleScrollTop() {
       }
     }
     .send-msg {
-      position: relative;
+      position: absolute;
+      bottom: 0;
+      left: 0;
       box-sizing: border-box;
-      padding: 4px 10px;
+      padding: 2px 10px;
       width: 100%;
+      height: 135px;
       .disableSpeaking {
         cursor: no-drop;
 
@@ -1267,8 +1303,12 @@ function handleScrollTop() {
         outline: none;
         border: 1px solid hsla(0, 0%, 60%, 0.2);
         border-radius: 4px;
-        background-color: #f1f2f3;
+        background-color: #fff;
         font-size: 14px;
+
+        &::placeholder {
+          font-size: 13px;
+        }
       }
       .btn {
         box-sizing: border-box;
@@ -1286,15 +1326,78 @@ function handleScrollTop() {
       }
     }
   }
+  &.isPageFull {
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 100;
+    justify-content: space-between;
+    margin: 0;
+    width: 100vw;
+    height: 100vh;
+
+    .left {
+      width: calc(100vw - 300px);
+      height: 100%;
+      border-radius: 0;
+      .head {
+        display: none;
+      }
+      .video-wrap {
+        height: calc(100% - 100px);
+        .remote-video {
+          :deep(video) {
+            max-width: 100%;
+          }
+          :deep(canvas) {
+            max-width: 100%;
+          }
+        }
+      }
+      .gift-list {
+        background-color: #8ec5fc;
+        background-image: linear-gradient(62deg, #8ec5fc 0%, #e0c3fc 100%);
+
+        .item {
+          .name {
+            color: white;
+          }
+          .price {
+            color: black;
+          }
+        }
+      }
+    }
+    .right {
+      width: 300px;
+      height: 100%;
+      border-radius: 0;
+      .rank-wrap {
+        background-color: #8ec5fc;
+        background-image: linear-gradient(62deg, #8ec5fc 0%, #e0c3fc 100%);
+      }
+
+      .send-msg {
+        background-color: #0093e9;
+        background-image: linear-gradient(328deg, #0093e9 0%, #80d0c7 100%);
+      }
+    }
+  }
 }
 
 // 屏幕宽度大于1500的时候
 @media screen and (min-width: $w-1500) {
   .pull-wrap {
-    width: $w-1350;
+    width: $w-1450;
 
     .left {
-      width: $w-1000;
+      width: $w-1100;
+      :deep(video) {
+        max-width: $w-1100;
+      }
+      :deep(canvas) {
+        max-width: $w-1100;
+      }
     }
     .right {
       width: $w-300;

+ 1 - 0
src/views/pull/recharge/index.vue

@@ -102,6 +102,7 @@ async function startPay() {
 
 <style lang="scss" scoped>
 .recharge-wrap {
+  position: fixed;
   .title {
     display: flex;
     align-items: center;

+ 5 - 1
src/views/push/index.vue

@@ -351,7 +351,7 @@
           ></textarea>
           <div
             class="btn"
-            @click="sendDanmu"
+            @click="handleSendDanmu"
           >
             发送
           </div>
@@ -608,6 +608,10 @@ watch(
   }
 );
 
+function handleSendDanmu() {
+  sendDanmu();
+}
+
 function handlePushStr(str) {
   danmuStr.value += str;
   showEmoji.value = false;