use-push.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. import { getRandomString, windowReload } from 'billd-utils';
  2. import {
  3. Ref,
  4. nextTick,
  5. onMounted,
  6. onUnmounted,
  7. reactive,
  8. ref,
  9. watch,
  10. } from 'vue';
  11. import { useRoute, useRouter } from 'vue-router';
  12. import { fetchRtcV1Publish } from '@/api/srs';
  13. import {
  14. fetchCreateUserLiveRoom,
  15. fetchUserHasLiveRoom,
  16. } from '@/api/userLiveRoom';
  17. import { WEBSOCKET_URL } from '@/constant';
  18. import {
  19. DanmuMsgTypeEnum,
  20. IAnswer,
  21. ICandidate,
  22. IDanmu,
  23. IHeartbeat,
  24. IJoin,
  25. ILiveUser,
  26. IMessage,
  27. IOffer,
  28. IOtherJoin,
  29. LiveRoomTypeEnum,
  30. MediaTypeEnum,
  31. } from '@/interface';
  32. import { SRSWebRTCClass } from '@/network/srsWebRtc';
  33. import { WebRTCClass } from '@/network/webRtc';
  34. import {
  35. WebSocketClass,
  36. WsConnectStatusEnum,
  37. WsMsgTypeEnum,
  38. prettierReceiveWebsocket,
  39. } from '@/network/webSocket';
  40. import { useNetworkStore } from '@/store/network';
  41. import { useUserStore } from '@/store/user';
  42. import { loginTip } from './use-login';
  43. import { useTip } from './use-tip';
  44. export function usePush({
  45. localVideoRef,
  46. remoteVideoRef,
  47. isSRS,
  48. }: {
  49. localVideoRef: Ref<HTMLVideoElement | undefined>;
  50. remoteVideoRef: Ref<HTMLVideoElement[]>;
  51. isSRS?: boolean;
  52. }) {
  53. const route = useRoute();
  54. const router = useRouter();
  55. const userStore = useUserStore();
  56. const networkStore = useNetworkStore();
  57. const heartbeatTimer = ref();
  58. const roomId = ref('-1');
  59. const roomName = ref('');
  60. const danmuStr = ref('');
  61. const isDone = ref(false);
  62. const joined = ref(false);
  63. const disabled = ref(false);
  64. const localStream = ref();
  65. const offerSended = ref(new Set());
  66. const webRTC = ref<WebRTCClass | SRSWebRTCClass>();
  67. const maxBitrate = ref([
  68. {
  69. label: '1000',
  70. value: 1000,
  71. },
  72. {
  73. label: '2000',
  74. value: 2000,
  75. },
  76. {
  77. label: '3000',
  78. value: 3000,
  79. },
  80. {
  81. label: '4000',
  82. value: 4000,
  83. },
  84. {
  85. label: '5000',
  86. value: 5000,
  87. },
  88. {
  89. label: '6000',
  90. value: 6000,
  91. },
  92. {
  93. label: '7000',
  94. value: 7000,
  95. },
  96. {
  97. label: '8000',
  98. value: 8000,
  99. },
  100. {
  101. label: '9000',
  102. value: 9000,
  103. },
  104. {
  105. label: '10000',
  106. value: 10000,
  107. },
  108. ]);
  109. const currentMaxBitrate = ref(maxBitrate.value[0].value);
  110. const resolutionRatio = ref([
  111. {
  112. label: '1440P',
  113. value: 1440,
  114. },
  115. {
  116. label: '1080P',
  117. value: 1080,
  118. },
  119. {
  120. label: '720P',
  121. value: 720,
  122. },
  123. {
  124. label: '360P',
  125. value: 360,
  126. },
  127. ]);
  128. const currentResolutionRatio = ref(resolutionRatio.value[1].value);
  129. const track = reactive({
  130. audio: 1,
  131. video: 1,
  132. });
  133. const streamurl = ref('');
  134. const damuList = ref<IDanmu[]>([]);
  135. const liveUserList = ref<ILiveUser[]>([]);
  136. const allMediaTypeList = {
  137. [MediaTypeEnum.camera]: {
  138. type: MediaTypeEnum.camera,
  139. txt: '摄像头',
  140. },
  141. [MediaTypeEnum.screen]: {
  142. type: MediaTypeEnum.screen,
  143. txt: '窗口',
  144. },
  145. };
  146. const currMediaTypeList = ref<
  147. {
  148. type: MediaTypeEnum;
  149. txt: string;
  150. }[]
  151. >([]);
  152. const currMediaType = ref<{
  153. type: MediaTypeEnum;
  154. txt: string;
  155. }>();
  156. watch(
  157. () => currentMaxBitrate.value,
  158. async (newVal) => {
  159. const res = await webRTC.value?.setMaxBitrate(newVal);
  160. if (res === 1) {
  161. window.$message.success('切换码率成功!');
  162. } else {
  163. window.$message.success('切换码率失败!');
  164. }
  165. }
  166. );
  167. watch(
  168. () => currentResolutionRatio.value,
  169. async (newVal) => {
  170. const res = await webRTC.value?.setResolutionRatio(newVal);
  171. if (res === 1) {
  172. window.$message.success('切换分辨率成功!');
  173. } else {
  174. window.$message.success('切换分辨率失败!');
  175. }
  176. }
  177. );
  178. watch(
  179. () => userStore.userInfo,
  180. async (newVal) => {
  181. if (newVal) {
  182. const res = await userHasLiveRoom();
  183. if (!res) {
  184. await useTip('你还没有直播间,是否立即开通?');
  185. await handleCreateUserLiveRoom();
  186. } else {
  187. const rtmpUrl = newVal.live_rooms![0]!.rtmp_url!.replace(
  188. 'rtmp',
  189. 'webrtc'
  190. );
  191. streamurl.value = rtmpUrl;
  192. }
  193. }
  194. },
  195. { immediate: true }
  196. );
  197. onMounted(() => {
  198. roomId.value = route.query.roomId as string;
  199. if (!loginTip()) return;
  200. });
  201. onUnmounted(() => {
  202. clearInterval(heartbeatTimer.value);
  203. closeWs();
  204. closeRtc();
  205. });
  206. function closeWs() {
  207. const instance = networkStore.wsMap.get(roomId.value);
  208. instance?.close();
  209. }
  210. function closeRtc() {
  211. networkStore.rtcMap.forEach((rtc) => {
  212. rtc.close();
  213. });
  214. }
  215. async function userHasLiveRoom() {
  216. const res = await fetchUserHasLiveRoom(userStore.userInfo?.id!);
  217. if (res.code === 200 && res.data) {
  218. roomName.value = res.data.live_room?.name || '';
  219. roomId.value = `${res.data.live_room?.id || -1}`;
  220. router.push({ query: { ...route.query, roomId: roomId.value } });
  221. return true;
  222. }
  223. return false;
  224. }
  225. async function handleCreateUserLiveRoom() {
  226. try {
  227. const res = await fetchCreateUserLiveRoom();
  228. if (res.code === 200) {
  229. window.$message.success('开通直播间成功!');
  230. setTimeout(() => {
  231. windowReload();
  232. }, 500);
  233. }
  234. } catch (error) {
  235. console.log(error);
  236. }
  237. }
  238. async function startLive() {
  239. if (!loginTip()) return;
  240. const flag = await userHasLiveRoom();
  241. if (!flag) {
  242. await useTip('你还没有直播间,是否立即开通?');
  243. await handleCreateUserLiveRoom();
  244. return;
  245. }
  246. if (!roomNameIsOk()) return;
  247. if (currMediaTypeList.value.length <= 0) {
  248. window.$message.warning('请选择一个素材!');
  249. return;
  250. }
  251. disabled.value = true;
  252. const instance = new WebSocketClass({
  253. roomId: roomId.value,
  254. url: WEBSOCKET_URL,
  255. isAnchor: true,
  256. });
  257. instance.update();
  258. initReceive();
  259. }
  260. /** 原生的webrtc时,receiver必传 */
  261. async function startNewWebRtc({
  262. receiver,
  263. videoEl = localVideoRef.value!,
  264. }: {
  265. receiver?: string;
  266. videoEl?: HTMLVideoElement;
  267. }) {
  268. if (isSRS) {
  269. console.warn('开始new SRSWebRTCClass', `${roomId.value}___${receiver!}`);
  270. const rtc = new SRSWebRTCClass({
  271. roomId: `${roomId.value}___${getSocketId()}`,
  272. videoEl,
  273. maxBitrate: currentMaxBitrate.value,
  274. resolutionRatio: currentResolutionRatio.value,
  275. });
  276. webRTC.value = rtc;
  277. localStream.value.getTracks().forEach((track) => {
  278. rtc.addTrack({
  279. track,
  280. stream: localStream.value,
  281. });
  282. });
  283. try {
  284. const offer = await rtc.createOffer();
  285. if (!offer) return;
  286. await rtc.setLocalDescription(offer);
  287. const res = await fetchRtcV1Publish({
  288. api: `/rtc/v1/publish/`,
  289. clientip: null,
  290. sdp: offer.sdp!,
  291. streamurl: userStore.userInfo!.live_rooms![0]!.rtmp_url!.replace(
  292. 'rtmp',
  293. 'webrtc'
  294. ),
  295. tid: getRandomString(10),
  296. });
  297. await rtc.setRemoteDescription(res.data.sdp);
  298. } catch (error) {
  299. console.log(error);
  300. }
  301. } else {
  302. console.warn('开始new WebRTCClass', `${roomId.value}___${receiver!}`);
  303. const rtc = new WebRTCClass({
  304. roomId: `${roomId.value}___${receiver!}`,
  305. videoEl,
  306. });
  307. webRTC.value = rtc;
  308. return rtc;
  309. }
  310. }
  311. function handleCoverImg() {
  312. const canvas = document.createElement('canvas');
  313. const { width, height } = localVideoRef.value!.getBoundingClientRect();
  314. const rate = width / height;
  315. const coverWidth = width * 0.5;
  316. const coverHeight = coverWidth / rate;
  317. canvas.width = coverWidth;
  318. canvas.height = coverHeight;
  319. canvas
  320. .getContext('2d')!
  321. .drawImage(localVideoRef.value!, 0, 0, coverWidth, coverHeight);
  322. // webp比png的体积小非常多!因此coverWidth就可以不用压缩太夸张
  323. const dataURL = canvas.toDataURL('image/webp');
  324. return dataURL;
  325. }
  326. function handleHeartbeat(liveId: number) {
  327. heartbeatTimer.value = setInterval(() => {
  328. const instance = networkStore.wsMap.get(roomId.value);
  329. if (!instance) return;
  330. const heartbeatData: IHeartbeat['data'] = {
  331. live_id: liveId,
  332. live_room_id: Number(roomId.value),
  333. };
  334. instance.send({
  335. msgType: WsMsgTypeEnum.heartbeat,
  336. data: heartbeatData,
  337. });
  338. }, 1000 * 5);
  339. }
  340. function addTrack() {
  341. if (!localStream.value) return;
  342. liveUserList.value.forEach((item) => {
  343. if (item.id !== getSocketId()) {
  344. localStream.value.getTracks().forEach((track) => {
  345. const rtc = networkStore.getRtcMap(`${roomId.value}___${item.id}`);
  346. // rtc?.addTransceiver(track, localStream.value);
  347. rtc?.addTrack(track, localStream.value);
  348. });
  349. }
  350. });
  351. }
  352. function sendJoin() {
  353. const instance = networkStore.wsMap.get(roomId.value);
  354. if (!instance) return;
  355. const joinData: IJoin['data'] = {
  356. live_room: {
  357. id: Number(roomId.value),
  358. name: roomName.value,
  359. cover_img: handleCoverImg(),
  360. type: isSRS ? LiveRoomTypeEnum.user_srs : LiveRoomTypeEnum.user_wertc,
  361. },
  362. track,
  363. };
  364. instance.send({
  365. msgType: WsMsgTypeEnum.join,
  366. data: joinData,
  367. });
  368. }
  369. async function sendOffer({
  370. sender,
  371. receiver,
  372. }: {
  373. sender: string;
  374. receiver: string;
  375. }) {
  376. if (isDone.value) return;
  377. const instance = networkStore.wsMap.get(roomId.value);
  378. if (!instance) return;
  379. const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
  380. if (!rtc) return;
  381. const sdp = await rtc.createOffer();
  382. await rtc.setLocalDescription(sdp);
  383. instance.send({
  384. msgType: WsMsgTypeEnum.offer,
  385. data: {
  386. sdp,
  387. sender,
  388. receiver,
  389. live_room_id: roomId.value,
  390. },
  391. });
  392. }
  393. function batchSendOffer() {
  394. console.log('batchSendOffer');
  395. liveUserList.value.forEach(async (item) => {
  396. console.log(item, 'liveUserList');
  397. const socketId = item.id;
  398. if (!offerSended.value.has(socketId) && socketId !== getSocketId()) {
  399. const rtc = networkStore.getRtcMap(`${roomId.value}___${socketId}`);
  400. if (!rtc) {
  401. await startNewWebRtc({
  402. receiver: socketId,
  403. videoEl: localVideoRef.value,
  404. });
  405. }
  406. await addTrack();
  407. console.log('执行sendOffer', {
  408. sender: getSocketId(),
  409. receiver: socketId,
  410. });
  411. sendOffer({ sender: getSocketId(), receiver: socketId });
  412. offerSended.value.add(socketId);
  413. }
  414. });
  415. }
  416. function getSocketId() {
  417. return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
  418. }
  419. function initReceive() {
  420. const instance = networkStore.wsMap.get(roomId.value);
  421. if (!instance?.socketIo) return;
  422. // websocket连接成功
  423. instance.socketIo.on(WsConnectStatusEnum.connect, () => {
  424. prettierReceiveWebsocket(WsConnectStatusEnum.connect);
  425. if (!instance) return;
  426. instance.status = WsConnectStatusEnum.connect;
  427. instance.update();
  428. sendJoin();
  429. });
  430. // websocket连接断开
  431. instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
  432. prettierReceiveWebsocket(WsConnectStatusEnum.disconnect, instance);
  433. if (!instance) return;
  434. instance.status = WsConnectStatusEnum.disconnect;
  435. instance.update();
  436. });
  437. // 收到offer
  438. instance.socketIo.on(WsMsgTypeEnum.offer, (data: IOffer) => {
  439. prettierReceiveWebsocket(
  440. WsMsgTypeEnum.offer,
  441. `发送者:${data.data.sender},接收者:${data.data.receiver}`,
  442. data
  443. );
  444. if (isSRS) return;
  445. if (!instance) return;
  446. if (data.data.receiver === getSocketId()) {
  447. console.log('收到offer,这个offer是发给我的');
  448. nextTick(async () => {
  449. const rtc = await startNewWebRtc({
  450. receiver: data.data.sender,
  451. videoEl: remoteVideoRef.value[data.data.sender],
  452. });
  453. if (rtc) {
  454. await rtc.setRemoteDescription(data.data.sdp);
  455. const sdp = await rtc.createAnswer();
  456. await rtc.setLocalDescription(sdp);
  457. const answerData: IAnswer = {
  458. sdp,
  459. sender: getSocketId(),
  460. receiver: data.data.sender,
  461. live_room_id: data.data.live_room_id,
  462. };
  463. instance.send({
  464. msgType: WsMsgTypeEnum.answer,
  465. data: answerData,
  466. });
  467. }
  468. });
  469. } else {
  470. console.log('收到offer,但是这个offer不是发给我的');
  471. }
  472. });
  473. // 收到answer
  474. instance.socketIo.on(WsMsgTypeEnum.answer, async (data: IOffer) => {
  475. prettierReceiveWebsocket(
  476. WsMsgTypeEnum.answer,
  477. `发送者:${data.data.sender},接收者:${data.data.receiver}`,
  478. data
  479. );
  480. if (isSRS) return;
  481. if (isDone.value) return;
  482. if (!instance) return;
  483. const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socket_id}`);
  484. if (!rtc) return;
  485. rtc.rtcStatus.answer = true;
  486. rtc.update();
  487. if (data.data.receiver === getSocketId()) {
  488. console.log('收到answer,这个answer是发给我的');
  489. await rtc.setRemoteDescription(data.data.sdp);
  490. } else {
  491. console.log('收到answer,但这个answer不是发给我的');
  492. }
  493. });
  494. // 收到candidate
  495. instance.socketIo.on(WsMsgTypeEnum.candidate, (data: ICandidate) => {
  496. prettierReceiveWebsocket(
  497. WsMsgTypeEnum.candidate,
  498. `发送者:${data.data.sender},接收者:${data.data.receiver}`,
  499. data
  500. );
  501. if (isSRS) return;
  502. if (isDone.value) return;
  503. if (!instance) return;
  504. const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socket_id}`);
  505. if (!rtc) return;
  506. if (data.socket_id !== getSocketId()) {
  507. console.log('不是我发的candidate');
  508. const candidate = new RTCIceCandidate({
  509. sdpMid: data.data.sdpMid,
  510. sdpMLineIndex: data.data.sdpMLineIndex,
  511. candidate: data.data.candidate,
  512. });
  513. rtc.peerConnection
  514. ?.addIceCandidate(candidate)
  515. .then(() => {
  516. console.log('candidate成功');
  517. })
  518. .catch((err) => {
  519. console.error('candidate失败', err);
  520. });
  521. } else {
  522. console.log('是我发的candidate');
  523. }
  524. });
  525. // 管理员正在直播
  526. instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data) => {
  527. prettierReceiveWebsocket(WsMsgTypeEnum.roomLiveing, data);
  528. });
  529. // 当前所有在线用户
  530. instance.socketIo.on(WsMsgTypeEnum.liveUser, (data) => {
  531. prettierReceiveWebsocket(WsMsgTypeEnum.liveUser, data);
  532. });
  533. // 收到用户发送消息
  534. instance.socketIo.on(WsMsgTypeEnum.message, (data: IMessage) => {
  535. prettierReceiveWebsocket(WsMsgTypeEnum.message, data);
  536. if (!instance) return;
  537. damuList.value.push({
  538. socket_id: data.socket_id,
  539. msgType: DanmuMsgTypeEnum.danmu,
  540. msg: data.data.msg,
  541. userInfo: data.user_info,
  542. });
  543. });
  544. // 用户加入房间完成
  545. instance.socketIo.on(WsMsgTypeEnum.joined, (data: IJoin) => {
  546. prettierReceiveWebsocket(WsMsgTypeEnum.joined, data);
  547. handleHeartbeat(data.data.live_id || -1);
  548. joined.value = true;
  549. liveUserList.value.push({
  550. id: `${getSocketId()}`,
  551. userInfo: data.user_info,
  552. });
  553. if (isSRS) {
  554. startNewWebRtc({});
  555. } else {
  556. batchSendOffer();
  557. }
  558. });
  559. // 其他用户加入房间
  560. instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data: IOtherJoin) => {
  561. prettierReceiveWebsocket(WsMsgTypeEnum.otherJoin, data);
  562. liveUserList.value.push({
  563. id: data.data.join_socket_id,
  564. userInfo: data.data.liveRoom.user,
  565. });
  566. const danmu: IDanmu = {
  567. msgType: DanmuMsgTypeEnum.otherJoin,
  568. socket_id: data.data.join_socket_id,
  569. userInfo: data.data.liveRoom.user,
  570. msg: '',
  571. };
  572. damuList.value.push(danmu);
  573. if (isSRS) return;
  574. if (joined.value) {
  575. batchSendOffer();
  576. }
  577. });
  578. // 用户离开房间
  579. instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
  580. prettierReceiveWebsocket(WsMsgTypeEnum.leave, data);
  581. if (!instance) return;
  582. instance.send({
  583. msgType: WsMsgTypeEnum.leave,
  584. data: { roomId: instance.roomId },
  585. });
  586. });
  587. // 用户离开房间完成
  588. instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
  589. prettierReceiveWebsocket(WsMsgTypeEnum.leaved, data);
  590. networkStore.rtcMap
  591. .get(`${roomId.value}___${data.socketId as string}`)
  592. ?.close();
  593. const res = liveUserList.value.filter(
  594. (item) => item.id !== data.socketId
  595. );
  596. liveUserList.value = res;
  597. damuList.value.push({
  598. socket_id: data.socketId,
  599. msgType: DanmuMsgTypeEnum.userLeaved,
  600. msg: '',
  601. });
  602. });
  603. }
  604. function roomNameIsOk() {
  605. if (!roomName.value.length) {
  606. window.$message.warning('请输入房间名!');
  607. return false;
  608. }
  609. if (roomName.value.length < 3 || roomName.value.length > 30) {
  610. window.$message.warning('房间名要求3-30个字符!');
  611. return false;
  612. }
  613. return true;
  614. }
  615. /** 摄像头 */
  616. async function startGetUserMedia() {
  617. if (!localStream.value) {
  618. // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
  619. const event = await navigator.mediaDevices.getUserMedia({
  620. video: {
  621. height: currentResolutionRatio.value,
  622. // frameRate: 30,
  623. },
  624. // video: true,
  625. audio: true,
  626. });
  627. const audio = event.getAudioTracks();
  628. const video = event.getVideoTracks();
  629. track.audio = audio.length ? 1 : 2;
  630. track.video = video.length ? 1 : 2;
  631. console.log('getUserMedia成功', event);
  632. currMediaType.value = allMediaTypeList[MediaTypeEnum.camera];
  633. currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.camera]);
  634. if (!localVideoRef.value) return;
  635. localVideoRef.value.srcObject = event;
  636. localStream.value = event;
  637. }
  638. }
  639. /** 窗口 */
  640. async function startGetDisplayMedia() {
  641. if (!localStream.value) {
  642. // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
  643. const event = await navigator.mediaDevices.getDisplayMedia({
  644. video: {
  645. height: currentResolutionRatio.value,
  646. // frameRate: 30,
  647. },
  648. // video: true,
  649. audio: true,
  650. });
  651. const audio = event.getAudioTracks();
  652. const video = event.getVideoTracks();
  653. track.audio = audio.length ? 1 : 2;
  654. track.video = video.length ? 1 : 2;
  655. console.log('getDisplayMedia成功', event);
  656. currMediaType.value = allMediaTypeList[MediaTypeEnum.screen];
  657. currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.screen]);
  658. if (!localVideoRef.value) return;
  659. localVideoRef.value.srcObject = event;
  660. localStream.value = event;
  661. }
  662. }
  663. function keydownDanmu(event: KeyboardEvent) {
  664. const key = event.key.toLowerCase();
  665. if (key === 'enter') {
  666. event.preventDefault();
  667. sendDanmu();
  668. }
  669. }
  670. function confirmRoomName() {
  671. if (!roomNameIsOk()) return;
  672. disabled.value = true;
  673. }
  674. function sendDanmu() {
  675. if (!danmuStr.value.length) {
  676. window.$message.warning('请输入弹幕内容!');
  677. return;
  678. }
  679. const instance = networkStore.wsMap.get(roomId.value);
  680. if (!instance) {
  681. window.$message.error('还没开播,不能发送弹幕!');
  682. return;
  683. }
  684. const messageData: IMessage['data'] = {
  685. msg: danmuStr.value,
  686. msgType: DanmuMsgTypeEnum.danmu,
  687. live_room_id: Number(roomId.value),
  688. };
  689. instance.send({
  690. msgType: WsMsgTypeEnum.message,
  691. data: messageData,
  692. });
  693. damuList.value.push({
  694. socket_id: getSocketId(),
  695. msgType: DanmuMsgTypeEnum.danmu,
  696. msg: danmuStr.value,
  697. userInfo: userStore.userInfo,
  698. });
  699. danmuStr.value = '';
  700. }
  701. /** 结束直播 */
  702. function endLive() {
  703. disabled.value = false;
  704. currMediaTypeList.value = [];
  705. localStream.value = null;
  706. localVideoRef.value!.srcObject = null;
  707. clearInterval(heartbeatTimer.value);
  708. const instance = networkStore.wsMap.get(roomId.value);
  709. if (instance) {
  710. instance.send({
  711. msgType: WsMsgTypeEnum.roomNoLive,
  712. data: {},
  713. });
  714. }
  715. setTimeout(() => {
  716. closeWs();
  717. closeRtc();
  718. }, 500);
  719. }
  720. async function getAllMediaDevices() {
  721. const res = await navigator.mediaDevices.enumerateDevices();
  722. // const audioInput = res.filter(
  723. // (item) => item.kind === 'audioinput' && item.deviceId !== 'default'
  724. // );
  725. // const videoInput = res.filter(
  726. // (item) => item.kind === 'videoinput' && item.deviceId !== 'default'
  727. // );
  728. return res;
  729. }
  730. async function initPush() {
  731. const all = await getAllMediaDevices();
  732. allMediaTypeList[MediaTypeEnum.camera] = {
  733. txt: all.find((item) => item.kind === 'videoinput')?.label || '摄像头',
  734. type: MediaTypeEnum.camera,
  735. };
  736. localVideoRef.value?.addEventListener('loadstart', () => {
  737. console.warn('视频流-loadstart');
  738. const rtc = networkStore.getRtcMap(roomId.value);
  739. if (!rtc) return;
  740. rtc.rtcStatus.loadstart = true;
  741. rtc.update();
  742. });
  743. localVideoRef.value?.addEventListener('loadedmetadata', () => {
  744. console.warn('视频流-loadedmetadata');
  745. const rtc = networkStore.getRtcMap(roomId.value);
  746. if (!rtc) return;
  747. rtc.rtcStatus.loadedmetadata = true;
  748. rtc.update();
  749. if (isSRS) return;
  750. if (joined.value) {
  751. batchSendOffer();
  752. }
  753. });
  754. }
  755. return {
  756. initPush,
  757. confirmRoomName,
  758. getSocketId,
  759. startGetDisplayMedia,
  760. startGetUserMedia,
  761. startLive,
  762. endLive,
  763. sendDanmu,
  764. keydownDanmu,
  765. currentResolutionRatio,
  766. currentMaxBitrate,
  767. resolutionRatio,
  768. maxBitrate,
  769. disabled,
  770. danmuStr,
  771. roomName,
  772. damuList,
  773. liveUserList,
  774. currMediaTypeList,
  775. };
  776. }