use-pull.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. import { getRandomString } from 'billd-utils';
  2. import { Ref, nextTick, onUnmounted, reactive, ref, watch } from 'vue';
  3. import { useRoute } from 'vue-router';
  4. import { fetchRtcV1Play } from '@/api/srs';
  5. import { useFlvPlay } from '@/hooks/use-play';
  6. import {
  7. DanmuMsgTypeEnum,
  8. IAdminIn,
  9. ICandidate,
  10. IDanmu,
  11. ILive,
  12. ILiveUser,
  13. IOffer,
  14. MediaTypeEnum,
  15. } from '@/interface';
  16. import { SRSWebRTCClass } from '@/network/srsWebRtc';
  17. import { WebRTCClass } from '@/network/webRtc';
  18. import {
  19. WebSocketClass,
  20. WsConnectStatusEnum,
  21. WsMsgTypeEnum,
  22. prettierReceiveWebsocket,
  23. } from '@/network/webSocket';
  24. import { useNetworkStore } from '@/store/network';
  25. import { useUserStore } from '@/store/user';
  26. export function usePull({
  27. localVideoRef,
  28. remoteVideoRef,
  29. isSRS,
  30. isFlv,
  31. }: {
  32. localVideoRef: Ref<HTMLVideoElement[]>;
  33. remoteVideoRef: Ref<HTMLVideoElement | undefined>;
  34. isSRS?: boolean;
  35. isFlv?: boolean;
  36. }) {
  37. const route = useRoute();
  38. const userStore = useUserStore();
  39. const networkStore = useNetworkStore();
  40. const heartbeatTimer = ref();
  41. const roomId = ref(route.params.roomId as string);
  42. const roomName = ref('');
  43. const userName = ref('');
  44. const userAvatar = ref('');
  45. const streamurl = ref('');
  46. const flvurl = ref('');
  47. const danmuStr = ref('');
  48. const balance = ref('0.00');
  49. const damuList = ref<IDanmu[]>([]);
  50. const liveUserList = ref<ILiveUser[]>([]);
  51. const isDone = ref(false);
  52. const roomNoLive = ref(false);
  53. const localStream = ref();
  54. const sidebarList = ref<
  55. {
  56. socketId: string;
  57. }[]
  58. >([]);
  59. const track = reactive({
  60. audio: true,
  61. video: true,
  62. });
  63. const giftList = ref([
  64. { name: '鲜花', ico: '', price: '免费' },
  65. { name: '肥宅水', ico: '', price: '2元' },
  66. { name: '小鸡腿', ico: '', price: '3元' },
  67. { name: '大鸡腿', ico: '', price: '5元' },
  68. { name: '一杯咖啡', ico: '', price: '10元' },
  69. ]);
  70. const offerSended = ref(new Set());
  71. const hooksRtcMap = ref(new Set());
  72. const sender = ref();
  73. const allMediaTypeList = {
  74. [MediaTypeEnum.camera]: {
  75. type: MediaTypeEnum.camera,
  76. txt: '摄像头',
  77. },
  78. [MediaTypeEnum.screen]: {
  79. type: MediaTypeEnum.screen,
  80. txt: '窗口',
  81. },
  82. };
  83. const currMediaTypeList = ref<
  84. {
  85. type: MediaTypeEnum;
  86. txt: string;
  87. }[]
  88. >([]);
  89. const currMediaType = ref<{
  90. type: MediaTypeEnum;
  91. txt: string;
  92. }>();
  93. onUnmounted(() => {
  94. clearInterval(heartbeatTimer.value);
  95. });
  96. /** 摄像头 */
  97. async function startGetUserMedia() {
  98. if (!localStream.value) {
  99. // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
  100. const event = await navigator.mediaDevices.getUserMedia({
  101. video: true,
  102. audio: true,
  103. });
  104. console.log('getUserMedia成功', event);
  105. currMediaType.value = allMediaTypeList[MediaTypeEnum.camera];
  106. currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.camera]);
  107. // localVideoRef.value.forEach((item) => {
  108. // item.srcObject = event;
  109. // });
  110. localStream.value = event;
  111. }
  112. }
  113. /** 窗口 */
  114. async function startGetDisplayMedia() {
  115. if (!localStream.value) {
  116. // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
  117. const event = await navigator.mediaDevices.getDisplayMedia({
  118. video: true,
  119. audio: true,
  120. });
  121. const audio = event.getAudioTracks();
  122. const video = event.getVideoTracks();
  123. track.audio = !!audio.length;
  124. track.video = !!video.length;
  125. console.log('getDisplayMedia成功', event);
  126. currMediaType.value = allMediaTypeList[MediaTypeEnum.screen];
  127. currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.screen]);
  128. localStream.value = event;
  129. }
  130. }
  131. watch(
  132. [
  133. () => userStore.userInfo,
  134. () => networkStore.wsMap.get(roomId.value)?.socketIo?.connected,
  135. ],
  136. ([userInfo, connected]) => {
  137. if (userInfo) {
  138. balance.value = userInfo.wallet?.balance || '0.00';
  139. }
  140. if (userInfo && connected) {
  141. const instance = networkStore.wsMap.get(roomId.value);
  142. if (!instance) return;
  143. instance.send({
  144. msgType: WsMsgTypeEnum.updateJoinInfo,
  145. data: {
  146. userInfo: userStore.userInfo,
  147. },
  148. });
  149. }
  150. }
  151. );
  152. function initPull() {
  153. console.warn('开始new WebSocketClass');
  154. const ws = new WebSocketClass({
  155. roomId: roomId.value,
  156. url:
  157. process.env.NODE_ENV === 'development'
  158. ? 'ws://localhost:4300'
  159. : 'wss://live.hsslive.cn',
  160. isAdmin: false,
  161. });
  162. ws.update();
  163. initReceive();
  164. remoteVideoRef.value?.addEventListener('loadstart', () => {
  165. console.warn('视频流-loadstart');
  166. const rtc = networkStore.getRtcMap(roomId.value);
  167. if (!rtc) return;
  168. rtc.rtcStatus.loadstart = true;
  169. rtc.update();
  170. });
  171. remoteVideoRef.value?.addEventListener('loadedmetadata', () => {
  172. console.warn('视频流-loadedmetadata');
  173. const rtc = networkStore.getRtcMap(roomId.value);
  174. if (!rtc) return;
  175. rtc.rtcStatus.loadedmetadata = true;
  176. rtc.update();
  177. });
  178. }
  179. function handleHeartbeat() {
  180. heartbeatTimer.value = setInterval(() => {
  181. const instance = networkStore.wsMap.get(roomId.value);
  182. if (!instance) return;
  183. instance.send({
  184. msgType: WsMsgTypeEnum.heartbeat,
  185. });
  186. }, 1000 * 5);
  187. }
  188. function closeWs() {
  189. const instance = networkStore.wsMap.get(roomId.value);
  190. instance?.close();
  191. }
  192. function closeRtc() {
  193. networkStore.rtcMap.forEach((rtc) => {
  194. rtc.close();
  195. });
  196. }
  197. function getSocketId() {
  198. return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
  199. }
  200. function sendJoin() {
  201. const instance = networkStore.wsMap.get(roomId.value);
  202. if (!instance) return;
  203. instance.send({
  204. msgType: WsMsgTypeEnum.join,
  205. data: { userInfo: userStore.userInfo },
  206. });
  207. }
  208. function addTransceiver(socketId: string) {
  209. if (!localStream.value) return;
  210. if (socketId !== getSocketId()) {
  211. localStream.value.getTracks().forEach((track) => {
  212. const rtc = networkStore.getRtcMap(`${roomId.value}___${socketId}`);
  213. rtc?.addTransceiver(track, localStream.value);
  214. });
  215. }
  216. }
  217. function addTrack() {
  218. if (!localStream.value) return;
  219. liveUserList.value.forEach((item) => {
  220. if (item.socketId !== getSocketId()) {
  221. localStream.value.getTracks().forEach((track) => {
  222. const rtc = networkStore.getRtcMap(
  223. `${roomId.value}___${item.socketId}`
  224. );
  225. console.log(rtc, track, localStream.value, 9998);
  226. rtc?.addTrack(track, localStream.value);
  227. });
  228. }
  229. });
  230. }
  231. async function sendOffer({
  232. sender,
  233. receiver,
  234. }: {
  235. sender: string;
  236. receiver: string;
  237. }) {
  238. if (isDone.value) return;
  239. const instance = networkStore.wsMap.get(roomId.value);
  240. if (!instance) return;
  241. const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
  242. if (!rtc) return;
  243. const sdp = await rtc.createOffer();
  244. await rtc.setLocalDescription(sdp);
  245. instance.send({
  246. msgType: WsMsgTypeEnum.offer,
  247. data: { sdp, sender, receiver },
  248. });
  249. }
  250. async function batchSendOffer(socketId: string) {
  251. await nextTick(async () => {
  252. console.log('batchSendOffer', offerSended.value, liveUserList.value);
  253. console.log(socketId, 2222222);
  254. if (!offerSended.value.has(socketId) && socketId !== getSocketId()) {
  255. console.log('kkkkkk', socketId);
  256. hooksRtcMap.value.add(await startNewWebRtc({ receiver: socketId }));
  257. await addTransceiver(socketId);
  258. console.warn('new WebRTCClass完成');
  259. console.log('执行sendOffer', {
  260. sender: getSocketId(),
  261. receiver: socketId,
  262. });
  263. sendOffer({ sender: getSocketId(), receiver: socketId });
  264. offerSended.value.add(socketId);
  265. }
  266. });
  267. }
  268. function addVideo() {
  269. sidebarList.value.push({ socketId: getSocketId() });
  270. nextTick(() => {
  271. liveUserList.value.forEach(async (item) => {
  272. if (item.socketId === getSocketId()) {
  273. localVideoRef.value[getSocketId()].srcObject = localStream.value;
  274. }
  275. if (!offerSended.value.has(item.socketId)) {
  276. hooksRtcMap.value.add(
  277. await startNewWebRtc({
  278. receiver: item.socketId,
  279. videoEl: localVideoRef.value[item.socketId],
  280. // videoEl: localVideoRef.value[sender.value],
  281. })
  282. );
  283. await addTransceiver(item.socketId);
  284. console.warn('new WebRTCClass完成');
  285. console.log('执行sendOffer', {
  286. sender: getSocketId(),
  287. receiver: item.socketId,
  288. });
  289. sendOffer({ sender: getSocketId(), receiver: item.socketId });
  290. offerSended.value.add(item.socketId);
  291. }
  292. });
  293. });
  294. }
  295. /** 原生的webrtc时,receiver必传 */
  296. async function startNewWebRtc({
  297. receiver,
  298. videoEl = remoteVideoRef.value!,
  299. }: {
  300. receiver?: string;
  301. videoEl?: HTMLVideoElement;
  302. }) {
  303. if (isSRS) {
  304. console.warn('开始new SRSWebRTCClass', getSocketId());
  305. const rtc = new SRSWebRTCClass({
  306. roomId: `${roomId.value}___${getSocketId()}`,
  307. videoEl,
  308. });
  309. rtc.rtcStatus.joined = true;
  310. rtc.update();
  311. if (track.video) {
  312. rtc.peerConnection?.addTransceiver('video', { direction: 'recvonly' });
  313. }
  314. if (track.audio) {
  315. rtc.peerConnection?.addTransceiver('audio', { direction: 'recvonly' });
  316. }
  317. try {
  318. const offer = await rtc.createOffer();
  319. if (!offer) return;
  320. await rtc.setLocalDescription(offer);
  321. const res: any = await fetchRtcV1Play({
  322. api: `${
  323. process.env.NODE_ENV === 'development'
  324. ? 'http://localhost:1985'
  325. : 'https://live.hsslive.cn/srs'
  326. }/rtc/v1/play/`,
  327. clientip: null,
  328. sdp: offer.sdp!,
  329. streamurl: streamurl.value,
  330. tid: getRandomString(10),
  331. });
  332. await rtc.setRemoteDescription(
  333. new RTCSessionDescription({ type: 'answer', sdp: res.sdp })
  334. );
  335. } catch (error) {
  336. console.log(error);
  337. }
  338. } else {
  339. console.warn('开始new WebRTCClass');
  340. const rtc = new WebRTCClass({
  341. roomId: `${roomId.value}___${receiver!}`,
  342. videoEl,
  343. });
  344. return rtc;
  345. }
  346. }
  347. function keydownDanmu(event: KeyboardEvent) {
  348. const key = event.key.toLowerCase();
  349. if (key === 'enter') {
  350. event.preventDefault();
  351. sendDanmu();
  352. }
  353. }
  354. function sendDanmu() {
  355. if (!danmuStr.value.trim().length) {
  356. window.$message.warning('请输入弹幕内容!');
  357. return;
  358. }
  359. const instance = networkStore.wsMap.get(roomId.value);
  360. if (!instance) return;
  361. const danmu: IDanmu = {
  362. socketId: getSocketId(),
  363. userInfo: userStore.userInfo,
  364. msgType: DanmuMsgTypeEnum.danmu,
  365. msg: danmuStr.value,
  366. };
  367. instance.send({
  368. msgType: WsMsgTypeEnum.message,
  369. data: danmu,
  370. });
  371. damuList.value.push(danmu);
  372. danmuStr.value = '';
  373. }
  374. function initReceive() {
  375. const instance = networkStore.wsMap.get(roomId.value);
  376. if (!instance?.socketIo) return;
  377. // websocket连接成功
  378. instance.socketIo.on(WsConnectStatusEnum.connect, () => {
  379. prettierReceiveWebsocket(WsConnectStatusEnum.connect);
  380. handleHeartbeat();
  381. if (!instance) return;
  382. instance.status = WsConnectStatusEnum.connect;
  383. instance.update();
  384. sendJoin();
  385. });
  386. // websocket连接断开
  387. instance.socketIo.on(WsConnectStatusEnum.disconnect, () => {
  388. prettierReceiveWebsocket(WsConnectStatusEnum.disconnect);
  389. if (!instance) return;
  390. instance.status = WsConnectStatusEnum.disconnect;
  391. instance.update();
  392. });
  393. // 收到offer
  394. instance.socketIo.on(WsMsgTypeEnum.offer, async (data: IOffer) => {
  395. prettierReceiveWebsocket(
  396. WsMsgTypeEnum.offer,
  397. `发送者:${data.data.sender},接收者:${data.data.receiver}`,
  398. data
  399. );
  400. if (isSRS) return;
  401. if (!instance) return;
  402. if (data.data.receiver === getSocketId()) {
  403. if (!data.isAdmin) {
  404. sidebarList.value.push({ socketId: data.data.sender });
  405. }
  406. await nextTick(async () => {
  407. console.log('收到offer,这个offer是发给我的');
  408. sender.value = data.data.sender;
  409. const rtc = await startNewWebRtc({
  410. receiver: data.data.sender,
  411. videoEl: data.isAdmin
  412. ? remoteVideoRef.value
  413. : localVideoRef.value[data.data.sender],
  414. });
  415. if (rtc) {
  416. await rtc.setRemoteDescription(data.data.sdp);
  417. const sdp = await rtc.createAnswer();
  418. await rtc.setLocalDescription(sdp);
  419. instance.send({
  420. msgType: WsMsgTypeEnum.answer,
  421. data: {
  422. sdp,
  423. sender: getSocketId(),
  424. receiver: data.data.sender,
  425. },
  426. });
  427. }
  428. });
  429. } else {
  430. console.log('收到offer,但是这个offer不是发给我的');
  431. }
  432. });
  433. // 收到answer
  434. instance.socketIo.on(WsMsgTypeEnum.answer, async (data: IOffer) => {
  435. prettierReceiveWebsocket(
  436. WsMsgTypeEnum.answer,
  437. `发送者:${data.data.sender},接收者:${data.data.receiver}`,
  438. data
  439. );
  440. if (isSRS) return;
  441. if (!instance) return;
  442. const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
  443. if (!rtc) return;
  444. rtc.rtcStatus.answer = true;
  445. rtc.update();
  446. if (data.data.receiver === getSocketId()) {
  447. console.log('收到answer,这个answer是发给我的');
  448. await rtc.setRemoteDescription(data.data.sdp);
  449. } else {
  450. console.log('收到answer,但这个answer不是发给我的');
  451. }
  452. });
  453. // 收到candidate
  454. instance.socketIo.on(WsMsgTypeEnum.candidate, (data: ICandidate) => {
  455. prettierReceiveWebsocket(
  456. WsMsgTypeEnum.candidate,
  457. `发送者:${data.data.sender},接收者:${data.data.receiver}`,
  458. data
  459. );
  460. if (isSRS) return;
  461. if (!instance) return;
  462. const rtc = networkStore.getRtcMap(`${roomId.value}___${data.socketId}`);
  463. if (!rtc) return;
  464. if (data.data.receiver === getSocketId()) {
  465. console.log('是发给我的candidate');
  466. const candidate = new RTCIceCandidate({
  467. sdpMid: data.data.sdpMid,
  468. sdpMLineIndex: data.data.sdpMLineIndex,
  469. candidate: data.data.candidate,
  470. });
  471. rtc.peerConnection
  472. ?.addIceCandidate(candidate)
  473. .then(() => {
  474. console.log('candidate成功');
  475. })
  476. .catch((err) => {
  477. console.error('candidate失败', err);
  478. });
  479. } else {
  480. console.log('不是发给我的candidate');
  481. }
  482. });
  483. // 管理员正在直播
  484. instance.socketIo.on(WsMsgTypeEnum.roomLiveing, (data: IAdminIn) => {
  485. prettierReceiveWebsocket(WsMsgTypeEnum.roomLiveing, data);
  486. if (isSRS && !isFlv) {
  487. startNewWebRtc({});
  488. }
  489. });
  490. // 管理员不在直播
  491. instance.socketIo.on(WsMsgTypeEnum.roomNoLive, (data: IAdminIn) => {
  492. prettierReceiveWebsocket(WsMsgTypeEnum.roomNoLive, data);
  493. roomNoLive.value = true;
  494. closeRtc();
  495. });
  496. // 当前所有在线用户
  497. instance.socketIo.on(WsMsgTypeEnum.liveUser, (data) => {
  498. prettierReceiveWebsocket(WsMsgTypeEnum.liveUser, data);
  499. if (!instance) return;
  500. liveUserList.value = data.map((item) => ({
  501. avatar: 'red',
  502. socketId: item.id,
  503. }));
  504. // batchSendOffer();
  505. });
  506. // 收到用户发送消息
  507. instance.socketIo.on(WsMsgTypeEnum.message, (data) => {
  508. prettierReceiveWebsocket(WsMsgTypeEnum.message, data);
  509. if (!instance) return;
  510. const danmu: IDanmu = {
  511. msgType: DanmuMsgTypeEnum.danmu,
  512. socketId: data.socketId,
  513. userInfo: data.data.userInfo,
  514. msg: data.data.msg,
  515. };
  516. damuList.value.push(danmu);
  517. });
  518. // 用户加入房间
  519. instance.socketIo.on(WsMsgTypeEnum.joined, (data: { data: ILive }) => {
  520. prettierReceiveWebsocket(WsMsgTypeEnum.joined, data);
  521. roomName.value = data.data.live_room?.roomName!;
  522. userName.value = data.data.user?.username!;
  523. userAvatar.value = data.data.user?.avatar!;
  524. track.audio = data.data.track_audio!;
  525. track.video = data.data.track_video!;
  526. streamurl.value = data.data.streamurl!;
  527. flvurl.value = data.data.flvurl!;
  528. if (isFlv) {
  529. useFlvPlay(flvurl.value, remoteVideoRef.value!);
  530. }
  531. instance.send({ msgType: WsMsgTypeEnum.getLiveUser });
  532. });
  533. // 其他用户加入房间
  534. instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
  535. prettierReceiveWebsocket(WsMsgTypeEnum.otherJoin, data);
  536. const danmu: IDanmu = {
  537. msgType: DanmuMsgTypeEnum.otherJoin,
  538. socketId: data.data.socketId,
  539. userInfo: data.data.userInfo,
  540. msg: '',
  541. };
  542. damuList.value.push(danmu);
  543. batchSendOffer(data.data.socketId);
  544. });
  545. // 用户离开房间
  546. instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
  547. prettierReceiveWebsocket(WsMsgTypeEnum.leave, data);
  548. if (!instance) return;
  549. instance.send({
  550. msgType: WsMsgTypeEnum.leave,
  551. data: { roomId: instance.roomId },
  552. });
  553. });
  554. // 用户离开房间完成
  555. instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
  556. prettierReceiveWebsocket(WsMsgTypeEnum.leaved, data);
  557. if (!instance) return;
  558. const res = liveUserList.value.filter(
  559. (item) => item.socketId !== data.socketId
  560. );
  561. liveUserList.value = res;
  562. const danmu: IDanmu = {
  563. msgType: DanmuMsgTypeEnum.userLeaved,
  564. socketId: data.socketId,
  565. userInfo: data.data.userInfo,
  566. msg: '',
  567. };
  568. damuList.value.push(danmu);
  569. });
  570. }
  571. return {
  572. initPull,
  573. closeWs,
  574. closeRtc,
  575. getSocketId,
  576. keydownDanmu,
  577. sendDanmu,
  578. batchSendOffer,
  579. startGetUserMedia,
  580. startGetDisplayMedia,
  581. addTrack,
  582. addVideo,
  583. balance,
  584. roomName,
  585. userName,
  586. userAvatar,
  587. roomNoLive,
  588. damuList,
  589. giftList,
  590. liveUserList,
  591. danmuStr,
  592. localStream,
  593. sender,
  594. sidebarList,
  595. };
  596. }