mediasoup-client
Version:
mediasoup client side TypeScript library
565 lines (564 loc) • 22.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
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 = __importStar(require("./utils"));
const ortc = __importStar(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,
};
// 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;
}
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 } = 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);
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;
}