diff --git a/.github/workflows/flutter_analysis.yml b/.github/workflows/flutter_analysis.yml index f27cda34..21098500 100644 --- a/.github/workflows/flutter_analysis.yml +++ b/.github/workflows/flutter_analysis.yml @@ -19,7 +19,7 @@ jobs: - name: Install Flutter uses: subosito/flutter-action@v2.8.0 with: - channel: master + channel: stable - run: flutter upgrade - run: flutter pub get @@ -41,7 +41,7 @@ jobs: - name: Install Flutter uses: subosito/flutter-action@v2.8.0 with: - channel: master + channel: stable - run: flutter upgrade - run: flutter pub get diff --git a/README.md b/README.md index 28fd9834..32301032 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,16 @@ lib ├───firebase_options.dart [auto-generated firebase configuration.] └───main.dart [entry-point of the application.] +packages +│ +├───unity_multi_window [multi-window support for desktop platforms.] +│ +├───unity_video_player +│ ├───unity_video_player [the core video player logic.] +│ ├───unity_video_player_flutter [video player used as fallback, used on embedded platforms.] +│ ├───unity_video_player_main [main video player logic, used on most platforms.] +│ ├───unity_video_player_platform_interface [the platform interface for the video player.] +│ └───unity_video_player_web [web specific video player logic, used on web.] ``` Feel free to send any pull-requests to add any features you wish or fix any bugs you notice. diff --git a/lib/models/device.dart b/lib/models/device.dart index 9d555fd4..b8bf6ad3 100644 --- a/lib/models/device.dart +++ b/lib/models/device.dart @@ -100,8 +100,8 @@ class Device { /// The type of zoom matrix of this device. /// - /// Defaults to a 4x4 matrix. - final MatrixType matrixType; + /// If not provided, used from the settings. + final MatrixType? matrixType; /// A list of text overlays that will be rendered over the video. final List overlays; @@ -124,7 +124,7 @@ class Device { required this.server, this.hasPTZ = false, this.url, - this.matrixType = MatrixType.t16, + this.matrixType, this.overlays = const [], this.preferredStreamingType, this.externalData, @@ -372,7 +372,7 @@ class Device { 'server': server.toJson(devices: false), 'hasPTZ': hasPTZ, 'url': url, - 'matrixType': matrixType.index, + if (matrixType != null) 'matrixType': matrixType!.index, 'overlays': overlays.map((e) => e.toMap()).toList(), 'preferredStreamingType': preferredStreamingType?.name, 'externalData': externalData?.toMap(), diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 4b1c0de9..28c122d1 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -17,10 +17,12 @@ * along with this program. If not, see . */ +import 'dart:io'; + import 'package:bluecherry_client/providers/app_provider_interface.dart'; import 'package:bluecherry_client/providers/downloads_provider.dart'; -import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; import 'package:bluecherry_client/utils/storage.dart'; +import 'package:bluecherry_client/utils/video_player.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -40,6 +42,7 @@ class _SettingsOption { late final String Function(T value) saveAs; late final T Function(String value) loadFrom; + final ValueChanged? onChanged; late T _value; @@ -47,6 +50,7 @@ class _SettingsOption { set value(T newValue) { SettingsProvider.instance.updateProperty(() { _value = newValue; + onChanged?.call(value); }); } @@ -61,6 +65,7 @@ class _SettingsOption { T Function(String value)? loadFrom, this.min, this.max, + this.onChanged, }) { Future.microtask(() async { _value = getDefault != null ? await getDefault!() : def; @@ -341,6 +346,17 @@ class SettingsProvider extends UnityProvider { loadFrom: (value) => MatrixType.values[int.parse(value)], saveAs: (value) => value.index.toString(), ); + final kSoftwareZooming = _SettingsOption( + def: Platform.isMacOS ? true : false, + key: 'other.software_zoom', + onChanged: (value) { + for (final player in UnityPlayers.players.values) { + player + ..resetCrop() + ..softwareZoom = value; + } + }, + ); final kShowDebugInfo = _SettingsOption( def: kDebugMode, key: 'other.show_debug_info', @@ -405,6 +421,7 @@ class SettingsProvider extends UnityProvider { kShowReleaseNotes.loadData(data), kDefaultBetaMatrixedZoomEnabled.loadData(data), kMatrixSize.loadData(data), + kSoftwareZooming.loadData(data), kShowDebugInfo.loadData(data), kShowNetworkUsage.loadData(data), ]); @@ -460,6 +477,29 @@ class SettingsProvider extends UnityProvider { kTimelineInitialPoint.saveAs(kTimelineInitialPoint.value), kThemeMode.key: kThemeMode.saveAs(kThemeMode.value), kLanguageCode.key: kLanguageCode.saveAs(kLanguageCode.value), + kDateFormat.key: kDateFormat.saveAs(kDateFormat.value), + kTimeFormat.key: kTimeFormat.saveAs(kTimeFormat.value), + kLaunchAppOnStartup.key: + kLaunchAppOnStartup.saveAs(kLaunchAppOnStartup.value), + kMinimizeToTray.key: kMinimizeToTray.saveAs(kMinimizeToTray.value), + kAnimationsEnabled.key: + kAnimationsEnabled.saveAs(kAnimationsEnabled.value), + kHighContrast.key: kHighContrast.saveAs(kHighContrast.value), + kLargeFont.key: kLargeFont.saveAs(kLargeFont.value), + kAllowDataCollection.key: + kAllowDataCollection.saveAs(kAllowDataCollection.value), + kAllowCrashReports.key: + kAllowCrashReports.saveAs(kAllowCrashReports.value), + kAutoUpdate.key: kAutoUpdate.saveAs(kAutoUpdate.value), + kShowReleaseNotes.key: + kShowReleaseNotes.saveAs(kShowReleaseNotes.value), + kDefaultBetaMatrixedZoomEnabled.key: kDefaultBetaMatrixedZoomEnabled + .saveAs(kDefaultBetaMatrixedZoomEnabled.value), + kMatrixSize.key: kMatrixSize.saveAs(kMatrixSize.value), + kSoftwareZooming.key: kSoftwareZooming.saveAs(kSoftwareZooming.value), + kShowDebugInfo.key: kShowDebugInfo.saveAs(kShowDebugInfo.value), + kShowNetworkUsage.key: + kShowNetworkUsage.saveAs(kShowNetworkUsage.value), }); } catch (e) { debugPrint('Error saving settings: $e'); diff --git a/lib/screens/layouts/desktop/external_stream.dart b/lib/screens/layouts/desktop/external_stream.dart index 8477ff05..d42d6e90 100644 --- a/lib/screens/layouts/desktop/external_stream.dart +++ b/lib/screens/layouts/desktop/external_stream.dart @@ -28,28 +28,12 @@ import 'package:bluecherry_client/utils/extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:unity_video_player/unity_video_player.dart'; import 'package:uuid/uuid.dart'; -enum MatrixType { - t16(4), - t9(3), - t4(2), - t1(1); - - final int size; - - const MatrixType(this.size); - - @override - String toString() { - return switch (this) { - MatrixType.t16 => '4x4', - MatrixType.t9 => '3x3', - MatrixType.t4 => '2x2', - MatrixType.t1 => '1x1', - }; - } +export 'package:unity_video_player/unity_video_player.dart' show MatrixType; +extension MatrixTypeExtension on MatrixType { Widget get icon { return switch (this) { MatrixType.t16 => const Icon(Icons.grid_4x4), @@ -58,16 +42,6 @@ enum MatrixType { MatrixType.t1 => const Icon(Icons.square_outlined), }; } - - MatrixType get next { - return switch (this) { - MatrixType.t16 => MatrixType.t9, - MatrixType.t9 => MatrixType.t4, - MatrixType.t4 => MatrixType.t16, - // ideally, t1 is never reached - MatrixType.t1 => MatrixType.t16, - }; - } } class AddExternalStreamDialog extends StatefulWidget { diff --git a/lib/screens/layouts/desktop/multicast_view.dart b/lib/screens/layouts/desktop/multicast_view.dart index b08c6672..cb6c2138 100644 --- a/lib/screens/layouts/desktop/multicast_view.dart +++ b/lib/screens/layouts/desktop/multicast_view.dart @@ -101,7 +101,8 @@ class _MulticastViewportState extends State { return const SizedBox.shrink(); } - final size = widget.device.matrixType.size; + final matrixType = widget.device.matrixType ?? settings.kMatrixSize.value; + final size = matrixType.size; if (view.player.isCropped) { return Listener( onPointerSignal: (event) async { @@ -155,9 +156,7 @@ class _MulticastViewportState extends State { onDoubleTap: () { views.updateDevice( widget.device, - widget.device.copyWith( - matrixType: widget.device.matrixType.next, - ), + widget.device.copyWith(matrixType: matrixType.next), ); }, onPressed: () { diff --git a/lib/screens/layouts/desktop/stream_data.dart b/lib/screens/layouts/desktop/stream_data.dart index 68f128d4..f7f0c65b 100644 --- a/lib/screens/layouts/desktop/stream_data.dart +++ b/lib/screens/layouts/desktop/stream_data.dart @@ -250,7 +250,8 @@ class _StreamDataState extends State { Center( child: ToggleButtons( isSelected: MatrixType.values.map((type) { - return type.index == matrixType.index; + return type.index == + (matrixType?.index ?? settings.kMatrixSize.value); }).toList(), onPressed: (type) => setState(() { matrixType = MatrixType.values[type]; diff --git a/lib/screens/settings/advanced_options.dart b/lib/screens/settings/advanced_options.dart index 113983da..f98f4969 100644 --- a/lib/screens/settings/advanced_options.dart +++ b/lib/screens/settings/advanced_options.dart @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +import 'dart:io'; + import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/layouts/desktop/external_stream.dart'; import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; @@ -52,29 +54,47 @@ class AdvancedOptionsSettings extends StatelessWidget { value: settings.kDefaultBetaMatrixedZoomEnabled.value, onChanged: (value) { if (value != null) { - settings.updateProperty(() { - settings.kDefaultBetaMatrixedZoomEnabled.value = value; - }); + settings.kDefaultBetaMatrixedZoomEnabled.value = value; } }, ), - if (kDebugMode) - OptionsChooserTile( - title: 'Default Matrix Size', - icon: Icons.view_quilt, - value: MatrixType.t4, - values: MatrixType.values.map((size) { - return Option( - value: size, - text: size.toString(), - ); - }), - onChanged: (v) { - settings.updateProperty(() { - settings.kMatrixSize.value = v; - }); - }, + OptionsChooserTile( + title: 'Default Matrix Size', + icon: Icons.view_quilt, + value: settings.kMatrixSize.value, + values: MatrixType.values.map((size) { + return Option( + value: size, + text: size.toString(), + ); + }), + onChanged: (v) { + settings.kMatrixSize.value = v; + }, + ), + CheckboxListTile.adaptive( + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.center_focus_strong), + ), + title: const Text('Software zoom'), + subtitle: Text( + 'When enabled, the matrix zoom will not happen in the GPU. This is ' + 'useful when the hardware zoom is not working properly. ' + '${Platform.isMacOS ? 'On macOS, this can not be disabled.' : ''}', ), + value: Platform.isMacOS ? true : settings.kSoftwareZooming.value, + onChanged: Platform.isMacOS + ? null + : (v) { + if (v != null) { + settings.kSoftwareZooming.value = v; + } + }, + dense: false, + ), const SubHeader('Developer Options'), if (!kIsWeb) ...[ FutureBuilder( @@ -82,7 +102,11 @@ class AdvancedOptionsSettings extends StatelessWidget { builder: (context, snapshot) { return ListTile( contentPadding: DesktopSettings.horizontalPadding, - leading: const Icon(Icons.bug_report), + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.bug_report), + ), title: const Text('Open log file'), subtitle: Text(snapshot.data?.path ?? loc.loading), trailing: const Icon(Icons.navigate_next), @@ -100,7 +124,11 @@ class AdvancedOptionsSettings extends StatelessWidget { builder: (context, snapshot) { return ListTile( contentPadding: DesktopSettings.horizontalPadding, - leading: const Icon(Icons.home), + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.home), + ), title: const Text('Open app data'), subtitle: Text(snapshot.data?.path ?? loc.loading), trailing: const Icon(Icons.navigate_next), @@ -113,9 +141,13 @@ class AdvancedOptionsSettings extends StatelessWidget { ); }, ), - CheckboxListTile( + CheckboxListTile.adaptive( contentPadding: DesktopSettings.horizontalPadding, - secondary: const Icon(Icons.adb), + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.adb), + ), title: const Text('Debug info'), subtitle: const Text( 'Display useful information for debugging, such as video metadata ' @@ -124,17 +156,19 @@ class AdvancedOptionsSettings extends StatelessWidget { value: settings.kShowDebugInfo.value, onChanged: (v) { if (v != null) { - settings.updateProperty(() { - settings.kShowDebugInfo.value = v; - }); + settings.kShowDebugInfo.value = v; } }, dense: false, ), if (kDebugMode) - CheckboxListTile( + CheckboxListTile.adaptive( contentPadding: DesktopSettings.horizontalPadding, - secondary: const Icon(Icons.network_check), + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.network_check), + ), title: const Text('Network Usage'), subtitle: const Text( 'Display network usage information over playing videos.', @@ -151,7 +185,11 @@ class AdvancedOptionsSettings extends StatelessWidget { ), ListTile( contentPadding: DesktopSettings.horizontalPadding, - leading: const Icon(Icons.restore), + leading: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.restore), + ), title: const Text('Restore Defaults'), subtitle: const Text( 'Restore all settings to their default values. This will not ' diff --git a/lib/utils/video_player.dart b/lib/utils/video_player.dart index c2f8368a..0b0593db 100644 --- a/lib/utils/video_player.dart +++ b/lib/utils/video_player.dart @@ -119,6 +119,8 @@ class UnityPlayers with ChangeNotifier { }, onReload: setSource, title: device.fullName, + matrixType: device.matrixType ?? settings.kMatrixSize.value, + softwareZoom: settings.kSoftwareZooming.value, ) ..setVolume(0.0) ..setSpeed(1.0); diff --git a/packages/unity_video_player/unity_video_player_flutter/lib/unity_video_player_flutter.dart b/packages/unity_video_player/unity_video_player_flutter/lib/unity_video_player_flutter.dart index ae98aa45..e057e07d 100644 --- a/packages/unity_video_player/unity_video_player_flutter/lib/unity_video_player_flutter.dart +++ b/packages/unity_video_player/unity_video_player_flutter/lib/unity_video_player_flutter.dart @@ -32,6 +32,8 @@ class UnityVideoPlayerFlutterInterface extends UnityVideoPlayerInterface { RTSPProtocol? rtspProtocol, VoidCallback? onReload, String? title, + MatrixType matrixType = MatrixType.t16, + bool softwareZoom = false, }) { final player = UnityVideoPlayerFlutter( width: width, diff --git a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart index ba304361..2755f41f 100644 --- a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart +++ b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart @@ -29,13 +29,17 @@ class UnityVideoPlayerMediaKitInterface extends UnityVideoPlayerInterface { RTSPProtocol? rtspProtocol, VoidCallback? onReload, String? title, + MatrixType matrixType = MatrixType.t16, + bool softwareZoom = false, }) { final player = UnityVideoPlayerMediaKit( width: width, height: height, enableCache: enableCache, title: title, - ); + ) + ..matrixType = matrixType + ..softwareZoom = softwareZoom; UnityVideoPlayerInterface.registerPlayer(player); return player; } @@ -50,6 +54,7 @@ class UnityVideoPlayerMediaKitInterface extends UnityVideoPlayerInterface { Color color = const Color(0xFF000000), }) { videoBuilder ??= (context, video) => video; + final mkPlayer = (player as UnityVideoPlayerMediaKit); return Builder(builder: (context) { return Stack(children: [ @@ -58,8 +63,9 @@ class UnityVideoPlayerMediaKitInterface extends UnityVideoPlayerInterface { context, _MKVideo( key: ValueKey(player), - player: (player as UnityVideoPlayerMediaKit).mkPlayer, + player: mkPlayer.mkPlayer, videoController: player.mkVideoController, + mkPlayer: mkPlayer, color: color, fit: () { return switch (fit) { @@ -88,12 +94,14 @@ class _MKVideo extends StatefulWidget { super.key, required this.player, required this.videoController, + required this.mkPlayer, required this.fit, required this.color, }); final Player player; final VideoController videoController; + final UnityVideoPlayerMediaKit mkPlayer; final BoxFit fit; final Color color; @@ -107,15 +115,14 @@ class _MKVideoState extends State<_MKVideo> { @override void didUpdateWidget(covariant _MKVideo oldWidget) { super.didUpdateWidget(oldWidget); - videoKey.currentState?.update( - fit: widget.fit, - fill: widget.color, - ); + if (oldWidget.fit != widget.fit || oldWidget.color != widget.color) { + videoKey.currentState?.update(fit: widget.fit, fill: widget.color); + } } @override Widget build(BuildContext context) { - return Video( + final videoWidget = Video( key: videoKey, controller: widget.videoController, fill: widget.color, @@ -123,6 +130,40 @@ class _MKVideoState extends State<_MKVideo> { controls: NoVideoControls, wakelock: UnityVideoPlayerInterface.wakelockEnabled, ); + + if (!widget.mkPlayer.softwareZoom) { + return videoWidget; + } + + // digital zoom + + final videoSize = widget.mkPlayer.maxSize; + final zoomRect = widget.mkPlayer.viewportRect; + + final rectXFactor = + !widget.mkPlayer.isCropped ? 0.0 : zoomRect.left / videoSize.width; + final rectYFactor = + !widget.mkPlayer.isCropped ? 0.0 : zoomRect.top / videoSize.height; + + return LayoutBuilder(builder: (context, constraints) { + final consts = !widget.mkPlayer.isCropped + ? constraints + : constraints * widget.mkPlayer.matrixType.size.toDouble(); + final rectX = consts.maxWidth * rectXFactor; + final rectY = consts.maxHeight * rectYFactor; + return Stack(children: [ + const Positioned.fill(child: SizedBox.shrink()), + Positioned( + left: -rectX, + top: -rectY, + child: SizedBox( + width: consts.maxWidth, + height: consts.maxHeight, + child: videoWidget, + ), + ), + ]); + }); } } @@ -390,29 +431,42 @@ class UnityVideoPlayerMediaKit extends UnityVideoPlayer { @override Future resetCrop() => crop(-1, -1, -1); + Rect viewportRect = Rect.zero; + /// Crops the current video into a box at the given row and column @override Future crop(int row, int col, int size) async { if (kIsWeb) return; - final player = mkPlayer.platform as dynamic; - // On linux, the mpv binaries used come from the distros (sudo apt install mpv ...) - // As of now (18 nov 2023), the "video-crop" parameter is not supported on - // most distros. In this case, there is the "vf=crop" parameter that does - // the same thing. "video-crop" is preferred on the other platforms because - // of its performance. - - if (row == -1 || col == -1 || size == -1) { - if (Platform.isLinux) { - await player.setProperty('vf', 'crop='); - } else { - await player.setProperty('video-crop', '0x0+0+0'); - } + + final reset = row == -1 || col == -1 || size == -1; + // final player = mkPlayer.platform as dynamic; + + final Future Function(Rect rect) crop; + if (softwareZoom) { + // On macOS, the mpv options don't seem to work properly. Because of this, + // software zoom is used instead. + crop = (rect) async { + viewportRect = rect; + }; + } else if (Platform.isLinux) { + // On linux, the mpv binaries used come from the distros (sudo apt install mpv ...) + // As of now (18 nov 2023), the "video-crop" parameter is not supported on + // most distros. In this case, there is the "vf=crop" parameter that does + // the same thing. "video-crop" is preferred on the other platforms because + // of its performance. + crop = _cropWithFilter; + } else { + crop = _cropWithoutFilter; + } + + if (reset) { + await crop(Rect.zero); _isCropped = false; } else if (width != null && height != null) { final tileWidth = maxSize.width / size; final tileHeight = maxSize.height / size; - final viewportRect = Rect.fromLTWH( + viewportRect = Rect.fromLTWH( col * tileWidth, row * tileHeight, tileWidth, @@ -420,31 +474,49 @@ class UnityVideoPlayerMediaKit extends UnityVideoPlayer { ); debugPrint( - 'Cropping | row=$row | col=$col | size=$maxSize | viewport=$viewportRect', + 'Cropping $softwareZoom | row=$row | col=$col | size=$maxSize | viewport=$viewportRect', ); - if (Platform.isLinux) { - await player.setProperty( - 'vf', - 'crop=' - '${viewportRect.width.toInt()}:' - '${viewportRect.height.toInt()}:' - '${viewportRect.left.toInt()}:' - '${viewportRect.top.toInt()}', - ); - } else { - await player.setProperty( - 'video-crop', - '${viewportRect.width.toInt()}x' - '${viewportRect.height.toInt()}+' - '${viewportRect.left.toInt()}+' - '${viewportRect.top.toInt()}', - ); - } + await crop(viewportRect); _isCropped = true; } } + Future _cropWithFilter(Rect viewportRect) async { + // Usage as dynamic is necessary because the property is not available on the + // web platform, and the compiler will complain about it. + final player = mkPlayer.platform as dynamic; + if (viewportRect.isEmpty) { + await player.setProperty('vf', 'crop='); + } else { + await player.setProperty( + 'vf', + 'crop=' + '${viewportRect.width.toInt()}:' + '${viewportRect.height.toInt()}:' + '${viewportRect.left.toInt()}:' + '${viewportRect.top.toInt()}', + ); + } + } + + Future _cropWithoutFilter(Rect viewportRect) async { + // Usage as dynamic is necessary because the property is not available on the + // web platform, and the compiler will complain about it. + final player = mkPlayer.platform as dynamic; + if (viewportRect.isEmpty) { + await player.setProperty('video-crop', '0x0+0+0'); + } else { + await player.setProperty( + 'video-crop', + '${viewportRect.width.toInt()}x' + '${viewportRect.height.toInt()}+' + '${viewportRect.left.toInt()}+' + '${viewportRect.top.toInt()}', + ); + } + } + @override bool get isCropped => _isCropped; @@ -456,8 +528,8 @@ class UnityVideoPlayerMediaKit extends UnityVideoPlayer { final platform = mkPlayer.platform as dynamic; await platform.unobserveProperty('estimated-vf-fps'); - await platform.unobserveProperty('dwidth'); - await platform.unobserveProperty('dheight'); + await platform.unobserveProperty('width'); + await platform.unobserveProperty('height'); } await _fpsStreamController.close(); await mkPlayer.dispose(); diff --git a/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart b/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart index 5c0637cd..a5b15285 100644 --- a/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart +++ b/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart @@ -165,6 +165,16 @@ class UnityVideoView extends StatefulWidget { /// * [Hero.tag], the identifier for this particular hero. final dynamic heroTag; + /// The matrix type to use for the video view. + /// + /// Defaults to [MatrixType.t16]. + final MatrixType matrixType; + + /// Whether to use software zoom. + /// + /// Defaults to `false`. + final bool softwareZoom; + /// Creates a new video view. const UnityVideoView({ super.key, @@ -174,6 +184,8 @@ class UnityVideoView extends StatefulWidget { this.videoBuilder, this.color = const Color(0xFF000000), this.heroTag, + this.matrixType = MatrixType.t16, + this.softwareZoom = false, }); static VideoViewInheritance of(BuildContext context) { @@ -289,6 +301,9 @@ abstract class UnityVideoPlayer with ChangeNotifier { VoidCallback? onReload; late LateVideoBehavior lateVideoBehavior; + MatrixType matrixType = MatrixType.t1; + bool softwareZoom = false; + /// Creates a new [UnityVideoPlayer] instance. /// /// The [quality] parameter is used to set the rendering resolution of the @@ -314,6 +329,8 @@ abstract class UnityVideoPlayer with ChangeNotifier { VoidCallback? onReload, String? title, LateVideoBehavior lateVideoBehavior = LateVideoBehavior.automatic, + MatrixType matrixType = MatrixType.t1, + bool softwareZoom = false, }) { return UnityVideoPlayerInterface.instance.createPlayer( width: quality?.resolution.width.toInt(), @@ -325,7 +342,9 @@ abstract class UnityVideoPlayer with ChangeNotifier { ..quality = quality ..fallbackUrl = fallbackUrl ..onReload = onReload - ..lateVideoBehavior = lateVideoBehavior; + ..lateVideoBehavior = lateVideoBehavior + ..matrixType = matrixType + ..softwareZoom = softwareZoom; } static const timerInterval = Duration(seconds: 6); @@ -540,3 +559,34 @@ abstract class UnityVideoPlayer with ChangeNotifier { super.dispose(); } } + +enum MatrixType { + t16(4), + t9(3), + t4(2), + t1(1); + + final int size; + + const MatrixType(this.size); + + @override + String toString() { + return switch (this) { + MatrixType.t16 => '4x4', + MatrixType.t9 => '3x3', + MatrixType.t4 => '2x2', + MatrixType.t1 => '1x1', + }; + } + + MatrixType get next { + return switch (this) { + MatrixType.t16 => MatrixType.t9, + MatrixType.t9 => MatrixType.t4, + MatrixType.t4 => MatrixType.t16, + // ideally, t1 is never reached + MatrixType.t1 => MatrixType.t16, + }; + } +}