From b9817e26a12ab695a060f55a3fd94ea14c8bbebb Mon Sep 17 00:00:00 2001 From: Predidit <34627277+Predidit@users.noreply.github.com> Date: Sun, 14 Jun 2026 18:48:57 +0800 Subject: [PATCH] draft --- media_kit/README.md | 1 + media_kit/lib/src/models/player_state.dart | 25 ++- media_kit/lib/src/models/player_stream.dart | 4 + .../lib/src/player/native/player/real.dart | 21 ++ media_kit/lib/src/player/platform_player.dart | 8 + media_kit/lib/src/player/web/player/real.dart | 3 + .../src/models/media/player_state_test.dart | 4 + media_kit/test/src/player/player_test.dart | 1 + media_kit_test/lib/main.dart | 16 ++ .../lib/tests/12.video_bitrate.dart | 185 ++++++++++++++++++ media_kit_test/test/widget_test.dart | 30 --- 11 files changed, 262 insertions(+), 36 deletions(-) create mode 100644 media_kit_test/lib/tests/12.video_bitrate.dart delete mode 100644 media_kit_test/test/widget_test.dart diff --git a/media_kit/README.md b/media_kit/README.md index a5fd5811c..2f057d2d2 100644 --- a/media_kit/README.md +++ b/media_kit/README.md @@ -466,6 +466,7 @@ The following state(s) are available as events: | `Stream` | `audioParams` | Audio parameters of the currently playing media source e.g. sample rate, channels, etc. | | `Stream` | `videoParams` | Video parameters of the currently playing media source e.g. width, height, rotation etc. | | `Stream` | `audioBitrate` | Audio bitrate of the currently playing media source. | +| `Stream` | `videoBitrate` | Video bitrate of the currently playing media source. | | `Stream` | `audioDevice` | Currently selected audio device. | | `Stream>` | `audioDevices` | Currently available audio devices. | | `Stream` | `track` | Currently selected video, audio and subtitle track. | diff --git a/media_kit/lib/src/models/player_state.dart b/media_kit/lib/src/models/player_state.dart index 3d081c8ec..5931c61b3 100644 --- a/media_kit/lib/src/models/player_state.dart +++ b/media_kit/lib/src/models/player_state.dart @@ -11,6 +11,8 @@ import 'package:media_kit/src/models/audio_params.dart'; import 'package:media_kit/src/models/video_params.dart'; import 'package:media_kit/src/models/playlist_mode.dart'; +const Object _undefined = Object(); + /// {@template player_state} /// /// PlayerState @@ -68,6 +70,9 @@ class PlayerState { /// Audio bitrate of the currently playing [Media]. final double? audioBitrate; + /// Video bitrate of the currently playing [Media]. + final double? videoBitrate; + /// Currently selected [AudioDevice]. final AudioDevice audioDevice; @@ -106,6 +111,7 @@ class PlayerState { this.audioParams = const AudioParams(), this.videoParams = const VideoParams(), this.audioBitrate, + this.videoBitrate, this.audioDevice = const AudioDevice('auto', ''), this.audioDevices = const [AudioDevice('auto', '')], this.track = const Track(), @@ -130,13 +136,14 @@ class PlayerState { PlaylistMode? playlistMode, AudioParams? audioParams, VideoParams? videoParams, - double? audioBitrate, + Object? audioBitrate = _undefined, + Object? videoBitrate = _undefined, AudioDevice? audioDevice, List? audioDevices, Track? track, Tracks? tracks, - int? width, - int? height, + Object? width = _undefined, + Object? height = _undefined, List? subtitle, }) { return PlayerState( @@ -154,13 +161,18 @@ class PlayerState { playlistMode: playlistMode ?? this.playlistMode, audioParams: audioParams ?? this.audioParams, videoParams: videoParams ?? this.videoParams, - audioBitrate: audioBitrate ?? this.audioBitrate, + audioBitrate: identical(audioBitrate, _undefined) + ? this.audioBitrate + : audioBitrate as double?, + videoBitrate: identical(videoBitrate, _undefined) + ? this.videoBitrate + : videoBitrate as double?, audioDevice: audioDevice ?? this.audioDevice, audioDevices: audioDevices ?? this.audioDevices, track: track ?? this.track, tracks: tracks ?? this.tracks, - width: width ?? this.width, - height: height ?? this.height, + width: identical(width, _undefined) ? this.width : width as int?, + height: identical(height, _undefined) ? this.height : height as int?, subtitle: subtitle ?? this.subtitle, ); } @@ -182,6 +194,7 @@ class PlayerState { 'audioParams: $audioParams, ' 'videoParams: $videoParams, ' 'audioBitrate: $audioBitrate, ' + 'videoBitrate: $videoBitrate, ' 'audioDevice: $audioDevice, ' 'audioDevices: $audioDevices, ' 'track: $track, ' diff --git a/media_kit/lib/src/models/player_stream.dart b/media_kit/lib/src/models/player_stream.dart index 6724acad8..8e5f3e86c 100644 --- a/media_kit/lib/src/models/player_stream.dart +++ b/media_kit/lib/src/models/player_stream.dart @@ -69,6 +69,9 @@ class PlayerStream { /// Audio bitrate of the currently playing [Media]. final Stream audioBitrate; + /// Video bitrate of the currently playing [Media]. + final Stream videoBitrate; + /// Currently selected [AudioDevice]s. final Stream audioDevice; @@ -113,6 +116,7 @@ class PlayerStream { this.audioParams, this.videoParams, this.audioBitrate, + this.videoBitrate, this.audioDevice, this.audioDevices, this.track, diff --git a/media_kit/lib/src/player/native/player/real.dart b/media_kit/lib/src/player/native/player/real.dart index c94235cc6..5363cf541 100644 --- a/media_kit/lib/src/player/native/player/real.dart +++ b/media_kit/lib/src/player/native/player/real.dart @@ -317,6 +317,9 @@ class NativePlayer extends PlatformPlayer { // if (!audioBitrateController.isClosed) { // audioBitrateController.add(null); // } + if (!videoBitrateController.isClosed) { + videoBitrateController.add(null); + } // if (!audioDeviceController.isClosed) { // audioDeviceController.add(AudioDevice.auto()); // } @@ -1696,6 +1699,23 @@ class NativePlayer extends PlatformPlayer { } } } + if (prop.ref.name.cast().toDartString() == 'video-bitrate' && + prop.ref.format == generated.mpv_format.MPV_FORMAT_DOUBLE) { + if (state.playlist.index < state.playlist.medias.length && + state.playlist.index >= 0) { + final bitrate = prop.ref.data.cast().value; + if (!videoBitrateController.isClosed && + bitrate != state.videoBitrate) { + videoBitrateController.add(bitrate); + state = state.copyWith(videoBitrate: bitrate); + } + } else { + if (!videoBitrateController.isClosed) { + videoBitrateController.add(null); + state = state.copyWith(videoBitrate: null); + } + } + } if (prop.ref.name.cast().toDartString() == 'track-list' && prop.ref.format == generated.mpv_format.MPV_FORMAT_NODE) { final value = prop.ref.data.cast(); @@ -2454,6 +2474,7 @@ class NativePlayer extends PlatformPlayer { 'cache-buffering-state': generated.mpv_format.MPV_FORMAT_DOUBLE, 'audio-params': generated.mpv_format.MPV_FORMAT_NODE, 'audio-bitrate': generated.mpv_format.MPV_FORMAT_DOUBLE, + 'video-bitrate': generated.mpv_format.MPV_FORMAT_DOUBLE, 'audio-device': generated.mpv_format.MPV_FORMAT_NODE, 'audio-device-list': generated.mpv_format.MPV_FORMAT_NODE, 'video-params': generated.mpv_format.MPV_FORMAT_NODE, diff --git a/media_kit/lib/src/player/platform_player.dart b/media_kit/lib/src/player/platform_player.dart index b20650f20..d78ff8f6b 100644 --- a/media_kit/lib/src/player/platform_player.dart +++ b/media_kit/lib/src/player/platform_player.dart @@ -86,6 +86,9 @@ abstract class PlatformPlayer { audioBitrateController.stream.distinct( (previous, current) => previous == current, ), + videoBitrateController.stream.distinct( + (previous, current) => previous == current, + ), audioDeviceController.stream.distinct( (previous, current) => previous == current, ), @@ -133,6 +136,7 @@ abstract class PlatformPlayer { audioParamsController.close(), videoParamsController.close(), audioBitrateController.close(), + videoBitrateController.close(), audioDeviceController.close(), audioDevicesController.close(), trackController.close(), @@ -371,6 +375,10 @@ abstract class PlatformPlayer { final StreamController audioBitrateController = StreamController.broadcast(); + @protected + final StreamController videoBitrateController = + StreamController.broadcast(); + @protected final StreamController audioDeviceController = StreamController.broadcast(); diff --git a/media_kit/lib/src/player/web/player/real.dart b/media_kit/lib/src/player/web/player/real.dart index b4d5d0d1e..9c1226135 100644 --- a/media_kit/lib/src/player/web/player/real.dart +++ b/media_kit/lib/src/player/web/player/real.dart @@ -510,6 +510,9 @@ class WebPlayer extends PlatformPlayer { if (!audioBitrateController.isClosed) { audioBitrateController.add(null); } + if (!videoBitrateController.isClosed) { + videoBitrateController.add(null); + } // if (!audioDeviceController.isClosed) { // audioDeviceController.add(AudioDevice.auto()); // } diff --git a/media_kit/test/src/models/media/player_state_test.dart b/media_kit/test/src/models/media/player_state_test.dart index b8869daad..b231256d7 100644 --- a/media_kit/test/src/models/media/player_state_test.dart +++ b/media_kit/test/src/models/media/player_state_test.dart @@ -71,6 +71,10 @@ void main() { state.audioBitrate, isNull, ); + expect( + state.videoBitrate, + isNull, + ); expect(state.videoParams.pixelformat, isNull); expect(state.videoParams.hwPixelformat, isNull); expect(state.videoParams.w, isNull); diff --git a/media_kit/test/src/player/player_test.dart b/media_kit/test/src/player/player_test.dart index 6ffb87f1e..30e711179 100644 --- a/media_kit/test/src/player/player_test.dart +++ b/media_kit/test/src/player/player_test.dart @@ -2792,6 +2792,7 @@ void main() { expect(player.state.audioParams, equals(const AudioParams())); expect(player.state.videoParams, equals(const VideoParams())); expect(player.state.audioBitrate, equals(null)); + expect(player.state.videoBitrate, equals(null)); expect(player.state.track, equals(const Track())); expect(player.state.tracks, equals(const Tracks())); expect(player.state.width, equals(null)); diff --git a/media_kit_test/lib/main.dart b/media_kit_test/lib/main.dart index 3f24a173c..58c856089 100644 --- a/media_kit_test/lib/main.dart +++ b/media_kit_test/lib/main.dart @@ -17,6 +17,7 @@ import 'tests/08.screenshot.dart'; import 'tests/09.seamless.dart'; import 'tests/10.programmatic_fullscreen.dart'; import 'tests/11.video_view_parameters.dart'; +import 'tests/12.video_bitrate.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -246,6 +247,21 @@ class PrimaryScreen extends StatelessWidget { ); }, ), + ListTile( + title: const Text( + 'video_bitrate.dart', + style: TextStyle(fontSize: 14.0), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const VideoBitrateScreen(), + ), + ); + }, + ), ], ), ); diff --git a/media_kit_test/lib/tests/12.video_bitrate.dart b/media_kit_test/lib/tests/12.video_bitrate.dart new file mode 100644 index 000000000..bde8951f2 --- /dev/null +++ b/media_kit_test/lib/tests/12.video_bitrate.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:media_kit_video/media_kit_video.dart'; + +import '../common/globals.dart'; +import '../common/sources/sources.dart'; +import '../common/widgets.dart'; + +class VideoBitrateScreen extends StatefulWidget { + const VideoBitrateScreen({super.key}); + + @override + State createState() => _VideoBitrateScreenState(); +} + +class _VideoBitrateScreenState extends State { + late final Player player = Player(); + late final VideoController controller = VideoController( + player, + configuration: configuration.value, + ); + + @override + void initState() { + super.initState(); + player.open(Media(sources[0])); + player.stream.error.listen((error) => debugPrint(error)); + } + + @override + void dispose() { + player.dispose(); + super.dispose(); + } + + List get items => [ + _BitrateTile( + title: 'Audio bitrate', + initialData: player.state.audioBitrate, + stream: player.stream.audioBitrate, + ), + _BitrateTile( + title: 'Video bitrate', + initialData: player.state.videoBitrate, + stream: player.stream.videoBitrate, + ), + const Divider(height: 1.0), + for (int i = 0; i < sources.length; i++) + ListTile( + title: Text( + 'Video $i', + style: const TextStyle( + fontSize: 14.0, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + onTap: () { + player.open(Media(sources[i])); + }, + ), + ]; + + @override + Widget build(BuildContext context) { + final horizontal = + MediaQuery.of(context).size.width > MediaQuery.of(context).size.height; + return Scaffold( + appBar: AppBar( + title: const Text('package:media_kit'), + ), + floatingActionButton: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FloatingActionButton( + heroTag: 'file', + tooltip: 'Open [File]', + onPressed: () => showFilePicker(context, player), + child: const Icon(Icons.file_open), + ), + const SizedBox(width: 16.0), + FloatingActionButton( + heroTag: 'uri', + tooltip: 'Open [Uri]', + onPressed: () => showURIPicker(context, player), + child: const Icon(Icons.link), + ), + ], + ), + body: SizedBox.expand( + child: horizontal + ? Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + flex: 3, + child: Container( + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Card( + clipBehavior: Clip.antiAlias, + margin: const EdgeInsets.all(32.0), + child: Video( + controller: controller, + ), + ), + ), + const SizedBox(height: 32.0), + ], + ), + ), + ), + const VerticalDivider(width: 1.0, thickness: 1.0), + Expanded( + flex: 1, + child: ListView( + children: items, + ), + ), + ], + ) + : ListView( + children: [ + Video( + controller: controller, + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.width * 9.0 / 16.0, + ), + ...items, + ], + ), + ), + ); + } +} + +class _BitrateTile extends StatelessWidget { + final String title; + final double? initialData; + final Stream stream; + + const _BitrateTile({ + required this.title, + required this.initialData, + required this.stream, + }); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + initialData: initialData, + stream: stream, + builder: (context, snapshot) { + return ListTile( + title: Text( + title, + style: const TextStyle(fontSize: 14.0), + ), + subtitle: Text( + _formatBitrate(snapshot.data), + style: const TextStyle(fontSize: 20.0), + ), + ); + }, + ); + } + + String _formatBitrate(double? value) { + if (value == null || !value.isFinite || value <= 0.0) { + return 'N/A'; + } + if (value >= 1000000.0) { + return '${(value / 1000000.0).toStringAsFixed(2)} Mbps'; + } + if (value >= 1000.0) { + return '${(value / 1000.0).toStringAsFixed(0)} Kbps'; + } + return '${value.toStringAsFixed(0)} bps'; + } +} diff --git a/media_kit_test/test/widget_test.dart b/media_kit_test/test/widget_test.dart deleted file mode 100644 index d010f762e..000000000 --- a/media_kit_test/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:media_kit_test/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}