index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. <template>
  2. <div class="webrtc-push-wrap">
  3. <div
  4. ref="topRef"
  5. class="left"
  6. >
  7. <div
  8. ref="containerRef"
  9. class="container"
  10. >
  11. <div class="video-wrap">
  12. <video
  13. id="localVideo"
  14. ref="localVideoRef"
  15. autoplay
  16. webkit-playsinline="true"
  17. playsinline
  18. x-webkit-airplay="allow"
  19. x5-video-player-type="h5"
  20. x5-video-player-fullscreen="true"
  21. x5-video-orientation="portraint"
  22. muted
  23. ></video>
  24. <div
  25. v-if="currMediaTypeList.length > 0"
  26. class="controls"
  27. >
  28. <VideoControls></VideoControls>
  29. </div>
  30. <div
  31. v-if="!currMediaTypeList || currMediaTypeList.length <= 0"
  32. class="add-wrap"
  33. >
  34. <n-space>
  35. <n-button
  36. class="item"
  37. @click="startGetUserMedia"
  38. >
  39. 摄像头
  40. </n-button>
  41. <n-button
  42. class="item"
  43. @click="startGetDisplayMedia"
  44. >
  45. 窗口
  46. </n-button>
  47. </n-space>
  48. </div>
  49. </div>
  50. <div class="sidebar">
  51. <div class="title">在线人员</div>
  52. <div
  53. v-for="(item, index) in sidebarList"
  54. :key="index"
  55. class="item"
  56. >
  57. <video
  58. :ref="(el) => (remoteVideoRef[item.socketId] = el)"
  59. autoplay
  60. webkit-playsinline="true"
  61. playsinline
  62. x-webkit-airplay="allow"
  63. x5-video-player-type="h5"
  64. x5-video-player-fullscreen="true"
  65. x5-video-orientation="portraint"
  66. muted
  67. ></video>
  68. </div>
  69. </div>
  70. </div>
  71. <div
  72. ref="bottomRef"
  73. class="room-control"
  74. >
  75. <div class="info">
  76. <div
  77. class="avatar"
  78. :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
  79. ></div>
  80. <div class="detail">
  81. <div class="top">
  82. <n-input-group>
  83. <n-input
  84. v-model:value="roomName"
  85. size="small"
  86. placeholder="输入房间名"
  87. :style="{ width: '50%' }"
  88. :disabled="disabled"
  89. />
  90. <n-button
  91. size="small"
  92. type="primary"
  93. :disabled="disabled"
  94. @click="confirmRoomName"
  95. >
  96. 确定
  97. </n-button>
  98. </n-input-group>
  99. </div>
  100. <div class="bottom">
  101. <span>socketId:{{ getSocketId() }}</span>
  102. </div>
  103. </div>
  104. </div>
  105. <div class="other">
  106. <div class="top">
  107. <span class="item">
  108. <i class="ico"></i>
  109. <span>正在观看人数:{{ liveUserList.length }}</span>
  110. </span>
  111. </div>
  112. <div class="bottom">
  113. <n-space>
  114. <n-button
  115. type="info"
  116. size="small"
  117. @click="startLive"
  118. >
  119. 开始直播
  120. </n-button>
  121. <n-button
  122. type="info"
  123. size="small"
  124. @click="endLive"
  125. >
  126. 结束直播
  127. </n-button>
  128. </n-space>
  129. </div>
  130. </div>
  131. </div>
  132. </div>
  133. <div class="right">
  134. <div class="resource-card">
  135. <div class="title">素材列表</div>
  136. <div class="list">
  137. <div
  138. v-for="(item, index) in currMediaTypeList"
  139. :key="index"
  140. class="item"
  141. >
  142. <span class="name">{{ item.txt }}</span>
  143. </div>
  144. </div>
  145. </div>
  146. <div class="danmu-card">
  147. <div class="title">弹幕互动</div>
  148. <div class="list-wrap">
  149. <div class="list">
  150. <div
  151. v-for="(item, index) in damuList"
  152. :key="index"
  153. class="item"
  154. >
  155. <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
  156. <span class="name">{{ item.socketId }}:</span>
  157. <span class="msg">{{ item.msg }}</span>
  158. </template>
  159. <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
  160. <span class="name system">系统通知:</span>
  161. <span class="msg">{{ item.socketId }}进入直播!</span>
  162. </template>
  163. <template
  164. v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
  165. >
  166. <span class="name system">系统通知:</span>
  167. <span class="msg">{{ item.socketId }}离开直播!</span>
  168. </template>
  169. </div>
  170. </div>
  171. </div>
  172. <div class="send-msg">
  173. <input
  174. v-model="danmuStr"
  175. class="ipt"
  176. @keydown="keydownDanmu"
  177. />
  178. <n-button
  179. type="info"
  180. size="small"
  181. @click="sendDanmu"
  182. >
  183. 发送
  184. </n-button>
  185. </div>
  186. </div>
  187. </div>
  188. </div>
  189. </template>
  190. <script lang="ts" setup>
  191. import { onMounted, onUnmounted, ref } from 'vue';
  192. import { useRoute } from 'vue-router';
  193. import { usePush } from '@/hooks/use-push';
  194. import { DanmuMsgTypeEnum, liveTypeEnum } from '@/interface';
  195. import { useUserStore } from '@/store/user';
  196. const route = useRoute();
  197. const userStore = useUserStore();
  198. const liveType = route.query.liveType;
  199. const topRef = ref<HTMLDivElement>();
  200. const bottomRef = ref<HTMLDivElement>();
  201. const containerRef = ref<HTMLDivElement>();
  202. const localVideoRef = ref<HTMLVideoElement>();
  203. const remoteVideoRef = ref<HTMLVideoElement[]>([]);
  204. const {
  205. initPush,
  206. confirmRoomName,
  207. getSocketId,
  208. startGetDisplayMedia,
  209. startGetUserMedia,
  210. startLive,
  211. endLive,
  212. closeWs,
  213. closeRtc,
  214. sendDanmu,
  215. keydownDanmu,
  216. disabled,
  217. danmuStr,
  218. roomName,
  219. damuList,
  220. liveUserList,
  221. currMediaTypeList,
  222. sidebarList,
  223. } = usePush({
  224. localVideoRef,
  225. remoteVideoRef,
  226. isSRS: liveType === liveTypeEnum.srsPush,
  227. });
  228. onUnmounted(() => {
  229. closeWs();
  230. closeRtc();
  231. });
  232. onMounted(() => {
  233. initPush();
  234. if (topRef.value && bottomRef.value && containerRef.value) {
  235. const res =
  236. bottomRef.value.getBoundingClientRect().top -
  237. topRef.value.getBoundingClientRect().top;
  238. containerRef.value.style.height = `${res}px`;
  239. }
  240. });
  241. </script>
  242. <style lang="scss" scoped>
  243. .webrtc-push-wrap {
  244. margin: 20px auto 0;
  245. min-width: $large-width;
  246. height: 700px;
  247. text-align: center;
  248. .left {
  249. position: relative;
  250. display: inline-block;
  251. overflow: hidden;
  252. box-sizing: border-box;
  253. width: $large-left-width;
  254. height: 100%;
  255. border-radius: 6px;
  256. background-color: white;
  257. color: #9499a0;
  258. vertical-align: top;
  259. .container {
  260. display: flex;
  261. align-items: center;
  262. justify-content: space-between;
  263. height: 100%;
  264. background-color: #fff;
  265. .video-wrap {
  266. position: relative;
  267. display: flex;
  268. flex: 1;
  269. height: 100%;
  270. background-color: rgba($color: #000000, $alpha: 0.5);
  271. #localVideo {
  272. max-width: 100%;
  273. max-height: 100%;
  274. }
  275. .controls {
  276. display: none;
  277. }
  278. &:hover {
  279. .controls {
  280. display: block;
  281. }
  282. }
  283. .add-wrap {
  284. position: absolute;
  285. top: 50%;
  286. left: 50%;
  287. display: flex;
  288. align-items: center;
  289. justify-content: space-around;
  290. padding: 0 20px;
  291. height: 50px;
  292. border-radius: 5px;
  293. background-color: white;
  294. transform: translate(-50%, -50%);
  295. }
  296. }
  297. .sidebar {
  298. width: 130px;
  299. height: 100%;
  300. background-color: rgba($color: #000000, $alpha: 0.3);
  301. .title {
  302. color: white;
  303. }
  304. .join {
  305. color: white;
  306. cursor: pointer;
  307. }
  308. video {
  309. max-width: 100%;
  310. }
  311. }
  312. }
  313. .room-control {
  314. position: absolute;
  315. right: 0;
  316. bottom: 0;
  317. left: 0;
  318. display: flex;
  319. justify-content: space-between;
  320. padding: 20px;
  321. background-color: papayawhip;
  322. .info {
  323. display: flex;
  324. align-items: center;
  325. .avatar {
  326. margin-right: 20px;
  327. width: 64px;
  328. height: 64px;
  329. border-radius: 50%;
  330. background-color: skyblue;
  331. background-position: center center;
  332. background-size: cover;
  333. background-repeat: no-repeat;
  334. }
  335. .detail {
  336. display: flex;
  337. flex-direction: column;
  338. text-align: initial;
  339. .top {
  340. margin-bottom: 10px;
  341. color: #18191c;
  342. .btn {
  343. margin-left: 10px;
  344. }
  345. }
  346. .bottom {
  347. font-size: 14px;
  348. }
  349. }
  350. }
  351. .other {
  352. display: flex;
  353. flex-direction: column;
  354. justify-content: center;
  355. font-size: 12px;
  356. .top {
  357. display: flex;
  358. align-items: center;
  359. .item {
  360. display: flex;
  361. align-items: center;
  362. margin-right: 20px;
  363. .ico {
  364. display: inline-block;
  365. margin-right: 4px;
  366. width: 10px;
  367. height: 10px;
  368. border-radius: 50%;
  369. background-color: skyblue;
  370. }
  371. }
  372. }
  373. .bottom {
  374. margin-top: 10px;
  375. }
  376. }
  377. }
  378. }
  379. .right {
  380. position: relative;
  381. display: inline-block;
  382. box-sizing: border-box;
  383. margin-left: 10px;
  384. width: 240px;
  385. height: 100%;
  386. border-radius: 6px;
  387. background-color: white;
  388. color: #9499a0;
  389. .resource-card {
  390. box-sizing: border-box;
  391. margin-bottom: 5%;
  392. margin-bottom: 10px;
  393. padding: 10px;
  394. width: 100%;
  395. height: 290px;
  396. border-radius: 6px;
  397. background-color: papayawhip;
  398. .title {
  399. text-align: initial;
  400. }
  401. .item {
  402. display: flex;
  403. align-items: center;
  404. justify-content: space-between;
  405. margin: 5px 0;
  406. font-size: 12px;
  407. }
  408. }
  409. .danmu-card {
  410. box-sizing: border-box;
  411. padding: 10px;
  412. width: 100%;
  413. height: 400px;
  414. border-radius: 6px;
  415. background-color: papayawhip;
  416. text-align: initial;
  417. .title {
  418. margin-bottom: 10px;
  419. }
  420. .list {
  421. margin-bottom: 10px;
  422. height: 300px;
  423. .item {
  424. margin-bottom: 10px;
  425. font-size: 12px;
  426. .name {
  427. color: #9499a0;
  428. }
  429. .msg {
  430. color: #61666d;
  431. }
  432. }
  433. }
  434. .send-msg {
  435. display: flex;
  436. align-items: center;
  437. box-sizing: border-box;
  438. .ipt {
  439. display: block;
  440. box-sizing: border-box;
  441. margin: 0 auto;
  442. margin-right: 10px;
  443. padding: 10px;
  444. width: 80%;
  445. height: 30px;
  446. outline: none;
  447. border: 1px solid hsla(0, 0%, 60%, 0.2);
  448. border-radius: 4px;
  449. background-color: #f1f2f3;
  450. font-size: 14px;
  451. }
  452. }
  453. }
  454. }
  455. }
  456. // 屏幕宽度小于$large-width的时候
  457. @media screen and (max-width: $large-width) {
  458. .webrtc-push-wrap {
  459. .left {
  460. width: $medium-left-width;
  461. }
  462. .right {
  463. .list {
  464. .item {
  465. }
  466. }
  467. }
  468. }
  469. }
  470. </style>