diff --git a/.gitignore b/.gitignore index 15bfdccc4f..9878bbc092 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,5 @@ macos/Frameworks # SPM .build/ -.swiftpm/ \ No newline at end of file +.swiftpm/ +example/android/build diff --git a/CHANGELOG.md b/CHANGELOG.md index bf898edd20..fa46be11ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,17 @@ # Changelog -[3.0.1] - upcoming - -**New** +[3.0.1] - 2026.07.10 * [iOS] Added an onAudioRouteChangeEvent that fires whenever audio output route changes. +* Synced flutter-webrtc v1.5.2 + * [iOS] fix: audio session output port override for speaker toggle (#1941) + * [Android] fix: tolerate Qualcomm/Hisi encoders in VideoFileRenderer and guard muxer writes against invalid sample buffers. (#2031 and #2030) + * [Android] feat: add fullScreenOnly option to requestCapturePermission to force entire-screen capture on API 34+ (#2079) + * [Android] fix: recreate the texture surface when the incoming frame size changes, so simulcast layer upgrades no longer stay blurry. (#2085) + * [Android] Support AGP 9's built-in Kotlin: apply the Kotlin Gradle Plugin only when built-in Kotlin is inactive (AGP < 9, or AGP 9 with `android.builtInKotlin=false`), set the JVM target through the `kotlin { compilerOptions {} }` DSL when available, and fall back to the legacy `kotlinOptions` DSL for apps still on Kotlin Gradle Plugin 1.8.x. (#2075) + * [Windows/Linux] chore: drive prebuilt libwebrtc download from third_party/libwebrtc_version.ini. Bump WebRTC version to 144.7559.09. (#2061 and #2078) + * [Windows/Linux] fix: map echoCancellation/noiseSuppression/autoGainControl constraints to RTCAudioOptions (#2068) [3.0.0] - 2026-05-14 diff --git a/android/build.gradle b/android/build.gradle index f711ec3636..5bc9d8eb03 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ group 'io.getstream.webrtc.flutter' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.9.10' + ext.kotlin_version = '2.2.20' repositories { google() mavenCentral() @@ -10,7 +10,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.1.1' + classpath 'com.android.tools.build:gradle:8.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,7 +25,18 @@ rootProject.allprojects { } apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' + +// AGP 9's built-in Kotlin compiles Kotlin itself and rejects the Kotlin Gradle +// Plugin. Apply KGP only when built-in Kotlin is NOT active: that means AGP < 9, +// or AGP 9 with android.builtInKotlin=false (the configuration Flutter currently +// ships by default while the ecosystem migrates). +def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0] as int +def builtInKotlinActive = agpMajor >= 9 && + (!project.hasProperty('android.builtInKotlin') || + Boolean.parseBoolean(project.property('android.builtInKotlin').toString())) +if (!builtInKotlinActive) { + apply plugin: 'kotlin-android' +} android { if (project.android.hasProperty("namespace")) { @@ -47,9 +58,21 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } +} - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 +// Configure the Kotlin JVM target. The compilerOptions DSL requires KGP 1.9+ or +// AGP 9 built-in Kotlin; older Flutter app templates ship KGP 1.8.x, which only +// supports the legacy kotlinOptions DSL. +def kotlinExt = project.extensions.findByName('kotlin') +if (kotlinExt?.hasProperty('compilerOptions')) { + kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + } + } +} else { + android.kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() } } @@ -57,5 +80,4 @@ dependencies { implementation("io.getstream:stream-video-webrtc-android:145.9.0") implementation 'com.github.davidliu:audioswitch:89582c47c9a04c62f90aa5e57251af4800a62c9a' implementation 'androidx.annotation:annotation:1.1.0' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/android/src/main/java/io/getstream/webrtc/flutter/GetUserMediaImpl.java b/android/src/main/java/io/getstream/webrtc/flutter/GetUserMediaImpl.java index b20586a068..ae52d0b312 100755 --- a/android/src/main/java/io/getstream/webrtc/flutter/GetUserMediaImpl.java +++ b/android/src/main/java/io/getstream/webrtc/flutter/GetUserMediaImpl.java @@ -13,6 +13,7 @@ import android.hardware.camera2.CameraManager; import android.media.AudioDeviceInfo; import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionConfig; import android.media.projection.MediaProjectionManager; import android.net.Uri; import android.os.Build; @@ -110,6 +111,7 @@ public class GetUserMediaImpl { private static final String PROJECTION_DATA = "PROJECTION_DATA"; private static final String RESULT_RECEIVER = "RESULT_RECEIVER"; private static final String REQUEST_CODE = "REQUEST_CODE"; + private static final String FULL_SCREEN_ONLY = "FULL_SCREEN_ONLY"; static final String TAG = FlutterWebRTCPlugin.TAG; @@ -138,6 +140,10 @@ public class GetUserMediaImpl { private int audioChannelCount = 1; public void screenRequestPermissions(ResultReceiver resultReceiver) { + screenRequestPermissions(resultReceiver, false); + } + + public void screenRequestPermissions(ResultReceiver resultReceiver, boolean fullScreenOnly) { mediaProjectionData = null; final Activity activity = stateProvider.getActivity(); if (activity == null) { @@ -148,6 +154,7 @@ public void screenRequestPermissions(ResultReceiver resultReceiver) { Bundle args = new Bundle(); args.putParcelable(RESULT_RECEIVER, resultReceiver); args.putInt(REQUEST_CODE, CAPTURE_PERMISSION_REQUEST_CODE); + args.putBoolean(FULL_SCREEN_ONLY, fullScreenOnly); ScreenRequestPermissionsFragment fragment = new ScreenRequestPermissionsFragment(); fragment.setArguments(args); @@ -166,6 +173,10 @@ public void screenRequestPermissions(ResultReceiver resultReceiver) { } public void requestCapturePermission(final Result result) { + requestCapturePermission(result, false); + } + + public void requestCapturePermission(final Result result, final boolean fullScreenOnly) { screenRequestPermissions( new ResultReceiver(new Handler(Looper.getMainLooper())) { @Override @@ -178,7 +189,8 @@ protected void onReceiveResult(int requestCode, Bundle resultData) { result.success(false); } } - }); + }, + fullScreenOnly); } public static class ScreenRequestPermissionsFragment extends Fragment { @@ -207,6 +219,7 @@ private void checkSelfPermissions(boolean requestPermissions) { resultReceiver = args.getParcelable(RESULT_RECEIVER); requestCode = args.getInt(REQUEST_CODE); + boolean fullScreenOnly = args.getBoolean(FULL_SCREEN_ONLY, false); hasRequestedPermission = true; @@ -216,13 +229,13 @@ private void checkSelfPermissions(boolean requestPermissions) { new Handler(Looper.getMainLooper()).postDelayed(() -> { Activity currentActivity = getActivity(); if (currentActivity != null && !currentActivity.isFinishing() && isAdded()) { - requestStart(currentActivity, requestCode); + requestStart(currentActivity, requestCode, fullScreenOnly); } }, 100); } } - public void requestStart(Activity activity, int requestCode) { + public void requestStart(Activity activity, int requestCode, boolean fullScreenOnly) { if (android.os.Build.VERSION.SDK_INT < minAPILevel) { Log.w( TAG, @@ -231,9 +244,21 @@ public void requestStart(Activity activity, int requestCode) { MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE); + // On Android 14+ (API 34), opt in to capturing the entire display so the + // consent dialog no longer offers the single-app option. + Intent captureIntent; + if (fullScreenOnly + && android.os.Build.VERSION.SDK_INT + >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + captureIntent = + mediaProjectionManager.createScreenCaptureIntent( + MediaProjectionConfig.createConfigForDefaultDisplay()); + } else { + captureIntent = mediaProjectionManager.createScreenCaptureIntent(); + } + // call for the projection manager - this.startActivityForResult( - mediaProjectionManager.createScreenCaptureIntent(), requestCode); + this.startActivityForResult(captureIntent, requestCode); } } diff --git a/android/src/main/java/io/getstream/webrtc/flutter/MethodCallHandlerImpl.java b/android/src/main/java/io/getstream/webrtc/flutter/MethodCallHandlerImpl.java index 144274a45b..e54fdd9c1b 100644 --- a/android/src/main/java/io/getstream/webrtc/flutter/MethodCallHandlerImpl.java +++ b/android/src/main/java/io/getstream/webrtc/flutter/MethodCallHandlerImpl.java @@ -1206,7 +1206,9 @@ public void onInterruptionEnd() { "unknown factoryId " + factoryId, result); break; } - nf.getUserMediaImpl.requestCapturePermission(result); + Boolean fullScreenOnlyArg = call.argument("fullScreenOnly"); + boolean fullScreenOnly = fullScreenOnlyArg != null && fullScreenOnlyArg; + nf.getUserMediaImpl.requestCapturePermission(result, fullScreenOnly); break; } case "getDisplayMedia": { diff --git a/android/src/main/java/io/getstream/webrtc/flutter/SurfaceTextureRenderer.java b/android/src/main/java/io/getstream/webrtc/flutter/SurfaceTextureRenderer.java index 14ee7e5fc0..bbcec4dac5 100755 --- a/android/src/main/java/io/getstream/webrtc/flutter/SurfaceTextureRenderer.java +++ b/android/src/main/java/io/getstream/webrtc/flutter/SurfaceTextureRenderer.java @@ -98,15 +98,46 @@ public void pauseVideo() { // VideoSink interface. @Override public void onFrame(VideoFrame frame) { - if(surface == null) { - producer.setSize(frame.getRotatedWidth(),frame.getRotatedHeight()); - surface = producer.getSurface(); - createEglSurface(surface); + synchronized (surfaceLock) { + if(surface == null) { + producer.setSize(frame.getRotatedWidth(),frame.getRotatedHeight()); + surface = producer.getSurface(); + createEglSurface(surface); + } else if (frameSizeChanged(frame)) { + // The producer's backing buffers are fixed-size: setSize() only takes + // effect for a Surface obtained afterwards. Without recreating the EGL + // surface here, a simulcast layer upgrade keeps rendering into the old + // low-resolution buffer and the video stays blurry. + releaseEglSurface(() -> {}); + // Clear the field before re-obtaining: if getSurface() throws, the + // next frame takes the surface == null path and recreates cleanly + // rather than rendering into the already-released surface. + surface = null; + producer.setSize(frame.getRotatedWidth(), frame.getRotatedHeight()); + surface = producer.getSurface(); + createEglSurface(surface); + } } updateFrameDimensionsAndReportEvents(frame); super.onFrame(frame); } + private boolean frameSizeChanged(VideoFrame frame) { + synchronized (layoutLock) { + return !isRenderingPaused + && (rotatedFrameWidth != frame.getRotatedWidth() + || rotatedFrameHeight != frame.getRotatedHeight()); + } + } + + // Guards surface lifecycle transitions: creation/recreation happens on the + // frame delivery thread while destruction arrives on the main thread via + // the producer's onSurfaceCleanup callback. Serializing the two prevents a + // frame from re-creating the EGL surface against a Surface the producer is + // concurrently invalidating. surfaceDestroyed() blocks on the EGL release + // while holding this lock; the latch is signaled by the EglRenderer render + // thread, which never acquires it, so the wait cannot deadlock. + private final Object surfaceLock = new Object(); private Surface surface = null; private TextureRegistry.SurfaceProducer producer; @@ -130,10 +161,12 @@ public void onSurfaceCleanup() { public void onSurfaceCleanup() { ThreadUtils.checkIsOnMainThread(); - final CountDownLatch completionLatch = new CountDownLatch(1); - releaseEglSurface(completionLatch::countDown); - ThreadUtils.awaitUninterruptibly(completionLatch); - surface = null; + synchronized (surfaceLock) { + final CountDownLatch completionLatch = new CountDownLatch(1); + releaseEglSurface(completionLatch::countDown); + ThreadUtils.awaitUninterruptibly(completionLatch); + surface = null; + } } // Update frame dimensions and report any changes to |rendererEvents|. @@ -157,7 +190,6 @@ private void updateFrameDimensionsAndReportEvents(VideoFrame frame) { } rotatedFrameWidth = frame.getRotatedWidth(); rotatedFrameHeight = frame.getRotatedHeight(); - producer.setSize(rotatedFrameWidth, rotatedFrameHeight); frameRotation = frame.getRotation(); } } diff --git a/android/src/main/java/io/getstream/webrtc/flutter/record/VideoFileRenderer.java b/android/src/main/java/io/getstream/webrtc/flutter/record/VideoFileRenderer.java index 403e56115a..8ebbab7e35 100644 --- a/android/src/main/java/io/getstream/webrtc/flutter/record/VideoFileRenderer.java +++ b/android/src/main/java/io/getstream/webrtc/flutter/record/VideoFileRenderer.java @@ -58,6 +58,7 @@ class VideoFileRenderer implements VideoSink, SamplesReadyCallback { private GlRectDrawer drawer; private Surface surface; private MediaCodec audioEncoder; + private boolean encoderInitFailed = false; VideoFileRenderer(String outputFile, final EglBase.Context sharedContext, boolean withAudio) throws IOException { renderThread = new HandlerThread(TAG + "RenderThread"); @@ -89,20 +90,16 @@ private boolean tryConfigureEncoder(EncoderConfig config) { format.setInteger(MediaFormat.KEY_BIT_RATE, config.bitrate); format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); - // Use YUV420 semi-planar size (1.5 bytes per pixel) to reduce memory usage - format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, config.width * config.height * 3 / 2); - format.setInteger(MediaFormat.KEY_PRIORITY, 0); Log.d(TAG, "Trying encoder config: " + config); encoder = MediaCodec.createEncoderByType(MIME_TYPE); String codecName = encoder.getName(); Log.d(TAG, "Codec name: " + codecName); - if ("OMX.hisi.video.encoder.avc".equals(codecName)) { - Log.w(TAG, "hisi h264 encoder does not set 'MediaFormat.KEY_PROFILE'."); - //format.setInteger(MediaFormat.KEY_PROFILE, config.profile); - }else{ + if (shouldForceCodecProfile(codecName)) { format.setInteger(MediaFormat.KEY_PROFILE, config.profile); + } else { + Log.w(TAG, "Skip explicit H264 profile for codec: " + codecName); } encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); @@ -127,6 +124,14 @@ private boolean tryConfigureEncoder(EncoderConfig config) { } } + private boolean shouldForceCodecProfile(String codecName) { + if (codecName == null) { + return true; + } + return !codecName.startsWith("OMX.qcom.") + && !"OMX.hisi.video.encoder.avc".equals(codecName); + } + private boolean startEncoder() { try { encoder.start(); @@ -167,7 +172,8 @@ private List getSupportedConfigurations(int frameWidth, int frame new int[]{640, 360}, new int[]{426, 240})) { // only add resolutions bellow the original stream resolution - if (res[0] <= frameWidth && res[1] <= frameHeight) { + if (res[0] <= frameWidth && res[1] <= frameHeight + && !containsResolution(resolutions, res[0], res[1])) { resolutions.add(res); } } @@ -210,23 +216,103 @@ private boolean isProfileSupported(MediaCodecInfo codecInfo, String mimeType, in return false; } + private boolean containsResolution(List resolutions, int width, int height) { + for (int[] resolution : resolutions) { + if (resolution[0] == width && resolution[1] == height) { + return true; + } + } + return false; + } + + private void resetVideoEncoderState() { + encoderStarted = false; + outputFileWidth = -1; + outputFileHeight = -1; + encoderOutputBuffers = null; + trackIndex = -1; + videoFrameStart = 0; + } - private void initVideoEncoder(int frameWidth, int frameHeight) { - if (encoder != null) { - encoder.stop(); - encoder.release(); - encoder = null; + private void releaseVideoEncoderResources() { + drawer = null; + frameDrawer = null; + + if (eglBase != null) { + try { + eglBase.release(); + } catch (Exception e) { + Log.w(TAG, "Failed to release EGL base", e); + } finally { + eglBase = null; + } } + if (surface != null) { - surface.release(); - surface = null; + try { + surface.release(); + } catch (Exception e) { + Log.w(TAG, "Failed to release input surface", e); + } finally { + surface = null; + } + } + + if (encoder != null) { + try { + encoder.stop(); + } catch (Exception e) { + Log.w(TAG, "Failed to stop encoder during cleanup", e); + } + + try { + encoder.release(); + } catch (Exception e) { + Log.w(TAG, "Failed to release encoder during cleanup", e); + } finally { + encoder = null; + } } + } + + private boolean setupEncoderSurface(EglBase.Context eglContext, String contextLabel) { + try { + eglBase = EglBase.create(eglContext, EglBase.CONFIG_RECORDABLE); + Log.d(TAG, "EGL context created with " + contextLabel + " context"); + eglBase.createSurface(surface); + eglBase.makeCurrent(); + drawer = new GlRectDrawer(); + Log.d(TAG, "Encoder surface setup complete (" + contextLabel + "): " + surface); + return true; + } catch (Exception e) { + Log.w(TAG, "Failed to setup EGL surface with " + contextLabel + " context", e); + if (eglBase != null) { + try { + eglBase.release(); + } catch (Exception releaseError) { + Log.w(TAG, "Failed to release EGL base after setup failure", releaseError); + } finally { + eglBase = null; + } + } + drawer = null; + return false; + } + } + + + private void initVideoEncoder(int frameWidth, int frameHeight) { + releaseVideoEncoderResources(); + resetVideoEncoderState(); + encoderInitFailed = false; // Check codec capabilities MediaCodecInfo codecInfo = null; + String codecName = null; try { MediaCodec codec = MediaCodec.createEncoderByType(MIME_TYPE); codecInfo = codec.getCodecInfo(); + codecName = codecInfo.getName(); codec.release(); } catch (Exception e) { Log.e(TAG, "Failed to get codec info: " + e.getMessage()); @@ -246,7 +332,13 @@ private void initVideoEncoder(int frameWidth, int frameHeight) { Log.d(TAG, "Skipping unsupported bitrate: " + config); continue; } - if (!isProfileSupported(codecInfo, MIME_TYPE, config.profile)) { + if (!shouldForceCodecProfile(codecName) + && config.profile != MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline) { + Log.d(TAG, "Skipping redundant profile retry for codec " + codecName + ": " + config); + continue; + } + if (shouldForceCodecProfile(codecName) + && !isProfileSupported(codecInfo, MIME_TYPE, config.profile)) { Log.d(TAG, "Skipping unsupported profile: " + config); continue; } @@ -258,17 +350,21 @@ private void initVideoEncoder(int frameWidth, int frameHeight) { CountDownLatch latch = new CountDownLatch(1); renderThreadHandler.post(() -> { try { - eglBase = EglBase.create(sharedContext, EglBase.CONFIG_RECORDABLE); - Log.d(TAG, "EGL context created"); - eglBase.createSurface(surface); - eglBase.makeCurrent(); - drawer = new GlRectDrawer(); - encoderStarted = true; - encoderInitializing = false; - Log.d(TAG, "Encoder surface setup complete: " + surface); - } catch (Exception e) { - Log.e(TAG, "Failed to setup EGL surface: " + e.getMessage()); + boolean didSetup = false; + if (sharedContext != null) { + didSetup = setupEncoderSurface(sharedContext, "shared"); + } + if (!didSetup) { + didSetup = setupEncoderSurface(null, "standalone"); + } + encoderStarted = didSetup; + if (!didSetup) { + resetVideoEncoderState(); + releaseVideoEncoderResources(); + Log.e(TAG, "Failed to setup EGL surface for config: " + config); + } } finally { + encoderInitializing = false; latch.countDown(); } }); @@ -284,10 +380,16 @@ private void initVideoEncoder(int frameWidth, int frameHeight) { } } + resetVideoEncoderState(); + encoderInitializing = false; + encoderInitFailed = true; Log.e(TAG, "Failed to configure and start encoder with any supported configuration."); } @Override public void onFrame(VideoFrame frame) { + if (!isRunning || encoderInitFailed) { + return; + } frame.retain(); if (outputFileWidth == -1 && !encoderInitializing) { encoderInitializing = true; @@ -295,11 +397,15 @@ public void onFrame(VideoFrame frame) { int frameHeight = frame.getRotatedHeight(); initVideoEncoder(frameWidth, frameHeight); } + if (!encoderStarted || outputFileWidth == -1 || outputFileHeight == -1) { + frame.release(); + return; + } renderThreadHandler.post(() -> renderFrameOnRenderThread(frame)); } private void renderFrameOnRenderThread(VideoFrame frame) { - if (drawer == null) { + if (!encoderStarted || drawer == null || eglBase == null || encoder == null) { Log.e(TAG, "drawer is null — skipping frame render"); frame.release(); return; @@ -310,8 +416,8 @@ private void renderFrameOnRenderThread(VideoFrame frame) { } frameDrawer.drawFrame(frame, drawer, null, 0, 0, outputFileWidth, outputFileHeight); frame.release(); - drainEncoder(); eglBase.swapBuffers(); + drainEncoder(); } /** @@ -401,13 +507,23 @@ private void drainEncoder() { Log.e(TAG, "encoderOutputBuffer " + encoderStatus + " was null"); break; } + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + bufferInfo.size = 0; + } + if (bufferInfo.size <= 0 || bufferInfo.offset < 0) { + encoder.releaseOutputBuffer(encoderStatus, false); + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + break; + } + continue; + } // It's usually necessary to adjust the ByteBuffer values to match BufferInfo. encodedData.position(bufferInfo.offset); encodedData.limit(bufferInfo.offset + bufferInfo.size); if (videoFrameStart == 0 && bufferInfo.presentationTimeUs != 0) { videoFrameStart = bufferInfo.presentationTimeUs; } - bufferInfo.presentationTimeUs -= videoFrameStart; + bufferInfo.presentationTimeUs = Math.max(0, bufferInfo.presentationTimeUs - videoFrameStart); if (muxerStarted) mediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo); isRunning = isRunning && (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == 0; @@ -464,6 +580,16 @@ private void drainAudio() { Log.e(TAG, "encoderOutputBuffer " + encoderStatus + " was null"); break; } + if ((audioBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + audioBufferInfo.size = 0; + } + if (audioBufferInfo.size <= 0 || audioBufferInfo.offset < 0) { + audioEncoder.releaseOutputBuffer(encoderStatus, false); + if ((audioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + break; + } + continue; + } // It's usually necessary to adjust the ByteBuffer values to match BufferInfo. encodedData.position(audioBufferInfo.offset); diff --git a/common/cpp/include/flutter_video_renderer.h b/common/cpp/include/flutter_video_renderer.h index e2f283f560..b92fd93a0e 100644 --- a/common/cpp/include/flutter_video_renderer.h +++ b/common/cpp/include/flutter_video_renderer.h @@ -55,7 +55,7 @@ class FlutterVideoRenderer scoped_refptr frame_; std::unique_ptr texture_; std::shared_ptr pixel_buffer_; - mutable std::shared_ptr rgb_buffer_; + mutable std::shared_ptr rgb_buffer_; mutable std::mutex mutex_; RTCVideoFrame::VideoRotation rotation_ = RTCVideoFrame::kVideoRotation_0; }; diff --git a/common/cpp/src/flutter_media_stream.cc b/common/cpp/src/flutter_media_stream.cc index c3b193c60a..8889c80aed 100644 --- a/common/cpp/src/flutter_media_stream.cc +++ b/common/cpp/src/flutter_media_stream.cc @@ -66,6 +66,61 @@ void addDefaultAudioConstraints( audioConstraints->AddOptionalConstraint("googDAEchoCancellation", "true"); } +// Reads a boolean audio-processing flag from the audio constraint map. +// Supports flat W3C keys, mandatory sub-map keys, and optional list entries +// (each a single-pair map, the format used by the LiveKit SDK and others). +// Accepts bool values or "true"/"false" strings. Falls back to defaultValue +// when the key is absent (W3C default is true for AEC/NS/AGC). +static bool getAudioProcessingFlag(const EncodableMap& audioMap, + const std::vector& keys, + bool defaultValue) { + auto readBoolValue = [](const EncodableValue& v, bool def) -> bool { + if (TypeIs(v)) return GetValue(v); + if (TypeIs(v)) { + const std::string& s = GetValue(v); + if (s == "true") return true; + if (s == "false") return false; + } + return def; + }; + + for (const std::string& key : keys) { + // Flat W3C key at the top level. + auto it = audioMap.find(EncodableValue(key)); + if (it != audioMap.end()) { + return readBoolValue(it->second, defaultValue); + } + + // Inside "mandatory" sub-map. + auto mandatoryIt = audioMap.find(EncodableValue("mandatory")); + if (mandatoryIt != audioMap.end() && + TypeIs(mandatoryIt->second)) { + const EncodableMap& mandatory = + GetValue(mandatoryIt->second); + auto mit = mandatory.find(EncodableValue(key)); + if (mit != mandatory.end()) { + return readBoolValue(mit->second, defaultValue); + } + } + + // Inside "optional" list — each entry is a single-pair map. + auto optionalIt = audioMap.find(EncodableValue("optional")); + if (optionalIt != audioMap.end() && + TypeIs(optionalIt->second)) { + const EncodableList& list = GetValue(optionalIt->second); + for (const EncodableValue& item : list) { + if (!TypeIs(item)) continue; + const EncodableMap& entry = GetValue(item); + auto eit = entry.find(EncodableValue(key)); + if (eit != entry.end()) { + return readBoolValue(eit->second, defaultValue); + } + } + } + } + return defaultValue; +} + std::string getSourceIdConstraint(const EncodableMap& mediaConstraints) { auto it = mediaConstraints.find(EncodableValue("optional")); if (it != mediaConstraints.end() && TypeIs(it->second)) { @@ -96,6 +151,7 @@ void FlutterMediaStream::GetUserAudio(const EncodableMap& constraints, EncodableMap& params) { bool enable_audio = false; scoped_refptr audioConstraints; + RTCAudioOptions audio_options; std::string sourceId; std::string deviceId; auto it = constraints.find(EncodableValue("audio")); @@ -107,6 +163,11 @@ void FlutterMediaStream::GetUserAudio(const EncodableMap& constraints, enable_audio = GetValue(audio); sourceId = ""; deviceId = ""; + // audio: true — keep software processing on (W3C/WebRTC default). + audio_options.echo_cancellation = true; + audio_options.noise_suppression = true; + audio_options.auto_gain_control = true; + audio_options.highpass_filter = false; } if (TypeIs(audio)) { EncodableMap localMap = GetValue(audio); @@ -114,6 +175,16 @@ void FlutterMediaStream::GetUserAudio(const EncodableMap& constraints, deviceId = getDeviceIdConstraint(localMap); audioConstraints = base_->ParseMediaConstraints(localMap); enable_audio = true; + // Map W3C/goog-prefixed constraint keys to RTCAudioOptions so the + // software AEC/NS/AGC can actually be toggled from the Dart side. + audio_options.echo_cancellation = getAudioProcessingFlag( + localMap, {"echoCancellation", "googEchoCancellation"}, true); + audio_options.noise_suppression = getAudioProcessingFlag( + localMap, {"noiseSuppression", "googNoiseSuppression"}, true); + audio_options.auto_gain_control = getAudioProcessingFlag( + localMap, {"autoGainControl", "googAutoGainControl"}, true); + audio_options.highpass_filter = getAudioProcessingFlag( + localMap, {"highpassFilter", "googHighpassFilter"}, false); } } @@ -150,8 +221,8 @@ void FlutterMediaStream::GetUserAudio(const EncodableMap& constraints, } } - scoped_refptr source = - base_->factory_->CreateAudioSource("audio_input"); + scoped_refptr source = base_->factory_->CreateAudioSource( + "audio_input", RTCAudioSource::SourceType::kMicrophone, audio_options); std::string uuid = base_->GenerateUUID(); scoped_refptr track = base_->factory_->CreateAudioTrack(source, uuid.c_str()); @@ -169,9 +240,12 @@ void FlutterMediaStream::GetUserAudio(const EncodableMap& constraints, EncodableMap settings; settings[EncodableValue("deviceId")] = EncodableValue(sourceId); settings[EncodableValue("kind")] = EncodableValue("audioinput"); - settings[EncodableValue("autoGainControl")] = EncodableValue(true); - settings[EncodableValue("echoCancellation")] = EncodableValue(true); - settings[EncodableValue("noiseSuppression")] = EncodableValue(true); + settings[EncodableValue("autoGainControl")] = + EncodableValue(audio_options.auto_gain_control); + settings[EncodableValue("echoCancellation")] = + EncodableValue(audio_options.echo_cancellation); + settings[EncodableValue("noiseSuppression")] = + EncodableValue(audio_options.noise_suppression); settings[EncodableValue("channelCount")] = EncodableValue(1); settings[EncodableValue("latency")] = EncodableValue(0); track_info[EncodableValue("settings")] = EncodableValue(settings); diff --git a/common/darwin/Classes/AudioUtils.m b/common/darwin/Classes/AudioUtils.m index 968490d586..7b8653e101 100644 --- a/common/darwin/Classes/AudioUtils.m +++ b/common/darwin/Classes/AudioUtils.m @@ -95,7 +95,7 @@ + (void)setSpeakerphoneOn:(BOOL)enable { AVAudioSessionCategoryOptionAllowBluetooth error:&error]; - success = [session overrideOutputAudioPort:kAudioSessionProperty_OverrideAudioRoute + success = [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error]; if (!success) NSLog(@"setSpeakerphoneOn: Port override failed due to: %@", error); diff --git a/elinux/CMakeLists.txt b/elinux/CMakeLists.txt index 07f954c0ff..9a48477a62 100644 --- a/elinux/CMakeLists.txt +++ b/elinux/CMakeLists.txt @@ -48,12 +48,12 @@ target_include_directories(${PLUGIN_NAME} INTERFACE target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin - "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libwebrtc/lib/${FLUTTER_TARGET_PLATFORM}/libwebrtc.so" + "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libwebrtc/lib/libwebrtc.so" ) # List of absolute paths to libraries that should be bundled with the plugin set(flutter_webrtc_bundled_libraries - "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libwebrtc/lib/${FLUTTER_TARGET_PLATFORM}/libwebrtc.so" + "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libwebrtc/lib/libwebrtc.so" PARENT_SCOPE ) diff --git a/example/android/gradle.properties b/example/android/gradle.properties index f018a61817..475a62803f 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,3 +1,7 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 73e10df7d4..c00cc5ac46 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Nov 09 20:10:39 CST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/example/android/settings.gradle b/example/android/settings.gradle index f4e0879bf3..e14c0bebfa 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.6.0" apply false - id "org.jetbrains.kotlin.android" version "2.1.0" apply false + id "com.android.application" version "8.11.1" apply false + id "org.jetbrains.kotlin.android" version "2.2.20" apply false } include ":app" \ No newline at end of file diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt index e5b4202e39..671eb5af3c 100644 --- a/example/windows/CMakeLists.txt +++ b/example/windows/CMakeLists.txt @@ -57,6 +57,11 @@ add_subdirectory("runner") # them to the application. include(flutter/generated_plugins.cmake) +# VS 2026 treats C++/WinRT's experimental coroutine include as a hard error. +if(TARGET permission_handler_windows_plugin) + target_compile_definitions(permission_handler_windows_plugin PRIVATE + _SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS) +endif() # === Installation === # Support files are copied into place next to the executable, so that it can diff --git a/ios/stream_webrtc_flutter.podspec b/ios/stream_webrtc_flutter.podspec index 2a191c8041..b6536d2da7 100644 --- a/ios/stream_webrtc_flutter.podspec +++ b/ios/stream_webrtc_flutter.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'stream_webrtc_flutter' - s.version = '3.0.0' + s.version = '3.0.1' s.summary = 'Flutter WebRTC plugin for iOS.' s.description = <<-DESC A new flutter plugin project. diff --git a/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/AudioUtils.m b/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/AudioUtils.m index dff8b1eddf..d124eca654 100644 --- a/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/AudioUtils.m +++ b/ios/stream_webrtc_flutter/Sources/stream_webrtc_flutter/AudioUtils.m @@ -95,8 +95,7 @@ + (void)setSpeakerphoneOn:(BOOL)enable { AVAudioSessionCategoryOptionAllowBluetooth error:&error]; - success = [session overrideOutputAudioPort:kAudioSessionProperty_OverrideAudioRoute - error:&error]; + success = [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error]; if (!success) NSLog(@"setSpeakerphoneOn: Port override failed due to: %@", error); } diff --git a/lib/src/helper.dart b/lib/src/helper.dart index 1be8db24ab..4de7fd1ea7 100644 --- a/lib/src/helper.dart +++ b/lib/src/helper.dart @@ -200,10 +200,21 @@ class Helper { AppleNativeAudioManagement.getAppleAudioConfigurationForMode(mode, preferSpeakerOutput: preferSpeakerOutput)); - /// Request capture permission for Android - static Future requestCapturePermission() async { + /// Request capture permission for Android. + /// + /// When [fullScreenOnly] is true and running on Android 14+ (API 34), the + /// MediaProjection consent dialog only offers entire-screen capture and the + /// single-app option is removed (via + /// `MediaProjectionConfig.createConfigForDefaultDisplay()`). Has no effect on + /// older Android versions. Defaults to false, which keeps the platform's + /// default user-choice dialog. + static Future requestCapturePermission( + {bool fullScreenOnly = false}) async { if (WebRTC.platformIsAndroid) { - return await WebRTC.invokeMethod('requestCapturePermission'); + return await WebRTC.invokeMethod( + 'requestCapturePermission', + {'fullScreenOnly': fullScreenOnly}, + ); } else { throw Exception('requestCapturePermission only support for Android'); } diff --git a/lib/src/native/native_peer_connection_factory.dart b/lib/src/native/native_peer_connection_factory.dart index 8059dbd962..67c1127aef 100644 --- a/lib/src/native/native_peer_connection_factory.dart +++ b/lib/src/native/native_peer_connection_factory.dart @@ -136,7 +136,7 @@ class NativePeerConnectionFactory { /// Requests Android screen-capture permission. The granted projection data /// lives on this factory's `GetUserMediaImpl`, so the subsequent /// [getDisplayMedia] call must be issued through this same instance. - Future requestCapturePermission() async { + Future requestCapturePermission({bool fullScreenOnly = false}) async { _checkDisposed('requestCapturePermission'); if (!WebRTC.platformIsAndroid) { throw Exception('requestCapturePermission only supported for Android'); @@ -145,6 +145,7 @@ class NativePeerConnectionFactory { 'requestCapturePermission', { 'factoryId': factoryId, + 'fullScreenOnly': fullScreenOnly, }, ); return result == true; diff --git a/lib/src/web/native_peer_connection_factory.dart b/lib/src/web/native_peer_connection_factory.dart index a29a85d54a..49efa518ab 100644 --- a/lib/src/web/native_peer_connection_factory.dart +++ b/lib/src/web/native_peer_connection_factory.dart @@ -36,7 +36,8 @@ class NativePeerConnectionFactory { Future createLocalMediaStream(String label) async => throw UnimplementedError(); - Future requestCapturePermission() async => throw UnimplementedError(); + Future requestCapturePermission({bool fullScreenOnly = false}) async => + throw UnimplementedError(); Future getRtpSenderCapabilities(String kind) async => throw UnimplementedError(); diff --git a/lib/stream_webrtc_flutter.dart b/lib/stream_webrtc_flutter.dart index b36075ce9c..6168994f65 100644 --- a/lib/stream_webrtc_flutter.dart +++ b/lib/stream_webrtc_flutter.dart @@ -29,3 +29,6 @@ export 'src/native/rtc_video_platform_view.dart'; const String androidWebRTCVersion = '145.9.0'; const String iosWebRTCVersion = '145.9.0'; +const String macOsWebRTCVersion = '145.9.0'; +const String windowsWebRTCVersion = '144.7559.09'; +const String linuxWebRTCVersion = '144.7559.09'; diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 154a670ed6..342ec0e453 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -54,12 +54,12 @@ target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) target_link_libraries(${PLUGIN_NAME} PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libwebrtc/lib/${FLUTTER_TARGET_PLATFORM}/libwebrtc.so" + "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libwebrtc/lib/libwebrtc.so" ) # List of absolute paths to libraries that should be bundled with the plugin set(flutter_webrtc_bundled_libraries - "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libwebrtc/lib/${FLUTTER_TARGET_PLATFORM}/libwebrtc.so" + "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libwebrtc/lib/libwebrtc.so" PARENT_SCOPE ) diff --git a/macos/stream_webrtc_flutter.podspec b/macos/stream_webrtc_flutter.podspec index a35803f062..fdab367bd0 100644 --- a/macos/stream_webrtc_flutter.podspec +++ b/macos/stream_webrtc_flutter.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'stream_webrtc_flutter' - s.version = '3.0.0' + s.version = '3.0.1' s.summary = 'Flutter WebRTC plugin for macOS.' s.description = <<-DESC A new flutter plugin project. diff --git a/pubspec.yaml b/pubspec.yaml index c304ca5d6b..43fb2b43fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: stream_webrtc_flutter description: Flutter WebRTC plugin for iOS/Android/Destkop/Web, based on GoogleWebRTC. -version: 3.0.0 +version: 3.0.1 homepage: https://github.com/GetStream/webrtc-flutter environment: sdk: ">=3.6.0 <4.0.0" diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 56aae5aab2..621c8eba26 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -1,25 +1,143 @@ include(ExternalProject) -set(ZIPFILE "${CMAKE_CURRENT_LIST_DIR}/downloads/libwebrtc.zip") -set(DOWNLOAD_URL "https://github.com/flutter-webrtc/flutter-webrtc/releases/download/v1.2.1/libwebrtc.zip") +# Flutter adds plugins to example builds through .plugin_symlinks. Windows +# CMake refuses to extract archives through that symlink path, so resolve the +# real third_party directory before downloading or extracting libwebrtc. +get_filename_component(LIBWEBRTC_THIRD_PARTY_DIR "${CMAKE_CURRENT_LIST_DIR}" REALPATH) + +# Load binary version + download URL template from libwebrtc_version.ini so +# bumping the prebuilt release does not require editing CMake. +set(LIBWEBRTC_VERSION_INI "${LIBWEBRTC_THIRD_PARTY_DIR}/libwebrtc_version.ini") +if(NOT EXISTS "${LIBWEBRTC_VERSION_INI}") + message(FATAL_ERROR "libwebrtc: missing version manifest at ${LIBWEBRTC_VERSION_INI}") +endif() + +set(LIBWEBRTC_BINARY_VERSION "") +set(LIBWEBRTC_DOWNLOAD_URL_BASE "") +file(STRINGS "${LIBWEBRTC_VERSION_INI}" _libwebrtc_ini_lines) +foreach(_line IN LISTS _libwebrtc_ini_lines) + string(STRIP "${_line}" _line) + # Skip blanks, comments (# or ;) and section headers ([name]). + if(_line STREQUAL "" OR _line MATCHES "^[#;]" OR _line MATCHES "^\\[.*\\]$") + continue() + endif() + if(NOT _line MATCHES "^([^=]+)=(.*)$") + continue() + endif() + string(STRIP "${CMAKE_MATCH_1}" _key) + string(STRIP "${CMAKE_MATCH_2}" _value) + if(_key STREQUAL "binary_version") + set(LIBWEBRTC_BINARY_VERSION "${_value}") + elseif(_key STREQUAL "download_url") + set(LIBWEBRTC_DOWNLOAD_URL_BASE "${_value}") + endif() +endforeach() + +if(LIBWEBRTC_BINARY_VERSION STREQUAL "" OR LIBWEBRTC_DOWNLOAD_URL_BASE STREQUAL "") + message(FATAL_ERROR "libwebrtc: ${LIBWEBRTC_VERSION_INI} must define binary_version and download_url") +endif() + +# Strip any trailing slashes so we control the separator when composing the URL. +string(REGEX REPLACE "/+$" "" LIBWEBRTC_DOWNLOAD_URL_BASE "${LIBWEBRTC_DOWNLOAD_URL_BASE}") + +# Resolve OS field for the release asset name (win / linux) +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(LIBWEBRTC_OS "win") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(LIBWEBRTC_OS "linux") +else() + message(FATAL_ERROR "libwebrtc: unsupported target OS '${CMAKE_SYSTEM_NAME}'") +endif() + +# Resolve arch field for the release asset name (x64 / arm64) +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + # Visual Studio generator exposes the selected arch via CMAKE_GENERATOR_PLATFORM + string(TOLOWER "${CMAKE_GENERATOR_PLATFORM}" _libwebrtc_plat) + if(_libwebrtc_plat STREQUAL "arm64") + set(LIBWEBRTC_ARCH "arm64") + elseif(_libwebrtc_plat STREQUAL "x64" OR _libwebrtc_plat STREQUAL "") + # Empty platform usually means Ninja/default x64 host build + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(LIBWEBRTC_ARCH "x64") + else() + message(FATAL_ERROR "libwebrtc: 32-bit Windows is not supported") + endif() + else() + message(FATAL_ERROR "libwebrtc: unsupported Windows arch '${CMAKE_GENERATOR_PLATFORM}'") + endif() +else() + # Flutter Linux / eLinux pass the target as e.g. linux-x64 or linux-arm64 + if(DEFINED FLUTTER_TARGET_PLATFORM AND FLUTTER_TARGET_PLATFORM MATCHES "arm64") + set(LIBWEBRTC_ARCH "arm64") + elseif(DEFINED FLUTTER_TARGET_PLATFORM AND FLUTTER_TARGET_PLATFORM MATCHES "x64") + set(LIBWEBRTC_ARCH "x64") + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + set(LIBWEBRTC_ARCH "arm64") + else() + set(LIBWEBRTC_ARCH "x64") + endif() +endif() + +set(LIBWEBRTC_ASSET "libwebrtc-${LIBWEBRTC_OS}-${LIBWEBRTC_ARCH}-release") +set(ZIPFILE "${LIBWEBRTC_THIRD_PARTY_DIR}/downloads/${LIBWEBRTC_ASSET}.zip") + +# Final URL = //.zip +set(DOWNLOAD_URL "${LIBWEBRTC_DOWNLOAD_URL_BASE}/${LIBWEBRTC_BINARY_VERSION}/${LIBWEBRTC_ASSET}.zip") + +function(_libwebrtc_extract_and_normalize zip_path dest_dir) + # Extract into an isolated staging dir so we can normalize the top-level + # layout regardless of what folder name the archive uses (e.g. + # libwebrtc-linux-x64-release/ vs. libwebrtc/ vs. files at the root). + set(_staging "${dest_dir}/.libwebrtc_extract") + file(REMOVE_RECURSE "${_staging}") + file(MAKE_DIRECTORY "${_staging}") + file(ARCHIVE_EXTRACT INPUT "${zip_path}" DESTINATION "${_staging}") + + if(EXISTS "${dest_dir}/libwebrtc") + file(REMOVE_RECURSE "${dest_dir}/libwebrtc") + endif() + + file(GLOB _entries "${_staging}/*") + list(LENGTH _entries _entry_count) + set(_promoted FALSE) + if(_entry_count EQUAL 1) + list(GET _entries 0 _only) + if(IS_DIRECTORY "${_only}") + file(RENAME "${_only}" "${dest_dir}/libwebrtc") + set(_promoted TRUE) + endif() + endif() + if(NOT _promoted) + # Archive root holds the files directly — wrap them in libwebrtc/. + file(MAKE_DIRECTORY "${dest_dir}/libwebrtc") + foreach(_entry IN LISTS _entries) + get_filename_component(_name "${_entry}" NAME) + file(RENAME "${_entry}" "${dest_dir}/libwebrtc/${_name}") + endforeach() + endif() + + file(REMOVE_RECURSE "${_staging}") +endfunction() if(NOT EXISTS "${ZIPFILE}") message(NOTICE "download: ${DOWNLOAD_URL}") - file(DOWNLOAD "${DOWNLOAD_URL}" - ${ZIPFILE} - STATUS download_status - LOG download_log) + file(MAKE_DIRECTORY "${LIBWEBRTC_THIRD_PARTY_DIR}/downloads") + file(DOWNLOAD "${DOWNLOAD_URL}" + "${ZIPFILE}" + TLS_VERIFY ON + INACTIVITY_TIMEOUT 120 + STATUS download_status + LOG download_log) - if(NOT download_status EQUAL 0) + if(NOT download_status EQUAL 0) message(FATAL_ERROR "Failed to download dependency: ${download_log}") - endif() + endif() - file(ARCHIVE_EXTRACT INPUT ${ZIPFILE} DESTINATION "${CMAKE_CURRENT_LIST_DIR}") + _libwebrtc_extract_and_normalize("${ZIPFILE}" "${LIBWEBRTC_THIRD_PARTY_DIR}") else() - if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/libwebrtc") + if(NOT EXISTS "${LIBWEBRTC_THIRD_PARTY_DIR}/libwebrtc") message(NOTICE "libwebrtc directory does not exist after extraction.") - file(ARCHIVE_EXTRACT INPUT ${ZIPFILE} DESTINATION "${CMAKE_CURRENT_LIST_DIR}") + _libwebrtc_extract_and_normalize("${ZIPFILE}" "${LIBWEBRTC_THIRD_PARTY_DIR}") endif() message(TRACE "libwebrtc already downloaded.") endif() - diff --git a/third_party/libwebrtc_version.ini b/third_party/libwebrtc_version.ini new file mode 100644 index 0000000000..e1b6221b2e --- /dev/null +++ b/third_party/libwebrtc_version.ini @@ -0,0 +1,7 @@ +# libwebrtc prebuilt binary release info. +# See https://github.com/webrtc-sdk/libwebrtc/releases for available versions. +# `download_url` is the base release-download URL; the version tag, asset name +# and `.zip` suffix are appended at configure time by third_party/CMakeLists.txt. +[libwebrtc] +binary_version = libwebrtc.m144.7559.09 +download_url = https://github.com/webrtc-sdk/libwebrtc/releases/download diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 7febe29450..54ce4c83da 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -49,11 +49,10 @@ target_include_directories(${PLUGIN_NAME} INTERFACE target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin - "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libwebrtc/lib/win64/libwebrtc.dll.lib" + "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libwebrtc/lib/libwebrtc.dll.lib" ) # List of absolute paths to libraries that should be bundled with the plugin -set(stream_webrtc_flutter_bundled_libraries - "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libwebrtc/lib/win64/libwebrtc.dll" +set(stream_webrtc_flutter_bundled_libraries "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libwebrtc/lib/libwebrtc.dll" PARENT_SCOPE )