diff --git a/android/src/main/java/com/oney/WebRTCModule/SerializeUtils.java b/android/src/main/java/com/oney/WebRTCModule/SerializeUtils.java index 749a3a5ba..bf1ec2872 100644 --- a/android/src/main/java/com/oney/WebRTCModule/SerializeUtils.java +++ b/android/src/main/java/com/oney/WebRTCModule/SerializeUtils.java @@ -170,11 +170,7 @@ public static ReadableMap serializeRtpParameters(RtpParameters params) { } // Serializing sdpFmptLine. if (!codec.parameters.isEmpty()) { - final String sdpFmptLineParams = codec.parameters.keySet() - .stream() - .map(key -> key + "=" + codec.parameters.get(key)) - .collect(Collectors.joining(";")); - codecMap.putString("sdpFmtpLine", sdpFmptLineParams); + codecMap.putString("sdpFmtpLine", serializeSdpParameters(codec.parameters)); } codecs.pushMap(codecMap); @@ -192,6 +188,39 @@ public static ReadableMap serializeRtpParameters(RtpParameters params) { return result; } + public static ReadableMap serializeRtpCapabilities(RtpCapabilities capabilities) { + WritableMap result = Arguments.createMap(); + WritableArray codecs = Arguments.createArray(); + + capabilities.codecs.forEach(codec -> codecs.pushMap(serializeRtpCapabilitiesCodec(codec))); + + result.putArray("codecs", codecs); + return result; + } + + public static ReadableMap serializeRtpCapabilitiesCodec(RtpCapabilities.CodecCapability codec) { + WritableMap codecMap = Arguments.createMap(); + codecMap.putInt("payloadType", codec.preferredPayloadType); + codecMap.putString("mimeType", codec.mimeType); + codecMap.putInt("clockRate", codec.clockRate); + if (codec.numChannels != null) { + codecMap.putInt("channels", codec.numChannels); + } + if (!codec.parameters.isEmpty()) { + codecMap.putString("sdpFmtpLine", serializeSdpParameters(codec.parameters)); + } + + return codecMap; + } + + // For serializing sdpFmptLine. + public static String serializeSdpParameters(Map parameters) { + return parameters.keySet() + .stream() + .map(key -> key + "=" + parameters.get(key)) + .collect(Collectors.joining(";")); + } + /** * Parsing APIs */ diff --git a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java index ca72005d1..3f9dd15a3 100644 --- a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java +++ b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java @@ -1,6 +1,7 @@ package com.oney.WebRTCModule; import android.util.Log; +import android.util.Pair; import android.util.SparseArray; import androidx.annotation.NonNull; @@ -709,6 +710,68 @@ public void transceiverSetDirection(int id, String senderId, String direction, P }); } + @ReactMethod(isBlockingSynchronousMethod = true) + public void transceiverSetCodecPreferences(int id, String senderId, ReadableArray codecPreferences) { + ThreadUtils.runOnExecutor(() -> { + WritableMap identifier = Arguments.createMap(); + WritableMap params = Arguments.createMap(); + identifier.putInt("peerConnectionId", id); + identifier.putString("transceiverId", senderId); + try { + PeerConnectionObserver pco = mPeerConnectionObservers.get(id); + if (pco == null) { + Log.d(TAG, "transceiverSetDirection() peerConnectionObserver is null"); + return; + } + RtpTransceiver transceiver = pco.getTransceiver(senderId); + if (transceiver == null) { + Log.d(TAG, "transceiverSetDirection() transceiver is null"); + return; + } + + // Convert JSON codec capabilities to the actual objects. + RtpTransceiver.RtpTransceiverDirection direction = transceiver.getDirection(); + List, RtpCapabilities.CodecCapability>> availableCodecs = new ArrayList<>(); + + if (direction.equals(RtpTransceiver.RtpTransceiverDirection.SEND_RECV) + || direction.equals(RtpTransceiver.RtpTransceiverDirection.SEND_ONLY)) { + RtpCapabilities capabilities = mFactory.getRtpSenderCapabilities(transceiver.getMediaType()); + for (RtpCapabilities.CodecCapability codec : capabilities.codecs) { + Map codecDict = SerializeUtils.serializeRtpCapabilitiesCodec(codec).toHashMap(); + availableCodecs.add(new Pair<>(codecDict, codec)); + } + } + + if (direction.equals(RtpTransceiver.RtpTransceiverDirection.SEND_RECV) + || direction.equals(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY)) { + RtpCapabilities capabilities = mFactory.getRtpReceiverCapabilities(transceiver.getMediaType()); + for (RtpCapabilities.CodecCapability codec : capabilities.codecs) { + Map codecDict = SerializeUtils.serializeRtpCapabilitiesCodec(codec).toHashMap(); + availableCodecs.add(new Pair<>(codecDict, codec)); + } + } + + // Codec preferences is order sensitive. + List codecsToSet = new ArrayList<>(); + + for (int i = 0; i < codecPreferences.size(); i++) { + Map codecPref = codecPreferences.getMap(i).toHashMap(); + for (Pair, RtpCapabilities.CodecCapability> pair : availableCodecs) { + Map availableCodecDict = pair.first; + if (codecPref.equals(availableCodecDict)) { + codecsToSet.add(pair.second); + break; + } + } + } + + transceiver.setCodecPreferences(codecsToSet); + } catch (Exception e) { + Log.d(TAG, "transceiverSetCodecPreferences(): " + e.getMessage()); + } + }); + } + @ReactMethod public void getDisplayMedia(Promise promise) { ThreadUtils.runOnExecutor(() -> getUserMediaImpl.getDisplayMedia(promise)); @@ -1129,18 +1192,21 @@ public void onSetFailure(String s) { } @ReactMethod(isBlockingSynchronousMethod = true) - public WritableMap receiverGetCapabilities() { + public WritableMap receiverGetCapabilities(String kind) { try { return (WritableMap) ThreadUtils .submitToExecutor((Callable) () -> { - VideoCodecInfo[] videoCodecInfos = mVideoDecoderFactory.getSupportedCodecs(); - WritableMap params = Arguments.createMap(); - WritableArray codecs = Arguments.createArray(); - for (VideoCodecInfo codecInfo : videoCodecInfos) { - codecs.pushMap(SerializeUtils.serializeVideoCodecInfo(codecInfo)); + MediaStreamTrack.MediaType mediaType; + if (kind.equals("audio")) { + mediaType = MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO; + } else if (kind.equals("video")) { + mediaType = MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO; + } else { + return Arguments.createMap(); } - params.putArray("codecs", codecs); - return params; + + RtpCapabilities capabilities = mFactory.getRtpReceiverCapabilities(mediaType); + return SerializeUtils.serializeRtpCapabilities(capabilities); }) .get(); } catch (ExecutionException | InterruptedException e) { @@ -1150,18 +1216,21 @@ public WritableMap receiverGetCapabilities() { } @ReactMethod(isBlockingSynchronousMethod = true) - public WritableMap senderGetCapabilities() { + public WritableMap senderGetCapabilities(String kind) { try { return (WritableMap) ThreadUtils .submitToExecutor((Callable) () -> { - VideoCodecInfo[] videoCodecInfos = mVideoEncoderFactory.getSupportedCodecs(); - WritableMap params = Arguments.createMap(); - WritableArray codecs = Arguments.createArray(); - for (VideoCodecInfo codecInfo : videoCodecInfos) { - codecs.pushMap(SerializeUtils.serializeVideoCodecInfo(codecInfo)); + MediaStreamTrack.MediaType mediaType; + if (kind.equals("audio")) { + mediaType = MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO; + } else if (kind.equals("video")) { + mediaType = MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO; + } else { + return Arguments.createMap(); } - params.putArray("codecs", codecs); - return params; + + RtpCapabilities capabilities = mFactory.getRtpSenderCapabilities(mediaType); + return SerializeUtils.serializeRtpCapabilities(capabilities); }) .get(); } catch (ExecutionException | InterruptedException e) { diff --git a/ios/RCTWebRTC/SerializeUtils.h b/ios/RCTWebRTC/SerializeUtils.h index 62e53948a..1b7f1367f 100644 --- a/ios/RCTWebRTC/SerializeUtils.h +++ b/ios/RCTWebRTC/SerializeUtils.h @@ -1,4 +1,5 @@ #import +#import #import #import #import @@ -13,6 +14,8 @@ receiver:(RTCRtpReceiver *_Nonnull)receiver; + (NSDictionary *_Nonnull)trackToJSONWithPeerConnectionId:(nonnull NSNumber *)id track:(RTCMediaStreamTrack *_Nonnull)track; ++ (NSDictionary *_Nonnull)capabilitiesToJSON:(RTCRtpCapabilities *_Nonnull)capabilities; ++ (NSDictionary *_Nonnull)codecCapabilityToJSON:(RTCRtpCodecCapability *_Nonnull)codec; + (NSString *_Nonnull)serializeDirection:(RTCRtpTransceiverDirection)direction; + (RTCRtpTransceiverDirection)parseDirection:(NSString *_Nonnull)direction; + (RTCRtpTransceiverInit *_Nonnull)parseTransceiverOptions:(NSDictionary *_Nonnull)parameters; diff --git a/ios/RCTWebRTC/SerializeUtils.m b/ios/RCTWebRTC/SerializeUtils.m index 08e31148d..c4deef1b0 100644 --- a/ios/RCTWebRTC/SerializeUtils.m +++ b/ios/RCTWebRTC/SerializeUtils.m @@ -130,13 +130,7 @@ + (NSDictionary *)parametersToJSON:(RTCRtpParameters *)params { } if (codec.parameters.count) { - NSMutableArray *parts = [NSMutableArray arrayWithCapacity:codec.parameters.count]; - [codec.parameters - enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL *_Nonnull stop) { - [parts addObject:[NSString stringWithFormat:@"%@=%@", key, value]]; - }]; - - codecDictionary[@"sdpFmtpLine"] = [parts componentsJoinedByString:@";"]; + codecDictionary[@"sdpFmtpLine"] = [self serializeSdpParameters:codec.parameters]; } [codecs addObject:codecDictionary]; @@ -176,6 +170,47 @@ + (NSDictionary *)trackToJSONWithPeerConnectionId:(NSNumber *)id track:(RTCMedia }; } ++ (NSDictionary *)capabilitiesToJSON:(RTCRtpCapabilities *)capabilities { + NSMutableArray *codecs = [NSMutableArray new]; + + for (RTCRtpCodecCapability *codec in capabilities.codecs) { + [codecs addObject:[self codecCapabilityToJSON:codec]]; + } + + return @{@"codecs" : codecs}; +} + ++ (NSDictionary *)codecCapabilityToJSON:(RTCRtpCodecCapability *)codec { + NSMutableDictionary *codecDictionary = [NSMutableDictionary new]; + + codecDictionary[@"payloadType"] = codec.preferredPayloadType; + codecDictionary[@"mimeType"] = codec.mimeType; + codecDictionary[@"clockRate"] = codec.clockRate; + + if (codec.numChannels) { + codecDictionary[@"channels"] = codec.numChannels; + } + + if (codec.parameters.count) { + codecDictionary[@"sdpFmtpLine"] = [self serializeSdpParameters:codec.parameters]; + } + + return codecDictionary; +} + ++ (NSString *)serializeSdpParameters:(NSDictionary *)parameters { + if (parameters == nil || parameters.count == 0) { + return nil; + } + + NSMutableArray *parts = [NSMutableArray arrayWithCapacity:parameters.count]; + [parameters enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL *_Nonnull stop) { + [parts addObject:[NSString stringWithFormat:@"%@=%@", key, value]]; + }]; + + return [parts componentsJoinedByString:@";"]; +} + + (NSString *)serializeDirection:(RTCRtpTransceiverDirection)direction { if (direction == RTCRtpTransceiverDirectionInactive) { return @"inactive"; diff --git a/ios/RCTWebRTC/WebRTCModule+Transceivers.m b/ios/RCTWebRTC/WebRTCModule+Transceivers.m index 3981d9b92..7b9ae2bd1 100644 --- a/ios/RCTWebRTC/WebRTCModule+Transceivers.m +++ b/ios/RCTWebRTC/WebRTCModule+Transceivers.m @@ -3,6 +3,7 @@ #import #import +#import #import #import @@ -11,31 +12,37 @@ @implementation WebRTCModule (Transceivers) -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(senderGetCapabilities) { +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(senderGetCapabilities : (NSString *)kind) { __block id params; dispatch_sync(self.workerQueue, ^{ - NSMutableArray *videoCodecs = [NSMutableArray new]; - for (RTCVideoCodecInfo *videoCodecInfo in [self.encoderFactory supportedCodecs]) { - [videoCodecs addObject:@{@"mimeType" : [NSString stringWithFormat:@"video/%@", videoCodecInfo.name]}]; + RTCRtpMediaType mediaType = RTCRtpMediaTypeUnsupported; + if ([kind isEqual:@"audio"]) { + mediaType = RTCRtpMediaTypeAudio; + } else if ([kind isEqual:@"video"]) { + mediaType = RTCRtpMediaTypeVideo; } - params = @{@"codecs" : videoCodecs}; + RTCRtpCapabilities *capabilities = [self.peerConnectionFactory rtpSenderCapabilitiesFor:mediaType]; + params = [SerializeUtils capabilitiesToJSON:capabilities]; }); return params; } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(receiverGetCapabilities) { +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(receiverGetCapabilities : (NSString *)kind) { __block id params; dispatch_sync(self.workerQueue, ^{ - NSMutableArray *videoCodecs = [NSMutableArray new]; - for (RTCVideoCodecInfo *videoCodecInfo in [self.decoderFactory supportedCodecs]) { - [videoCodecs addObject:@{@"mimeType" : [NSString stringWithFormat:@"video/%@", videoCodecInfo.name]}]; + RTCRtpMediaType mediaType = RTCRtpMediaTypeUnsupported; + if ([kind isEqual:@"audio"]) { + mediaType = RTCRtpMediaTypeAudio; + } else if ([kind isEqual:@"video"]) { + mediaType = RTCRtpMediaTypeVideo; } - params = @{@"codecs" : videoCodecs}; + RTCRtpCapabilities *capabilities = [self.peerConnectionFactory rtpSenderCapabilitiesFor:mediaType]; + params = [SerializeUtils capabilitiesToJSON:capabilities]; }); return params; @@ -178,6 +185,78 @@ @implementation WebRTCModule (Transceivers) resolve(@true); } +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(transceiverSetCodecPreferences + : (nonnull NSNumber *)objectID senderId + : (NSString *)senderId codecPreferences + : (NSArray *)codecPreferences) { + RTCPeerConnection *peerConnection = self.peerConnections[objectID]; + + if (peerConnection == nil) { + RCTLogWarn(@"PeerConnection %@ not found in transceiverSetCodecPreferences()", objectID); + return nil; + } + + RTCRtpTransceiver *transceiver = nil; + for (RTCRtpTransceiver *t in peerConnection.transceivers) { + if ([senderId isEqual:t.sender.senderId]) { + transceiver = t; + break; + } + } + + if (transceiver == nil) { + RCTLogWarn(@"transceiverSetCodecPreferences() transceiver is null"); + return nil; + } + + // Get the available codecs + RTCRtpTransceiverDirection direction = transceiver.direction; + NSMutableArray *availableCodecs = [NSMutableArray new]; + if (direction == RTCRtpTransceiverDirectionSendRecv || direction == RTCRtpTransceiverDirectionSendOnly) { + RTCRtpCapabilities *capabilities = [self.peerConnectionFactory rtpSenderCapabilitiesFor:transceiver.mediaType]; + for (RTCRtpCodecCapability *codec in capabilities.codecs) { + NSDictionary *codecDict = [SerializeUtils codecCapabilityToJSON:codec]; + [availableCodecs addObject:@{ + @"dict" : codecDict, + @"codec" : codec, + }]; + } + } + if (direction == RTCRtpTransceiverDirectionSendRecv || direction == RTCRtpTransceiverDirectionRecvOnly) { + RTCRtpCapabilities *capabilities = + [self.peerConnectionFactory rtpReceiverCapabilitiesFor:transceiver.mediaType]; + for (RTCRtpCodecCapability *codec in capabilities.codecs) { + NSDictionary *codecDict = [SerializeUtils codecCapabilityToJSON:codec]; + [availableCodecs addObject:@{ + @"dict" : codecDict, + @"codec" : codec, + }]; + } + } + + // Convert JSON codec capabilities to the actual objects. + // Codec preferences is order sensitive. + NSMutableArray *codecsToSet = [NSMutableArray new]; + + for (NSDictionary *codecDict in codecPreferences) { + for (NSDictionary *entry in availableCodecs) { + NSDictionary *availableCodecDict = [entry objectForKey:@"dict"]; + if ([codecDict isEqualToDictionary:availableCodecDict]) { + [codecsToSet addObject:[entry objectForKey:@"codec"]]; + break; + } + } + } + + NSError *error; + [transceiver setCodecPreferences:codecsToSet error:&error]; + + if (error) { + RTCLogError(@"transceiverSetCodecPreferences() Could not set preferences: %@", error); + } + return nil; +} + - (RTCRtpParameters *)updateParametersWithOptions:(NSDictionary *)options params:(RTCRtpParameters *)params { NSArray *encodingsArray = options[@"encodings"]; NSArray *encodings = params.encodings; diff --git a/src/RTCRtpCapabilities.ts b/src/RTCRtpCapabilities.ts index 7f422595c..1d0fbc7b7 100644 --- a/src/RTCRtpCapabilities.ts +++ b/src/RTCRtpCapabilities.ts @@ -1,12 +1,7 @@ -import { NativeModules } from 'react-native'; - import RTCRtpCodecCapability from './RTCRtpCodecCapability'; -const { WebRTCModule } = NativeModules; /** - * @brief represents codec capabilities for senders and receivers. Currently - * this only supports codec names and does not have other - * fields like clockRate and numChannels and such. + * @brief represents codec capabilities for senders and receivers. */ export default class RTCRtpCapabilities { _codecs: RTCRtpCodecCapability[] = []; @@ -19,47 +14,3 @@ export default class RTCRtpCapabilities { return this._codecs; } } - - -function getCapabilities(endpoint: 'sender' | 'receiver'): RTCRtpCapabilities | null { - switch (endpoint) { - case 'sender': { - const capabilities = WebRTCModule.senderGetCapabilities(); - - if (!capabilities) { - return null; - } - - return new RTCRtpCapabilities(capabilities.codecs); - } - - case 'receiver': { - const capabilities = WebRTCModule.receiverGetCapabilities(); - - if (!capabilities) { - return null; - } - - return new RTCRtpCapabilities(capabilities.codecs); - } - - default: - throw new TypeError('Invalid endpoint: ' + endpoint); - } -} - - -/** - * Hardcoded audio capabilities based on the WebRTC native documentation: - * https://webrtc.github.io/webrtc-org/faq/. The mime type is specified in - * https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-2. - */ -export const DEFAULT_AUDIO_CAPABILITIES = new RTCRtpCapabilities([ - new RTCRtpCodecCapability({ mimeType: 'audio/G722' }), - new RTCRtpCodecCapability({ mimeType: 'audio/iLBC' }), -]); - -// Initialize capabilities on module import -export const senderCapabilities = getCapabilities('sender'); -export const receiverCapabilities = getCapabilities('receiver'); - diff --git a/src/RTCRtpReceiver.ts b/src/RTCRtpReceiver.ts index e63d36fb3..228fce008 100644 --- a/src/RTCRtpReceiver.ts +++ b/src/RTCRtpReceiver.ts @@ -1,7 +1,7 @@ import { NativeModules } from 'react-native'; import MediaStreamTrack from './MediaStreamTrack'; -import RTCRtpCapabilities, { DEFAULT_AUDIO_CAPABILITIES, receiverCapabilities } from './RTCRtpCapabilities'; +import RTCRtpCapabilities from './RTCRtpCapabilities'; import { RTCRtpParametersInit } from './RTCRtpParameters'; import RTCRtpReceiveParameters from './RTCRtpReceiveParameters'; @@ -29,15 +29,7 @@ export default class RTCRtpReceiver { } static getCapabilities(kind: 'audio' | 'video'): RTCRtpCapabilities { - if (kind === 'audio') { - return DEFAULT_AUDIO_CAPABILITIES; - } - - if (!receiverCapabilities) { - throw new Error('Receiver Capabilities is null'); - } - - return receiverCapabilities; + return WebRTCModule.receiverGetCapabilities(kind); } getStats() { diff --git a/src/RTCRtpSender.ts b/src/RTCRtpSender.ts index 7a53d6331..4366b32fd 100644 --- a/src/RTCRtpSender.ts +++ b/src/RTCRtpSender.ts @@ -1,7 +1,7 @@ import { NativeModules } from 'react-native'; import MediaStreamTrack from './MediaStreamTrack'; -import RTCRtpCapabilities, { senderCapabilities, DEFAULT_AUDIO_CAPABILITIES } from './RTCRtpCapabilities'; +import RTCRtpCapabilities from './RTCRtpCapabilities'; import RTCRtpSendParameters, { RTCRtpSendParametersInit } from './RTCRtpSendParameters'; const { WebRTCModule } = NativeModules; @@ -39,15 +39,7 @@ export default class RTCRtpSender { } static getCapabilities(kind: 'audio' | 'video'): RTCRtpCapabilities { - if (kind === 'audio') { - return DEFAULT_AUDIO_CAPABILITIES; - } - - if (!senderCapabilities) { - throw new Error('sender Capabilities are null'); - } - - return senderCapabilities; + return WebRTCModule.senderGetCapabilities(kind); } getParameters(): RTCRtpSendParameters { diff --git a/src/RTCRtpTransceiver.ts b/src/RTCRtpTransceiver.ts index ded126530..ad3d859f2 100644 --- a/src/RTCRtpTransceiver.ts +++ b/src/RTCRtpTransceiver.ts @@ -1,5 +1,6 @@ import { NativeModules } from 'react-native'; +import RTCRtpCodecCapability from './RTCRtpCodecCapability'; import RTCRtpReceiver from './RTCRtpReceiver'; import RTCRtpSender from './RTCRtpSender'; @@ -89,6 +90,14 @@ export default class RTCRtpTransceiver { .then(() => this._setStopped()); } + setCodecPreferences(codecs: RTCRtpCodecCapability[]) { + WebRTCModule.transceiverSetCodecPreferences( + this._peerConnectionId, + this.sender.id, + codecs + ); + } + _setStopped() { this._stopped = true; this._direction = 'stopped';