UNPKG

mediasoup-client

Version:

mediasoup client side TypeScript library

550 lines (549 loc) 21.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Device = void 0; exports.detectDevice = detectDevice; exports.detectDeviceAsync = detectDeviceAsync; const Logger_1 = require("./Logger"); const enhancedEvents_1 = require("./enhancedEvents"); const errors_1 = require("./errors"); const utils = require("./utils"); const ortc = require("./ortc"); const Transport_1 = require("./Transport"); const Chrome111_1 = require("./handlers/Chrome111"); const Chrome74_1 = require("./handlers/Chrome74"); const Firefox120_1 = require("./handlers/Firefox120"); const Safari12_1 = require("./handlers/Safari12"); const ReactNative106_1 = require("./handlers/ReactNative106"); const logger = new Logger_1.Logger('Device'); /** * Sync mediasoup-client Handler detection. */ function detectDevice(userAgent, userAgentData) { logger.debug('detectDevice()'); if (!userAgent && typeof navigator === 'object') { userAgent = navigator.userAgent; } if (!userAgentData && typeof navigator === 'object') { userAgentData = navigator.userAgentData; } return detectDeviceImpl(userAgent, userAgentData); } /** * Async mediasoup-client Handler detection. * * @remarks * - Currently it runs same logic than `detectDevice()`. * - In the future this function could give better results than * `detectDevice()`. */ async function detectDeviceAsync(userAgent, userAgentData) { logger.debug('detectDeviceAsync()'); if (!userAgent && typeof navigator === 'object') { userAgent = navigator.userAgent; } if (!userAgentData && typeof navigator === 'object') { userAgentData = navigator.userAgentData; } return detectDeviceImpl(userAgent, userAgentData); } class Device { // RTC handler factory. _handlerFactory; // Handler name. _handlerName; // Loaded flag. _loaded = false; // Callback for sending Transports to request sending extended RTP capabilities // on demand. _getSendExtendedRtpCapabilities; // Local RTP capabilities for receiving media. _recvRtpCapabilities; // Local RTP capabilities for sending media. _sendRtpCapabilities; // Whether we can produce audio/video based on remote RTP capabilities. _canProduceByKind = { audio: false, video: false, }; // Local SCTP capabilities. _sctpCapabilities; // Observer instance. _observer = new enhancedEvents_1.EnhancedEventEmitter(); /** * Create a new Device to connect to mediasoup server. It uses a more advanced * device detection. * * @throws {UnsupportedError} if device is not supported. */ static async factory({ handlerName, handlerFactory, } = {}) { logger.debug('factory()'); if (handlerName && handlerFactory) { throw new TypeError('just one of handlerName or handlerInterface can be given'); } if (!handlerName && !handlerFactory) { handlerName = await detectDeviceAsync(); if (!handlerName) { throw new errors_1.UnsupportedError('device not supported'); } } return new Device({ handlerName, handlerFactory }); } /** * Create a new Device to connect to mediasoup server. * * @throws {UnsupportedError} if device is not supported. */ constructor({ handlerName, handlerFactory } = {}) { logger.debug('constructor()'); if (handlerName && handlerFactory) { throw new TypeError('just one of handlerName or handlerInterface can be given'); } if (handlerFactory) { this._handlerFactory = handlerFactory; } else { if (handlerName) { logger.debug('constructor() | handler given: %s', handlerName); } else { handlerName = detectDevice(); if (handlerName) { logger.debug('constructor() | detected handler: %s', handlerName); } else { throw new errors_1.UnsupportedError('device not supported'); } } switch (handlerName) { case 'Chrome111': { this._handlerFactory = Chrome111_1.Chrome111.createFactory(); break; } case 'Chrome74': { this._handlerFactory = Chrome74_1.Chrome74.createFactory(); break; } case 'Firefox120': { this._handlerFactory = Firefox120_1.Firefox120.createFactory(); break; } case 'Safari12': { this._handlerFactory = Safari12_1.Safari12.createFactory(); break; } case 'ReactNative106': { this._handlerFactory = ReactNative106_1.ReactNative106.createFactory(); break; } default: { throw new TypeError(`unknown handlerName "${handlerName}"`); } } } this._handlerName = this._handlerFactory.name; } /** * The RTC handler name. */ get handlerName() { return this._handlerName; } /** * Whether the Device is loaded. */ get loaded() { return this._loaded; } /** * RTP capabilities of the Device for receiving media. * * @deprecated Use {@link recvRtpCapabilities} instead. * * @throws {InvalidStateError} if not loaded. */ get rtpCapabilities() { return this.recvRtpCapabilities; } /** * RTP capabilities of the Device for receiving media. * * @throws {InvalidStateError} if not loaded. */ get recvRtpCapabilities() { if (!this._loaded) { throw new errors_1.InvalidStateError('not loaded'); } return this._recvRtpCapabilities; } /** * RTP capabilities of the Device for sending media. * * @throws {InvalidStateError} if not loaded. */ get sendRtpCapabilities() { if (!this._loaded) { throw new errors_1.InvalidStateError('not loaded'); } return this._sendRtpCapabilities; } /** * SCTP capabilities of the Device. * * @throws {InvalidStateError} if not loaded. */ get sctpCapabilities() { if (!this._loaded) { throw new errors_1.InvalidStateError('not loaded'); } return this._sctpCapabilities; } get observer() { return this._observer; } /** * Initialize the Device. */ async load({ routerRtpCapabilities, preferLocalCodecsOrder = false, }) { logger.debug('load() [routerRtpCapabilities:%o]', routerRtpCapabilities); if (this._loaded) { throw new errors_1.InvalidStateError('already loaded'); } // Clone given router RTP capabilities to not modify input data. const clonedRouterRtpCapabilities = utils.clone(routerRtpCapabilities); // This may throw. ortc.validateAndNormalizeRtpCapabilities(clonedRouterRtpCapabilities); const { getNativeRtpCapabilities, getNativeSctpCapabilities } = this._handlerFactory; const clonedNativeRecvRtpCapabilities = utils.clone(await getNativeRtpCapabilities({ direction: 'recvonly' })); logger.debug('load() | got native receiving RTP capabilities:%o', clonedNativeRecvRtpCapabilities); // This may throw. ortc.validateAndNormalizeRtpCapabilities(clonedNativeRecvRtpCapabilities); const clonedNativeSendRtpCapabilities = utils.clone(await getNativeRtpCapabilities({ direction: 'sendonly' })); logger.debug('load() | got native sending RTP capabilities:%o', clonedNativeSendRtpCapabilities); // This may throw. ortc.validateAndNormalizeRtpCapabilities(clonedNativeSendRtpCapabilities); this._getSendExtendedRtpCapabilities = (nativeSendRtpCapabilities) => { return utils.clone(ortc.getExtendedRtpCapabilities(nativeSendRtpCapabilities, clonedRouterRtpCapabilities, preferLocalCodecsOrder)); }; const recvExtendedRtpCapabilities = ortc.getExtendedRtpCapabilities(clonedNativeRecvRtpCapabilities, clonedRouterRtpCapabilities, /* preferLocalCodecsOrder */ false); // Generate our receiving RTP capabilities for receiving media. this._recvRtpCapabilities = ortc.getRecvRtpCapabilities(recvExtendedRtpCapabilities); logger.debug('load() | got receiving RTP capabilities:%o', this._recvRtpCapabilities); // This may throw. ortc.validateAndNormalizeRtpCapabilities(this._recvRtpCapabilities); const sendExtendedRtpCapabilities = ortc.getExtendedRtpCapabilities(clonedNativeSendRtpCapabilities, clonedRouterRtpCapabilities, preferLocalCodecsOrder); // Generate our sending RTP capabilities for sending media. this._sendRtpCapabilities = ortc.getSendRtpCapabilities(sendExtendedRtpCapabilities); logger.debug('load() | got sending RTP capabilities:%o', this._sendRtpCapabilities); // This may throw. ortc.validateAndNormalizeRtpCapabilities(this._sendRtpCapabilities); // Check whether we can produce audio/video. this._canProduceByKind.audio = ortc.canSend('audio', this._sendRtpCapabilities); this._canProduceByKind.video = ortc.canSend('video', this._sendRtpCapabilities); // Generate our SCTP capabilities. this._sctpCapabilities = await getNativeSctpCapabilities(); // This may throw. ortc.validateSctpCapabilities(this._sctpCapabilities); logger.debug('load() | got native SCTP capabilities:%o', this._sctpCapabilities); logger.debug('load() succeeded'); this._loaded = true; } /** * Whether we can produce audio/video. * * @throws {InvalidStateError} if not loaded. * @throws {TypeError} if wrong arguments. */ canProduce(kind) { if (!this._loaded) { throw new errors_1.InvalidStateError('not loaded'); } else if (kind !== 'audio' && kind !== 'video') { throw new TypeError(`invalid kind "${kind}"`); } return this._canProduceByKind[kind]; } /** * Creates a Transport for sending media. * * @throws {InvalidStateError} if not loaded. * @throws {TypeError} if wrong arguments. */ createSendTransport({ id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, appData, }) { logger.debug('createSendTransport()'); return this.createTransport({ direction: 'send', id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, appData, }); } /** * Creates a Transport for receiving media. * * @throws {InvalidStateError} if not loaded. * @throws {TypeError} if wrong arguments. */ createRecvTransport({ id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, appData, }) { logger.debug('createRecvTransport()'); return this.createTransport({ direction: 'recv', id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, appData, }); } createTransport({ direction, id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, appData, }) { if (!this._loaded) { throw new errors_1.InvalidStateError('not loaded'); } else if (typeof id !== 'string') { throw new TypeError('missing id'); } else if (typeof iceParameters !== 'object') { throw new TypeError('missing iceParameters'); } else if (!Array.isArray(iceCandidates)) { throw new TypeError('missing iceCandidates'); } else if (typeof dtlsParameters !== 'object') { throw new TypeError('missing dtlsParameters'); } else if (sctpParameters && typeof sctpParameters !== 'object') { throw new TypeError('wrong sctpParameters'); } else if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); } // Create a new Transport. const transport = new Transport_1.Transport({ direction, id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, appData, handlerFactory: this._handlerFactory, getSendExtendedRtpCapabilities: this._getSendExtendedRtpCapabilities, recvRtpCapabilities: this._recvRtpCapabilities, canProduceByKind: this._canProduceByKind, }); // Emit observer event. this._observer.safeEmit('newtransport', transport); return transport; } } exports.Device = Device; function detectDeviceImpl(userAgent, userAgentData) { logger.debug('detectDeviceImpl() [userAgent:"%s", userAgentData:%o]', userAgent, userAgentData); const chromiumMajorVersion = getChromiumMajorVersion(userAgent, userAgentData); if (chromiumMajorVersion) { if (chromiumMajorVersion >= 111) { logger.debug('detectDeviceImpl() | using Chrome111 handler'); return 'Chrome111'; } else if (chromiumMajorVersion >= 74) { logger.debug('detectDeviceImpl() | using Chrome74 handler'); return 'Chrome74'; } else { logger.warn('detectDeviceImpl() | unsupported Chromium based browser/version'); return undefined; } } const firefoxMajorVersion = getFirefoxMajorVersion(userAgent); if (firefoxMajorVersion) { if (firefoxMajorVersion >= 120) { logger.debug('detectDeviceImpl() | using Firefox120 handler'); return 'Firefox120'; } else { logger.warn('detectDeviceImpl() | unsupported Firefox browser/version'); return undefined; } } const macOSWebKitMajorVersion = getMacOSWebKitMajorVersion(userAgent); if (macOSWebKitMajorVersion) { if (macOSWebKitMajorVersion >= 605) { logger.debug('detectDeviceImpl() | using Safari12 handler'); return 'Safari12'; } else { logger.warn('detectDeviceImpl() | unsupported desktop Safari browser/version'); return undefined; } } const iOSWebKitMajorVersion = getIOSWebKitMajorVersion(userAgent); if (iOSWebKitMajorVersion) { if (iOSWebKitMajorVersion >= 605) { logger.debug('detectDeviceImpl() | using Safari12 handler'); return 'Safari12'; } else { logger.warn('detectDeviceImpl() | unsupported iOS Safari based browser/version'); return undefined; } } if (isReactNative()) { if (typeof RTCPeerConnection !== 'undefined' && typeof RTCRtpTransceiver !== 'undefined') { logger.debug('detectDeviceImpl() | using ReactNative106 handler'); return 'ReactNative106'; } else { logger.warn('detectDeviceImpl() | unsupported react-native-webrtc version without RTCPeerConnection or RTCRtpTransceiver, forgot to call registerGlobals() on it?'); return undefined; } } logger.warn('detectDeviceImpl() | device not supported [userAgent:"%s", userAgentData:%o]', userAgent, userAgentData); return undefined; } function getChromiumMajorVersion(userAgent, userAgentData) { logger.debug('getChromiumMajorVersion()'); if (isIOS(userAgent, userAgentData)) { logger.debug('getChromiumMajorVersion() | this is iOS => undefined'); return undefined; } if (isReactNative()) { logger.debug('getChromiumMajorVersion() | this is React-Native => undefined'); return undefined; } if (userAgentData) { // Some nasty browser extensions define their own custom // `navigator.userAgentData`` without mandatory `brands` field (or with // `brands` with string value instead of array), so let's be ready for it. const brands = Array.isArray(userAgentData.brands) ? userAgentData.brands : []; const chromiumBrand = brands.find(b => b.brand === 'Chromium'); if (chromiumBrand) { const majorVersion = Number(chromiumBrand.version); logger.debug(`getChromiumMajorVersion() | Chromium major version based on NavigatorUAData => ${majorVersion}`); return majorVersion; } } const match = userAgent?.match(/\b(?:Chrome|Chromium)\/(\w+)/i); if (match?.[1]) { const majorVersion = Number(match[1]); logger.debug(`getChromiumMajorVersion() | Chromium major version based on User-Agent => ${majorVersion}`); return majorVersion; } logger.debug('getChromiumMajorVersion() | this is not Chromium => undefined'); return undefined; } function getFirefoxMajorVersion(userAgent) { logger.debug('getFirefoxMajorVersion()'); if (isIOS(userAgent)) { logger.debug('getFirefoxMajorVersion() | this is iOS => undefined'); return undefined; } if (isReactNative()) { logger.debug('getFirefoxMajorVersion() | this is React-Native => undefined'); return undefined; } const match = userAgent?.match(/\bFirefox\/(\w+)/i); if (match?.[1]) { const majorVersion = Number(match[1]); logger.debug(`getFirefoxMajorVersion() | Firefox major version based on User-Agent => ${majorVersion}`); return majorVersion; } logger.debug('getFirefoxMajorVersion() | this is not Firefox => undefined'); return undefined; } function getMacOSWebKitMajorVersion(userAgent) { logger.debug('getMacOSWebKitMajorVersion()'); if (isIOS(userAgent)) { logger.debug('getMacOSWebKitMajorVersion() | this is iOS => undefined'); return undefined; } if (isReactNative()) { logger.debug('getMacOSWebKitMajorVersion() | this is React-Native => undefined'); return undefined; } const isSafari = userAgent && /\bSafari\b/i.test(userAgent) && !/\bChrome\b/i.test(userAgent) && !/\bChromium\b/i.test(userAgent) && !/\bFirefox\b/i.test(userAgent); if (!isSafari) { logger.debug('getMacOSWebKitMajorVersion() | this is not Safari => undefined'); return undefined; } const match = userAgent.match(/AppleWebKit\/(\w+)/i); if (match?.[1]) { const majorVersion = Number(match[1]); logger.debug(`getMacOSWebKitMajorVersion() | WebKit major version based on User-Agent => ${majorVersion}`); return majorVersion; } logger.debug('getMacOSWebKitMajorVersion() | this is not WebKit => undefined'); return undefined; } function getIOSWebKitMajorVersion(userAgent) { logger.debug('getIOSWebKitMajorVersion()'); if (!isIOS(userAgent)) { logger.debug('getIOSWebKitMajorVersion() | this is not iOS => undefined'); return undefined; } if (isReactNative()) { logger.debug('getIOSWebKitMajorVersion() | this is React-Native => undefined'); return undefined; } const match = userAgent?.match(/AppleWebKit\/(\w+)/i); if (match?.[1]) { const majorVersion = Number(match[1]); logger.debug(`getIOSWebKitMajorVersion() | WebKit major version based on User-Agent => ${majorVersion}`); return majorVersion; } logger.debug('getIOSWebKitMajorVersion() | this is not WebKit => undefined'); return undefined; } function isIOS(userAgent, userAgentData) { logger.debug('isIOS()'); if (userAgentData?.platform === 'iOS') { logger.debug('isIOS() | this is iOS based on NavigatorUAData.platform => true'); return true; } if (userAgentData?.platform) { logger.debug('isIOS() | this is not iOS based on NavigatorUAData.platform => false'); return false; } if (userAgent && /iPad|iPhone|iPod/.test(userAgent)) { logger.debug('isIOS() | this is iOS based on User-Agent => true'); return true; } // iPadOS 13+ identifies itself as Mac (to force desktop view mode in some // websites) but we know it's iOS if it has touch screen. if (typeof navigator === 'object' && navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) { logger.debug('isIOS() | this is iPadOS 13+ based on User-Agent => true'); return true; } logger.debug('isIOS() | this is not iOS => false'); return false; } function isReactNative() { logger.debug('isReactNative()'); if (typeof navigator === 'object' && navigator.product === 'ReactNative') { logger.debug('isReactNative() | this is React-Native based on navigator.product'); return true; } logger.debug('isReactNative() | this is not React-Native => false'); return false; }