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"