index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. <template>
  2. <div class="home-wrap">
  3. <div class="left">
  4. <div class="head">
  5. <div class="info">
  6. <div class="avatar"></div>
  7. <div class="detail">
  8. <div class="top">
  9. <span class="tag">未开播</span>
  10. <!-- 房东的猫livehouse/音乐节 -->
  11. {{ networkStore.rtcMap.get(roomId)?.rtcStatus }}
  12. </div>
  13. <div class="bottom">
  14. <span class="tag">UP 3</span>
  15. up名字
  16. </div>
  17. </div>
  18. </div>
  19. <div class="other">
  20. <div class="top">
  21. <span class="item">
  22. <i class="ico"></i>
  23. <span>直播间管理</span>
  24. </span>
  25. <span class="item">
  26. <i class="ico"></i>
  27. <span>1人看过</span>
  28. </span>
  29. <span class="item">
  30. <i class="ico"></i>
  31. <span>分享</span>
  32. </span>
  33. </div>
  34. <div class="bottom">关注量:5</div>
  35. </div>
  36. </div>
  37. <div class="video-wrap">
  38. <video
  39. id="localVideo"
  40. ref="localVideoRef"
  41. autoplay
  42. webkit-playsinline="true"
  43. playsinline
  44. x-webkit-airplay="allow"
  45. x5-video-player-type="h5"
  46. x5-video-player-fullscreen="true"
  47. x5-video-orientation="portraint"
  48. :muted="muted"
  49. controls
  50. ></video>
  51. </div>
  52. <div class="gift">
  53. <div
  54. v-for="(item, index) in giftList"
  55. :key="index"
  56. class="item"
  57. >
  58. <div class="ico"></div>
  59. <div class="name">{{ item.name }}</div>
  60. <div class="price">{{ item.price }}</div>
  61. </div>
  62. </div>
  63. </div>
  64. <div class="right">
  65. <div class="tab">
  66. <span>在线用户</span>
  67. <span> | </span>
  68. <span>大航海</span>
  69. </div>
  70. <div class="user-list">
  71. <div
  72. v-for="(item, index) in userList"
  73. :key="index"
  74. class="item"
  75. >
  76. <div class="info">
  77. <div class="avatar"></div>
  78. <div class="nickname">{{ item.nickname }}</div>
  79. </div>
  80. <div class="expr">{{ item.expr }}</div>
  81. </div>
  82. </div>
  83. <div class="msg-list">
  84. <div
  85. v-for="(item, index) in msgList"
  86. :key="index"
  87. class="item"
  88. >
  89. <span class="name">{{ item.nickname }}:</span>
  90. <span class="msg">{{ item.msg }}</span>
  91. </div>
  92. </div>
  93. <div class="send-msg">
  94. <textarea class="ipt"></textarea>
  95. <div class="btn">发送</div>
  96. </div>
  97. </div>
  98. </div>
  99. </template>
  100. <script lang="ts" setup>
  101. import { onMounted, ref, watch } from 'vue';
  102. import { useRoute } from 'vue-router';
  103. import { liveTypeEnum } from '@/interface';
  104. import { WebRTCClass } from '@/network/webRtc';
  105. import {
  106. WebSocketClass,
  107. WsConnectStatusEnum,
  108. WsMsgTypeEnum,
  109. } from '@/network/webSocket';
  110. import { useAppStore } from '@/store/app';
  111. import { useNetworkStore } from '@/store/network';
  112. const networkStore = useNetworkStore();
  113. const roomIdRef = ref<HTMLInputElement>();
  114. const joinRef = ref<HTMLButtonElement>();
  115. const leaveRef = ref<HTMLButtonElement>();
  116. const roomId = ref<string>('19990507');
  117. const websocketInstant = ref<WebSocketClass>();
  118. // const userList = ref<{ id: string; rooms: string[] }[]>([]);
  119. const muted = ref(true);
  120. const localVideoRef = ref<HTMLVideoElement>();
  121. const localStream = ref();
  122. const currType = ref(liveTypeEnum.camera); // 1:摄像头,2:录屏
  123. const id = ref('');
  124. const route = useRoute();
  125. const appStore = useAppStore();
  126. const isAdmin = ref(route.query.id === '1234');
  127. const giftList = ref([
  128. { name: '鲜花', ico: '', price: '免费' },
  129. { name: '肥宅水', ico: '', price: '2元' },
  130. { name: '小鸡腿', ico: '', price: '3元' },
  131. { name: '大鸡腿', ico: '', price: '5元' },
  132. { name: '一杯咖啡', ico: '', price: '10元' },
  133. ]);
  134. const msgList = ref([
  135. { nickname: '鲜花', msg: '423425' },
  136. { nickname: '肥宅水', msg: 'sdgdsgsg' },
  137. { nickname: '小鸡腿', msg: '63463gsd' },
  138. { nickname: '大鸡腿', msg: '46326fb26' },
  139. { nickname: '一杯咖啡', msg: 'shgd544' },
  140. { nickname: 'sdsg', msg: 'shgd544' },
  141. { nickname: 'gdsg', msg: 'we' },
  142. { nickname: 'sgdx', msg: 'shgd544' },
  143. { nickname: 'gsdx', msg: 'ew' },
  144. { nickname: 'gs', msg: 'etew' },
  145. { nickname: 'gwe', msg: 'shgd544' },
  146. { nickname: 'tewtwe', msg: 'shgd544' },
  147. { nickname: 'hdfh', msg: 'ew' },
  148. { nickname: '534', msg: 'etew' },
  149. { nickname: '234232', msg: 'shgd544' },
  150. ]);
  151. const userList = ref([
  152. { nickname: '鲜花', avatar: '423425', expr: 100 },
  153. { nickname: '肥宅水', avatar: 'sdgdsgsg', expr: 100 },
  154. { nickname: '小鸡腿', avatar: '63463gsd', expr: 100 },
  155. { nickname: '大鸡腿', avatar: '46326fb26', expr: 100 },
  156. { nickname: '一杯咖啡', avatar: 'shgd544', expr: 100 },
  157. ]);
  158. interface IOffer {
  159. socketId: string;
  160. roomId: string;
  161. data: {
  162. sdp: any;
  163. };
  164. isAdmin: boolean;
  165. }
  166. interface ICandidate {
  167. socketId: string;
  168. roomId: string;
  169. data: {
  170. candidate: string;
  171. sdpMid: string | null;
  172. sdpMLineIndex: number | null;
  173. };
  174. }
  175. onMounted(() => {
  176. id.value = route.query.id as string;
  177. websocketInstant.value = new WebSocketClass({
  178. roomId: roomId.value,
  179. url:
  180. process.env.NODE_ENV === 'development'
  181. ? 'ws://localhost:4300'
  182. : 'wss://live.hsslive.cn',
  183. isAdmin: isAdmin.value,
  184. });
  185. websocketInstant.value.update();
  186. initReceive();
  187. sendJoin();
  188. localVideoRef.value?.addEventListener('loadstart', () => {
  189. console.warn('视频流-loadstart');
  190. const rtc = networkStore.rtcMap.get(roomId.value);
  191. if (!rtc) return;
  192. rtc.rtcStatus.loadstart = true;
  193. rtc.update();
  194. });
  195. localVideoRef.value?.addEventListener('loadedmetadata', () => {
  196. console.warn('视频流-loadedmetadata');
  197. const rtc = networkStore.rtcMap.get(roomId.value);
  198. if (!rtc) return;
  199. rtc.rtcStatus.loadedmetadata = true;
  200. rtc.update();
  201. setTimeout(async () => {
  202. if (isAdmin.value) {
  203. console.warn('发送管理员正在直播消息');
  204. websocketInstant.value?.send({
  205. msgType: WsMsgTypeEnum.adminIn,
  206. data: { socketId: getSocketId(), roomId: roomId.value },
  207. });
  208. await sendOffer();
  209. }
  210. }, 100);
  211. });
  212. });
  213. watch(
  214. () => appStore.liveStatus,
  215. (newVal) => {
  216. if (newVal) {
  217. console.log('开始直播');
  218. join();
  219. }
  220. }
  221. );
  222. function getSocketId() {
  223. return networkStore.wsMap.get(roomId.value!)?.socketIo?.id;
  224. }
  225. function sendJoin() {
  226. const instance = networkStore.wsMap.get(roomId.value);
  227. if (!instance) return;
  228. instance.send({ msgType: WsMsgTypeEnum.join, data: {} });
  229. }
  230. async function join() {
  231. console.log('join的房间号', roomId.value);
  232. if (!roomId.value) {
  233. console.error('房间号不能为空!');
  234. alert('房间号不能为空!');
  235. return;
  236. }
  237. if (isAdmin.value) {
  238. try {
  239. if (currType.value === liveTypeEnum.camera) {
  240. await startMediaDevices();
  241. } else if (currType.value === liveTypeEnum.screen) {
  242. await startGetDisplayMedia();
  243. }
  244. } catch (error) {
  245. console.log('用户拒绝', error);
  246. }
  247. }
  248. }
  249. function initReceive() {
  250. const instance = websocketInstant.value;
  251. if (!instance?.socketIo) return;
  252. // websocket连接成功
  253. instance.socketIo.on(WsConnectStatusEnum.connect, () => {
  254. console.log('【websocket】websocket连接成功', instance.socketIo?.id);
  255. if (!instance) return;
  256. instance.status = WsConnectStatusEnum.connect;
  257. instance.update();
  258. });
  259. // websocket连接断开
  260. instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
  261. console.log('【websocket】websocket连接断开', instance);
  262. if (!instance) return;
  263. instance.status = WsConnectStatusEnum.disconnect;
  264. instance.update();
  265. });
  266. // 当前所有在线用户
  267. instance.socketIo.on(WsMsgTypeEnum.adminIn, (data) => {
  268. console.log('【websocket】收到管理员正在直播', data);
  269. sendOffer();
  270. });
  271. // 当前所有在线用户
  272. instance.socketIo.on(WsMsgTypeEnum.liveUser, (data) => {
  273. console.log('【websocket】当前所有在线用户');
  274. if (!instance) return;
  275. userList.value = data;
  276. });
  277. // 收到offer
  278. instance.socketIo.on(WsMsgTypeEnum.offer, async (data: IOffer) => {
  279. console.warn('【websocket】收到offer', data);
  280. if (!instance) return;
  281. if (data.socketId !== getSocketId()) {
  282. const rtc = networkStore.rtcMap.get(roomId.value);
  283. if (!rtc) return;
  284. console.log('收到offer,并且这个offer不是我发的', data);
  285. await rtc.setRemoteDescription(data.data.sdp);
  286. const sdp = await rtc.createAnswer();
  287. console.warn('【websocket】发送answer', sdp);
  288. websocketInstant.value?.send({
  289. msgType: WsMsgTypeEnum.answer,
  290. data: { sdp },
  291. });
  292. } else {
  293. console.log('收到offer,并且这个offer是我发的');
  294. }
  295. });
  296. // 收到answer
  297. instance.socketIo.on(WsMsgTypeEnum.answer, async (data: IOffer) => {
  298. console.warn('【websocket】收到answer', data);
  299. if (!instance) return;
  300. // if (!networkStore.rtcMap.get(roomId.value)?.rtcStatus.createOffer) return;
  301. if (data.socketId !== getSocketId()) {
  302. console.log('不是我发的answer');
  303. const rtc = networkStore.rtcMap.get(roomId.value);
  304. if (!rtc) return;
  305. // await rtc.setRemoteDescription(data.data.sdp);
  306. // const sdp = await rtc.createAnswer();
  307. // console.warn('【websocket】发送answer', sdp);
  308. // websocketInstant.value?.send({
  309. // msgType: WsMsgTypeEnum.answer,
  310. // data: { sdp },
  311. // });
  312. } else {
  313. console.log('是我发的answer');
  314. }
  315. });
  316. // 收到candidate
  317. instance.socketIo.on(WsMsgTypeEnum.candidate, (data: ICandidate) => {
  318. if (!instance) return;
  319. console.warn('【websocket】收到candidate', data);
  320. const rtc = networkStore.rtcMap.get(roomId.value);
  321. if (!rtc) return;
  322. if (data.socketId !== getSocketId()) {
  323. console.log('不是我发的candidate');
  324. const candidate = new RTCIceCandidate({
  325. sdpMid: data.data.sdpMid,
  326. sdpMLineIndex: data.data.sdpMLineIndex,
  327. candidate: data.data.candidate,
  328. });
  329. rtc.peerConnection
  330. ?.addIceCandidate(candidate)
  331. .then(() => {
  332. console.log('candidate成功');
  333. rtc.rtcStatus.icecandidate = true;
  334. rtc.update();
  335. })
  336. .catch((err) => {
  337. console.error('candidate失败', err);
  338. });
  339. } else {
  340. console.log('是我发的candidate');
  341. }
  342. });
  343. // 用户加入房间
  344. instance.socketIo.on(WsMsgTypeEnum.join, (data) => {
  345. console.log('【websocket】用户加入房间', data);
  346. if (!instance) return;
  347. });
  348. // 用户加入房间
  349. instance.socketIo.on(WsMsgTypeEnum.joined, (data) => {
  350. console.log('【websocket】用户加入房间完成', data);
  351. if (!instance) return;
  352. console.warn('开始new WebRTCClass');
  353. const rtc = new WebRTCClass({ roomId: roomId.value });
  354. rtc.rtcStatus.joined = true;
  355. rtc.update();
  356. });
  357. // 其他用户加入房间
  358. instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
  359. console.log('【websocket】其他用户加入房间', data);
  360. if (!instance) return;
  361. });
  362. // 用户离开房间
  363. instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
  364. console.log('【websocket】用户离开房间', data);
  365. if (!instance) return;
  366. instance.socketIo?.emit(WsMsgTypeEnum.leave, {
  367. roomId: instance.roomId,
  368. });
  369. });
  370. // 用户离开房间完成
  371. instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
  372. console.log('【websocket】用户离开房间完成', data);
  373. if (!instance) return;
  374. instance.close();
  375. });
  376. }
  377. async function startMediaDevices() {
  378. currType.value = liveTypeEnum.camera;
  379. // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
  380. const event = await navigator.mediaDevices.getUserMedia({
  381. video: true,
  382. audio: true,
  383. });
  384. console.log('getUserMedia成功', event);
  385. if (!localVideoRef.value) return;
  386. localVideoRef.value.srcObject = event;
  387. localStream.value = event;
  388. localStream.value.getTracks().forEach((track) => {
  389. networkStore.rtcMap.get(roomId.value)?.addTrack(track, localStream.value);
  390. });
  391. }
  392. async function startGetDisplayMedia() {
  393. currType.value = liveTypeEnum.screen;
  394. // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
  395. const event = await navigator.mediaDevices.getDisplayMedia({
  396. video: true,
  397. audio: true,
  398. });
  399. console.log('getDisplayMedia成功', event);
  400. if (!localVideoRef.value) return;
  401. localVideoRef.value.srcObject = event;
  402. localStream.value = event;
  403. localStream.value.getTracks().forEach((track) => {
  404. console.log(track, networkStore.rtcMap.get(roomId.value));
  405. networkStore.rtcMap.get(roomId.value)?.addTrack(track, localStream.value);
  406. });
  407. }
  408. async function sendOffer() {
  409. if (!websocketInstant.value) return;
  410. const rtc = networkStore.rtcMap.get(roomId.value);
  411. if (!rtc) return;
  412. const sdp = await rtc.createOffer();
  413. await rtc.setLocalDescription(sdp);
  414. console.warn('【websocket】发送offer', sdp);
  415. websocketInstant.value.send({
  416. msgType: WsMsgTypeEnum.offer,
  417. data: { sdp },
  418. });
  419. }
  420. function leave() {
  421. if (joinRef.value && leaveRef.value && roomIdRef.value) {
  422. roomIdRef.value.disabled = false;
  423. joinRef.value.disabled = false;
  424. leaveRef.value.disabled = true;
  425. }
  426. if (!websocketInstant.value) return;
  427. websocketInstant.value.socketIo?.emit(WsMsgTypeEnum.leave, {
  428. roomId: websocketInstant.value.roomId,
  429. });
  430. }
  431. </script>
  432. <style lang="scss" scoped>
  433. .home-wrap {
  434. display: flex;
  435. justify-content: space-between;
  436. margin: 20px auto 0;
  437. min-width: 1200px;
  438. width: 80%;
  439. .left {
  440. min-width: 1000px;
  441. border-radius: 10px;
  442. background-color: white;
  443. color: #9499a0;
  444. .head {
  445. display: flex;
  446. justify-content: space-between;
  447. padding: 20px;
  448. .tag {
  449. display: inline-block;
  450. margin-right: 5px;
  451. padding: 1px 4px;
  452. border: 1px solid;
  453. border-radius: 2px;
  454. color: #9499a0;
  455. font-size: 12px;
  456. }
  457. .info {
  458. display: flex;
  459. align-items: center;
  460. .avatar {
  461. margin-right: 20px;
  462. width: 64px;
  463. height: 64px;
  464. border-radius: 50%;
  465. background-color: yellow;
  466. }
  467. .detail {
  468. .top {
  469. margin-bottom: 10px;
  470. color: #18191c;
  471. }
  472. .bottom {
  473. font-size: 14px;
  474. }
  475. }
  476. }
  477. .other {
  478. display: flex;
  479. flex-direction: column;
  480. justify-content: center;
  481. font-size: 12px;
  482. .top {
  483. display: flex;
  484. align-items: center;
  485. .item {
  486. display: flex;
  487. align-items: center;
  488. margin-right: 20px;
  489. .ico {
  490. display: inline-block;
  491. margin-right: 4px;
  492. width: 10px;
  493. height: 10px;
  494. border-radius: 50%;
  495. background-color: skyblue;
  496. }
  497. }
  498. }
  499. .bottom {
  500. margin-top: 10px;
  501. }
  502. }
  503. }
  504. .video-wrap {
  505. height: 500px;
  506. background-color: #18191c;
  507. #localVideo {
  508. width: 100%;
  509. height: 100%;
  510. }
  511. }
  512. .gift {
  513. display: flex;
  514. align-items: center;
  515. justify-content: space-between;
  516. padding: 10px 20px;
  517. background-color: white;
  518. .item {
  519. margin-right: 10px;
  520. text-align: center;
  521. .ico {
  522. width: 50px;
  523. height: 50px;
  524. background-color: skyblue;
  525. }
  526. .name {
  527. color: #18191c;
  528. font-size: 12px;
  529. }
  530. .price {
  531. color: #9499a0;
  532. font-size: 12px;
  533. }
  534. }
  535. }
  536. }
  537. .right {
  538. position: relative;
  539. box-sizing: border-box;
  540. min-width: 300px;
  541. border-radius: 10px;
  542. background-color: white;
  543. color: #9499a0;
  544. .tab {
  545. display: flex;
  546. align-items: center;
  547. justify-content: space-evenly;
  548. padding: 5px 0;
  549. font-size: 12px;
  550. }
  551. .user-list {
  552. overflow-y: scroll;
  553. padding: 0 15px;
  554. height: 100px;
  555. .item {
  556. display: flex;
  557. align-items: center;
  558. justify-content: space-between;
  559. margin-bottom: 10px;
  560. font-size: 12px;
  561. .info {
  562. display: flex;
  563. align-items: center;
  564. .avatar {
  565. margin-right: 5px;
  566. width: 25px;
  567. height: 25px;
  568. border-radius: 50%;
  569. background-color: skyblue;
  570. }
  571. .nickname {
  572. color: black;
  573. }
  574. }
  575. }
  576. }
  577. .msg-list {
  578. overflow-y: scroll;
  579. padding: 0 15px;
  580. height: 350px;
  581. .item {
  582. margin-bottom: 10px;
  583. font-size: 12px;
  584. .name {
  585. color: #9499a0;
  586. }
  587. .msg {
  588. color: #61666d;
  589. }
  590. }
  591. }
  592. .send-msg {
  593. position: absolute;
  594. bottom: 15px;
  595. box-sizing: border-box;
  596. padding: 0 10px;
  597. width: 100%;
  598. .ipt {
  599. display: block;
  600. box-sizing: border-box;
  601. margin: 0 auto;
  602. padding: 10px;
  603. width: 100%;
  604. height: 60px;
  605. outline: none;
  606. border: 1px solid hsla(0, 0%, 60%, 0.2);
  607. border-radius: 4px;
  608. background-color: #f1f2f3;
  609. font-size: 14px;
  610. }
  611. .btn {
  612. box-sizing: border-box;
  613. margin-top: 10px;
  614. margin-left: auto;
  615. padding: 5px;
  616. width: 80px;
  617. border-radius: 4px;
  618. background-color: #23ade5;
  619. color: white;
  620. text-align: center;
  621. font-size: 12px;
  622. }
  623. }
  624. }
  625. }
  626. </style>