use-pull.ts 18 KB

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