import type { Ref } from 'vue'; // import { Message as $msg } from '@arco-design/web-vue'; import { getToken } from '@/api/system'; import E2EEWorker from 'livekit-client/e2ee-worker?worker'; import type { // ChatMessage, RoomConnectOptions, RoomOptions, // ScalabilityMode, // SimulationScenario, // VideoCaptureOptions, // VideoCodec, } from 'livekit-client'; import { ConnectionQuality, ConnectionState, DisconnectReason, ExternalE2EEKeyProvider, LocalAudioTrack, // LocalParticipant, LogLevel, MediaDeviceFailure, Participant, ParticipantEvent, RemoteParticipant, // RemoteTrackPublication, // RemoteVideoTrack, Room, RoomEvent, ScreenSharePresets, // Track, TrackPublication, // VideoPresets, // VideoQuality, // createAudioAnalyser, setLogLevel, // supportsAV1, // supportsVP9, } from 'livekit-client'; import FingerprintJS from '@fingerprintjs/fingerprintjs'; // 指纹 const getFingerprint = async () => { const fpPromise = FingerprintJS.load(); const fp = await fpPromise; const result = await fp.get(); // const components = { // languages: result.components.languages, // colorDepth: result.components.colorDepth, // deviceMemory: result.components.deviceMemory, // hardwareConcurrency: result.components.hardwareConcurrency, // timezone: result.components.timezone, // platform: result.components.platform, // vendor: result.components.vendor, // vendorFlavors: result.components.vendorFlavors, // cookiesEnabled: result.components.cookiesEnabled, // colorGamut: result.components.colorGamut, // hdr: result.components.hdr, // videoCard: result.components.videoCard, // }; return result.visitorId; }; const state = { isFrontFacing: false, encoder: new TextEncoder(), decoder: new TextDecoder(), defaultDevices: new Map(), bitrateInterval: undefined as any, e2eeKeyProvider: new ExternalE2EEKeyProvider(), }; let currentRoom: Room | undefined; let startTime: number; function appendLog(...args: any[]) { let logger = ''; for (let i = 0; i < arguments.length; i += 1) { if (typeof args[i] === 'object') { logger += `${ JSON && JSON.stringify ? JSON.stringify(args[i], undefined, 2) : args[i] } `; } else { logger += `${args[i]} `; } } // eslint-disable-next-line no-console console.log(logger); } function participantConnected(participant: Participant) { appendLog( 'participant', participant.identity, 'connected', participant.metadata ); // eslint-disable-next-line no-console console.log('tracks', participant.trackPublications); participant .on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => { appendLog('track was muted', pub.trackSid, participant.identity); }) .on(ParticipantEvent.TrackUnmuted, (pub: TrackPublication) => { appendLog('track was unmuted', pub.trackSid, participant.identity); }) .on(ParticipantEvent.IsSpeakingChanged, () => { // eslint-disable-next-line no-console console.log(participant); }) .on(ParticipantEvent.ConnectionQualityChanged, () => { // eslint-disable-next-line no-console console.log(participant); }); } function participantDisconnected(participant: RemoteParticipant) { appendLog('participant', participant.sid, 'disconnected'); // eslint-disable-next-line no-console console.log(participant); } function handleRoomDisconnect(reason?: DisconnectReason) { if (!currentRoom) return; appendLog('disconnected from room', { reason }); currentRoom = undefined; (window as { [k: string]: any }).currentRoom = undefined; } const connectToRoom = async ( url: string, token: string, roomOptions?: RoomOptions, connectOptions?: RoomConnectOptions, shouldPublish?: boolean ): Promise => { const room = new Room(roomOptions); startTime = Date.now(); await room.prepareConnection(url, token); const prewarmTime = Date.now() - startTime; appendLog(`prewarmed connection in ${prewarmTime}ms`); room .on(RoomEvent.ParticipantConnected, participantConnected) .on(RoomEvent.ParticipantDisconnected, participantDisconnected) .on(RoomEvent.Disconnected, handleRoomDisconnect) .on(RoomEvent.Reconnecting, () => appendLog('Reconnecting to room')) .on(RoomEvent.Reconnected, async () => { appendLog( 'Successfully reconnected. server', await room.engine.getConnectedServerAddress() ); }) .on(RoomEvent.LocalTrackPublished, (pub) => { const track = pub.track as LocalAudioTrack; if (track instanceof LocalAudioTrack) { // const { calculateVolume } = createAudioAnalyser(track); // setInterval(() => { // $('local-volume')?.setAttribute( // 'value', // calculateVolume().toFixed(4) // ); // }, 200); } }) .on(RoomEvent.RoomMetadataChanged, (metadata) => { appendLog('new metadata for room', metadata); }) .on(RoomEvent.MediaDevicesError, (e: Error) => { const failure = MediaDeviceFailure.getFailure(e); appendLog('media device failure', failure); }) .on( RoomEvent.ConnectionQualityChanged, (quality: ConnectionQuality, participant?: Participant) => { appendLog('connection quality changed', participant?.identity, quality); } ) .on(RoomEvent.TrackSubscribed, (track, pub, participant) => { appendLog('subscribed to track', pub.trackSid, participant.identity); }) .on(RoomEvent.TrackUnsubscribed, (_, pub) => { appendLog('unsubscribed from track', pub.trackSid); }) .on(RoomEvent.SignalConnected, async () => { const signalConnectionTime = Date.now() - startTime; appendLog(`signal connection established in ${signalConnectionTime}ms`); // speed up publishing by starting to publish before it's fully connected // publishing is accepted as soon as signal connection has established if (shouldPublish) { await room.localParticipant.enableCameraAndMicrophone(); appendLog(`tracks published in ${Date.now() - startTime}ms`); } }) .on(RoomEvent.TrackStreamStateChanged, (pub, streamState, participant) => { appendLog( `stream state changed for ${pub.trackSid} (${ participant.identity }) to ${streamState.toString()}` ); }); try { // read and set current key from input const cryptoKey = ''; const e2eChack = false; state.e2eeKeyProvider.setKey(cryptoKey); if (e2eChack) { await room.setE2EEEnabled(true); } await room.connect(url, token, connectOptions); const elapsed = Date.now() - startTime; appendLog( `successfully connected to ${room.name} in ${Math.round(elapsed)}ms`, await room.engine.getConnectedServerAddress() ); } catch (error: any) { let message: any = error; if (error.message) { message = error.message; } appendLog('could not connect:', message); return undefined; } currentRoom = room; (window as { [k: string]: any }).currentRoom = room; room.remoteParticipants.forEach((participant) => { participantConnected(participant); }); participantConnected(room.localParticipant); return room; }; function renderBitrate() { if (!currentRoom || currentRoom.state !== ConnectionState.Connected) { return; } const participants: Participant[] = [ ...currentRoom.remoteParticipants.values(), ]; participants.push(currentRoom.localParticipant); // eslint-disable-next-line no-restricted-syntax for (const p of participants) { let totalBitrate = 0; // eslint-disable-next-line no-restricted-syntax for (const t of p.trackPublications.values()) { if (t.track) { totalBitrate += t.track.currentBitrate; } // if (t.source === Track.Source.Camera) { // if (t.videoTrack instanceof RemoteVideoTrack) { // // eslint-disable-next-line no-console // console.log(t.videoTrack.getDecoderImplementation() ?? ''); // } // } } let displayText = ''; if (totalBitrate > 0) { displayText = `${Math.round(totalBitrate / 1024).toLocaleString()} kbps`; } // eslint-disable-next-line no-console console.log(displayText); } } const connectWithFormInput = async ( url: any, token: any, disabled: Ref ) => { const simulcast = true; // 同步播出 const dynacast = true; // Dynacast const forceTURN = false; // 强制转向 const adaptiveStream = true; // 自适应流 const shouldPublish = true; // 发布 // const preferredCodec = '' as VideoCodec; // const scalabilityMode = ''; // const cryptoKey = ''; const autoSubscribe = true; // 自动订阅 const e2eeEnabled = false; const audioOutputId = ''; // E2EE密钥 setLogLevel(LogLevel.debug); const roomOpts: RoomOptions = { adaptiveStream, dynacast, audioOutput: { deviceId: audioOutputId, }, publishDefaults: { simulcast, // videoSimulcastLayers: [VideoPresets.h90, VideoPresets.h216], // videoCodec: preferredCodec || 'vp8', dtx: true, red: true, forceStereo: false, screenShareEncoding: ScreenSharePresets.h1080fps30.encoding, scalabilityMode: 'L3T3_KEY', }, // videoCaptureDefaults: { // resolution: VideoPresets.h720.resolution, // }, e2ee: e2eeEnabled ? { keyProvider: state.e2eeKeyProvider, worker: new E2EEWorker() } : undefined, }; // if ( // roomOpts.publishDefaults?.videoCodec === 'av1' || // roomOpts.publishDefaults?.videoCodec === 'vp9' // ) { // roomOpts.publishDefaults.backupCodec = true; // if (scalabilityMode !== '') { // roomOpts.publishDefaults.scalabilityMode = // scalabilityMode as ScalabilityMode; // } // } const connectOpts: RoomConnectOptions = { autoSubscribe, }; if (forceTURN) { connectOpts.rtcConfig = { iceTransportPolicy: 'relay', }; } await connectToRoom(url, token, roomOpts, connectOpts, shouldPublish); disabled.value = 'open'; state.bitrateInterval = setInterval(renderBitrate, 1000); }; const disconnectRoom = (disabled: Ref) => { if (currentRoom) { currentRoom.disconnect(); } if (state.bitrateInterval) { clearInterval(state.bitrateInterval); } disabled.value = 'close'; }; export const IntercomFn = async (disabled: Ref, guid: string) => { if (disabled.value === 'open') { disconnectRoom(disabled); return; } disabled.value = 'loading'; const fingerprint = await getFingerprint(); const token = await getToken({ Room: guid, Identity: fingerprint }); connectWithFormInput( `wss://${import.meta.env.VITE_API_INTERCOM_IP}`, token, disabled ); }; export default null;