From a0b88b5aba428e2088ccc7894044542e95ea6149 Mon Sep 17 00:00:00 2001 From: CloudWebRTC Date: Sat, 21 Dec 2024 10:18:23 +0800 Subject: [PATCH] feat: support FocusMode/ExposureMode for camera capture options. (#658) * feat: support FocusMode/ExposureMode for camera capture options. * bump version for flutter-webrtc. * fix analyze. --- example/lib/theme.dart | 8 +-- example/lib/widgets/participant.dart | 33 +++++----- example/lib/widgets/participant_info.dart | 2 +- example/lib/widgets/participant_stats.dart | 2 +- example/lib/widgets/text_field.dart | 2 +- lib/src/core/engine.dart | 10 ++- lib/src/publication/remote.dart | 5 +- lib/src/track/options.dart | 14 ++++ lib/src/widgets/video_track_renderer.dart | 77 +++++++++++++++++----- pubspec.yaml | 2 +- 10 files changed, 104 insertions(+), 51 deletions(-) diff --git a/example/lib/theme.dart b/example/lib/theme.dart index 53f9a9cb..09b6fca8 100644 --- a/example/lib/theme.dart +++ b/example/lib/theme.dart @@ -44,7 +44,7 @@ class LiveKitTheme { // backgroundColor: WidgetStateProperty.all(accentColor), backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.disabled)) { - return accentColor.withOpacity(0.5); + return accentColor.withValues(alpha: 0.5); } return accentColor; }), @@ -59,13 +59,13 @@ class LiveKitTheme { if (states.contains(WidgetState.selected)) { return accentColor; } - return accentColor.withOpacity(0.3); + return accentColor.withValues(alpha: 0.3); }), thumbColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return Colors.white; } - return Colors.white.withOpacity(0.3); + return Colors.white.withValues(alpha: 0.3); }), ), dialogTheme: DialogTheme( @@ -87,7 +87,7 @@ class LiveKitTheme { color: LKColors.lkBlue, ), hintStyle: TextStyle( - color: LKColors.lkBlue.withOpacity(.5), + color: LKColors.lkBlue.withValues(alpha: 5), ), enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, diff --git a/example/lib/widgets/participant.dart b/example/lib/widgets/participant.dart index 1e090646..488d1132 100644 --- a/example/lib/widgets/participant.dart +++ b/example/lib/widgets/participant.dart @@ -176,19 +176,19 @@ abstract class _ParticipantWidgetState right: 30, child: ParticipantStatsWidget( participant: widget.participant, - )), - if(activeAudioTrack != null && !activeAudioTrack!.muted) Positioned( - top: 10, - right: 10, - left: 10, - bottom: 10, - child: SoundWaveformWidget( - key: ValueKey(activeAudioTrack!.hashCode), - audioTrack: activeAudioTrack!, - width: 8, - ), - ), - + )), + if (activeAudioTrack != null && !activeAudioTrack!.muted) + Positioned( + top: 10, + right: 10, + left: 10, + bottom: 10, + child: SoundWaveformWidget( + key: ValueKey(activeAudioTrack!.hashCode), + audioTrack: activeAudioTrack!, + width: 8, + ), + ), ], ), ); @@ -279,7 +279,7 @@ class RemoteTrackPublicationMenuWidget extends StatelessWidget { @override Widget build(BuildContext context) => Material( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), child: PopupMenuButton( tooltip: 'Subscribe menu', icon: Icon(icon, @@ -317,7 +317,7 @@ class RemoteTrackFPSMenuWidget extends StatelessWidget { @override Widget build(BuildContext context) => Material( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), child: PopupMenuButton( tooltip: 'Preferred FPS', icon: Icon(icon, color: Colors.white), @@ -351,7 +351,7 @@ class RemoteTrackQualityMenuWidget extends StatelessWidget { @override Widget build(BuildContext context) => Material( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), child: PopupMenuButton( tooltip: 'Preferred Quality', icon: Icon(icon, color: Colors.white), @@ -373,4 +373,3 @@ class RemoteTrackQualityMenuWidget extends StatelessWidget { ), ); } - diff --git a/example/lib/widgets/participant_info.dart b/example/lib/widgets/participant_info.dart index 52a40f2a..1064b5fe 100644 --- a/example/lib/widgets/participant_info.dart +++ b/example/lib/widgets/participant_info.dart @@ -43,7 +43,7 @@ class ParticipantInfoWidget extends StatelessWidget { @override Widget build(BuildContext context) => Container( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), padding: const EdgeInsets.symmetric( vertical: 7, horizontal: 10, diff --git a/example/lib/widgets/participant_stats.dart b/example/lib/widgets/participant_stats.dart index c6342707..09231fc1 100644 --- a/example/lib/widgets/participant_stats.dart +++ b/example/lib/widgets/participant_stats.dart @@ -148,7 +148,7 @@ class _ParticipantStatsWidgetState extends State { @override Widget build(BuildContext context) { return Container( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 8, diff --git a/example/lib/widgets/text_field.dart b/example/lib/widgets/text_field.dart index db9c42d6..f4173f3d 100644 --- a/example/lib/widgets/text_field.dart +++ b/example/lib/widgets/text_field.dart @@ -31,7 +31,7 @@ class LKTextField extends StatelessWidget { decoration: BoxDecoration( border: Border.all( width: 1, - color: Colors.white.withOpacity(.3), + color: Colors.white.withValues(alpha: .3), ), borderRadius: BorderRadius.circular(8), ), diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index e2d2be85..0180b275 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -1082,12 +1082,10 @@ extension EnginePrivateMethods on Engine { extension EngineInternalMethods on Engine { @internal - List dataChannelInfo() => - [_reliableDCPub, _lossyDCPub] - .whereNotNull() - .where((e) => e.id != -1) - .map((e) => e.toLKInfoType()) - .toList(); + List dataChannelInfo() => [ + _reliableDCPub, + _lossyDCPub + ].nonNulls.where((e) => e.id != -1).map((e) => e.toLKInfoType()).toList(); @internal Future createSimulcastTransceiverSender( LocalVideoTrack track, diff --git a/lib/src/publication/remote.dart b/lib/src/publication/remote.dart index bcb03d6c..5daa170f 100644 --- a/lib/src/publication/remote.dart +++ b/lib/src/publication/remote.dart @@ -17,7 +17,6 @@ import 'dart:math'; import 'package:flutter/widgets.dart'; -import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import '../core/signal_client.dart'; @@ -155,9 +154,9 @@ class RemoteTrackPublication // filter visible build contexts final viewSizes = videoTrack.viewKeys .map((e) => e.currentContext) - .whereNotNull() + .nonNulls .map((e) => e.findRenderObject() as RenderBox?) - .whereNotNull() + .nonNulls .where((e) => e.hasSize) .map((e) => e.size); diff --git a/lib/src/track/options.dart b/lib/src/track/options.dart index e1f3a945..d7f500ce 100644 --- a/lib/src/track/options.dart +++ b/lib/src/track/options.dart @@ -25,6 +25,10 @@ enum CameraPosition { back, } +enum CameraFocusMode { auto, locked } + +enum CameraExposureMode { auto, locked } + /// Convenience extension for [CameraPosition]. extension CameraPositionExt on CameraPosition { /// Return a [CameraPosition] which front and back is switched. @@ -41,8 +45,16 @@ class CameraCaptureOptions extends VideoCaptureOptions { /// set to false to only toggle enabled instead of stop/replaceTrack for muting final bool stopCameraCaptureOnMute; + /// The focus mode to use for the camera. + final CameraFocusMode focusMode; + + /// The exposure mode to use for the camera. + final CameraExposureMode exposureMode; + const CameraCaptureOptions({ this.cameraPosition = CameraPosition.front, + this.focusMode = CameraFocusMode.auto, + this.exposureMode = CameraExposureMode.auto, String? deviceId, double? maxFrameRate, VideoParameters params = VideoParametersPresets.h720_169, @@ -55,6 +67,8 @@ class CameraCaptureOptions extends VideoCaptureOptions { CameraCaptureOptions.from({required VideoCaptureOptions captureOptions}) : cameraPosition = CameraPosition.front, + focusMode = CameraFocusMode.auto, + exposureMode = CameraExposureMode.auto, stopCameraCaptureOnMute = true, super( params: captureOptions.params, diff --git a/lib/src/widgets/video_track_renderer.dart b/lib/src/widgets/video_track_renderer.dart index c2787203..88f4b479 100644 --- a/lib/src/widgets/video_track_renderer.dart +++ b/lib/src/widgets/video_track_renderer.dart @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:math'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -77,6 +79,25 @@ class _VideoTrackRendererState extends State { return _renderer!; } + void setZoom(double zoomLevel) async { + final videoTrack = _renderer?.srcObject!.getVideoTracks().first; + if (videoTrack == null) return; + await rtc.Helper.setZoom(videoTrack, zoomLevel); + } + + void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { + final videoTrack = _renderer?.srcObject!.getVideoTracks().first; + if (videoTrack == null) return; + + final point = Point( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ); + + rtc.Helper.setFocusPoint(videoTrack, point); + rtc.Helper.setExposurePoint(videoTrack, point); + } + void disposeRenderer() { try { _renderer?.srcObject = null; @@ -158,6 +179,28 @@ class _VideoTrackRendererState extends State { }, ); + Widget _videoRendererView() { + if (lkPlatformIs(PlatformType.iOS) && + [VideoRenderMode.auto, VideoRenderMode.platformView] + .contains(widget.renderMode)) { + return rtc.RTCVideoPlatFormView( + mirror: _shouldMirror(), + objectFit: widget.fit, + onViewReady: (controller) { + _renderer = controller; + _renderer?.srcObject = widget.track.mediaStream; + _attach(); + }, + ); + } + return rtc.RTCVideoView( + _renderer! as rtc.RTCVideoRenderer, + mirror: _shouldMirror(), + filterQuality: FilterQuality.medium, + objectFit: widget.fit, + ); + } + Widget _videoViewForNative() => FutureBuilder( future: _initializeRenderer(), builder: (context, snapshot) { @@ -172,24 +215,24 @@ class _VideoTrackRendererState extends State { ?.addPostFrameCallback((timeStamp) { widget.track.onVideoViewBuild?.call(_internalKey); }); - if (lkPlatformIs(PlatformType.iOS) && - [VideoRenderMode.auto, VideoRenderMode.platformView] - .contains(widget.renderMode)) { - return rtc.RTCVideoPlatFormView( - mirror: _shouldMirror(), - objectFit: widget.fit, - onViewReady: (controller) { - _renderer = controller; - _renderer?.srcObject = widget.track.mediaStream; - _attach(); - }, - ); + + if (!lkPlatformIsMobile() || widget.track is! LocalVideoTrack) { + return _videoRendererView(); } - return rtc.RTCVideoView( - _renderer! as rtc.RTCVideoRenderer, - mirror: _shouldMirror(), - filterQuality: FilterQuality.medium, - objectFit: widget.fit, + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return GestureDetector( + onScaleStart: (details) {}, + onScaleUpdate: (details) { + if (details.scale != 1.0) { + setZoom(details.scale); + } + }, + onTapDown: (TapDownDetails details) => + onViewFinderTap(details, constraints), + child: _videoRendererView(), + ); + }, ); }, ); diff --git a/pubspec.yaml b/pubspec.yaml index f76bd2e3..482b2eba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: uuid: '>=3.0.6' synchronized: ^3.0.0+3 protobuf: ^3.0.0 - flutter_webrtc: ^0.12.3 + flutter_webrtc: ^0.12.4 device_info_plus: ^11.1.1 js: '>=0.6.4' platform_detect: ^2.0.7