Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bc486df
build(deps-dev): bump minimatch from 3.1.2 to 3.1.5 (#1791)
dependabot[bot] Apr 30, 2026
fdc4771
build(deps-dev): bump @babel/plugin-transform-modules-systemjs (#1809)
dependabot[bot] May 9, 2026
7f851d5
build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (#1773)
dependabot[bot] May 9, 2026
3655418
fix: preserve facing mode in applyConstraints
naveenkirugulige May 11, 2026
1e99057
ci: pinact (#84)
davidliu May 26, 2026
bfb1b73
chore(pinact): pin/update GitHub Actions (#85)
davidliu May 26, 2026
287fca2
android: bump libwebrtc to 144.7559.05 (#83)
davidliu May 26, 2026
7713a83
release: 144.1.0 (#86)
davidliu May 26, 2026
e5d8781
ios: fix trigger broadcast picker on new arch
Calinteodor Jun 4, 2026
196cbb3
fix(ios): bound AudioDeviceModuleObserver JS waits to break the deadl…
hiroshihorie Jun 17, 2026
c751bd6
ios: bump WebRTC-SDK to 144.7559.10 (#92)
hiroshihorie Jun 18, 2026
a95d57d
fix(ios): skip AudioDeviceModule JS round-trips when no handler is re…
hiroshihorie Jun 18, 2026
d315093
release: 144.1.1 (#93)
davidliu Jun 18, 2026
c4ea2d3
build(deps-dev): bump js-yaml from 4.1.1 to 4.2.0 (#1818)
dependabot[bot] Jun 23, 2026
0c63622
registerGlobals: don't set RTCRtpSender/RTCRtpReceiver twice
fippo Jul 4, 2026
e8face3
dispose the peer connection after signalingState changes to "closed" …
fippo Jul 5, 2026
3537d38
build(deps-dev): bump @babel/core in /examples/GumTestApp_macOS (#1817)
dependabot[bot] Jul 5, 2026
5892a0f
ios(RCTWebRTC): enable Center Stage for devices that support it
Calinteodor Jul 8, 2026
0a89cc0
ci(ios): pin runner to macos-15 (Xcode 16.4)
Calinteodor Jul 8, 2026
78904d2
android: fix race condition in getDisplayMedia
saghul Mar 20, 2026
b9b5ded
release: 124.0.8
saghul Jul 21, 2026
cf6101c
build(deps-dev): bump js-yaml from 4.2.0 to 4.3.0
dependabot[bot] Jul 21, 2026
a312ef9
feat(ios): configure the default audio session natively in the observ…
hiroshihorie Jul 23, 2026
2edc2f0
release: 144.1.2 (#97)
davidliu Jul 23, 2026
0483cc8
android: release intermediate video effect frames
HanSeonWoo Jul 28, 2026
78d3094
fix(android): guard against duplicate onActivityResult in screen capture
Jul 29, 2026
014a8cf
build: ship the vendored declarations in the typescript build
saghul Jul 31, 2026
9a5a928
dispose the peer connection after signalingState changes to "closed" …
fippo Jul 5, 2026
b95c41c
registerGlobals: don't set RTCRtpSender/RTCRtpReceiver twice
fippo Jul 4, 2026
7694cad
fix: preserve facing mode in applyConstraints
naveenkirugulige May 11, 2026
9bfe18a
fix(android): guard against duplicate onActivityResult in screen capture
Jul 29, 2026
5785bca
ios: fix trigger broadcast picker on new arch
Calinteodor Jun 4, 2026
e9eaa8f
ios(RCTWebRTC): enable Center Stage for devices that support it
Calinteodor Jul 8, 2026
4754c5f
build: ship the vendored declarations in the typescript build
saghul Jul 31, 2026
5b08af1
merge: sync merge-base with rn-webrtc/master
santhoshvai Aug 13, 2026
e167503
merge: sync merge-base with livekit/master
santhoshvai Aug 13, 2026
6aadb83
style: clang-format the Center Stage changes
santhoshvai Aug 13, 2026
aefbef7
fix: don't mutate the caller's constraints in applyConstraints
santhoshvai Aug 13, 2026
b1c0b91
Merge branch 'master' into sync-upstream
santhoshvai Aug 13, 2026
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
24 changes: 20 additions & 4 deletions android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ public void onServiceDisconnected(ComponentName name) {
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
super.onActivityResult(activity, requestCode, resultCode, data);
if (requestCode == PERMISSION_REQUEST_CODE) {
// Guard against a duplicate onActivityResult dispatch. Some hosts (e.g.
// react-native-navigation) forward the activity result to every registered
// ActivityEventListener more than once, so this callback can fire twice for a
// single getDisplayMedia() request. The first pass consumes displayMediaPromise;
// a second pass would call reject()/resolve() on a null promise and crash.
if (displayMediaPromise == null) {
return;
}

if (resultCode != Activity.RESULT_OK) {
displayMediaPromise.reject("DOMException", "NotAllowedError");
displayMediaPromise = null;
Expand Down Expand Up @@ -429,12 +438,19 @@ public void run() {
}

private void createScreenStream() {
// A duplicate dispatch (see onActivityResult above) can schedule this more than once: a
// repeated launch() rebinds the service, so onServiceConnected can fire twice. The
// single-threaded executor runs them in order, so by the time a duplicate runs the first has
// already consumed displayMediaPromise. Bail out instead of dereferencing a null promise or
// creating a second screen stream.
if (displayMediaPromise == null) {
return;
}

final PeerConnectionFactoryProvider factoryProvider = displayMediaFactory;
if (factoryProvider == null || factoryProvider.isDisposed()) {
if (displayMediaPromise != null) {
displayMediaPromise.reject("ERR_MODULE_DISPOSED", "WebRTCModule disposed during getDisplayMedia");
displayMediaPromise = null;
}
displayMediaPromise.reject("ERR_MODULE_DISPOSED", "WebRTCModule disposed during getDisplayMedia");
displayMediaPromise = null;
displayMediaFactory = null;
return;
}
Expand Down
45 changes: 22 additions & 23 deletions ios/RCTWebRTC/ScreenCapturePickerViewManager.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#if TARGET_OS_IOS

#import <React/RCTUIManager.h>
#import <React/RCTLog.h>
#import <ReplayKit/ReplayKit.h>

#import "ScreenCapturePickerViewManager.h"
Expand Down Expand Up @@ -30,29 +30,28 @@ - (NSString *)preferredExtension {
}

RCT_EXPORT_METHOD(show : (nonnull NSNumber *)reactTag) {
[self.bridge.uiManager
addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[RPSystemBroadcastPickerView class]]) {
RCTLogError(@"Invalid view returned from registry, expecting "
@"RPSystemBroadcastPickerView, got: %@",
view);
} else {
// Simulate a click
UIButton *btn = nil;

for (UIView *subview in ((RPSystemBroadcastPickerView *)view).subviews) {
if ([subview isKindOfClass:[UIButton class]]) {
btn = (UIButton *)subview;
}
}
if (btn != nil) {
[btn sendActionsForControlEvents:UIControlEventTouchUpInside];
} else {
RCTLogError(@"RPSystemBroadcastPickerView button not found");
}
dispatch_async(dispatch_get_main_queue(), ^{
RPSystemBroadcastPickerView *picker = self->_broadcastPickerView;
if (![picker isKindOfClass:[RPSystemBroadcastPickerView class]]) {
RCTLogError(@"Invalid broadcast picker view, expecting "
@"RPSystemBroadcastPickerView, got: %@",
picker);
return;
}

UIButton *btn = nil;

for (UIView *subview in picker.subviews) {
if ([subview isKindOfClass:[UIButton class]]) {
btn = (UIButton *)subview;
}
}];
}
if (btn != nil) {
[btn sendActionsForControlEvents:UIControlEventTouchUpInside];
} else {
RCTLogError(@"RPSystemBroadcastPickerView button not found");
}
});
}

@end
Expand Down
56 changes: 55 additions & 1 deletion ios/RCTWebRTC/VideoCaptureController.m
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,40 @@ - (void)startCapture {
return;
}

// Enable Center Stage when the device supports it; cooperative with Control Center.
if (@available(iOS 16.0, *)) {
BOOL centerStageSupported = NO;
for (AVCaptureDeviceFormat *fmt in [RTCCameraVideoCapturer supportedFormatsForDevice:self.device]) {
if (fmt.isCenterStageSupported) {
centerStageSupported = YES;
break;
}
}
if (centerStageSupported) {
AVCaptureDevice.centerStageControlMode = AVCaptureCenterStageControlModeCooperative;
AVCaptureDevice.centerStageEnabled = YES;
} else if (AVCaptureDevice.isCenterStageEnabled) {
AVCaptureDevice.centerStageEnabled = NO;
}
}

AVCaptureDeviceFormat *format = [self selectFormatForDevice:self.device
withTargetWidth:self.width
withTargetHeight:self.height];

// On multi cam devices selectFormatForDevice also requires format.multiCamSupported, and that set
// can be disjoint from the Center Stage one, leaving nothing to pick. Give up Center Stage rather
// than the capture: the control mode is cooperative above, so we are allowed to turn it off, and
// doing so lets AVFoundation accept a non-Center-Stage activeFormat.
if (@available(iOS 16.0, *)) {
if (!format && AVCaptureDevice.isCenterStageEnabled) {
RCTLogWarn(@"[VideoCaptureController] No Center Stage format for device %@, disabling Center Stage",
self.device);
AVCaptureDevice.centerStageEnabled = NO;
format = [self selectFormatForDevice:self.device withTargetWidth:self.width withTargetHeight:self.height];
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

if (!format) {
RCTLogWarn(@"[VideoCaptureController] No valid formats for device %@", self.device);

Expand All @@ -63,6 +94,17 @@ - (void)startCapture {

self.selectedFormat = format;

// Clamp fps to the range Center Stage allows for this format.
int fps = self.frameRate;
if (@available(iOS 16.0, *)) {
if (AVCaptureDevice.isCenterStageEnabled) {
AVFrameRateRange *csRange = format.videoFrameRateRangeForCenterStage;
if (csRange) {
fps = MAX((int)csRange.minFrameRate, MIN(fps, (int)csRange.maxFrameRate));
}
}
}

AVCaptureSession *session = self.capturer.captureSession;
if (@available(iOS 16.0, *)) {
BOOL enable = self.enableMultitaskingCameraAccess;
Expand All @@ -84,7 +126,7 @@ - (void)startCapture {
__weak VideoCaptureController *weakSelf = self;
[self.capturer startCaptureWithDevice:self.device
format:format
fps:self.frameRate
fps:fps
completionHandler:^(NSError *err) {
if (err) {
RCTLogError(@"[VideoCaptureController] Error starting capture: %@", err);
Expand Down Expand Up @@ -296,6 +338,11 @@ - (AVCaptureDeviceFormat *)selectFormatForDevice:(AVCaptureDevice *)device
AVCaptureDeviceFormat *selectedFormat = nil;
int currentDiff = INT_MAX;

BOOL centerStageEnabled = NO;
if (@available(iOS 16.0, *)) {
centerStageEnabled = AVCaptureDevice.isCenterStageEnabled;
}

for (AVCaptureDeviceFormat *format in formats) {
// Only use multi cam formats when on multi cam supported devices.
if (@available(iOS 13.0, macOS 14.0, tvOS 17.0, *)) {
Expand All @@ -304,6 +351,13 @@ - (AVCaptureDeviceFormat *)selectFormatForDevice:(AVCaptureDevice *)device
}
}

// Center Stage only permits supported formats.
if (@available(iOS 16.0, *)) {
if (centerStageEnabled && !format.isCenterStageSupported) {
continue;
}
}

CMVideoDimensions dimension = CMVideoFormatDescriptionGetDimensions(format.formatDescription);
FourCharCode pixelFormat = CMFormatDescriptionGetMediaSubType(format.formatDescription);
int diff = abs(targetWidth - dimension.width) + abs(targetHeight - dimension.height);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"build": "bob build",
"lint": "eslint --max-warnings 0 . && tsc --noEmit",
"lintfix": "eslint --max-warnings 0 --fix . && tsc --noEmit",
"prepare": "husky install && bob build",
"prepare": "husky install && bob build && node tools/postbuild.mjs",
"format": "tools/format.sh",
"semantic-release": "semantic-release"
},
Expand Down
13 changes: 11 additions & 2 deletions src/MediaStreamTrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,19 @@ export default class MediaStreamTrack extends EventTarget<MediaStreamTrackEventM
return;
}

const normalized = normalizeConstraints({ video: constraints ?? true });
// Work on a copy: `constraints` belongs to the caller, who may have frozen it, and
// _constraints must not alias an object the caller can mutate after we return.
const effective: MediaTrackConstraints = { ...constraints };

// Preserve current facing mode when user doesn't specify one
if (constraints && !effective.facingMode && this._settings?.facingMode) {
effective.facingMode = this._settings.facingMode;
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const normalized = normalizeConstraints({ video: constraints ? effective : true });

this._settings = await WebRTCModule.mediaStreamTrackApplyConstraints(this.id, normalized.video);
this._constraints = constraints ?? {};
this._constraints = effective;
}

clone(): MediaStreamTrack {
Expand Down
14 changes: 7 additions & 7 deletions src/RTCPeerConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,13 +742,6 @@ export default class RTCPeerConnection extends EventTarget<RTCPeerConnectionEven
this.connectionState = ev.connectionState;

this.dispatchEvent(new Event('connectionstatechange'));

if (ev.connectionState === 'closed') {
// This PeerConnection is done, clean up.
removeListener(this);

WebRTCModule.peerConnectionDispose(this._pcId);
}
});

addListener(this, 'peerConnectionSignalingStateChanged', (ev: any) => {
Expand All @@ -759,6 +752,13 @@ export default class RTCPeerConnection extends EventTarget<RTCPeerConnectionEven
this.signalingState = ev.signalingState;

this.dispatchEvent(new Event('signalingstatechange'));

if (ev.signalingState === 'closed') {
// This PeerConnection is done, clean up.
removeListener(this);

WebRTCModule.peerConnectionDispose(this._pcId);
}
});

// Consider moving away from this event: https://github.com/WebKit/WebKit/pull/3953
Expand Down
2 changes: 0 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ function registerGlobals(): void {
global.RTCIceCandidate = RTCIceCandidate;
global.RTCCertificate = RTCCertificate;
global.RTCPeerConnection = RTCPeerConnection;
global.RTCRtpReceiver = RTCRtpReceiver;
global.RTCRtpSender = RTCRtpReceiver;
global.RTCSessionDescription = RTCSessionDescription;
global.MediaStream = MediaStream;
global.MediaStreamTrack = MediaStreamTrack;
Expand Down
67 changes: 67 additions & 0 deletions tools/postbuild.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env node

/**
* Fixes up the bob build output for the declaration files which are shipped as-is
* (currently the ones for the vendored event-target-shim). bob mishandles them in
* both directions, so for every .d.ts file in src/ we:
*
* - Copy it into the typescript output. That target builds with
* `tsc --emitDeclarationOnly`, and tsc never emits output for .d.ts inputs, so the
* relative imports pointing at them fail to resolve for consumers using the
* declaration files, which silently strips the EventTarget members off our classes.
*
* - Delete the empty module babel emitted for it in the commonjs and module outputs.
* Those targets compile every source file and rewrite the extension to .js, turning
* index.d.ts into an index.d.js which holds no code and which nothing imports.
*/

import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';

const root = path.resolve(import.meta.dirname, '..');
const source = path.join(root, 'src');
const output = path.join(root, 'lib');
const declarationsOutput = path.join(output, 'typescript');
const compiledOutputs = [ path.join(output, 'commonjs'), path.join(output, 'module') ];

/**
* Finds every declaration file below the given directory, as paths relative to it.
*/
function findDeclarations(directory, prefix = '') {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
const entryPath = path.join(prefix, entry.name);

if (entry.isDirectory()) {
return findDeclarations(path.join(directory, entry.name), entryPath);
}

return entry.name.endsWith('.d.ts') ? [ entryPath ] : [];
});
}

if (!fs.existsSync(output)) {
console.error(`${path.relative(root, output)} not found, run "bob build" first.`);
process.exit(1);
}

for (const declaration of findDeclarations(source)) {
const target = path.join(declarationsOutput, declaration);

fs.mkdirSync(path.dirname(target), { recursive: true });
fs.copyFileSync(path.join(source, declaration), target);

console.info(`Copied src/${declaration} -> ${path.relative(root, target)}`);

for (const compiledOutput of compiledOutputs) {
const compiled = path.join(compiledOutput, declaration.replace(/\.ts$/, '.js'));

for (const artifact of [ compiled, `${compiled}.map` ]) {
if (fs.existsSync(artifact)) {
fs.rmSync(artifact);

console.info(`Removed ${path.relative(root, artifact)}`);
}
}
}
}
Loading