Răsfoiți Sursa

feat: webm推流

shuisheng 2 ani în urmă
părinte
comite
b4c3807491

+ 1 - 1
package.json

@@ -47,7 +47,7 @@
     "pinia": "^2.0.33",
     "pinia-plugin-persistedstate": "^3.2.0",
     "qrcode": "^1.5.3",
-    "socket.io-client": "^4.6.1",
+    "socket.io-client": "^4.7.2",
     "video.js": "^8.3.0",
     "vue": "^3.3.4",
     "vue-demi": "^0.13.11",

+ 13 - 13
pnpm-lock.yaml

@@ -51,8 +51,8 @@ dependencies:
     specifier: ^1.5.3
     version: 1.5.3
   socket.io-client:
-    specifier: ^4.6.1
-    version: 4.6.1
+    specifier: ^4.7.2
+    version: 4.7.2
   video.js:
     specifier: ^8.3.0
     version: 8.3.0
@@ -5047,12 +5047,12 @@ packages:
     resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
     engines: {node: '>= 0.8'}
 
-  /engine.io-client@6.4.0:
-    resolution: {integrity: sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==}
+  /engine.io-client@6.5.2:
+    resolution: {integrity: sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==}
     dependencies:
       '@socket.io/component-emitter': 3.1.0
       debug: 4.3.4(supports-color@9.3.1)
-      engine.io-parser: 5.0.6
+      engine.io-parser: 5.2.1
       ws: 8.11.0
       xmlhttprequest-ssl: 2.0.0
     transitivePeerDependencies:
@@ -5061,8 +5061,8 @@ packages:
       - utf-8-validate
     dev: false
 
-  /engine.io-parser@5.0.6:
-    resolution: {integrity: sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==}
+  /engine.io-parser@5.2.1:
+    resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==}
     engines: {node: '>=10.0.0'}
     dev: false
 
@@ -9784,22 +9784,22 @@ packages:
       is-fullwidth-code-point: 4.0.0
     dev: true
 
-  /socket.io-client@4.6.1:
-    resolution: {integrity: sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==}
+  /socket.io-client@4.7.2:
+    resolution: {integrity: sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==}
     engines: {node: '>=10.0.0'}
     dependencies:
       '@socket.io/component-emitter': 3.1.0
       debug: 4.3.4(supports-color@9.3.1)
-      engine.io-client: 6.4.0
-      socket.io-parser: 4.2.2
+      engine.io-client: 6.5.2
+      socket.io-parser: 4.2.4
     transitivePeerDependencies:
       - bufferutil
       - supports-color
       - utf-8-validate
     dev: false
 
-  /socket.io-parser@4.2.2:
-    resolution: {integrity: sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==}
+  /socket.io-parser@4.2.4:
+    resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
     engines: {node: '>=10.0.0'}
     dependencies:
       '@socket.io/component-emitter': 3.1.0

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

@@ -11,7 +11,7 @@ import {
   LiveLineEnum,
   LiveRoomTypeEnum,
 } from '@/interface';
-import { WsMsgTypeEnum } from '@/network/webSocket';
+import { WsMsgTypeEnum } from '@/interface-ws';
 import { useAppStore } from '@/store/app';
 import { usePiniaCacheStore } from '@/store/cache';
 import { useNetworkStore } from '@/store/network';

+ 5 - 9
src/hooks/use-push.ts

@@ -7,8 +7,7 @@ import {
   fetchUserHasLiveRoom,
 } from '@/api/userLiveRoom';
 import { DanmuMsgTypeEnum, ILiveRoom, IMessage } from '@/interface';
-import { WsMsrBlobType } from '@/interface-ws';
-import { WsMsgTypeEnum } from '@/network/webSocket';
+import { WsMsgTypeEnum, WsMsrBlobType } from '@/interface-ws';
 import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
@@ -242,8 +241,7 @@ export function usePush() {
     });
   }
 
-  async function startLive({ type, receiver }) {
-    console.log('startLive');
+  async function startLive({ type, receiver, chunkDelay }) {
     if (!loginTip()) return;
     const flag = await userHasLiveRoom();
     if (!flag) {
@@ -277,6 +275,7 @@ export function usePush() {
       name: roomName.value,
       type,
       receiver,
+      chunkDelay,
     });
   }
 
@@ -293,9 +292,7 @@ export function usePush() {
     closeRtc();
   }
 
-  function sendBlob(data: { blob; blobId: string; chunk }) {
-    roomLiving.value = false;
-    localStream.value = undefined;
+  function sendBlob(data: { blob; blobId: string; delay }) {
     const instance = networkStore.wsMap.get(roomId.value);
     if (instance) {
       instance.send<WsMsrBlobType['data']>({
@@ -304,11 +301,10 @@ export function usePush() {
           live_room_id: Number(roomId.value),
           blob: data.blob,
           blob_id: data.blobId,
-          chunk: data.chunk,
+          delay: data.delay,
         },
       });
     }
-    closeRtc();
   }
 
   function roomNameIsOk() {

+ 8 - 7
src/hooks/use-srs-ws.ts

@@ -14,11 +14,13 @@ import {
   WSGetRoomAllUserType,
   WsAnswerType,
   WsCandidateType,
+  WsConnectStatusEnum,
   WsGetLiveUserType,
   WsHeartbeatType,
   WsJoinType,
   WsLeavedType,
   WsMessageType,
+  WsMsgTypeEnum,
   WsOfferType,
   WsOtherJoinType,
   WsRoomLivingType,
@@ -26,12 +28,7 @@ import {
   WsUpdateJoinInfoType,
 } from '@/interface-ws';
 import { WebRTCClass } from '@/network/webRTC';
-import {
-  WebSocketClass,
-  WsConnectStatusEnum,
-  WsMsgTypeEnum,
-  prettierReceiveWsMsg,
-} from '@/network/webSocket';
+import { WebSocketClass, prettierReceiveWsMsg } from '@/network/webSocket';
 import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
@@ -132,12 +129,14 @@ export const useSrsWs = () => {
     name,
     type,
     receiver,
+    chunkDelay,
   }: {
     coverImg?: string;
     name?: string;
     type: LiveRoomTypeEnum;
     receiver: string;
     videoEl?: HTMLVideoElement;
+    chunkDelay?: number;
   }) {
     console.log('handleStartLivehandleStartLive', receiver);
     networkStore.wsMap.get(roomId.value)?.send<WsStartLiveType['data']>({
@@ -146,6 +145,7 @@ export const useSrsWs = () => {
         cover_img: coverImg!,
         name: name!,
         type,
+        chunkDelay,
       },
     });
     if (type === LiveRoomTypeEnum.user_msr) {
@@ -216,8 +216,9 @@ export const useSrsWs = () => {
     });
 
     // websocket连接断开
-    ws.socketIo.on(WsConnectStatusEnum.disconnect, () => {
+    ws.socketIo.on(WsConnectStatusEnum.disconnect, (err) => {
       prettierReceiveWsMsg(WsConnectStatusEnum.disconnect, ws);
+      console.log('websocket连接断开', err);
       if (!ws) return;
       ws.status = WsConnectStatusEnum.disconnect;
       ws.update();

+ 53 - 2
src/interface-ws.ts

@@ -5,6 +5,57 @@ import {
   LiveRoomTypeEnum,
 } from './interface';
 
+// websocket连接状态
+export enum WsConnectStatusEnum {
+  /** 已连接 */
+  connection = 'connection',
+  /** 连接中 */
+  connecting = 'connecting',
+  /** 已连接 */
+  connected = 'connected',
+  /** 断开连接中 */
+  disconnecting = 'disconnecting',
+  /** 已断开连接 */
+  disconnect = 'disconnect',
+  /** 重新连接 */
+  reconnect = 'reconnect',
+  /** 客户端的已连接 */
+  connect = 'connect',
+}
+
+// websocket消息类型
+export enum WsMsgTypeEnum {
+  /** 用户进入聊天 */
+  join = 'join',
+  /** 用户进入聊天完成 */
+  joined = 'joined',
+  /** 用户进入聊天 */
+  otherJoin = 'otherJoin',
+  /** 用户退出聊天 */
+  leave = 'leave',
+  /** 用户退出聊天完成 */
+  leaved = 'leaved',
+  /** 当前所有在线用户 */
+  liveUser = 'liveUser',
+  /** 用户发送消息 */
+  message = 'message',
+  /** 房间正在直播 */
+  roomLiving = 'roomLiving',
+  /** 房间不在直播 */
+  roomNoLive = 'roomNoLive',
+  getLiveUser = 'getLiveUser',
+  updateJoinInfo = 'updateJoinInfo',
+  heartbeat = 'heartbeat',
+  startLive = 'startLive',
+  endLive = 'endLive',
+
+  offer = 'offer',
+  answer = 'answer',
+  candidate = 'candidate',
+
+  msrBlob = 'msrBlob',
+}
+
 export interface IWsFormat<T> {
   /** 用户socket_id */
   socket_id: string;
@@ -51,6 +102,7 @@ export type WsStartLiveType = IWsFormat<{
   cover_img: string;
   name: string;
   type: LiveRoomTypeEnum;
+  chunkDelay: number;
 }>;
 
 export type WsJoinType = IWsFormat<{
@@ -98,6 +150,5 @@ export type WsMsrBlobType = IWsFormat<{
   live_room_id: number;
   blob: any;
   blob_id: string;
-  /** 每个blob多少秒 */
-  chunk: number;
+  delay: number;
 }>;

+ 1 - 3
src/network/webRTC.ts

@@ -1,12 +1,10 @@
 import { getRandomString } from 'billd-utils';
 
 import { MediaTypeEnum } from '@/interface';
-import { WsCandidateType } from '@/interface-ws';
+import { WsCandidateType, WsMsgTypeEnum } from '@/interface-ws';
 import { AppRootState, useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 
-import { WsMsgTypeEnum } from './webSocket';
-
 export class WebRTCClass {
   roomId = '-1';
   receiver = '';

+ 1 - 51
src/network/webSocket.ts

@@ -1,59 +1,9 @@
 import { Socket, io } from 'socket.io-client';
 
-import { IWsFormat } from '@/interface-ws';
+import { IWsFormat, WsConnectStatusEnum, WsMsgTypeEnum } from '@/interface-ws';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
 
-// websocket连接状态
-export enum WsConnectStatusEnum {
-  /** 已连接 */
-  connection = 'connection',
-  /** 连接中 */
-  connecting = 'connecting',
-  /** 已连接 */
-  connected = 'connected',
-  /** 断开连接中 */
-  disconnecting = 'disconnecting',
-  /** 已断开连接 */
-  disconnect = 'disconnect',
-  /** 重新连接 */
-  reconnect = 'reconnect',
-  /** 客户端的已连接 */
-  connect = 'connect',
-}
-
-// websocket消息类型
-export enum WsMsgTypeEnum {
-  /** 用户进入聊天 */
-  join = 'join',
-  /** 用户进入聊天完成 */
-  joined = 'joined',
-  /** 用户进入聊天 */
-  otherJoin = 'otherJoin',
-  /** 用户退出聊天 */
-  leave = 'leave',
-  /** 用户退出聊天完成 */
-  leaved = 'leaved',
-  /** 当前所有在线用户 */
-  liveUser = 'liveUser',
-  /** 用户发送消息 */
-  message = 'message',
-  /** 房间正在直播 */
-  roomLiving = 'roomLiving',
-  /** 房间不在直播 */
-  roomNoLive = 'roomNoLive',
-  getLiveUser = 'getLiveUser',
-  updateJoinInfo = 'updateJoinInfo',
-  heartbeat = 'heartbeat',
-  startLive = 'startLive',
-
-  offer = 'offer',
-  answer = 'answer',
-  candidate = 'candidate',
-
-  msrBlob = 'msrBlob',
-}
-
 export function prettierReceiveWsMsg(...arg) {
   console.warn('【websocket】收到消息', ...arg);
 }

+ 52 - 20
src/views/push/index.vue

@@ -117,7 +117,7 @@
               v-else
               type="error"
               size="small"
-              @click="endLive"
+              @click="handleEndLive"
             >
               结束直播
             </n-button>
@@ -283,7 +283,6 @@ import {
   VolumeMuteOutline,
 } from '@vicons/ionicons5';
 import { fabric } from 'fabric';
-import MediaStreamRecorder from 'msr';
 import {
   Raw,
   markRaw,
@@ -371,21 +370,41 @@ const wrapSize = reactive({
 const workerTimerId = ref(-1);
 const bodyAppendChildElArr = ref<HTMLElement[]>([]);
 const liveType = Number(route.query.liveType);
-const mediaRecorder = ref();
+const recorder = ref<MediaRecorder>();
+const sendBlobTimer = ref();
+const bolbId = ref(0);
+const chunkDelay = ref(1000 * 2);
+
+function handleSendBlob(event: BlobEvent) {
+  bolbId.value += 1;
+  sendBlob({
+    blob: event.data,
+    blobId: `${bolbId.value}`,
+    delay: chunkDelay.value,
+  });
+}
 
 function handleMsr(stream: MediaStream) {
-  mediaRecorder.value = new MediaStreamRecorder(stream);
-  setInterval(() => {
-    console.log(stream.getAudioTracks());
-  }, 1000);
-  mediaRecorder.value.mimeType = 'video/webm';
-  const chunk = 1000 * 2;
-  let id = 0;
-  mediaRecorder.value.ondataavailable = function (blob) {
-    id += 1;
-    sendBlob({ blob, blobId: `${id}`, chunk });
-  };
-  mediaRecorder.value.start(chunk);
+  // https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter
+  const mimeType = 'video/webm;codecs=avc1.4d002a,opus';
+  // const mimeType = 'video/webm;codecs=avc1.64001f,opus'; // b站的参数
+
+  if (!MediaRecorder.isTypeSupported(mimeType)) {
+    console.error('不支持', mimeType);
+    return;
+  } else {
+    console.log('支持', mimeType);
+  }
+  recorder.value = new MediaRecorder(stream, {
+    mimeType,
+    // videoBitsPerSecond: 1000 * 2000,
+    // audioBitsPerSecond: 1000 * 2000,
+  });
+  recorder.value.ondataavailable = handleSendBlob;
+  sendBlobTimer.value = setInterval(function () {
+    recorder.value?.stop();
+    recorder.value?.start();
+  }, chunkDelay.value);
 }
 
 watch(
@@ -409,7 +428,7 @@ onMounted(() => {
 });
 
 onUnmounted(() => {
-  mediaRecorder.value.stop();
+  recorder.value?.stop();
   bodyAppendChildElArr.value.forEach((el) => {
     el.remove();
   });
@@ -498,12 +517,13 @@ function handleMixedAudio() {
   if (webaudioVideo.value) {
     webaudioVideo.value.remove();
   }
-  webaudioVideo.value = createVideo({ appendChild: true, show: false });
+  webaudioVideo.value = createVideo({ appendChild: true, show: true });
   bodyAppendChildElArr.value.push(webaudioVideo.value);
   webaudioVideo.value.className = 'web-audio-video';
   webaudioVideo.value!.srcObject = destination.stream;
   const resAudio = destination.stream.getAudioTracks()[0];
   canvasVideoStream.value?.addTrack(resAudio);
+  console.log(resAudio, 'resAudio');
   networkStore.rtcMap.forEach((rtc) => {
     const sender = rtc.peerConnection
       ?.getSenders()
@@ -515,13 +535,26 @@ function handleMixedAudio() {
         ?.replaceTrack(resAudio);
     }
   });
-  console.log(canvasVideoStream.value?.getAudioTracks());
+}
+
+function handleEndLive() {
+  clearInterval(sendBlobTimer.value);
+  recorder.value?.removeEventListener('dataavailable', handleSendBlob);
+  endLive();
 }
 
 function handleStartLive() {
+  if (!appStore.allTrack.length) {
+    window.$message.warning('至少选择一个素材');
+    return;
+  }
   handleMixedAudio();
   lastCoverImg.value = generateBase64(pushCanvasRef.value!);
-  startLive({ type: liveType, receiver: mySocketId.value });
+  startLive({
+    type: liveType,
+    receiver: mySocketId.value,
+    chunkDelay: chunkDelay.value,
+  });
   if (liveType === LiveRoomTypeEnum.user_msr) {
     const stream = pushCanvasRef.value!.captureStream();
     // @ts-ignore
@@ -714,7 +747,6 @@ function initCanvas() {
   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回调');
   });

+ 11 - 2
src/views/rank/index.vue

@@ -62,7 +62,16 @@
           <div class="rank">
             <i>{{ item.rank >= 10 ? item.rank : '0' + item.rank }}</i>
           </div>
-          <div class="left">
+          <div
+            class="left"
+            @click="
+              currRankType !== RankTypeEnum.blog &&
+                router.push({
+                  name: routerName.profile,
+                  params: { userId: item.user.id },
+                })
+            "
+          >
             <img
               :src="item.user.avatar"
               class="avatar"
@@ -492,7 +501,7 @@ onMounted(() => {
           display: flex;
           align-items: center;
           font-size: 12px;
-
+          cursor: pointer;
           .avatar {
             margin-right: 10px;
             width: 28px;