Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

Commit

Permalink
api: use modern style constraints
Browse files Browse the repository at this point in the history
Legacy constraints are still supported. This unlocks changing the
capture resolution and framerate.

In addition, proper createOffer / createAnswer options are implemented:
exposed as options in JS, but translated to constraints-style in native
since that's what the Java and Objective-C APIs have to offer.
  • Loading branch information
saghul committed Jul 24, 2019
1 parent 762a7e2 commit e78b092
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 351 deletions.
22 changes: 9 additions & 13 deletions RTCPeerConnection.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import EventTarget from 'event-target-shim';
import {DeviceEventEmitter, NativeModules} from 'react-native';
import * as RTCUtil from './RTCUtil';

import MediaStream from './MediaStream';
import MediaStreamEvent from './MediaStreamEvent';
Expand All @@ -14,6 +13,7 @@ import RTCSessionDescription from './RTCSessionDescription';
import RTCIceCandidate from './RTCIceCandidate';
import RTCIceCandidateEvent from './RTCIceCandidateEvent';
import RTCEvent from './RTCEvent';
import * as RTCUtil from './RTCUtil';

const {WebRTCModule} = NativeModules;

Expand All @@ -40,15 +40,11 @@ type RTCIceConnectionState =
'closed';

/**
* The default constraints of RTCPeerConnection's createOffer() and
* createAnswer().
* The default constraints of RTCPeerConnection's createOffer().
*/
const DEFAULT_SDP_CONSTRAINTS = {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true,
},
optional: [],
const DEFAULT_OFFER_OPTIONS = {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};

const PEER_CONNECTION_EVENTS = [
Expand Down Expand Up @@ -117,11 +113,11 @@ export default class RTCPeerConnection extends EventTarget(PEER_CONNECTION_EVENT
}
}

createOffer(options) {
createOffer(options = DEFAULT_OFFER_OPTIONS) {
return new Promise((resolve, reject) => {
WebRTCModule.peerConnectionCreateOffer(
this._peerConnectionId,
RTCUtil.mergeMediaConstraints(options, DEFAULT_SDP_CONSTRAINTS),
RTCUtil.normalizeOfferAnswerOptions(options),
(successful, data) => {
if (successful) {
resolve(new RTCSessionDescription(data));
Expand All @@ -132,11 +128,11 @@ export default class RTCPeerConnection extends EventTarget(PEER_CONNECTION_EVENT
});
}

createAnswer(options) {
createAnswer(options = {}) {
return new Promise((resolve, reject) => {
WebRTCModule.peerConnectionCreateAnswer(
this._peerConnectionId,
RTCUtil.mergeMediaConstraints(options, DEFAULT_SDP_CONSTRAINTS),
RTCUtil.normalizeOfferAnswerOptions(options),
(successful, data) => {
if (successful) {
resolve(new RTCSessionDescription(data));
Expand Down
177 changes: 160 additions & 17 deletions RTCUtil.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,178 @@
'use strict';

const DEFAULT_AUDIO_CONSTRAINTS = {};

const DEFAULT_VIDEO_CONSTRAINTS = {
facingMode: 'user',
frameRate: 30,
height: 720,
width: 1280
};

const ASPECT_RATIO = 16 / 9;

const STANDARD_OA_OPTIONS = [
'iceRestart',
'offerToReceiveAudio',
'offerToReceiveVideo',
'voiceActivityDetection'
];

function getDefaultMediaConstraints(mediaType) {
switch(mediaType) {
case 'audio':
return DEFAULT_AUDIO_CONSTRAINTS;
case 'video':
return DEFAULT_VIDEO_CONSTRAINTS;
default:
throw new TypeError(`Invalid media type: ${mediaType}`);
}
}

function extractString(constraints, prop) {
const value = constraints[prop];
const type = typeof value;

if (type === 'object') {
for (const v of [ 'exact', 'ideal' ]) {
if (value[v]) {
return value[v];
}
}
} else if (type === 'string') {
return value;
}
}

function extractNumber(constraints, prop) {
const value = constraints[prop];
const type = typeof value;

if (type === 'number') {
return Number.parseInt(value);
} else if (type === 'object') {
for (const v of [ 'exact', 'ideal', 'min', 'max' ]) {
if (value[v]) {
return Number.parseInt(value[v]);
}
}
}
}

function normalizeMediaConstraints(constraints, mediaType) {
switch(mediaType) {
case 'audio':
return constraints;
case 'video': {
let c;
if (constraints.mandatory) {
// Old style.
c = {
deviceId: extractString(constraints.optional || {}, 'sourceId'),
facingMode: extractString(constraints, 'facingMode'),
frameRate: extractNumber(constraints.mandatory, 'minFrameRate'),
height: extractNumber(constraints.mandatory, 'minHeight'),
width: extractNumber(constraints.mandatory, 'minWidth')
};
} else {
// New style.
c = {
deviceId: extractString(constraints, 'deviceId'),
facingMode: extractString(constraints, 'facingMode'),
frameRate: extractNumber(constraints, 'frameRate'),
height: extractNumber(constraints, 'height'),
width: extractNumber(constraints, 'width')
};
}

if (!c.deviceId) {
delete c.deviceId;
}

if (!c.facingMode || (c.facingMode !== 'user' && c.facingMode !== 'environment')) {
c.facingMode = DEFAULT_VIDEO_CONSTRAINTS.facingMode;
}

if (!c.frameRate) {
c.frameRate = DEFAULT_VIDEO_CONSTRAINTS.frameRate;
}

if (!c.height && !c.width) {
c.height = DEFAULT_VIDEO_CONSTRAINTS.height;
c.width = DEFAULT_VIDEO_CONSTRAINTS.width;
} else if (!c.height) {
c.height = Math.round(c.width / ASPECT_RATIO);
} else if (!c.width) {
c.width = Math.round(c.height * ASPECT_RATIO);
}

return c;
}
default:
throw new TypeError(`Invalid media type: ${mediaType}`);
}
}

/**
* Internal util for deep clone object. Object.assign() only does a shallow copy
*
* @param {Object} obj - object to be cloned
* @return {Object} cloned obj
*/
function _deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
return JSON.parse(JSON.stringify(obj));
}

/**
* Merge custom constraints with the default one. The custom one take precedence.
* Normalize options passed to createOffer() / createAnswer().
*
* @param {Object} custom - custom webrtc constraints
* @param {Object} def - default webrtc constraints
* @return {Object} constraints - merged webrtc constraints
* @param {Object} options - user supplied options
* @return {Object} newOptions - normalized options
*/
export function mergeMediaConstraints(custom, def) {
const constraints = (def ? _deepClone(def) : {});
if (custom) {
if (custom.mandatory) {
constraints.mandatory = {...constraints.mandatory, ...custom.mandatory};
export function normalizeOfferAnswerOptions(options = {}) {
const newOptions = {};

if (!options) {
return newOptions;
}
if (custom.optional && Array.isArray(custom.optional)) {
// `optional` is an array, webrtc only finds first and ignore the rest if duplicate.
constraints.optional = custom.optional.concat(constraints.optional);

// Convert standard options into WebRTC internal constant names.
// See: https://github.com/jitsi/webrtc/blob/0cd6ce4de669bed94ba47b88cb71b9be0341bb81/sdk/media_constraints.cc#L113
for (const [ key, value ] of Object.entries(options)) {
if (STANDARD_OA_OPTIONS.indexOf(key) !== -1) {
// offerToReceiveAudio -> OfferToReceiveAudio
const newKey = key.charAt(0).toUpperCase() + key.slice(1);
newOptions[newKey] = String(Boolean(value));
} else {
newOptions[key] = value;
}
}
if (custom.facingMode) {
constraints.facingMode = custom.facingMode.toString(); // string, 'user' or the default 'environment'

return newOptions;
}

/**
* Normalize the given constraints in something we can work with.
*/
export function normalizeConstraints(constraints) {
const c = _deepClone(constraints);

for (const mediaType of [ 'audio', 'video' ]) {
const mediaTypeConstraints = c[mediaType];
const typeofMediaTypeConstraints = typeof mediaTypeConstraints;

if (typeofMediaTypeConstraints !== 'undefined') {
if (typeofMediaTypeConstraints === 'boolean') {
if (mediaTypeConstraints) {
c[mediaType] = getDefaultMediaConstraints(mediaType);
}
} else if (typeofMediaTypeConstraints === 'object') {
c[mediaType] = normalizeMediaConstraints(mediaTypeConstraints, mediaType);
} else {
throw new TypeError(`constraints.${mediaType} is neither a boolean nor a dictionary`);
}
}
}
}
return constraints;

return c;
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,13 @@ class GetUserMediaImpl {
}

private AudioTrack createAudioTrack(ReadableMap constraints) {
MediaConstraints audioConstraints
= webRTCModule.parseMediaConstraints(constraints.getMap("audio"));
ReadableMap audioConstraintsMap = constraints.getMap("audio");

Log.d(TAG, "getUserMedia(audio): " + audioConstraints);
Log.d(TAG, "getUserMedia(audio): " + audioConstraintsMap);

String id = UUID.randomUUID().toString();
PeerConnectionFactory pcFactory = webRTCModule.mFactory;
AudioSource audioSource = pcFactory.createAudioSource(audioConstraints);
AudioSource audioSource = pcFactory.createAudioSource(webRTCModule.constraintsForOptions(audioConstraintsMap));
AudioTrack track = pcFactory.createAudioTrack(id, audioSource);
tracks.put(
id,
Expand Down
Loading

0 comments on commit e78b092

Please sign in to comment.