From e78b092ed6b8f59fa927ed998ec9571bc3c22452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Wed, 17 Jul 2019 15:54:02 +0100 Subject: [PATCH] api: use modern style constraints 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. --- RTCPeerConnection.js | 22 +-- RTCUtil.js | 177 ++++++++++++++++-- .../oney/WebRTCModule/GetUserMediaImpl.java | 7 +- .../WebRTCModule/VideoCaptureController.java | 103 ++-------- .../com/oney/WebRTCModule/WebRTCModule.java | 76 ++------ getUserMedia.js | 83 +------- ios/RCTWebRTC/VideoCaptureController.m | 40 ++-- .../WebRTCModule+RTCPeerConnection.m | 79 ++------ 8 files changed, 236 insertions(+), 351 deletions(-) diff --git a/RTCPeerConnection.js b/RTCPeerConnection.js index e0ca3189d..32ba22217 100644 --- a/RTCPeerConnection.js +++ b/RTCPeerConnection.js @@ -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'; @@ -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; @@ -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 = [ @@ -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)); @@ -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)); diff --git a/RTCUtil.js b/RTCUtil.js index 82b71ec54..aa41ef0d3 100644 --- a/RTCUtil.js +++ b/RTCUtil.js @@ -1,5 +1,118 @@ '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 * @@ -7,29 +120,59 @@ * @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; } diff --git a/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java b/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java index 41382fc78..d00f4ea3c 100644 --- a/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java +++ b/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java @@ -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, diff --git a/android/src/main/java/com/oney/WebRTCModule/VideoCaptureController.java b/android/src/main/java/com/oney/WebRTCModule/VideoCaptureController.java index c33c0d2a5..2f77065ef 100644 --- a/android/src/main/java/com/oney/WebRTCModule/VideoCaptureController.java +++ b/android/src/main/java/com/oney/WebRTCModule/VideoCaptureController.java @@ -2,9 +2,7 @@ import android.util.Log; -import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableType; import org.webrtc.CameraEnumerator; import org.webrtc.CameraVideoCapturer; @@ -20,23 +18,15 @@ public class VideoCaptureController { private static final String TAG = VideoCaptureController.class.getSimpleName(); - /** - * Default values for width, height and fps (respectively) which will be - * used to open the camera at. - */ - private static final int DEFAULT_WIDTH = 1280; - private static final int DEFAULT_HEIGHT = 720; - private static final int DEFAULT_FPS = 30; - private boolean isFrontFacing; /** * Values for width, height and fps (respectively) which will be * used to open the camera at. */ - private int width = DEFAULT_WIDTH; - private int height = DEFAULT_HEIGHT; - private int fps = DEFAULT_FPS; + private final int width; + private final int height; + private final int fps; private CameraEnumerator cameraEnumerator; @@ -54,33 +44,17 @@ public class VideoCaptureController { */ private VideoCapturer videoCapturer; - public VideoCaptureController(CameraEnumerator cameraEnumerator, - ReadableMap constraints) { + public VideoCaptureController(CameraEnumerator cameraEnumerator, ReadableMap constraints) { this.cameraEnumerator = cameraEnumerator; - ReadableMap videoConstraintsMandatory = null; + width = constraints.getInt("width"); + height = constraints.getInt("height"); + fps = constraints.getInt("frameRate"); - if (constraints.hasKey("mandatory") - && constraints.getType("mandatory") == ReadableType.Map) { - videoConstraintsMandatory = constraints.getMap("mandatory"); - } + String deviceId = ReactBridgeUtil.getMapStrValue(constraints, "deviceId"); + String facingMode = ReactBridgeUtil.getMapStrValue(constraints, "facingMode"); - String sourceId = getSourceIdConstraint(constraints); - String facingMode = getFacingMode(constraints); - - videoCapturer = createVideoCapturer(sourceId, facingMode); - - if (videoConstraintsMandatory != null) { - width = videoConstraintsMandatory.hasKey("minWidth") - ? videoConstraintsMandatory.getInt("minWidth") - : DEFAULT_WIDTH; - height = videoConstraintsMandatory.hasKey("minHeight") - ? videoConstraintsMandatory.getInt("minHeight") - : DEFAULT_HEIGHT; - fps = videoConstraintsMandatory.hasKey("minFrameRate") - ? videoConstraintsMandatory.getInt("minFrameRate") - : DEFAULT_FPS; - } + videoCapturer = createVideoCapturer(deviceId, facingMode); } public void dispose() { @@ -179,23 +153,23 @@ public void onCameraSwitchError(String s) { * Constructs a new {@code VideoCapturer} instance attempting to satisfy * specific constraints. * - * @param sourceId the ID of the requested video source. If not + * @param deviceId the ID of the requested video device. If not * {@code null} and a {@code VideoCapturer} can be created for it, then * {@code facingMode} is ignored. * @param facingMode the facing of the requested video source such as * {@code user} and {@code environment}. If {@code null}, "user" is * presumed. * @return a {@code VideoCapturer} satisfying the {@code facingMode} or - * {@code sourceId} constraint + * {@code deviceId} constraint */ - private VideoCapturer createVideoCapturer(String sourceId, String facingMode) { + private VideoCapturer createVideoCapturer(String deviceId, String facingMode) { String[] deviceNames = cameraEnumerator.getDeviceNames(); List failedDevices = new ArrayList<>(); - // If sourceId is specified, then it takes precedence over facingMode. - if (sourceId != null) { + // If deviceId is specified, then it takes precedence over facingMode. + if (deviceId != null) { for (String name : deviceNames) { - if (name.equals(sourceId)) { + if (name.equals(deviceId)) { VideoCapturer videoCapturer = cameraEnumerator.createCapturer(name, cameraEventsHandler); String message = "Create user-specified camera " + name; @@ -267,49 +241,4 @@ private VideoCapturer createVideoCapturer(String sourceId, String facingMode) { return null; } - - /** - * Retrieves "facingMode" constraint value. - * - * @param mediaConstraints a {@code ReadableMap} which represents "GUM" - * constraints argument. - * @return String value of "facingMode" constraints in "GUM" or - * {@code null} if not specified. - */ - private String getFacingMode(ReadableMap mediaConstraints) { - return - mediaConstraints == null - ? null - : ReactBridgeUtil.getMapStrValue(mediaConstraints, "facingMode"); - } - - /** - * Retrieves "sourceId" constraint value. - * - * @param mediaConstraints a {@code ReadableMap} which represents "GUM" - * constraints argument - * @return String value of "sourceId" optional "GUM" constraint or - * {@code null} if not specified. - */ - private String getSourceIdConstraint(ReadableMap mediaConstraints) { - if (mediaConstraints != null - && mediaConstraints.hasKey("optional") - && mediaConstraints.getType("optional") == ReadableType.Array) { - ReadableArray optional = mediaConstraints.getArray("optional"); - - for (int i = 0, size = optional.size(); i < size; i++) { - if (optional.getType(i) == ReadableType.Map) { - ReadableMap option = optional.getMap(i); - - if (option.hasKey("sourceId") - && option.getType("sourceId") - == ReadableType.String) { - return option.getString("sourceId"); - } - } - } - } - - return null; - } } diff --git a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java index 2a43dcbe3..d8c5e2542 100644 --- a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java +++ b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java @@ -461,63 +461,23 @@ private static MediaStreamTrack getLocalTrack( } /** - * Parses a constraint set specified in the form of a JavaScript object into - * a specific List of MediaConstraints.KeyValuePairs. + * Turns an "options" ReadableMap into a MediaConstraints object. * - * @param src The constraint set in the form of a JavaScript object to - * parse. - * @param dst The List of MediaConstraints.KeyValuePairs - * into which the specified src is to be parsed. - */ - private void parseConstraints( - ReadableMap src, - List dst) { - ReadableMapKeySetIterator keyIterator = src.keySetIterator(); - - while (keyIterator.hasNextKey()) { - String key = keyIterator.nextKey(); - String value = ReactBridgeUtil.getMapStrValue(src, key); - - dst.add(new MediaConstraints.KeyValuePair(key, value)); - } - } - - /** - * Parses mandatory and optional "GUM" constraints described by a specific - * ReadableMap. - * - * @param constraints A ReadableMap which represents a JavaScript - * object specifying the constraints to be parsed into a + * @param options A ReadableMap which represents a JavaScript + * object specifying the options to be parsed into a * MediaConstraints instance. * @return A new MediaConstraints instance initialized with the - * mandatory and optional constraint keys and values specified by - * constraints. + * mandatory keys and values specified by options. */ - MediaConstraints parseMediaConstraints(ReadableMap constraints) { + MediaConstraints constraintsForOptions(ReadableMap options) { MediaConstraints mediaConstraints = new MediaConstraints(); + ReadableMapKeySetIterator keyIterator = options.keySetIterator(); - if (constraints.hasKey("mandatory") - && constraints.getType("mandatory") == ReadableType.Map) { - parseConstraints( - constraints.getMap("mandatory"), - mediaConstraints.mandatory); - } else { - Log.d(TAG, "mandatory constraints are not a map"); - } - - if (constraints.hasKey("optional") - && constraints.getType("optional") == ReadableType.Array) { - ReadableArray optional = constraints.getArray("optional"); + while (keyIterator.hasNextKey()) { + String key = keyIterator.nextKey(); + String value = ReactBridgeUtil.getMapStrValue(options, key); - for (int i = 0, size = optional.size(); i < size; i++) { - if (optional.getType(i) == ReadableType.Map) { - parseConstraints( - optional.getMap(i), - mediaConstraints.optional); - } - } - } else { - Log.d(TAG, "optional constraints are not an array"); + mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(key, value)); } return mediaConstraints; @@ -700,14 +660,14 @@ private void peerConnectionRemoveStreamAsync(String streamId, int id) { @ReactMethod public void peerConnectionCreateOffer(int id, - ReadableMap constraints, + ReadableMap options, Callback callback) { ThreadUtils.runOnExecutor(() -> - peerConnectionCreateOfferAsync(id, constraints, callback)); + peerConnectionCreateOfferAsync(id, options, callback)); } private void peerConnectionCreateOfferAsync(int id, - ReadableMap constraints, + ReadableMap options, final Callback callback) { PeerConnection peerConnection = getPeerConnection(id); @@ -731,7 +691,7 @@ public void onSetFailure(String s) {} @Override public void onSetSuccess() {} - }, parseMediaConstraints(constraints)); + }, constraintsForOptions(options)); } else { Log.d(TAG, "peerConnectionCreateOffer() peerConnection is null"); callback.invoke(false, "peerConnection is null"); @@ -740,14 +700,14 @@ public void onSetSuccess() {} @ReactMethod public void peerConnectionCreateAnswer(int id, - ReadableMap constraints, + ReadableMap options, Callback callback) { ThreadUtils.runOnExecutor(() -> - peerConnectionCreateAnswerAsync(id, constraints, callback)); + peerConnectionCreateAnswerAsync(id, options, callback)); } private void peerConnectionCreateAnswerAsync(int id, - ReadableMap constraints, + ReadableMap options, final Callback callback) { PeerConnection peerConnection = getPeerConnection(id); @@ -771,7 +731,7 @@ public void onSetFailure(String s) {} @Override public void onSetSuccess() {} - }, parseMediaConstraints(constraints)); + }, constraintsForOptions(options)); } else { Log.d(TAG, "peerConnectionCreateAnswer() peerConnection is null"); callback.invoke(false, "peerConnection is null"); diff --git a/getUserMedia.js b/getUserMedia.js index b395456a5..4506885b4 100644 --- a/getUserMedia.js +++ b/getUserMedia.js @@ -8,54 +8,8 @@ import MediaStreamError from './MediaStreamError'; import MediaStreamTrack from './MediaStreamTrack'; import permissions from './Permissions'; -const {WebRTCModule} = NativeModules; +const { WebRTCModule } = NativeModules; -// native side consume string eventually -const DEFAULT_VIDEO_CONSTRAINTS = { - mandatory: { - minWidth: '1280', - minHeight: '720', - minFrameRate: '30', - }, - facingMode: "environment", - optional: [], -}; - -function getDefaultMediaConstraints(mediaType) { - return (mediaType === 'audio' - ? {} // no audio default constraint currently - : RTCUtil.mergeMediaConstraints(DEFAULT_VIDEO_CONSTRAINTS)); -} - -// this will make sure we have the correct constraint structure -// TODO: support width/height range and the latest param names according to spec -// media constraints param name should follow spec. then we need a function to convert these `js names` -// into the real `const name that native defined` on both iOS and Android. -// see mediaTrackConstraints: https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackconstraints -function parseMediaConstraints(customConstraints, mediaType) { - return (mediaType === 'audio' - ? RTCUtil.mergeMediaConstraints(customConstraints) // no audio default constraint currently - : RTCUtil.mergeMediaConstraints(customConstraints, DEFAULT_VIDEO_CONSTRAINTS)); -} - -// this will make sure we have the correct value type -function normalizeMediaConstraints(constraints, mediaType) { - if (mediaType === 'audio') { - ; // to be added - } else { - // NOTE: android only support minXXX currently - for (const param of ['minWidth', 'minHeight', 'minFrameRate', 'maxWidth', 'maxHeight', 'maxFrameRate', ]) { - if (constraints.mandatory.hasOwnProperty(param)) { - // convert to correct type here so that native can consume directly without worries. - constraints.mandatory[param] = (Platform.OS === 'ios' - ? constraints.mandatory[param].toString() // ios consumes string - : parseInt(constraints.mandatory[param])); // android eats integer - } - } - } - - return constraints; -} export default function getUserMedia(constraints = {}) { // According to @@ -70,40 +24,11 @@ export default function getUserMedia(constraints = {}) { return Promise.reject(new TypeError('audio and/or video is required')); } - // Deep clone constraints. - constraints = JSON.parse(JSON.stringify(constraints)); - - // According to step 2 of the getUserMedia() algorithm, requestedMediaTypes - // is the set of media types in constraints with either a dictionary value - // or a value of "true". - for (const mediaType of [ 'audio', 'video' ]) { - // According to the spec, the types of the audio and video members of - // MediaStreamConstraints are either boolean or MediaTrackConstraints - // (i.e. dictionary). - const mediaTypeConstraints = constraints[mediaType]; - const typeofMediaTypeConstraints = typeof mediaTypeConstraints; - if (typeofMediaTypeConstraints !== 'undefined') { - if (typeofMediaTypeConstraints === 'boolean') { - if (mediaTypeConstraints) { - constraints[mediaType] = getDefaultMediaConstraints(mediaType); - } - } else if (typeofMediaTypeConstraints === 'object') { - // Note: object constraints for audio is not implemented in native side - constraints[mediaType] = parseMediaConstraints(mediaTypeConstraints, mediaType); - } else { - return Promise.reject( - new TypeError('constraints.' + mediaType + ' is neither a boolean nor a dictionary')); - } - - // final check constraints and convert value to native accepted type - if (typeof constraints[mediaType] === 'object') { - constraints[mediaType] = normalizeMediaConstraints(constraints[mediaType], mediaType); - } - } - } + // Normalize constraints. + constraints = RTCUtil.normalizeConstraints(constraints); // Request required permissions - let reqPermissions = []; + const reqPermissions = []; if (constraints.audio) { reqPermissions.push(permissions.request({ name: 'microphone' })); } else { diff --git a/ios/RCTWebRTC/VideoCaptureController.m b/ios/RCTWebRTC/VideoCaptureController.m index d941f0a31..b3c04fadf 100644 --- a/ios/RCTWebRTC/VideoCaptureController.m +++ b/ios/RCTWebRTC/VideoCaptureController.m @@ -1,15 +1,14 @@ #import "VideoCaptureController.h" -static int DEFAULT_WIDTH = 1280; -static int DEFAULT_HEIGHT = 720; -static int DEFAULT_FPS = 30; - @implementation VideoCaptureController { RTCCameraVideoCapturer *_capturer; - NSString *_sourceId; + NSString *_deviceId; BOOL _usingFrontCamera; + int _width; + int _height; + int _fps; } -(instancetype)initWithCapturer:(RTCCameraVideoCapturer *)capturer @@ -21,10 +20,12 @@ -(instancetype)initWithCapturer:(RTCCameraVideoCapturer *)capturer // Default to the front camera. _usingFrontCamera = YES; - // Check the video contraints: examine facingMode and sourceId - // and pick a default if neither are specified. + _deviceId = constraints[@"deviceId"]; + _width = [constraints[@"width"] intValue]; + _height = [constraints[@"height"] intValue]; + _fps = [constraints[@"frameRate"] intValue]; + id facingMode = constraints[@"facingMode"]; - id optionalConstraints = constraints[@"optional"]; if (facingMode && [facingMode isKindOfClass:[NSString class]]) { AVCaptureDevicePosition position; @@ -39,17 +40,6 @@ -(instancetype)initWithCapturer:(RTCCameraVideoCapturer *)capturer } _usingFrontCamera = position == AVCaptureDevicePositionFront; - } else if (optionalConstraints && [optionalConstraints isKindOfClass:[NSArray class]]) { - NSArray *options = optionalConstraints; - for (id item in options) { - if ([item isKindOfClass:[NSDictionary class]]) { - NSString *sourceId = ((NSDictionary *)item)[@"sourceId"]; - if (sourceId && sourceId.length > 0) { - _sourceId = sourceId; - break; - } - } - } } } @@ -58,8 +48,8 @@ -(instancetype)initWithCapturer:(RTCCameraVideoCapturer *)capturer -(void)startCapture { AVCaptureDevice *device; - if (_sourceId) { - device = [AVCaptureDevice deviceWithUniqueID:_sourceId]; + if (_deviceId) { + device = [AVCaptureDevice deviceWithUniqueID:_deviceId]; } if (!device) { AVCaptureDevicePosition position @@ -69,19 +59,17 @@ -(void)startCapture { device = [self findDeviceForPosition:position]; } - // TODO: Extract width and height from constraints. AVCaptureDeviceFormat *format = [self selectFormatForDevice:device - withTargetWidth:DEFAULT_WIDTH - withTargetHeight:DEFAULT_HEIGHT]; + withTargetWidth:_width + withTargetHeight:_height]; if (!format) { NSLog(@"[VideoCaptureController] No valid formats for device %@", device); return; } - // TODO: Extract fps from constraints. - [_capturer startCaptureWithDevice:device format:format fps:DEFAULT_FPS]; + [_capturer startCaptureWithDevice:device format:format fps:_fps]; NSLog(@"[VideoCaptureController] Capture started"); } diff --git a/ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.m b/ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.m index 488942356..4fdb73585 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.m @@ -142,7 +142,7 @@ @implementation WebRTCModule (RTCPeerConnection) RCT_EXPORT_METHOD(peerConnectionCreateOffer:(nonnull NSNumber *)objectID - constraints:(NSDictionary *)constraints + options:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback) { RTCPeerConnection *peerConnection = self.peerConnections[objectID]; @@ -150,8 +150,12 @@ @implementation WebRTCModule (RTCPeerConnection) return; } + RTCMediaConstraints *constraints = + [[RTCMediaConstraints alloc] initWithMandatoryConstraints:options + optionalConstraints:nil]; + [peerConnection - offerForConstraints:[self parseMediaConstraints:constraints] + offerForConstraints:constraints completionHandler:^(RTCSessionDescription *sdp, NSError *error) { if (error) { callback(@[ @@ -169,7 +173,7 @@ @implementation WebRTCModule (RTCPeerConnection) } RCT_EXPORT_METHOD(peerConnectionCreateAnswer:(nonnull NSNumber *)objectID - constraints:(NSDictionary *)constraints + options:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback) { RTCPeerConnection *peerConnection = self.peerConnections[objectID]; @@ -177,8 +181,12 @@ @implementation WebRTCModule (RTCPeerConnection) return; } + RTCMediaConstraints *constraints = + [[RTCMediaConstraints alloc] initWithMandatoryConstraints:options + optionalConstraints:nil]; + [peerConnection - answerForConstraints:[self parseMediaConstraints:constraints] + answerForConstraints:constraints completionHandler:^(RTCSessionDescription *sdp, NSError *error) { if (error) { callback(@[ @@ -501,67 +509,4 @@ - (void)peerConnection:(nonnull RTCPeerConnection *)peerConnection didRemoveIceC // TODO } - -/** - * Parses the constraint keys and values of a specific JavaScript object into - * a specific NSMutableDictionary in a format suitable for the - * initialization of a RTCMediaConstraints instance. - * - * @param src The JavaScript object which defines constraint keys and values and - * which is to be parsed into the specified dst. - * @param dst The NSMutableDictionary into which the constraint keys - * and values defined by src are to be written in a format suitable for - * the initialization of a RTCMediaConstraints instance. - */ -- (void)parseJavaScriptConstraints:(NSDictionary *)src - intoWebRTCConstraints:(NSMutableDictionary *)dst { - for (id srcKey in src) { - id srcValue = src[srcKey]; - NSString *dstValue; - - if ([srcValue isKindOfClass:[NSNumber class]]) { - dstValue = [srcValue boolValue] ? @"true" : @"false"; - } else { - dstValue = [srcValue description]; - } - dst[[srcKey description]] = dstValue; - } -} - -/** - * Parses a JavaScript object into a new RTCMediaConstraints instance. - * - * @param constraints The JavaScript object to parse into a new - * RTCMediaConstraints instance. - * @returns A new RTCMediaConstraints instance initialized with the - * mandatory and optional constraint keys and values specified by - * constraints. - */ -- (RTCMediaConstraints *)parseMediaConstraints:(NSDictionary *)constraints { - id mandatory = constraints[@"mandatory"]; - NSMutableDictionary *mandatory_ - = [NSMutableDictionary new]; - - if ([mandatory isKindOfClass:[NSDictionary class]]) { - [self parseJavaScriptConstraints:(NSDictionary *)mandatory - intoWebRTCConstraints:mandatory_]; - } - - id optional = constraints[@"optional"]; - NSMutableDictionary *optional_ - = [NSMutableDictionary new]; - - if ([optional isKindOfClass:[NSArray class]]) { - for (id o in (NSArray *)optional) { - if ([o isKindOfClass:[NSDictionary class]]) { - [self parseJavaScriptConstraints:(NSDictionary *)o - intoWebRTCConstraints:optional_]; - } - } - } - - return [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory_ - optionalConstraints:optional_]; -} - @end