Skip to content

feat: add a video track dimensions event #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,12 @@ VideoTrack createVideoTrack(AbstractVideoCaptureController videoCaptureControlle

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

// Add dimension detection for local video tracks immediately when created
VideoTrackAdapter localTrackAdapter = new VideoTrackAdapter(webRTCModule, -1); // Use -1 for local tracks
localTrackAdapter.addDimensionDetector(track);

track.setEnabled(true);
tracks.put(id, new TrackPrivate(track, videoSource, videoCaptureController, surfaceTextureHelper));
tracks.put(id, new TrackPrivate(track, videoSource, videoCaptureController, surfaceTextureHelper, localTrackAdapter));

videoCaptureController.startCapture();

Expand All @@ -444,8 +448,14 @@ MediaStreamTrack cloneTrack(String trackId) {
String id = UUID.randomUUID().toString();
MediaStreamTrack nativeTrack = track.track;
final MediaStreamTrack clonedNativeTrack;
VideoTrackAdapter clonedVideoTrackAdapter = null;

if (nativeTrack instanceof VideoTrack) {
clonedNativeTrack = pcFactory.createVideoTrack(id, (VideoSource) track.mediaSource);

// Create dimension detection for cloned video tracks
clonedVideoTrackAdapter = new VideoTrackAdapter(webRTCModule, -1);
clonedVideoTrackAdapter.addDimensionDetector((VideoTrack) clonedNativeTrack);
} else {
clonedNativeTrack = pcFactory.createAudioTrack(id, (AudioSource) track.mediaSource);
}
Expand All @@ -455,7 +465,8 @@ MediaStreamTrack cloneTrack(String trackId) {
clonedNativeTrack,
track.mediaSource,
track.videoCaptureController,
track.surfaceTextureHelper
track.surfaceTextureHelper,
clonedVideoTrackAdapter
);
clone.setParent(track);
tracks.put(id, clone);
Expand Down Expand Up @@ -519,6 +530,11 @@ private static class TrackPrivate {

private final SurfaceTextureHelper surfaceTextureHelper;

/**
* The {@code VideoTrackAdapter} for dimension detection if {@link #track} is a {@link VideoTrack}.
*/
public final VideoTrackAdapter videoTrackAdapter;

/**
* Whether this object has been disposed or not.
*/
Expand All @@ -538,16 +554,28 @@ private static class TrackPrivate {
* @param videoCaptureController the {@code AbstractVideoCaptureController} from which the
* specified {@code mediaSource} was created if the specified
* {@code track} is a {@link VideoTrack}
* @param surfaceTextureHelper the {@code SurfaceTextureHelper} for video rendering
* @param videoTrackAdapter the {@code VideoTrackAdapter} for dimension detection if video track
*/
public TrackPrivate(MediaStreamTrack track, MediaSource mediaSource,
AbstractVideoCaptureController videoCaptureController, SurfaceTextureHelper surfaceTextureHelper) {
AbstractVideoCaptureController videoCaptureController, SurfaceTextureHelper surfaceTextureHelper,
VideoTrackAdapter videoTrackAdapter) {
this.track = track;
this.mediaSource = mediaSource;
this.videoCaptureController = videoCaptureController;
this.surfaceTextureHelper = surfaceTextureHelper;
this.videoTrackAdapter = videoTrackAdapter;
this.disposed = false;
}

/**
* Backwards compatibility constructor for audio tracks
*/
public TrackPrivate(MediaStreamTrack track, MediaSource mediaSource,
AbstractVideoCaptureController videoCaptureController, SurfaceTextureHelper surfaceTextureHelper) {
this(track, mediaSource, videoCaptureController, surfaceTextureHelper, null);
}

public void dispose() {
final boolean isClone = this.isClone();
if (!disposed) {
Expand All @@ -557,6 +585,11 @@ public void dispose() {
}
}

// Clean up VideoTrackAdapter for video tracks
if (!isClone && videoTrackAdapter != null && track instanceof VideoTrack) {
videoTrackAdapter.removeDimensionDetector((VideoTrack) track);
}

/*
* As per webrtc library documentation - The caller still has ownership of {@code
* surfaceTextureHelper} and is responsible for making sure surfaceTextureHelper.dispose() is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class PeerConnectionObserver implements PeerConnection.Observer {
final Map<String, String> remoteStreamIds; // Stream ID -> React tag
final Map<String, MediaStream> remoteStreams; // React tag -> MediaStream
final Map<String, MediaStreamTrack> remoteTracks;
private final VideoTrackAdapter videoTrackAdapters;
final VideoTrackAdapter videoTrackAdapters;
private final WebRTCModule webRTCModule;

PeerConnectionObserver(WebRTCModule webRTCModule, int id) {
Expand Down Expand Up @@ -75,6 +75,16 @@ void dispose() {
for (MediaStreamTrack track : this.remoteTracks.values()) {
if (track instanceof VideoTrack) {
videoTrackAdapters.removeAdapter((VideoTrack) track);
videoTrackAdapters.removeDimensionDetector((VideoTrack) track);
}
}

// Remove video track adapters for local tracks (from senders)
for (RtpSender sender : this.peerConnection.getSenders()) {
MediaStreamTrack track = sender.track();
if (track instanceof VideoTrack) {
videoTrackAdapters.removeAdapter((VideoTrack) track);
// Note: dimension detection for local tracks is cleaned up when track is disposed
}
}

Expand Down Expand Up @@ -432,6 +442,7 @@ public void onAddTrack(final RtpReceiver receiver, final MediaStream[] mediaStre
if (!existingTrack) {
if (track.kind().equals(MediaStreamTrack.VIDEO_TRACK_KIND)) {
videoTrackAdapters.addAdapter((VideoTrack) track);
videoTrackAdapters.addDimensionDetector((VideoTrack) track);
}
remoteTracks.put(track.id(), track);
}
Expand Down
81 changes: 81 additions & 0 deletions android/src/main/java/com/oney/WebRTCModule/VideoTrackAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class VideoTrackAdapter {
static final long MUTE_DELAY = 1500;

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

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

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

public void addDimensionDetector(VideoTrack videoTrack) {
String trackId = videoTrack.id();
if (dimensionDetectorMap.containsKey(trackId)) {
Log.w(TAG, "Attempted to add dimension detector twice for track ID: " + trackId);
return;
}

VideoDimensionDetectorImpl dimensionDetector = new VideoDimensionDetectorImpl(trackId);
Log.d(TAG, "Created dimension detector for " + trackId);
dimensionDetectorMap.put(trackId, dimensionDetector);
videoTrack.addSink(dimensionDetector);
}

public void removeDimensionDetector(VideoTrack videoTrack) {
String trackId = videoTrack.id();
VideoDimensionDetectorImpl dimensionDetector = dimensionDetectorMap.remove(trackId);
if (dimensionDetector == null) {
Log.w(TAG, "removeDimensionDetector - no detector for " + trackId);
return;
}

videoTrack.removeSink(dimensionDetector);
dimensionDetector.dispose();
Log.d(TAG, "Deleted dimension detector for " + trackId);
}

/**
* Implements 'mute'/'unmute' events for remote video tracks through
* the {@link VideoSink} interface.
Expand Down Expand Up @@ -134,4 +161,58 @@ void dispose() {
}
}
}

/**
* Implements dimension change events for remote video tracks through
* the {@link VideoSink} interface.
*/
private class VideoDimensionDetectorImpl implements VideoSink {
private volatile boolean disposed;
private int currentWidth = 0;
private int currentHeight = 0;
private boolean hasInitialSize = false;
private final String trackId;

VideoDimensionDetectorImpl(String trackId) {
this.trackId = trackId;
}

@Override
public void onFrame(VideoFrame frame) {
if (disposed) {
return;
}

int width = frame.getBuffer().getWidth();
int height = frame.getBuffer().getHeight();

// Check if this is a meaningful size change
if (!hasInitialSize) {
currentWidth = width;
currentHeight = height;
hasInitialSize = true;
emitDimensionChangeEvent(width, height);
} else if (currentWidth != width || currentHeight != height) {
currentWidth = width;
currentHeight = height;
emitDimensionChangeEvent(width, height);
}
}

private void emitDimensionChangeEvent(int width, int height) {
WritableMap params = Arguments.createMap();
params.putInt("pcId", peerConnectionId);
params.putString("trackId", trackId);
params.putInt("width", width);
params.putInt("height", height);

Log.d(TAG, "Dimension change event pcId: " + peerConnectionId + " trackId: " + trackId + " dimensions: " + width + "x" + height);

VideoTrackAdapter.this.webRTCModule.sendEvent("videoTrackDimensionChanged", params);
}

void dispose() {
disposed = true;
}
}
}
17 changes: 17 additions & 0 deletions android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,11 @@ public WritableMap peerConnectionAddTransceiver(int id, ReadableMap options) {
MediaStreamTrack track = getLocalTrack(trackId);
transceiver = pco.addTransceiver(
track, SerializeUtils.parseTransceiverOptions(options.getMap("init")));

// Add mute detection for local video tracks (dimension detection is handled at track creation)
if (track instanceof VideoTrack) {
pco.videoTrackAdapters.addAdapter((VideoTrack) track);
}

} else {
// This should technically never happen as the JS side checks for that.
Expand Down Expand Up @@ -556,6 +561,11 @@ public WritableMap peerConnectionAddTrack(int id, String trackId, ReadableMap op
}
}
RtpSender sender = pco.getPeerConnection().addTrack(track, streamIds);

// Add mute detection for local video tracks (dimension detection is handled at track creation)
if (track instanceof VideoTrack) {
pco.videoTrackAdapters.addAdapter((VideoTrack) track);
}

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

// Remove video track adapters for local tracks
MediaStreamTrack track = sender.track();
if (track instanceof VideoTrack) {
pco.videoTrackAdapters.removeAdapter((VideoTrack) track);
// Note: dimension detection for local tracks is cleaned up when track is disposed
}

return pco.getPeerConnection().removeTrack(sender);
})
.get();
Expand Down
3 changes: 3 additions & 0 deletions ios/RCTWebRTC/WebRTCModule+RTCMediaStream.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@
(CaptureController * (^)(RTCVideoSource *))captureControllerCreator;
- (NSArray *)createMediaStream:(NSArray<RTCMediaStreamTrack *> *)tracks;

- (void)addLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack;
- (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack;

@end
60 changes: 59 additions & 1 deletion ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import "WebRTCModuleOptions.h"
#import "WebRTCModule+RTCMediaStream.h"
#import "WebRTCModule+RTCPeerConnection.h"
#import "WebRTCModule+VideoTrackAdapter.h"

#import "ProcessorProvider.h"
#import "ScreenCaptureController.h"
Expand Down Expand Up @@ -60,6 +61,9 @@ - (RTCVideoTrack *)createVideoTrackWithCaptureController:
videoTrack.captureController = captureController;
[captureController startCapture];

// Add dimension detection for local video tracks immediately
[self addLocalVideoTrackDimensionDetection:videoTrack];

return videoTrack;
#endif
}
Expand Down Expand Up @@ -136,6 +140,9 @@ - (RTCVideoTrack *)createVideoTrack:(NSDictionary *)constraints {
[videoCaptureController startCapture];
#endif

// Add dimension detection for local video tracks immediately
[self addLocalVideoTrackDimensionDetection:videoTrack];

return videoTrack;
#endif
}
Expand All @@ -159,6 +166,9 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack {
videoTrack.captureController = screenCaptureController;
[screenCaptureController startCapture];

// Add dimension detection for local video tracks immediately
[self addLocalVideoTrackDimensionDetection:videoTrack];

return videoTrack;
}

Expand Down Expand Up @@ -276,7 +286,7 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack {
#endif
}

#pragma mark - Other stream related APIs
#pragma mark - enumerateDevices

RCT_EXPORT_METHOD(enumerateDevices : (RCTResponseSenderBlock)callback) {
#if TARGET_OS_TV
Expand Down Expand Up @@ -332,6 +342,45 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack {
#endif
}

#pragma mark - Local Video Track Dimension Detection

- (void)addLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack {
if (!videoTrack) {
return;
}

// Create a dimension detector for this local track
VideoDimensionDetector *detector = [[VideoDimensionDetector alloc] initWith:@(-1) // -1 for local tracks
trackId:videoTrack.trackId
webRTCModule:self];

// Store the detector using associated objects on the track itself
objc_setAssociatedObject(videoTrack, @selector(addLocalVideoTrackDimensionDetection:), detector, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

// Add the detector as a renderer to the track
[videoTrack addRenderer:detector];

RCTLogTrace(@"[VideoTrackAdapter] Local dimension detector created for track %@", videoTrack.trackId);
}

- (void)removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)videoTrack {
if (!videoTrack) {
return;
}

// Get the associated detector
VideoDimensionDetector *detector = objc_getAssociatedObject(videoTrack, @selector(addLocalVideoTrackDimensionDetection:));

if (detector) {
[videoTrack removeRenderer:detector];
[detector dispose];
objc_setAssociatedObject(videoTrack, @selector(addLocalVideoTrackDimensionDetection:), nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
RCTLogTrace(@"[VideoTrackAdapter] Local dimension detector removed for track %@", videoTrack.trackId);
}
}

#pragma mark - Other stream related APIs

RCT_EXPORT_METHOD(mediaStreamCreate : (nonnull NSString *)streamID) {
RTCMediaStream *mediaStream = [self.peerConnectionFactory mediaStreamWithStreamId:streamID];
self.localStreams[streamID] = mediaStream;
Expand Down Expand Up @@ -393,6 +442,11 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack {

RTCMediaStreamTrack *track = self.localTracks[trackID];
if (track) {
// Clean up dimension detection for local video tracks
if ([track.kind isEqualToString:@"video"]) {
[self removeLocalVideoTrackDimensionDetection:(RTCVideoTrack *)track];
}

track.isEnabled = NO;
[track.captureController stopCapture];
[self.localTracks removeObjectForKey:trackID];
Expand Down Expand Up @@ -425,6 +479,10 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack {
RTCVideoSource *videoSource = originalVideoTrack.source;
RTCVideoTrack *videoTrack = [self.peerConnectionFactory videoTrackWithSource:videoSource trackId:trackUUID];
videoTrack.isEnabled = originalTrack.isEnabled;

// Add dimension detection for cloned local video tracks
[self addLocalVideoTrackDimensionDetection:videoTrack];

[self.localTracks setObject:videoTrack forKey:trackUUID];
for (NSString* streamId in self.localStreams) {
RTCMediaStream* stream = [self.localStreams objectForKey:streamId];
Expand Down
Loading
Loading