diff --git a/.gitignore b/.gitignore
index d5a1b496f..ba4d9bdf9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,12 @@ examples/GumTestApp_macOS/package-lock.json
*.zip
lib/
src/*.js
-
+# Android/IntelliJ
+#
+build/
+.idea
+.gradle
+local.properties
+*.iml
+*.hprof
+.cxx/
diff --git a/android/build.gradle b/android/build.gradle
index 83e3afde4..a113bdb28 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,15 +1,48 @@
import java.nio.file.Paths
-apply plugin: 'com.android.library'
+buildscript {
+ ext.getExtOrDefault = {name, fallback ->
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback
+ }
+
+ repositories {
+ google()
+ mavenCentral()
+ maven { url 'https://central.sonatype.com/repository/maven-snapshots/' }
+ }
+
+ dependencies {
+ classpath("com.android.tools.build:gradle:7.3.1")
+ // noinspection DifferentKotlinGradleVersion
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion', '1.8.10')}"
+ }
+}
+
+apply plugin: "com.android.library"
+apply plugin: "kotlin-android"
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
+def supportsNamespace() {
+ def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
+ def major = parsed[0].toInteger()
+ def minor = parsed[1].toInteger()
+
+ // Namespace support was added in 7.3.0
+ return (major == 7 && minor >= 3) || major >= 8
+}
+
android {
- def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
- if (agpVersion.tokenize('.')[0].toInteger() >= 7) {
- namespace "com.oney.WebRTCModule"
+ if (supportsNamespace()) {
+ namespace "com.oney.WebRTCModule"
+
+ sourceSets {
+ main {
+ manifest.srcFile "src/main/AndroidManifestNew.xml"
+ }
+ }
}
compileSdkVersion safeExtGet('compileSdkVersion', 24)
@@ -31,7 +64,8 @@ android {
}
dependencies {
- api 'io.getstream:stream-webrtc-android:1.3.8'
+ api 'io.getstream:stream-webrtc-android:1.3.9'
implementation 'com.facebook.react:react-native:+'
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:${getExtOrDefault('kotlinVersion', '1.8.10')}"
implementation "androidx.core:core:1.7.0"
}
diff --git a/android/src/main/AndroidManifestNew.xml b/android/src/main/AndroidManifestNew.xml
new file mode 100644
index 000000000..0d246d51b
--- /dev/null
+++ b/android/src/main/AndroidManifestNew.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/android/src/main/java/com/oney/WebRTCModule/EglUtils.java b/android/src/main/java/com/oney/WebRTCModule/EglUtils.java
index f315b36b5..1310a68c9 100644
--- a/android/src/main/java/com/oney/WebRTCModule/EglUtils.java
+++ b/android/src/main/java/com/oney/WebRTCModule/EglUtils.java
@@ -1,16 +1,11 @@
package com.oney.WebRTCModule;
-
-import android.os.Build.VERSION;
-import android.util.Log;
-
import org.webrtc.EglBase;
public class EglUtils {
/**
* The root {@link EglBase} instance shared by the entire application for
* the sake of reducing the utilization of system resources (such as EGL
- * contexts). It selects between {@link EglBase10} and {@link EglBase14}
- * by performing a runtime check.
+ * contexts).
*/
private static EglBase rootEglBase;
@@ -20,47 +15,13 @@ public class EglUtils {
*/
public static synchronized EglBase getRootEglBase() {
if (rootEglBase == null) {
- // XXX EglBase14 will report that isEGL14Supported() but its
- // getEglConfig() will fail with a RuntimeException with message
- // "Unable to find any matching EGL config". Fall back to EglBase10
- // in the described scenario.
- EglBase eglBase = null;
- int[] configAttributes = EglBase.CONFIG_PLAIN;
- RuntimeException cause = null;
-
- try {
- // WebRTC internally does this check in isEGL14Supported, but it's no longer exposed
- // in the public API
- if (VERSION.SDK_INT >= 18) {
- eglBase = EglBase.createEgl14(configAttributes);
- }
- } catch (RuntimeException ex) {
- // Fall back to EglBase10.
- cause = ex;
- }
-
- if (eglBase == null) {
- try {
- eglBase = EglBase.createEgl10(configAttributes);
- } catch (RuntimeException ex) {
- // Neither EglBase14, nor EglBase10 succeeded to initialize.
- cause = ex;
- }
- }
-
- if (cause != null) {
- Log.e(EglUtils.class.getName(), "Failed to create EglBase", cause);
- } else {
- rootEglBase = eglBase;
- }
+ rootEglBase = EglBase.create();
}
-
return rootEglBase;
}
public static EglBase.Context getRootEglBaseContext() {
EglBase eglBase = getRootEglBase();
-
return eglBase == null ? null : eglBase.getEglBaseContext();
}
}
diff --git a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java
index 23528fe12..012cd7929 100644
--- a/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java
+++ b/android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java
@@ -23,14 +23,14 @@
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.oney.WebRTCModule.audio.AudioProcessingFactoryProvider;
import com.oney.WebRTCModule.audio.AudioProcessingController;
-import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoDecoderFactory;
-import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoEncoderFactory;
+import com.oney.WebRTCModule.webrtcutils.SelectiveVideoDecoderFactory;
import org.webrtc.*;
import org.webrtc.audio.AudioDeviceModule;
import org.webrtc.audio.JavaAudioDeviceModule;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -83,14 +83,9 @@ public WebRTCModule(ReactApplicationContext reactContext) {
if (encoderFactory == null || decoderFactory == null) {
// Initialize EGL context required for HW acceleration.
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
+ encoderFactory = new SimulcastAlignedVideoEncoderFactory(eglContext, true, true, ResolutionAdjustment.MULTIPLE_OF_16);
+ decoderFactory = new SelectiveVideoDecoderFactory(eglContext, false, Arrays.asList("VP9", "AV1"));
- if (eglContext != null) {
- encoderFactory = new H264AndSoftwareVideoEncoderFactory(eglContext);
- decoderFactory = new H264AndSoftwareVideoDecoderFactory(eglContext);
- } else {
- encoderFactory = new SoftwareVideoEncoderFactory();
- decoderFactory = new SoftwareVideoDecoderFactory();
- }
}
if (adm == null) {
@@ -134,9 +129,11 @@ private PeerConnection getPeerConnection(int id) {
}
void sendEvent(String eventName, @Nullable ReadableMap params) {
- getReactApplicationContext()
- .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
- .emit(eventName, params);
+ if (getReactApplicationContext().hasActiveReactInstance()) {
+ getReactApplicationContext()
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
+ .emit(eventName, params);
+ }
}
private PeerConnection.IceServer createIceServer(String url) {
diff --git a/android/src/main/java/com/oney/WebRTCModule/webrtcutils/SelectiveVideoDecoderFactory.kt b/android/src/main/java/com/oney/WebRTCModule/webrtcutils/SelectiveVideoDecoderFactory.kt
new file mode 100644
index 000000000..441bfc4dc
--- /dev/null
+++ b/android/src/main/java/com/oney/WebRTCModule/webrtcutils/SelectiveVideoDecoderFactory.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
+ *
+ * Licensed under the Stream License;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://github.com/GetStream/stream-video-android/blob/main/LICENSE
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.oney.WebRTCModule.webrtcutils
+
+import org.webrtc.EglBase
+import org.webrtc.SoftwareVideoDecoderFactory
+import org.webrtc.VideoCodecInfo
+import org.webrtc.VideoDecoder
+import org.webrtc.VideoDecoderFactory
+
+internal class SelectiveVideoDecoderFactory(
+ sharedContext: EglBase.Context?,
+ private var forceSWCodec: Boolean = false,
+ private var forceSWCodecs: List = listOf("VP9", "AV1"),
+) : VideoDecoderFactory {
+ private val softwareVideoDecoderFactory = SoftwareVideoDecoderFactory()
+ private val wrappedVideoDecoderFactory = WrappedVideoDecoderFactory(sharedContext, forceSWCodec)
+
+ /**
+ * Set to true to force software codecs.
+ */
+ fun setForceSWCodec(forceSWCodec: Boolean) {
+ this.forceSWCodec = forceSWCodec
+ }
+
+ /**
+ * Set a list of codecs for which to use software codecs.
+ */
+ fun setForceSWCodecList(forceSWCodecs: List) {
+ this.forceSWCodecs = forceSWCodecs
+ }
+
+ override fun createDecoder(videoCodecInfo: VideoCodecInfo): VideoDecoder? {
+ if (forceSWCodecs.isNotEmpty()) {
+ if (forceSWCodecs.contains(videoCodecInfo.name)) {
+ return softwareVideoDecoderFactory.createDecoder(videoCodecInfo)
+ }
+ }
+ if (forceSWCodec) {
+ return wrappedVideoDecoderFactory.createDecoder(videoCodecInfo)
+ }
+ return wrappedVideoDecoderFactory.createDecoder(videoCodecInfo)
+ }
+
+ override fun getSupportedCodecs(): Array {
+ return wrappedVideoDecoderFactory.supportedCodecs
+ }
+}
diff --git a/android/src/main/java/com/oney/WebRTCModule/webrtcutils/SelectiveVideoEncoderFactory.kt b/android/src/main/java/com/oney/WebRTCModule/webrtcutils/SelectiveVideoEncoderFactory.kt
new file mode 100644
index 000000000..fa2be1ba1
--- /dev/null
+++ b/android/src/main/java/com/oney/WebRTCModule/webrtcutils/SelectiveVideoEncoderFactory.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
+ *
+ * Licensed under the Stream License;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://github.com/GetStream/stream-video-android/blob/main/LICENSE
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.oney.WebRTCModule.webrtcutils
+
+import org.webrtc.EglBase
+import org.webrtc.ResolutionAdjustment
+import org.webrtc.SimulcastAlignedVideoEncoderFactory
+import org.webrtc.SoftwareVideoEncoderFactory
+import org.webrtc.VideoCodecInfo
+import org.webrtc.VideoEncoder
+import org.webrtc.VideoEncoderFactory
+
+internal class SelectiveVideoEncoderFactory(
+ sharedContext: EglBase.Context?,
+ enableIntelVp8Encoder: Boolean,
+ enableH264HighProfile: Boolean,
+ private var forceSWCodec: Boolean = false,
+ private var forceSWCodecs: List = listOf("VP9", "AV1"),
+) : VideoEncoderFactory {
+ private val softwareVideoEncoderFactory = SoftwareVideoEncoderFactory()
+ private val simulcastVideoEncoderFactoryWrapper: SimulcastAlignedVideoEncoderFactory
+
+ init {
+ simulcastVideoEncoderFactoryWrapper =
+ SimulcastAlignedVideoEncoderFactory(sharedContext, enableIntelVp8Encoder, enableH264HighProfile, ResolutionAdjustment.NONE)
+ }
+
+ /**
+ * Set to true to force software codecs.
+ */
+ fun setForceSWCodec(forceSWCodec: Boolean) {
+ this.forceSWCodec = forceSWCodec
+ }
+
+ /**
+ * Set a list of codecs for which to use software codecs.
+ */
+ fun setForceSWCodecList(forceSWCodecs: List) {
+ this.forceSWCodecs = forceSWCodecs
+ }
+
+ override fun createEncoder(videoCodecInfo: VideoCodecInfo): VideoEncoder? {
+ if (forceSWCodec) {
+ return softwareVideoEncoderFactory.createEncoder(videoCodecInfo)
+ }
+ if (forceSWCodecs.isNotEmpty()) {
+ if (forceSWCodecs.contains(videoCodecInfo.name)) {
+ return softwareVideoEncoderFactory.createEncoder(videoCodecInfo)
+ }
+ }
+ return simulcastVideoEncoderFactoryWrapper.createEncoder(videoCodecInfo)
+ }
+
+ override fun getSupportedCodecs(): Array {
+ return if (forceSWCodec && forceSWCodecs.isEmpty()) {
+ softwareVideoEncoderFactory.supportedCodecs
+ } else {
+ simulcastVideoEncoderFactoryWrapper.supportedCodecs
+ }
+ }
+}
diff --git a/android/src/main/java/com/oney/WebRTCModule/webrtcutils/WrappedVideoDecoderFactory.java b/android/src/main/java/com/oney/WebRTCModule/webrtcutils/WrappedVideoDecoderFactory.java
new file mode 100644
index 000000000..c94910d5a
--- /dev/null
+++ b/android/src/main/java/com/oney/WebRTCModule/webrtcutils/WrappedVideoDecoderFactory.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
+ *
+ * Licensed under the Stream License;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://github.com/GetStream/stream-video-android/blob/main/LICENSE
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.oney.WebRTCModule.webrtcutils;
+
+import androidx.annotation.Nullable;
+
+import org.webrtc.*;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+
+/**
+ * A patch on top of https://github.com/GetStream/webrtc/blob/main/sdk/android/api/org/webrtc/WrappedVideoDecoderFactory.java
+ * It disables direct-to-SurfaceTextureFrame rendering for c2 exynos/qualcomm/mediatek hardware decoder
+ */
+public class WrappedVideoDecoderFactory implements VideoDecoderFactory {
+ // Known hardware decoders to have failures when it outputs to a SurfaceTexture directly
+ private static final String[] DECODER_DENYLIST_PREFIXES = {
+ "OMX.qcom.",
+ "OMX.hisi.",
+ // https://github.com/androidx/media/issues/2003
+// "c2.exynos.",
+// "c2.qti.",
+// // https://github.com/androidx/media/blob/bfe5930f7f29c6492d60e3d01a90abd3c138b615/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java#L1499
+// "c2.mtk.",
+ };
+
+ private final boolean forceSWCodec;
+
+ public WrappedVideoDecoderFactory(@Nullable EglBase.Context eglContext, boolean forceSWCodec) {
+ this.hardwareVideoDecoderFactory = new HardwareVideoDecoderFactory(eglContext);
+ this.platformSoftwareVideoDecoderFactory = new PlatformSoftwareVideoDecoderFactory(eglContext);
+ this.forceSWCodec = forceSWCodec;
+ }
+
+ private final VideoDecoderFactory hardwareVideoDecoderFactory;
+ private final VideoDecoderFactory hardwareVideoDecoderFactoryWithoutEglContext = new HardwareVideoDecoderFactory(null) ;
+ private final VideoDecoderFactory softwareVideoDecoderFactory = new SoftwareVideoDecoderFactory();
+ @Nullable
+ private final VideoDecoderFactory platformSoftwareVideoDecoderFactory;
+
+ @Override
+ public VideoDecoder createDecoder(VideoCodecInfo codecType) {
+ VideoDecoder softwareDecoder = this.softwareVideoDecoderFactory.createDecoder(codecType);
+ VideoDecoder hardwareDecoder = null;
+ if (!forceSWCodec) {
+ hardwareDecoder = this.hardwareVideoDecoderFactory.createDecoder(codecType);
+ }
+ if (softwareDecoder == null && this.platformSoftwareVideoDecoderFactory != null) {
+ softwareDecoder = this.platformSoftwareVideoDecoderFactory.createDecoder(codecType);
+ }
+ if(hardwareDecoder != null && disableSurfaceTextureFrame(hardwareDecoder.getImplementationName())) {
+ hardwareDecoder.release();
+ hardwareDecoder = this.hardwareVideoDecoderFactoryWithoutEglContext.createDecoder(codecType);
+ }
+
+ if (hardwareDecoder != null && softwareDecoder != null) {
+ return new VideoDecoderFallback(softwareDecoder, hardwareDecoder);
+ } else {
+ return hardwareDecoder != null ? hardwareDecoder : softwareDecoder;
+ }
+ }
+
+ private boolean disableSurfaceTextureFrame(String name) {
+ for (String prefix : DECODER_DENYLIST_PREFIXES) {
+ if (name.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public VideoCodecInfo[] getSupportedCodecs() {
+ LinkedHashSet supportedCodecInfos = new LinkedHashSet<>();
+ supportedCodecInfos.addAll(Arrays.asList(this.softwareVideoDecoderFactory.getSupportedCodecs()));
+ if (!forceSWCodec) {
+ supportedCodecInfos.addAll(Arrays.asList(this.hardwareVideoDecoderFactory.getSupportedCodecs()));
+ }
+ if (this.platformSoftwareVideoDecoderFactory != null) {
+ supportedCodecInfos.addAll(Arrays.asList(this.platformSoftwareVideoDecoderFactory.getSupportedCodecs()));
+ }
+
+ return supportedCodecInfos.toArray(new VideoCodecInfo[supportedCodecInfos.size()]);
+ }
+}
diff --git a/examples/GumTestApp/android/app/src/main/AndroidManifest.xml b/examples/GumTestApp/android/app/src/main/AndroidManifest.xml
index 3b3580cc6..55f770abe 100644
--- a/examples/GumTestApp/android/app/src/main/AndroidManifest.xml
+++ b/examples/GumTestApp/android/app/src/main/AndroidManifest.xml
@@ -1,5 +1,11 @@
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 157fb6cb0..26cb9b478 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@stream-io/react-native-webrtc",
- "version": "125.4.2",
+ "version": "125.4.3-rc.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@stream-io/react-native-webrtc",
- "version": "125.4.2",
+ "version": "125.4.3-rc.1",
"license": "MIT",
"dependencies": {
"base64-js": "1.5.1",
diff --git a/package.json b/package.json
index 3c925ff28..7c7164a8a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@stream-io/react-native-webrtc",
- "version": "125.4.2",
+ "version": "125.4.3-rc.1",
"repository": {
"type": "git",
"url": "git+https://github.com/GetStream/react-native-webrtc.git"