Skip to content

Commit

Permalink
api: add RTCRtpReceiver/RTCRtpSender getStats implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
davidliu committed Nov 28, 2022
1 parent e903b31 commit f78f3e6
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import org.webrtc.MediaStream;
import org.webrtc.MediaStreamTrack;
import org.webrtc.PeerConnection;
import org.webrtc.RTCStats;
import org.webrtc.RTCStatsReport;
import org.webrtc.RtpReceiver;
import org.webrtc.RtpSender;
import org.webrtc.RtpTransceiver;
Expand All @@ -29,11 +31,14 @@
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

class PeerConnectionObserver implements PeerConnection.Observer {
Expand Down Expand Up @@ -239,6 +244,146 @@ void getStats(Promise promise) {
});
}


/**
* @param trackIdentifier sender or receiver id
* @param streamType "outbound-rtp" for sender or "inbound-rtp" for receiver
*/
void getFilteredStats(String trackIdentifier, boolean isReceiver, Promise promise) {

peerConnection.getStats(rtcStatsReport -> {
Map<String, RTCStats> statsMap = rtcStatsReport.getStatsMap();
Set<RTCStats> filteredStats = new HashSet<>();
// Get track stats
RTCStats trackStats = getTrackStats(trackIdentifier, statsMap);
if (trackStats == null) {
Log.w(TAG, "getStats: couldn't find track stats!");
RTCStatsReport report = new RTCStatsReport((long) rtcStatsReport.getTimestampUs(), new HashMap<>());
promise.resolve(StringUtils.statsToJSON(report));
return;
}

filteredStats.add(trackStats);
String trackId = trackStats.getId();

// Get stream stats
RTCStats streamStats = getStreamStats(trackId, statsMap);
if (streamStats != null) {
filteredStats.add(streamStats);
}

// Get streamType stats and associated information
Set<Long> ssrcs = new HashSet<>();
Set<String> codecIds = new HashSet<>();

String streamType;
if (isReceiver) {
streamType = "inbound-rtp";
} else {
streamType = "outbound-rtp";
}

for (RTCStats stats : statsMap.values()) {
if (stats.getType().equals(streamType) && trackId.equals(stats.getMembers().get("trackId"))) {
ssrcs.add((Long) stats.getMembers().get("ssrc"));
codecIds.add((String) stats.getMembers().get("codecId"));
filteredStats.add(stats);
}
}


// Get candidate information
RTCStats candidatePairStats = null;
for (RTCStats stats : statsMap.values()) {
if (stats.getType().equals("candidate-pair") && stats.getMembers().get("nominated").equals(true)) {
candidatePairStats = stats;
break;
}
}

String localCandidateId = null;
String remoteCandidateId = null;
if (candidatePairStats != null) {
filteredStats.add(candidatePairStats);
localCandidateId = (String) candidatePairStats.getMembers().get("localCandidateId");
remoteCandidateId = (String) candidatePairStats.getMembers().get("remoteCandidateId");
}

// Sweep for any remaining stats we want.
filteredStats.addAll(getExtraStats(trackIdentifier, ssrcs, codecIds, localCandidateId, remoteCandidateId, statsMap));

Map<String, RTCStats> filteredStatsMap = new HashMap<>();
for (RTCStats stats : filteredStats) {
filteredStatsMap.put(stats.getId(), stats);
}
RTCStatsReport filteredStatsReport = new RTCStatsReport((long) rtcStatsReport.getTimestampUs(), filteredStatsMap);
promise.resolve(StringUtils.statsToJSON(filteredStatsReport));
});
}

// Note: trackIdentifier can differ from the internal stats trackId
// trackIdentifier refers to the sender or receiver id
@Nullable
private RTCStats getTrackStats(String trackIdentifier, Map<String, RTCStats> statsMap) {
for (RTCStats stats : statsMap.values()) {
if (stats.getType().equals("track") && trackIdentifier.equals(stats.getMembers().get("trackIdentifier"))) {
return stats;
}
}
return null;
}


@Nullable
private RTCStats getStreamStats(String trackId, Map<String, RTCStats> statsMap) {
for (RTCStats stats : statsMap.values()) {
if (stats.getType().equals("stream")
&& Arrays.asList((String[]) stats.getMembers().get("trackIds")).contains(trackId)) {
return stats;
}
}
return null;
}

// Note: trackIdentifier can differ from the internal stats trackId
// trackIdentifier refers to the sender or receiver id
public Set<RTCStats> getExtraStats(
String trackIdentifier,
Set<Long> ssrcs,
Set<String> codecIds,
@Nullable String localCandidateId,
@Nullable String remoteCandidateId,
Map<String, RTCStats> statsMap) {
Set<RTCStats> extraStats = new HashSet<>();
for (RTCStats stats : statsMap.values()) {
switch (stats.getType()) {
case "certificate":
case "transport":
extraStats.add(stats);
break;
}

if (stats.getId().equals(localCandidateId) || stats.getId().equals(remoteCandidateId)) {
extraStats.add(stats);
continue;
}

if (ssrcs.contains(stats.getMembers().get("ssrc"))) {
extraStats.add(stats);
continue;
}
if (trackIdentifier.equals(stats.getMembers().get("trackIdentifier"))) {
extraStats.add(stats);
continue;
}
if (codecIds.contains(stats.getId())) {
extraStats.add(stats);
}
}

return extraStats;
}

@Override
public void onIceCandidate(final IceCandidate candidate) {
Log.d(TAG, "onIceCandidate");
Expand Down
25 changes: 25 additions & 0 deletions android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,31 @@ public WritableMap senderGetCapabilities() {
}
}

@ReactMethod
public void receiverGetStats(int pcId, String receiverId, Promise promise) {
ThreadUtils.runOnExecutor(() -> {
PeerConnectionObserver pco = mPeerConnectionObservers.get(pcId);
if (pco == null || pco.getPeerConnection() == null) {
Log.d(TAG, "peerConnectionGetStats() peerConnection is null");
promise.reject(new Exception("PeerConnection ID not found"));
} else {
pco.getFilteredStats(receiverId, true, promise);
}
});
}
@ReactMethod
public void senderGetStats(int pcId, String senderId, Promise promise) {
ThreadUtils.runOnExecutor(() -> {
PeerConnectionObserver pco = mPeerConnectionObservers.get(pcId);
if (pco == null || pco.getPeerConnection() == null) {
Log.d(TAG, "peerConnectionGetStats() peerConnection is null");
promise.reject(new Exception("PeerConnection ID not found"));
} else {
pco.getFilteredStats(senderId, false, promise);
}
});
}

@ReactMethod
public void peerConnectionAddICECandidate(int pcId,
ReadableMap candidateMap,
Expand Down
58 changes: 58 additions & 0 deletions ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.m
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,64 @@ @implementation WebRTCModule (RTCPeerConnection)
}];
}

RCT_EXPORT_METHOD(receiverGetStats:(nonnull NSNumber *) pcId
receiverId:(nonnull NSString *) receiverId
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
RTCPeerConnection *peerConnection = self.peerConnections[pcId];
if (!peerConnection) {
reject(@"invalid_id", @"PeerConnection ID not found", nil);
return;
}

RTCRtpReceiver *receiver;
for (RTCRtpReceiver *findRecv in peerConnection.receivers) {
if ([findRecv.receiverId isEqualToString:receiverId]) {
receiver = findRecv;
break;
}
}

if (!receiver) {
reject(@"invalid_id", @"Receiver ID not found", nil);
return;
}

[peerConnection statisticsForReceiver:receiver completionHandler:^(RTCStatisticsReport *report) {
resolve([self statsToJSON:report]);
}];
}

RCT_EXPORT_METHOD(senderGetStats:(nonnull NSNumber *) pcId
senderId:(nonnull NSString *) senderId
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
RTCPeerConnection *peerConnection = self.peerConnections[pcId];
if (!peerConnection) {
reject(@"invalid_id", @"PeerConnection ID not found", nil);
return;
}

RTCRtpSender *sender;
for (RTCRtpSender *findSend in peerConnection.senders) {
if ([findSend.senderId isEqualToString:senderId]) {
sender = findSend;
break;
}
}

if(!sender) {
reject(@"invalid_id", @"Sender ID not found", nil);
return;
}

[peerConnection statisticsForSender:sender completionHandler:^(RTCStatisticsReport *report) {
resolve([self statsToJSON:report]);
}];
}

RCT_EXPORT_METHOD(peerConnectionRestartIce:(nonnull NSNumber *)objectID)
{
RTCPeerConnection *peerConnection = self.peerConnections[objectID];
Expand Down
17 changes: 17 additions & 0 deletions src/RTCRtpReceiver.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { NativeModules } from 'react-native';

import MediaStreamTrack from './MediaStreamTrack';
import RTCRtpCapabilities, { DEFAULT_AUDIO_CAPABILITIES, receiverCapabilities } from './RTCRtpCapabilities';
import { RTCRtpParametersInit } from './RTCRtpParameters';
import RTCRtpReceiveParameters from './RTCRtpReceiveParameters';

const { WebRTCModule } = NativeModules;

export default class RTCRtpReceiver {
_id: string;
Expand Down Expand Up @@ -37,6 +40,20 @@ export default class RTCRtpReceiver {
return receiverCapabilities;
}

getStats() {
return WebRTCModule.receiverGetStats(this._peerConnectionId, this._id).then(data =>
/* On both Android and iOS it is faster to construct a single
JSON string representing the Map of StatsReports and have it
pass through the React Native bridge rather than the Map of
StatsReports. While the implementations do try to be faster in
general, the stress is on being faster to pass through the React
Native bridge which is a bottleneck that tends to be visible in
the UI when there is congestion involving UI-related passing.
*/
new Map(JSON.parse(data))
);
}

getParameters(): RTCRtpReceiveParameters {
return this._rtpParameters;
}
Expand Down
14 changes: 14 additions & 0 deletions src/RTCRtpSender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ export default class RTCRtpSender {
this._rtpParameters = new RTCRtpSendParameters(newParameters);
}

getStats() {
return WebRTCModule.senderGetStats(this._peerConnectionId, this._id).then(data =>
/* On both Android and iOS it is faster to construct a single
JSON string representing the Map of StatsReports and have it
pass through the React Native bridge rather than the Map of
StatsReports. While the implementations do try to be faster in
general, the stress is on being faster to pass through the React
Native bridge which is a bottleneck that tends to be visible in
the UI when there is congestion involving UI-related passing.
*/
new Map(JSON.parse(data))
);
}

get track() {
return this._track;
}
Expand Down

0 comments on commit f78f3e6

Please sign in to comment.