mediasoup-client
Version:
mediasoup client side TypeScript library
500 lines (499 loc) • 19.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Device = void 0;
exports.detectDeviceAsync = detectDeviceAsync;
exports.detectDevice = detectDevice;
const ua_parser_js_1 = require("ua-parser-js");
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 Chrome70_1 = require("./handlers/Chrome70");
const Chrome67_1 = require("./handlers/Chrome67");
const Chrome55_1 = require("./handlers/Chrome55");
const Firefox120_1 = require("./handlers/Firefox120");
const Firefox60_1 = require("./handlers/Firefox60");
const Safari12_1 = require("./handlers/Safari12");
const Safari11_1 = require("./handlers/Safari11");
const Edge11_1 = require("./handlers/Edge11");
const ReactNativeUnifiedPlan_1 = require("./handlers/ReactNativeUnifiedPlan");
const ReactNative_1 = require("./handlers/ReactNative");
const logger = new Logger_1.Logger('Device');
/**
* Async mediasoup-client Handler detection. More powerful than
* `detectDevice()`.
*/
async function detectDeviceAsync(userAgent) {
logger.debug('detectDeviceAsync() [userAgent:%s]', userAgent);
if (!userAgent && typeof navigator === 'object') {
userAgent = navigator.userAgent;
}
const uaParserResult = await (0, ua_parser_js_1.UAParser)(userAgent).withFeatureCheck();
return detectDeviceImpl(uaParserResult);
}
/**
* Sync mediasoup-client Handler detection.
*
* @deprecated It only relies on navigator.userAgent. Use `detectDeviceAsync()`
* instead.
*/
function detectDevice(userAgent) {
logger.debug('detectDevice() [userAgent:%s]', userAgent);
if (!userAgent && typeof navigator === 'object') {
userAgent = navigator.userAgent;
}
const uaParserResult = (0, ua_parser_js_1.UAParser)(userAgent);
return detectDeviceImpl(uaParserResult);
}
class Device {
// RTC handler factory.
_handlerFactory;
// Handler name.
_handlerName;
// Loaded flag.
_loaded = false;
// Extended RTP capabilities.
_extendedRtpCapabilities;
// Local RTP capabilities for receiving media.
_recvRtpCapabilities;
// Whether we can produce audio/video based on computed extended RTP
// capabilities.
_canProduceByKind;
// 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 'Chrome70': {
this._handlerFactory = Chrome70_1.Chrome70.createFactory();
break;
}
case 'Chrome67': {
this._handlerFactory = Chrome67_1.Chrome67.createFactory();
break;
}
case 'Chrome55': {
this._handlerFactory = Chrome55_1.Chrome55.createFactory();
break;
}
case 'Firefox120': {
this._handlerFactory = Firefox120_1.Firefox120.createFactory();
break;
}
case 'Firefox60': {
this._handlerFactory = Firefox60_1.Firefox60.createFactory();
break;
}
case 'Safari12': {
this._handlerFactory = Safari12_1.Safari12.createFactory();
break;
}
case 'Safari11': {
this._handlerFactory = Safari11_1.Safari11.createFactory();
break;
}
case 'Edge11': {
this._handlerFactory = Edge11_1.Edge11.createFactory();
break;
}
case 'ReactNativeUnifiedPlan': {
this._handlerFactory = ReactNativeUnifiedPlan_1.ReactNativeUnifiedPlan.createFactory();
break;
}
case 'ReactNative': {
this._handlerFactory = ReactNative_1.ReactNative.createFactory();
break;
}
default: {
throw new TypeError(`unknown handlerName "${handlerName}"`);
}
}
}
// Create a temporal handler to get its name.
const handler = this._handlerFactory();
this._handlerName = handler.name;
handler.close();
this._extendedRtpCapabilities = undefined;
this._recvRtpCapabilities = undefined;
this._canProduceByKind = {
audio: false,
video: false,
};
this._sctpCapabilities = undefined;
}
/**
* 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.
*
* @throws {InvalidStateError} if not loaded.
*/
get rtpCapabilities() {
if (!this._loaded) {
throw new errors_1.InvalidStateError('not loaded');
}
return this._recvRtpCapabilities;
}
/**
* 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);
// Temporal handler to get its capabilities.
let handler;
try {
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.validateRtpCapabilities(clonedRouterRtpCapabilities);
handler = this._handlerFactory();
const nativeRtpCapabilities = await handler.getNativeRtpCapabilities();
logger.debug('load() | got native RTP capabilities:%o', nativeRtpCapabilities);
// Clone obtained native RTP capabilities to not modify input data.
const clonedNativeRtpCapabilities = utils.clone(nativeRtpCapabilities);
// This may throw.
ortc.validateRtpCapabilities(clonedNativeRtpCapabilities);
// Get extended RTP capabilities.
this._extendedRtpCapabilities = ortc.getExtendedRtpCapabilities(clonedNativeRtpCapabilities, clonedRouterRtpCapabilities, preferLocalCodecsOrder);
logger.debug('load() | got extended RTP capabilities:%o', this._extendedRtpCapabilities);
// Check whether we can produce audio/video.
this._canProduceByKind.audio = ortc.canSend('audio', this._extendedRtpCapabilities);
this._canProduceByKind.video = ortc.canSend('video', this._extendedRtpCapabilities);
// Generate our receiving RTP capabilities for receiving media.
this._recvRtpCapabilities = ortc.getRecvRtpCapabilities(this._extendedRtpCapabilities);
// This may throw.
ortc.validateRtpCapabilities(this._recvRtpCapabilities);
logger.debug('load() | got receiving RTP capabilities:%o', this._recvRtpCapabilities);
// Generate our SCTP capabilities.
this._sctpCapabilities = await handler.getNativeSctpCapabilities();
logger.debug('load() | got native SCTP capabilities:%o', this._sctpCapabilities);
// This may throw.
ortc.validateSctpCapabilities(this._sctpCapabilities);
logger.debug('load() succeeded');
this._loaded = true;
handler.close();
}
catch (error) {
if (handler) {
handler.close();
}
throw error;
}
}
/**
* 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, proprietaryConstraints, appData, }) {
logger.debug('createSendTransport()');
return this.createTransport({
direction: 'send',
id: id,
iceParameters: iceParameters,
iceCandidates: iceCandidates,
dtlsParameters: dtlsParameters,
sctpParameters: sctpParameters,
iceServers: iceServers,
iceTransportPolicy: iceTransportPolicy,
additionalSettings: additionalSettings,
proprietaryConstraints: proprietaryConstraints,
appData: 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, proprietaryConstraints, appData, }) {
logger.debug('createRecvTransport()');
return this.createTransport({
direction: 'recv',
id: id,
iceParameters: iceParameters,
iceCandidates: iceCandidates,
dtlsParameters: dtlsParameters,
sctpParameters: sctpParameters,
iceServers: iceServers,
iceTransportPolicy: iceTransportPolicy,
additionalSettings: additionalSettings,
proprietaryConstraints: proprietaryConstraints,
appData: appData,
});
}
createTransport({ direction, id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, 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,
proprietaryConstraints,
appData,
handlerFactory: this._handlerFactory,
extendedRtpCapabilities: this._extendedRtpCapabilities,
canProduceByKind: this._canProduceByKind,
});
// Emit observer event.
this._observer.safeEmit('newtransport', transport);
return transport;
}
}
exports.Device = Device;
function detectDeviceImpl(uaParserResult) {
// React-Native.
// NOTE: react-native-webrtc >= 1.75.0 is required.
// NOTE: For Unified-Plan support, react-native-webrtc version >= 106.0.0 is
// required.
if (typeof navigator === 'object' && navigator.product === 'ReactNative') {
logger.debug('detectDeviceImpl() | React-Native detected');
if (typeof RTCPeerConnection === 'undefined') {
logger.warn('detectDeviceImpl() | unsupported react-native-webrtc without RTCPeerConnection, forgot to call registerGlobals()?');
return undefined;
}
if (typeof RTCRtpTransceiver !== 'undefined') {
logger.debug('detectDeviceImpl() | ReactNative UnifiedPlan handler chosen');
return 'ReactNativeUnifiedPlan';
}
else {
logger.debug('detectDeviceImpl() | ReactNative PlanB handler chosen');
return 'ReactNative';
}
}
// Browser.
else {
logger.debug('detectDeviceImpl() | browser detected [userAgent:%s, parsed:%o]', uaParserResult.ua, uaParserResult);
const browser = uaParserResult.browser;
const browserName = browser.name?.toLowerCase();
const browserVersion = parseInt(browser.major ?? '0');
const engine = uaParserResult.engine;
const engineName = engine.name?.toLowerCase();
const os = uaParserResult.os;
const osName = os.name?.toLowerCase();
const osVersion = parseFloat(os.version ?? '0');
const device = uaParserResult.device;
const deviceModel = device.model?.toLowerCase();
const isIOS = osName === 'ios' || deviceModel === 'ipad';
const isChrome = browserName &&
[
'chrome',
'chromium',
'mobile chrome',
'chrome webview',
'chrome headless',
].includes(browserName);
const isFirefox = browserName &&
['firefox', 'mobile firefox', 'mobile focus'].includes(browserName);
const isSafari = browserName && ['safari', 'mobile safari'].includes(browserName);
const isEdge = browserName && ['edge'].includes(browserName);
// Chrome, Chromium, and Edge.
if ((isChrome || isEdge) && !isIOS && browserVersion >= 111) {
return 'Chrome111';
}
else if ((isChrome && !isIOS && browserVersion >= 74) ||
(isEdge && !isIOS && browserVersion >= 88)) {
return 'Chrome74';
}
else if (isChrome && !isIOS && browserVersion >= 70) {
return 'Chrome70';
}
else if (isChrome && !isIOS && browserVersion >= 67) {
return 'Chrome67';
}
else if (isChrome && !isIOS && browserVersion >= 55) {
return 'Chrome55';
}
// Firefox.
else if (isFirefox && !isIOS && browserVersion >= 120) {
return 'Firefox120';
}
else if (isFirefox && !isIOS && browserVersion >= 60) {
return 'Firefox60';
}
// Firefox on iOS (so Safari).
else if (isFirefox && isIOS && osVersion >= 14.3) {
return 'Safari12';
}
// Safari with Unified-Plan support enabled.
else if (isSafari &&
browserVersion >= 12 &&
typeof RTCRtpTransceiver !== 'undefined' &&
RTCRtpTransceiver.prototype.hasOwnProperty('currentDirection')) {
return 'Safari12';
}
// Safari with Plab-B support.
else if (isSafari && browserVersion >= 11) {
return 'Safari11';
}
// Old Edge with ORTC support.
else if (isEdge && !isIOS && browserVersion >= 11 && browserVersion <= 18) {
return 'Edge11';
}
// Best effort for WebKit based browsers in iOS.
else if (engineName === 'webkit' &&
isIOS &&
typeof RTCRtpTransceiver !== 'undefined' &&
RTCRtpTransceiver.prototype.hasOwnProperty('currentDirection')) {
return 'Safari12';
}
// Best effort for Chromium based browsers.
else if (engineName === 'blink') {
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
const match = uaParserResult.ua.match(/(?:(?:Chrome|Chromium))[ /](\w+)/i);
if (match) {
const version = Number(match[1]);
if (version >= 111) {
return 'Chrome111';
}
else if (version >= 74) {
return 'Chrome74';
}
else if (version >= 70) {
return 'Chrome70';
}
else if (version >= 67) {
return 'Chrome67';
}
else {
return 'Chrome55';
}
}
else {
return 'Chrome111';
}
}
// Unsupported browser.
else {
logger.warn('detectDeviceImpl() | browser not supported [name:%s, version:%s]', browserName, browserVersion);
return undefined;
}
}
}