UNPKG

livekit-client

Version:

JavaScript/TypeScript client SDK for LiveKit

264 lines (242 loc) 8.45 kB
import { workerLogger } from '../../logger'; import { VideoCodec } from '../../room/track/options'; import { KEY_PROVIDER_DEFAULTS } from '../constants'; import { CryptorErrorReason } from '../errors'; import { CryptorEvent, KeyHandlerEvent } from '../events'; import type { E2EEWorkerMessage, ErrorMessage, InitAck, KeyProviderOptions, RatchetMessage, RatchetRequestMessage, } from '../types'; import { FrameCryptor, encryptionEnabledMap } from './FrameCryptor'; import { ParticipantKeyHandler } from './ParticipantKeyHandler'; const participantCryptors: FrameCryptor[] = []; const participantKeys: Map<string, ParticipantKeyHandler> = new Map(); let sharedKeyHandler: ParticipantKeyHandler | undefined; let isEncryptionEnabled: boolean = false; let useSharedKey: boolean = false; let sifTrailer: Uint8Array | undefined; let keyProviderOptions: KeyProviderOptions = KEY_PROVIDER_DEFAULTS; let rtpMap: Map<number, VideoCodec> = new Map(); workerLogger.setDefaultLevel('info'); onmessage = (ev) => { const { kind, data }: E2EEWorkerMessage = ev.data; switch (kind) { case 'init': workerLogger.setLevel(data.loglevel); workerLogger.info('worker initialized'); keyProviderOptions = data.keyProviderOptions; useSharedKey = !!data.keyProviderOptions.sharedKey; // acknowledge init successful const ackMsg: InitAck = { kind: 'initAck', data: { enabled: isEncryptionEnabled }, }; postMessage(ackMsg); break; case 'enable': setEncryptionEnabled(data.enabled, data.participantIdentity); workerLogger.info( `updated e2ee enabled status for ${data.participantIdentity} to ${data.enabled}`, ); // acknowledge enable call successful postMessage(ev.data); break; case 'decode': let cryptor = getTrackCryptor(data.participantIdentity, data.trackId); cryptor.setupTransform( kind, data.readableStream, data.writableStream, data.trackId, data.codec, ); break; case 'encode': let pubCryptor = getTrackCryptor(data.participantIdentity, data.trackId); pubCryptor.setupTransform( kind, data.readableStream, data.writableStream, data.trackId, data.codec, ); break; case 'setKey': if (useSharedKey) { setSharedKey(data.key, data.keyIndex); } else if (data.participantIdentity) { workerLogger.info( `set participant sender key ${data.participantIdentity} index ${data.keyIndex}`, ); getParticipantKeyHandler(data.participantIdentity).setKey(data.key, data.keyIndex); } else { workerLogger.error('no participant Id was provided and shared key usage is disabled'); } break; case 'removeTransform': unsetCryptorParticipant(data.trackId, data.participantIdentity); break; case 'updateCodec': getTrackCryptor(data.participantIdentity, data.trackId).setVideoCodec(data.codec); break; case 'setRTPMap': // this is only used for the local participant rtpMap = data.map; participantCryptors.forEach((cr) => { if (cr.getParticipantIdentity() === data.participantIdentity) { cr.setRtpMap(data.map); } }); break; case 'ratchetRequest': handleRatchetRequest(data); break; case 'setSifTrailer': handleSifTrailer(data.trailer); break; default: break; } }; async function handleRatchetRequest(data: RatchetRequestMessage['data']) { if (useSharedKey) { const keyHandler = getSharedKeyHandler(); await keyHandler.ratchetKey(data.keyIndex); keyHandler.resetKeyStatus(); } else if (data.participantIdentity) { const keyHandler = getParticipantKeyHandler(data.participantIdentity); await keyHandler.ratchetKey(data.keyIndex); keyHandler.resetKeyStatus(); } else { workerLogger.error( 'no participant Id was provided for ratchet request and shared key usage is disabled', ); } } function getTrackCryptor(participantIdentity: string, trackId: string) { let cryptors = participantCryptors.filter((c) => c.getTrackId() === trackId); if (cryptors.length > 1) { const debugInfo = cryptors .map((c) => { return { participant: c.getParticipantIdentity() }; }) .join(','); workerLogger.error( `Found multiple cryptors for the same trackID ${trackId}. target participant: ${participantIdentity} `, { participants: debugInfo }, ); } let cryptor = cryptors[0]; if (!cryptor) { workerLogger.info('creating new cryptor for', { participantIdentity }); if (!keyProviderOptions) { throw Error('Missing keyProvider options'); } cryptor = new FrameCryptor({ participantIdentity, keys: getParticipantKeyHandler(participantIdentity), keyProviderOptions, sifTrailer, }); cryptor.setRtpMap(rtpMap); setupCryptorErrorEvents(cryptor); participantCryptors.push(cryptor); } else if (participantIdentity !== cryptor.getParticipantIdentity()) { // assign new participant id to track cryptor and pass in correct key handler cryptor.setParticipant(participantIdentity, getParticipantKeyHandler(participantIdentity)); } return cryptor; } function getParticipantKeyHandler(participantIdentity: string) { if (useSharedKey) { return getSharedKeyHandler(); } let keys = participantKeys.get(participantIdentity); if (!keys) { keys = new ParticipantKeyHandler(participantIdentity, keyProviderOptions); keys.on(KeyHandlerEvent.KeyRatcheted, emitRatchetedKeys); participantKeys.set(participantIdentity, keys); } return keys; } function getSharedKeyHandler() { if (!sharedKeyHandler) { workerLogger.debug('creating new shared key handler'); sharedKeyHandler = new ParticipantKeyHandler('shared-key', keyProviderOptions); } return sharedKeyHandler; } function unsetCryptorParticipant(trackId: string, participantIdentity: string) { const cryptors = participantCryptors.filter( (c) => c.getParticipantIdentity() === participantIdentity && c.getTrackId() === trackId, ); if (cryptors.length > 1) { workerLogger.error('Found multiple cryptors for the same participant and trackID combination', { trackId, participantIdentity, }); } const cryptor = cryptors[0]; if (!cryptor) { workerLogger.warn('Could not unset participant on cryptor', { trackId, participantIdentity }); } else { cryptor.unsetParticipant(); } } function setEncryptionEnabled(enable: boolean, participantIdentity: string) { workerLogger.debug(`setting encryption enabled for all tracks of ${participantIdentity}`, { enable, }); encryptionEnabledMap.set(participantIdentity, enable); } function setSharedKey(key: CryptoKey, index?: number) { workerLogger.info('set shared key', { index }); getSharedKeyHandler().setKey(key, index); } function setupCryptorErrorEvents(cryptor: FrameCryptor) { cryptor.on(CryptorEvent.Error, (error) => { const msg: ErrorMessage = { kind: 'error', data: { error: new Error(`${CryptorErrorReason[error.reason]}: ${error.message}`) }, }; postMessage(msg); }); } function emitRatchetedKeys(material: CryptoKey, participantIdentity: string, keyIndex?: number) { const msg: RatchetMessage = { kind: `ratchetKey`, data: { participantIdentity, keyIndex, material, }, }; postMessage(msg); } function handleSifTrailer(trailer: Uint8Array) { sifTrailer = trailer; participantCryptors.forEach((c) => { c.setSifTrailer(trailer); }); } // Operations using RTCRtpScriptTransform. // @ts-ignore if (self.RTCTransformEvent) { workerLogger.debug('setup transform event'); // @ts-ignore self.onrtctransform = (event: RTCTransformEvent) => { // @ts-ignore .transformer property is part of RTCTransformEvent const transformer = event.transformer; workerLogger.debug('transformer', transformer); // @ts-ignore monkey patching non standard flag transformer.handled = true; const { kind, participantIdentity, trackId, codec } = transformer.options; const cryptor = getTrackCryptor(participantIdentity, trackId); workerLogger.debug('transform', { codec }); cryptor.setupTransform(kind, transformer.readable, transformer.writable, trackId, codec); }; }