use-push.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import { windowReload } from 'billd-utils';
  2. import { Ref, onMounted, onUnmounted, ref, watch } from 'vue';
  3. import { useRoute, useRouter } from 'vue-router';
  4. import {
  5. fetchCreateUserLiveRoom,
  6. fetchUserHasLiveRoom,
  7. } from '@/api/userLiveRoom';
  8. import {
  9. DanmuMsgTypeEnum,
  10. IMessage,
  11. MediaTypeEnum,
  12. liveTypeEnum,
  13. } from '@/interface';
  14. import { WsMsgTypeEnum } from '@/network/webSocket';
  15. import { useAppStore } from '@/store/app';
  16. import { useNetworkStore } from '@/store/network';
  17. import { useUserStore } from '@/store/user';
  18. import { createVideo } from '@/utils';
  19. import { loginTip } from './use-login';
  20. import { useTip } from './use-tip';
  21. import { useWs } from './use-ws';
  22. export function usePush({
  23. localVideoRef,
  24. remoteVideoRef,
  25. isSRS,
  26. }: {
  27. localVideoRef: Ref<HTMLVideoElement | undefined>;
  28. remoteVideoRef: Ref<HTMLVideoElement[]>;
  29. isSRS: boolean;
  30. }) {
  31. const route = useRoute();
  32. const router = useRouter();
  33. const appStore = useAppStore();
  34. const userStore = useUserStore();
  35. const networkStore = useNetworkStore();
  36. const roomId = ref('');
  37. const roomName = ref('');
  38. const danmuStr = ref('');
  39. const isLiving = ref(false);
  40. const videoElArr = ref<HTMLVideoElement[]>([]);
  41. const allMediaTypeList: {
  42. [index: string]: { type: MediaTypeEnum; txt: string };
  43. } = {
  44. [MediaTypeEnum.camera]: {
  45. type: MediaTypeEnum.camera,
  46. txt: '摄像头',
  47. },
  48. [MediaTypeEnum.screen]: {
  49. type: MediaTypeEnum.screen,
  50. txt: '窗口',
  51. },
  52. [MediaTypeEnum.microphone]: {
  53. type: MediaTypeEnum.microphone,
  54. txt: '麦克风',
  55. },
  56. };
  57. const {
  58. getSocketId,
  59. initWs,
  60. fabricCanvasEl,
  61. canvasVideoStream,
  62. lastCoverImg,
  63. heartbeatTimer,
  64. localStream,
  65. liveUserList,
  66. damuList,
  67. maxBitrate,
  68. maxFramerate,
  69. resolutionRatio,
  70. currentMaxFramerate,
  71. currentMaxBitrate,
  72. currentResolutionRatio,
  73. addTrack,
  74. delTrack,
  75. } = useWs();
  76. watch(
  77. () => localStream.value,
  78. (stream) => {
  79. console.log('localStream变了');
  80. console.log('音频轨:', stream?.getAudioTracks());
  81. console.log('视频轨:', stream?.getVideoTracks());
  82. videoElArr.value.forEach((dom) => {
  83. dom.remove();
  84. });
  85. stream?.getVideoTracks().forEach((track) => {
  86. console.log('视频轨enabled:', track.id, track.enabled);
  87. const video = createVideo({});
  88. video.setAttribute('track-id', track.id);
  89. video.srcObject = new MediaStream([track]);
  90. localVideoRef.value?.appendChild(video);
  91. videoElArr.value.push(video);
  92. });
  93. stream?.getAudioTracks().forEach((track) => {
  94. console.log('音频轨enabled:', track.id, track.enabled);
  95. const video = createVideo({});
  96. video.setAttribute('track-id', track.id);
  97. video.srcObject = new MediaStream([track]);
  98. localVideoRef.value?.appendChild(video);
  99. videoElArr.value.push(video);
  100. });
  101. },
  102. { deep: true }
  103. );
  104. watch(
  105. () => userStore.userInfo,
  106. async (newVal) => {
  107. if (newVal) {
  108. const res = await userHasLiveRoom();
  109. if (!res) {
  110. await useTip('你还没有直播间,是否立即开通?');
  111. await handleCreateUserLiveRoom();
  112. }
  113. }
  114. },
  115. { immediate: true }
  116. );
  117. onMounted(() => {
  118. roomId.value = route.query.roomId as string;
  119. if (!loginTip()) return;
  120. });
  121. onUnmounted(() => {
  122. closeWs();
  123. closeRtc();
  124. });
  125. function handleCoverImg(dom: HTMLVideoElement) {
  126. const canvas = document.createElement('canvas');
  127. const { width, height } = dom.getBoundingClientRect();
  128. const rate = width / height;
  129. const coverWidth = width * 0.5;
  130. const coverHeight = coverWidth / rate;
  131. canvas.width = coverWidth;
  132. canvas.height = coverHeight;
  133. canvas.getContext('2d')!.drawImage(dom, 0, 0, coverWidth, coverHeight);
  134. // webp比png的体积小非常多!因此coverWidth就可以不用压缩太夸张
  135. const dataURL = canvas.toDataURL('image/webp');
  136. return dataURL;
  137. }
  138. function closeWs() {
  139. const instance = networkStore.wsMap.get(roomId.value);
  140. instance?.close();
  141. }
  142. function closeRtc() {
  143. networkStore.rtcMap.forEach((rtc) => {
  144. rtc.close();
  145. });
  146. }
  147. async function userHasLiveRoom() {
  148. const res = await fetchUserHasLiveRoom(userStore.userInfo?.id!);
  149. if (res.code === 200 && res.data) {
  150. roomName.value = res.data.live_room?.name || '';
  151. roomId.value = `${res.data.live_room?.id || ''}`;
  152. router.push({ query: { ...route.query, roomId: roomId.value } });
  153. return true;
  154. }
  155. return false;
  156. }
  157. async function handleCreateUserLiveRoom() {
  158. try {
  159. const res = await fetchCreateUserLiveRoom();
  160. if (res.code === 200) {
  161. window.$message.success('开通直播间成功!');
  162. setTimeout(() => {
  163. windowReload();
  164. }, 500);
  165. }
  166. } catch (error) {
  167. console.log(error);
  168. }
  169. }
  170. async function startLive() {
  171. if (!loginTip()) return;
  172. const flag = await userHasLiveRoom();
  173. if (!flag) {
  174. await useTip('你还没有直播间,是否立即开通?');
  175. await handleCreateUserLiveRoom();
  176. return;
  177. }
  178. if (!roomNameIsOk()) return;
  179. if (appStore.allTrack.length <= 0) {
  180. window.$message.warning('请选择一个素材!');
  181. return;
  182. }
  183. isLiving.value = true;
  184. const el = appStore.allTrack.find((item) => {
  185. if (item.video === 1) {
  186. return true;
  187. }
  188. });
  189. if (el) {
  190. const res1 = videoElArr.value.find(
  191. (item) => item.getAttribute('track-id') === el.track.id
  192. );
  193. if (res1) {
  194. lastCoverImg.value = handleCoverImg(res1);
  195. }
  196. }
  197. initWs({
  198. isAnchor: true,
  199. roomId: roomId.value,
  200. isSRS,
  201. isPull: false,
  202. currentMaxBitrate: currentMaxBitrate.value,
  203. currentMaxFramerate: currentMaxFramerate.value,
  204. currentResolutionRatio: currentResolutionRatio.value,
  205. roomLiveType: isSRS ? liveTypeEnum.srsPush : liveTypeEnum.webrtcPush,
  206. });
  207. return;
  208. }
  209. /** 结束直播 */
  210. function endLive() {
  211. isLiving.value = false;
  212. localStream.value = undefined;
  213. clearInterval(heartbeatTimer.value);
  214. const instance = networkStore.wsMap.get(roomId.value);
  215. if (instance) {
  216. instance.send({
  217. msgType: WsMsgTypeEnum.roomNoLive,
  218. data: {},
  219. });
  220. }
  221. setTimeout(() => {
  222. closeWs();
  223. closeRtc();
  224. }, 500);
  225. }
  226. function roomNameIsOk() {
  227. if (!roomName.value.length) {
  228. window.$message.warning('请输入房间名!');
  229. return false;
  230. }
  231. if (roomName.value.length < 3 || roomName.value.length > 30) {
  232. window.$message.warning('房间名要求3-30个字符!');
  233. return false;
  234. }
  235. return true;
  236. }
  237. function keydownDanmu(event: KeyboardEvent) {
  238. const key = event.key.toLowerCase();
  239. if (key === 'enter') {
  240. event.preventDefault();
  241. sendDanmu();
  242. }
  243. }
  244. function confirmRoomName() {
  245. if (!roomNameIsOk()) return;
  246. }
  247. function sendDanmu() {
  248. if (!danmuStr.value.length) {
  249. window.$message.warning('请输入弹幕内容!');
  250. return;
  251. }
  252. const instance = networkStore.wsMap.get(roomId.value);
  253. if (!instance) {
  254. window.$message.error('还没开播,不能发送弹幕!');
  255. return;
  256. }
  257. const messageData: IMessage['data'] = {
  258. msg: danmuStr.value,
  259. msgType: DanmuMsgTypeEnum.danmu,
  260. live_room_id: Number(roomId.value),
  261. };
  262. instance.send({
  263. msgType: WsMsgTypeEnum.message,
  264. data: messageData,
  265. });
  266. damuList.value.push({
  267. socket_id: getSocketId(),
  268. msgType: DanmuMsgTypeEnum.danmu,
  269. msg: danmuStr.value,
  270. userInfo: userStore.userInfo,
  271. });
  272. danmuStr.value = '';
  273. }
  274. return {
  275. confirmRoomName,
  276. getSocketId,
  277. startLive,
  278. endLive,
  279. sendDanmu,
  280. keydownDanmu,
  281. localStream,
  282. fabricCanvasEl,
  283. canvasVideoStream,
  284. isLiving,
  285. allMediaTypeList,
  286. currentResolutionRatio,
  287. currentMaxBitrate,
  288. currentMaxFramerate,
  289. resolutionRatio,
  290. maxBitrate,
  291. maxFramerate,
  292. danmuStr,
  293. roomName,
  294. damuList,
  295. liveUserList,
  296. addTrack,
  297. delTrack,
  298. };
  299. }