Skip to content

Commit 6fe29e4

Browse files
authored
feat: add a video track dimensions event (#14)
Adds a way to detect dimensions change for local and remote tracks on every frame update and then emit to JS. And in JS we store the dimensions in the settings.
1 parent a7e99c2 commit 6fe29e4

14 files changed

+412
-5
lines changed

android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -425,8 +425,12 @@ VideoTrack createVideoTrack(AbstractVideoCaptureController videoCaptureControlle
425425

426426
VideoTrack track = pcFactory.createVideoTrack(id, videoSource);
427427

428+
// Add dimension detection for local video tracks immediately when created
429+
VideoTrackAdapter localTrackAdapter = new VideoTrackAdapter(webRTCModule, -1); // Use -1 for local tracks
430+
localTrackAdapter.addDimensionDetector(track);
431+
428432
track.setEnabled(true);
429-
tracks.put(id, new TrackPrivate(track, videoSource, videoCaptureController, surfaceTextureHelper));
433+
tracks.put(id, new TrackPrivate(track, videoSource, videoCaptureController, surfaceTextureHelper, localTrackAdapter));
430434

431435
videoCaptureController.startCapture();
432436

@@ -444,8 +448,14 @@ MediaStreamTrack cloneTrack(String trackId) {
444448
String id = UUID.randomUUID().toString();
445449
MediaStreamTrack nativeTrack = track.track;
446450
final MediaStreamTrack clonedNativeTrack;
451+
VideoTrackAdapter clonedVideoTrackAdapter = null;
452+
447453
if (nativeTrack instanceof VideoTrack) {
448454
clonedNativeTrack = pcFactory.createVideoTrack(id, (VideoSource) track.mediaSource);
455+
456+
// Create dimension detection for cloned video tracks
457+
clonedVideoTrackAdapter = new VideoTrackAdapter(webRTCModule, -1);
458+
clonedVideoTrackAdapter.addDimensionDetector((VideoTrack) clonedNativeTrack);
449459
} else {
450460
clonedNativeTrack = pcFactory.createAudioTrack(id, (AudioSource) track.mediaSource);
451461
}
@@ -455,7 +465,8 @@ MediaStreamTrack cloneTrack(String trackId) {
455465
clonedNativeTrack,
456466
track.mediaSource,
457467
track.videoCaptureController,
458-
track.surfaceTextureHelper
468+
track.surfaceTextureHelper,
469+
clonedVideoTrackAdapter
459470
);
460471
clone.setParent(track);
461472
tracks.put(id, clone);
@@ -519,6 +530,11 @@ private static class TrackPrivate {
519530

520531
private final SurfaceTextureHelper surfaceTextureHelper;
521532

533+
/**
534+
* The {@code VideoTrackAdapter} for dimension detection if {@link #track} is a {@link VideoTrack}.
535+
*/
536+
public final VideoTrackAdapter videoTrackAdapter;
537+
522538
/**
523539
* Whether this object has been disposed or not.
524540
*/
@@ -538,16 +554,28 @@ private static class TrackPrivate {
538554
* @param videoCaptureController the {@code AbstractVideoCaptureController} from which the
539555
* specified {@code mediaSource} was created if the specified
540556
* {@code track} is a {@link VideoTrack}
557+
* @param surfaceTextureHelper the {@code SurfaceTextureHelper} for video rendering
558+
* @param videoTrackAdapter the {@code VideoTrackAdapter} for dimension detection if video track
541559
*/
542560
public TrackPrivate(MediaStreamTrack track, MediaSource mediaSource,
543-
AbstractVideoCaptureController videoCaptureController, SurfaceTextureHelper surfaceTextureHelper) {
561+
AbstractVideoCaptureController videoCaptureController, SurfaceTextureHelper surfaceTextureHelper,
562+
VideoTrackAdapter videoTrackAdapter) {
544563
this.track = track;
545564
this.mediaSource = mediaSource;
546565
this.videoCaptureController = videoCaptureController;
547566
this.surfaceTextureHelper = surfaceTextureHelper;
567+
this.videoTrackAdapter = videoTrackAdapter;
548568
this.disposed = false;
549569
}
550570

571+
/**
572+
* Backwards compatibility constructor for audio tracks
573+
*/
574+
public TrackPrivate(MediaStreamTrack track, MediaSource mediaSource,
575+
AbstractVideoCaptureController videoCaptureController, SurfaceTextureHelper surfaceTextureHelper) {
576+
this(track, mediaSource, videoCaptureController, surfaceTextureHelper, null);
577+
}
578+
551579
public void dispose() {
552580
final boolean isClone = this.isClone();
553581
if (!disposed) {
@@ -557,6 +585,11 @@ public void dispose() {
557585
}
558586
}
559587

588+
// Clean up VideoTrackAdapter for video tracks
589+
if (!isClone && videoTrackAdapter != null && track instanceof VideoTrack) {
590+
videoTrackAdapter.removeDimensionDetector((VideoTrack) track);
591+
}
592+
560593
/*
561594
* As per webrtc library documentation - The caller still has ownership of {@code
562595
* surfaceTextureHelper} and is responsible for making sure surfaceTextureHelper.dispose() is

android/src/main/java/com/oney/WebRTCModule/PeerConnectionObserver.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class PeerConnectionObserver implements PeerConnection.Observer {
4141
final Map<String, String> remoteStreamIds; // Stream ID -> React tag
4242
final Map<String, MediaStream> remoteStreams; // React tag -> MediaStream
4343
final Map<String, MediaStreamTrack> remoteTracks;
44-
private final VideoTrackAdapter videoTrackAdapters;
44+
final VideoTrackAdapter videoTrackAdapters;
4545
private final WebRTCModule webRTCModule;
4646

4747
PeerConnectionObserver(WebRTCModule webRTCModule, int id) {
@@ -75,6 +75,16 @@ void dispose() {
7575
for (MediaStreamTrack track : this.remoteTracks.values()) {
7676
if (track instanceof VideoTrack) {
7777
videoTrackAdapters.removeAdapter((VideoTrack) track);
78+
videoTrackAdapters.removeDimensionDetector((VideoTrack) track);
79+
}
80+
}
81+
82+
// Remove video track adapters for local tracks (from senders)
83+
for (RtpSender sender : this.peerConnection.getSenders()) {
84+
MediaStreamTrack track = sender.track();
85+
if (track instanceof VideoTrack) {
86+
videoTrackAdapters.removeAdapter((VideoTrack) track);
87+
// Note: dimension detection for local tracks is cleaned up when track is disposed
7888
}
7989
}
8090

@@ -432,6 +442,7 @@ public void onAddTrack(final RtpReceiver receiver, final MediaStream[] mediaStre
432442
if (!existingTrack) {
433443
if (track.kind().equals(MediaStreamTrack.VIDEO_TRACK_KIND)) {
434444
videoTrackAdapters.addAdapter((VideoTrack) track);
445+
videoTrackAdapters.addDimensionDetector((VideoTrack) track);
435446
}
436447
remoteTracks.put(track.id(), track);
437448
}

android/src/main/java/com/oney/WebRTCModule/VideoTrackAdapter.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class VideoTrackAdapter {
2323
static final long MUTE_DELAY = 1500;
2424

2525
private Map<String, TrackMuteUnmuteImpl> muteImplMap = new HashMap<>();
26+
private Map<String, VideoDimensionDetectorImpl> dimensionDetectorMap = new HashMap<>();
2627

2728
private Timer timer = new Timer("VideoTrackMutedTimer");
2829

@@ -62,6 +63,32 @@ public void removeAdapter(VideoTrack videoTrack) {
6263
Log.d(TAG, "Deleted adapter for " + trackId);
6364
}
6465

66+
public void addDimensionDetector(VideoTrack videoTrack) {
67+
String trackId = videoTrack.id();
68+
if (dimensionDetectorMap.containsKey(trackId)) {
69+
Log.w(TAG, "Attempted to add dimension detector twice for track ID: " + trackId);
70+
return;
71+
}
72+
73+
VideoDimensionDetectorImpl dimensionDetector = new VideoDimensionDetectorImpl(trackId);
74+
Log.d(TAG, "Created dimension detector for " + trackId);
75+
dimensionDetectorMap.put(trackId, dimensionDetector);
76+
videoTrack.addSink(dimensionDetector);
77+
}
78+
79+
public void removeDimensionDetector(VideoTrack videoTrack) {
80+
String trackId = videoTrack.id();
81+
VideoDimensionDetectorImpl dimensionDetector = dimensionDetectorMap.remove(trackId);
82+
if (dimensionDetector == null) {
83+
Log.w(TAG, "removeDimensionDetector - no detector for " + trackId);
84+
return;
85+
}
86+
87+
videoTrack.removeSink(dimensionDetector);
88+
dimensionDetector.dispose();
89+
Log.d(TAG, "Deleted dimension detector for " + trackId);
90+
}
91+
6592
/**
6693
* Implements 'mute'/'unmute' events for remote video tracks through
6794
* the {@link VideoSink} interface.
@@ -134,4 +161,58 @@ void dispose() {
134161
}
135162
}
136163
}
164+
165+
/**
166+
* Implements dimension change events for remote video tracks through
167+
* the {@link VideoSink} interface.
168+
*/
169+
private class VideoDimensionDetectorImpl implements VideoSink {
170+
private volatile boolean disposed;
171+
private int currentWidth = 0;
172+
private int currentHeight = 0;
173+
private boolean hasInitialSize = false;
174+
private final String trackId;
175+
176+
VideoDimensionDetectorImpl(String trackId) {
177+
this.trackId = trackId;
178+
}
179+
180+
@Override
181+
public void onFrame(VideoFrame frame) {
182+
if (disposed) {
183+
return;
184+
}
185+
186+
int width = frame.getBuffer().getWidth();
187+
int height = frame.getBuffer().getHeight();
188+
189+
// Check if this is a meaningful size change
190+
if (!hasInitialSize) {
191+
currentWidth = width;
192+
currentHeight = height;
193+
hasInitialSize = true;
194+
emitDimensionChangeEvent(width, height);
195+
} else if (currentWidth != width || currentHeight != height) {
196+
currentWidth = width;
197+
currentHeight = height;
198+
emitDimensionChangeEvent(width, height);
199+
}
200+
}
201+
202+
private void emitDimensionChangeEvent(int width, int height) {
203+
WritableMap params = Arguments.createMap();
204+
params.putInt("pcId", peerConnectionId);
205+
params.putString("trackId", trackId);
206+
params.putInt("width", width);
207+
params.putInt("height", height);
208+
209+
Log.d(TAG, "Dimension change event pcId: " + peerConnectionId + " trackId: " + trackId + " dimensions: " + width + "x" + height);
210+
211+
VideoTrackAdapter.this.webRTCModule.sendEvent("videoTrackDimensionChanged", params);
212+
}
213+
214+
void dispose() {
215+
disposed = true;
216+
}
217+
}
137218
}

android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,11 @@ public WritableMap peerConnectionAddTransceiver(int id, ReadableMap options) {
504504
MediaStreamTrack track = getLocalTrack(trackId);
505505
transceiver = pco.addTransceiver(
506506
track, SerializeUtils.parseTransceiverOptions(options.getMap("init")));
507+
508+
// Add mute detection for local video tracks (dimension detection is handled at track creation)
509+
if (track instanceof VideoTrack) {
510+
pco.videoTrackAdapters.addAdapter((VideoTrack) track);
511+
}
507512

508513
} else {
509514
// This should technically never happen as the JS side checks for that.
@@ -556,6 +561,11 @@ public WritableMap peerConnectionAddTrack(int id, String trackId, ReadableMap op
556561
}
557562
}
558563
RtpSender sender = pco.getPeerConnection().addTrack(track, streamIds);
564+
565+
// Add mute detection for local video tracks (dimension detection is handled at track creation)
566+
if (track instanceof VideoTrack) {
567+
pco.videoTrackAdapters.addAdapter((VideoTrack) track);
568+
}
559569

560570
// Need to get the corresponding transceiver as well
561571
RtpTransceiver transceiver = pco.getTransceiver(sender.id());
@@ -591,6 +601,13 @@ public boolean peerConnectionRemoveTrack(int id, String senderId) {
591601
return false;
592602
}
593603

604+
// Remove video track adapters for local tracks
605+
MediaStreamTrack track = sender.track();
606+
if (track instanceof VideoTrack) {
607+
pco.videoTrackAdapters.removeAdapter((VideoTrack) track);
608+
// Note: dimension detection for local tracks is cleaned up when track is disposed
609+
}
610+
594611
return pco.getPeerConnection().removeTrack(sender);
595612
})
596613
.get();

ios/RCTWebRTC/WebRTCModule+RTCMediaStream.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@
1010
(CaptureController * (^)(RTCVideoSource *))captureControllerCreator;
1111
- (NSArray *)createMediaStream:(NSArray<RTCMediaStreamTrack *> *)tracks;
1212

13+
- (void)addLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack;
14+
- (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack;
15+
1316
@end

ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#import "WebRTCModuleOptions.h"
1111
#import "WebRTCModule+RTCMediaStream.h"
1212
#import "WebRTCModule+RTCPeerConnection.h"
13+
#import "WebRTCModule+VideoTrackAdapter.h"
1314

1415
#import "ProcessorProvider.h"
1516
#import "ScreenCaptureController.h"
@@ -60,6 +61,9 @@ - (RTCVideoTrack *)createVideoTrackWithCaptureController:
6061
videoTrack.captureController = captureController;
6162
[captureController startCapture];
6263

64+
// Add dimension detection for local video tracks immediately
65+
[self addLocalVideoTrackDimensionDetection:videoTrack];
66+
6367
return videoTrack;
6468
#endif
6569
}
@@ -136,6 +140,9 @@ - (RTCVideoTrack *)createVideoTrack:(NSDictionary *)constraints {
136140
[videoCaptureController startCapture];
137141
#endif
138142

143+
// Add dimension detection for local video tracks immediately
144+
[self addLocalVideoTrackDimensionDetection:videoTrack];
145+
139146
return videoTrack;
140147
#endif
141148
}
@@ -159,6 +166,9 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack {
159166
videoTrack.captureController = screenCaptureController;
160167
[screenCaptureController startCapture];
161168

169+
// Add dimension detection for local video tracks immediately
170+
[self addLocalVideoTrackDimensionDetection:videoTrack];
171+
162172
return videoTrack;
163173
}
164174

@@ -276,7 +286,7 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack {
276286
#endif
277287
}
278288

279-
#pragma mark - Other stream related APIs
289+
#pragma mark - enumerateDevices
280290

281291
RCT_EXPORT_METHOD(enumerateDevices : (RCTResponseSenderBlock)callback) {
282292
#if TARGET_OS_TV
@@ -332,6 +342,45 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack {
332342
#endif
333343
}
334344

345+
#pragma mark - Local Video Track Dimension Detection
346+
347+
- (void)addLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack {
348+
if (!videoTrack) {
349+
return;
350+
}
351+
352+
// Create a dimension detector for this local track
353+
VideoDimensionDetector *detector = [[VideoDimensionDetector alloc] initWith:@(-1) // -1 for local tracks
354+
trackId:videoTrack.trackId
355+
webRTCModule:self];
356+
357+
// Store the detector using associated objects on the track itself
358+
objc_setAssociatedObject(videoTrack, @selector(addLocalVideoTrackDimensionDetection:), detector, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
359+
360+
// Add the detector as a renderer to the track
361+
[videoTrack addRenderer:detector];
362+
363+
RCTLogTrace(@"[VideoTrackAdapter] Local dimension detector created for track %@", videoTrack.trackId);
364+
}
365+
366+
- (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack {
367+
if (!videoTrack) {
368+
return;
369+
}
370+
371+
// Get the associated detector
372+
VideoDimensionDetector *detector = objc_getAssociatedObject(videoTrack, @selector(addLocalVideoTrackDimensionDetection:));
373+
374+
if (detector) {
375+
[videoTrack removeRenderer:detector];
376+
[detector dispose];
377+
objc_setAssociatedObject(videoTrack, @selector(addLocalVideoTrackDimensionDetection:), nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
378+
RCTLogTrace(@"[VideoTrackAdapter] Local dimension detector removed for track %@", videoTrack.trackId);
379+
}
380+
}
381+
382+
#pragma mark - Other stream related APIs
383+
335384
RCT_EXPORT_METHOD(mediaStreamCreate : (nonnull NSString *)streamID) {
336385
RTCMediaStream *mediaStream = [self.peerConnectionFactory mediaStreamWithStreamId:streamID];
337386
self.localStreams[streamID] = mediaStream;
@@ -393,6 +442,11 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack {
393442

394443
RTCMediaStreamTrack *track = self.localTracks[trackID];
395444
if (track) {
445+
// Clean up dimension detection for local video tracks
446+
if ([track.kind isEqualToString:@"video"]) {
447+
[self removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)track];
448+
}
449+
396450
track.isEnabled = NO;
397451
[track.captureController stopCapture];
398452
[self.localTracks removeObjectForKey:trackID];
@@ -425,6 +479,10 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack {
425479
RTCVideoSource *videoSource = originalVideoTrack.source;
426480
RTCVideoTrack *videoTrack = [self.peerConnectionFactory videoTrackWithSource:videoSource trackId:trackUUID];
427481
videoTrack.isEnabled = originalTrack.isEnabled;
482+
483+
// Add dimension detection for cloned local video tracks
484+
[self addLocalVideoTrackDimensionDetection:videoTrack];
485+
428486
[self.localTracks setObject:videoTrack forKey:trackUUID];
429487
for (NSString* streamId in self.localStreams) {
430488
RTCMediaStream* stream = [self.localStreams objectForKey:streamId];

0 commit comments

Comments
 (0)