Skip to content

Commit 2d0fc95

Browse files
committed
Merge branch 'master' into ios-render-improv
# Conflicts: # ios/RCTWebRTC/WebRTCModule.m # package.json
2 parents b877d25 + 3209a9b commit 2d0fc95

100 files changed

Lines changed: 6782 additions & 859 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/upstream-sync.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Upstream Sync Skill
2+
3+
Sync fork with one or more upstream remotes via cherry-pick + merge-base advance.
4+
5+
## When to use
6+
7+
User says: "sync with upstream", "cherry-pick from X", "merge upstream", or similar.
8+
9+
## Workflow
10+
11+
### Phase 1: Explore divergence
12+
13+
```bash
14+
git fetch <remote>
15+
git log --oneline --right-only <branch>...<remote>/master --no-merges
16+
```
17+
18+
For each upstream-only commit, get files changed:
19+
20+
```bash
21+
git log --right-only <branch>...<remote>/master --no-merges --format="%h %s" | while read hash msg; do
22+
echo "=== $hash $msg ==="; git diff-tree --no-commit-id --name-only -r $hash; echo
23+
done
24+
```
25+
26+
### Phase 2: Triage commits
27+
28+
| Category | Action |
29+
|----------|--------|
30+
| Already in fork | SKIP |
31+
| Release/version bumps | SKIP |
32+
| Lock file only | SKIP |
33+
| Native WebRTC lib version changes | SKIP (fork uses StreamWebRTC) |
34+
| Merge commits | SKIP |
35+
| Cosmetic formatting | SKIP (run formatters separately) |
36+
| Bug fixes | CHERRY-PICK |
37+
| New features | CHERRY-PICK (ask user) |
38+
| Refactoring | CHERRY-PICK (evaluate risk) |
39+
| Docs/CI/tools | Ask user |
40+
41+
Check for equivalents: `git log --oneline --left-only <branch>...<remote>/master | grep -i "<keyword>"`
42+
43+
### Phase 3: Ask user
44+
45+
Present triage. Ask about large/risky features, optional items, anything ambiguous.
46+
47+
### Phase 4: Cherry-pick in order
48+
49+
```bash
50+
git checkout -b sync/upstream-cherry-picks <base-branch>
51+
```
52+
53+
Order: TS fixes → Android fixes → iOS fixes → small features → large features → docs.
54+
55+
If conflict: resolve, `git add`, `git cherry-pick --continue --no-edit`.
56+
If empty after resolution: `git cherry-pick --skip`.
57+
58+
### Phase 5: Merge to advance merge-base
59+
60+
Without this, future merges replay ALL upstream commits including skipped ones.
61+
62+
```bash
63+
git merge <remote>/master --no-commit
64+
65+
# Conflicted files — keep ours
66+
git diff --name-only --diff-filter=U | xargs git checkout --ours
67+
# Files deleted in our branch — remove
68+
git rm <deleted-files>
69+
# Auto-merged files — reset to our version
70+
git diff --cached --name-only --diff-filter=M | xargs git checkout HEAD --
71+
# Unwanted new files from upstream — remove
72+
git diff --cached --name-only --diff-filter=A # review, then:
73+
git rm -f <unwanted-files>
74+
75+
git add -A
76+
git diff --cached --stat HEAD # should be empty or near-empty
77+
git commit -m "merge: sync merge-base with <remote>/master"
78+
```
79+
80+
Verify: `git log --oneline --right-only <branch>...<remote>/master | wc -l` should be `0`.
81+
82+
### Phase 6: Verify
83+
84+
Run ALL of these. Do not skip any.
85+
86+
```bash
87+
npm run lint
88+
cd examples/GumTestApp/android && ./gradlew assembleDebug
89+
cd examples/GumTestApp/ios && pod install && \
90+
xcodebuild -workspace GumTestApp.xcworkspace -scheme GumTestApp \
91+
-sdk iphonesimulator -configuration Debug build
92+
```
93+
94+
### Phase 7: Format native files
95+
96+
```bash
97+
git ls-files | grep -e "\(\.java\|\.h\|\.m\)$" | grep -v examples | xargs npx clang-format -i
98+
```
99+
100+
Rebuild Android + iOS to confirm, then commit.
101+
102+
### Phase 8: Update package-lock.json
103+
104+
If `package.json` dependencies changed, lock file will be stale.
105+
106+
```bash
107+
npm install
108+
git add package-lock.json && git commit -m "chore: update package-lock.json"
109+
```
110+
111+
## Preservation rules
112+
113+
These MUST NOT change during sync:
114+
115+
| File | Guard |
116+
|------|-------|
117+
| `android/build.gradle` | Must keep `io.getstream:stream-video-webrtc-android:*` |
118+
| `stream-react-native-webrtc.podspec` | Must keep `StreamWebRTC` dependency |
119+
| `ios/RCTWebRTC/Utils/AudioDeviceModule/` | Fork's custom audio engine — untouched |
120+
| `SpeechActivityDetector.java` | Fork's custom VAD — untouched |
121+
| `AudioDeviceModule.ts`, `AudioDeviceModuleEvents.ts` | Fork's custom TS APIs — untouched |
122+
123+
Post-sync: `grep -r "org.webrtc:google-webrtc\|webrtc-ios" --include="*.gradle" --include="*.podspec" .` must return nothing.
124+
125+
## Pitfalls
126+
127+
1. **Always run native builds, not just tsc.** Cherry-picks can pass tsc but fail gradlew/xcodebuild.
128+
129+
2. **Native API names differ across WebRTC versions.** Enum values, type names, and method signatures may not exist in our WebRTC SDK. After cherry-picking from a fork on a different WebRTC version, verify types exist before building.
130+
131+
3. **`git add -A` re-adds files you removed.** Use `git rm -f` (not `--cached`) to remove from both index and disk.
132+
133+
4. **Auto-merged files need `git checkout HEAD --`, not `--ours`.** `--ours` only works on conflicted files. For auto-merged files with unwanted changes, use `git checkout HEAD -- <file>`.
134+
135+
5. **Watch for duplicates after conflict resolution.** Duplicate variable declarations, closing braces, or imports when keeping both sides of a conflict.
136+
137+
6. **Advance merge-base for EVERY upstream remote.** If syncing with multiple upstreams, merge each one separately. Otherwise the un-advanced remote replays all its history on the next merge.
138+
139+
7. **Upstream podspec/build files leak into cherry-picks.** Other forks have their own podspec (e.g., `livekit-react-native-webrtc.podspec`). Always `git rm` them when they appear.
140+
141+
8. **Cross-check cherry-picks against all upstreams for reverts.** Before cherry-picking a commit from one upstream, search the other upstreams for the same change — it may have been tried and reverted. Run: `git log --all --oneline -S "<key code snippet>"` to find if the same change exists elsewhere in history with a subsequent revert.
142+
143+
9. **Verify cherry-picked changes still exist on upstream HEAD.** A commit could have been added and later reverted/modified by a subsequent commit on the same upstream. After cherry-picking, verify the actual code still matches upstream's current state: `git show <remote>/master:<file> | grep "<key code>"`. Don't just trust that a commit was made — it may have been undone.
144+
145+
10. **Squash-merging the sync PR destroys the merge-base advancement.** The merge commits from Phase 5 (`git merge <remote>/master`) are the only thing that tells git "we've seen up to this point." Squash-merge flattens them into a single commit, losing that information. When merging the sync PR, use **"Create a merge commit"** (regular merge), not squash. If already squashed, create a follow-up PR with ghost merges: `git merge -s ours <remote>/master` for each upstream, then merge that PR with a regular merge commit.

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
examples
22
lib
3+
src/vendor
34
tools
45

56
metro.*.js

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ WebRTC.xcframework
1212
WebRTC.dSYMs
1313
examples/GumTestApp/package-lock.json
1414
examples/GumTestApp_macOS/package-lock.json
15+
**/.xcode.env.local
16+
**/PLAN.md
1517
*.jar
1618
*.tgz
1719
*.zip

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v22
1+
v24

Documentation/AndroidInstallation.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,55 @@ In `android/app/proguard-rules.pro` add the following on a new line.
7272
-keep class org.webrtc.** { *; }
7373
```
7474

75+
## Set audio category (output) to media
76+
77+
The audio is considered calls by default. If you don't want your audio to be treated as a call stream you need to change the category. To set the category:
78+
79+
if your Android files are written in Java, modify `MainApplication.java`:
80+
```java
81+
// add imports
82+
import com.oney.WebRTCModule.WebRTCModuleOptions;
83+
import android.media.AudioAttributes;
84+
import org.webrtc.audio.JavaAudioDeviceModule;
85+
86+
public class MainApplication extends Application implements ReactApplication {
87+
@Override
88+
public void onCreate() {
89+
// append this before WebRTCModule initializes
90+
WebRTCModuleOptions options = WebRTCModuleOptions.getInstance();
91+
AudioAttributes audioAttributes = AudioAttributes.Builder()
92+
.setUsage(AudioAttributes.USAGE_MEDIA)
93+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
94+
.build();
95+
options.audioDeviceModule = JavaAudioDeviceModule.builder(this)
96+
.setAudioAttributes(audioAttributes)
97+
.createAudioDeviceModule();
98+
}
99+
}
100+
```
101+
102+
if your Android files are written in Kotlin, modify `MainApplication.kt`:
103+
```kt
104+
// add imports
105+
import com.oney.WebRTCModule.WebRTCModuleOptions;
106+
import android.media.AudioAttributes
107+
import org.webrtc.audio.JavaAudioDeviceModule;
108+
109+
class MainApplication : Application(), ReactApplication {
110+
override fun onCreate() {
111+
// append this before WebRTCModule initializes
112+
val options = WebRTCModuleOptions.getInstance()
113+
val audioAttributes = AudioAttributes.Builder()
114+
.setUsage(AudioAttributes.USAGE_MEDIA)
115+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
116+
.build()
117+
options.audioDeviceModule = JavaAudioDeviceModule.builder(this)
118+
.setAudioAttributes(audioAttributes)
119+
.createAudioDeviceModule()
120+
}
121+
}
122+
```
123+
75124
## Fatal Exception: java.lang.UnsatisfiedLinkError
76125

77126
```

Documentation/BasicUsage.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,22 @@ try {
112112
};
113113
```
114114

115+
### Using Media Constraints on getDisplayMedia (Android Only)
116+
117+
It is possible to use mediaConstraints on getDisplayMedia to restricts the user to capturing the default display using the custom boolean parameter `createConfigForDefaultDisplay`.
118+
A resolution scale can also be applied using `resolutionScale` parameter. Value is a number between 0 and 1.
119+
120+
This configuration in only available for android, so will you have to add the 'android' key in constraints.
121+
122+
```javascript
123+
const displayMediaStreamConstraints = {
124+
android: {
125+
createConfigForDefaultDisplay: true,
126+
resolutionScale: 0.5,
127+
},
128+
};
129+
```
130+
115131
## Destroying the Media Stream
116132

117133
Cycling all of the tracks and stopping them is more than enough to clean up after a call has finished.
@@ -232,11 +248,9 @@ That will allow you to enable and disable video streams on demand while a call i
232248

233249
```javascript
234250
let sessionConstraints = {
235-
mandatory: {
236-
OfferToReceiveAudio: true,
237-
OfferToReceiveVideo: true,
238-
VoiceActivityDetection: true
239-
}
251+
offerToReceiveAudio: true,
252+
offerToReceiveVideo: true,
253+
voiceActivityDetection: true
240254
};
241255
```
242256

Documentation/CallGuide.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,9 @@ So you can now start creating an offer which then needs sending send off to the
143143

144144
```javascript
145145
let sessionConstraints = {
146-
mandatory: {
147-
OfferToReceiveAudio: true,
148-
OfferToReceiveVideo: true,
149-
VoiceActivityDetection: true
150-
}
146+
offerToReceiveAudio: true,
147+
offerToReceiveVideo: true,
148+
voiceActivityDetection: true
151149
};
152150

153151
try {

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ Don't worry, there are plans to include a much more broader example with backend
7575
Come join our [Discourse Community](https://react-native-webrtc.discourse.group/) if you want to discuss any React Native and WebRTC related topics.
7676
Everyone is welcome and every little helps.
7777

78+
## Picture-in-Picture (PIP)
79+
80+
This package does not include a built-in PIP implementation. PIP support is available via [`@stream-io/video-react-native-sdk`](https://github.com/GetStream/stream-video-js).
81+
7882
## Related Projects
7983

8084
Looking for extra functionality coverage?

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ public boolean stopCapture() {
9797

9898
public void applyConstraints(ReadableMap constraints, @Nullable Consumer<Exception> onFinishedCallback) {
9999
if (onFinishedCallback != null) {
100-
onFinishedCallback.accept(new UnsupportedOperationException("This video track does not support applyConstraints."));
100+
onFinishedCallback.accept(
101+
new UnsupportedOperationException("This video track does not support applyConstraints."));
101102
}
102103
}
103104

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.oney.WebRTCModule;
2+
3+
import android.util.Log;
4+
5+
import com.facebook.react.bridge.Arguments;
6+
import com.facebook.react.bridge.WritableMap;
7+
8+
import org.webrtc.AudioTrack;
9+
import org.webrtc.AudioTrackSink;
10+
11+
import java.nio.ByteBuffer;
12+
import java.util.HashMap;
13+
import java.util.Map;
14+
import java.util.concurrent.atomic.AtomicBoolean;
15+
16+
/**
17+
* Fires the W3C 'unmute' event on a remote audio track when the first
18+
* decoded PCM buffer arrives via {@link AudioTrackSink}.
19+
*
20+
* IMPORTANT — only the initial muted → unmuted transition is detectable.
21+
* Subsequent mute events (e.g. network stall mid-call) cannot be detected
22+
* from the sink: Android's audio render path and WebRTC's NetEq synthesize
23+
* silence / PLC frames whenever RTP stops, so {@code onData} keeps firing
24+
* at a steady rate regardless of network state. For "remote participant
25+
* muted their mic" UI, use the out-of-band participant state from your
26+
* signaling layer — that is the correct source of truth, not this adapter.
27+
*
28+
* Only attach to remote audio tracks. {@code AudioTrackSink} callbacks
29+
* are not delivered for local tracks.
30+
*/
31+
public class AudioTrackAdapter {
32+
static final String TAG = AudioTrackAdapter.class.getCanonicalName();
33+
34+
private final Map<String, FirstDataUnmuteSink> sinks = new HashMap<>();
35+
private final int peerConnectionId;
36+
private final WebRTCModule webRTCModule;
37+
38+
public AudioTrackAdapter(WebRTCModule webRTCModule, int peerConnectionId) {
39+
this.peerConnectionId = peerConnectionId;
40+
this.webRTCModule = webRTCModule;
41+
}
42+
43+
public void addAdapter(AudioTrack audioTrack) {
44+
String trackId = audioTrack.id();
45+
if (sinks.containsKey(trackId)) {
46+
Log.w(TAG, "Attempted to add adapter twice for track ID: " + trackId);
47+
return;
48+
}
49+
FirstDataUnmuteSink sink = new FirstDataUnmuteSink(trackId);
50+
sinks.put(trackId, sink);
51+
audioTrack.addSink(sink);
52+
Log.d(TAG, "Created adapter for " + trackId);
53+
}
54+
55+
public void removeAdapter(AudioTrack audioTrack) {
56+
String trackId = audioTrack.id();
57+
FirstDataUnmuteSink sink = sinks.remove(trackId);
58+
if (sink == null) {
59+
Log.w(TAG, "removeAdapter - no adapter for " + trackId);
60+
return;
61+
}
62+
audioTrack.removeSink(sink);
63+
Log.d(TAG, "Deleted adapter for " + trackId);
64+
}
65+
66+
private class FirstDataUnmuteSink implements AudioTrackSink {
67+
private final AtomicBoolean fired = new AtomicBoolean(false);
68+
private final String trackId;
69+
70+
FirstDataUnmuteSink(String trackId) {
71+
this.trackId = trackId;
72+
}
73+
74+
@Override
75+
public void onData(ByteBuffer audioData,
76+
int bitsPerSample,
77+
int sampleRate,
78+
int numberOfChannels,
79+
int numberOfFrames,
80+
long absoluteCaptureTimestampMs) {
81+
if (!fired.compareAndSet(false, true)) {
82+
return;
83+
}
84+
WritableMap params = Arguments.createMap();
85+
params.putInt("pcId", peerConnectionId);
86+
params.putString("trackId", trackId);
87+
params.putBoolean("muted", false);
88+
Log.d(TAG, "Unmute event pcId: " + peerConnectionId + " trackId: " + trackId);
89+
webRTCModule.sendEvent("mediaStreamTrackMuteChanged", params);
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)