use-pull.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import { getRandomString } from 'billd-utils';
  2. import { nextTick, onUnmounted, ref, watch } from 'vue';
  3. import { useRoute } from 'vue-router';
  4. import { commentAuthTip, loginTip } from '@/hooks/use-login';
  5. import { useFlvPlay, useHlsPlay } from '@/hooks/use-play';
  6. import { useWebsocket } from '@/hooks/use-websocket';
  7. import { useWebRtcRtmpToRtc } from '@/hooks/webrtc/rtmpToRtc';
  8. import {
  9. DanmuMsgTypeEnum,
  10. LiveLineEnum,
  11. LiveRenderEnum,
  12. WsMessageContentTypeEnum,
  13. WsMessageMsgIsFileEnum,
  14. } from '@/interface';
  15. import { useAppStore } from '@/store/app';
  16. import { useCacheStore } from '@/store/cache';
  17. import { useNetworkStore } from '@/store/network';
  18. import {
  19. ILiveRoom,
  20. LiveRoomTypeEnum,
  21. LiveRoomUseCDNEnum,
  22. } from '@/types/ILiveRoom';
  23. import { WsMessageType, WsMsgTypeEnum } from '@/types/websocket';
  24. import { createVideo, videoFullBox, videoToCanvas } from '@/utils';
  25. export function usePull() {
  26. const route = useRoute();
  27. const networkStore = useNetworkStore();
  28. const cacheStore = useCacheStore();
  29. const appStore = useAppStore();
  30. const danmuStr = ref('');
  31. const roomId = ref('');
  32. const msgIsFile = ref(WsMessageMsgIsFileEnum.no);
  33. const danmuMsgType = ref<DanmuMsgTypeEnum>(DanmuMsgTypeEnum.danmu);
  34. const liveRoomInfo = ref<ILiveRoom>();
  35. const autoplayVal = ref(false);
  36. const videoLoading = ref(false);
  37. const isPlaying = ref(false);
  38. const showPlayBtn = ref(false);
  39. const flvurl = ref('');
  40. const hlsurl = ref('');
  41. const videoWrapRef = ref<HTMLDivElement>();
  42. const videoResolution = ref();
  43. const remoteVideo = ref<Array<HTMLVideoElement | HTMLCanvasElement>>([]);
  44. const remoteStream = ref<MediaStream[]>([]);
  45. const { mySocketId, initWs, roomLiving, anchorInfo, liveUserList, damuList } =
  46. useWebsocket();
  47. const { updateWebRtcRtmpToRtcConfig, webRtcRtmpToRtc } = useWebRtcRtmpToRtc();
  48. const { flvVideoEl, flvIsPlaying, startFlvPlay, destroyFlv } = useFlvPlay();
  49. const { hlsVideoEl, hlsIsPlaying, startHlsPlay, destroyHls } = useHlsPlay();
  50. const stopDrawingArr = ref<any[]>([]);
  51. const rtcVideo = ref<HTMLVideoElement[]>([]);
  52. let changeWrapSizeFn;
  53. onUnmounted(() => {
  54. handleStopDrawing();
  55. destroyFlv();
  56. destroyHls();
  57. });
  58. function handleStopDrawing() {
  59. changeWrapSizeFn = undefined;
  60. stopDrawingArr.value.forEach((cb) => cb());
  61. stopDrawingArr.value = [];
  62. remoteVideo.value.forEach((el) => el.remove());
  63. remoteVideo.value = [];
  64. }
  65. function handleVideoWrapResize() {
  66. nextTick(() => {
  67. if (videoWrapRef.value) {
  68. const rect = videoWrapRef.value.getBoundingClientRect();
  69. changeWrapSizeFn?.({ width: rect.width, height: rect.height });
  70. }
  71. });
  72. }
  73. function videoPlay(videoEl: HTMLVideoElement) {
  74. stopDrawingArr.value.forEach((cb) => cb());
  75. stopDrawingArr.value = [];
  76. if (appStore.videoControls.renderMode === LiveRenderEnum.canvas) {
  77. if (videoEl && videoWrapRef.value) {
  78. const rect = videoWrapRef.value.getBoundingClientRect();
  79. const { canvas, stopDrawing, changeWrapSize } = videoToCanvas({
  80. wrapSize: {
  81. width: rect.width,
  82. height: rect.height,
  83. },
  84. videoEl,
  85. videoResize: ({ w, h }) => {
  86. videoResolution.value = `${w}x${h}`;
  87. },
  88. });
  89. changeWrapSizeFn = changeWrapSize;
  90. stopDrawingArr.value.push(stopDrawing);
  91. remoteVideo.value.push(canvas);
  92. videoLoading.value = false;
  93. }
  94. } else if (appStore.videoControls.renderMode === LiveRenderEnum.video) {
  95. if (videoEl && videoWrapRef.value) {
  96. const rect = videoWrapRef.value.getBoundingClientRect();
  97. const { changeWrapSize } = videoFullBox({
  98. wrapSize: {
  99. width: rect.width,
  100. height: rect.height,
  101. },
  102. videoEl,
  103. videoResize: ({ w, h }) => {
  104. videoResolution.value = `${w}x${h}`;
  105. },
  106. });
  107. changeWrapSizeFn = changeWrapSize;
  108. remoteVideo.value.push(videoEl);
  109. videoLoading.value = false;
  110. }
  111. }
  112. }
  113. watch(
  114. () => hlsVideoEl.value,
  115. (newval) => {
  116. if (newval) {
  117. // @ts-ignore
  118. remoteStream.value.push(newval.captureStream());
  119. }
  120. }
  121. );
  122. watch(
  123. () => flvVideoEl.value,
  124. (newval) => {
  125. if (newval) {
  126. // @ts-ignore
  127. remoteStream.value.push(newval.captureStream());
  128. }
  129. }
  130. );
  131. watch(
  132. () => appStore.videoControlsValue.pageFullMode,
  133. () => {
  134. handleVideoWrapResize();
  135. }
  136. );
  137. watch(
  138. [() => appStore.videoControls.renderMode, () => remoteStream.value],
  139. () => {
  140. handleStopDrawing();
  141. remoteStream.value.forEach((v) => {
  142. const el = createVideo({});
  143. el.srcObject = v;
  144. videoPlay(el);
  145. });
  146. },
  147. {
  148. deep: true,
  149. }
  150. );
  151. watch(
  152. () => remoteVideo.value,
  153. (newval) => {
  154. newval.forEach((videoEl) => {
  155. videoWrapRef.value?.appendChild(videoEl);
  156. });
  157. },
  158. {
  159. deep: true,
  160. immediate: true,
  161. }
  162. );
  163. watch(
  164. () => cacheStore.muted,
  165. (newVal) => {
  166. appStore.pageIsClick = true;
  167. rtcVideo.value.forEach((v) => {
  168. v.muted = newVal;
  169. });
  170. if (!newVal) {
  171. cacheStore.volume = cacheStore.volume || appStore.normalVolume;
  172. } else {
  173. cacheStore.volume = 0;
  174. }
  175. }
  176. );
  177. watch(
  178. () => cacheStore.volume,
  179. (newVal) => {
  180. rtcVideo.value.forEach((v) => {
  181. v.volume = newVal / 100;
  182. });
  183. if (!newVal) {
  184. cacheStore.muted = true;
  185. } else {
  186. cacheStore.muted = false;
  187. }
  188. },
  189. {
  190. immediate: true,
  191. }
  192. );
  193. function handleRtmpToRtcPlay() {
  194. console.log('handleRtmpToRtcPlay');
  195. handleStopDrawing();
  196. videoLoading.value = true;
  197. appStore.liveLine = LiveLineEnum['rtmp-rtc'];
  198. updateWebRtcRtmpToRtcConfig({
  199. isPk: false,
  200. roomId: roomId.value,
  201. });
  202. const videoEl = createVideo({
  203. appendChild: true,
  204. muted: appStore.pageIsClick ? cacheStore.muted : true,
  205. });
  206. rtcVideo.value.push(videoEl);
  207. webRtcRtmpToRtc.newWebRtc({
  208. sender: mySocketId.value,
  209. receiver: 'rtmpToRtc',
  210. videoEl,
  211. sucessCb: (stream) => {
  212. remoteStream.value.push(stream);
  213. },
  214. });
  215. webRtcRtmpToRtc.sendOffer({
  216. sender: mySocketId.value,
  217. receiver: 'rtmpToRtc',
  218. });
  219. }
  220. function handleHlsPlay() {
  221. console.log('handleHlsPlay', hlsurl.value);
  222. handleStopDrawing();
  223. videoLoading.value = true;
  224. appStore.liveLine = LiveLineEnum.hls;
  225. startHlsPlay({
  226. hlsurl: hlsurl.value,
  227. });
  228. }
  229. function handleFlvPlay() {
  230. console.log('handleFlvPlay', flvurl.value);
  231. handleStopDrawing();
  232. videoLoading.value = true;
  233. appStore.liveLine = LiveLineEnum.flv;
  234. startFlvPlay({
  235. flvurl: flvurl.value,
  236. });
  237. }
  238. function handlePlay(data: ILiveRoom) {
  239. roomLiving.value = true;
  240. flvurl.value =
  241. data.cdn === LiveRoomUseCDNEnum.yes &&
  242. [LiveRoomTypeEnum.tencent_css, LiveRoomTypeEnum.tencent_css_pk].includes(
  243. data.type!
  244. )
  245. ? data.cdn_flv_url!
  246. : data.flv_url!;
  247. hlsurl.value =
  248. data.cdn === LiveRoomUseCDNEnum.yes &&
  249. [LiveRoomTypeEnum.tencent_css, LiveRoomTypeEnum.tencent_css_pk].includes(
  250. data.type!
  251. )
  252. ? data.cdn_hls_url!
  253. : data.hls_url!;
  254. function play() {
  255. if (appStore.liveLine === LiveLineEnum.flv) {
  256. handleFlvPlay();
  257. } else if (appStore.liveLine === LiveLineEnum.hls) {
  258. handleHlsPlay();
  259. }
  260. }
  261. if (LiveRoomTypeEnum.pk === data.type && !route.query.pkKey) {
  262. play();
  263. } else if (
  264. [
  265. LiveRoomTypeEnum.system,
  266. LiveRoomTypeEnum.srs,
  267. LiveRoomTypeEnum.obs,
  268. LiveRoomTypeEnum.msr,
  269. LiveRoomTypeEnum.pk,
  270. LiveRoomTypeEnum.forward_bilibili,
  271. LiveRoomTypeEnum.forward_huya,
  272. LiveRoomTypeEnum.forward_all,
  273. LiveRoomTypeEnum.tencent_css,
  274. LiveRoomTypeEnum.tencent_css_pk,
  275. ].includes(data.type!)
  276. ) {
  277. play();
  278. } else if (
  279. [
  280. LiveRoomTypeEnum.wertc_live,
  281. LiveRoomTypeEnum.wertc_meeting_one,
  282. LiveRoomTypeEnum.wertc_meeting_two,
  283. ].includes(data.type!)
  284. ) {
  285. appStore.liveLine = LiveLineEnum.rtc;
  286. }
  287. }
  288. watch(
  289. [() => roomLiving.value, () => appStore.liveRoomInfo],
  290. ([val, liveRoomInfo]) => {
  291. if (val && liveRoomInfo) {
  292. showPlayBtn.value = false;
  293. if (
  294. [
  295. LiveRoomTypeEnum.system,
  296. LiveRoomTypeEnum.msr,
  297. LiveRoomTypeEnum.srs,
  298. LiveRoomTypeEnum.obs,
  299. LiveRoomTypeEnum.tencent_css,
  300. LiveRoomTypeEnum.tencent_css_pk,
  301. LiveRoomTypeEnum.forward_bilibili,
  302. LiveRoomTypeEnum.forward_huya,
  303. LiveRoomTypeEnum.forward_all,
  304. ].includes(liveRoomInfo.type!)
  305. ) {
  306. handlePlay(liveRoomInfo!);
  307. } else if (LiveRoomTypeEnum.pk === liveRoomInfo.type!) {
  308. if (!route.query.pkKey) {
  309. handlePlay(liveRoomInfo!);
  310. }
  311. }
  312. }
  313. if (!roomLiving.value) {
  314. closeRtc();
  315. handleStopDrawing();
  316. }
  317. },
  318. {
  319. deep: true,
  320. immediate: true,
  321. }
  322. );
  323. watch(
  324. () => appStore.liveLine,
  325. (newVal) => {
  326. console.log('liveLine变了', newVal);
  327. handleStopDrawing();
  328. destroyFlv();
  329. destroyHls();
  330. remoteStream.value = [];
  331. if (!roomLiving.value) {
  332. return;
  333. }
  334. switch (newVal) {
  335. case LiveLineEnum.flv:
  336. handleFlvPlay();
  337. break;
  338. case LiveLineEnum.hls:
  339. handleHlsPlay();
  340. break;
  341. case LiveLineEnum.rtc:
  342. break;
  343. case LiveLineEnum['rtmp-rtc']:
  344. handleRtmpToRtcPlay();
  345. break;
  346. }
  347. }
  348. );
  349. watch(
  350. () => hlsIsPlaying.value,
  351. (newVal) => {
  352. isPlaying.value = newVal;
  353. }
  354. );
  355. watch(
  356. () => flvIsPlaying.value,
  357. (newVal) => {
  358. isPlaying.value = newVal;
  359. }
  360. );
  361. function initRoomId(id: string) {
  362. roomId.value = id;
  363. }
  364. function initPull(data: { autolay?: boolean }) {
  365. if (data.autolay === undefined) {
  366. autoplayVal.value = true;
  367. } else {
  368. autoplayVal.value = data.autolay;
  369. }
  370. initWs({
  371. roomId: roomId.value,
  372. isAnchor: false,
  373. });
  374. }
  375. function closeWs() {
  376. networkStore.wsMap.forEach((ws) => {
  377. networkStore.removeWs(ws.roomId);
  378. });
  379. }
  380. function closeRtc() {
  381. networkStore.rtcMap.forEach((rtc) => {
  382. networkStore.removeRtc(rtc.receiver);
  383. });
  384. }
  385. function keydownDanmu(event: KeyboardEvent) {
  386. const key = event.key.toLowerCase();
  387. if (key === 'enter') {
  388. event.preventDefault();
  389. sendDanmuTxt(danmuStr.value);
  390. }
  391. }
  392. function sendDanmuReward(txt: string) {
  393. if (!loginTip()) {
  394. return;
  395. }
  396. if (!commentAuthTip()) {
  397. return;
  398. }
  399. if (!txt.trim().length) {
  400. window.$message.warning('请输入弹幕内容!');
  401. return;
  402. }
  403. const instance = networkStore.wsMap.get(roomId.value);
  404. if (!instance) return;
  405. const messageData: WsMessageType['data'] = {
  406. content: txt,
  407. content_type: WsMessageContentTypeEnum.txt,
  408. msg_type: DanmuMsgTypeEnum.reward,
  409. live_room_id: Number(roomId.value),
  410. isBilibili: false,
  411. };
  412. instance.send({
  413. requestId: getRandomString(8),
  414. msgType: WsMsgTypeEnum.message,
  415. data: messageData,
  416. });
  417. }
  418. function sendDanmuTxt(txt: string) {
  419. if (!loginTip()) {
  420. return;
  421. }
  422. if (!commentAuthTip()) {
  423. return;
  424. }
  425. if (!txt.trim().length) {
  426. window.$message.warning('请输入弹幕内容!');
  427. return;
  428. }
  429. const instance = networkStore.wsMap.get(roomId.value);
  430. if (!instance) return;
  431. const messageData: WsMessageType['data'] = {
  432. content: txt,
  433. content_type: WsMessageContentTypeEnum.txt,
  434. msg_type: DanmuMsgTypeEnum.danmu,
  435. live_room_id: Number(roomId.value),
  436. isBilibili: false,
  437. };
  438. instance.send({
  439. requestId: getRandomString(8),
  440. msgType: WsMsgTypeEnum.message,
  441. data: messageData,
  442. });
  443. danmuStr.value = '';
  444. }
  445. function sendDanmuImg(url: string) {
  446. if (!loginTip()) {
  447. return;
  448. }
  449. if (!commentAuthTip()) {
  450. return;
  451. }
  452. if (!url.trim().length) {
  453. window.$message.warning('图片不能为空!');
  454. return;
  455. }
  456. const instance = networkStore.wsMap.get(roomId.value);
  457. if (!instance) return;
  458. const requestId = getRandomString(8);
  459. const messageData: WsMessageType['data'] = {
  460. content: url,
  461. content_type: WsMessageContentTypeEnum.img,
  462. msg_type: DanmuMsgTypeEnum.danmu,
  463. live_room_id: Number(roomId.value),
  464. isBilibili: false,
  465. };
  466. instance.send({
  467. requestId,
  468. msgType: WsMsgTypeEnum.message,
  469. data: messageData,
  470. });
  471. }
  472. return {
  473. initWs,
  474. videoWrapRef,
  475. handlePlay,
  476. handleStopDrawing,
  477. initPull,
  478. closeWs,
  479. closeRtc,
  480. keydownDanmu,
  481. sendDanmuReward,
  482. sendDanmuTxt,
  483. sendDanmuImg,
  484. showPlayBtn,
  485. danmuMsgType,
  486. isPlaying,
  487. msgIsFile,
  488. mySocketId,
  489. videoResolution,
  490. remoteVideo,
  491. roomLiving,
  492. autoplayVal,
  493. videoLoading,
  494. damuList,
  495. liveUserList,
  496. danmuStr,
  497. liveRoomInfo,
  498. anchorInfo,
  499. initRoomId,
  500. };
  501. }