index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. <template>
  2. <div class="home-wrap">
  3. <div class="play-container">
  4. <div class="bg"></div>
  5. <div class="container">
  6. <div class="left">
  7. <div
  8. v-if="currentLiveRoom?.live_room?.cdn === 1"
  9. class="cdn-ico"
  10. >
  11. <div class="txt">CDN</div>
  12. </div>
  13. <div
  14. class="cover"
  15. :style="{
  16. backgroundImage: `url(${
  17. currentLiveRoom?.live_room?.cover_img ||
  18. currentLiveRoom?.user?.avatar
  19. })`,
  20. }"
  21. ></div>
  22. <div v-loading="videoLoading">
  23. <div
  24. v-if="currentLiveRoom?.live_room?.flv_url"
  25. ref="canvasRef"
  26. ></div>
  27. </div>
  28. <template v-if="currentLiveRoom">
  29. <VideoControls></VideoControls>
  30. <div
  31. class="join-btn"
  32. :style="{
  33. display: !isMobile() ? 'none' : showControls ? 'block' : 'none',
  34. }"
  35. >
  36. <div
  37. v-if="
  38. currentLiveRoom.live_room?.type ===
  39. LiveRoomTypeEnum.user_wertc
  40. "
  41. class="btn webrtc"
  42. @click="joinRtcRoom()"
  43. >
  44. 进入直播(webrtc)
  45. </div>
  46. <div
  47. v-if="
  48. currentLiveRoom.live_room?.type === LiveRoomTypeEnum.user_srs
  49. "
  50. class="btn webrtc"
  51. @click="joinRtcRoom()"
  52. >
  53. 进入直播(srs-webrtc)
  54. </div>
  55. <div
  56. v-if="
  57. currentLiveRoom.live_room?.type !==
  58. LiveRoomTypeEnum.user_wertc
  59. "
  60. class="btn flv"
  61. @click="joinRoom(true)"
  62. >
  63. 进入直播(flv)
  64. </div>
  65. <div
  66. v-if="
  67. currentLiveRoom.live_room?.type !==
  68. LiveRoomTypeEnum.user_wertc
  69. "
  70. class="btn hls"
  71. @click="joinRoom(false)"
  72. >
  73. 进入直播(hls)
  74. </div>
  75. </div>
  76. </template>
  77. </div>
  78. <div class="right">
  79. <div
  80. v-if="topLiveRoomList.length"
  81. class="list"
  82. >
  83. <div
  84. v-for="(item, index) in topLiveRoomList"
  85. :key="index"
  86. :class="{
  87. item: 1,
  88. active: item.live_room_id === currentLiveRoom?.live_room_id,
  89. }"
  90. :style="{
  91. backgroundImage: `url(${
  92. item.live_room?.cover_img || item?.user?.avatar
  93. })`,
  94. }"
  95. @click="changeLiveRoom(item)"
  96. >
  97. <div
  98. class="border"
  99. :style="{
  100. opacity:
  101. item.live_room_id === currentLiveRoom?.live_room_id ? 1 : 0,
  102. }"
  103. ></div>
  104. <div
  105. v-if="item.live_room_id === currentLiveRoom?.live_room_id"
  106. class="triangle"
  107. ></div>
  108. <div class="txt">{{ item.live_room?.name }}</div>
  109. </div>
  110. </div>
  111. <div
  112. v-else
  113. class="none"
  114. >
  115. 当前没有在线的直播间
  116. </div>
  117. </div>
  118. </div>
  119. </div>
  120. <div class="area-container">
  121. <div class="area-item">
  122. <div class="title">推荐直播</div>
  123. <div class="live-room-list">
  124. <div
  125. v-for="(iten, indey) in otherLiveRoomList"
  126. :key="indey"
  127. class="live-room"
  128. @click="joinOtherRoom(iten.live_room!)"
  129. >
  130. <div
  131. class="cover"
  132. :style="{
  133. backgroundImage: `url('${
  134. iten?.live_room?.cover_img || iten?.user?.avatar
  135. }')`,
  136. }"
  137. >
  138. <div
  139. v-if="iten?.live_room?.cdn === 1"
  140. class="cdn-ico"
  141. >
  142. <div class="txt">CDN</div>
  143. </div>
  144. <div class="txt">{{ iten?.user?.username }}</div>
  145. </div>
  146. <div class="desc">{{ iten?.live_room?.name }}</div>
  147. </div>
  148. <div
  149. v-if="!otherLiveRoomList.length"
  150. class="null"
  151. >
  152. 暂无数据
  153. </div>
  154. </div>
  155. </div>
  156. </div>
  157. <div class="foot">*部分内容来源网络,如有侵权,请联系我删除~</div>
  158. </div>
  159. </template>
  160. <script lang="ts" setup>
  161. import { isMobile } from 'billd-utils';
  162. import { onMounted, ref } from 'vue';
  163. import { useRoute, useRouter } from 'vue-router';
  164. import { fetchLiveList } from '@/api/live';
  165. import { usePull } from '@/hooks/use-pull';
  166. import { ILive, ILiveRoom, LiveRoomTypeEnum, liveTypeEnum } from '@/interface';
  167. import { routerName } from '@/router';
  168. const route = useRoute();
  169. const router = useRouter();
  170. const canvasRef = ref<Element>();
  171. const showControls = ref(false);
  172. const topNums = ref(6);
  173. const topLiveRoomList = ref<ILive[]>([]);
  174. const otherLiveRoomList = ref<ILive[]>([]);
  175. const currentLiveRoom = ref<ILive>();
  176. const localVideoRef = ref<HTMLVideoElement[]>([]);
  177. const { handleHlsPlay, videoLoading } = usePull({
  178. localVideoRef,
  179. canvasRef,
  180. liveType: route.query.liveType as liveTypeEnum,
  181. isSRS: [
  182. liveTypeEnum.srsWebrtcPull,
  183. liveTypeEnum.srsFlvPull,
  184. liveTypeEnum.srsHlsPull,
  185. ].includes(route.query.liveType as liveTypeEnum),
  186. });
  187. function changeLiveRoom(item: ILive) {
  188. if (item.id === currentLiveRoom.value?.id) return;
  189. currentLiveRoom.value = item;
  190. canvasRef.value?.childNodes?.forEach((item) => {
  191. item.remove();
  192. });
  193. if (
  194. [
  195. LiveRoomTypeEnum.user_srs,
  196. LiveRoomTypeEnum.user_obs,
  197. LiveRoomTypeEnum.system,
  198. ].includes(item.live_room?.type!)
  199. ) {
  200. handleHlsPlay(item.live_room?.hls_url!);
  201. }
  202. }
  203. async function getLiveRoomList() {
  204. try {
  205. const res = await fetchLiveList({
  206. orderName: 'created_at',
  207. orderBy: 'desc',
  208. });
  209. if (res.code === 200) {
  210. topLiveRoomList.value = res.data.rows.slice(0, topNums.value);
  211. otherLiveRoomList.value = res.data.rows.slice(topNums.value);
  212. if (res.data.total) {
  213. currentLiveRoom.value = topLiveRoomList.value[0];
  214. if (
  215. [
  216. LiveRoomTypeEnum.user_srs,
  217. LiveRoomTypeEnum.user_obs,
  218. LiveRoomTypeEnum.system,
  219. ].includes(currentLiveRoom.value?.live_room?.type!)
  220. ) {
  221. handleHlsPlay(currentLiveRoom.value.live_room?.hls_url!);
  222. }
  223. }
  224. }
  225. } catch (error) {
  226. console.log(error);
  227. }
  228. }
  229. onMounted(() => {
  230. getLiveRoomList();
  231. });
  232. function joinOtherRoom(item: ILiveRoom) {
  233. currentLiveRoom.value = item;
  234. joinRoom(false);
  235. }
  236. function joinRtcRoom() {
  237. if (currentLiveRoom.value?.live_room?.type === LiveRoomTypeEnum.user_srs) {
  238. router.push({
  239. name: routerName.pull,
  240. params: {
  241. roomId: currentLiveRoom.value.live_room_id,
  242. },
  243. query: {
  244. liveType: liveTypeEnum.srsWebrtcPull,
  245. },
  246. });
  247. } else {
  248. router.push({
  249. name: routerName.pull,
  250. params: {
  251. roomId: currentLiveRoom.value?.live_room_id,
  252. },
  253. query: {
  254. liveType: liveTypeEnum.webrtcPull,
  255. },
  256. });
  257. }
  258. }
  259. function joinRoom(isFlv: boolean) {
  260. router.push({
  261. name: routerName.pull,
  262. params: { roomId: currentLiveRoom.value?.live_room_id },
  263. query: {
  264. liveType: isFlv ? liveTypeEnum.srsFlvPull : liveTypeEnum.srsHlsPull,
  265. },
  266. });
  267. }
  268. </script>
  269. <style lang="scss" scoped>
  270. .home-wrap {
  271. .play-container {
  272. position: relative;
  273. .bg {
  274. position: absolute;
  275. top: 0;
  276. right: 0;
  277. left: 0;
  278. z-index: -1;
  279. width: 100%;
  280. height: 100%;
  281. background-color: papayawhip;
  282. background-position: center;
  283. background-repeat: no-repeat;
  284. }
  285. .container {
  286. display: flex;
  287. box-sizing: border-box;
  288. margin: 0 auto;
  289. padding: 15px 0;
  290. justify-content: center;
  291. height: calc($w-1100 / $video-ratio);
  292. .left {
  293. position: relative;
  294. display: inline-block;
  295. overflow: hidden;
  296. box-sizing: border-box;
  297. flex-shrink: 0;
  298. margin-right: 20px;
  299. width: $w-1100;
  300. height: 100%;
  301. border-radius: 4px;
  302. background-color: rgba($color: #000000, $alpha: 0.3);
  303. @extend %coverBg;
  304. .cdn-ico {
  305. position: absolute;
  306. top: -9px;
  307. right: -10px;
  308. z-index: 2;
  309. width: 70px;
  310. height: 32px;
  311. background-color: #f87c48;
  312. color: white;
  313. transform: rotate(45deg);
  314. transform-origin: bottom;
  315. .txt {
  316. margin-top: 11px;
  317. margin-left: 20px;
  318. background-image: initial !important;
  319. font-size: 14px;
  320. }
  321. }
  322. .cover {
  323. position: absolute;
  324. background-position: center center;
  325. background-size: cover;
  326. filter: blur(10px);
  327. inset: 0;
  328. }
  329. :deep(canvas) {
  330. position: absolute;
  331. top: 0;
  332. left: 50%;
  333. width: 100%;
  334. height: 100%;
  335. transform: translate(-50%);
  336. user-select: none;
  337. }
  338. :deep(video) {
  339. position: absolute;
  340. top: 0;
  341. left: 50%;
  342. width: 100%;
  343. height: 100%;
  344. transform: translate(-50%);
  345. user-select: none;
  346. }
  347. .controls {
  348. display: none;
  349. }
  350. &:hover {
  351. .join-btn {
  352. display: inline-flex !important;
  353. }
  354. }
  355. .join-btn {
  356. position: absolute;
  357. top: 50%;
  358. left: 50%;
  359. z-index: 1;
  360. display: none;
  361. align-items: center;
  362. align-items: center;
  363. justify-content: center;
  364. box-sizing: border-box;
  365. width: 80%;
  366. transform: translate(-50%, -50%);
  367. .btn {
  368. padding: 14px 26px;
  369. border: 2px solid rgba($color: papayawhip, $alpha: 0.5);
  370. border-radius: 6px;
  371. background-color: rgba(0, 0, 0, 0.3);
  372. color: papayawhip;
  373. font-size: 16px;
  374. cursor: pointer;
  375. &:hover {
  376. background-color: rgba($color: papayawhip, $alpha: 0.5);
  377. color: white;
  378. }
  379. &.webrtc {
  380. margin-right: 10px;
  381. }
  382. &.flv {
  383. margin-right: 10px;
  384. }
  385. }
  386. }
  387. }
  388. .right {
  389. flex-shrink: 0;
  390. display: inline-block;
  391. overflow: scroll;
  392. box-sizing: border-box;
  393. padding: 12px 10px;
  394. height: 100%;
  395. border-radius: 4px;
  396. background-color: rgba($color: #000000, $alpha: 0.3);
  397. @extend %hideScrollbar;
  398. .list {
  399. .item {
  400. position: relative;
  401. box-sizing: border-box;
  402. margin-bottom: 10px;
  403. width: 200px;
  404. height: 110px;
  405. border-radius: 4px;
  406. background-color: rgba($color: #000000, $alpha: 0.3);
  407. cursor: pointer;
  408. @extend %coverBg;
  409. &:last-child {
  410. margin-bottom: 0;
  411. }
  412. .border {
  413. position: absolute;
  414. top: 0;
  415. right: 0;
  416. bottom: 0;
  417. left: 0;
  418. z-index: 1;
  419. border: 2px solid papayawhip;
  420. border-radius: 4px;
  421. }
  422. .triangle {
  423. position: absolute;
  424. top: 50%;
  425. left: 0;
  426. display: inline-block;
  427. border: 5px solid transparent;
  428. border-right-color: papayawhip;
  429. transform: translate(-100%, -50%);
  430. }
  431. &.active {
  432. &::before {
  433. background-color: transparent;
  434. }
  435. }
  436. &:hover {
  437. &::before {
  438. background-color: transparent;
  439. }
  440. }
  441. &::before {
  442. position: absolute;
  443. display: block;
  444. width: 100%;
  445. height: 100%;
  446. border-radius: 4px;
  447. background-color: rgba(0, 0, 0, 0.4);
  448. content: '';
  449. transition: all cubic-bezier(0.22, 0.58, 0.12, 0.98) 0.4s;
  450. }
  451. .txt {
  452. position: absolute;
  453. bottom: 0;
  454. left: 0;
  455. box-sizing: border-box;
  456. padding: 4px 8px;
  457. width: 100%;
  458. border-radius: 0 0 4px 4px;
  459. background-image: linear-gradient(
  460. -180deg,
  461. rgba(0, 0, 0, 0),
  462. rgba(0, 0, 0, 0.6)
  463. );
  464. color: white;
  465. text-align: initial;
  466. font-size: 13px;
  467. @extend %singleEllipsis;
  468. }
  469. }
  470. }
  471. .none {
  472. width: 200px;
  473. color: white;
  474. text-align: center;
  475. font-size: 14px;
  476. }
  477. }
  478. }
  479. }
  480. .area-container {
  481. margin: 10px auto;
  482. width: $w-1350;
  483. box-sizing: border-box;
  484. .area-item {
  485. .title {
  486. padding: 10px 0;
  487. font-size: 26px;
  488. }
  489. .live-room {
  490. display: inline-block;
  491. margin-right: 32px;
  492. margin-bottom: 10px;
  493. width: 300px;
  494. cursor: pointer;
  495. .cover {
  496. position: relative;
  497. overflow: hidden;
  498. width: 100%;
  499. height: 150px;
  500. border-radius: 8px;
  501. background-position: center center;
  502. background-size: cover;
  503. .cdn-ico {
  504. position: absolute;
  505. top: -10px;
  506. right: -10px;
  507. z-index: 2;
  508. width: 70px;
  509. height: 28px;
  510. background-color: #f87c48;
  511. color: white;
  512. transform: rotate(45deg);
  513. transform-origin: bottom;
  514. .txt {
  515. margin-left: 18px;
  516. background-image: initial !important;
  517. font-size: 13px;
  518. }
  519. }
  520. .txt {
  521. position: absolute;
  522. bottom: 0;
  523. left: 0;
  524. box-sizing: border-box;
  525. padding: 4px 8px;
  526. width: 100%;
  527. border-radius: 0 0 4px 4px;
  528. background-image: linear-gradient(
  529. -180deg,
  530. rgba(0, 0, 0, 0),
  531. rgba(0, 0, 0, 0.6)
  532. );
  533. color: white;
  534. text-align: initial;
  535. font-size: 13px;
  536. @extend %singleEllipsis;
  537. }
  538. }
  539. .desc {
  540. margin-top: 4px;
  541. font-size: 14px;
  542. @extend %singleEllipsis;
  543. }
  544. }
  545. }
  546. }
  547. .foot {
  548. margin-top: 10px;
  549. text-align: center;
  550. }
  551. }
  552. // 屏幕宽度小于1330的时候
  553. @media screen and (max-width: 1330px) {
  554. .home-wrap {
  555. .play-container {
  556. .container {
  557. height: calc($w-900 / $video-ratio);
  558. .left {
  559. width: $w-900;
  560. }
  561. .right {
  562. }
  563. }
  564. }
  565. .area-container {
  566. width: $w-1150;
  567. }
  568. }
  569. }
  570. </style>