shuisheng %!s(int64=2) %!d(string=hai) anos
pai
achega
d1c77163e1

+ 1 - 1
package.json

@@ -36,7 +36,7 @@
     "axios": "^1.2.1",
     "billd-deploy": "^1.0.16",
     "billd-html-webpack-plugin": "^1.0.1",
-    "billd-scss": "^0.0.6",
+    "billd-scss": "^0.0.7",
     "billd-utils": "^0.0.12",
     "browser-tool": "^1.0.5",
     "js-cookie": "^3.0.5",

+ 4 - 4
pnpm-lock.yaml

@@ -18,7 +18,7 @@ specifiers:
   babel-loader: ^8.2.2
   billd-deploy: ^1.0.16
   billd-html-webpack-plugin: ^1.0.1
-  billd-scss: ^0.0.6
+  billd-scss: ^0.0.7
   billd-utils: ^0.0.12
   browser-tool: ^1.0.5
   chalk: ^4
@@ -85,7 +85,7 @@ dependencies:
   axios: 1.3.4
   billd-deploy: 1.0.16_imakxv3mh5kp5z5uouwrjmnj5q
   billd-html-webpack-plugin: 1.0.1_imakxv3mh5kp5z5uouwrjmnj5q
-  billd-scss: 0.0.6
+  billd-scss: 0.0.7
   billd-utils: 0.0.12_typescript@4.9.5
   browser-tool: 1.0.5
   js-cookie: 3.0.5
@@ -3192,8 +3192,8 @@ packages:
       - webpack-cli
     dev: false
 
-  /billd-scss/0.0.6:
-    resolution: {integrity: sha512-pnoWXjq5/Iq5lB4hHEWeFebFbuPO7akPMz9HblcY7TKZZ7PuYgu1G2+2ZQtl7SvibBsT/iWHjKPKGDjVFb3FDA==}
+  /billd-scss/0.0.7:
+    resolution: {integrity: sha512-tB2z5TqDe0VObIU0MkbzicSh8tkfhrO3eN5lu6nWBcRljPyJsgjvoxhuYfXEBsnlYS8UWqQK/aYrc35IXZtUDA==}
     requiresBuild: true
     dev: false
 

+ 1 - 0
script/config/webpack.dev.ts

@@ -85,6 +85,7 @@ export default new Promise((resolve) => {
               },
             },
             '/api': {
+              // target: 'http://localhost:4200',
               target: 'http://localhost:4300',
               // target: 'https://live.hsslive.cn/aliyun-hk/',
               secure: false, // 默认情况下(secure: true),不接受在HTTPS上运行的带有无效证书的后端服务器。设置secure: false后,后端服务器的HTTPS有无效证书也可运行

+ 7 - 0
src/api/liveRoom.ts

@@ -10,3 +10,10 @@ export function fetchLiveRoomList(params: {
     params,
   });
 }
+
+export function fetchUpdateLiveRoomKey() {
+  return request.instance({
+    url: '/live_room/update_key',
+    method: 'put',
+  });
+}

+ 16 - 0
src/api/user.ts

@@ -7,7 +7,23 @@ export function fetchUserInfo() {
     method: 'get',
   });
 }
+export function fetchFindUser(userId: number) {
+  return request.instance({
+    url: `/user/find/${userId}`,
+    method: 'get',
+  });
+}
 
 export function fetchUserList(params: { orderName: string; orderBy: string }) {
   return request.get<IPaging<IUser>>('/user/list', { params });
 }
+
+export function fetchBlogUserList(params: {
+  orderName: string;
+  orderBy: string;
+}) {
+  return request.get<IPaging<IUser>>(
+    'https://api.hsslive.cn/prodapi/user/list',
+    { params }
+  );
+}

+ 115 - 77
src/hooks/use-pull.ts

@@ -6,13 +6,19 @@ import { fetchRtcV1Play } from '@/api/srs';
 import { useFlvPlay } from '@/hooks/use-play';
 import {
   DanmuMsgTypeEnum,
-  IAdminIn,
+  IAnswer,
   ICandidate,
   IDanmu,
+  IJoin,
   ILive,
   ILiveUser,
+  IMessage,
   IOffer,
+  IOtherJoin,
+  IUpdateJoinInfo,
+  LiveRoomTypeEnum,
   MediaTypeEnum,
+  liveTypeEnum,
 } from '@/interface';
 import { SRSWebRTCClass } from '@/network/srsWebRtc';
 import { WebRTCClass } from '@/network/webRtc';
@@ -52,6 +58,7 @@ export function usePull({
   const balance = ref('0.00');
   const damuList = ref<IDanmu[]>([]);
   const liveUserList = ref<ILiveUser[]>([]);
+  const videoLoading = ref(false);
   const isDone = ref(false);
   const roomNoLive = ref(false);
   const localStream = ref();
@@ -64,8 +71,8 @@ export function usePull({
   const { startPlay } = useFlvPlay();
 
   const track = reactive({
-    audio: true,
-    video: true,
+    audio: 1,
+    video: 1,
   });
   const giftList = ref([
     { name: '鲜花', ico: '', price: '免费' },
@@ -111,12 +118,13 @@ export function usePull({
         video: true,
         audio: true,
       });
+      const audio = event.getAudioTracks();
+      const video = event.getVideoTracks();
+      track.audio = audio.length ? 1 : 2;
+      track.video = video.length ? 1 : 2;
       console.log('getUserMedia成功', event);
       currMediaType.value = allMediaTypeList[MediaTypeEnum.camera];
       currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.camera]);
-      // localVideoRef.value.forEach((item) => {
-      //   item.srcObject = event;
-      // });
       localStream.value = event;
     }
   }
@@ -131,8 +139,8 @@ export function usePull({
       });
       const audio = event.getAudioTracks();
       const video = event.getVideoTracks();
-      track.audio = !!audio.length;
-      track.video = !!video.length;
+      track.audio = audio.length ? 1 : 2;
+      track.video = video.length ? 1 : 2;
       console.log('getDisplayMedia成功', event);
       currMediaType.value = allMediaTypeList[MediaTypeEnum.screen];
       currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.screen]);
@@ -152,17 +160,19 @@ export function usePull({
       if (userInfo && connected) {
         const instance = networkStore.wsMap.get(roomId.value);
         if (!instance) return;
+        const data: IUpdateJoinInfo['data'] = {
+          live_room_id: Number(roomId.value),
+        };
         instance.send({
           msgType: WsMsgTypeEnum.updateJoinInfo,
-          data: {
-            userInfo: userStore.userInfo,
-          },
+          data,
         });
       }
     }
   );
 
   function initPull() {
+    videoLoading.value = true;
     console.warn('开始new WebSocketClass');
     const ws = new WebSocketClass({
       roomId: roomId.value,
@@ -170,7 +180,7 @@ export function usePull({
         process.env.NODE_ENV === 'development'
           ? 'ws://localhost:4300'
           : 'wss://live.hsslive.cn',
-      isAdmin: false,
+      isAnchor: false,
     });
     ws.update();
     initReceive();
@@ -184,6 +194,7 @@ export function usePull({
 
     remoteVideoRef.value?.addEventListener('loadedmetadata', () => {
       console.warn('视频流-loadedmetadata');
+      videoLoading.value = false;
       const rtc = networkStore.getRtcMap(roomId.value);
       if (!rtc) return;
       rtc.rtcStatus.loadedmetadata = true;
@@ -219,9 +230,17 @@ export function usePull({
   function sendJoin() {
     const instance = networkStore.wsMap.get(roomId.value);
     if (!instance) return;
+    const joinData: IJoin['data'] = {
+      live_room: {
+        id: Number(roomId.value),
+        name: roomName.value,
+        type: isSRS ? LiveRoomTypeEnum.user_srs : LiveRoomTypeEnum.user_wertc,
+      },
+      track,
+    };
     instance.send({
       msgType: WsMsgTypeEnum.join,
-      data: { userInfo: userStore.userInfo },
+      data: joinData,
     });
   }
 
@@ -238,12 +257,9 @@ export function usePull({
   function addTrack() {
     if (!localStream.value) return;
     liveUserList.value.forEach((item) => {
-      if (item.socketId !== getSocketId()) {
+      if (item.id !== getSocketId()) {
         localStream.value.getTracks().forEach((track) => {
-          const rtc = networkStore.getRtcMap(
-            `${roomId.value}___${item.socketId}`
-          );
-          console.log(rtc, track, localStream.value, 9998);
+          const rtc = networkStore.getRtcMap(`${roomId.value}___${item.id}`);
           rtc?.addTrack(track, localStream.value);
         });
       }
@@ -264,13 +280,20 @@ export function usePull({
     if (!rtc) return;
     const sdp = await rtc.createOffer();
     await rtc.setLocalDescription(sdp);
+    const offerData = {
+      sdp,
+      sender,
+      receiver,
+      live_room_id: roomId.value,
+    };
     instance.send({
       msgType: WsMsgTypeEnum.offer,
-      data: { sdp, sender, receiver },
+      data: offerData,
     });
   }
 
   async function batchSendOffer(socketId: string) {
+    console.log('batchSendOffer', socketId);
     await nextTick(async () => {
       if (!offerSended.value.has(socketId) && socketId !== getSocketId()) {
         hooksRtcMap.value.add(await startNewWebRtc({ receiver: socketId }));
@@ -289,24 +312,25 @@ export function usePull({
     sidebarList.value.push({ socketId: getSocketId() });
     nextTick(() => {
       liveUserList.value.forEach(async (item) => {
-        if (item.socketId === getSocketId()) {
+        const socketId = item.id;
+        if (socketId === getSocketId()) {
           localVideoRef.value[getSocketId()].srcObject = localStream.value;
         }
-        if (!offerSended.value.has(item.socketId)) {
+        if (!offerSended.value.has(socketId)) {
           hooksRtcMap.value.add(
             await startNewWebRtc({
-              receiver: item.socketId,
-              videoEl: localVideoRef.value[item.socketId],
+              receiver: socketId,
+              videoEl: localVideoRef.value[socketId],
               // videoEl: localVideoRef.value[sender.value],
             })
           );
-          await addTransceiver(item.socketId);
+          await addTransceiver(socketId);
           console.log('执行sendOffer', {
             sender: getSocketId(),
-            receiver: item.socketId,
+            receiver: socketId,
           });
-          sendOffer({ sender: getSocketId(), receiver: item.socketId });
-          offerSended.value.add(item.socketId);
+          sendOffer({ sender: getSocketId(), receiver: socketId });
+          offerSended.value.add(socketId);
         }
       });
     });
@@ -381,14 +405,19 @@ export function usePull({
     const instance = networkStore.wsMap.get(roomId.value);
     if (!instance) return;
     const danmu: IDanmu = {
-      socketId: getSocketId(),
+      socket_id: getSocketId(),
       userInfo: userStore.userInfo,
       msgType: DanmuMsgTypeEnum.danmu,
       msg: danmuStr.value,
     };
+    const messageData: IMessage['data'] = {
+      msg: danmuStr.value,
+      msgType: DanmuMsgTypeEnum.danmu,
+      live_room_id: Number(roomId.value),
+    };
     instance.send({
       msgType: WsMsgTypeEnum.message,
-      data: danmu,
+      data: messageData,
     });
     damuList.value.push(danmu);
     danmuStr.value = '';
@@ -415,6 +444,38 @@ export function usePull({
       instance.update();
     });
 
+    // 用户加入房间
+    instance.socketIo.on(
+      WsMsgTypeEnum.joined,
+      async (data: { data: ILive }) => {
+        prettierReceiveWebsocket(WsMsgTypeEnum.joined, data);
+        roomName.value = data.data.live_room?.name!;
+        userName.value = data.data.user?.username!;
+        userAvatar.value = data.data.user?.avatar!;
+        track.audio = data.data.track_audio!;
+        track.video = data.data.track_video!;
+        coverImg.value = data.data.live_room?.cover_img!;
+        flvurl.value = data.data.live_room?.flv_url!;
+        streamurl.value = data.data.live_room!.rtmp_url!.replace(
+          'rtmp',
+          'webrtc'
+        );
+        if (route.query.liveType === liveTypeEnum.srsWebrtcPull) {
+          console.log('');
+        } else if (
+          data.data.live_room?.type === LiveRoomTypeEnum.user_srs ||
+          data.data.live_room?.type === LiveRoomTypeEnum.user_obs ||
+          data.data.live_room?.type === LiveRoomTypeEnum.system
+        ) {
+          await startPlay({
+            flvurl: flvurl.value,
+            videoEl: remoteVideoRef.value!,
+          });
+        }
+        instance.send({ msgType: WsMsgTypeEnum.getLiveUser });
+      }
+    );
+
     // 收到offer
     instance.socketIo.on(WsMsgTypeEnum.offer, async (data: IOffer) => {
       prettierReceiveWebsocket(
@@ -425,7 +486,7 @@ export function usePull({
       if (isSRS) return;
       if (!instance) return;
       if (data.data.receiver === getSocketId()) {
-        if (!data.isAdmin) {
+        if (!data.is_anchor) {
           sidebarList.value.push({ socketId: data.data.sender });
         }
         await nextTick(async () => {
@@ -433,7 +494,7 @@ export function usePull({
           sender.value = data.data.sender;
           const rtc = await startNewWebRtc({
             receiver: data.data.sender,
-            videoEl: data.isAdmin
+            videoEl: data.is_anchor
               ? remoteVideoRef.value
               : localVideoRef.value[data.data.sender],
           });
@@ -441,13 +502,15 @@ export function usePull({
             await rtc.setRemoteDescription(data.data.sdp);
             const sdp = await rtc.createAnswer();
             await rtc.setLocalDescription(sdp);
+            const answerData: IAnswer = {
+              sdp,
+              sender: getSocketId(),
+              receiver: data.data.sender,
+              live_room_id: data.data.live_room_id,
+            };
             instance.send({
               msgType: WsMsgTypeEnum.answer,
-              data: {
-                sdp,
-                sender: getSocketId(),
-                receiver: data.data.sender,
-              },
+              data: answerData,
             });
           }
         });
@@ -465,7 +528,7 @@ export function usePull({
       );
       if (isSRS) return;
       if (!instance) return;
-      const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
+      const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socket_id}`);
       if (!rtc) return;
       rtc.rtcStatus.answer = true;
       rtc.update();
@@ -486,7 +549,7 @@ export function usePull({
       );
       if (isSRS) return;
       if (!instance) return;
-      const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
+      const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socket_id}`);
       if (!rtc) return;
       if (data.data.receiver === getSocketId()) {
         console.log('是发给我的candidate');
@@ -509,7 +572,7 @@ export function usePull({
     });
 
     // 管理员正在直播
-    instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
+    instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data) => {
       prettierReceiveWebsocket(WsMsgTypeEnum.roomLiveing, data);
       if (isSRS && !isFlv) {
         startNewWebRtc({});
@@ -517,70 +580,44 @@ export function usePull({
     });
 
     // 管理员不在直播
-    instance.socketIo.on(WsMsgTypeEnum.roomNoLive, (data: IAdminIn) => {
+    instance.socketIo.on(WsMsgTypeEnum.roomNoLive, (data) => {
       prettierReceiveWebsocket(WsMsgTypeEnum.roomNoLive, data);
       roomNoLive.value = true;
       closeRtc();
     });
 
     // 当前所有在线用户
-    instance.socketIo.on(WsMsgTypeEnum.liveUser, (data) => {
+    instance.socketIo.on(WsMsgTypeEnum.liveUser, (data: ILiveUser[]) => {
       prettierReceiveWebsocket(WsMsgTypeEnum.liveUser, data);
       if (!instance) return;
-      liveUserList.value = data.map((item) => ({
-        avatar: 'red',
-        socketId: item.id,
-      }));
+      liveUserList.value = data;
       // batchSendOffer();
     });
 
     // 收到用户发送消息
-    instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
+    instance.socketIo.on(WsMsgTypeEnum.message, (data: IMessage) => {
       prettierReceiveWebsocket(WsMsgTypeEnum.message, data);
       if (!instance) return;
       const danmu: IDanmu = {
         msgType: DanmuMsgTypeEnum.danmu,
-        socketId: data.socketId,
-        userInfo: data.data.userInfo,
+        socket_id: data.socket_id,
+        userInfo: data.user_info,
         msg: data.data.msg,
       };
       damuList.value.push(danmu);
     });
 
-    // 用户加入房间
-    instance.socketIo.on(
-      WsMsgTypeEnum.joined,
-      async (data: { data: ILive }) => {
-        prettierReceiveWebsocket(WsMsgTypeEnum.joined, data);
-        roomName.value = data.data.live_room?.name!;
-        userName.value = data.data.user?.username!;
-        userAvatar.value = data.data.user?.avatar!;
-        track.audio = data.data.track_audio!;
-        track.video = data.data.track_video!;
-        streamurl.value = data.data.streamurl!;
-        flvurl.value = data.data.flvurl!;
-        coverImg.value = data.data.coverImg!;
-        if (isFlv) {
-          await startPlay({
-            flvurl: flvurl.value,
-            videoEl: remoteVideoRef.value!,
-          });
-        }
-        instance.send({ msgType: WsMsgTypeEnum.getLiveUser });
-      }
-    );
-
     // 其他用户加入房间
-    instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
+    instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data: IOtherJoin) => {
       prettierReceiveWebsocket(WsMsgTypeEnum.otherJoin, data);
       const danmu: IDanmu = {
         msgType: DanmuMsgTypeEnum.otherJoin,
-        socketId: data.data.socketId,
-        userInfo: data.data.userInfo,
+        socket_id: data.data.join_socket_id,
+        userInfo: data.data.user,
         msg: '',
       };
       damuList.value.push(danmu);
-      batchSendOffer(data.data.socketId);
+      batchSendOffer(data.data.join_socket_id);
     });
 
     // 用户离开房间
@@ -598,12 +635,12 @@ export function usePull({
       prettierReceiveWebsocket(WsMsgTypeEnum.leaved, data);
       if (!instance) return;
       const res = liveUserList.value.filter(
-        (item) => item.socketId !== data.socketId
+        (item) => item.id !== data.socketId
       );
       liveUserList.value = res;
       const danmu: IDanmu = {
         msgType: DanmuMsgTypeEnum.userLeaved,
-        socketId: data.socketId,
+        socket_id: data.socketId,
         userInfo: data.data.userInfo,
         msg: '',
       };
@@ -623,6 +660,7 @@ export function usePull({
     startGetDisplayMedia,
     addTrack,
     addVideo,
+    videoLoading,
     balance,
     roomName,
     userName,

+ 95 - 58
src/hooks/use-push.ts

@@ -17,12 +17,16 @@ import {
 } from '@/api/userLiveRoom';
 import {
   DanmuMsgTypeEnum,
-  IAdminIn,
+  IAnswer,
   ICandidate,
   IDanmu,
+  IHeartbeat,
   IJoin,
   ILiveUser,
+  IMessage,
   IOffer,
+  IOtherJoin,
+  LiveRoomTypeEnum,
   MediaTypeEnum,
 } from '@/interface';
 import { SRSWebRTCClass } from '@/network/srsWebRtc';
@@ -63,8 +67,8 @@ export function usePush({
   const offerSended = ref(new Set());
 
   const track = reactive({
-    audio: true,
-    video: true,
+    audio: 1,
+    video: 1,
   });
   const streamurl = ref('');
   const flvurl = ref('');
@@ -101,6 +105,12 @@ export function usePush({
         if (!res) {
           await useTip('你还没有直播间,是否立即开通?');
           await handleCreateUserLiveRoom();
+        } else {
+          const rtmpUrl = newVal.live_rooms![0]!.rtmp_url!.replace(
+            'rtmp',
+            'webrtc'
+          );
+          streamurl.value = rtmpUrl;
         }
       }
     }
@@ -108,9 +118,6 @@ export function usePush({
 
   onMounted(() => {
     roomId.value = route.query.roomId as string;
-    streamurl.value = `webrtc://${
-      process.env.NODE_ENV === 'development' ? 'localhost' : 'live.hsslive.cn'
-    }/livestream/roomId___${roomId.value}`;
     flvurl.value = `${
       process.env.NODE_ENV === 'development'
         ? 'http://localhost:5001'
@@ -179,7 +186,7 @@ export function usePush({
         process.env.NODE_ENV === 'development'
           ? 'ws://localhost:4300'
           : 'wss://live.hsslive.cn',
-      isAdmin: true,
+      isAnchor: true,
     });
     instance.update();
     initReceive();
@@ -194,7 +201,7 @@ export function usePush({
     videoEl?: HTMLVideoElement;
   }) {
     if (isSRS) {
-      console.warn('开始new SRSWebRTCClass');
+      console.warn('开始new SRSWebRTCClass', `${roomId.value}___${receiver!}`);
       const rtc = new SRSWebRTCClass({
         roomId: `${roomId.value}___${getSocketId()}`,
         videoEl,
@@ -218,7 +225,10 @@ export function usePush({
           }/rtc/v1/publish/`,
           clientip: null,
           sdp: offer.sdp!,
-          streamurl: streamurl.value,
+          streamurl: userStore.userInfo!.live_rooms![0]!.rtmp_url!.replace(
+            'rtmp',
+            'webrtc'
+          ),
           tid: getRandomString(10),
         });
         await rtc.setRemoteDescription(
@@ -228,7 +238,7 @@ export function usePush({
         console.log(error);
       }
     } else {
-      console.warn('开始new WebRTCClass');
+      console.warn('开始new WebRTCClass', `${roomId.value}___${receiver!}`);
       const rtc = new WebRTCClass({
         roomId: `${roomId.value}___${receiver!}`,
         videoEl,
@@ -257,9 +267,13 @@ export function usePush({
     heartbeatTimer.value = setInterval(() => {
       const instance = networkStore.wsMap.get(roomId.value);
       if (!instance) return;
+      const heartbeatData: IHeartbeat['data'] = {
+        live_id: liveId,
+        live_room_id: Number(roomId.value),
+      };
       instance.send({
         msgType: WsMsgTypeEnum.heartbeat,
-        data: { liveId },
+        data: heartbeatData,
       });
     }, 1000 * 5);
   }
@@ -267,11 +281,9 @@ export function usePush({
   function addTrack() {
     if (!localStream.value) return;
     liveUserList.value.forEach((item) => {
-      if (item.socketId !== getSocketId()) {
+      if (item.id !== getSocketId()) {
         localStream.value.getTracks().forEach((track) => {
-          const rtc = networkStore.getRtcMap(
-            `${roomId.value}___${item.socketId}`
-          );
+          const rtc = networkStore.getRtcMap(`${roomId.value}___${item.id}`);
           rtc?.addTransceiver(track, localStream.value);
         });
       }
@@ -281,20 +293,18 @@ export function usePush({
   function sendJoin() {
     const instance = networkStore.wsMap.get(roomId.value);
     if (!instance) return;
+    const joinData: IJoin['data'] = {
+      live_room: {
+        id: Number(roomId.value),
+        name: roomName.value,
+        cover_img: handleCoverImg(),
+        type: isSRS ? LiveRoomTypeEnum.user_srs : LiveRoomTypeEnum.user_wertc,
+      },
+      track,
+    };
     instance.send({
       msgType: WsMsgTypeEnum.join,
-      data: {
-        roomName: roomName.value,
-        coverImg: handleCoverImg(),
-        srs: isSRS
-          ? {
-              streamurl: streamurl.value,
-              flvurl: flvurl.value,
-            }
-          : undefined,
-        track,
-        userInfo: userStore.userInfo,
-      },
+      data: joinData,
     });
   }
 
@@ -314,23 +324,35 @@ export function usePush({
     await rtc.setLocalDescription(sdp);
     instance.send({
       msgType: WsMsgTypeEnum.offer,
-      data: { sdp, sender, receiver, isAdmin: true },
+      data: {
+        sdp,
+        sender,
+        receiver,
+        live_room_id: roomId.value,
+      },
     });
   }
 
   function batchSendOffer() {
+    console.log('batchSendOffer');
     liveUserList.value.forEach(async (item) => {
-      if (
-        !offerSended.value.has(item.socketId) &&
-        item.socketId !== getSocketId()
-      ) {
+      console.log(item, 'liveUserList');
+      const socketId = item.id;
+      if (!offerSended.value.has(socketId) && socketId !== getSocketId()) {
+        const rtc = networkStore.getRtcMap(`${roomId.value}___${socketId}`);
+        if (!rtc) {
+          await startNewWebRtc({
+            receiver: socketId,
+            videoEl: localVideoRef.value,
+          });
+        }
         await addTrack();
         console.log('执行sendOffer', {
           sender: getSocketId(),
-          receiver: item.socketId,
+          receiver: socketId,
         });
-        sendOffer({ sender: getSocketId(), receiver: item.socketId });
-        offerSended.value.add(item.socketId);
+        sendOffer({ sender: getSocketId(), receiver: socketId });
+        offerSended.value.add(socketId);
       }
     });
   }
@@ -379,9 +401,15 @@ export function usePush({
             await rtc.setRemoteDescription(data.data.sdp);
             const sdp = await rtc.createAnswer();
             await rtc.setLocalDescription(sdp);
+            const answerData: IAnswer = {
+              sdp,
+              sender: getSocketId(),
+              receiver: data.data.sender,
+              live_room_id: data.data.live_room_id,
+            };
             instance.send({
               msgType: WsMsgTypeEnum.answer,
-              data: { sdp, sender: getSocketId(), receiver: data.data.sender },
+              data: answerData,
             });
           }
         });
@@ -400,7 +428,7 @@ export function usePush({
       if (isSRS) return;
       if (isDone.value) return;
       if (!instance) return;
-      const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
+      const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socket_id}`);
       if (!rtc) return;
       rtc.rtcStatus.answer = true;
       rtc.update();
@@ -422,9 +450,9 @@ export function usePush({
       if (isSRS) return;
       if (isDone.value) return;
       if (!instance) return;
-      const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
+      const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socket_id}`);
       if (!rtc) return;
-      if (data.socketId !== getSocketId()) {
+      if (data.socket_id !== getSocketId()) {
         console.log('不是我发的candidate');
         const candidate = new RTCIceCandidate({
           sdpMid: data.data.sdpMid,
@@ -445,7 +473,7 @@ export function usePush({
     });
 
     // 管理员正在直播
-    instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
+    instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data) => {
       prettierReceiveWebsocket(WsMsgTypeEnum.roomLiveing, data);
     });
 
@@ -455,23 +483,25 @@ export function usePush({
     });
 
     // 收到用户发送消息
-    instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
+    instance.socketIo.on(WsMsgTypeEnum.message, (data: IMessage) => {
       prettierReceiveWebsocket(WsMsgTypeEnum.message, data);
       if (!instance) return;
       damuList.value.push({
-        socketId: data.socketId,
+        socket_id: data.socket_id,
         msgType: DanmuMsgTypeEnum.danmu,
         msg: data.data.msg,
+        userInfo: data.user_info,
       });
     });
 
     // 用户加入房间完成
     instance.socketIo.on(WsMsgTypeEnum.joined, (data: IJoin) => {
       prettierReceiveWebsocket(WsMsgTypeEnum.joined, data);
-      handleHeartbeat(data.data.liveId!);
+      handleHeartbeat(data.data.live_id || -1);
       joined.value = true;
       liveUserList.value.push({
-        socketId: `${getSocketId()}`,
+        id: `${getSocketId()}`,
+        userInfo: data.user_info,
       });
       if (isSRS) {
         startNewWebRtc({});
@@ -481,15 +511,16 @@ export function usePush({
     });
 
     // 其他用户加入房间
-    instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
+    instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data: IOtherJoin) => {
       prettierReceiveWebsocket(WsMsgTypeEnum.otherJoin, data);
       liveUserList.value.push({
-        socketId: data.data.socketId,
+        id: data.data.join_socket_id,
+        userInfo: data.data.user,
       });
       damuList.value.push({
-        socketId: data.data.socketId,
-        userInfo: data.data.userInfo,
         msgType: DanmuMsgTypeEnum.otherJoin,
+        socket_id: data.data.join_socket_id,
+        userInfo: data.data.user,
         msg: '',
       });
       if (isSRS) return;
@@ -512,11 +543,11 @@ export function usePush({
     instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
       prettierReceiveWebsocket(WsMsgTypeEnum.leaved, data);
       const res = liveUserList.value.filter(
-        (item) => item.socketId !== data.socketId
+        (item) => item.id !== data.socketId
       );
       liveUserList.value = res;
       damuList.value.push({
-        socketId: data.socketId,
+        socket_id: data.socketId,
         msgType: DanmuMsgTypeEnum.userLeaved,
         msg: '',
       });
@@ -528,8 +559,8 @@ export function usePush({
       window.$message.warning('请输入房间名!');
       return false;
     }
-    if (roomName.value.length < 3 || roomName.value.length > 10) {
-      window.$message.warning('房间名要求3-10个字符!');
+    if (roomName.value.length < 3 || roomName.value.length > 30) {
+      window.$message.warning('房间名要求3-30个字符!');
       return false;
     }
     return true;
@@ -545,8 +576,8 @@ export function usePush({
       });
       const audio = event.getAudioTracks();
       const video = event.getVideoTracks();
-      track.audio = !!audio.length;
-      track.video = !!video.length;
+      track.audio = audio.length ? 1 : 2;
+      track.video = video.length ? 1 : 2;
       console.log('getUserMedia成功', event);
       currMediaType.value = allMediaTypeList[MediaTypeEnum.camera];
       currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.camera]);
@@ -566,8 +597,8 @@ export function usePush({
       });
       const audio = event.getAudioTracks();
       const video = event.getVideoTracks();
-      track.audio = !!audio.length;
-      track.video = !!video.length;
+      track.audio = audio.length ? 1 : 2;
+      track.video = video.length ? 1 : 2;
       console.log('getDisplayMedia成功', event);
       currMediaType.value = allMediaTypeList[MediaTypeEnum.screen];
       currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.screen]);
@@ -600,14 +631,20 @@ export function usePush({
       window.$message.error('还没开播,不能发送弹幕!');
       return;
     }
+    const messageData: IMessage['data'] = {
+      msg: danmuStr.value,
+      msgType: DanmuMsgTypeEnum.danmu,
+      live_room_id: Number(roomId.value),
+    };
     instance.send({
       msgType: WsMsgTypeEnum.message,
-      data: { msg: danmuStr.value },
+      data: messageData,
     });
     damuList.value.push({
-      socketId: getSocketId(),
+      socket_id: getSocketId(),
       msgType: DanmuMsgTypeEnum.danmu,
       msg: danmuStr.value,
+      userInfo: userStore.userInfo,
     });
     danmuStr.value = '';
   }

+ 82 - 35
src/interface.ts

@@ -41,6 +41,7 @@ export enum RankTypeEnum {
   user = 'user',
   sponsors = 'sponsors',
   wallet = 'wallet',
+  blog = 'blog',
 }
 
 export interface IWallet {
@@ -136,12 +137,26 @@ export interface IGoods {
   deleted_at?: string;
 }
 
+/** 直播间类型 */
+export enum LiveRoomTypeEnum {
+  system, // 系统直播
+  user_wertc, // 主播使用webrtc直播(用户只能看webrtc直播)
+  user_srs, // 主播使用srs直播(用户可以看webrtc或flv直播)
+  user_obs, // 主播使用obs/ffmpeg直播(用户只能看flv直播)
+}
+
 export interface ILiveRoom {
   id?: number;
   /** 用户信息 */
   user?: IUser;
   /** 直播间名字 */
   name?: string;
+  user_live_room?: IUserLiveRoom & { user: IUser };
+  /** 权重 */
+  weight?: number;
+  key?: string;
+  type?: LiveRoomTypeEnum;
+  cover_img?: string;
   rtmp_url?: string;
   flv_url?: string;
   hls_url?: string;
@@ -255,20 +270,17 @@ export interface IQqUser {
 
 export interface ILive {
   id?: number;
-  /** 1:系统直播;2:用户直播 */
-  system?: number;
   /** 用户信息 */
   user?: IUser;
   /** 直播间信息 */
   live_room?: ILiveRoom;
-  socketId?: string;
+  socket_id?: string;
   user_id?: number;
   live_room_id?: number;
-  track_video?: boolean;
-  track_audio?: boolean;
-  coverImg?: string;
-  streamurl?: string;
-  flvurl?: string;
+  /** 1开启;2关闭 */
+  track_video?: number;
+  /** 1开启;2关闭 */
+  track_audio?: number;
   created_at?: string;
   updated_at?: string;
   deleted_at?: string;
@@ -285,41 +297,90 @@ export enum DanmuMsgTypeEnum {
   userLeaved,
 }
 
+export interface IUpdateJoinInfo {
+  socket_id: string;
+  is_anchor: boolean;
+  user_info?: IUser;
+  data: {
+    live_room_id: number;
+  };
+}
+
 export interface ILiveUser {
-  socketId: string;
+  id: string;
+  rooms?: string[];
   userInfo?: IUser;
 }
 
 export interface IDanmu {
   msgType: DanmuMsgTypeEnum;
   msg: string;
-  socketId: string;
+  socket_id: string;
   userInfo?: IUser;
 }
 
-export interface IAdminIn {
-  roomId: string;
-  socketId: string;
-  isAdmin: boolean;
-  data: any;
+export interface IMessage {
+  socket_id: string;
+  is_anchor: boolean;
+  user_info?: IUser;
+  data: {
+    msgType: DanmuMsgTypeEnum;
+    msg: string;
+    live_room_id: number;
+  };
+}
+
+export type IOtherJoin = {
+  data: IUserLiveRoom & {
+    join_socket_id: string;
+  };
+};
+
+export interface IJoin {
+  socket_id: string;
+  is_anchor: boolean;
+  user_info?: IUser;
+  data: {
+    live_id?: number;
+    live_room: ILiveRoom;
+    track: { audio: number; video: number };
+  };
 }
 
 export interface IOffer {
-  socketId: string;
-  roomId: string;
+  socket_id: string;
+  is_anchor: boolean;
+  user_info?: IUser;
   data: {
     sdp: any;
-    target: string;
     sender: string;
     receiver: string;
+    live_room_id: number;
+  };
+}
+
+export interface IAnswer {
+  sdp: any;
+  sender: string;
+  receiver: string;
+  live_room_id: number;
+}
+export interface IHeartbeat {
+  socket_id: string;
+  is_anchor: boolean;
+  user_info?: IUser;
+  data?: {
+    live_id: number;
+    live_room_id: number;
   };
-  isAdmin: boolean;
 }
 
 export interface ICandidate {
-  socketId: string;
-  roomId: string;
+  socket_id: string;
+  is_anchor: boolean;
+  user_info?: IUser;
   data: {
+    live_room_id: number;
     candidate: string;
     sdpMid: string | null;
     sdpMLineIndex: number | null;
@@ -327,17 +388,3 @@ export interface ICandidate {
     sender: string;
   };
 }
-
-export interface IJoin {
-  roomId: string;
-  socketId: string;
-  data: {
-    roomName: string;
-    coverImg: string;
-    userInfo?: IUser;
-    srs?: { streamurl: string; flvurl: string };
-    track: { audio: boolean; video: boolean };
-    liveId?: number;
-  };
-  isAdmin?: boolean;
-}

+ 17 - 5
src/layout/head/index.vue

@@ -217,14 +217,19 @@
           <div
             class="btn"
             :style="{ backgroundImage: `url(${userStore.userInfo.avatar})` }"
-            @click="useQQLogin()"
           ></div>
         </template>
         <template #list>
           <div class="list">
             <a
               class="item"
-              @click.prevent="userStore.logout()"
+              @click.prevent="router.push({ name: routerName.account })"
+            >
+              <div class="txt">个人信息</div>
+            </a>
+            <a
+              class="item"
+              @click.prevent="handleLogout"
             >
               <div class="txt">退出</div>
             </a>
@@ -236,7 +241,7 @@
 </template>
 
 <script lang="ts" setup>
-import { openToTarget } from 'billd-utils';
+import { openToTarget, windowReload } from 'billd-utils';
 import { onMounted, ref } from 'vue';
 import { useRouter } from 'vue-router';
 
@@ -307,6 +312,13 @@ const plugins = ref([
   },
 ]);
 
+function handleLogout() {
+  userStore.logout();
+  setTimeout(() => {
+    windowReload();
+  }, 500);
+}
+
 function handleJump(item) {
   if (item.url) {
     openToTarget(item.url);
@@ -541,10 +553,10 @@ function handleStartLive(key) {
         @extend %containBg;
       }
       .list {
-        width: 70px;
+        width: 90px;
         .item {
           display: flex;
-          justify-content: center;
+          // justify-content: flex-end;
           padding: 0 15px;
           cursor: pointer;
           &:hover {

+ 32 - 10
src/layout/modal/index.vue

@@ -1,24 +1,46 @@
 <template>
   <n-modal
     v-model:show="showModal"
-    title="提示"
+    title="公告"
     preset="dialog"
     positive-text="OK"
   >
-    目前billd-live项目的
-    <a
-      href="https://github.com/galaxy-s10/billd-live"
-      target="_blank"
-    >
-      github仓库
-    </a>
-    还是公开状态,可能将在不久后设置为私有,但是会对老用户(前20名用户,可以在排行榜看当前用户数量)开放,抓紧时间登录一下吧~
+    <p>
+      billd-live开始支持ffmpeg、obs推流,欢迎体验!
+      <b
+        class="link"
+        @click="openToTarget('https://www.hsslive.cn/article/150')"
+      >
+        查看教程
+      </b>
+    </p>
+    <p>
+      billd-live付费课开始预定!299元/人,定金50,付定金后减50,实付249!<b
+        class="link"
+        @click="openToTarget('https://www.hsslive.cn/article/151')"
+      >
+        查看详情
+      </b>
+    </p>
   </n-modal>
 </template>
 
 <script lang="ts" setup>
+import { openToTarget } from 'billd-utils';
 import { ref } from 'vue';
 
+import router, { routerName } from '@/router';
 // const showModal = ref(process.env.NODE_ENV === 'production');
-const showModal = ref(false);
+// const showModal = ref(false);
+// const showModal = ref(true);
+const showModal = ref(router.currentRoute.value.name === routerName.home);
 </script>
+
+<style lang="scss" scoped>
+.link {
+  color: $theme-color-gold;
+  text-decoration: none;
+  font-weight: 500;
+  cursor: pointer;
+}
+</style>

+ 4 - 2
src/network/webRtc.ts

@@ -1,5 +1,6 @@
 import browserTool from 'browser-tool';
 
+import { ICandidate } from '@/interface';
 import { useNetworkStore } from '@/store/network';
 
 import { WsMsgTypeEnum } from './webSocket';
@@ -492,12 +493,13 @@ export class WebRTCClass {
         console.log('准备发送candidate', event.candidate.candidate);
         const roomId = this.roomId.split('___')[0];
         const receiver = this.roomId.split('___')[1];
-        const data = {
+        const data: ICandidate['data'] = {
           candidate: event.candidate.candidate,
           sdpMid: event.candidate.sdpMid,
           sdpMLineIndex: event.candidate.sdpMLineIndex,
-          sender: networkStore.wsMap.get(roomId)?.socketIo?.id,
+          sender: networkStore.wsMap.get(roomId)?.socketIo?.id || '',
           receiver,
+          live_room_id: Number(roomId),
         };
         networkStore.wsMap
           .get(roomId)

+ 9 - 10
src/network/webSocket.ts

@@ -63,15 +63,15 @@ export class WebSocketClass {
   status: WsConnectStatusEnum = WsConnectStatusEnum.disconnect;
   url = '';
   roomId = '-1';
-  isAdmin = false;
+  isAnchor = false;
 
-  constructor(data: { roomId: string; url: string; isAdmin: boolean }) {
+  constructor(data: { roomId: string; url: string; isAnchor: boolean }) {
     if (!window.WebSocket) {
       alert('当前环境不支持WebSocket!');
       return;
     }
     this.roomId = data.roomId;
-    this.isAdmin = data.isAdmin;
+    this.isAnchor = data.isAnchor;
     this.url = data.url;
     this.socketIo = io(data.url, {
       transports: ['websocket'],
@@ -92,14 +92,13 @@ export class WebSocketClass {
     }
     console.warn('【websocket】发送消息', msgType, data);
     const userStore = useUserStore();
-    this.socketIo?.emit(msgType, {
-      roomId: this.roomId,
-      socketId: this.socketIo.id,
-      isAdmin: this.isAdmin,
-      user_id: userStore.userInfo?.id,
-      user_token: userStore.token,
+    const sendData = {
+      socket_id: this.socketIo.id,
+      is_anchor: this.isAnchor,
+      user_info: userStore.userInfo,
       data,
-    });
+    };
+    this.socketIo?.emit(msgType, sendData);
   };
 
   // 更新store

+ 6 - 0
src/router/index.ts

@@ -7,6 +7,7 @@ import type { RouteRecordRaw } from 'vue-router';
 export const routerName = {
   home: 'home',
   about: 'about',
+  account: 'account',
   rank: 'rank',
   sponsors: 'sponsors',
   support: 'support',
@@ -78,6 +79,11 @@ export const defaultRoutes: RouteRecordRaw[] = [
         path: '/profile/:userId',
         component: () => import('@/views/profile/index.vue'),
       },
+      {
+        name: routerName.account,
+        path: '/account',
+        component: () => import('@/views/account/index.vue'),
+      },
       {
         name: routerName.sponsors,
         path: '/sponsors',

+ 130 - 0
src/views/account/index.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="profile-wrap">
+    <div class="uid">用户id:{{ userInfo?.id }}</div>
+    <div class="avatar">
+      <span class="txt">用户头像:</span>
+      <Avatar
+        :avatar="userInfo?.avatar"
+        :size="30"
+      ></Avatar>
+    </div>
+    <div class="username">用户昵称:{{ userInfo?.username }}</div>
+    <div class="pull-url">
+      <span>直播间:</span>
+      <span
+        v-if="!userInfo?.live_rooms?.length"
+        class="link"
+        @click="openLiveRoom"
+      >
+        未开通
+      </span>
+      <div v-else>
+        <div>直播间名字:{{ userInfo?.live_rooms?.[0].name }}</div>
+        <div>
+          直播间地址1(webrtc开播时):https://live.hsslive.cn/pull/<span>
+            {{ userInfo?.live_rooms?.[0].id }}?liveType=webrtcPull
+          </span>
+        </div>
+        <div>
+          直播间地址2(srs-webrtc开播时):https://live.hsslive.cn/pull/<span>
+            {{ userInfo?.live_rooms?.[0].id }}?liveType=srsWebrtcPull
+          </span>
+        </div>
+        <div>
+          直播间地址3(srs-webrtc开播或obs推流时):https://live.hsslive.cn/pull/<span>
+            {{ userInfo?.live_rooms?.[0].id }}?liveType=srsFlvPull
+          </span>
+        </div>
+        <div
+          v-loading="keyLoading"
+          class="rtmp-url"
+        >
+          <span>
+            推流地址:{{ newRtmpUrl || userInfo?.live_rooms?.[0].rtmp_url }},
+          </span>
+          <span
+            class="link"
+            @click="handleUpdateKey"
+          >
+            点我更新
+          </span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { openToTarget } from 'billd-utils';
+import { onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { fetchUpdateLiveRoomKey } from '@/api/liveRoom';
+import { fetchUserInfo } from '@/api/user';
+import { loginTip } from '@/hooks/use-login';
+import { IUser, liveTypeEnum } from '@/interface';
+import { routerName } from '@/router';
+
+const newRtmpUrl = ref();
+const keyLoading = ref(false);
+const router = useRouter();
+
+const userInfo = ref<IUser>();
+
+async function handleUserInfo() {
+  const res = await fetchUserInfo();
+  if (res.code === 200) {
+    userInfo.value = res.data;
+  }
+}
+
+function openLiveRoom() {
+  if (!loginTip()) {
+    return;
+  }
+  const url = router.resolve({
+    name: routerName.push,
+    query: { liveType: liveTypeEnum.srsWebrtcPull },
+  });
+  openToTarget(url.href);
+}
+
+async function handleUpdateKey() {
+  try {
+    keyLoading.value = true;
+    const res = await fetchUpdateLiveRoomKey();
+    if (res.code === 200) {
+      newRtmpUrl.value = res.data.rtmp_url;
+    }
+  } catch (error) {
+    console.log(error);
+  } finally {
+    keyLoading.value = false;
+  }
+}
+
+onMounted(() => {
+  handleUserInfo();
+});
+</script>
+
+<style lang="scss" scoped>
+.profile-wrap {
+  padding: 10px;
+  .link {
+    color: $theme-color-gold;
+    text-decoration: none;
+    cursor: pointer;
+  }
+  .avatar {
+    display: flex;
+    align-items: center;
+    .txt {
+      margin-right: 10px;
+    }
+  }
+  .rtmp-url {
+    position: relative;
+  }
+}
+</style>

+ 37 - 13
src/views/home/index.vue

@@ -5,7 +5,9 @@
       <div class="left">
         <div
           class="cover"
-          :style="{ backgroundImage: `url(${currentLiveRoom?.coverImg})` }"
+          :style="{
+            backgroundImage: `url(${currentLiveRoom?.live_room?.cover_img})`,
+          }"
         ></div>
         <!-- x-webkit-airplay这个属性应该是使此视频支持ios的AirPlay功能 -->
         <!-- playsinline、 webkit-playsinline IOS微信浏览器支持小窗内播放 -->
@@ -13,7 +15,7 @@
         <!-- x5-video-player-fullscreen 全屏设置 -->
         <!-- x5-video-orientation 声明播放器支持的方向,可选值landscape横屏,portraint竖屏。默认值portraint。 -->
         <video
-          v-if="currentLiveRoom?.flvurl"
+          v-if="currentLiveRoom?.live_room?.flv_url"
           id="localVideo"
           ref="localVideoRef"
           autoplay
@@ -42,14 +44,18 @@
             }"
           >
             <div
-              v-if="currentLiveRoom.system === 2"
+              v-if="
+                currentLiveRoom.live_room?.type !== LiveRoomTypeEnum.user_obs
+              "
               class="btn webrtc"
               @click="joinRoom()"
             >
               进入直播(webrtc)
             </div>
             <div
-              v-if="currentLiveRoom?.flvurl"
+              v-if="
+                currentLiveRoom?.live_room?.type === LiveRoomTypeEnum.user_srs
+              "
               class="btn flv"
               @click="joinFlvRoom()"
             >
@@ -70,7 +76,7 @@
               item: 1,
               active: item.live_room_id === currentLiveRoom?.live_room_id,
             }"
-            :style="{ backgroundImage: `url(${item.coverImg})` }"
+            :style="{ backgroundImage: `url(${item.live_room?.cover_img})` }"
             @click="changeLiveRoom(item)"
           >
             <div
@@ -84,7 +90,7 @@
               v-if="item.live_room_id === currentLiveRoom?.live_room_id"
               class="triangle"
             ></div>
-            <div class="txt">{{ item.live_room?.roomName }}</div>
+            <div class="txt">{{ item.live_room?.name }}</div>
           </div>
         </div>
         <div
@@ -96,7 +102,7 @@
       </div>
     </div>
 
-    <div class="foot"></div>
+    <div class="foot">*部分内容来源网络,如有侵权,请联系我删除~</div>
   </div>
 </template>
 
@@ -107,7 +113,7 @@ import { useRouter } from 'vue-router';
 
 import { fetchLiveList } from '@/api/live';
 import { useFlvPlay } from '@/hooks/use-play';
-import { ILive, liveTypeEnum } from '@/interface';
+import { ILive, LiveRoomTypeEnum, liveTypeEnum } from '@/interface';
 import { routerName } from '@/router';
 import { useAppStore } from '@/store/app';
 
@@ -123,8 +129,15 @@ const { startPlay } = useFlvPlay();
 function changeLiveRoom(item: ILive) {
   currentLiveRoom.value = item;
   nextTick(async () => {
-    if (item.flvurl) {
-      await startPlay({ flvurl: item.flvurl, videoEl: localVideoRef.value! });
+    if (
+      item.live_room?.type === LiveRoomTypeEnum.user_srs ||
+      item.live_room?.type === LiveRoomTypeEnum.user_obs ||
+      item.live_room?.type === LiveRoomTypeEnum.system
+    ) {
+      await startPlay({
+        flvurl: item.live_room.flv_url!,
+        videoEl: localVideoRef.value!,
+      });
     }
   });
 }
@@ -140,9 +153,15 @@ async function getLiveRoomList() {
       if (res.data.total) {
         currentLiveRoom.value = res.data.rows[0];
         nextTick(async () => {
-          if (currentLiveRoom.value?.flvurl) {
+          if (
+            currentLiveRoom.value?.live_room?.type ===
+              LiveRoomTypeEnum.user_srs ||
+            currentLiveRoom.value?.live_room?.type ===
+              LiveRoomTypeEnum.user_obs ||
+            currentLiveRoom.value?.live_room?.type === LiveRoomTypeEnum.system
+          ) {
             await startPlay({
-              flvurl: currentLiveRoom.value.flvurl,
+              flvurl: currentLiveRoom.value.live_room.flv_url!,
               videoEl: localVideoRef.value!,
             });
           }
@@ -159,7 +178,7 @@ onMounted(() => {
 });
 
 function joinRoom() {
-  if (currentLiveRoom.value?.streamurl) {
+  if (currentLiveRoom.value?.live_room?.type === LiveRoomTypeEnum.user_srs) {
     router.push({
       name: routerName.pull,
       params: {
@@ -183,6 +202,8 @@ function joinRoom() {
 }
 
 function joinFlvRoom() {
+  // console.log(currentLiveRoom.value?.live_room_id);
+  // return;
   router.push({
     name: routerName.pull,
     params: { roomId: currentLiveRoom.value?.live_room_id },
@@ -349,6 +370,8 @@ function joinFlvRoom() {
             color: white;
             text-align: initial;
             font-size: 13px;
+
+            @extend %singleEllipsis;
           }
         }
       }
@@ -360,6 +383,7 @@ function joinFlvRoom() {
     }
   }
   .foot {
+    margin-top: 10px;
     text-align: center;
   }
 }

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

@@ -1,32 +1,45 @@
 <template>
-  <div class="profile-wrap">
-    <div class="uid">用户id:{{ userInfo?.id }}</div>
+  <div
+    v-loading="loading"
+    class="profile-wrap"
+  >
+    <div class="uid">用户id:{{ userInfo?.id }}</div>
     <div class="avatar">
-      <span class="txt">用户头像:</span>
+      <span class="txt">用户头像</span>
       <Avatar
         :avatar="userInfo?.avatar"
         :size="30"
       ></Avatar>
     </div>
-    <div class="username">用户昵称:{{ userInfo?.username }}</div>
-    <div class="pull-url">
-      推流地址:{{ userInfo?.live_rooms?.[0].rtmp_url }}
+    <div class="username">用户昵称{{ userInfo?.username }}</div>
+    <div class="live-room">
+      直播间:{{ userInfo?.live_rooms?.[0]?.name || '未开通' }}
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
+import { useRoute } from 'vue-router';
 
-import { fetchUserInfo } from '@/api/user';
+import { fetchFindUser } from '@/api/user';
 import { IUser } from '@/interface';
 
+const loading = ref(false);
+const route = useRoute();
 const userInfo = ref<IUser>();
 
 async function handleUserInfo() {
-  const res = await fetchUserInfo();
-  if (res.code === 200) {
-    userInfo.value = res.data;
+  try {
+    loading.value = true;
+    const res = await fetchFindUser(Number(route.params.userId as string));
+    if (res.code === 200) {
+      userInfo.value = res.data;
+    }
+  } catch (error) {
+    console.log(error);
+  } finally {
+    loading.value = false;
   }
 }
 
@@ -37,6 +50,7 @@ onMounted(() => {
 
 <style lang="scss" scoped>
 .profile-wrap {
+  position: relative;
   padding: 10px;
   .avatar {
     display: flex;

+ 18 - 9
src/views/pull/index.vue

@@ -15,7 +15,7 @@
             <div class="detail">
               <div class="top">{{ userName || '-' }}</div>
               <div class="bottom">
-                <span>{{ roomName }}</span>
+                <span>{{ roomName }},{{ getSocketId() }}</span>
               </div>
             </div>
           </div>
@@ -24,7 +24,10 @@
           ref="containerRef"
           class="container"
         >
-          <div class="video-wrap">
+          <div
+            v-loading="videoLoading"
+            class="video-wrap"
+          >
             <div
               class="cover"
               :style="{ backgroundImage: `url(${coverImg})` }"
@@ -128,15 +131,20 @@
         </div>
         <div class="user-list">
           <div
-            v-for="(item, index) in liveUserList.filter((item) =>
-              userStore.userInfo ? item.socketId !== getSocketId() : true
+            v-for="(item, index) in liveUserList.filter(
+              (item) => item.id !== getSocketId()
             )"
             :key="index"
             class="item"
           >
             <div class="info">
-              <div class="avatar"></div>
-              <div class="username">{{ item.socketId }}</div>
+              <div
+                class="avatar"
+                :style="{ backgroundImage: `url(${item.userInfo?.avatar})` }"
+              ></div>
+              <div class="username">
+                {{ item.userInfo?.username || item.id }}
+              </div>
             </div>
           </div>
           <div
@@ -161,20 +169,20 @@
           >
             <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
               <span class="name">
-                {{ item.userInfo?.username || item.socketId }}:
+                {{ 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">
-                {{ item.userInfo?.username || item.socketId }}进入直播!
+                {{ item.userInfo?.username || item.socket_id }}进入直播!
               </span>
             </template>
             <template v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved">
               <span class="name system">系统通知:</span>
               <span class="msg">
-                {{ item.userInfo?.username || item.socketId }}离开直播!
+                {{ item.userInfo?.username || item.socket_id }}离开直播!
               </span>
             </template>
           </div>
@@ -245,6 +253,7 @@ const {
   startGetDisplayMedia,
   addTrack,
   addVideo,
+  videoLoading,
   balance,
   roomName,
   userName,

+ 14 - 6
src/views/push/index.vue

@@ -52,13 +52,13 @@
           <div class="title">在线人员</div>
           <div
             v-for="(item, index) in liveUserList.filter(
-              (item) => item.socketId !== getSocketId()
+              (item) => item.id !== getSocketId()
             )"
             :key="index"
             class="item"
           >
             <video
-              :ref="(el) => (remoteVideoRef[item.socketId] = el)"
+              :ref="(el) => (remoteVideoRef[item.id] = el)"
               autoplay
               webkit-playsinline="true"
               playsinline
@@ -68,7 +68,7 @@
               x5-video-orientation="portraint"
               muted
             ></video>
-            <div>{{ item.socketId }}</div>
+            <div>{{ item.userInfo?.username || item.id }}</div>
           </div>
         </div>
       </div>
@@ -159,18 +159,26 @@
               class="item"
             >
               <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
-                <span class="name">{{ item.socketId }}:</span>
+                <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">{{ item.socketId }}进入直播!</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">{{ item.socketId }}离开直播!</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>离开直播!</span>
+                </span>
               </template>
             </div>
           </div>

+ 69 - 34
src/views/rank/index.vue

@@ -24,10 +24,11 @@
           <div
             class="avatar"
             @click="
-              router.push({
-                name: routerName.profile,
-                params: { userId: item.user.id },
-              })
+              currRankType !== RankTypeEnum.blog &&
+                router.push({
+                  name: routerName.profile,
+                  params: { userId: item.user.id },
+                })
             "
           >
             <Avatar
@@ -79,7 +80,7 @@
             />
             <div class="username">{{ item.user.username }}</div>
             <div class="wallet">
-              <div v-if="item.balance && currRankType === RankTypeEnum.wallet">
+              <div v-if="currRankType === RankTypeEnum.wallet">
                 (钱包:{{ item.balance }})
               </div>
             </div>
@@ -112,7 +113,7 @@
 import { onMounted, ref } from 'vue';
 
 import { fetchLiveRoomList } from '@/api/liveRoom';
-import { fetchUserList } from '@/api/user';
+import { fetchBlogUserList, fetchUserList } from '@/api/user';
 import { fetchWalletList } from '@/api/wallet';
 import { fullLoading } from '@/components/FullLoading';
 import { ILive, IUser, liveTypeEnum, RankTypeEnum } from '@/interface';
@@ -136,6 +137,10 @@ const rankTypeList = ref<IRankType[]>([
     type: RankTypeEnum.wallet,
     label: '土豪榜',
   },
+  {
+    type: RankTypeEnum.blog,
+    label: '博客用户',
+  },
 ]);
 
 const mockDataNums = 4;
@@ -209,11 +214,7 @@ async function getWalletList() {
       const length = res.data.rows.length;
       rankList.value = res.data.rows.map((item, index) => {
         return {
-          user: {
-            id: item.id!,
-            username: item.username!,
-            avatar: item.avatar!,
-          },
+          user: item.user,
           rank: index + 1,
           level: 1,
           score: 1,
@@ -236,7 +237,7 @@ async function getLiveRoomList() {
     fullLoading({ loading: true });
     const res = await fetchLiveRoomList({
       orderName: 'updated_at',
-      orderBy: 'desc',
+      orderBy: 'asc',
     });
     if (res.code === 200) {
       const length = res.data.rows.length;
@@ -250,7 +251,6 @@ async function getLiveRoomList() {
           rank: index + 1,
           level: 1,
           score: 1,
-          live: item.live,
         };
       });
       if (length < mockDataNums) {
@@ -264,27 +264,6 @@ async function getLiveRoomList() {
   }
 }
 
-function changeCurrRankType(type: RankTypeEnum) {
-  currRankType.value = type;
-  switch (type) {
-    case RankTypeEnum.liveRoom:
-      getLiveRoomList();
-      break;
-    case RankTypeEnum.user:
-      getUserList();
-      break;
-    case RankTypeEnum.wallet:
-      getWalletList();
-      break;
-    default:
-      break;
-  }
-}
-
-onMounted(() => {
-  changeCurrRankType(currRankType.value);
-});
-
 async function getUserList() {
   try {
     fullLoading({ loading: true });
@@ -317,6 +296,62 @@ async function getUserList() {
     fullLoading({ loading: false });
   }
 }
+async function getBlogUserList() {
+  try {
+    fullLoading({ loading: true });
+    const res = await fetchBlogUserList({
+      orderName: 'updated_at',
+      orderBy: 'desc',
+    });
+    if (res.code === 200) {
+      const length = res.data.rows.length;
+      rankList.value = res.data.rows.map((item, index) => {
+        return {
+          user: {
+            id: item.id!,
+            username: item.username!,
+            avatar: item.avatar!,
+          },
+          rank: index + 1,
+          level: 1,
+          score: 1,
+          balance: '',
+        };
+      });
+      if (length < mockDataNums) {
+        rankList.value.push(...mockRank.slice(length));
+      }
+    }
+  } catch (error) {
+    console.log(error);
+  } finally {
+    fullLoading({ loading: false });
+  }
+}
+
+function changeCurrRankType(type: RankTypeEnum) {
+  currRankType.value = type;
+  switch (type) {
+    case RankTypeEnum.liveRoom:
+      getLiveRoomList();
+      break;
+    case RankTypeEnum.user:
+      getUserList();
+      break;
+    case RankTypeEnum.blog:
+      getBlogUserList();
+      break;
+    case RankTypeEnum.wallet:
+      getWalletList();
+      break;
+    default:
+      break;
+  }
+}
+
+onMounted(() => {
+  changeCurrRankType(currRankType.value);
+});
 </script>
 
 <style lang="scss" scoped>