shuisheng 1 年間 前
コミット
5ac5f7150a

+ 40 - 16
src/hooks/use-push.ts

@@ -19,7 +19,7 @@ import {
   WsRoomNoLiveType,
 } from '@/types/websocket';
 import { createVideo, generateBase64 } from '@/utils';
-import { handleMaxFramerate } from '@/utils/network/webRTC';
+import { handlConstraints } from '@/utils/network/webRTC';
 
 import { commentAuthTip, loginTip } from './use-login';
 import { useTip } from './use-tip';
@@ -54,6 +54,8 @@ export function usePush() {
     currentMaxFramerate,
     currentMaxBitrate,
     currentResolutionRatio,
+    currentAudioContentHint,
+    currentVideoContentHint,
   } = useWebsocket();
 
   onMounted(() => {
@@ -79,13 +81,31 @@ export function usePush() {
 
   watch(
     () => currentResolutionRatio.value,
-    (newVal) => {
-      networkStore.rtcMap.forEach(async (rtc) => {
-        const res = await rtc.setResolutionRatio(newVal);
-        if (res === 1) {
-          window.$message.success('切换分辨率成功!');
-        } else {
-          window.$message.success('切换分辨率失败!');
+    (newval) => {
+      console.log('分辨率变了', newval);
+      networkStore.rtcMap.forEach((rtc) => {
+        if (canvasVideoStream.value) {
+          handlConstraints({
+            frameRate: rtc.maxFramerate,
+            height: newval,
+            stream: canvasVideoStream.value,
+          });
+        }
+      });
+    }
+  );
+
+  watch(
+    () => currentMaxFramerate.value,
+    (newval) => {
+      console.log('帧率变了', newval);
+      networkStore.rtcMap.forEach((rtc) => {
+        if (canvasVideoStream.value) {
+          handlConstraints({
+            frameRate: newval,
+            height: rtc.resolutionRatio,
+            stream: canvasVideoStream.value,
+          });
         }
       });
     }
@@ -93,9 +113,10 @@ export function usePush() {
 
   watch(
     () => currentMaxBitrate.value,
-    (newVal) => {
+    (newval) => {
+      console.log('码率变了', newval);
       networkStore.rtcMap.forEach(async (rtc) => {
-        const res = await rtc.setMaxBitrate(newVal);
+        const res = await rtc.setMaxBitrate(newval);
         if (res === 1) {
           window.$message.success('切换码率成功!');
         } else {
@@ -220,12 +241,13 @@ export function usePush() {
         }
       }
     }
-
-    handleMaxFramerate({
-      stream: canvasVideoStream.value!,
-      height: currentResolutionRatio.value,
-      frameRate: currentMaxFramerate.value,
-    });
+    if (canvasVideoStream.value) {
+      handlConstraints({
+        stream: canvasVideoStream.value,
+        height: currentResolutionRatio.value,
+        frameRate: currentMaxFramerate.value,
+      });
+    }
     handleStartLive({
       name: roomName.value,
       type,
@@ -352,6 +374,8 @@ export function usePush() {
     currentResolutionRatio,
     currentMaxBitrate,
     currentMaxFramerate,
+    currentAudioContentHint,
+    currentVideoContentHint,
     danmuStr,
     roomName,
     damuList,

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

@@ -122,6 +122,50 @@ export const useRTCParams = () => {
       disabled: true,
     },
   ]);
+  const videoContentHint = ref([
+    {
+      label: '默认',
+      value: '',
+      disabled: false,
+    },
+    {
+      label: '运动',
+      value: 'motion',
+      disabled: false,
+    },
+    {
+      label: '文本',
+      value: 'text',
+      disabled: false,
+    },
+    {
+      label: '平衡',
+      value: 'detail',
+      disabled: false,
+    },
+  ]);
+  const audioContentHint = ref([
+    {
+      label: '默认',
+      value: '',
+      disabled: false,
+    },
+    {
+      label: '音乐',
+      value: 'music',
+      disabled: false,
+    },
+    {
+      label: '语言',
+      value: 'speech',
+      disabled: false,
+    },
+    {
+      label: '语音识别',
+      value: 'speech-recognition',
+      disabled: false,
+    },
+  ]);
   watch(
     () => userStore.userInfo,
     (newval) => {
@@ -184,5 +228,12 @@ export const useRTCParams = () => {
       },
     };
 
-  return { maxBitrate, maxFramerate, resolutionRatio, allMediaTypeList };
+  return {
+    maxBitrate,
+    maxFramerate,
+    resolutionRatio,
+    videoContentHint,
+    audioContentHint,
+    allMediaTypeList,
+  };
 };

+ 25 - 6
src/hooks/use-websocket.ts

@@ -49,6 +49,7 @@ import {
 import {
   createNullVideo,
   handleUserMedia,
+  setAudioTrackContentHints,
   setVideoTrackContentHints,
 } from '@/utils';
 import {
@@ -67,7 +68,13 @@ export const useWebsocket = () => {
   const userStore = useUserStore();
   const networkStore = useNetworkStore();
 
-  const { maxBitrate, maxFramerate, resolutionRatio } = useRTCParams();
+  const {
+    maxBitrate,
+    maxFramerate,
+    resolutionRatio,
+    videoContentHint,
+    audioContentHint,
+  } = useRTCParams();
   const { updateWebRtcRemoteDeskConfig, webRtcRemoteDesk } =
     useWebRtcRemoteDesk();
   const { updateWebRtcMeetingPkConfig, webRtcMeetingPk } = useWebRtcMeetingPk();
@@ -97,6 +104,8 @@ export const useWebsocket = () => {
   const currentMaxBitrate = ref(maxBitrate.value[3].value);
   const currentMaxFramerate = ref(maxFramerate.value[2].value);
   const currentResolutionRatio = ref(resolutionRatio.value[3].value);
+  const currentVideoContentHint = ref(videoContentHint.value[3].value);
+  const currentAudioContentHint = ref(audioContentHint.value[0].value);
   const timerObj = ref({});
   const damuList = ref<IDanmu[]>([]);
 
@@ -182,6 +191,19 @@ export const useWebsocket = () => {
         msrMaxDelay,
       },
     });
+    if (canvasVideoStream.value) {
+      setVideoTrackContentHints(
+        canvasVideoStream.value,
+        // @ts-ignore
+        currentVideoContentHint.value
+      );
+      setAudioTrackContentHints(
+        canvasVideoStream.value,
+        // @ts-ignore
+        currentAudioContentHint.value
+      );
+    }
+
     if (type === LiveRoomTypeEnum.srs) {
       updateWebRtcSrsConfig({
         isPk: false,
@@ -831,11 +853,6 @@ export const useWebsocket = () => {
         ) {
           return;
         }
-        setVideoTrackContentHints(
-          // @ts-ignore
-          canvasVideoStream.value,
-          'detail'
-        );
         if (data.live_room.type === LiveRoomTypeEnum.wertc_live) {
           updateWebRtcLiveConfig({
             roomId: roomId.value,
@@ -995,5 +1012,7 @@ export const useWebsocket = () => {
     currentMaxFramerate,
     currentMaxBitrate,
     currentResolutionRatio,
+    currentAudioContentHint,
+    currentVideoContentHint,
   };
 };

+ 18 - 0
src/utils/index.ts

@@ -20,6 +20,24 @@ export function setVideoTrackContentHints(
     console.log('setVideoTrackContentHints', track.id, hint);
   });
 }
+/**
+ * music,该曲目应被视为包含音乐。设置该值时 MediaStreamTrack.kind的值必须为"audio"。
+ * speech,该轨道应被视为包含语音数据。设置该值时 MediaStreamTrack.kind的值必须为"audio"。
+ * speech-recognition,该轨道应被视为包含用于机器语音识别的数据。设置该值时 MediaStreamTrack.kind的值必须为"audio"。
+ * detail,应将曲目视为视频细节格外重要。例如,带有文本内容、绘画或线条艺术的演示文稿或网页。设置该值时 MediaStreamTrack.kind的值必须为"video"。
+ * text,轨道应该被视为视频细节特别重要,并且明显的锐利边缘和颜色一致的区域可能经常出现。例如,带有文本内容的演示文稿或网页。设置该值时 MediaStreamTrack.kind的值必须为"video"。
+ * motion,应将轨道视为包含运动很重要的视频。例如,网络摄像头视频、电影或视频游戏。设置该值时 MediaStreamTrack.kind的值必须为"video"。
+ */
+export function setAudioTrackContentHints(
+  stream: MediaStream,
+  hint: 'music' | 'speech' | 'speech-recognition'
+) {
+  const tracks = stream.getAudioTracks();
+  tracks.forEach((track) => {
+    track.contentHint = hint;
+    console.log('setAudioTrackContentHints', track.id, hint);
+  });
+}
 
 /**
  * 将base64转换为file

+ 5 - 66
src/utils/network/webRTC.ts

@@ -6,15 +6,15 @@ import { AppRootState, useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 import { WsCandidateType, WsMsgTypeEnum } from '@/types/websocket';
 
-/** 设置分辨率 */
-export async function handleResolutionRatio(data: {
+/** 设置约束 */
+export async function handlConstraints(data: {
   frameRate: number;
   height: number;
   stream: MediaStream;
 }): Promise<number> {
   const { frameRate, height, stream } = data;
   const queue: Promise<any>[] = [];
-  console.log('开始设置分辨率', height);
+  console.log('开始设置约束', JSON.stringify({ height, frameRate }));
   stream.getTracks().forEach((track) => {
     if (track.kind === 'video') {
       queue.push(
@@ -27,39 +27,10 @@ export async function handleResolutionRatio(data: {
   });
   try {
     await Promise.all(queue);
-    console.log('设置分辨率成功');
+    console.log('设置设置约束成功');
     return 1;
   } catch (error) {
-    console.error('设置分辨率失败', height, error);
-    return 0;
-  }
-}
-
-/** 设置帧率 */
-export async function handleMaxFramerate(data: {
-  frameRate: number;
-  height: number;
-  stream: MediaStream;
-}): Promise<number> {
-  const { frameRate, height, stream } = data;
-  const queue: Promise<any>[] = [];
-  console.log('开始设置帧率', frameRate);
-  stream.getTracks().forEach((track) => {
-    if (track.kind === 'video') {
-      queue.push(
-        track.applyConstraints({
-          height: { ideal: height },
-          frameRate: { ideal: frameRate },
-        })
-      );
-    }
-  });
-  try {
-    await Promise.all(queue);
-    console.log('设置帧率成功');
-    return 1;
-  } catch (error) {
-    console.error('设置帧率失败', frameRate, error);
+    console.error('设置设置约束失败', error);
     return 0;
   }
 }
@@ -95,14 +66,12 @@ export class WebRTCClass {
     isSRS: boolean;
     sender: string;
     receiver: string;
-    localStream?: MediaStream;
   }) {
     this.roomId = data.roomId;
     this.videoEl = data.videoEl;
     // document.body.appendChild(this.videoEl);
     this.sender = data.sender;
     this.receiver = data.receiver;
-    this.localStream = data.localStream;
     if (data.maxBitrate) {
       this.maxBitrate = data.maxBitrate;
     }
@@ -142,30 +111,6 @@ export class WebRTCClass {
     }
   };
 
-  /** 设置分辨率 */
-  setResolutionRatio = async (height: number) => {
-    if (this.localStream) {
-      const res = await handleResolutionRatio({
-        frameRate: this.maxFramerate,
-        stream: this.localStream,
-        height,
-      });
-      return res;
-    }
-  };
-
-  /** 设置最大帧率 */
-  setMaxFramerate = async (maxFramerate: number) => {
-    if (this.localStream) {
-      const res = await handleMaxFramerate({
-        frameRate: maxFramerate,
-        stream: this.localStream,
-        height: this.resolutionRatio,
-      });
-      return res;
-    }
-  };
-
   /** 设置最大码率 */
   setMaxBitrate = (maxBitrate: number) => {
     console.log('开始设置最大码率', maxBitrate);
@@ -479,12 +424,6 @@ export class WebRTCClass {
           if (this.maxBitrate !== -1) {
             this.setMaxBitrate(this.maxBitrate);
           }
-          if (this.maxFramerate !== -1) {
-            this.setMaxFramerate(this.maxFramerate);
-          }
-          if (this.resolutionRatio !== -1) {
-            this.setResolutionRatio(this.resolutionRatio);
-          }
         }
         if (connectionState === 'disconnected') {
           // 表示至少有一个 ICE 连接处于 disconnected 状态,并且没有连接处于 failed、connecting 或 checking 状态。

+ 186 - 143
src/views/push/index.vue

@@ -46,7 +46,7 @@
             <template #trigger>
               <n-icon
                 size="26"
-                :color="recording ? 'red' : '#3f7ee8'"
+                :color="recording ? 'red' : THEME_COLOR"
               >
                 <Videocam v-if="!recording"></Videocam>
                 <VideocamOffSharp v-else></VideocamOffSharp>
@@ -87,92 +87,115 @@
           ></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"
+              <div class="name">
+                <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="other">
+                <span
+                  v-if="NODE_ENV === 'development'"
+                  class="item"
+                >
+                  <span>{{ mySocketId }}</span>
+                  <span>---</span>
+                  <span>{{
+                    liveRoomTypeEnumMap[appStore.liveRoomInfo?.type || '']
+                  }}</span>
+                </span>
+                <span
+                  class="item share"
+                  @click="handleShare"
                 >
-                  确定
-                </n-button>
-              </n-input-group>
+                  分享直播间
+                </span>
+                <span class="item">
+                  正在观看:
+                  {{ liveUserList.length }}
+                </span>
+              </div>
             </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">
-            <div class="item">
-              <i class="ico"></i>
-              <span>
-                正在观看:
-                {{ liveUserList.length }}
-              </span>
-            </div>
-            <div
-              class="item"
-              v-if="NODE_ENV === 'development'"
-            >
-              {{ liveRoomTypeEnumMap[appStore.liveRoomInfo?.type || ''] }}
+              <div class="rtc">
+                <div class="item">
+                  <div class="txt">码率:</div>
+                  <div class="down small">
+                    <n-select
+                      size="small"
+                      v-model:value="currentMaxBitrate"
+                      :options="maxBitrate"
+                    />
+                  </div>
+                </div>
+                <div class="item">
+                  <div class="txt">帧率:</div>
+                  <div class="down small">
+                    <n-select
+                      size="small"
+                      v-model:value="currentMaxFramerate"
+                      :options="maxFramerate"
+                    />
+                  </div>
+                </div>
+                <div class="item">
+                  <div class="txt">分辨率:</div>
+                  <div class="down big">
+                    <n-select
+                      size="small"
+                      v-model:value="currentResolutionRatio"
+                      :options="resolutionRatio"
+                    />
+                  </div>
+                </div>
+                <div class="item">
+                  <div class="txt">视频内容:</div>
+                  <div class="down small">
+                    <n-select
+                      size="small"
+                      v-model:value="currentVideoContentHint"
+                      :options="videoContentHint"
+                    />
+                  </div>
+                </div>
+                <div class="item">
+                  <div class="txt">音频内容:</div>
+                  <div class="down big">
+                    <n-select
+                      size="small"
+                      v-model:value="currentAudioContentHint"
+                      :options="audioContentHint"
+                    />
+                  </div>
+                </div>
+              </div>
+              <n-button
+                v-if="!roomLiving"
+                type="primary"
+                @click="handleStartLive"
+              >
+                开始直播
+              </n-button>
+              <n-button
+                v-else
+                type="error"
+                @click="handleEndLive"
+              >
+                结束直播
+              </n-button>
             </div>
           </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="handleEndLive"
-            >
-              结束直播
-            </n-button>
-          </div>
         </div>
       </div>
     </div>
@@ -436,7 +459,7 @@ import {
   VolumeMuteOutline,
 } from '@vicons/ionicons5';
 import { AVRecorder } from '@webav/av-recorder';
-import { getRandomString } from 'billd-utils';
+import { copyToClipBoard, getRandomString } from 'billd-utils';
 import { fabric } from 'fabric';
 import {
   Raw,
@@ -460,6 +483,7 @@ import { emojiArray } from '@/emoji';
 import { commentAuthTip, loginTip } from '@/hooks/use-login';
 import { usePush } from '@/hooks/use-push';
 import { useRTCParams } from '@/hooks/use-rtcParams';
+import { useTip } from '@/hooks/use-tip';
 import { useQiniuJsUpload } from '@/hooks/use-upload';
 import {
   DanmuMsgTypeEnum,
@@ -479,6 +503,7 @@ import {
   createVideo,
   formatDownTime2,
   generateBase64,
+  getLiveRoomPageUrl,
   getRandomEnglishString,
   handleUserMedia,
   readFile,
@@ -496,8 +521,14 @@ const userStore = useUserStore();
 const appStore = useAppStore();
 const networkStore = useNetworkStore();
 const cacheStore = usePiniaCacheStore();
-const { maxBitrate, maxFramerate, resolutionRatio, allMediaTypeList } =
-  useRTCParams();
+const {
+  maxBitrate,
+  maxFramerate,
+  resolutionRatio,
+  audioContentHint,
+  videoContentHint,
+  allMediaTypeList,
+} = useRTCParams();
 
 const {
   confirmRoomName,
@@ -515,6 +546,8 @@ const {
   currentResolutionRatio,
   currentMaxBitrate,
   currentMaxFramerate,
+  currentAudioContentHint,
+  currentVideoContentHint,
   danmuStr,
   roomName,
   damuList,
@@ -576,7 +609,8 @@ watch(
 
 watch(
   () => currentMaxBitrate.value,
-  () => {
+  (newval) => {
+    console.log('码率变了', newval);
     if (liveType === LiveRoomTypeEnum.msr) {
       const stream = pushCanvasRef.value!.captureStream();
       const audioTrack = webaudioVideo
@@ -591,11 +625,20 @@ watch(
 
 watch(
   () => currentMaxFramerate.value,
-  () => {
+  (newval) => {
+    console.log('帧率变了,修改画布', newval);
     renderFrame();
   }
 );
 
+watch(
+  () => currentResolutionRatio.value,
+  (newval, oldval) => {
+    console.log('分辨率变了,修改画布', newval);
+    changeCanvasAttr({ newHeight: newval, oldHeight: oldval });
+  }
+);
+
 watch(
   () => networkStore.rtcMap,
   (newVal) => {
@@ -603,12 +646,6 @@ watch(
       if (appStore.allTrack.find((v) => v.mediaName === item.receiver)) {
         return;
       }
-      // if (lockMap.value.has(item.localStream?.id)) {
-      //   return;
-      // }
-      // if (item.localStream?.id) {
-      //   lockMap.value.add(item.localStream?.id);
-      // }
       addMediaOk({
         id: getRandomEnglishString(6),
         openEye: true,
@@ -1119,6 +1156,20 @@ async function uploadLivePreview() {
   }
 }
 
+function handleShare() {
+  useTip({
+    content: `直播间地址:${getLiveRoomPageUrl(+roomId.value)}`,
+    title: '分享',
+    confirmButtonText: '复制',
+    hiddenCancel: true,
+  })
+    .then(() => {
+      copyToClipBoard(getLiveRoomPageUrl(+roomId.value));
+      window.$message.success('复制成功');
+    })
+    .catch();
+}
+
 function handleStartLive() {
   if (!appStore.allTrack.length) {
     window.$message.warning('至少选择一个素材');
@@ -1247,13 +1298,6 @@ function autoCreateVideo(data: {
   });
 }
 
-watch(
-  () => currentResolutionRatio.value,
-  (newHeight, oldHeight) => {
-    changeCanvasAttr({ newHeight, oldHeight });
-  }
-);
-
 // 容器宽高,1280*720,即720p
 // canvas容器宽高,2560*1440,即1440p
 
@@ -2450,17 +2494,15 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
     .room-control {
       display: flex;
       justify-content: space-between;
-      padding: 20px;
+      padding: 15px;
+      border-radius: 0 0 6px 6px;
       background-color: papayawhip;
-
       .info {
         display: flex;
-        align-items: center;
-
+        width: 100%;
         .avatar {
-          margin-right: 20px;
-          width: 55px;
-          height: 55px;
+          width: 80px;
+          height: 80px;
           border-radius: 50%;
           background-position: center center;
           background-size: cover;
@@ -2468,50 +2510,51 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
         }
         .detail {
           display: flex;
+          flex: 1;
           flex-direction: column;
-          flex-shrink: 0;
-          width: 200px;
-          text-align: initial;
+          margin-left: 20px;
+          font-size: 14px;
+
           .top {
-            margin-bottom: 10px;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
             color: #18191c;
+            .other {
+              .item {
+                margin-right: 10px;
+                &.share {
+                  cursor: pointer;
+                }
+              }
+            }
           }
           .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;
+            display: flex;
+            align-items: center;
+            flex: 1;
+            justify-content: space-between;
+            .rtc {
+              display: flex;
+              align-items: center;
+              flex: 0.95;
+              .item {
+                display: flex;
+                align-items: center;
+                padding-right: 10px;
+                .down {
+                  &.small {
+                    width: 90px;
+                  }
+                  &.big {
+                    width: 110px;
+                  }
+                }
+              }
+            }
           }
         }
       }
-      .other {
-        display: flex;
-        flex-direction: column;
-        justify-content: center;
-        font-size: 12px;
-        .top {
-        }
-        .bottom {
-          margin-top: 10px;
-        }
-      }
     }
   }
   .right {