Intercom.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import type { Ref } from 'vue';
  2. // import { Message as $msg } from '@arco-design/web-vue';
  3. import { getToken } from '@/api/system';
  4. import E2EEWorker from 'livekit-client/e2ee-worker?worker';
  5. import type {
  6. // ChatMessage,
  7. RoomConnectOptions,
  8. RoomOptions,
  9. // ScalabilityMode,
  10. // SimulationScenario,
  11. // VideoCaptureOptions,
  12. // VideoCodec,
  13. } from 'livekit-client';
  14. import {
  15. ConnectionQuality,
  16. ConnectionState,
  17. DisconnectReason,
  18. ExternalE2EEKeyProvider,
  19. LocalAudioTrack,
  20. // LocalParticipant,
  21. LogLevel,
  22. MediaDeviceFailure,
  23. Participant,
  24. ParticipantEvent,
  25. RemoteParticipant,
  26. // RemoteTrackPublication,
  27. // RemoteVideoTrack,
  28. Room,
  29. RoomEvent,
  30. ScreenSharePresets,
  31. // Track,
  32. TrackPublication,
  33. // VideoPresets,
  34. // VideoQuality,
  35. // createAudioAnalyser,
  36. setLogLevel,
  37. // supportsAV1,
  38. // supportsVP9,
  39. } from 'livekit-client';
  40. import FingerprintJS from '@fingerprintjs/fingerprintjs';
  41. // 指纹
  42. const getFingerprint = async () => {
  43. const fpPromise = FingerprintJS.load();
  44. const fp = await fpPromise;
  45. const result = await fp.get();
  46. // const components = {
  47. // languages: result.components.languages,
  48. // colorDepth: result.components.colorDepth,
  49. // deviceMemory: result.components.deviceMemory,
  50. // hardwareConcurrency: result.components.hardwareConcurrency,
  51. // timezone: result.components.timezone,
  52. // platform: result.components.platform,
  53. // vendor: result.components.vendor,
  54. // vendorFlavors: result.components.vendorFlavors,
  55. // cookiesEnabled: result.components.cookiesEnabled,
  56. // colorGamut: result.components.colorGamut,
  57. // hdr: result.components.hdr,
  58. // videoCard: result.components.videoCard,
  59. // };
  60. return result.visitorId;
  61. };
  62. const state = {
  63. isFrontFacing: false,
  64. encoder: new TextEncoder(),
  65. decoder: new TextDecoder(),
  66. defaultDevices: new Map<MediaDeviceKind, string>(),
  67. bitrateInterval: undefined as any,
  68. e2eeKeyProvider: new ExternalE2EEKeyProvider(),
  69. };
  70. let currentRoom: Room | undefined;
  71. let startTime: number;
  72. function appendLog(...args: any[]) {
  73. let logger = '';
  74. for (let i = 0; i < arguments.length; i += 1) {
  75. if (typeof args[i] === 'object') {
  76. logger += `${
  77. JSON && JSON.stringify ? JSON.stringify(args[i], undefined, 2) : args[i]
  78. } `;
  79. } else {
  80. logger += `${args[i]} `;
  81. }
  82. }
  83. // eslint-disable-next-line no-console
  84. console.log(logger);
  85. }
  86. function participantConnected(participant: Participant) {
  87. appendLog(
  88. 'participant',
  89. participant.identity,
  90. 'connected',
  91. participant.metadata
  92. );
  93. // eslint-disable-next-line no-console
  94. console.log('tracks', participant.trackPublications);
  95. participant
  96. .on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => {
  97. appendLog('track was muted', pub.trackSid, participant.identity);
  98. })
  99. .on(ParticipantEvent.TrackUnmuted, (pub: TrackPublication) => {
  100. appendLog('track was unmuted', pub.trackSid, participant.identity);
  101. })
  102. .on(ParticipantEvent.IsSpeakingChanged, () => {
  103. // eslint-disable-next-line no-console
  104. console.log(participant);
  105. })
  106. .on(ParticipantEvent.ConnectionQualityChanged, () => {
  107. // eslint-disable-next-line no-console
  108. console.log(participant);
  109. });
  110. }
  111. function participantDisconnected(participant: RemoteParticipant) {
  112. appendLog('participant', participant.sid, 'disconnected');
  113. // eslint-disable-next-line no-console
  114. console.log(participant);
  115. }
  116. function handleRoomDisconnect(reason?: DisconnectReason) {
  117. if (!currentRoom) return;
  118. appendLog('disconnected from room', { reason });
  119. currentRoom = undefined;
  120. (window as { [k: string]: any }).currentRoom = undefined;
  121. }
  122. const connectToRoom = async (
  123. url: string,
  124. token: string,
  125. roomOptions?: RoomOptions,
  126. connectOptions?: RoomConnectOptions,
  127. shouldPublish?: boolean
  128. ): Promise<Room | undefined> => {
  129. const room = new Room(roomOptions);
  130. startTime = Date.now();
  131. await room.prepareConnection(url, token);
  132. const prewarmTime = Date.now() - startTime;
  133. appendLog(`prewarmed connection in ${prewarmTime}ms`);
  134. room
  135. .on(RoomEvent.ParticipantConnected, participantConnected)
  136. .on(RoomEvent.ParticipantDisconnected, participantDisconnected)
  137. .on(RoomEvent.Disconnected, handleRoomDisconnect)
  138. .on(RoomEvent.Reconnecting, () => appendLog('Reconnecting to room'))
  139. .on(RoomEvent.Reconnected, async () => {
  140. appendLog(
  141. 'Successfully reconnected. server',
  142. await room.engine.getConnectedServerAddress()
  143. );
  144. })
  145. .on(RoomEvent.LocalTrackPublished, (pub) => {
  146. const track = pub.track as LocalAudioTrack;
  147. if (track instanceof LocalAudioTrack) {
  148. // const { calculateVolume } = createAudioAnalyser(track);
  149. // setInterval(() => {
  150. // $('local-volume')?.setAttribute(
  151. // 'value',
  152. // calculateVolume().toFixed(4)
  153. // );
  154. // }, 200);
  155. }
  156. })
  157. .on(RoomEvent.RoomMetadataChanged, (metadata) => {
  158. appendLog('new metadata for room', metadata);
  159. })
  160. .on(RoomEvent.MediaDevicesError, (e: Error) => {
  161. const failure = MediaDeviceFailure.getFailure(e);
  162. appendLog('media device failure', failure);
  163. })
  164. .on(
  165. RoomEvent.ConnectionQualityChanged,
  166. (quality: ConnectionQuality, participant?: Participant) => {
  167. appendLog('connection quality changed', participant?.identity, quality);
  168. }
  169. )
  170. .on(RoomEvent.TrackSubscribed, (track, pub, participant) => {
  171. appendLog('subscribed to track', pub.trackSid, participant.identity);
  172. })
  173. .on(RoomEvent.TrackUnsubscribed, (_, pub) => {
  174. appendLog('unsubscribed from track', pub.trackSid);
  175. })
  176. .on(RoomEvent.SignalConnected, async () => {
  177. const signalConnectionTime = Date.now() - startTime;
  178. appendLog(`signal connection established in ${signalConnectionTime}ms`);
  179. // speed up publishing by starting to publish before it's fully connected
  180. // publishing is accepted as soon as signal connection has established
  181. if (shouldPublish) {
  182. await room.localParticipant.enableCameraAndMicrophone();
  183. appendLog(`tracks published in ${Date.now() - startTime}ms`);
  184. }
  185. })
  186. .on(RoomEvent.TrackStreamStateChanged, (pub, streamState, participant) => {
  187. appendLog(
  188. `stream state changed for ${pub.trackSid} (${
  189. participant.identity
  190. }) to ${streamState.toString()}`
  191. );
  192. });
  193. try {
  194. // read and set current key from input
  195. const cryptoKey = '';
  196. const e2eChack = false;
  197. state.e2eeKeyProvider.setKey(cryptoKey);
  198. if (e2eChack) {
  199. await room.setE2EEEnabled(true);
  200. }
  201. await room.connect(url, token, connectOptions);
  202. const elapsed = Date.now() - startTime;
  203. appendLog(
  204. `successfully connected to ${room.name} in ${Math.round(elapsed)}ms`,
  205. await room.engine.getConnectedServerAddress()
  206. );
  207. } catch (error: any) {
  208. let message: any = error;
  209. if (error.message) {
  210. message = error.message;
  211. }
  212. appendLog('could not connect:', message);
  213. return undefined;
  214. }
  215. currentRoom = room;
  216. (window as { [k: string]: any }).currentRoom = room;
  217. room.remoteParticipants.forEach((participant) => {
  218. participantConnected(participant);
  219. });
  220. participantConnected(room.localParticipant);
  221. return room;
  222. };
  223. function renderBitrate() {
  224. if (!currentRoom || currentRoom.state !== ConnectionState.Connected) {
  225. return;
  226. }
  227. const participants: Participant[] = [
  228. ...currentRoom.remoteParticipants.values(),
  229. ];
  230. participants.push(currentRoom.localParticipant);
  231. // eslint-disable-next-line no-restricted-syntax
  232. for (const p of participants) {
  233. let totalBitrate = 0;
  234. // eslint-disable-next-line no-restricted-syntax
  235. for (const t of p.trackPublications.values()) {
  236. if (t.track) {
  237. totalBitrate += t.track.currentBitrate;
  238. }
  239. // if (t.source === Track.Source.Camera) {
  240. // if (t.videoTrack instanceof RemoteVideoTrack) {
  241. // // eslint-disable-next-line no-console
  242. // console.log(t.videoTrack.getDecoderImplementation() ?? '');
  243. // }
  244. // }
  245. }
  246. let displayText = '';
  247. if (totalBitrate > 0) {
  248. displayText = `${Math.round(totalBitrate / 1024).toLocaleString()} kbps`;
  249. }
  250. // eslint-disable-next-line no-console
  251. console.log(displayText);
  252. }
  253. }
  254. const connectWithFormInput = async (
  255. url: any,
  256. token: any,
  257. disabled: Ref<string>
  258. ) => {
  259. const simulcast = true; // 同步播出
  260. const dynacast = true; // Dynacast
  261. const forceTURN = false; // 强制转向
  262. const adaptiveStream = true; // 自适应流
  263. const shouldPublish = true; // 发布
  264. // const preferredCodec = '' as VideoCodec;
  265. // const scalabilityMode = '';
  266. // const cryptoKey = '';
  267. const autoSubscribe = true; // 自动订阅
  268. const e2eeEnabled = false;
  269. const audioOutputId = ''; // E2EE密钥
  270. setLogLevel(LogLevel.debug);
  271. const roomOpts: RoomOptions = {
  272. adaptiveStream,
  273. dynacast,
  274. audioOutput: {
  275. deviceId: audioOutputId,
  276. },
  277. publishDefaults: {
  278. simulcast,
  279. // videoSimulcastLayers: [VideoPresets.h90, VideoPresets.h216],
  280. // videoCodec: preferredCodec || 'vp8',
  281. dtx: true,
  282. red: true,
  283. forceStereo: false,
  284. screenShareEncoding: ScreenSharePresets.h1080fps30.encoding,
  285. scalabilityMode: 'L3T3_KEY',
  286. },
  287. // videoCaptureDefaults: {
  288. // resolution: VideoPresets.h720.resolution,
  289. // },
  290. e2ee: e2eeEnabled
  291. ? { keyProvider: state.e2eeKeyProvider, worker: new E2EEWorker() }
  292. : undefined,
  293. };
  294. // if (
  295. // roomOpts.publishDefaults?.videoCodec === 'av1' ||
  296. // roomOpts.publishDefaults?.videoCodec === 'vp9'
  297. // ) {
  298. // roomOpts.publishDefaults.backupCodec = true;
  299. // if (scalabilityMode !== '') {
  300. // roomOpts.publishDefaults.scalabilityMode =
  301. // scalabilityMode as ScalabilityMode;
  302. // }
  303. // }
  304. const connectOpts: RoomConnectOptions = {
  305. autoSubscribe,
  306. };
  307. if (forceTURN) {
  308. connectOpts.rtcConfig = {
  309. iceTransportPolicy: 'relay',
  310. };
  311. }
  312. await connectToRoom(url, token, roomOpts, connectOpts, shouldPublish);
  313. disabled.value = 'open';
  314. state.bitrateInterval = setInterval(renderBitrate, 1000);
  315. };
  316. const disconnectRoom = (disabled: Ref<string>) => {
  317. if (currentRoom) {
  318. currentRoom.disconnect();
  319. }
  320. if (state.bitrateInterval) {
  321. clearInterval(state.bitrateInterval);
  322. }
  323. disabled.value = 'close';
  324. };
  325. export const IntercomFn = async (disabled: Ref<string>, guid: string) => {
  326. if (disabled.value === 'open') {
  327. disconnectRoom(disabled);
  328. return;
  329. }
  330. disabled.value = 'loading';
  331. const fingerprint = await getFingerprint();
  332. const token = await getToken({ Room: guid, Identity: fingerprint });
  333. connectWithFormInput(
  334. `wss://${import.meta.env.VITE_API_INTERCOM_IP}`,
  335. token,
  336. disabled
  337. );
  338. };
  339. export default null;