UNPKG

@thumbmarkjs/thumbmarkjs

Version:

![GitHub package.json dynamic](https://img.shields.io/github/package-json/version/ilkkapeltola/thumbmarkjs) ![NPM Version](https://img.shields.io/npm/v/@thumbmarkjs/thumbmarkjs) ![NPM Downloads](https://img.shields.io/npm/dm/%40thumbmarkjs%2Fthumbmarkjs

136 lines (116 loc) 4.88 kB
import { componentInterface } from '../../factory'; import { hash } from '../../utils/hash'; import { stableStringify } from '../../utils/stableStringify'; export default async function getWebRTC(): Promise<componentInterface | null> { return new Promise((resolve) => { try { // Check if WebRTC is supported const RTCPeerConnection = (window as any).RTCPeerConnection || (window as any).webkitRTCPeerConnection || (window as any).mozRTCPeerConnection; if (!RTCPeerConnection) { resolve({ supported: false, error: 'WebRTC not supported' }); return; } const config = { iceCandidatePoolSize: 1, iceServers: [] }; const connection = new RTCPeerConnection(config); connection.createDataChannel(''); // trigger ICE gathering const processOffer = async () => { try { const offerOptions = { offerToReceiveAudio: true, offerToReceiveVideo: true }; const offer = await connection.createOffer(offerOptions); await connection.setLocalDescription(offer); const sdp = offer.sdp || ''; // Extract RTP extensions const extensions = [...new Set((sdp.match(/extmap:\d+ [^\n\r]+/g) || []).map((x: string) => x.replace(/extmap:\d+ /, '')))].sort(); // Extract codec information const getDescriptors = (mediaType: string) => { const match = sdp.match(new RegExp(`m=${mediaType} [^\\s]+ [^\\s]+ ([^\\n\\r]+)`)); return match ? match[1].split(' ') : []; }; const constructDescriptions = (mediaType: string, descriptors: string[]) => { return descriptors.map(descriptor => { const matcher = new RegExp(`(rtpmap|fmtp|rtcp-fb):${descriptor} (.+)`, 'g'); const matches = [...sdp.matchAll(matcher)]; if (!matches.length) return null; const description: any = {}; matches.forEach(match => { const [_, type, data] = match; const parts = data.split('/'); if (type === 'rtpmap') { description.mimeType = `${mediaType}/${parts[0]}`; description.clockRate = +parts[1]; if (mediaType === 'audio') description.channels = +parts[2] || 1; } else if (type === 'rtcp-fb') { description.feedbackSupport = description.feedbackSupport || []; description.feedbackSupport.push(data); } else if (type === 'fmtp') { description.sdpFmtpLine = data; } }); return description; }).filter(Boolean); }; const audioCodecs = constructDescriptions('audio', getDescriptors('audio')); const videoCodecs = constructDescriptions('video', getDescriptors('video')); const compressedData = { audio: { count: audioCodecs.length, hash: hash(stableStringify(audioCodecs)) }, video: { count: videoCodecs.length, hash: hash(stableStringify(videoCodecs)) }, extensionsHash: hash(stableStringify(extensions)) }; // Set up for ICE candidate collection with timeout const result = await new Promise<componentInterface>((resolveResult) => { const timeout = setTimeout(() => { connection.removeEventListener('icecandidate', onIceCandidate); connection.close(); resolveResult({ supported: true, ...compressedData, timeout: true }); }, 3000); const onIceCandidate = (event: RTCPeerConnectionIceEvent) => { const candidateObj = event.candidate; if (!candidateObj || !candidateObj.candidate) return; clearTimeout(timeout); connection.removeEventListener('icecandidate', onIceCandidate); connection.close(); resolveResult({ supported: true, ...compressedData, candidateType: candidateObj.type || '' }); }; connection.addEventListener('icecandidate', onIceCandidate); }); resolve({ details: result, hash: hash(stableStringify(result)), }); } catch (error) { connection.close(); resolve({ supported: true, error: `WebRTC offer failed: ${(error as Error).message}` }); } }; processOffer(); } catch (error) { resolve({ supported: false, error: `WebRTC error: ${(error as Error).message}` }); } }); }