Skip to content

Commit a0b88b5

Browse files
authored
feat: support FocusMode/ExposureMode for camera capture options. (#658)
* feat: support FocusMode/ExposureMode for camera capture options. * bump version for flutter-webrtc. * fix analyze.
1 parent ada82ff commit a0b88b5

File tree

10 files changed

+104
-51
lines changed

10 files changed

+104
-51
lines changed

example/lib/theme.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class LiveKitTheme {
4444
// backgroundColor: WidgetStateProperty.all<Color>(accentColor),
4545
backgroundColor: WidgetStateProperty.resolveWith((states) {
4646
if (states.contains(WidgetState.disabled)) {
47-
return accentColor.withOpacity(0.5);
47+
return accentColor.withValues(alpha: 0.5);
4848
}
4949
return accentColor;
5050
}),
@@ -59,13 +59,13 @@ class LiveKitTheme {
5959
if (states.contains(WidgetState.selected)) {
6060
return accentColor;
6161
}
62-
return accentColor.withOpacity(0.3);
62+
return accentColor.withValues(alpha: 0.3);
6363
}),
6464
thumbColor: WidgetStateProperty.resolveWith((states) {
6565
if (states.contains(WidgetState.selected)) {
6666
return Colors.white;
6767
}
68-
return Colors.white.withOpacity(0.3);
68+
return Colors.white.withValues(alpha: 0.3);
6969
}),
7070
),
7171
dialogTheme: DialogTheme(
@@ -87,7 +87,7 @@ class LiveKitTheme {
8787
color: LKColors.lkBlue,
8888
),
8989
hintStyle: TextStyle(
90-
color: LKColors.lkBlue.withOpacity(.5),
90+
color: LKColors.lkBlue.withValues(alpha: 5),
9191
),
9292
enabledBorder: InputBorder.none,
9393
focusedBorder: InputBorder.none,

example/lib/widgets/participant.dart

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -176,19 +176,19 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
176176
right: 30,
177177
child: ParticipantStatsWidget(
178178
participant: widget.participant,
179-
)),
180-
if(activeAudioTrack != null && !activeAudioTrack!.muted) Positioned(
181-
top: 10,
182-
right: 10,
183-
left: 10,
184-
bottom: 10,
185-
child: SoundWaveformWidget(
186-
key: ValueKey(activeAudioTrack!.hashCode),
187-
audioTrack: activeAudioTrack!,
188-
width: 8,
189-
),
190-
),
191-
179+
)),
180+
if (activeAudioTrack != null && !activeAudioTrack!.muted)
181+
Positioned(
182+
top: 10,
183+
right: 10,
184+
left: 10,
185+
bottom: 10,
186+
child: SoundWaveformWidget(
187+
key: ValueKey(activeAudioTrack!.hashCode),
188+
audioTrack: activeAudioTrack!,
189+
width: 8,
190+
),
191+
),
192192
],
193193
),
194194
);
@@ -279,7 +279,7 @@ class RemoteTrackPublicationMenuWidget extends StatelessWidget {
279279

280280
@override
281281
Widget build(BuildContext context) => Material(
282-
color: Colors.black.withOpacity(0.3),
282+
color: Colors.black.withValues(alpha: 0.3),
283283
child: PopupMenuButton<Function>(
284284
tooltip: 'Subscribe menu',
285285
icon: Icon(icon,
@@ -317,7 +317,7 @@ class RemoteTrackFPSMenuWidget extends StatelessWidget {
317317

318318
@override
319319
Widget build(BuildContext context) => Material(
320-
color: Colors.black.withOpacity(0.3),
320+
color: Colors.black.withValues(alpha: 0.3),
321321
child: PopupMenuButton<Function>(
322322
tooltip: 'Preferred FPS',
323323
icon: Icon(icon, color: Colors.white),
@@ -351,7 +351,7 @@ class RemoteTrackQualityMenuWidget extends StatelessWidget {
351351

352352
@override
353353
Widget build(BuildContext context) => Material(
354-
color: Colors.black.withOpacity(0.3),
354+
color: Colors.black.withValues(alpha: 0.3),
355355
child: PopupMenuButton<Function>(
356356
tooltip: 'Preferred Quality',
357357
icon: Icon(icon, color: Colors.white),
@@ -373,4 +373,3 @@ class RemoteTrackQualityMenuWidget extends StatelessWidget {
373373
),
374374
);
375375
}
376-

example/lib/widgets/participant_info.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class ParticipantInfoWidget extends StatelessWidget {
4343

4444
@override
4545
Widget build(BuildContext context) => Container(
46-
color: Colors.black.withOpacity(0.3),
46+
color: Colors.black.withValues(alpha: 0.3),
4747
padding: const EdgeInsets.symmetric(
4848
vertical: 7,
4949
horizontal: 10,

example/lib/widgets/participant_stats.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
148148
@override
149149
Widget build(BuildContext context) {
150150
return Container(
151-
color: Colors.black.withOpacity(0.3),
151+
color: Colors.black.withValues(alpha: 0.3),
152152
padding: const EdgeInsets.symmetric(
153153
vertical: 8,
154154
horizontal: 8,

example/lib/widgets/text_field.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class LKTextField extends StatelessWidget {
3131
decoration: BoxDecoration(
3232
border: Border.all(
3333
width: 1,
34-
color: Colors.white.withOpacity(.3),
34+
color: Colors.white.withValues(alpha: .3),
3535
),
3636
borderRadius: BorderRadius.circular(8),
3737
),

lib/src/core/engine.dart

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,12 +1082,10 @@ extension EnginePrivateMethods on Engine {
10821082

10831083
extension EngineInternalMethods on Engine {
10841084
@internal
1085-
List<lk_rtc.DataChannelInfo> dataChannelInfo() =>
1086-
[_reliableDCPub, _lossyDCPub]
1087-
.whereNotNull()
1088-
.where((e) => e.id != -1)
1089-
.map((e) => e.toLKInfoType())
1090-
.toList();
1085+
List<lk_rtc.DataChannelInfo> dataChannelInfo() => [
1086+
_reliableDCPub,
1087+
_lossyDCPub
1088+
].nonNulls.where((e) => e.id != -1).map((e) => e.toLKInfoType()).toList();
10911089
@internal
10921090
Future<rtc.RTCRtpSender> createSimulcastTransceiverSender(
10931091
LocalVideoTrack track,

lib/src/publication/remote.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import 'dart:math';
1717

1818
import 'package:flutter/widgets.dart';
1919

20-
import 'package:collection/collection.dart';
2120
import 'package:meta/meta.dart';
2221

2322
import '../core/signal_client.dart';
@@ -155,9 +154,9 @@ class RemoteTrackPublication<T extends RemoteTrack>
155154
// filter visible build contexts
156155
final viewSizes = videoTrack.viewKeys
157156
.map((e) => e.currentContext)
158-
.whereNotNull()
157+
.nonNulls
159158
.map((e) => e.findRenderObject() as RenderBox?)
160-
.whereNotNull()
159+
.nonNulls
161160
.where((e) => e.hasSize)
162161
.map((e) => e.size);
163162

lib/src/track/options.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ enum CameraPosition {
2525
back,
2626
}
2727

28+
enum CameraFocusMode { auto, locked }
29+
30+
enum CameraExposureMode { auto, locked }
31+
2832
/// Convenience extension for [CameraPosition].
2933
extension CameraPositionExt on CameraPosition {
3034
/// Return a [CameraPosition] which front and back is switched.
@@ -41,8 +45,16 @@ class CameraCaptureOptions extends VideoCaptureOptions {
4145
/// set to false to only toggle enabled instead of stop/replaceTrack for muting
4246
final bool stopCameraCaptureOnMute;
4347

48+
/// The focus mode to use for the camera.
49+
final CameraFocusMode focusMode;
50+
51+
/// The exposure mode to use for the camera.
52+
final CameraExposureMode exposureMode;
53+
4454
const CameraCaptureOptions({
4555
this.cameraPosition = CameraPosition.front,
56+
this.focusMode = CameraFocusMode.auto,
57+
this.exposureMode = CameraExposureMode.auto,
4658
String? deviceId,
4759
double? maxFrameRate,
4860
VideoParameters params = VideoParametersPresets.h720_169,
@@ -55,6 +67,8 @@ class CameraCaptureOptions extends VideoCaptureOptions {
5567

5668
CameraCaptureOptions.from({required VideoCaptureOptions captureOptions})
5769
: cameraPosition = CameraPosition.front,
70+
focusMode = CameraFocusMode.auto,
71+
exposureMode = CameraExposureMode.auto,
5872
stopCameraCaptureOnMute = true,
5973
super(
6074
params: captureOptions.params,

lib/src/widgets/video_track_renderer.dart

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import 'dart:math';
16+
1517
import 'package:flutter/foundation.dart';
1618
import 'package:flutter/material.dart';
1719

@@ -77,6 +79,25 @@ class _VideoTrackRendererState extends State<VideoTrackRenderer> {
7779
return _renderer!;
7880
}
7981

82+
void setZoom(double zoomLevel) async {
83+
final videoTrack = _renderer?.srcObject!.getVideoTracks().first;
84+
if (videoTrack == null) return;
85+
await rtc.Helper.setZoom(videoTrack, zoomLevel);
86+
}
87+
88+
void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
89+
final videoTrack = _renderer?.srcObject!.getVideoTracks().first;
90+
if (videoTrack == null) return;
91+
92+
final point = Point<double>(
93+
details.localPosition.dx / constraints.maxWidth,
94+
details.localPosition.dy / constraints.maxHeight,
95+
);
96+
97+
rtc.Helper.setFocusPoint(videoTrack, point);
98+
rtc.Helper.setExposurePoint(videoTrack, point);
99+
}
100+
80101
void disposeRenderer() {
81102
try {
82103
_renderer?.srcObject = null;
@@ -158,6 +179,28 @@ class _VideoTrackRendererState extends State<VideoTrackRenderer> {
158179
},
159180
);
160181

182+
Widget _videoRendererView() {
183+
if (lkPlatformIs(PlatformType.iOS) &&
184+
[VideoRenderMode.auto, VideoRenderMode.platformView]
185+
.contains(widget.renderMode)) {
186+
return rtc.RTCVideoPlatFormView(
187+
mirror: _shouldMirror(),
188+
objectFit: widget.fit,
189+
onViewReady: (controller) {
190+
_renderer = controller;
191+
_renderer?.srcObject = widget.track.mediaStream;
192+
_attach();
193+
},
194+
);
195+
}
196+
return rtc.RTCVideoView(
197+
_renderer! as rtc.RTCVideoRenderer,
198+
mirror: _shouldMirror(),
199+
filterQuality: FilterQuality.medium,
200+
objectFit: widget.fit,
201+
);
202+
}
203+
161204
Widget _videoViewForNative() => FutureBuilder(
162205
future: _initializeRenderer(),
163206
builder: (context, snapshot) {
@@ -172,24 +215,24 @@ class _VideoTrackRendererState extends State<VideoTrackRenderer> {
172215
?.addPostFrameCallback((timeStamp) {
173216
widget.track.onVideoViewBuild?.call(_internalKey);
174217
});
175-
if (lkPlatformIs(PlatformType.iOS) &&
176-
[VideoRenderMode.auto, VideoRenderMode.platformView]
177-
.contains(widget.renderMode)) {
178-
return rtc.RTCVideoPlatFormView(
179-
mirror: _shouldMirror(),
180-
objectFit: widget.fit,
181-
onViewReady: (controller) {
182-
_renderer = controller;
183-
_renderer?.srcObject = widget.track.mediaStream;
184-
_attach();
185-
},
186-
);
218+
219+
if (!lkPlatformIsMobile() || widget.track is! LocalVideoTrack) {
220+
return _videoRendererView();
187221
}
188-
return rtc.RTCVideoView(
189-
_renderer! as rtc.RTCVideoRenderer,
190-
mirror: _shouldMirror(),
191-
filterQuality: FilterQuality.medium,
192-
objectFit: widget.fit,
222+
return LayoutBuilder(
223+
builder: (BuildContext context, BoxConstraints constraints) {
224+
return GestureDetector(
225+
onScaleStart: (details) {},
226+
onScaleUpdate: (details) {
227+
if (details.scale != 1.0) {
228+
setZoom(details.scale);
229+
}
230+
},
231+
onTapDown: (TapDownDetails details) =>
232+
onViewFinderTap(details, constraints),
233+
child: _videoRendererView(),
234+
);
235+
},
193236
);
194237
},
195238
);

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ dependencies:
3737
uuid: '>=3.0.6'
3838
synchronized: ^3.0.0+3
3939
protobuf: ^3.0.0
40-
flutter_webrtc: ^0.12.3
40+
flutter_webrtc: ^0.12.4
4141
device_info_plus: ^11.1.1
4242
js: '>=0.6.4'
4343
platform_detect: ^2.0.7

0 commit comments

Comments
 (0)