Ver Fonte

fix: 优化

shuisheng há 1 ano atrás
pai
commit
3825ed13f8

+ 3 - 1
README.md

@@ -262,7 +262,9 @@ pnpm run dev
 
 ### 流媒体服务器环境
 
-> 配置:2 核 CPU,2G 内存,带宽 30M(香港)
+> ~~配置:2 核 CPU,2G 内存,带宽 30M(香港)~~,2G内存也能跑,但偶尔会占满内存导致服务器卡死。
+>
+> 配置:2 核 CPU,4G 内存,带宽 30M(香港)
 
 - 操作系统:Alibaba Cloud Linux release 3 (Soaring Falcon)
 - node 版本:v16.20.0

+ 1 - 0
package.json

@@ -40,6 +40,7 @@
     "@nut-tree/nut-js": "^4.0.0",
     "@nut-tree/shared": "^4.0.0",
     "@vicons/ionicons5": "^0.12.0",
+    "@vueuse/core": "^10.11.1",
     "@webav/av-recorder": "^0.3.3",
     "axios": "^1.2.1",
     "billd-deploy": "^1.0.25",

+ 51 - 0
pnpm-lock.yaml

@@ -23,6 +23,9 @@ importers:
       '@vicons/ionicons5':
         specifier: ^0.12.0
         version: 0.12.0
+      '@vueuse/core':
+        specifier: ^10.11.1
+        version: 10.11.1(vue@3.3.4)
       '@webav/av-recorder':
         specifier: ^0.3.3
         version: 0.3.3
@@ -1967,6 +1970,9 @@ packages:
   '@types/video.js@7.3.52':
     resolution: {integrity: sha512-WFj/HkNVCfkchXDeDU0QbimC356FB5vva3g5mgsjk8n3UMKqP9S522rQAmu9LGPiCmShZRPuAlkXmbp5WId6ow==}
 
+  '@types/web-bluetooth@0.0.20':
+    resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
+
   '@types/wicg-file-system-access@2020.9.8':
     resolution: {integrity: sha512-ggMz8nOygG7d/stpH40WVaNvBwuyYLnrg5Mbyf6bmsj/8+gb6Ei4ZZ9/4PNpcPNTT8th9Q8sM8wYmWGjMWLX/A==}
 
@@ -2110,6 +2116,15 @@ packages:
   '@vue/shared@3.3.4':
     resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==}
 
+  '@vueuse/core@10.11.1':
+    resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
+
+  '@vueuse/metadata@10.11.1':
+    resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
+
+  '@vueuse/shared@10.11.1':
+    resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
+
   '@webassemblyjs/ast@1.11.1':
     resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
 
@@ -7075,6 +7090,17 @@ packages:
       '@vue/composition-api':
         optional: true
 
+  vue-demi@0.14.10:
+    resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
+    engines: {node: '>=12'}
+    hasBin: true
+    peerDependencies:
+      '@vue/composition-api': ^1.0.0-rc.1
+      vue: ^3.0.0-0 || ^2.6.0
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+
   vue-eslint-parser@9.3.1:
     resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==}
     engines: {node: ^14.17.0 || >=16.0.0}
@@ -9241,6 +9267,8 @@ snapshots:
 
   '@types/video.js@7.3.52': {}
 
+  '@types/web-bluetooth@0.0.20': {}
+
   '@types/wicg-file-system-access@2020.9.8': {}
 
   '@types/ws@8.5.4':
@@ -9451,6 +9479,25 @@ snapshots:
 
   '@vue/shared@3.3.4': {}
 
+  '@vueuse/core@10.11.1(vue@3.3.4)':
+    dependencies:
+      '@types/web-bluetooth': 0.0.20
+      '@vueuse/metadata': 10.11.1
+      '@vueuse/shared': 10.11.1(vue@3.3.4)
+      vue-demi: 0.14.10(vue@3.3.4)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+
+  '@vueuse/metadata@10.11.1': {}
+
+  '@vueuse/shared@10.11.1(vue@3.3.4)':
+    dependencies:
+      vue-demi: 0.14.10(vue@3.3.4)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+
   '@webassemblyjs/ast@1.11.1':
     dependencies:
       '@webassemblyjs/helper-numbers': 1.11.1
@@ -14993,6 +15040,10 @@ snapshots:
     dependencies:
       vue: 3.3.4
 
+  vue-demi@0.14.10(vue@3.3.4):
+    dependencies:
+      vue: 3.3.4
+
   vue-eslint-parser@9.3.1(eslint@8.36.0):
     dependencies:
       debug: 4.3.4(supports-color@9.3.1)

+ 15 - 0
public/index.html

@@ -7,6 +7,12 @@
       content="IE=edge"
     />
     <meta name="viewport" />
+    <!-- B站接口获取图片资源出现403的解决方案 -->
+    <meta
+      name="referrer"
+      content="no-referrer"
+    />
+
     <link
       rel="icon"
       href="<%= BASE_URL %>favicon.ico"
@@ -21,6 +27,15 @@
         s.parentNode.insertBefore(hm, s);
       })();
     </script>
+    <!-- 谷歌广告 -->
+    <script
+      async
+      src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6064454674933772"
+      crossorigin="anonymous"
+    ></script>
+    <!-- <script>
+      (adsbygoogle = window.adsbygoogle || []).push({});
+    </script> -->
   </head>
 
   <body>

+ 37 - 0
src/api/bilibili.ts

@@ -0,0 +1,37 @@
+import request from '@/utils/request';
+
+export function fetchLiveBilibiliGetUserRecommend(params: {
+  page;
+  page_size;
+  platform;
+}) {
+  // return request.get(
+  //   'https://live-api.hsslive.cn/apilivebilibilicom/xlive/web-interface/v1/second/getUserRecommend',
+  //   {
+  //     params,
+  //   }
+  // );
+  return request.get('/bilibili/api_live_bilibili_com_get', {
+    params: {
+      url: 'xlive/web-interface/v1/second/getUserRecommend',
+      ...params,
+    },
+  });
+}
+
+export function fetchLiveBilibiliPlayUrl(params: { cid; platform }) {
+  return request.get('/bilibili/api_live_bilibili_com_get', {
+    params: {
+      url: 'room/v1/Room/playUrl',
+      ...params,
+    },
+  });
+}
+export function fetchLiveBilibiliRoomGetInfo(params: { room_id }) {
+  return request.get('/bilibili/api_live_bilibili_com_get', {
+    params: {
+      url: 'room/v1/Room/get_info',
+      ...params,
+    },
+  });
+}

+ 2 - 0
src/api/giftRecord.ts

@@ -27,10 +27,12 @@ export function fetchGiftRecordCreate(data: {
   goodsId: number;
   liveRoomId: number;
   goodsNums: number;
+  isBilibili: boolean;
 }) {
   return request.post('/gift_record/create', {
     live_room_id: data.liveRoomId,
     goods_id: data.goodsId,
     goods_nums: data.goodsNums,
+    is_bilibili: data.isBilibili,
   });
 }

BIN
src/assets/img/pay-course.webp


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

@@ -95,6 +95,7 @@ export const useWebsocket = () => {
   const roomLiving = ref(false);
   const isAnchor = ref(false);
   const isRemoteDesk = ref(false);
+  const isBilibili = ref(false);
   const anchorInfo = ref<IUser>();
   const anchorSocketId = ref('');
   const canvasVideoStream = ref<MediaStream>();
@@ -304,6 +305,7 @@ export const useWebsocket = () => {
       requestId: getRandomString(8),
       msgType: WsMsgTypeEnum.join,
       data: {
+        isBilibili: isBilibili.value,
         isRemoteDesk: isRemoteDesk.value,
         socket_id: mySocketId.value,
         live_room_id: Number(roomId.value),
@@ -955,6 +957,7 @@ export const useWebsocket = () => {
   function initWs(data: {
     isAnchor: boolean;
     roomId: string;
+    isBilibili?: boolean;
     isRemoteDesk?: boolean;
     currentResolutionRatio?: number;
     currentMaxFramerate?: number;
@@ -965,6 +968,9 @@ export const useWebsocket = () => {
     if (data.isRemoteDesk) {
       isRemoteDesk.value = data.isRemoteDesk;
     }
+    if (data.isBilibili) {
+      isBilibili.value = data.isBilibili;
+    }
     if (data.currentMaxBitrate) {
       currentMaxBitrate.value = data.currentMaxBitrate;
     }

+ 122 - 0
src/interface.ts

@@ -5,6 +5,128 @@ import {
 } from './types/ILiveRoom';
 import { IUser } from './types/IUser';
 
+export interface IBilibiliLiveUserRecommend {
+  roomid: number;
+  uid: number;
+  title: string;
+  uname: string;
+  online: number;
+  user_cover: string;
+  user_cover_flag: number;
+  system_cover: string;
+  cover: string;
+  show_cover: string;
+  link: string;
+  face: string;
+  parent_id: number;
+  parent_name: string;
+  area_id: number;
+  area_name: string;
+  area_v2_parent_id: number;
+  area_v2_parent_name: string;
+  area_v2_id: number;
+  area_v2_name: string;
+  session_id: string;
+  group_id: number;
+  show_callback: string;
+  click_callback: string;
+  web_pendent: string;
+  pk_id: number;
+  pendant_info: {
+    '1': {
+      pendent_id: number;
+      content: string;
+      color: string;
+      pic: string;
+      position: number;
+      type: string;
+      name: string;
+    };
+  };
+  verify: { role: number; desc: string; type: number };
+  head_box: { name: string; value: string; desc: string };
+  head_box_type: number;
+  is_auto_play: number;
+  flag: number;
+  watched_show: {
+    switch: boolean;
+    num: number;
+    text_small: string;
+    text_large: string;
+    icon: string;
+    icon_location: number;
+    icon_web: string;
+  };
+  is_nft: number;
+  nft_dmark: string;
+  play_together_goods?: any;
+  watermark: string;
+}
+export interface IBilibiliLiveRoomInfo {
+  uid: number;
+  room_id: number;
+  short_id: number;
+  attention: number;
+  online: number;
+  is_portrait: boolean;
+  description: string;
+  live_status: number;
+  area_id: number;
+  parent_area_id: number;
+  parent_area_name: string;
+  old_area_id: number;
+  background: string;
+  title: string;
+  user_cover: string;
+  keyframe: string;
+  is_strict_room: boolean;
+  live_time: string;
+  tags: string;
+  is_anchor: number;
+  room_silent_type: string;
+  room_silent_level: number;
+  room_silent_second: number;
+  area_name: string;
+  pendants: string;
+  area_pendants: string;
+  hot_words: string[];
+  hot_words_status: number;
+  verify: string;
+  new_pendants: {
+    frame: {
+      name: string;
+      value: string;
+      position: number;
+      desc: string;
+      area: number;
+      area_old: number;
+      bg_color: string;
+      bg_pic: string;
+      use_old_area: boolean;
+    };
+    badge?: any;
+    mobile_frame: {
+      name: string;
+      value: string;
+      position: number;
+      desc: string;
+      area: number;
+      area_old: number;
+      bg_color: string;
+      bg_pic: string;
+      use_old_area: boolean;
+    };
+    mobile_badge?: any;
+  };
+  up_session: string;
+  pk_status: number;
+  pk_id: number;
+  battle_id: number;
+  allow_change_area_time: number;
+  allow_upload_cover_time: number;
+  studio_info: { status: number; master_list: any[] };
+}
+
 export interface IFlvStatistics {
   url: string;
   hasRedirect: boolean;

+ 7 - 2
src/layout/pc/head/index.vue

@@ -465,8 +465,8 @@ const about = ref([
     url: '',
   },
   {
-    label: 'layout.sponsor',
-    routerName: routerName.sponsors,
+    label: 'layout.author',
+    routerName: routerName.author,
     url: '',
   },
   {
@@ -474,6 +474,11 @@ const about = ref([
     routerName: routerName.group,
     url: '',
   },
+  {
+    label: 'layout.sponsor',
+    routerName: routerName.sponsors,
+    url: '',
+  },
   {
     label: 'layout.release',
     routerName: routerName.release,

+ 1 - 1
src/layout/pc/index.vue

@@ -52,7 +52,7 @@ const appStore = useAppStore();
     position: fixed;
     right: 10px;
     bottom: 10px;
-    width: 200px;
+    width: 300px;
     border-radius: 10px;
     cursor: pointer;
   }

+ 3 - 0
src/layout/pc/sidebar/index.vue

@@ -24,6 +24,7 @@
     <div
       class="item"
       @click="handleJump"
+      v-if="userStore.userInfo"
     >
       <div class="ico wallet"></div>
       <div class="txt">{{ t('layout.myWallet') }}</div>
@@ -36,8 +37,10 @@ import { useI18n } from 'vue-i18n';
 
 import { loginTip } from '@/hooks/use-login';
 import router, { routerName } from '@/router';
+import { useUserStore } from '@/store/user';
 
 const { t } = useI18n();
+const userStore = useUserStore();
 
 function handleJump() {
   if (!loginTip()) {

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

@@ -39,6 +39,7 @@ export default nameSpaceWrap('layout', {
   team: 'Team',
   officialGroup: 'Official Group',
   release: 'Version Release',
+  author: 'Author',
 
   srsLive: 'SRS Live',
   forwardBilibili: 'forward Bilibili',

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

@@ -39,6 +39,7 @@ export default nameSpaceWrap('layout', {
   team: '团队',
   officialGroup: '官方群',
   release: '版本发布',
+  author: '作者',
 
   srsLive: 'SRS直播(推荐)',
   forwardBilibili: '转推b站(beta)',

+ 6 - 0
src/router/index.ts

@@ -41,6 +41,7 @@ export const routerName = {
   team: 'team',
   oauth: 'oauth',
   release: 'release',
+  author: 'author',
   pushStreamDifferent: 'pushStreamDifferent',
   notFound: 'notFound',
   group: 'group',
@@ -95,6 +96,11 @@ export const defaultRoutes: RouteRecordRaw[] = [
             path: 'release',
             component: () => import('@/views/about/release/index.vue'),
           },
+          {
+            name: routerName.author,
+            path: 'author',
+            component: () => import('@/views/about/author/index.vue'),
+          },
           {
             name: routerName.pushStreamDifferent,
             path: 'pushStreamDifferent',

+ 1 - 0
src/types/websocket.ts

@@ -234,6 +234,7 @@ export type WsJoinType = IWsFormat<{
   anchor_info?: IUser;
   user_info?: IUser;
   isRemoteDesk?: boolean;
+  isBilibili?: boolean;
   socket_list?: string[];
 }>;
 

+ 10 - 0
src/utils/index.ts

@@ -2,6 +2,16 @@
 import { computeBox, getRangeRandom } from 'billd-utils';
 import sparkMD5 from 'spark-md5';
 
+export const googleAd = () => {
+  const el = document.createElement('script');
+  el.src =
+    'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6064454674933772';
+  const head = document.getElementsByTagName('head')[0];
+  el.async = true;
+  el.crossOrigin = 'anonymous';
+  head.appendChild(el);
+};
+
 /**
  * music,该曲目应被视为包含音乐。设置该值时 MediaStreamTrack.kind的值必须为"audio"。
  * speech,该轨道应被视为包含语音数据。设置该值时 MediaStreamTrack.kind的值必须为"audio"。

+ 57 - 0
src/views/about/author/index.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="author-wrap">
+    <h1 class="title">作者</h1>
+    <p class="desc">主业前端开发,兴趣使然,故业余时间开发了billd-live。</p>
+    <div class="hr"></div>
+    <div class="info">
+      <div class="title">微信</div>
+      <img
+        src="@/assets/img/my-wechat.webp"
+        alt=""
+        class="my-wechat"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup></script>
+
+<style lang="scss" scoped>
+.author-wrap {
+  box-sizing: border-box;
+  margin: 0 auto;
+  padding-top: 50px;
+  width: 960px;
+  .link {
+    color: $theme-color-gold;
+    text-decoration: none;
+    cursor: pointer;
+  }
+
+  .title {
+    margin: 0;
+    font-weight: 500;
+    font-size: 40px;
+  }
+  .desc {
+    margin: 0;
+    color: #3c3c3cb3;
+    font-size: 16px;
+    line-height: 1.8;
+  }
+  .hr {
+    margin: 60px 0 20px 0;
+    width: 100%;
+    height: 1px;
+    background-color: #e7e7e7;
+  }
+  .info {
+    .title {
+      font-size: 20px;
+    }
+    .my-wechat {
+      height: 500px;
+    }
+  }
+}
+</style>

+ 1 - 1
src/views/about/group/index.vue

@@ -99,7 +99,7 @@
       </p>
       <p>2. 闲聊勿扰。</p>
     </div>
-    <h1 class="title">🚀 我的微信</h1>
+    <h1 class="title">🚀 作者微信</h1>
 
     <img
       src="@/assets/img/my-wechat.webp"

+ 162 - 62
src/views/home/index.vue

@@ -1,5 +1,8 @@
 <template>
-  <div class="home-wrap">
+  <div
+    class="home-wrap"
+    ref="doc"
+  >
     <div class="play-container">
       <div
         v-if="configBg && configBg !== ''"
@@ -84,7 +87,7 @@
             >
               <div
                 class="btn"
-                @click="joinRoom({ roomId: currentLiveRoom.live_room?.id! })"
+                @click="joinRoom(currentLiveRoom.live_room)"
               >
                 进入直播
               </div>
@@ -161,11 +164,7 @@
             v-for="(iten, indey) in otherLiveRoomList"
             :key="indey"
             class="live-room"
-            @click="
-              joinRoom({
-                roomId: iten.live_room?.id!,
-              })
-            "
+            @click="joinRoom(iten.live_room)"
           >
             <div
               class="cover"
@@ -205,20 +204,39 @@
       </div>
     </div>
 
+    <!-- <div
+      style="position: fixed; bottom: 200px; right: 0; background-color: red"
+    >
+      {{ arrivedState }}
+    </div> -->
+
+    <div class="ad-wrap">
+      <!-- live-首页广告位1 -->
+      <ins
+        class="adsbygoogle"
+        style="display: block"
+        data-ad-client="ca-pub-6064454674933772"
+        data-ad-slot="3325489849"
+        data-ad-format="auto"
+        data-full-width-responsive="true"
+      ></ins>
+    </div>
     <div class="foot">*{{ t('home.copyrightTip') }}~</div>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref } from 'vue';
+import { useScroll } from '@vueuse/core';
+import { onMounted, reactive, ref, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useRoute, useRouter } from 'vue-router';
 
+import { fetchLiveBilibiliGetUserRecommend } from '@/api/bilibili';
 import { fetchLiveList } from '@/api/live';
 import { fetchFindLiveConfigByKey } from '@/api/liveConfig';
 import { sliderList } from '@/constant';
 import { usePull } from '@/hooks/use-pull';
-import { ILive, LiveLineEnum } from '@/interface';
+import { IBilibiliLiveUserRecommend, ILive, LiveLineEnum } from '@/interface';
 import { routerName } from '@/router';
 import { useAppStore } from '@/store/app';
 import {
@@ -232,6 +250,7 @@ const route = useRoute();
 const router = useRouter();
 const appStore = useAppStore();
 const canvasRef = ref<Element>();
+const loading = ref(false);
 const showJoinBtn = ref(false);
 const topNums = ref(6);
 const configBg = ref('');
@@ -246,7 +265,9 @@ const interactionList = ref<any[]>([]);
 const videoWrapTmpRef = ref<HTMLDivElement>();
 const remoteVideoRef = ref<HTMLDivElement>();
 const docW = document.documentElement.clientWidth;
+const doc = ref<HTMLElement | null>(null);
 
+const pageParams = reactive({ page: 0, page_size: 30, platform: 'web' });
 const { t } = useI18n();
 const {
   videoWrapRef,
@@ -256,14 +277,60 @@ const {
   handleStopDrawing,
   handlePlay,
 } = usePull(route.params.roomId as string);
-
+const { arrivedState } = useScroll(doc, {
+  offset: { top: 0, bottom: 400, right: 0, left: 0 },
+});
+const first = ref(true);
 onMounted(() => {
+  //  @ts-ignore
+  (adsbygoogle = window.adsbygoogle || []).push({});
   handleSlideList();
   getLiveRoomList();
   getBg();
   videoWrapRef.value = videoWrapTmpRef.value;
 });
 
+watch(
+  () => arrivedState.bottom,
+  async (newval) => {
+    if (newval && !first.value) {
+      const arr = await handleBilibil();
+      otherLiveRoomList.value.push(...arr);
+    }
+  }
+);
+
+async function handleBilibil() {
+  if (loading.value) return [];
+  loading.value = true;
+  let arr: any = [];
+  try {
+    pageParams.page += 1;
+    const res = await fetchLiveBilibiliGetUserRecommend(pageParams);
+    // const list = res.data?.list;
+    const list = res?.data?.data?.list;
+    if (list) {
+      arr = list.map((item) => {
+        return {
+          id: item.roomid,
+          user: { username: item.uname },
+          live_room: {
+            id: item.roomid,
+            name: item.title,
+            cover_img: item.cover,
+            is_bilibili: 1,
+          },
+        };
+      });
+    }
+  } catch (error) {
+    pageParams.page -= 1;
+    console.log(error);
+  }
+  loading.value = false;
+  return arr;
+}
+
 function handleSlideList() {
   const row = 2;
   const res: any[] = [];
@@ -329,6 +396,10 @@ async function getLiveRoomList() {
       orderBy: 'desc',
     });
     if (res.code === 200) {
+      const arr: IBilibiliLiveUserRecommend[] = await handleBilibil();
+      first.value = false;
+      // @ts-ignore
+      res.data.rows.push(...arr);
       topLiveRoomList.value = res.data.rows.slice(0, topNums.value);
       otherLiveRoomList.value = res.data.rows.slice(topNums.value);
       if (res.data.total) {
@@ -342,16 +413,19 @@ async function getLiveRoomList() {
   }
 }
 
-function joinRoom(data: { roomId: number }) {
+function joinRoom(data) {
   router.push({
     name: routerName.pull,
-    params: { roomId: data.roomId },
+    params: { roomId: data.id },
+    query: { is_bilibili: data.is_bilibili },
   });
 }
 </script>
 
 <style lang="scss" scoped>
 .home-wrap {
+  overflow: scroll;
+  height: calc(100vh - $header-height);
   .play-container {
     position: relative;
     z-index: 1;
@@ -637,68 +711,87 @@ function joinRoom(data: { roomId: number }) {
         padding: 10px 0;
         font-size: 26px;
       }
-      .live-room {
-        display: inline-block;
-        margin-right: 32px;
-        margin-bottom: 10px;
-        width: 300px;
-        cursor: pointer;
+      .live-room-list {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: space-between;
+        .live-room {
+          display: inline-block;
+          margin-right: 32px;
+          margin-bottom: 10px;
+          width: 300px;
+          cursor: pointer;
+
+          .cover {
+            position: relative;
+            overflow: hidden;
+            width: 100%;
+            height: 150px;
+            border-radius: 8px;
+            background-position: center center;
+            background-size: cover;
+            .cdn-ico {
+              position: absolute;
+              top: -10px;
+              right: -10px;
+              z-index: 2;
+              width: 70px;
+              height: 28px;
+              background-color: #f87c48;
+              color: white;
+              transform: rotate(45deg);
+              transform-origin: bottom;
+              .txt {
+                margin-left: 18px;
+                background-image: initial !important;
+                font-size: 13px;
+              }
+            }
 
-        .cover {
-          position: relative;
-          overflow: hidden;
-          width: 100%;
-          height: 150px;
-          border-radius: 8px;
-          background-position: center center;
-          background-size: cover;
-          .cdn-ico {
-            position: absolute;
-            top: -10px;
-            right: -10px;
-            z-index: 2;
-            width: 70px;
-            height: 28px;
-            background-color: #f87c48;
-            color: white;
-            transform: rotate(45deg);
-            transform-origin: bottom;
             .txt {
-              margin-left: 18px;
-              background-image: initial !important;
+              position: absolute;
+              bottom: 0;
+              left: 0;
+              box-sizing: border-box;
+              padding: 4px 8px;
+              width: 100%;
+              border-radius: 0 0 4px 4px;
+              background-image: linear-gradient(
+                -180deg,
+                rgba(0, 0, 0, 0),
+                rgba(0, 0, 0, 0.6)
+              );
+              color: white;
+              text-align: initial;
               font-size: 13px;
+
+              @extend %singleEllipsis;
             }
           }
-
-          .txt {
-            position: absolute;
-            bottom: 0;
-            left: 0;
-            box-sizing: border-box;
-            padding: 4px 8px;
-            width: 100%;
-            border-radius: 0 0 4px 4px;
-            background-image: linear-gradient(
-              -180deg,
-              rgba(0, 0, 0, 0),
-              rgba(0, 0, 0, 0.6)
-            );
-            color: white;
-            text-align: initial;
-            font-size: 13px;
+          .desc {
+            margin-top: 4px;
+            font-size: 14px;
 
             @extend %singleEllipsis;
           }
         }
-        .desc {
-          margin-top: 4px;
-          font-size: 14px;
-
-          @extend %singleEllipsis;
-        }
       }
     }
   }
+  .ad-wrap {
+    position: fixed;
+    top: 300px;
+    left: 10px;
+    width: 250px;
+    // height: 150px;
+    border-radius: 10px;
+    // background-color: red;
+    cursor: pointer;
+    ins {
+      width: 100%;
+      height: 100%;
+    }
+  }
   .foot {
     margin-top: 10px;
     text-align: center;
@@ -729,6 +822,13 @@ function joinRoom(data: { roomId: number }) {
     }
     .area-container {
       width: $w-1150;
+      .area-item {
+        .live-room-list {
+          .live-room {
+            width: 250px;
+          }
+        }
+      }
     }
   }
 }

+ 80 - 62
src/views/my/index.vue

@@ -46,6 +46,7 @@
             userStore.userInfo?.live_rooms?.[0]?.areas?.[0]?.name || '暂无分区'
           }}
         </div>
+
         <div
           v-if="
             userStore.userInfo?.auths?.find(
@@ -61,68 +62,6 @@
           >
             更新地址
           </div>
-          <div
-            class="cdn"
-            v-if="
-              userStore.userInfo?.auths?.find(
-                (v) =>
-                  v.auth_value === DEFAULT_AUTH_INFO.LIVE_PULL_SVIP.auth_value
-              )
-            "
-          >
-            <div>
-              <span>
-                RTMP推流地址(CDN):{{
-                  userStore.userInfo?.live_rooms?.[0]?.cdn_push_rtmp_url!
-                }},
-              </span>
-              <span
-                class="link"
-                @click="
-                  handleCopy(
-                    userStore.userInfo?.live_rooms?.[0]?.cdn_push_rtmp_url!
-                  )
-                "
-              >
-                复制
-              </span>
-            </div>
-            <div>
-              <span>
-                OBS服务器(CDN):{{
-                  userStore.userInfo?.live_rooms?.[0]?.cdn_push_obs_server!
-                }},
-              </span>
-              <span
-                class="link"
-                @click="
-                  handleCopy(
-                    userStore.userInfo?.live_rooms?.[0]?.cdn_push_obs_server!
-                  )
-                "
-              >
-                复制
-              </span>
-            </div>
-            <div>
-              <span>
-                OBS推流码(CDN):{{
-                  userStore.userInfo?.live_rooms?.[0]?.cdn_push_obs_stream_key!
-                }},
-              </span>
-              <span
-                class="link"
-                @click="
-                  handleCopy(
-                    userStore.userInfo?.live_rooms?.[0]
-                      ?.cdn_push_obs_stream_key!
-                  )
-                "
-              >
-                复制
-              </span>
-            </div>
-          </div>
 
           <div class="srs">
             <div>
@@ -177,6 +116,82 @@
               </span>
             </div>
           </div>
+
+          <br />
+
+          <div>
+            CDN直播:
+            <div
+              class="cdn"
+              v-if="
+                userStore.userInfo?.auths?.find(
+                  (v) =>
+                    v.auth_value === DEFAULT_AUTH_INFO.LIVE_PULL_SVIP.auth_value
+                )
+              "
+            >
+              <div>
+                <span>
+                  RTMP推流地址(CDN):{{
+                    userStore.userInfo?.live_rooms?.[0]?.cdn_push_rtmp_url!
+                  }},
+                </span>
+                <span
+                  class="link"
+                  @click="
+                    handleCopy(
+                      userStore.userInfo?.live_rooms?.[0]?.cdn_push_rtmp_url!
+                    )
+                  "
+                >
+                  复制
+                </span>
+              </div>
+              <div>
+                <span>
+                  OBS服务器(CDN):{{
+                    userStore.userInfo?.live_rooms?.[0]?.cdn_push_obs_server!
+                  }},
+                </span>
+                <span
+                  class="link"
+                  @click="
+                    handleCopy(
+                      userStore.userInfo?.live_rooms?.[0]?.cdn_push_obs_server!
+                    )
+                  "
+                >
+                  复制
+                </span>
+              </div>
+              <div>
+                <span>
+                  OBS推流码(CDN):{{
+                    userStore.userInfo?.live_rooms?.[0]
+                      ?.cdn_push_obs_stream_key!
+                  }},
+                </span>
+                <span
+                  class="link"
+                  @click="
+                    handleCopy(
+                      userStore.userInfo?.live_rooms?.[0]
+                        ?.cdn_push_obs_stream_key!
+                    )
+                  "
+                >
+                  复制
+                </span>
+              </div>
+            </div>
+            <div
+              v-else
+              class="link"
+              @click="router.push({ name: routerName.author })"
+            >
+              请联系作者开通~
+            </div>
+          </div>
         </div>
       </div>
     </div>
@@ -256,9 +271,12 @@ async function handleUpdateKey() {
   position: relative;
   padding: 10px;
   .link {
+    display: inline-block;
     color: $theme-color-gold;
     text-decoration: none;
     cursor: pointer;
+
+    user-select: none;
   }
   .avatar {
     display: flex;

+ 119 - 5
src/views/pull/index.vue

@@ -49,9 +49,6 @@
             </div>
             <div class="bottom">
               <span>{{ appStore.liveRoomInfo?.desc }}</span>
-              <span v-if="NODE_ENV === 'development'">
-                socketId:{{ mySocketId }}
-              </span>
               <span
                 class="area"
                 @click="
@@ -63,6 +60,10 @@
               >
                 {{ appStore.liveRoomInfo?.areas?.[0]?.name }}
               </span>
+
+              <span v-if="NODE_ENV === 'development'">
+                socketId:{{ mySocketId }}
+              </span>
             </div>
           </div>
         </div>
@@ -182,6 +183,17 @@
           <div class="price">立即充值</div>
         </div>
       </div>
+      <div class="ad-wrap-b">
+        <!-- live-拉流页面广告位2 -->
+        <ins
+          class="adsbygoogle"
+          style="display: block"
+          data-ad-client="ca-pub-6064454674933772"
+          data-ad-slot="2315064038"
+          data-ad-format="auto"
+          data-full-width-responsive="true"
+        ></ins>
+      </div>
     </div>
     <div class="right">
       <div class="rank-wrap">
@@ -400,6 +412,19 @@
         </div>
       </div>
     </div>
+
+    <div class="ad-wrap-a">
+      <!-- live-拉流页面广告位1 -->
+      <ins
+        class="adsbygoogle"
+        style="display: block"
+        data-ad-client="ca-pub-6064454674933772"
+        data-ad-slot="6397310081"
+        data-ad-format="auto"
+        data-full-width-responsive="true"
+      ></ins>
+    </div>
+
     <RechargeCpt
       :show="showRecharge"
       @close="(v) => (showRecharge = v)"
@@ -413,6 +438,10 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useRoute } from 'vue-router';
 
+import {
+  fetchLiveBilibiliPlayUrl,
+  fetchLiveBilibiliRoomGetInfo,
+} from '@/api/bilibili';
 import {
   fetchGiftGroupList,
   fetchGiftRecordCreate,
@@ -427,12 +456,14 @@ import { commentAuthTip, loginTip } from '@/hooks/use-login';
 import { useFullScreen, usePictureInPicture } from '@/hooks/use-play';
 import { usePull } from '@/hooks/use-pull';
 import { useQiniuJsUpload } from '@/hooks/use-upload';
+import { useWebsocket } from '@/hooks/use-websocket';
 import {
   DanmuMsgTypeEnum,
   GiftRecordStatusEnum,
   GoodsTypeEnum,
   IGiftRecord,
   IGoods,
+  LiveLineEnum,
   LiveRenderEnum,
   WsMessageMsgIsFileEnum,
   WsMessageMsgIsShowEnum,
@@ -442,6 +473,7 @@ import router, { routerName } from '@/router';
 import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
+import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
 import { WsDisableSpeakingType, WsMsgTypeEnum } from '@/types/websocket';
 import { formatMoney, formatTimeHour, handleUserMedia } from '@/utils';
 import { NODE_ENV } from 'script/constant';
@@ -472,6 +504,7 @@ const remoteVideoRef = ref<HTMLDivElement>();
 const uploadRef = ref<HTMLInputElement>();
 const danmuIptRef = ref<HTMLTextAreaElement>();
 const loopGetLiveUserTimer = ref();
+const isBilibili = ref(false);
 
 const {
   initPull,
@@ -493,6 +526,8 @@ const {
   anchorInfo,
 } = usePull(roomId.value);
 
+const { initWs } = useWebsocket();
+
 const rtcRtt = computed(() => {
   const arr: any[] = [];
   networkStore.rtcMap.forEach((rtc) => {
@@ -510,6 +545,8 @@ const rtcLoss = computed(() => {
 });
 
 onMounted(() => {
+  // @ts-ignore
+  (adsbygoogle = window.adsbygoogle || []).push({});
   appStore.videoControls.fps = true;
   appStore.videoControls.fullMode = true;
   appStore.videoControls.kbs = true;
@@ -535,7 +572,19 @@ onMounted(() => {
     height.value = res;
   }
   getBg();
-  initPull({});
+  if (route.query.is_bilibili !== '1') {
+    isBilibili.value = false;
+    initPull({});
+  } else {
+    // initWs({
+    //   roomId: roomId.value,
+    //   isRemoteDesk: false,
+    //   isBilibili: true,
+    //   isAnchor: false,
+    // });
+    isBilibili.value = true;
+    handleBilibil();
+  }
   getGiftRecord();
   getGiftGroupList();
   handleSendGetLiveUser(Number(roomId.value));
@@ -547,6 +596,39 @@ onUnmounted(() => {
   clearInterval(loopGetLiveUserTimer.value);
 });
 
+async function handleBilibil() {
+  if (route.query.is_bilibili === '1') {
+    const flv = await fetchLiveBilibiliPlayUrl({
+      cid: route.params.roomId,
+      platform: 'web',
+    });
+    const hls = await fetchLiveBilibiliPlayUrl({
+      cid: route.params.roomId,
+      platform: 'h5',
+    });
+    const roomInfo = await fetchLiveBilibiliRoomGetInfo({
+      room_id: route.params.roomId,
+    });
+    console.log('roomInfo', roomInfo);
+    console.log(flv?.data?.data?.durl?.[0].url, 'flv');
+    console.log(hls?.data?.data?.durl?.[0].url, 'hls');
+    roomLiving.value = true;
+    appStore.liveLine = LiveLineEnum.flv;
+    anchorInfo.value = {
+      avatar: roomInfo?.data?.data?.user_cover,
+      username: roomInfo?.data?.data?.title,
+    };
+    appStore.liveRoomInfo = {
+      type: LiveRoomTypeEnum.system,
+      flv_url: flv?.data?.data?.durl?.[0].url,
+      hls_url: hls?.data?.data?.durl?.[0].url,
+      areas: [{ name: roomInfo?.data?.data?.area_name }],
+      desc: roomInfo?.data?.data?.description,
+    };
+    handleRefresh();
+  }
+}
+
 function handleSendGetLiveUser(liveRoomId: number) {
   async function main() {
     const res = await fetchLiveRoomOnlineUser({ live_room_id: liveRoomId });
@@ -797,6 +879,7 @@ async function handlePay(item: IGoods) {
     goodsId: item.id!,
     goodsNums: 1,
     liveRoomId: Number(roomId.value),
+    isBilibili: isBilibili.value,
   });
   if (res.code === 200) {
     window.$message.success('打赏成功!');
@@ -939,7 +1022,7 @@ function handleScrollTop() {
   .left {
     position: relative;
     display: inline-block;
-    overflow: hidden;
+    // overflow: hidden;
     box-sizing: border-box;
     width: $w-900;
     height: 740px;
@@ -1163,6 +1246,21 @@ function handleScrollTop() {
         }
       }
     }
+    .ad-wrap-b {
+      position: absolute;
+      bottom: -10px;
+      left: 0;
+      width: 100%;
+      // height: 150px;
+      border-radius: 10px;
+      // background-color: red;
+      cursor: pointer;
+      transform: translateY(100%);
+      ins {
+        width: 100%;
+        height: 100%;
+      }
+    }
   }
   .right {
     position: relative;
@@ -1374,6 +1472,22 @@ function handleScrollTop() {
       }
     }
   }
+
+  .ad-wrap-a {
+    position: fixed;
+    top: 300px;
+    left: 10px;
+    width: 250px;
+    // height: 150px;
+    border-radius: 10px;
+    // background-color: red;
+    cursor: pointer;
+    ins {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
   &.isPageFull {
     position: fixed;
     top: 0;

+ 63 - 9
test/test.json

@@ -1,11 +1,65 @@
 {
-  "url": "http://localhost:5001/livestream/roomId___4.flv?usertoken=32b617a7b7c8dd97947e9bdc945d4c65&userid=101&randomid=GJH6evlJ",
-  "hasRedirect": false,
-  "speed": 73.154296875,
-  "loaderType": "fetch-stream-loader",
-  "currentSegmentIndex": 0,
-  "totalSegmentCount": 1,
-  "playerType": "MSEPlayer",
-  "decodedFrames": 4458,
-  "droppedFrames": 2
+  "roomid": 545068,
+  "uid": 8739477,
+  "title": "德云色 18点解说!",
+  "uname": "老实憨厚的笑笑",
+  "online": 4972049,
+  "user_cover": "http://i0.hdslb.com/bfs/live/new_room_cover/5b194702d6c96774e049d5f7ab2d859ec5009755.jpg",
+  "user_cover_flag": 1,
+  "system_cover": "http://i0.hdslb.com/bfs/live-key-frame/keyframe08121830000000545068fh3wc4.jpg",
+  "cover": "http://i0.hdslb.com/bfs/live/new_room_cover/5b194702d6c96774e049d5f7ab2d859ec5009755.jpg",
+  "show_cover": "",
+  "link": "/545068?hotRank=0",
+  "face": "https://i0.hdslb.com/bfs/face/59da329a536b148b0e8d19143b686d2da06dad93.jpg",
+  "parent_id": 2,
+  "parent_name": "网游",
+  "area_id": 86,
+  "area_name": "英雄联盟",
+  "area_v2_parent_id": 2,
+  "area_v2_parent_name": "网游",
+  "area_v2_id": 86,
+  "area_v2_name": "英雄联盟",
+  "session_id": "579fbbb823062ad7109aa6991d66b9e5_66799746-DE90-4659-8DF0-F18EE6301655",
+  "group_id": 1000268,
+  "show_callback": "https://live-trace.bilibili.com/xlive/data-interface/v1/index/log?sessionID=579fbbb823062ad7109aa6991d66b9e5_66799746-DE90-4659-8DF0-F18EE6301655&group_id=1000268&biz=live&event_id=live_card_show&rule_key=&special_id=0&roomid=545068&parent_id=2&area_id=86&page=0&position=2",
+  "click_callback": "https://live-trace.bilibili.com/xlive/data-interface/v1/index/log?sessionID=579fbbb823062ad7109aa6991d66b9e5_66799746-DE90-4659-8DF0-F18EE6301655&group_id=1000268&biz=live&event_id=live_card_click&rule_key=&special_id=0&roomid=545068&parent_id=2&area_id=86&page=0&position=2",
+  "web_pendent": "",
+  "pk_id": 0,
+  "pendant_info": {
+    "1": {
+      "pendent_id": 426,
+      "content": "",
+      "color": "#FB9E60",
+      "pic": "https://i0.hdslb.com/bfs/live/539ce26c45cd4019f55b64cfbcedc3c01820e539.png",
+      "position": 1,
+      "type": "mobile_index_badge",
+      "name": "百人成就"
+    }
+  },
+  "verify": {
+    "role": 2,
+    "desc": "bilibili 2022百大UP主、2022直播年度弹幕人气奖UP主、职业游戏解说孙亚龙",
+    "type": 0
+  },
+  "head_box": {
+    "name": "百人舰队主播头像",
+    "value": "https://i0.hdslb.com/bfs/vc/071eb10548fe9bc482ff69331983d94192ce9507.png",
+    "desc": ""
+  },
+  "head_box_type": 1,
+  "is_auto_play": 1,
+  "flag": 0,
+  "watched_show": {
+    "switch": true,
+    "num": 119606,
+    "text_small": "11.9万",
+    "text_large": "11.9万人看过",
+    "icon": "https://i0.hdslb.com/bfs/live/a725a9e61242ef44d764ac911691a7ce07f36c1d.png",
+    "icon_location": 0,
+    "icon_web": "https://i0.hdslb.com/bfs/live/8d9d0f33ef8bf6f308742752d13dd0df731df19c.png"
+  },
+  "is_nft": 0,
+  "nft_dmark": "https://i0.hdslb.com/bfs/live/9f176ff49d28c50e9c53ec1c3297bd1ee539b3d6.gif",
+  "play_together_goods": null,
+  "watermark": ""
 }