use-pull.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. import { getRandomString } from 'billd-utils';
  2. import FlvJs from 'flv.js';
  3. import { Ref, nextTick, onUnmounted, reactive, ref, watch } from 'vue';
  4. import { useRoute } from 'vue-router';
  5. import { fetchRtcV1Play } from '@/api/srs';
  6. import { useFlvPlay } from '@/hooks/use-play';
  7. import {
  8. DanmuMsgTypeEnum,
  9. IAdminIn,
  10. ICandidate,
  11. IDanmu,
  12. ILive,
  13. ILiveUser,
  14. IOffer,
  15. MediaTypeEnum,
  16. } from '@/interface';
  17. import { SRSWebRTCClass } from '@/network/srsWebRtc';
  18. import { WebRTCClass } from '@/network/webRtc';
  19. import {
  20. WebSocketClass,
  21. WsConnectStatusEnum,
  22. WsMsgTypeEnum,
  23. prettierReceiveWebsocket,
  24. } from '@/network/webSocket';
  25. import { useNetworkStore } from '@/store/network';
  26. import { useUserStore } from '@/store/user';
  27. export function usePull({
  28. localVideoRef,
  29. remoteVideoRef,
  30. isSRS,
  31. isFlv,
  32. }: {
  33. localVideoRef: Ref<HTMLVideoElement[]>;
  34. remoteVideoRef: Ref<HTMLVideoElement | undefined>;
  35. isSRS?: boolean;
  36. isFlv?: boolean;
  37. }) {
  38. const route = useRoute();
  39. const userStore = useUserStore();
  40. const networkStore = useNetworkStore();
  41. const heartbeatTimer = ref();
  42. const roomId = ref(route.params.roomId as string);
  43. const roomName = ref('');
  44. const userName = ref('');
  45. const userAvatar = ref('');
  46. const streamurl = ref('');
  47. const flvurl = ref('');
  48. const danmuStr = ref('');
  49. const balance = ref('0.00');
  50. const damuList = ref<IDanmu[]>([]);
  51. const liveUserList = ref<ILiveUser[]>([]);
  52. const isDone = ref(false);
  53. const roomNoLive = ref(false);
  54. const localStream = ref();
  55. const sidebarList = ref<
  56. {
  57. socketId: string;
  58. }[]
  59. >([]);
  60. const player = ref<FlvJs.Player>();
  61. const track = reactive({
  62. audio: true,
  63. video: true,
  64. });
  65. const giftList = ref([
  66. { name: '鲜花', ico: '', price: '免费' },
  67. { name: '肥宅水', ico: '', price: '2元' },
  68. { name: '小鸡腿', ico: '', price: '3元' },
  69. { name: '大鸡腿', ico: '', price: '5元' },
  70. { name: '一杯咖啡', ico: '', price: '10元' },
  71. ]);
  72. const offerSended = ref(new Set());
  73. const hooksRtcMap = ref(new Set());
  74. const sender = ref();
  75. const allMediaTypeList = {
  76. [MediaTypeEnum.camera]: {
  77. type: MediaTypeEnum.camera,
  78. txt: '摄像头',
  79. },
  80. [MediaTypeEnum.screen]: {
  81. type: MediaTypeEnum.screen,
  82. txt: '窗口',
  83. },
  84. };
  85. const currMediaTypeList = ref<
  86. {
  87. type: MediaTypeEnum;
  88. txt: string;
  89. }[]
  90. >([]);
  91. const currMediaType = ref<{
  92. type: MediaTypeEnum;
  93. txt: string;
  94. }>();
  95. onUnmounted(() => {
  96. clearInterval(heartbeatTimer.value);
  97. if (player.value) {
  98. player.value.destroy();
  99. }
  100. });
  101. /** 摄像头 */
  102. async function startGetUserMedia() {
  103. if (!localStream.value) {
  104. // WARN navigator.mediaDevices在localhost和https才能用,http://192.168.1.103:8000局域网用不了
  105. const event = await navigator.mediaDevices.getUserMedia({
  106. video: true,
  107. audio: true,
  108. });
  109. console.log('getUserMedia成功', event);
  110. currMediaType.value = allMediaTypeList[MediaTypeEnum.camera];
  111. currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.camera]);
  112. // localVideoRef.value.forEach((item) => {
  113. // item.srcObject = event;
  114. // });
  115. localStream.value = event;
  116. }
  117. }
  118. /** 窗口 */
  119. async function startGetDisplayMedia() {
  120. if (!localStream.value) {
  121. // WARN navigator.mediaDevices.getDisplayMedia在localhost和https才能用,http://192.168.1.103:8000局域网用不了
  122. const event = await navigator.mediaDevices.getDisplayMedia({
  123. video: true,
  124. audio: true,
  125. });
  126. const audio = event.getAudioTracks();
  127. const video = event.getVideoTracks();
  128. track.audio = !!audio.length;
  129. track.video = !!video.length;
  130. console.log('getDisplayMedia成功', event);
  131. currMediaType.value = allMediaTypeList[MediaTypeEnum.screen];
  132. currMediaTypeList.value.push(allMediaTypeList[MediaTypeEnum.screen]);
  133. localStream.value = event;
  134. }
  135. }
  136. watch(
  137. [
  138. () => userStore.userInfo,
  139. () => networkStore.wsMap.get(roomId.value)?.socketIo?.connected,
  140. ],
  141. ([userInfo, connected]) => {
  142. if (userInfo) {
  143. balance.value = userInfo.wallet?.balance || '0.00';
  144. }
  145. if (userInfo && connected) {
  146. const instance = networkStore.wsMap.get(roomId.value);
  147. if (!instance) return;
  148. instance.send({
  149. msgType: WsMsgTypeEnum.updateJoinInfo,
  150. data: {
  151. userInfo: userStore.userInfo,
  152. },
  153. });
  154. }
  155. }
  156. );
  157. function initPull() {
  158. console.warn('开始new WebSocketClass');
  159. const ws = new WebSocketClass({
  160. roomId: roomId.value,
  161. url:
  162. process.env.NODE_ENV === 'development'
  163. ? 'ws://localhost:4300'
  164. : 'wss://live.hsslive.cn',
  165. isAdmin: false,
  166. });
  167. ws.update();
  168. initReceive();
  169. remoteVideoRef.value?.addEventListener('loadstart', () => {
  170. console.warn('视频流-loadstart');
  171. const rtc = networkStore.getRtcMap(roomId.value);
  172. if (!rtc) return;
  173. rtc.rtcStatus.loadstart = true;
  174. rtc.update();
  175. });
  176. remoteVideoRef.value?.addEventListener('loadedmetadata', () => {
  177. console.warn('视频流-loadedmetadata');
  178. const rtc = networkStore.getRtcMap(roomId.value);
  179. if (!rtc) return;
  180. rtc.rtcStatus.loadedmetadata = true;
  181. rtc.update();
  182. });
  183. }
  184. function handleHeartbeat() {
  185. heartbeatTimer.value = setInterval(() => {
  186. const instance = networkStore.wsMap.get(roomId.value);
  187. if (!instance) return;
  188. instance.send({
  189. msgType: WsMsgTypeEnum.heartbeat,
  190. });
  191. }, 1000 * 5);
  192. }
  193. function closeWs() {
  194. const instance = networkStore.wsMap.get(roomId.value);
  195. instance?.close();
  196. }
  197. function closeRtc() {
  198. networkStore.rtcMap.forEach((rtc) => {
  199. rtc.close();
  200. });
  201. }
  202. function getSocketId() {
  203. return networkStore.wsMap.get(roomId.value!)?.socketIo?.id || '-1';
  204. }
  205. function sendJoin() {
  206. const instance = networkStore.wsMap.get(roomId.value);
  207. if (!instance) return;
  208. instance.send({
  209. msgType: WsMsgTypeEnum.join,
  210. data: { userInfo: userStore.userInfo },
  211. });
  212. }
  213. function addTransceiver(socketId: string) {
  214. if (!localStream.value) return;
  215. if (socketId !== getSocketId()) {
  216. localStream.value.getTracks().forEach((track) => {
  217. const rtc = networkStore.getRtcMap(`${roomId.value}___${socketId}`);
  218. rtc?.addTransceiver(track, localStream.value);
  219. });
  220. }
  221. }
  222. function addTrack() {
  223. if (!localStream.value) return;
  224. liveUserList.value.forEach((item) => {
  225. if (item.socketId !== getSocketId()) {
  226. localStream.value.getTracks().forEach((track) => {
  227. const rtc = networkStore.getRtcMap(
  228. `${roomId.value}___${item.socketId}`
  229. );
  230. console.log(rtc, track, localStream.value, 9998);
  231. rtc?.addTrack(track, localStream.value);
  232. });
  233. }
  234. });
  235. }
  236. async function sendOffer({
  237. sender,
  238. receiver,
  239. }: {
  240. sender: string;
  241. receiver: string;
  242. }) {
  243. if (isDone.value) return;
  244. const instance = networkStore.wsMap.get(roomId.value);
  245. if (!instance) return;
  246. const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`);
  247. if (!rtc) return;
  248. const sdp = await rtc.createOffer();
  249. await rtc.setLocalDescription(sdp);
  250. instance.send({
  251. msgType: WsMsgTypeEnum.offer,
  252. data: { sdp, sender, receiver },
  253. });
  254. }
  255. async function batchSendOffer(socketId: string) {
  256. await nextTick(async () => {
  257. if (!offerSended.value.has(socketId) && socketId !== getSocketId()) {
  258. hooksRtcMap.value.add(await startNewWebRtc({ receiver: socketId }));
  259. await addTransceiver(socketId);
  260. console.log('执行sendOffer', {
  261. sender: getSocketId(),
  262. receiver: socketId,
  263. });
  264. sendOffer({ sender: getSocketId(), receiver: socketId });
  265. offerSended.value.add(socketId);
  266. }
  267. });
  268. }
  269. function addVideo() {
  270. sidebarList.value.push({ socketId: getSocketId() });
  271. nextTick(() => {
  272. liveUserList.value.forEach(async (item) => {
  273. if (item.socketId === getSocketId()) {
  274. localVideoRef.value[getSocketId()].srcObject = localStream.value;
  275. }
  276. if (!offerSended.value.has(item.socketId)) {
  277. hooksRtcMap.value.add(
  278. await startNewWebRtc({
  279. receiver: item.socketId,
  280. videoEl: localVideoRef.value[item.socketId],
  281. // videoEl: localVideoRef.value[sender.value],
  282. })
  283. );
  284. await addTransceiver(item.socketId);
  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(
  520. WsMsgTypeEnum.joined,
  521. async (data: { data: ILive }) => {
  522. prettierReceiveWebsocket(WsMsgTypeEnum.joined, data);
  523. roomName.value = data.data.live_room?.roomName!;
  524. userName.value = data.data.user?.username!;
  525. userAvatar.value = data.data.user?.avatar!;
  526. track.audio = data.data.track_audio!;
  527. track.video = data.data.track_video!;
  528. streamurl.value = data.data.streamurl!;
  529. flvurl.value = data.data.flvurl!;
  530. if (isFlv) {
  531. const { err, flvPlayer } = await useFlvPlay(
  532. flvurl.value,
  533. remoteVideoRef.value!
  534. );
  535. if (!err) {
  536. player.value = flvPlayer;
  537. }
  538. }
  539. instance.send({ msgType: WsMsgTypeEnum.getLiveUser });
  540. }
  541. );
  542. // 其他用户加入房间
  543. instance.socketIo.on(WsMsgTypeEnum.otherJoin, (data) => {
  544. prettierReceiveWebsocket(WsMsgTypeEnum.otherJoin, data);
  545. const danmu: IDanmu = {
  546. msgType: DanmuMsgTypeEnum.otherJoin,
  547. socketId: data.data.socketId,
  548. userInfo: data.data.userInfo,
  549. msg: '',
  550. };
  551. damuList.value.push(danmu);
  552. batchSendOffer(data.data.socketId);
  553. });
  554. // 用户离开房间
  555. instance.socketIo.on(WsMsgTypeEnum.leave, (data) => {
  556. prettierReceiveWebsocket(WsMsgTypeEnum.leave, data);
  557. if (!instance) return;
  558. instance.send({
  559. msgType: WsMsgTypeEnum.leave,
  560. data: { roomId: instance.roomId },
  561. });
  562. });
  563. // 用户离开房间完成
  564. instance.socketIo.on(WsMsgTypeEnum.leaved, (data) => {
  565. prettierReceiveWebsocket(WsMsgTypeEnum.leaved, data);
  566. if (!instance) return;
  567. const res = liveUserList.value.filter(
  568. (item) => item.socketId !== data.socketId
  569. );
  570. liveUserList.value = res;
  571. const danmu: IDanmu = {
  572. msgType: DanmuMsgTypeEnum.userLeaved,
  573. socketId: data.socketId,
  574. userInfo: data.data.userInfo,
  575. msg: '',
  576. };
  577. damuList.value.push(danmu);
  578. });
  579. }
  580. return {
  581. initPull,
  582. closeWs,
  583. closeRtc,
  584. getSocketId,
  585. keydownDanmu,
  586. sendDanmu,
  587. batchSendOffer,
  588. startGetUserMedia,
  589. startGetDisplayMedia,
  590. addTrack,
  591. addVideo,
  592. balance,
  593. roomName,
  594. userName,
  595. userAvatar,
  596. roomNoLive,
  597. damuList,
  598. giftList,
  599. liveUserList,
  600. danmuStr,
  601. localStream,
  602. sender,
  603. sidebarList,
  604. };
  605. }