From 4540ac7dda07c2135392a5eb2d0dbd3c3fa9ae14 Mon Sep 17 00:00:00 2001 From: nateshmbhat Date: Sat, 27 Dec 2025 20:50:00 +0530 Subject: [PATCH 1/7] Adds video track selection feature. implemented for android --- .../video_player/video_player/CHANGELOG.md | 1 + .../video_player/example/lib/main.dart | 15 + .../example/lib/video_tracks_demo.dart | 458 +++++++++ .../video_player/lib/video_player.dart | 209 +++- .../video_player/video_player/pubspec.yaml | 12 +- .../video_player/test/video_player_test.dart | 455 ++++----- .../video_player_android/CHANGELOG.md | 4 + .../videoplayer/ExoPlayerEventListener.java | 31 +- .../plugins/videoplayer/VideoPlayer.java | 166 +++ .../videoplayer/VideoPlayerCallbacks.java | 2 + .../VideoPlayerEventCallbacks.java | 5 + .../flutter/plugins/videoplayer/Messages.kt | 941 ++++++++++-------- .../VideoPlayerEventCallbacksTest.java | 24 + .../video_player_android/example/pubspec.yaml | 3 +- .../lib/src/android_video_player.dart | 145 ++- .../lib/src/messages.g.dart | 612 ++++++++---- .../pigeons/messages.dart | 53 +- .../video_player_android/pubspec.yaml | 3 +- .../test/android_video_player_test.dart | 467 +++++---- .../test/android_video_player_test.mocks.dart | 36 + .../CHANGELOG.md | 1 + .../lib/video_player_platform_interface.dart | 150 ++- .../video_player_platform_interface_test.dart | 120 +++ 23 files changed, 2810 insertions(+), 1103 deletions(-) create mode 100644 packages/video_player/video_player/example/lib/video_tracks_demo.dart diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 3524be91edc..74d50ef0a7d 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -5,6 +5,7 @@ versions of the endorsed platform implementations. * Applications built with older versions of Flutter will continue to use compatible versions of the platform implementations. +* Adds video track (quality) selection support via `getVideoTracks()`, `selectVideoTrack()`, and `isVideoTrackSupportAvailable()` methods. ## 2.10.1 diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index d47a5abc601..5b08c984d24 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -11,6 +11,8 @@ library; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; +import 'video_tracks_demo.dart'; + void main() { runApp(MaterialApp(home: _App())); } @@ -25,6 +27,19 @@ class _App extends StatelessWidget { appBar: AppBar( title: const Text('Video player example'), actions: [ + IconButton( + key: const ValueKey('video_tracks_demo'), + icon: const Icon(Icons.high_quality), + tooltip: 'Video Tracks Demo', + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => const VideoTracksDemo(), + ), + ); + }, + ), IconButton( key: const ValueKey('push_tab'), icon: const Icon(Icons.navigation), diff --git a/packages/video_player/video_player/example/lib/video_tracks_demo.dart b/packages/video_player/video_player/example/lib/video_tracks_demo.dart new file mode 100644 index 00000000000..811a52b7616 --- /dev/null +++ b/packages/video_player/video_player/example/lib/video_tracks_demo.dart @@ -0,0 +1,458 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +/// A demo page that showcases video track (quality) selection functionality. +class VideoTracksDemo extends StatefulWidget { + /// Creates a VideoTracksDemo widget. + const VideoTracksDemo({super.key}); + + @override + State createState() => _VideoTracksDemoState(); +} + +class _VideoTracksDemoState extends State { + VideoPlayerController? _controller; + List _videoTracks = []; + bool _isLoading = false; + String? _error; + bool _isAutoQuality = true; + + // Track previous state to detect relevant changes + bool _wasPlaying = false; + bool _wasInitialized = false; + + // Sample video URLs with multiple video tracks (HLS streams) + static const List _sampleVideos = [ + 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8', + 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8', + 'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4', + ]; + + int _selectedVideoIndex = 0; + + @override + void initState() { + super.initState(); + _initializeVideo(); + } + + Future _initializeVideo() async { + setState(() { + _isLoading = true; + _error = null; + _isAutoQuality = true; + }); + + try { + await _controller?.dispose(); + + final VideoPlayerController controller = VideoPlayerController.networkUrl( + Uri.parse(_sampleVideos[_selectedVideoIndex]), + ); + _controller = controller; + + await controller.initialize(); + + // Add listener for video player state changes + _controller!.addListener(_onVideoPlayerValueChanged); + + // Initialize tracking variables + _wasPlaying = _controller!.value.isPlaying; + _wasInitialized = _controller!.value.isInitialized; + + // Get video tracks after initialization + await _loadVideoTracks(); + if (!mounted) { + return; + } + setState(() { + _isLoading = false; + }); + } catch (e) { + if (!mounted) { + return; + } + setState(() { + _error = 'Failed to initialize video: $e'; + _isLoading = false; + }); + } + } + + Future _loadVideoTracks() async { + final VideoPlayerController? controller = _controller; + if (controller == null || !controller.value.isInitialized) { + return; + } + + // Check if video track selection is supported + if (!controller.isVideoTrackSupportAvailable()) { + if (!mounted) { + return; + } + setState(() { + _error = 'Video track selection is not supported on this platform.'; + _videoTracks = []; + }); + return; + } + + try { + final List tracks = await controller.getVideoTracks(); + if (!mounted) { + return; + } + setState(() { + _videoTracks = tracks; + }); + } catch (e) { + if (!mounted) { + return; + } + setState(() { + _error = 'Failed to load video tracks: $e'; + }); + } + } + + Future _selectVideoTrack(VideoTrack? track) async { + final VideoPlayerController? controller = _controller; + if (controller == null) { + return; + } + + try { + await controller.selectVideoTrack(track); + + setState(() { + _isAutoQuality = track == null; + }); + + // Reload tracks to update selection status + await _loadVideoTracks(); + + if (!mounted) { + return; + } + final String message = track == null + ? 'Switched to automatic quality' + : 'Selected video track: ${_getTrackLabel(track)}'; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); + } catch (e) { + if (!mounted) { + return; + } + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Failed to select video track: $e'))); + } + } + + String _getTrackLabel(VideoTrack track) { + if (track.label.isNotEmpty) { + return track.label; + } + if (track.height != null && track.width != null) { + return '${track.width}x${track.height}'; + } + if (track.height != null) { + return '${track.height}p'; + } + return 'Track ${track.id}'; + } + + String _formatBitrate(int? bitrate) { + if (bitrate == null) { + return 'Unknown'; + } + if (bitrate >= 1000000) { + return '${(bitrate / 1000000).toStringAsFixed(2)} Mbps'; + } + if (bitrate >= 1000) { + return '${(bitrate / 1000).toStringAsFixed(0)} Kbps'; + } + return '$bitrate bps'; + } + + void _onVideoPlayerValueChanged() { + if (!mounted || _controller == null) { + return; + } + + final VideoPlayerValue currentValue = _controller!.value; + bool shouldUpdate = false; + + // Check for relevant state changes that affect UI + if (currentValue.isPlaying != _wasPlaying) { + _wasPlaying = currentValue.isPlaying; + shouldUpdate = true; + } + + if (currentValue.isInitialized != _wasInitialized) { + _wasInitialized = currentValue.isInitialized; + shouldUpdate = true; + } + + // Only call setState if there are relevant changes + if (shouldUpdate) { + setState(() {}); + } + } + + @override + void dispose() { + _controller?.removeListener(_onVideoPlayerValueChanged); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Video Tracks Demo'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + ), + body: Column( + children: [ + // Video selection dropdown + Padding( + padding: const EdgeInsets.all(16.0), + child: DropdownMenu( + initialSelection: _selectedVideoIndex, + label: const Text('Select Video'), + inputDecorationTheme: const InputDecorationTheme( + border: OutlineInputBorder(), + ), + dropdownMenuEntries: _sampleVideos.indexed.map(((int, String) record) { + final (int index, String url) = record; + final String label = url.contains('.m3u8') + ? 'HLS Stream ${index + 1}' + : 'MP4 Video ${index + 1}'; + return DropdownMenuEntry(value: index, label: label); + }).toList(), + onSelected: (int? value) { + if (value != null && value != _selectedVideoIndex) { + setState(() { + _selectedVideoIndex = value; + }); + _initializeVideo(); + } + }, + ), + ), + + // Video player + Expanded( + flex: 2, + child: ColoredBox(color: Colors.black, child: _buildVideoPlayer()), + ), + + // Video tracks list + Expanded(flex: 3, child: _buildVideoTracksList()), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: _loadVideoTracks, + tooltip: 'Refresh Video Tracks', + child: const Icon(Icons.refresh), + ), + ); + } + + Widget _buildVideoPlayer() { + if (_isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (_error != null && _controller?.value.isInitialized != true) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error, size: 48, color: Colors.red[300]), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + _error!, + style: const TextStyle(color: Colors.white), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 16), + ElevatedButton(onPressed: _initializeVideo, child: const Text('Retry')), + ], + ), + ); + } + + final VideoPlayerController? controller = _controller; + if (controller?.value.isInitialized ?? false) { + return Stack( + alignment: Alignment.center, + children: [ + AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller), + ), + _buildPlayPauseButton(), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: VideoProgressIndicator(controller, allowScrubbing: true), + ), + ], + ); + } + + return const Center( + child: Text('No video loaded', style: TextStyle(color: Colors.white)), + ); + } + + Widget _buildPlayPauseButton() { + final VideoPlayerController? controller = _controller; + if (controller == null) { + return const SizedBox.shrink(); + } + + return Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(30), + ), + child: IconButton( + iconSize: 48, + color: Colors.white, + onPressed: () { + if (controller.value.isPlaying) { + controller.pause(); + } else { + controller.play(); + } + }, + icon: Icon(controller.value.isPlaying ? Icons.pause : Icons.play_arrow), + ), + ); + } + + Widget _buildVideoTracksList() { + return Container( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.high_quality), + const SizedBox(width: 8), + Text( + 'Video Tracks (${_videoTracks.length})', + style: Theme.of(context).textTheme.headlineSmall, + ), + ], + ), + const SizedBox(height: 8), + + // Auto quality option + Card( + margin: const EdgeInsets.only(bottom: 8.0), + child: ListTile( + leading: CircleAvatar( + backgroundColor: _isAutoQuality ? Colors.blue : Colors.grey, + child: Icon( + _isAutoQuality ? Icons.check : Icons.auto_awesome, + color: Colors.white, + ), + ), + title: Text( + 'Automatic Quality', + style: TextStyle( + fontWeight: _isAutoQuality ? FontWeight.bold : FontWeight.normal, + ), + ), + subtitle: const Text('Let the player choose the best quality'), + trailing: _isAutoQuality + ? const Icon(Icons.radio_button_checked, color: Colors.blue) + : const Icon(Icons.radio_button_unchecked), + onTap: _isAutoQuality ? null : () => _selectVideoTrack(null), + ), + ), + + const SizedBox(height: 8), + + if (_videoTracks.isEmpty && _error == null) + const Expanded( + child: Center( + child: Text( + 'No video tracks available.\nTry loading an HLS stream with multiple quality levels.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ), + ) + else if (_error != null && _controller?.value.isInitialized == true) + Expanded( + child: Center( + child: Text( + _error!, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16, color: Colors.orange), + ), + ), + ) + else + Expanded( + child: ListView.builder( + itemCount: _videoTracks.length, + itemBuilder: (BuildContext context, int index) { + final VideoTrack track = _videoTracks[index]; + return _buildVideoTrackTile(track); + }, + ), + ), + ], + ), + ); + } + + Widget _buildVideoTrackTile(VideoTrack track) { + final bool isSelected = track.isSelected && !_isAutoQuality; + + return Card( + margin: const EdgeInsets.only(bottom: 8.0), + child: ListTile( + leading: CircleAvatar( + backgroundColor: isSelected ? Colors.green : Colors.grey, + child: Icon(isSelected ? Icons.check : Icons.hd, color: Colors.white), + ), + title: Text( + _getTrackLabel(track), + style: TextStyle(fontWeight: isSelected ? FontWeight.bold : FontWeight.normal), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('ID: ${track.id}'), + if (track.width != null && track.height != null) + Text('Resolution: ${track.width}x${track.height}'), + Text('Bitrate: ${_formatBitrate(track.bitrate)}'), + if (track.frameRate != null) + Text('Frame Rate: ${track.frameRate!.toStringAsFixed(2)} fps'), + if (track.codec != null) Text('Codec: ${track.codec}'), + ], + ), + trailing: isSelected + ? const Icon(Icons.radio_button_checked, color: Colors.green) + : const Icon(Icons.radio_button_unchecked), + onTap: isSelected ? null : () => _selectVideoTrack(track), + ), + ); + } +} diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index f0589cb4686..0ba94b6c747 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -10,6 +10,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; +// Import platform VideoTrack for internal conversion +import 'package:video_player_platform_interface/video_player_platform_interface.dart' + as platform_interface; import 'src/closed_caption_file.dart'; @@ -464,9 +467,7 @@ class VideoPlayerController extends ValueNotifier { ); if (videoPlayerOptions?.mixWithOthers != null) { - await _videoPlayerPlatform.setMixWithOthers( - videoPlayerOptions!.mixWithOthers, - ); + await _videoPlayerPlatform.setMixWithOthers(videoPlayerOptions!.mixWithOthers); } _playerId = @@ -527,10 +528,7 @@ class VideoPlayerController extends ValueNotifier { value = value.copyWith(isBuffering: false); case VideoEventType.isPlayingStateUpdate: if (event.isPlaying ?? false) { - value = value.copyWith( - isPlaying: event.isPlaying, - isCompleted: false, - ); + value = value.copyWith(isPlaying: event.isPlaying, isCompleted: false); } else { value = value.copyWith(isPlaying: event.isPlaying); } @@ -621,9 +619,7 @@ class VideoPlayerController extends ValueNotifier { await _videoPlayerPlatform.play(_playerId); _timer?.cancel(); - _timer = Timer.periodic(const Duration(milliseconds: 100), ( - Timer timer, - ) async { + _timer = Timer.periodic(const Duration(milliseconds: 100), (Timer timer) async { if (_isDisposed) { return; } @@ -745,10 +741,7 @@ class VideoPlayerController extends ValueNotifier { /// * >0: The caption will have a negative offset. So you will get caption text from the past. /// * <0: The caption will have a positive offset. So you will get caption text from the future. void setCaptionOffset(Duration offset) { - value = value.copyWith( - captionOffset: offset, - caption: _getCaptionAt(value.position), - ); + value = value.copyWith(captionOffset: offset, caption: _getCaptionAt(value.position)); } /// The closed caption based on the current [position] in the video. @@ -782,9 +775,7 @@ class VideoPlayerController extends ValueNotifier { /// Sets a closed caption file. /// /// If [closedCaptionFile] is null, closed captions will be removed. - Future setClosedCaptionFile( - Future? closedCaptionFile, - ) async { + Future setClosedCaptionFile(Future? closedCaptionFile) async { await _updateClosedCaptionWithFuture(closedCaptionFile); _closedCaptionFileFuture = closedCaptionFile; } @@ -820,6 +811,70 @@ class VideoPlayerController extends ValueNotifier { } bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized; + + /// Gets the available video tracks (quality variants) for the video. + /// + /// Returns a list of [VideoTrack] objects representing the available + /// video quality variants. For HLS/DASH streams, this returns the different + /// quality levels available. For regular videos, this may return a single + /// track or an empty list. + /// + /// Note: On iOS 13-14, this returns an empty list as the AVAssetVariant API + /// requires iOS 15+. On web, this throws an [UnimplementedError]. + /// + /// Check [isVideoTrackSupportAvailable] before calling this method to ensure + /// the platform supports video track selection. + Future> getVideoTracks() async { + if (_isDisposedOrNotInitialized) { + return []; + } + final List platformTracks = await _videoPlayerPlatform + .getVideoTracks(_playerId); + return platformTracks + .map((platform_interface.VideoTrack track) => VideoTrack._fromPlatform(track)) + .toList(); + } + + /// Selects which video track (quality variant) is chosen for playback. + /// + /// Pass a [VideoTrack] to select a specific quality. + /// Pass `null` to enable automatic quality selection (adaptive streaming). + /// + /// On iOS, this sets `preferredPeakBitRate` on the AVPlayerItem. + /// On Android, this uses ExoPlayer's track selection override. + /// On web, this throws an [UnimplementedError]. + /// + /// Check [isVideoTrackSupportAvailable] before calling this method to ensure + /// the platform supports video track selection. + Future selectVideoTrack(VideoTrack? track) async { + if (_isDisposedOrNotInitialized) { + return; + } + // Convert app-facing VideoTrack to platform interface VideoTrack + final platform_interface.VideoTrack? platformTrack = track != null + ? platform_interface.VideoTrack( + id: track.id, + isSelected: track.isSelected, + label: track.label.isEmpty ? null : track.label, + bitrate: track.bitrate, + width: track.width, + height: track.height, + frameRate: track.frameRate, + codec: track.codec, + ) + : null; + await _videoPlayerPlatform.selectVideoTrack(_playerId, platformTrack); + } + + /// Returns whether video track selection is supported on this platform. + /// + /// Returns `true` on Android and iOS, `false` on web. + /// + /// Use this to check before calling [getVideoTracks] or [selectVideoTrack] + /// to avoid [UnimplementedError] exceptions on unsupported platforms. + bool isVideoTrackSupportAvailable() { + return _videoPlayerPlatform.isVideoTrackSupportAvailable(); + } } class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { @@ -971,11 +1026,7 @@ class VideoScrubber extends StatefulWidget { /// /// [controller] is the [VideoPlayerController] that will be controlled by /// this scrubber. - const VideoScrubber({ - super.key, - required this.child, - required this.controller, - }); + const VideoScrubber({super.key, required this.child, required this.controller}); /// The widget that will be displayed inside the gesture detector. final Widget child; @@ -1145,10 +1196,7 @@ class _VideoProgressIndicatorState extends State { child: progressIndicator, ); if (widget.allowScrubbing) { - return VideoScrubber( - controller: controller, - child: paddedProgressIndicator, - ); + return VideoScrubber(controller: controller, child: paddedProgressIndicator); } else { return paddedProgressIndicator; } @@ -1200,9 +1248,7 @@ class ClosedCaption extends StatelessWidget { final TextStyle effectiveTextStyle = textStyle ?? - DefaultTextStyle.of( - context, - ).style.copyWith(fontSize: 36.0, color: Colors.white); + DefaultTextStyle.of(context).style.copyWith(fontSize: 36.0, color: Colors.white); return Align( alignment: Alignment.bottomCenter, @@ -1222,3 +1268,108 @@ class ClosedCaption extends StatelessWidget { ); } } + +/// Represents a video track (quality variant) in a video with its metadata. +/// +/// For HLS/DASH streams, each [VideoTrack] represents a different quality +/// level (e.g., 1080p, 720p, 480p). For regular videos, there may be only +/// one track or none available. +@immutable +class VideoTrack { + /// Constructs an instance of [VideoTrack]. + const VideoTrack({ + required this.id, + required this.isSelected, + this.label = '', + this.bitrate, + this.width, + this.height, + this.frameRate, + this.codec, + }); + + /// Creates a [VideoTrack] from a platform interface [VideoTrack]. + factory VideoTrack._fromPlatform(platform_interface.VideoTrack track) { + return VideoTrack( + id: track.id, + isSelected: track.isSelected, + label: track.label ?? '', + bitrate: track.bitrate, + width: track.width, + height: track.height, + frameRate: track.frameRate, + codec: track.codec, + ); + } + + /// Unique identifier for the video track. + /// + /// The format is platform-specific: + /// - Android: `"{groupIndex}_{trackIndex}"` (e.g., `"0_2"`) + /// - iOS: `"variant_{bitrate}"` for HLS, `"asset_{trackID}"` for regular videos + final String id; + + /// Whether this track is currently selected. + final bool isSelected; + + /// Human-readable label for the track (e.g., "1080p", "720p"). + /// + /// Defaults to an empty string if not available from the platform. + final String label; + + /// Bitrate of the video track in bits per second. + /// + /// May be null if not available from the platform. + final int? bitrate; + + /// Video width in pixels. + /// + /// May be null if not available from the platform. + final int? width; + + /// Video height in pixels. + /// + /// May be null if not available from the platform. + final int? height; + + /// Frame rate in frames per second. + /// + /// May be null if not available from the platform. + final double? frameRate; + + /// Video codec used (e.g., "avc1", "hevc", "vp9"). + /// + /// May be null if not available from the platform. + final String? codec; + + @override + bool operator ==(Object other) { + return identical(this, other) || + other is VideoTrack && + runtimeType == other.runtimeType && + id == other.id && + isSelected == other.isSelected && + label == other.label && + bitrate == other.bitrate && + width == other.width && + height == other.height && + frameRate == other.frameRate && + codec == other.codec; + } + + @override + int get hashCode => + Object.hash(id, isSelected, label, bitrate, width, height, frameRate, codec); + + @override + String toString() => + 'VideoTrack(' + 'id: $id, ' + 'isSelected: $isSelected, ' + 'label: $label, ' + 'bitrate: $bitrate, ' + 'width: $width, ' + 'height: $height, ' + 'frameRate: $frameRate, ' + 'codec: $codec)'; +} diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index a71e4532339..61cb3cb6ba6 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -25,10 +25,14 @@ dependencies: flutter: sdk: flutter html: ^0.15.0 - video_player_android: ^2.8.1 - video_player_avfoundation: ^2.7.0 - video_player_platform_interface: ^6.3.0 - video_player_web: ^2.1.0 + video_player_android: + path: ../video_player_android + video_player_avfoundation: + path: ../video_player_avfoundation + video_player_platform_interface: + path: ../video_player_platform_interface + video_player_web: + path: ../video_player_web dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index ea565bd9073..c0b3c4c277f 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -10,7 +10,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player/video_player.dart'; -import 'package:video_player_platform_interface/video_player_platform_interface.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart' + hide VideoTrack; +import 'package:video_player_platform_interface/video_player_platform_interface.dart' + as platform_interface + show VideoTrack; const String _localhost = 'https://127.0.0.1'; final Uri _localhostUri = Uri.parse(_localhost); @@ -81,13 +85,19 @@ class FakeController extends ValueNotifier void setCaptionOffset(Duration delay) {} @override - Future setClosedCaptionFile( - Future? closedCaptionFile, - ) async {} + Future setClosedCaptionFile(Future? closedCaptionFile) async {} + + @override + Future> getVideoTracks() async => []; + + @override + Future selectVideoTrack(VideoTrack? track) async {} + + @override + bool isVideoTrackSupportAvailable() => false; } -Future _loadClosedCaption() async => - _FakeClosedCaptionFile(); +Future _loadClosedCaption() async => _FakeClosedCaptionFile(); class _FakeClosedCaptionFile extends ClosedCaptionFile { @override @@ -122,13 +132,9 @@ void main() { required bool shouldPlayInBackground, }) { expect(controller.value.isPlaying, true); - WidgetsBinding.instance.handleAppLifecycleStateChanged( - AppLifecycleState.paused, - ); + WidgetsBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.paused); expect(controller.value.isPlaying, shouldPlayInBackground); - WidgetsBinding.instance.handleAppLifecycleStateChanged( - AppLifecycleState.resumed, - ); + WidgetsBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.resumed); expect(controller.value.isPlaying, true); } @@ -172,38 +178,37 @@ void main() { ); }); - testWidgets( - 'VideoPlayer still listens for controller changes when reparented', - (WidgetTester tester) async { - final controller = FakeController(); - addTearDown(controller.dispose); - final GlobalKey videoKey = GlobalKey(); - final Widget videoPlayer = KeyedSubtree( - key: videoKey, - child: VideoPlayer(controller), - ); + testWidgets('VideoPlayer still listens for controller changes when reparented', ( + WidgetTester tester, + ) async { + final controller = FakeController(); + addTearDown(controller.dispose); + final GlobalKey videoKey = GlobalKey(); + final Widget videoPlayer = KeyedSubtree( + key: videoKey, + child: VideoPlayer(controller), + ); - await tester.pumpWidget(videoPlayer); - expect(find.byType(Texture), findsNothing); + await tester.pumpWidget(videoPlayer); + expect(find.byType(Texture), findsNothing); - // The VideoPlayer is reparented in the widget tree, before the - // underlying player is initialized. - await tester.pumpWidget(SizedBox(child: videoPlayer)); - controller.playerId = 321; - controller.value = controller.value.copyWith( - duration: const Duration(milliseconds: 100), - isInitialized: true, - ); + // The VideoPlayer is reparented in the widget tree, before the + // underlying player is initialized. + await tester.pumpWidget(SizedBox(child: videoPlayer)); + controller.playerId = 321; + controller.value = controller.value.copyWith( + duration: const Duration(milliseconds: 100), + isInitialized: true, + ); - await tester.pump(); - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Texture && widget.textureId == 321, - ), - findsOneWidget, - ); - }, - ); + await tester.pump(); + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Texture && widget.textureId == 321, + ), + findsOneWidget, + ); + }); testWidgets( 'VideoProgressIndicator still listens for controller changes after reparenting', @@ -224,9 +229,7 @@ void main() { ); await tester.pumpWidget(MaterialApp(home: progressIndicator)); await tester.pump(); - await tester.pumpWidget( - MaterialApp(home: SizedBox(child: progressIndicator)), - ); + await tester.pumpWidget(MaterialApp(home: SizedBox(child: progressIndicator))); expect((key.currentContext! as Element).dirty, isFalse); // Verify that changing value dirties the widget tree. controller.value = controller.value.copyWith( @@ -246,16 +249,12 @@ void main() { isInitialized: true, ); await tester.pumpWidget( - MaterialApp( - home: VideoProgressIndicator(controller, allowScrubbing: false), - ), + MaterialApp(home: VideoProgressIndicator(controller, allowScrubbing: false)), ); expect(tester.takeException(), isNull); }); - testWidgets('non-zero rotationCorrection value is used', ( - WidgetTester tester, - ) async { + testWidgets('non-zero rotationCorrection value is used', (WidgetTester tester) async { final controller = FakeController.value( const VideoPlayerValue(duration: Duration.zero, rotationCorrection: 180), ); @@ -283,9 +282,7 @@ void main() { group('ClosedCaption widget', () { testWidgets('uses a default text style', (WidgetTester tester) async { const text = 'foo'; - await tester.pumpWidget( - const MaterialApp(home: ClosedCaption(text: text)), - ); + await tester.pumpWidget(const MaterialApp(home: ClosedCaption(text: text))); final Text textWidget = tester.widget(find.text(text)); expect(textWidget.style!.fontSize, 36.0); @@ -316,9 +313,7 @@ void main() { expect(find.byType(Text), findsNothing); }); - testWidgets('Passes text contrast ratio guidelines', ( - WidgetTester tester, - ) async { + testWidgets('Passes text contrast ratio guidelines', (WidgetTester tester) async { const text = 'foo'; await tester.pumpWidget( const MaterialApp( @@ -342,10 +337,7 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {}, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); }); test('network with hint', () async { @@ -356,14 +348,8 @@ void main() { await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); - expect( - fakeVideoPlayerPlatform.dataSources[0].formatHint, - VideoFormat.dash, - ); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {}, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, VideoFormat.dash); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); }); test('network with some headers', () async { @@ -375,10 +361,9 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {'Authorization': 'Bearer token'}, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, { + 'Authorization': 'Bearer token', + }); }); }); @@ -390,10 +375,7 @@ void main() { addTearDown(controller.dispose); await controller.initialize(); await controller.play(); - verifyPlayStateRespondsToLifecycle( - controller, - shouldPlayInBackground: false, - ); + verifyPlayStateRespondsToLifecycle(controller, shouldPlayInBackground: false); }); test('asset', () async { @@ -413,10 +395,7 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {}, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); }); test('network url with hint', () async { @@ -428,14 +407,8 @@ void main() { await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); - expect( - fakeVideoPlayerPlatform.dataSources[0].formatHint, - VideoFormat.dash, - ); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {}, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, VideoFormat.dash); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); }); test('network url with some headers', () async { @@ -448,10 +421,9 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {'Authorization': 'Bearer token'}, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, { + 'Authorization': 'Bearer token', + }); }); test( @@ -479,64 +451,40 @@ void main() { expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); }, skip: kIsWeb /* Web does not support file assets. */); - test( - 'file with special characters', - () async { - final controller = VideoPlayerController.file(File('A #1 Hit.avi')); - await controller.initialize(); + test('file with special characters', () async { + final controller = VideoPlayerController.file(File('A #1 Hit.avi')); + await controller.initialize(); - final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; - expect( - uri.startsWith('file:///'), - true, - reason: 'Actual string: $uri', - ); - expect( - uri.endsWith('/A%20%231%20Hit.avi'), - true, - reason: 'Actual string: $uri', - ); - }, - skip: kIsWeb /* Web does not support file assets. */, - ); + final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; + expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); + expect(uri.endsWith('/A%20%231%20Hit.avi'), true, reason: 'Actual string: $uri'); + }, skip: kIsWeb /* Web does not support file assets. */); - test( - 'file with headers (m3u8)', - () async { - final controller = VideoPlayerController.file( - File('a.avi'), - httpHeaders: {'Authorization': 'Bearer token'}, - ); - await controller.initialize(); + test('file with headers (m3u8)', () async { + final controller = VideoPlayerController.file( + File('a.avi'), + httpHeaders: {'Authorization': 'Bearer token'}, + ); + await controller.initialize(); - final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; - expect( - uri.startsWith('file:///'), - true, - reason: 'Actual string: $uri', - ); - expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); + final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; + expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); + expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {'Authorization': 'Bearer token'}, - ); - }, - skip: kIsWeb /* Web does not support file assets. */, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, { + 'Authorization': 'Bearer token', + }); + }, skip: kIsWeb /* Web does not support file assets. */); - test( - 'successful initialize on controller with error clears error', - () async { - final controller = VideoPlayerController.network('https://127.0.0.1'); - fakeVideoPlayerPlatform.forceInitError = true; - await controller.initialize().catchError((dynamic e) {}); - expect(controller.value.hasError, equals(true)); - fakeVideoPlayerPlatform.forceInitError = false; - await controller.initialize(); - expect(controller.value.hasError, equals(false)); - }, - ); + test('successful initialize on controller with error clears error', () async { + final controller = VideoPlayerController.network('https://127.0.0.1'); + fakeVideoPlayerPlatform.forceInitError = true; + await controller.initialize().catchError((dynamic e) {}); + expect(controller.value.hasError, equals(true)); + fakeVideoPlayerPlatform.forceInitError = false; + await controller.initialize(); + expect(controller.value.hasError, equals(false)); + }); test( 'given controller with error when initialization succeeds it should clear error', @@ -555,9 +503,7 @@ void main() { }); test('contentUri', () async { - final controller = VideoPlayerController.contentUri( - Uri.parse('content://video'), - ); + final controller = VideoPlayerController.contentUri(Uri.parse('content://video')); await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'content://video'); @@ -588,9 +534,7 @@ void main() { }); test('play', () async { - final controller = VideoPlayerController.networkUrl( - Uri.parse('https://127.0.0.1'), - ); + final controller = VideoPlayerController.networkUrl(Uri.parse('https://127.0.0.1')); addTearDown(controller.dispose); await controller.initialize(); @@ -755,22 +699,14 @@ void main() { }); group('scrubbing', () { - testWidgets('restarts on release if already playing', ( - WidgetTester tester, - ) async { + testWidgets('restarts on release if already playing', (WidgetTester tester) async { final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - final progressWidget = VideoProgressIndicator( - controller, - allowScrubbing: true, - ); + final progressWidget = VideoProgressIndicator(controller, allowScrubbing: true); await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: progressWidget, - ), + Directionality(textDirection: TextDirection.ltr, child: progressWidget), ); await controller.play(); @@ -787,22 +723,14 @@ void main() { await tester.runAsync(controller.dispose); }); - testWidgets('does not restart when dragging to end', ( - WidgetTester tester, - ) async { + testWidgets('does not restart when dragging to end', (WidgetTester tester) async { final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - final progressWidget = VideoProgressIndicator( - controller, - allowScrubbing: true, - ); + final progressWidget = VideoProgressIndicator(controller, allowScrubbing: true); await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: progressWidget, - ), + Directionality(textDirection: TextDirection.ltr, child: progressWidget), ); await controller.play(); @@ -1013,9 +941,7 @@ void main() { final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.playerId]!; - fakeVideoEventStream.add( - VideoEvent(eventType: VideoEventType.completed), - ); + fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed)); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); @@ -1031,19 +957,13 @@ void main() { fakeVideoPlayerPlatform.streams[controller.playerId]!; fakeVideoEventStream.add( - VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: true, - ), + VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: true), ); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isTrue); fakeVideoEventStream.add( - VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: false, - ), + VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: false), ); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); @@ -1059,9 +979,7 @@ void main() { final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.playerId]!; - fakeVideoEventStream.add( - VideoEvent(eventType: VideoEventType.bufferingStart), - ); + fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.bufferingStart)); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isTrue); @@ -1081,9 +999,7 @@ void main() { DurationRange(bufferStart, bufferEnd).toString(), ); - fakeVideoEventStream.add( - VideoEvent(eventType: VideoEventType.bufferingEnd), - ); + fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.bufferingEnd)); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isFalse); await tester.runAsync(controller.dispose); @@ -1263,17 +1179,13 @@ void main() { }); test('errorDescription is changed when copy with another error', () { const original = VideoPlayerValue.erroneous('error'); - final VideoPlayerValue copy = original.copyWith( - errorDescription: 'new error', - ); + final VideoPlayerValue copy = original.copyWith(errorDescription: 'new error'); expect(copy.errorDescription, 'new error'); }); test('errorDescription is changed when copy with error', () { const original = VideoPlayerValue.uninitialized(); - final VideoPlayerValue copy = original.copyWith( - errorDescription: 'new error', - ); + final VideoPlayerValue copy = original.copyWith(errorDescription: 'new error'); expect(copy.errorDescription, 'new error'); }); @@ -1347,10 +1259,7 @@ void main() { await controller.initialize(); await controller.play(); - verifyPlayStateRespondsToLifecycle( - controller, - shouldPlayInBackground: true, - ); + verifyPlayStateRespondsToLifecycle(controller, shouldPlayInBackground: true); }); test('false allowBackgroundPlayback pauses playback', () async { @@ -1362,10 +1271,7 @@ void main() { await controller.initialize(); await controller.play(); - verifyPlayStateRespondsToLifecycle( - controller, - shouldPlayInBackground: false, - ); + verifyPlayStateRespondsToLifecycle(controller, shouldPlayInBackground: false); }); }); @@ -1438,10 +1344,7 @@ void main() { isCompletedTest(); if (!hasLooped) { fakeVideoEventStream.add( - VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: true, - ), + VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: true), ); hasLooped = !hasLooped; } @@ -1467,9 +1370,7 @@ void main() { final void Function() isCompletedTest = expectAsync0(() {}); - controller.value = controller.value.copyWith( - duration: const Duration(seconds: 10), - ); + controller.value = controller.value.copyWith(duration: const Duration(seconds: 10)); controller.addListener(() async { if (currentIsCompleted != controller.value.isCompleted) { @@ -1486,6 +1387,91 @@ void main() { await controller.seekTo(const Duration(seconds: 20)); }); + + group('video tracks', () { + test('isVideoTrackSupportAvailable returns platform value', () async { + final controller = VideoPlayerController.networkUrl(_localhostUri); + await controller.initialize(); + + expect(controller.isVideoTrackSupportAvailable(), true); + }); + + test('getVideoTracks returns empty list when not initialized', () async { + final controller = VideoPlayerController.networkUrl(_localhostUri); + + final tracks = await controller.getVideoTracks(); + + expect(tracks, isEmpty); + }); + + test('getVideoTracks returns tracks from platform', () async { + final controller = VideoPlayerController.networkUrl(_localhostUri); + await controller.initialize(); + + fakeVideoPlayerPlatform + .setVideoTracksForPlayer(controller.playerId, [ + const platform_interface.VideoTrack( + id: '0_0', + isSelected: true, + label: '1080p', + bitrate: 5000000, + width: 1920, + height: 1080, + ), + const platform_interface.VideoTrack( + id: '0_1', + isSelected: false, + label: '720p', + bitrate: 2500000, + width: 1280, + height: 720, + ), + ]); + + final tracks = await controller.getVideoTracks(); + + expect(tracks.length, 2); + expect(tracks[0].id, '0_0'); + expect(tracks[0].label, '1080p'); + expect(tracks[0].isSelected, true); + expect(tracks[0].bitrate, 5000000); + expect(tracks[1].id, '0_1'); + expect(tracks[1].label, '720p'); + expect(fakeVideoPlayerPlatform.calls, contains('getVideoTracks')); + }); + + test('selectVideoTrack calls platform with track', () async { + final controller = VideoPlayerController.networkUrl(_localhostUri); + await controller.initialize(); + + const track = VideoTrack( + id: '0_1', + isSelected: false, + label: '720p', + bitrate: 2500000, + ); + await controller.selectVideoTrack(track); + + expect(fakeVideoPlayerPlatform.calls, contains('selectVideoTrack')); + }); + + test('selectVideoTrack with null enables auto quality', () async { + final controller = VideoPlayerController.networkUrl(_localhostUri); + await controller.initialize(); + + await controller.selectVideoTrack(null); + + expect(fakeVideoPlayerPlatform.calls, contains('selectVideoTrack')); + }); + + test('selectVideoTrack does nothing when not initialized', () async { + final controller = VideoPlayerController.networkUrl(_localhostUri); + + await controller.selectVideoTrack(null); + + expect(fakeVideoPlayerPlatform.calls, isNot(contains('selectVideoTrack'))); + }); + }); } class FakeVideoPlayerPlatform extends VideoPlayerPlatform { @@ -1498,8 +1484,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { bool forceInitError = false; int nextPlayerId = 0; final Map _positions = {}; - final Map webOptions = - {}; + final Map webOptions = {}; @override Future create(DataSource dataSource) async { @@ -1508,10 +1493,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { streams[nextPlayerId] = stream; if (forceInitError) { stream.addError( - PlatformException( - code: 'VideoError', - message: 'Video player had error XYZ', - ), + PlatformException(code: 'VideoError', message: 'Video player had error XYZ'), ); } else { stream.add( @@ -1533,10 +1515,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { streams[nextPlayerId] = stream; if (forceInitError) { stream.addError( - PlatformException( - code: 'VideoError', - message: 'Video player had error XYZ', - ), + PlatformException(code: 'VideoError', message: 'Video player had error XYZ'), ); } else { stream.add( @@ -1616,14 +1595,40 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { } @override - Future setWebOptions( - int playerId, - VideoPlayerWebOptions options, - ) async { + Future setWebOptions(int playerId, VideoPlayerWebOptions options) async { if (!kIsWeb) { throw UnimplementedError('setWebOptions() is only available in the web.'); } calls.add('setWebOptions'); webOptions[playerId] = options; } + + // Video track selection support + final Map> _videoTracks = + >{}; + platform_interface.VideoTrack? _selectedVideoTrack; + + void setVideoTracksForPlayer(int playerId, List tracks) { + _videoTracks[playerId] = tracks; + } + + @override + Future> getVideoTracks(int playerId) async { + calls.add('getVideoTracks'); + return _videoTracks[playerId] ?? []; + } + + @override + Future selectVideoTrack( + int playerId, + platform_interface.VideoTrack? track, + ) async { + calls.add('selectVideoTrack'); + _selectedVideoTrack = track; + } + + @override + bool isVideoTrackSupportAvailable() { + return true; + } } diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index eeaf1f1c0bd..4d53b04bd9d 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Implements `getVideoTracks()` and `selectVideoTrack()` methods for video track (quality) selection using ExoPlayer. + ## 2.9.1 * Updates to Pigeon 26.1.5. diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java index 33988786a78..c776109a741 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java @@ -95,8 +95,12 @@ public void onIsPlayingChanged(boolean isPlaying) { @Override public void onTracksChanged(@NonNull Tracks tracks) { // Find the currently selected audio track and notify - String selectedTrackId = findSelectedAudioTrackId(tracks); - events.onAudioTrackChanged(selectedTrackId); + String selectedAudioTrackId = findSelectedAudioTrackId(tracks); + events.onAudioTrackChanged(selectedAudioTrackId); + + // Find the currently selected video track and notify + String selectedVideoTrackId = findSelectedVideoTrackId(tracks); + events.onVideoTrackChanged(selectedVideoTrackId); } /** @@ -121,4 +125,27 @@ private String findSelectedAudioTrackId(@NonNull Tracks tracks) { } return null; } + + /** + * Finds the ID of the currently selected video track. + * + * @param tracks The current tracks + * @return The track ID in format "groupIndex_trackIndex", or null if no video track is selected + */ + @Nullable + private String findSelectedVideoTrackId(@NonNull Tracks tracks) { + int groupIndex = 0; + for (Tracks.Group group : tracks.getGroups()) { + if (group.getType() == C.TRACK_TYPE_VIDEO && group.isSelected()) { + // Find the selected track within this group + for (int i = 0; i < group.length; i++) { + if (group.isTrackSelected(i)) { + return groupIndex + "_" + i; + } + } + } + groupIndex++; + } + return null; + } } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 7cfb5c1c13b..4758971e225 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -233,6 +233,172 @@ public void selectAudioTrack(long groupIndex, long trackIndex) { trackSelector.buildUponParameters().setOverrideForType(override).build()); } + // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. + @UnstableApi + @Override + public @NonNull NativeVideoTrackData getVideoTracks() { + List videoTracks = new ArrayList<>(); + + // Get the current tracks from ExoPlayer + Tracks tracks = exoPlayer.getCurrentTracks(); + + // Iterate through all track groups + for (int groupIndex = 0; groupIndex < tracks.getGroups().size(); groupIndex++) { + Tracks.Group group = tracks.getGroups().get(groupIndex); + + // Only process video tracks + if (group.getType() == C.TRACK_TYPE_VIDEO) { + for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { + Format format = group.getTrackFormat(trackIndex); + boolean isSelected = group.isTrackSelected(trackIndex); + + // Create video track data with metadata + ExoPlayerVideoTrackData videoTrack = + new ExoPlayerVideoTrackData( + (long) groupIndex, + (long) trackIndex, + format.label, + isSelected, + format.bitrate != Format.NO_VALUE ? (long) format.bitrate : null, + format.width != Format.NO_VALUE ? (long) format.width : null, + format.height != Format.NO_VALUE ? (long) format.height : null, + format.frameRate != Format.NO_VALUE ? (double) format.frameRate : null, + format.codecs != null ? format.codecs : null); + + videoTracks.add(videoTrack); + } + } + } + return new NativeVideoTrackData(videoTracks); + } + + // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. + @UnstableApi + @Override + public void selectVideoTrack(long groupIndex, long trackIndex) { + if (trackSelector == null) { + throw new IllegalStateException("Cannot select video track: track selector is null"); + } + + // If both indices are -1, clear the video track override (auto quality) + if (groupIndex == -1 && trackIndex == -1) { + // Clear video track override to enable adaptive streaming + trackSelector.setParameters( + trackSelector.buildUponParameters().clearOverridesOfType(C.TRACK_TYPE_VIDEO).build()); + return; + } + + // Get current tracks + Tracks tracks = exoPlayer.getCurrentTracks(); + + if (groupIndex < 0 || groupIndex >= tracks.getGroups().size()) { + throw new IllegalArgumentException( + "Cannot select video track: groupIndex " + + groupIndex + + " is out of bounds (available groups: " + + tracks.getGroups().size() + + ")"); + } + + Tracks.Group group = tracks.getGroups().get((int) groupIndex); + + // Verify it's a video track + if (group.getType() != C.TRACK_TYPE_VIDEO) { + throw new IllegalArgumentException( + "Cannot select video track: group at index " + + groupIndex + + " is not a video track (type: " + + group.getType() + + ")"); + } + + // Verify the track index is valid + if (trackIndex < 0 || (int) trackIndex >= group.length) { + throw new IllegalArgumentException( + "Cannot select video track: trackIndex " + + trackIndex + + " is out of bounds (available tracks in group: " + + group.length + + ")"); + } + + // Get the track group and create a selection override + TrackGroup trackGroup = group.getMediaTrackGroup(); + TrackSelectionOverride override = new TrackSelectionOverride(trackGroup, (int) trackIndex); + + // Check if the new track has different dimensions than the current track + Format currentFormat = exoPlayer.getVideoFormat(); + Format newFormat = trackGroup.getFormat((int) trackIndex); + boolean dimensionsChanged = + currentFormat != null + && (currentFormat.width != newFormat.width + || currentFormat.height != newFormat.height); + + // When video dimensions change, we need to force a complete renderer reset to avoid + // surface rendering issues. We do this by temporarily disabling the video track type, + // which causes ExoPlayer to release the current video renderer and MediaCodec decoder. + // After a brief delay, we re-enable video with the new track selection, which creates + // a fresh renderer properly configured for the new dimensions. + // + // Why is this necessary? + // When switching between video tracks with different resolutions (e.g., 720p to 1080p), + // the existing video surface and MediaCodec decoder may not properly reconfigure for the + // new dimensions. This can cause visual glitches where the video appears in the wrong + // position (e.g., top-left corner) or the old surface remains partially visible. + // By disabling the video track type, we force ExoPlayer to completely release the + // current renderer and decoder, ensuring a clean slate for the new resolution. + if (dimensionsChanged) { + final boolean wasPlaying = exoPlayer.isPlaying(); + final long currentPosition = exoPlayer.getCurrentPosition(); + + // Disable video track type to force renderer release + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) + .build()); + + // Re-enable video with the new track selection after allowing renderer to release. + // + // Why 150ms delay? + // This delay is necessary to allow the MediaCodec decoder and video renderer to fully + // release their resources before we attempt to create new ones. Without this delay, + // the new decoder may be initialized before the old one is completely released, leading + // to resource conflicts and rendering artifacts. The 150ms value was determined through + // empirical testing across various Android devices and provides a reliable balance + // between responsiveness and ensuring complete resource cleanup. Shorter delays (e.g., + // 50-100ms) were found to still cause glitches on some devices, while longer delays + // would unnecessarily impact user experience. + new android.os.Handler(android.os.Looper.getMainLooper()) + .postDelayed( + () -> { + // Guard against player disposal during the delay + if (trackSelector == null) { + return; + } + + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, false) + .setOverrideForType(override) + .build()); + + // Restore playback state + exoPlayer.seekTo(currentPosition); + if (wasPlaying) { + exoPlayer.play(); + } + }, + 150); + return; + } + + // Apply the track selection override normally if dimensions haven't changed + trackSelector.setParameters( + trackSelector.buildUponParameters().setOverrideForType(override).build()); + } + public void dispose() { if (disposeHandler != null) { disposeHandler.onDispose(); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java index 4cac902319e..45638321c04 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java @@ -26,4 +26,6 @@ public interface VideoPlayerCallbacks { void onIsPlayingStateUpdate(boolean isPlaying); void onAudioTrackChanged(@Nullable String selectedTrackId); + + void onVideoTrackChanged(@Nullable String selectedTrackId); } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java index a471ec960e6..21484cc2df3 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java @@ -68,4 +68,9 @@ public void onIsPlayingStateUpdate(boolean isPlaying) { public void onAudioTrackChanged(@Nullable String selectedTrackId) { eventSink.success(new AudioTrackChangedEvent(selectedTrackId)); } + + @Override + public void onVideoTrackChanged(@Nullable String selectedTrackId) { + eventSink.success(new VideoTrackChangedEvent(selectedTrackId)); + } } diff --git a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt index e546c744e56..20d0c7f6b9f 100644 --- a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt +++ b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt @@ -12,11 +12,10 @@ import io.flutter.plugin.common.BasicMessageChannel import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMessageCodec import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec import java.io.ByteArrayOutputStream import java.nio.ByteBuffer - private object MessagesPigeonUtils { fun wrapResult(result: Any?): List { @@ -25,53 +24,61 @@ private object MessagesPigeonUtils { fun wrapError(exception: Throwable): List { return if (exception is FlutterError) { - listOf(exception.code, exception.message, exception.details) + listOf( + exception.code, + exception.message, + exception.details + ) } else { listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)) + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) } } - fun deepEquals(a: Any?, b: Any?): Boolean { if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is IntArray && b is IntArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is LongArray && b is LongArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is DoubleArray && b is DoubleArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is Array<*> && b is Array<*>) { - return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } } if (a is List<*> && b is List<*>) { - return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } } if (a is Map<*, *> && b is Map<*, *>) { - return a.size == b.size && - a.all { (b as Map).contains(it.key) && deepEquals(it.value, b[it.key]) } + return a.size == b.size && a.all { + (b as Map).contains(it.key) && + deepEquals(it.value, b[it.key]) + } } return a == b } + } /** * Error class for passing custom error details to Flutter via a thrown PlatformException. - * * @property code The error code. * @property message The error message. * @property details The error details. Must be a datatype supported by the api codec. */ -class FlutterError( - val code: String, - override val message: String? = null, - val details: Any? = null +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null ) : Throwable() /** Pigeon equivalent of video_platform_interface's VideoFormat. */ @@ -106,25 +113,26 @@ enum class PlatformPlaybackState(val raw: Int) { } /** - * Generated class from Pigeon that represents data sent in messages. This class should not be - * extended by any user class outside of the generated file. + * Generated class from Pigeon that represents data sent in messages. + * This class should not be extended by any user class outside of the generated file. */ -sealed class PlatformVideoEvent +sealed class PlatformVideoEvent /** * Sent when the video is initialized and ready to play. * * Generated class from Pigeon that represents data sent in messages. */ -data class InitializationEvent( - /** The video duration in milliseconds. */ - val duration: Long, - /** The width of the video in pixels. */ - val width: Long, - /** The height of the video in pixels. */ - val height: Long, - /** The rotation that should be applied during playback. */ - val rotationCorrection: Long -) : PlatformVideoEvent() { +data class InitializationEvent ( + /** The video duration in milliseconds. */ + val duration: Long, + /** The width of the video in pixels. */ + val width: Long, + /** The height of the video in pixels. */ + val height: Long, + /** The rotation that should be applied during playback. */ + val rotationCorrection: Long +) : PlatformVideoEvent() + { companion object { fun fromList(pigeonVar_list: List): InitializationEvent { val duration = pigeonVar_list[0] as Long @@ -134,16 +142,14 @@ data class InitializationEvent( return InitializationEvent(duration, width, height, rotationCorrection) } } - fun toList(): List { return listOf( - duration, - width, - height, - rotationCorrection, + duration, + width, + height, + rotationCorrection, ) } - override fun equals(other: Any?): Boolean { if (other !is InitializationEvent) { return false @@ -151,8 +157,7 @@ data class InitializationEvent( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -164,20 +169,21 @@ data class InitializationEvent( * * Generated class from Pigeon that represents data sent in messages. */ -data class PlaybackStateChangeEvent(val state: PlatformPlaybackState) : PlatformVideoEvent() { +data class PlaybackStateChangeEvent ( + val state: PlatformPlaybackState +) : PlatformVideoEvent() + { companion object { fun fromList(pigeonVar_list: List): PlaybackStateChangeEvent { val state = pigeonVar_list[0] as PlatformPlaybackState return PlaybackStateChangeEvent(state) } } - fun toList(): List { return listOf( - state, + state, ) } - override fun equals(other: Any?): Boolean { if (other !is PlaybackStateChangeEvent) { return false @@ -185,8 +191,7 @@ data class PlaybackStateChangeEvent(val state: PlatformPlaybackState) : Platform if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -198,20 +203,21 @@ data class PlaybackStateChangeEvent(val state: PlatformPlaybackState) : Platform * * Generated class from Pigeon that represents data sent in messages. */ -data class IsPlayingStateEvent(val isPlaying: Boolean) : PlatformVideoEvent() { +data class IsPlayingStateEvent ( + val isPlaying: Boolean +) : PlatformVideoEvent() + { companion object { fun fromList(pigeonVar_list: List): IsPlayingStateEvent { val isPlaying = pigeonVar_list[0] as Boolean return IsPlayingStateEvent(isPlaying) } } - fun toList(): List { return listOf( - isPlaying, + isPlaying, ) } - override fun equals(other: Any?): Boolean { if (other !is IsPlayingStateEvent) { return false @@ -219,8 +225,7 @@ data class IsPlayingStateEvent(val isPlaying: Boolean) : PlatformVideoEvent() { if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -228,28 +233,27 @@ data class IsPlayingStateEvent(val isPlaying: Boolean) : PlatformVideoEvent() { /** * Sent when audio tracks change. * - * This includes when the selected audio track changes after calling selectAudioTrack. Corresponds - * to ExoPlayer's onTracksChanged. + * This includes when the selected audio track changes after calling selectAudioTrack. + * Corresponds to ExoPlayer's onTracksChanged. * * Generated class from Pigeon that represents data sent in messages. */ -data class AudioTrackChangedEvent( - /** The ID of the newly selected audio track, if any. */ - val selectedTrackId: String? = null -) : PlatformVideoEvent() { +data class AudioTrackChangedEvent ( + /** The ID of the newly selected audio track, if any. */ + val selectedTrackId: String? = null +) : PlatformVideoEvent() + { companion object { fun fromList(pigeonVar_list: List): AudioTrackChangedEvent { val selectedTrackId = pigeonVar_list[0] as String? return AudioTrackChangedEvent(selectedTrackId) } } - fun toList(): List { return listOf( - selectedTrackId, + selectedTrackId, ) } - override fun equals(other: Any?): Boolean { if (other !is AudioTrackChangedEvent) { return false @@ -257,8 +261,46 @@ data class AudioTrackChangedEvent( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Sent when video tracks change. + * + * This includes when the selected video track changes after calling selectVideoTrack. + * Corresponds to ExoPlayer's onTracksChanged. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class VideoTrackChangedEvent ( + /** + * The ID of the newly selected video track, if any. + * Will be null when auto quality selection is enabled. + */ + val selectedTrackId: String? = null +) : PlatformVideoEvent() + { + companion object { + fun fromList(pigeonVar_list: List): VideoTrackChangedEvent { + val selectedTrackId = pigeonVar_list[0] as String? + return VideoTrackChangedEvent(selectedTrackId) + } + } + fun toList(): List { + return listOf( + selectedTrackId, + ) } + override fun equals(other: Any?): Boolean { + if (other !is VideoTrackChangedEvent) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -268,20 +310,21 @@ data class AudioTrackChangedEvent( * * Generated class from Pigeon that represents data sent in messages. */ -data class PlatformVideoViewCreationParams(val playerId: Long) { +data class PlatformVideoViewCreationParams ( + val playerId: Long +) + { companion object { fun fromList(pigeonVar_list: List): PlatformVideoViewCreationParams { val playerId = pigeonVar_list[0] as Long return PlatformVideoViewCreationParams(playerId) } } - fun toList(): List { return listOf( - playerId, + playerId, ) } - override fun equals(other: Any?): Boolean { if (other !is PlatformVideoViewCreationParams) { return false @@ -289,19 +332,19 @@ data class PlatformVideoViewCreationParams(val playerId: Long) { if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ -data class CreationOptions( - val uri: String, - val formatHint: PlatformVideoFormat? = null, - val httpHeaders: Map, - val userAgent: String? = null -) { +data class CreationOptions ( + val uri: String, + val formatHint: PlatformVideoFormat? = null, + val httpHeaders: Map, + val userAgent: String? = null +) + { companion object { fun fromList(pigeonVar_list: List): CreationOptions { val uri = pigeonVar_list[0] as String @@ -311,16 +354,14 @@ data class CreationOptions( return CreationOptions(uri, formatHint, httpHeaders, userAgent) } } - fun toList(): List { return listOf( - uri, - formatHint, - httpHeaders, - userAgent, + uri, + formatHint, + httpHeaders, + userAgent, ) } - override fun equals(other: Any?): Boolean { if (other !is CreationOptions) { return false @@ -328,14 +369,17 @@ data class CreationOptions( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ -data class TexturePlayerIds(val playerId: Long, val textureId: Long) { +data class TexturePlayerIds ( + val playerId: Long, + val textureId: Long +) + { companion object { fun fromList(pigeonVar_list: List): TexturePlayerIds { val playerId = pigeonVar_list[0] as Long @@ -343,14 +387,12 @@ data class TexturePlayerIds(val playerId: Long, val textureId: Long) { return TexturePlayerIds(playerId, textureId) } } - fun toList(): List { return listOf( - playerId, - textureId, + playerId, + textureId, ) } - override fun equals(other: Any?): Boolean { if (other !is TexturePlayerIds) { return false @@ -358,19 +400,19 @@ data class TexturePlayerIds(val playerId: Long, val textureId: Long) { if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ -data class PlaybackState( - /** The current playback position, in milliseconds. */ - val playPosition: Long, - /** The current buffer position, in milliseconds. */ - val bufferPosition: Long -) { +data class PlaybackState ( + /** The current playback position, in milliseconds. */ + val playPosition: Long, + /** The current buffer position, in milliseconds. */ + val bufferPosition: Long +) + { companion object { fun fromList(pigeonVar_list: List): PlaybackState { val playPosition = pigeonVar_list[0] as Long @@ -378,14 +420,12 @@ data class PlaybackState( return PlaybackState(playPosition, bufferPosition) } } - fun toList(): List { return listOf( - playPosition, - bufferPosition, + playPosition, + bufferPosition, ) } - override fun equals(other: Any?): Boolean { if (other !is PlaybackState) { return false @@ -393,8 +433,7 @@ data class PlaybackState( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -404,16 +443,17 @@ data class PlaybackState( * * Generated class from Pigeon that represents data sent in messages. */ -data class AudioTrackMessage( - val id: String, - val label: String, - val language: String, - val isSelected: Boolean, - val bitrate: Long? = null, - val sampleRate: Long? = null, - val channelCount: Long? = null, - val codec: String? = null -) { +data class AudioTrackMessage ( + val id: String, + val label: String, + val language: String, + val isSelected: Boolean, + val bitrate: Long? = null, + val sampleRate: Long? = null, + val channelCount: Long? = null, + val codec: String? = null +) + { companion object { fun fromList(pigeonVar_list: List): AudioTrackMessage { val id = pigeonVar_list[0] as String @@ -424,24 +464,21 @@ data class AudioTrackMessage( val sampleRate = pigeonVar_list[5] as Long? val channelCount = pigeonVar_list[6] as Long? val codec = pigeonVar_list[7] as String? - return AudioTrackMessage( - id, label, language, isSelected, bitrate, sampleRate, channelCount, codec) + return AudioTrackMessage(id, label, language, isSelected, bitrate, sampleRate, channelCount, codec) } } - fun toList(): List { return listOf( - id, - label, - language, - isSelected, - bitrate, - sampleRate, - channelCount, - codec, + id, + label, + language, + isSelected, + bitrate, + sampleRate, + channelCount, + codec, ) } - override fun equals(other: Any?): Boolean { if (other !is AudioTrackMessage) { return false @@ -449,8 +486,7 @@ data class AudioTrackMessage( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -460,17 +496,18 @@ data class AudioTrackMessage( * * Generated class from Pigeon that represents data sent in messages. */ -data class ExoPlayerAudioTrackData( - val groupIndex: Long, - val trackIndex: Long, - val label: String? = null, - val language: String? = null, - val isSelected: Boolean, - val bitrate: Long? = null, - val sampleRate: Long? = null, - val channelCount: Long? = null, - val codec: String? = null -) { +data class ExoPlayerAudioTrackData ( + val groupIndex: Long, + val trackIndex: Long, + val label: String? = null, + val language: String? = null, + val isSelected: Boolean, + val bitrate: Long? = null, + val sampleRate: Long? = null, + val channelCount: Long? = null, + val codec: String? = null +) + { companion object { fun fromList(pigeonVar_list: List): ExoPlayerAudioTrackData { val groupIndex = pigeonVar_list[0] as Long @@ -482,33 +519,22 @@ data class ExoPlayerAudioTrackData( val sampleRate = pigeonVar_list[6] as Long? val channelCount = pigeonVar_list[7] as Long? val codec = pigeonVar_list[8] as String? - return ExoPlayerAudioTrackData( - groupIndex, - trackIndex, - label, - language, - isSelected, - bitrate, - sampleRate, - channelCount, - codec) + return ExoPlayerAudioTrackData(groupIndex, trackIndex, label, language, isSelected, bitrate, sampleRate, channelCount, codec) } } - fun toList(): List { return listOf( - groupIndex, - trackIndex, - label, - language, - isSelected, - bitrate, - sampleRate, - channelCount, - codec, + groupIndex, + trackIndex, + label, + language, + isSelected, + bitrate, + sampleRate, + channelCount, + codec, ) } - override fun equals(other: Any?): Boolean { if (other !is ExoPlayerAudioTrackData) { return false @@ -516,8 +542,7 @@ data class ExoPlayerAudioTrackData( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) - } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -527,23 +552,22 @@ data class ExoPlayerAudioTrackData( * * Generated class from Pigeon that represents data sent in messages. */ -data class NativeAudioTrackData( - /** ExoPlayer-based tracks */ - val exoPlayerTracks: List? = null -) { +data class NativeAudioTrackData ( + /** ExoPlayer-based tracks */ + val exoPlayerTracks: List? = null +) + { companion object { fun fromList(pigeonVar_list: List): NativeAudioTrackData { val exoPlayerTracks = pigeonVar_list[0] as List? return NativeAudioTrackData(exoPlayerTracks) } } - fun toList(): List { return listOf( - exoPlayerTracks, + exoPlayerTracks, ) } - override fun equals(other: Any?): Boolean { if (other !is NativeAudioTrackData) { return false @@ -551,61 +575,186 @@ data class NativeAudioTrackData( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Raw video track data from ExoPlayer Format objects. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class ExoPlayerVideoTrackData ( + val groupIndex: Long, + val trackIndex: Long, + val label: String? = null, + val isSelected: Boolean, + val bitrate: Long? = null, + val width: Long? = null, + val height: Long? = null, + val frameRate: Double? = null, + val codec: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): ExoPlayerVideoTrackData { + val groupIndex = pigeonVar_list[0] as Long + val trackIndex = pigeonVar_list[1] as Long + val label = pigeonVar_list[2] as String? + val isSelected = pigeonVar_list[3] as Boolean + val bitrate = pigeonVar_list[4] as Long? + val width = pigeonVar_list[5] as Long? + val height = pigeonVar_list[6] as Long? + val frameRate = pigeonVar_list[7] as Double? + val codec = pigeonVar_list[8] as String? + return ExoPlayerVideoTrackData(groupIndex, trackIndex, label, isSelected, bitrate, width, height, frameRate, codec) + } + } + fun toList(): List { + return listOf( + groupIndex, + trackIndex, + label, + isSelected, + bitrate, + width, + height, + frameRate, + codec, + ) } + override fun equals(other: Any?): Boolean { + if (other !is ExoPlayerVideoTrackData) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } +/** + * Container for raw video track data from Android ExoPlayer. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class NativeVideoTrackData ( + /** ExoPlayer-based tracks */ + val exoPlayerTracks: List? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): NativeVideoTrackData { + val exoPlayerTracks = pigeonVar_list[0] as List? + return NativeVideoTrackData(exoPlayerTracks) + } + } + fun toList(): List { + return listOf( + exoPlayerTracks, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is NativeVideoTrackData) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} private open class MessagesPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { 129.toByte() -> { - return (readValue(buffer) as Long?)?.let { PlatformVideoFormat.ofRaw(it.toInt()) } + return (readValue(buffer) as Long?)?.let { + PlatformVideoFormat.ofRaw(it.toInt()) + } } 130.toByte() -> { - return (readValue(buffer) as Long?)?.let { PlatformPlaybackState.ofRaw(it.toInt()) } + return (readValue(buffer) as Long?)?.let { + PlatformPlaybackState.ofRaw(it.toInt()) + } } 131.toByte() -> { - return (readValue(buffer) as? List)?.let { InitializationEvent.fromList(it) } + return (readValue(buffer) as? List)?.let { + InitializationEvent.fromList(it) + } } 132.toByte() -> { - return (readValue(buffer) as? List)?.let { PlaybackStateChangeEvent.fromList(it) } + return (readValue(buffer) as? List)?.let { + PlaybackStateChangeEvent.fromList(it) + } } 133.toByte() -> { - return (readValue(buffer) as? List)?.let { IsPlayingStateEvent.fromList(it) } + return (readValue(buffer) as? List)?.let { + IsPlayingStateEvent.fromList(it) + } } 134.toByte() -> { - return (readValue(buffer) as? List)?.let { AudioTrackChangedEvent.fromList(it) } + return (readValue(buffer) as? List)?.let { + AudioTrackChangedEvent.fromList(it) + } } 135.toByte() -> { return (readValue(buffer) as? List)?.let { - PlatformVideoViewCreationParams.fromList(it) + VideoTrackChangedEvent.fromList(it) } } 136.toByte() -> { - return (readValue(buffer) as? List)?.let { CreationOptions.fromList(it) } + return (readValue(buffer) as? List)?.let { + PlatformVideoViewCreationParams.fromList(it) + } } 137.toByte() -> { - return (readValue(buffer) as? List)?.let { TexturePlayerIds.fromList(it) } + return (readValue(buffer) as? List)?.let { + CreationOptions.fromList(it) + } } 138.toByte() -> { - return (readValue(buffer) as? List)?.let { PlaybackState.fromList(it) } + return (readValue(buffer) as? List)?.let { + TexturePlayerIds.fromList(it) + } } 139.toByte() -> { - return (readValue(buffer) as? List)?.let { AudioTrackMessage.fromList(it) } + return (readValue(buffer) as? List)?.let { + PlaybackState.fromList(it) + } } 140.toByte() -> { - return (readValue(buffer) as? List)?.let { ExoPlayerAudioTrackData.fromList(it) } + return (readValue(buffer) as? List)?.let { + AudioTrackMessage.fromList(it) + } } 141.toByte() -> { - return (readValue(buffer) as? List)?.let { NativeAudioTrackData.fromList(it) } + return (readValue(buffer) as? List)?.let { + ExoPlayerAudioTrackData.fromList(it) + } + } + 142.toByte() -> { + return (readValue(buffer) as? List)?.let { + NativeAudioTrackData.fromList(it) + } + } + 143.toByte() -> { + return (readValue(buffer) as? List)?.let { + ExoPlayerVideoTrackData.fromList(it) + } + } + 144.toByte() -> { + return (readValue(buffer) as? List)?.let { + NativeVideoTrackData.fromList(it) + } } else -> super.readValueOfType(type, buffer) } } - - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { when (value) { is PlatformVideoFormat -> { stream.write(129) @@ -631,34 +780,46 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { stream.write(134) writeValue(stream, value.toList()) } - is PlatformVideoViewCreationParams -> { + is VideoTrackChangedEvent -> { stream.write(135) writeValue(stream, value.toList()) } - is CreationOptions -> { + is PlatformVideoViewCreationParams -> { stream.write(136) writeValue(stream, value.toList()) } - is TexturePlayerIds -> { + is CreationOptions -> { stream.write(137) writeValue(stream, value.toList()) } - is PlaybackState -> { + is TexturePlayerIds -> { stream.write(138) writeValue(stream, value.toList()) } - is AudioTrackMessage -> { + is PlaybackState -> { stream.write(139) writeValue(stream, value.toList()) } - is ExoPlayerAudioTrackData -> { + is AudioTrackMessage -> { stream.write(140) writeValue(stream, value.toList()) } - is NativeAudioTrackData -> { + is ExoPlayerAudioTrackData -> { stream.write(141) writeValue(stream, value.toList()) } + is NativeAudioTrackData -> { + stream.write(142) + writeValue(stream, value.toList()) + } + is ExoPlayerVideoTrackData -> { + stream.write(143) + writeValue(stream, value.toList()) + } + is NativeVideoTrackData -> { + stream.write(144) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -669,47 +830,31 @@ val MessagesPigeonMethodCodec = StandardMethodCodec(MessagesPigeonCodec()) /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface AndroidVideoPlayerApi { fun initialize() - fun createForPlatformView(options: CreationOptions): Long - fun createForTextureView(options: CreationOptions): TexturePlayerIds - fun dispose(playerId: Long) - fun setMixWithOthers(mixWithOthers: Boolean) - fun getLookupKeyForAsset(asset: String, packageName: String?): String companion object { /** The codec used by AndroidVideoPlayerApi. */ - val codec: MessageCodec by lazy { MessagesPigeonCodec() } - /** - * Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the - * `binaryMessenger`. - */ + val codec: MessageCodec by lazy { + MessagesPigeonCodec() + } + /** Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the `binaryMessenger`. */ @JvmOverloads - fun setUp( - binaryMessenger: BinaryMessenger, - api: AndroidVideoPlayerApi?, - messageChannelSuffix: String = "" - ) { - val separatedMessageChannelSuffix = - if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + fun setUp(binaryMessenger: BinaryMessenger, api: AndroidVideoPlayerApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = - try { - api.initialize() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.initialize() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -717,21 +862,16 @@ interface AndroidVideoPlayerApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val optionsArg = args[0] as CreationOptions - val wrapped: List = - try { - listOf(api.createForPlatformView(optionsArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + listOf(api.createForPlatformView(optionsArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -739,21 +879,16 @@ interface AndroidVideoPlayerApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val optionsArg = args[0] as CreationOptions - val wrapped: List = - try { - listOf(api.createForTextureView(optionsArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + listOf(api.createForTextureView(optionsArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -761,22 +896,17 @@ interface AndroidVideoPlayerApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val playerIdArg = args[0] as Long - val wrapped: List = - try { - api.dispose(playerIdArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.dispose(playerIdArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -784,22 +914,17 @@ interface AndroidVideoPlayerApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val mixWithOthersArg = args[0] as Boolean - val wrapped: List = - try { - api.setMixWithOthers(mixWithOthersArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.setMixWithOthers(mixWithOthersArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -807,22 +932,17 @@ interface AndroidVideoPlayerApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val assetArg = args[0] as String val packageNameArg = args[1] as String? - val wrapped: List = - try { - listOf(api.getLookupKeyForAsset(assetArg, packageNameArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + listOf(api.getLookupKeyForAsset(assetArg, packageNameArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -854,39 +974,35 @@ interface VideoPlayerInstanceApi { fun getAudioTracks(): NativeAudioTrackData /** Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] */ fun selectAudioTrack(groupIndex: Long, trackIndex: Long) + /** Gets the available video tracks for the video. */ + fun getVideoTracks(): NativeVideoTrackData + /** + * Selects which video track is chosen for playback from its [groupIndex] and [trackIndex]. + * Pass -1 for both indices to enable auto quality selection. + */ + fun selectVideoTrack(groupIndex: Long, trackIndex: Long) companion object { /** The codec used by VideoPlayerInstanceApi. */ - val codec: MessageCodec by lazy { MessagesPigeonCodec() } - /** - * Sets up an instance of `VideoPlayerInstanceApi` to handle messages through the - * `binaryMessenger`. - */ + val codec: MessageCodec by lazy { + MessagesPigeonCodec() + } + /** Sets up an instance of `VideoPlayerInstanceApi` to handle messages through the `binaryMessenger`. */ @JvmOverloads - fun setUp( - binaryMessenger: BinaryMessenger, - api: VideoPlayerInstanceApi?, - messageChannelSuffix: String = "" - ) { - val separatedMessageChannelSuffix = - if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + fun setUp(binaryMessenger: BinaryMessenger, api: VideoPlayerInstanceApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val loopingArg = args[0] as Boolean - val wrapped: List = - try { - api.setLooping(loopingArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.setLooping(loopingArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -894,22 +1010,17 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val volumeArg = args[0] as Double - val wrapped: List = - try { - api.setVolume(volumeArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.setVolume(volumeArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -917,22 +1028,17 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val speedArg = args[0] as Double - val wrapped: List = - try { - api.setPlaybackSpeed(speedArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.setPlaybackSpeed(speedArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -940,20 +1046,15 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = - try { - api.play() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.play() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -961,20 +1062,15 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = - try { - api.pause() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.pause() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -982,22 +1078,32 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val positionArg = args[0] as Long - val wrapped: List = - try { - api.seekTo(positionArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.seekTo(positionArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getCurrentPosition()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1005,19 +1111,14 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = - try { - listOf(api.getCurrentPosition()) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + listOf(api.getBufferedPosition()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1025,19 +1126,33 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = - try { - listOf(api.getBufferedPosition()) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + listOf(api.getAudioTracks()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val groupIndexArg = args[0] as Long + val trackIndexArg = args[1] as Long + val wrapped: List = try { + api.selectAudioTrack(groupIndexArg, trackIndexArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1045,19 +1160,14 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getVideoTracks$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = - try { - listOf(api.getAudioTracks()) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + listOf(api.getVideoTracks()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1065,23 +1175,18 @@ interface VideoPlayerInstanceApi { } } run { - val channel = - BasicMessageChannel( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$separatedMessageChannelSuffix", - codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectVideoTrack$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val groupIndexArg = args[0] as Long val trackIndexArg = args[1] as Long - val wrapped: List = - try { - api.selectAudioTrack(groupIndexArg, trackIndexArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = try { + api.selectVideoTrack(groupIndexArg, trackIndexArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1092,8 +1197,9 @@ interface VideoPlayerInstanceApi { } } -private class MessagesPigeonStreamHandler(val wrapper: MessagesPigeonEventChannelWrapper) : - EventChannel.StreamHandler { +private class MessagesPigeonStreamHandler( + val wrapper: MessagesPigeonEventChannelWrapper +) : EventChannel.StreamHandler { var pigeonSink: PigeonEventSink? = null override fun onListen(p0: Any?, sink: EventChannel.EventSink) { @@ -1126,26 +1232,21 @@ class PigeonEventSink(private val sink: EventChannel.EventSink) { sink.endOfStream() } } - + abstract class VideoEventsStreamHandler : MessagesPigeonEventChannelWrapper { companion object { - fun register( - messenger: BinaryMessenger, - streamHandler: VideoEventsStreamHandler, - instanceName: String = "" - ) { - var channelName: String = - "dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents" + fun register(messenger: BinaryMessenger, streamHandler: VideoEventsStreamHandler, instanceName: String = "") { + var channelName: String = "dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents" if (instanceName.isNotEmpty()) { channelName += ".$instanceName" } val internalStreamHandler = MessagesPigeonStreamHandler(streamHandler) - EventChannel(messenger, channelName, MessagesPigeonMethodCodec) - .setStreamHandler(internalStreamHandler) + EventChannel(messenger, channelName, MessagesPigeonMethodCodec).setStreamHandler(internalStreamHandler) } } - // Implement methods from MessagesPigeonEventChannelWrapper - override fun onListen(p0: Any?, sink: PigeonEventSink) {} +// Implement methods from MessagesPigeonEventChannelWrapper +override fun onListen(p0: Any?, sink: PigeonEventSink) {} - override fun onCancel(p0: Any?) {} +override fun onCancel(p0: Any?) {} } + diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacksTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacksTest.java index acb3bfd2b40..b6eb88c8c30 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacksTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacksTest.java @@ -88,4 +88,28 @@ public void onIsPlayingStateUpdate() { IsPlayingStateEvent expected = new IsPlayingStateEvent(true); assertEquals(expected, actual); } + + @Test + public void onAudioTrackChanged() { + String trackId = "0_1"; + eventCallbacks.onAudioTrackChanged(trackId); + + verify(mockEventSink).success(eventCaptor.capture()); + + PlatformVideoEvent actual = eventCaptor.getValue(); + AudioTrackChangedEvent expected = new AudioTrackChangedEvent(trackId); + assertEquals(expected, actual); + } + + @Test + public void onVideoTrackChanged() { + String trackId = "0_2"; + eventCallbacks.onVideoTrackChanged(trackId); + + verify(mockEventSink).success(eventCaptor.capture()); + + PlatformVideoEvent actual = eventCaptor.getValue(); + VideoTrackChangedEvent expected = new VideoTrackChangedEvent(trackId); + assertEquals(expected, actual); + } } diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml index 07c5b497d5d..079e64d56f7 100644 --- a/packages/video_player/video_player_android/example/pubspec.yaml +++ b/packages/video_player/video_player_android/example/pubspec.yaml @@ -18,7 +18,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - video_player_platform_interface: ^6.6.0 + video_player_platform_interface: + path: ../../video_player_platform_interface dev_dependencies: espresso: ^0.4.0 diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index 84249bd41af..22ac7fbe81d 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -17,9 +17,7 @@ VideoPlayerInstanceApi _productionApiProvider(int playerId) { } /// The non-test implementation of `_videoEventStreamProvider`. -Stream _productionVideoEventStreamProvider( - String streamIdentifier, -) { +Stream _productionVideoEventStreamProvider(String streamIdentifier) { return pigeon.videoEvents(instanceName: streamIdentifier); } @@ -29,8 +27,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { /// Creates a new Android video player implementation instance. AndroidVideoPlayer({ @visibleForTesting AndroidVideoPlayerApi? pluginApi, - @visibleForTesting - VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, + @visibleForTesting VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, Stream Function(String streamIdentifier)? videoEventStreamProvider, }) : _api = pluginApi ?? AndroidVideoPlayerApi(), @@ -90,14 +87,9 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { case DataSourceType.asset: final String? asset = dataSource.asset; if (asset == null) { - throw ArgumentError( - '"asset" must be non-null for an asset data source', - ); + throw ArgumentError('"asset" must be non-null for an asset data source'); } - final String key = await _api.getLookupKeyForAsset( - asset, - dataSource.package, - ); + final String key = await _api.getLookupKeyForAsset(asset, dataSource.package); uri = 'asset:///$key'; case DataSourceType.network: uri = dataSource.uri; @@ -213,9 +205,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { final VideoPlayerViewState viewState = _playerWith(id: playerId).viewState; return switch (viewState) { - VideoPlayerTextureViewState(:final int textureId) => Texture( - textureId: textureId, - ), + VideoPlayerTextureViewState(:final int textureId) => Texture(textureId: textureId), VideoPlayerPlatformViewState() => PlatformViewPlayer(playerId: playerId), }; } @@ -266,14 +256,57 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { return true; } + @override + Future> getVideoTracks(int playerId) async { + final NativeVideoTrackData nativeData = await _playerWith( + id: playerId, + ).getVideoTracks(); + final tracks = []; + + // Convert ExoPlayer tracks to VideoTrack + if (nativeData.exoPlayerTracks != null) { + for (final ExoPlayerVideoTrackData track in nativeData.exoPlayerTracks!) { + // Construct a string ID from groupIndex and trackIndex for compatibility + final String trackId = '${track.groupIndex}_${track.trackIndex}'; + // Generate label from resolution if not provided + final String? label = + track.label ?? + (track.width != null && track.height != null ? '${track.height}p' : null); + tracks.add( + VideoTrack( + id: trackId, + isSelected: track.isSelected, + label: label, + bitrate: track.bitrate, + width: track.width, + height: track.height, + frameRate: track.frameRate, + codec: track.codec, + ), + ); + } + } + + return tracks; + } + + @override + Future selectVideoTrack(int playerId, VideoTrack? track) { + return _playerWith(id: playerId).selectVideoTrack(track); + } + + @override + bool isVideoTrackSupportAvailable() { + // Android with ExoPlayer supports video track selection + return true; + } + _PlayerInstance _playerWith({required int id}) { final _PlayerInstance? player = _players[id]; return player ?? (throw StateError('No active player with ID $id.')); } - PlatformVideoFormat? _platformVideoFormatFromVideoFormat( - VideoFormat? format, - ) { + PlatformVideoFormat? _platformVideoFormatFromVideoFormat(VideoFormat? format) { return switch (format) { VideoFormat.dash => PlatformVideoFormat.dash, VideoFormat.hls => PlatformVideoFormat.hls, @@ -314,6 +347,8 @@ class _PlayerInstance { int _lastBufferPosition = -1; bool _isBuffering = false; Completer? _audioTrackSelectionCompleter; + Completer? _videoTrackSelectionCompleter; + String? _expectedVideoTrackId; final VideoPlayerViewState viewState; @@ -384,6 +419,63 @@ class _PlayerInstance { } } + Future getVideoTracks() { + return _api.getVideoTracks(); + } + + Future selectVideoTrack(VideoTrack? track) async { + // Create a completer to wait for the track selection to complete + _videoTrackSelectionCompleter = Completer(); + + if (track == null) { + // Auto quality - pass -1, -1 to clear overrides + _expectedVideoTrackId = null; + try { + await _api.selectVideoTrack(-1, -1); + + // Wait for the onTracksChanged event from ExoPlayer with a timeout + await _videoTrackSelectionCompleter!.future.timeout( + const Duration(seconds: 5), + onTimeout: () { + // If we timeout, just continue - the track may still have been selected + }, + ); + } finally { + _videoTrackSelectionCompleter = null; + _expectedVideoTrackId = null; + } + return; + } + + // Extract groupIndex and trackIndex from the track id + final List parts = track.id.split('_'); + if (parts.length != 2) { + throw ArgumentError( + 'Invalid track id format: "${track.id}". Expected format: "groupIndex_trackIndex"', + ); + } + + final int groupIndex = int.parse(parts[0]); + final int trackIndex = int.parse(parts[1]); + + _expectedVideoTrackId = track.id; + + try { + await _api.selectVideoTrack(groupIndex, trackIndex); + + // Wait for the onTracksChanged event from ExoPlayer with a timeout + await _videoTrackSelectionCompleter!.future.timeout( + const Duration(seconds: 5), + onTimeout: () { + // If we timeout, just continue - the track may still have been selected + }, + ); + } finally { + _videoTrackSelectionCompleter = null; + _expectedVideoTrackId = null; + } + } + Future dispose() async { _isDisposed = true; _bufferPollingTimer?.cancel(); @@ -468,9 +560,7 @@ class _PlayerInstance { // should be synchronous with the state change. break; case PlatformPlaybackState.ended: - _eventStreamController.add( - VideoEvent(eventType: VideoEventType.completed), - ); + _eventStreamController.add(VideoEvent(eventType: VideoEventType.completed)); case PlatformPlaybackState.unknown: // Ignore unknown states. This isn't an error since the media // framework could add new states in the future. @@ -487,6 +577,19 @@ class _PlayerInstance { !_audioTrackSelectionCompleter!.isCompleted) { _audioTrackSelectionCompleter!.complete(); } + case VideoTrackChangedEvent _: + // Complete the video track selection completer only if: + // 1. A completer exists (we're waiting for a selection) + // 2. The completer hasn't already completed + // 3. The selected track ID matches what we're expecting (or we're expecting null for auto) + if (_videoTrackSelectionCompleter != null && + !_videoTrackSelectionCompleter!.isCompleted) { + // Complete if the track ID matches our expectation, or if we expected null (auto mode) + if (_expectedVideoTrackId == null || + event.selectedTrackId == _expectedVideoTrackId) { + _videoTrackSelectionCompleter!.complete(); + } + } } } diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index 1aca7dc531d..97571a2a499 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -17,33 +17,40 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } - bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && - a.indexed.every( - ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), - ); + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every( - (MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key]), - ); + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); } return a == b; } + /// Pigeon equivalent of video_platform_interface's VideoFormat. -enum PlatformVideoFormat { dash, hls, ss } +enum PlatformVideoFormat { + dash, + hls, + ss, +} /// Pigeon equivalent of Player's playback state. /// https://developer.android.com/media/media3/exoplayer/listening-to-player-events#playback-state -enum PlatformPlaybackState { idle, buffering, ready, ended, unknown } +enum PlatformPlaybackState { + idle, + buffering, + ready, + ended, + unknown, +} -sealed class PlatformVideoEvent {} +sealed class PlatformVideoEvent { +} /// Sent when the video is initialized and ready to play. class InitializationEvent extends PlatformVideoEvent { @@ -67,12 +74,16 @@ class InitializationEvent extends PlatformVideoEvent { int rotationCorrection; List _toList() { - return [duration, width, height, rotationCorrection]; + return [ + duration, + width, + height, + rotationCorrection, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static InitializationEvent decode(Object result) { result as List; @@ -98,35 +109,40 @@ class InitializationEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Sent when the video state changes. /// /// Corresponds to ExoPlayer's onPlaybackStateChanged. class PlaybackStateChangeEvent extends PlatformVideoEvent { - PlaybackStateChangeEvent({required this.state}); + PlaybackStateChangeEvent({ + required this.state, + }); PlatformPlaybackState state; List _toList() { - return [state]; + return [ + state, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlaybackStateChangeEvent decode(Object result) { result as List; - return PlaybackStateChangeEvent(state: result[0]! as PlatformPlaybackState); + return PlaybackStateChangeEvent( + state: result[0]! as PlatformPlaybackState, + ); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlaybackStateChangeEvent || - other.runtimeType != runtimeType) { + if (other is! PlaybackStateChangeEvent || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -137,28 +153,34 @@ class PlaybackStateChangeEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Sent when the video starts or stops playing. /// /// Corresponds to ExoPlayer's onIsPlayingChanged. class IsPlayingStateEvent extends PlatformVideoEvent { - IsPlayingStateEvent({required this.isPlaying}); + IsPlayingStateEvent({ + required this.isPlaying, + }); bool isPlaying; List _toList() { - return [isPlaying]; + return [ + isPlaying, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static IsPlayingStateEvent decode(Object result) { result as List; - return IsPlayingStateEvent(isPlaying: result[0]! as bool); + return IsPlayingStateEvent( + isPlaying: result[0]! as bool, + ); } @override @@ -175,7 +197,8 @@ class IsPlayingStateEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Sent when audio tracks change. @@ -183,22 +206,27 @@ class IsPlayingStateEvent extends PlatformVideoEvent { /// This includes when the selected audio track changes after calling selectAudioTrack. /// Corresponds to ExoPlayer's onTracksChanged. class AudioTrackChangedEvent extends PlatformVideoEvent { - AudioTrackChangedEvent({this.selectedTrackId}); + AudioTrackChangedEvent({ + this.selectedTrackId, + }); /// The ID of the newly selected audio track, if any. String? selectedTrackId; List _toList() { - return [selectedTrackId]; + return [ + selectedTrackId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static AudioTrackChangedEvent decode(Object result) { result as List; - return AudioTrackChangedEvent(selectedTrackId: result[0] as String?); + return AudioTrackChangedEvent( + selectedTrackId: result[0] as String?, + ); } @override @@ -215,33 +243,85 @@ class AudioTrackChangedEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Sent when video tracks change. +/// +/// This includes when the selected video track changes after calling selectVideoTrack. +/// Corresponds to ExoPlayer's onTracksChanged. +class VideoTrackChangedEvent extends PlatformVideoEvent { + VideoTrackChangedEvent({ + this.selectedTrackId, + }); + + /// The ID of the newly selected video track, if any. + /// Will be null when auto quality selection is enabled. + String? selectedTrackId; + + List _toList() { + return [ + selectedTrackId, + ]; + } + + Object encode() { + return _toList(); } + + static VideoTrackChangedEvent decode(Object result) { + result as List; + return VideoTrackChangedEvent( + selectedTrackId: result[0] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! VideoTrackChangedEvent || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; } /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { - PlatformVideoViewCreationParams({required this.playerId}); + PlatformVideoViewCreationParams({ + required this.playerId, + }); int playerId; List _toList() { - return [playerId]; + return [ + playerId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlatformVideoViewCreationParams decode(Object result) { result as List; - return PlatformVideoViewCreationParams(playerId: result[0]! as int); + return PlatformVideoViewCreationParams( + playerId: result[0]! as int, + ); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlatformVideoViewCreationParams || - other.runtimeType != runtimeType) { + if (other is! PlatformVideoViewCreationParams || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -252,7 +332,8 @@ class PlatformVideoViewCreationParams { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class CreationOptions { @@ -272,20 +353,23 @@ class CreationOptions { String? userAgent; List _toList() { - return [uri, formatHint, httpHeaders, userAgent]; + return [ + uri, + formatHint, + httpHeaders, + userAgent, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static CreationOptions decode(Object result) { result as List; return CreationOptions( uri: result[0]! as String, formatHint: result[1] as PlatformVideoFormat?, - httpHeaders: (result[2] as Map?)! - .cast(), + httpHeaders: (result[2] as Map?)!.cast(), userAgent: result[3] as String?, ); } @@ -304,23 +388,29 @@ class CreationOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class TexturePlayerIds { - TexturePlayerIds({required this.playerId, required this.textureId}); + TexturePlayerIds({ + required this.playerId, + required this.textureId, + }); int playerId; int textureId; List _toList() { - return [playerId, textureId]; + return [ + playerId, + textureId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static TexturePlayerIds decode(Object result) { result as List; @@ -344,11 +434,15 @@ class TexturePlayerIds { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class PlaybackState { - PlaybackState({required this.playPosition, required this.bufferPosition}); + PlaybackState({ + required this.playPosition, + required this.bufferPosition, + }); /// The current playback position, in milliseconds. int playPosition; @@ -357,12 +451,14 @@ class PlaybackState { int bufferPosition; List _toList() { - return [playPosition, bufferPosition]; + return [ + playPosition, + bufferPosition, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlaybackState decode(Object result) { result as List; @@ -386,7 +482,8 @@ class PlaybackState { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Represents an audio track in a video. @@ -432,8 +529,7 @@ class AudioTrackMessage { } Object encode() { - return _toList(); - } + return _toList(); } static AudioTrackMessage decode(Object result) { result as List; @@ -463,7 +559,8 @@ class AudioTrackMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Raw audio track data from ExoPlayer Format objects. @@ -513,8 +610,7 @@ class ExoPlayerAudioTrackData { } Object encode() { - return _toList(); - } + return _toList(); } static ExoPlayerAudioTrackData decode(Object result) { result as List; @@ -545,29 +641,32 @@ class ExoPlayerAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Container for raw audio track data from Android ExoPlayer. class NativeAudioTrackData { - NativeAudioTrackData({this.exoPlayerTracks}); + NativeAudioTrackData({ + this.exoPlayerTracks, + }); /// ExoPlayer-based tracks List? exoPlayerTracks; List _toList() { - return [exoPlayerTracks]; + return [ + exoPlayerTracks, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static NativeAudioTrackData decode(Object result) { result as List; return NativeAudioTrackData( - exoPlayerTracks: (result[0] as List?) - ?.cast(), + exoPlayerTracks: (result[0] as List?)?.cast(), ); } @@ -585,9 +684,136 @@ class NativeAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Raw video track data from ExoPlayer Format objects. +class ExoPlayerVideoTrackData { + ExoPlayerVideoTrackData({ + required this.groupIndex, + required this.trackIndex, + this.label, + required this.isSelected, + this.bitrate, + this.width, + this.height, + this.frameRate, + this.codec, + }); + + int groupIndex; + + int trackIndex; + + String? label; + + bool isSelected; + + int? bitrate; + + int? width; + + int? height; + + double? frameRate; + + String? codec; + + List _toList() { + return [ + groupIndex, + trackIndex, + label, + isSelected, + bitrate, + width, + height, + frameRate, + codec, + ]; + } + + Object encode() { + return _toList(); } + + static ExoPlayerVideoTrackData decode(Object result) { + result as List; + return ExoPlayerVideoTrackData( + groupIndex: result[0]! as int, + trackIndex: result[1]! as int, + label: result[2] as String?, + isSelected: result[3]! as bool, + bitrate: result[4] as int?, + width: result[5] as int?, + height: result[6] as int?, + frameRate: result[7] as double?, + codec: result[8] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! ExoPlayerVideoTrackData || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Container for raw video track data from Android ExoPlayer. +class NativeVideoTrackData { + NativeVideoTrackData({ + this.exoPlayerTracks, + }); + + /// ExoPlayer-based tracks + List? exoPlayerTracks; + + List _toList() { + return [ + exoPlayerTracks, + ]; + } + + Object encode() { + return _toList(); } + + static NativeVideoTrackData decode(Object result) { + result as List; + return NativeVideoTrackData( + exoPlayerTracks: (result[0] as List?)?.cast(), + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! NativeVideoTrackData || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; } + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -595,45 +821,54 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PlatformVideoFormat) { + } else if (value is PlatformVideoFormat) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is PlatformPlaybackState) { + } else if (value is PlatformPlaybackState) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is InitializationEvent) { + } else if (value is InitializationEvent) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is PlaybackStateChangeEvent) { + } else if (value is PlaybackStateChangeEvent) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is IsPlayingStateEvent) { + } else if (value is IsPlayingStateEvent) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is AudioTrackChangedEvent) { + } else if (value is AudioTrackChangedEvent) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is PlatformVideoViewCreationParams) { + } else if (value is VideoTrackChangedEvent) { buffer.putUint8(135); writeValue(buffer, value.encode()); - } else if (value is CreationOptions) { + } else if (value is PlatformVideoViewCreationParams) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is TexturePlayerIds) { + } else if (value is CreationOptions) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is PlaybackState) { + } else if (value is TexturePlayerIds) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is AudioTrackMessage) { + } else if (value is PlaybackState) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is ExoPlayerAudioTrackData) { + } else if (value is AudioTrackMessage) { buffer.putUint8(140); writeValue(buffer, value.encode()); - } else if (value is NativeAudioTrackData) { + } else if (value is ExoPlayerAudioTrackData) { buffer.putUint8(141); writeValue(buffer, value.encode()); + } else if (value is NativeAudioTrackData) { + buffer.putUint8(142); + writeValue(buffer, value.encode()); + } else if (value is ExoPlayerVideoTrackData) { + buffer.putUint8(143); + writeValue(buffer, value.encode()); + } else if (value is NativeVideoTrackData) { + buffer.putUint8(144); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -642,55 +877,55 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: final value = readValue(buffer) as int?; return value == null ? null : PlatformVideoFormat.values[value]; - case 130: + case 130: final value = readValue(buffer) as int?; return value == null ? null : PlatformPlaybackState.values[value]; - case 131: + case 131: return InitializationEvent.decode(readValue(buffer)!); - case 132: + case 132: return PlaybackStateChangeEvent.decode(readValue(buffer)!); - case 133: + case 133: return IsPlayingStateEvent.decode(readValue(buffer)!); - case 134: + case 134: return AudioTrackChangedEvent.decode(readValue(buffer)!); - case 135: + case 135: + return VideoTrackChangedEvent.decode(readValue(buffer)!); + case 136: return PlatformVideoViewCreationParams.decode(readValue(buffer)!); - case 136: + case 137: return CreationOptions.decode(readValue(buffer)!); - case 137: + case 138: return TexturePlayerIds.decode(readValue(buffer)!); - case 138: + case 139: return PlaybackState.decode(readValue(buffer)!); - case 139: + case 140: return AudioTrackMessage.decode(readValue(buffer)!); - case 140: + case 141: return ExoPlayerAudioTrackData.decode(readValue(buffer)!); - case 141: + case 142: return NativeAudioTrackData.decode(readValue(buffer)!); + case 143: + return ExoPlayerVideoTrackData.decode(readValue(buffer)!); + case 144: + return NativeVideoTrackData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } } } -const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec( - _PigeonCodec(), -); +const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec(_PigeonCodec()); class AndroidVideoPlayerApi { /// Constructor for [AndroidVideoPlayerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AndroidVideoPlayerApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + AndroidVideoPlayerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -698,8 +933,7 @@ class AndroidVideoPlayerApi { final String pigeonVar_messageChannelSuffix; Future initialize() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -721,16 +955,13 @@ class AndroidVideoPlayerApi { } Future createForPlatformView(CreationOptions options) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [options], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -751,16 +982,13 @@ class AndroidVideoPlayerApi { } Future createForTextureView(CreationOptions options) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [options], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -781,16 +1009,13 @@ class AndroidVideoPlayerApi { } Future dispose(int playerId) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [playerId], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -806,16 +1031,13 @@ class AndroidVideoPlayerApi { } Future setMixWithOthers(bool mixWithOthers) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [mixWithOthers], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -831,16 +1053,13 @@ class AndroidVideoPlayerApi { } Future getLookupKeyForAsset(String asset, String? packageName) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [asset, packageName], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([asset, packageName]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -865,13 +1084,9 @@ class VideoPlayerInstanceApi { /// Constructor for [VideoPlayerInstanceApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - VideoPlayerInstanceApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + VideoPlayerInstanceApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -880,16 +1095,13 @@ class VideoPlayerInstanceApi { /// Sets whether to automatically loop playback of the video. Future setLooping(bool looping) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [looping], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([looping]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -906,16 +1118,13 @@ class VideoPlayerInstanceApi { /// Sets the volume, with 0.0 being muted and 1.0 being full volume. Future setVolume(double volume) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [volume], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -932,16 +1141,13 @@ class VideoPlayerInstanceApi { /// Sets the playback speed as a multiple of normal speed. Future setPlaybackSpeed(double speed) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [speed], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([speed]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -958,8 +1164,7 @@ class VideoPlayerInstanceApi { /// Begins playback if the video is not currently playing. Future play() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -982,8 +1187,7 @@ class VideoPlayerInstanceApi { /// Pauses playback if the video is currently playing. Future pause() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1006,16 +1210,13 @@ class VideoPlayerInstanceApi { /// Seeks to the given playback position, in milliseconds. Future seekTo(int position) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [position], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([position]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1032,8 +1233,7 @@ class VideoPlayerInstanceApi { /// Returns the current playback position, in milliseconds. Future getCurrentPosition() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1061,8 +1261,7 @@ class VideoPlayerInstanceApi { /// Returns the current buffer position, in milliseconds. Future getBufferedPosition() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1090,8 +1289,7 @@ class VideoPlayerInstanceApi { /// Gets the available audio tracks for the video. Future getAudioTracks() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1119,16 +1317,65 @@ class VideoPlayerInstanceApi { /// Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] Future selectAudioTrack(int groupIndex, int trackIndex) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([groupIndex, trackIndex]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + /// Gets the available video tracks for the video. + Future getVideoTracks() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getVideoTracks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [groupIndex, trackIndex], + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as NativeVideoTrackData?)!; + } + } + + /// Selects which video track is chosen for playback from its [groupIndex] and [trackIndex]. + /// Pass -1 for both indices to enable auto quality selection. + Future selectVideoTrack(int groupIndex, int trackIndex) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectVideoTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([groupIndex, trackIndex]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1144,15 +1391,14 @@ class VideoPlayerInstanceApi { } } -Stream videoEvents({String instanceName = ''}) { +Stream videoEvents( {String instanceName = ''}) { if (instanceName.isNotEmpty) { instanceName = '.$instanceName'; } - final EventChannel videoEventsChannel = EventChannel( - 'dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents$instanceName', - pigeonMethodCodec, - ); + final EventChannel videoEventsChannel = + EventChannel('dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents$instanceName', pigeonMethodCodec); return videoEventsChannel.receiveBroadcastStream().map((dynamic event) { return event as PlatformVideoEvent; }); } + diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index 8666b074969..a9904dd6253 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -7,8 +7,7 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( dartOut: 'lib/src/messages.g.dart', - kotlinOut: - 'android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt', + kotlinOut: 'android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt', kotlinOptions: KotlinOptions(package: 'io.flutter.plugins.videoplayer'), copyrightHeader: 'pigeons/copyright.txt', ), @@ -60,6 +59,16 @@ class AudioTrackChangedEvent extends PlatformVideoEvent { late final String? selectedTrackId; } +/// Sent when video tracks change. +/// +/// This includes when the selected video track changes after calling selectVideoTrack. +/// Corresponds to ExoPlayer's onTracksChanged. +class VideoTrackChangedEvent extends PlatformVideoEvent { + /// The ID of the newly selected video track, if any. + /// Will be null when auto quality selection is enabled. + late final String? selectedTrackId; +} + /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { const PlatformVideoViewCreationParams({required this.playerId}); @@ -148,6 +157,39 @@ class NativeAudioTrackData { List? exoPlayerTracks; } +/// Raw video track data from ExoPlayer Format objects. +class ExoPlayerVideoTrackData { + ExoPlayerVideoTrackData({ + required this.groupIndex, + required this.trackIndex, + this.label, + required this.isSelected, + this.bitrate, + this.width, + this.height, + this.frameRate, + this.codec, + }); + + int groupIndex; + int trackIndex; + String? label; + bool isSelected; + int? bitrate; + int? width; + int? height; + double? frameRate; + String? codec; +} + +/// Container for raw video track data from Android ExoPlayer. +class NativeVideoTrackData { + NativeVideoTrackData({this.exoPlayerTracks}); + + /// ExoPlayer-based tracks + List? exoPlayerTracks; +} + @HostApi() abstract class AndroidVideoPlayerApi { void initialize(); @@ -192,6 +234,13 @@ abstract class VideoPlayerInstanceApi { /// Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] void selectAudioTrack(int groupIndex, int trackIndex); + + /// Gets the available video tracks for the video. + NativeVideoTrackData getVideoTracks(); + + /// Selects which video track is chosen for playback from its [groupIndex] and [trackIndex]. + /// Pass -1 for both indices to enable auto quality selection. + void selectVideoTrack(int groupIndex, int trackIndex); } @EventChannelApi() diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index dccca416899..818f4039bf1 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -20,7 +20,8 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^6.6.0 + video_player_platform_interface: + path: ../video_player_platform_interface dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart index 810a815fddf..a42679bc0cd 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -31,6 +31,15 @@ void main() { NativeAudioTrackData(exoPlayerTracks: []), ), ); + // Provide dummy values for video track types + provideDummy( + NativeVideoTrackData(exoPlayerTracks: []), + ); + provideDummy>( + Future.value( + NativeVideoTrackData(exoPlayerTracks: []), + ), + ); provideDummy>(Future.value()); (AndroidVideoPlayer, MockAndroidVideoPlayerApi, MockVideoPlayerInstanceApi) @@ -63,8 +72,7 @@ void main() { final player = AndroidVideoPlayer( pluginApi: pluginApi, playerApiProvider: (_) => instanceApi, - videoEventStreamProvider: (_) => - streamController.stream.asBroadcastStream(), + videoEventStreamProvider: (_) => streamController.stream.asBroadcastStream(), ); player.ensurePlayerInitialized( playerId, @@ -101,23 +109,17 @@ void main() { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 100); const newPlayerId = 2; - when(api.createForTextureView(any)).thenAnswer( - (_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100), - ); + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100)); const asset = 'someAsset'; const package = 'somePackage'; const assetKey = 'resultingAssetKey'; - when( - api.getLookupKeyForAsset(asset, package), - ).thenAnswer((_) async => assetKey); + when(api.getLookupKeyForAsset(asset, package)).thenAnswer((_) async => assetKey); final int? playerId = await player.create( - DataSource( - sourceType: DataSourceType.asset, - asset: asset, - package: package, - ), + DataSource(sourceType: DataSourceType.asset, asset: asset, package: package), ); final VerificationResult verification = verify( @@ -136,9 +138,9 @@ void main() { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 100); const newPlayerId = 2; - when(api.createForTextureView(any)).thenAnswer( - (_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100), - ); + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100)); const uri = 'https://example.com'; final int? playerId = await player.create( @@ -261,11 +263,7 @@ void main() { const fileUri = 'file:///foo/bar'; const headers = {'Authorization': 'Bearer token'}; await player.create( - DataSource( - sourceType: DataSourceType.file, - uri: fileUri, - httpHeaders: headers, - ), + DataSource(sourceType: DataSourceType.file, uri: fileUri, httpHeaders: headers), ); final VerificationResult verification = verify( api.createForTextureView(captureAny), @@ -278,16 +276,14 @@ void main() { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 100); const newPlayerId = 2; - when(api.createForTextureView(any)).thenAnswer( - (_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100), - ); + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100)); const asset = 'someAsset'; const package = 'somePackage'; const assetKey = 'resultingAssetKey'; - when( - api.getLookupKeyForAsset(asset, package), - ).thenAnswer((_) async => assetKey); + when(api.getLookupKeyForAsset(asset, package)).thenAnswer((_) async => assetKey); final int? playerId = await player.createWithOptions( VideoCreationOptions( @@ -316,9 +312,9 @@ void main() { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 100); const newPlayerId = 2; - when(api.createForTextureView(any)).thenAnswer( - (_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100), - ); + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100)); const uri = 'https://example.com'; final int? playerId = await player.createWithOptions( @@ -350,9 +346,9 @@ void main() { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 100); const newPlayerId = 2; - when(api.createForTextureView(any)).thenAnswer( - (_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100), - ); + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100)); const headers = {'Authorization': 'Bearer token'}; final int? playerId = await player.createWithOptions( @@ -378,9 +374,9 @@ void main() { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 100); const newPlayerId = 2; - when(api.createForTextureView(any)).thenAnswer( - (_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100), - ); + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100)); const fileUri = 'file:///foo/bar'; final int? playerId = await player.createWithOptions( @@ -456,39 +452,24 @@ void main() { }); test('setLooping', () async { - final ( - AndroidVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = + setUpMockPlayer(playerId: 1); await player.setLooping(1, true); verify(playerApi.setLooping(true)); }); test('play', () async { - final ( - AndroidVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = + setUpMockPlayer(playerId: 1); await player.play(1); verify(playerApi.play()); }); test('pause', () async { - final ( - AndroidVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = + setUpMockPlayer(playerId: 1); await player.pause(1); verify(playerApi.pause()); @@ -513,13 +494,8 @@ void main() { }); test('setVolume', () async { - final ( - AndroidVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = + setUpMockPlayer(playerId: 1); const volume = 0.7; await player.setVolume(1, volume); @@ -527,13 +503,8 @@ void main() { }); test('setPlaybackSpeed', () async { - final ( - AndroidVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = + setUpMockPlayer(playerId: 1); const speed = 1.5; await player.setPlaybackSpeed(1, speed); @@ -541,34 +512,19 @@ void main() { }); test('seekTo', () async { - final ( - AndroidVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = + setUpMockPlayer(playerId: 1); const positionMilliseconds = 12345; - await player.seekTo( - 1, - const Duration(milliseconds: positionMilliseconds), - ); + await player.seekTo(1, const Duration(milliseconds: positionMilliseconds)); verify(playerApi.seekTo(positionMilliseconds)); }); test('getPosition', () async { - final ( - AndroidVideoPlayer player, - _, - MockVideoPlayerInstanceApi playerApi, - ) = setUpMockPlayer( - playerId: 1, - ); + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = + setUpMockPlayer(playerId: 1); const positionMilliseconds = 12345; - when( - playerApi.getCurrentPosition(), - ).thenAnswer((_) async => positionMilliseconds); + when(playerApi.getCurrentPosition()).thenAnswer((_) async => positionMilliseconds); final Duration position = await player.getPosition(1); expect(position, const Duration(milliseconds: positionMilliseconds)); @@ -578,9 +534,7 @@ void main() { // Sets up a mock player that emits the given event structure as a success // callback on the internal platform channel event stream, and returns // the player's videoEventsFor(...) stream. - Stream mockPlayerEmitingEvents( - List events, - ) { + Stream mockPlayerEmitingEvents(List events) { const playerId = 1; final ( AndroidVideoPlayer player, @@ -597,15 +551,16 @@ void main() { } test('initialize', () async { - final Stream eventStream = - mockPlayerEmitingEvents([ - InitializationEvent( - duration: 98765, - width: 1920, - height: 1080, - rotationCorrection: 90, - ), - ]); + final Stream eventStream = mockPlayerEmitingEvents( + [ + InitializationEvent( + duration: 98765, + width: 1920, + height: 1080, + rotationCorrection: 90, + ), + ], + ); expect( eventStream, @@ -621,15 +576,16 @@ void main() { }); test('initialization triggers buffer update polling', () async { - final Stream eventStream = - mockPlayerEmitingEvents([ - InitializationEvent( - duration: 98765, - width: 1920, - height: 1080, - rotationCorrection: 90, - ), - ]); + final Stream eventStream = mockPlayerEmitingEvents( + [ + InitializationEvent( + duration: 98765, + width: 1920, + height: 1080, + rotationCorrection: 90, + ), + ], + ); expect( eventStream, @@ -642,9 +598,7 @@ void main() { ), VideoEvent( eventType: VideoEventType.bufferingUpdate, - buffered: [ - DurationRange(Duration.zero, Duration.zero), - ], + buffered: [DurationRange(Duration.zero, Duration.zero)], ), ]), ); @@ -659,9 +613,7 @@ void main() { expect( eventStream, - emitsInOrder([ - VideoEvent(eventType: VideoEventType.completed), - ]), + emitsInOrder([VideoEvent(eventType: VideoEventType.completed)]), ); }); @@ -679,9 +631,7 @@ void main() { // A buffer start should trigger a buffer update as well. VideoEvent( eventType: VideoEventType.bufferingUpdate, - buffered: [ - DurationRange(Duration.zero, Duration.zero), - ], + buffered: [DurationRange(Duration.zero, Duration.zero)], ), ]), ); @@ -704,9 +654,7 @@ void main() { VideoEvent(eventType: VideoEventType.bufferingStart), VideoEvent( eventType: VideoEventType.bufferingUpdate, - buffered: [ - DurationRange(Duration.zero, Duration.zero), - ], + buffered: [DurationRange(Duration.zero, Duration.zero)], ), // Emitted by ready. VideoEvent(eventType: VideoEventType.bufferingEnd), @@ -731,9 +679,7 @@ void main() { VideoEvent(eventType: VideoEventType.bufferingStart), VideoEvent( eventType: VideoEventType.bufferingUpdate, - buffered: [ - DurationRange(Duration.zero, Duration.zero), - ], + buffered: [DurationRange(Duration.zero, Duration.zero)], ), // Emitted by ready. VideoEvent(eventType: VideoEventType.bufferingEnd), @@ -758,9 +704,7 @@ void main() { VideoEvent(eventType: VideoEventType.bufferingStart), VideoEvent( eventType: VideoEventType.bufferingUpdate, - buffered: [ - DurationRange(Duration.zero, Duration.zero), - ], + buffered: [DurationRange(Duration.zero, Duration.zero)], ), // Emitted by ended. VideoEvent(eventType: VideoEventType.completed), @@ -777,10 +721,7 @@ void main() { expect( eventStream, emitsInOrder([ - VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: true, - ), + VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: true), ]), ); }); @@ -793,10 +734,7 @@ void main() { expect( eventStream, emitsInOrder([ - VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: false, - ), + VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: false), ]), ); }); @@ -813,9 +751,7 @@ void main() { final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = setUpMockPlayer(playerId: 1); when(api.getAudioTracks()).thenAnswer( - (_) async => NativeAudioTrackData( - exoPlayerTracks: [], - ), + (_) async => NativeAudioTrackData(exoPlayerTracks: []), ); final List tracks = await player.getAudioTracks(1); @@ -824,66 +760,61 @@ void main() { verify(api.getAudioTracks()); }); - test( - 'getAudioTracks converts native tracks to VideoAudioTrack', - () async { - final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = - setUpMockPlayer(playerId: 1); - when(api.getAudioTracks()).thenAnswer( - (_) async => NativeAudioTrackData( - exoPlayerTracks: [ - ExoPlayerAudioTrackData( - groupIndex: 0, - trackIndex: 1, - label: 'English', - language: 'en', - isSelected: true, - bitrate: 128000, - sampleRate: 44100, - channelCount: 2, - codec: 'mp4a.40.2', - ), - ExoPlayerAudioTrackData( - groupIndex: 0, - trackIndex: 2, - label: 'Spanish', - language: 'es', - isSelected: false, - bitrate: 128000, - sampleRate: 44100, - channelCount: 2, - codec: 'mp4a.40.2', - ), - ], - ), - ); - - final List tracks = await player.getAudioTracks(1); - - expect(tracks.length, 2); + test('getAudioTracks converts native tracks to VideoAudioTrack', () async { + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = + setUpMockPlayer(playerId: 1); + when(api.getAudioTracks()).thenAnswer( + (_) async => NativeAudioTrackData( + exoPlayerTracks: [ + ExoPlayerAudioTrackData( + groupIndex: 0, + trackIndex: 1, + label: 'English', + language: 'en', + isSelected: true, + bitrate: 128000, + sampleRate: 44100, + channelCount: 2, + codec: 'mp4a.40.2', + ), + ExoPlayerAudioTrackData( + groupIndex: 0, + trackIndex: 2, + label: 'Spanish', + language: 'es', + isSelected: false, + bitrate: 128000, + sampleRate: 44100, + channelCount: 2, + codec: 'mp4a.40.2', + ), + ], + ), + ); - expect(tracks[0].id, '0_1'); - expect(tracks[0].label, 'English'); - expect(tracks[0].language, 'en'); - expect(tracks[0].isSelected, true); - expect(tracks[0].bitrate, 128000); - expect(tracks[0].sampleRate, 44100); - expect(tracks[0].channelCount, 2); - expect(tracks[0].codec, 'mp4a.40.2'); + final List tracks = await player.getAudioTracks(1); - expect(tracks[1].id, '0_2'); - expect(tracks[1].label, 'Spanish'); - expect(tracks[1].language, 'es'); - expect(tracks[1].isSelected, false); - }, - ); + expect(tracks.length, 2); + + expect(tracks[0].id, '0_1'); + expect(tracks[0].label, 'English'); + expect(tracks[0].language, 'en'); + expect(tracks[0].isSelected, true); + expect(tracks[0].bitrate, 128000); + expect(tracks[0].sampleRate, 44100); + expect(tracks[0].channelCount, 2); + expect(tracks[0].codec, 'mp4a.40.2'); + + expect(tracks[1].id, '0_2'); + expect(tracks[1].label, 'Spanish'); + expect(tracks[1].language, 'es'); + expect(tracks[1].isSelected, false); + }); test('getAudioTracks handles null exoPlayerTracks', () async { final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = setUpMockPlayer(playerId: 1); - when( - api.getAudioTracks(), - ).thenAnswer((_) async => NativeAudioTrackData()); + when(api.getAudioTracks()).thenAnswer((_) async => NativeAudioTrackData()); final List tracks = await player.getAudioTracks(1); @@ -921,10 +852,7 @@ void main() { test('selectAudioTrack throws on trackId with too many parts', () async { final (AndroidVideoPlayer player, _, _) = setUpMockPlayer(playerId: 1); - expect( - () => player.selectAudioTrack(1, '1_2_3'), - throwsA(isA()), - ); + expect(() => player.selectAudioTrack(1, '1_2_3'), throwsA(isA())); }); test('selectAudioTrack completes on AudioTrackChangedEvent', () async { @@ -950,5 +878,144 @@ void main() { verify(api.selectAudioTrack(0, 1)); }); }); + + group('video tracks', () { + test('isVideoTrackSupportAvailable returns true', () { + final (AndroidVideoPlayer player, _, _) = setUpMockPlayer(playerId: 1); + + expect(player.isVideoTrackSupportAvailable(), true); + }); + + test('getVideoTracks returns empty list when no tracks', () async { + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = + setUpMockPlayer(playerId: 1); + when(api.getVideoTracks()).thenAnswer((_) async => NativeVideoTrackData()); + + final List tracks = await player.getVideoTracks(1); + + expect(tracks, isEmpty); + }); + + test('getVideoTracks converts native tracks to VideoTrack', () async { + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = + setUpMockPlayer(playerId: 1); + when(api.getVideoTracks()).thenAnswer( + (_) async => NativeVideoTrackData( + exoPlayerTracks: [ + ExoPlayerVideoTrackData( + groupIndex: 0, + trackIndex: 0, + label: '1080p', + isSelected: true, + bitrate: 5000000, + width: 1920, + height: 1080, + frameRate: 30.0, + codec: 'avc1.64001f', + ), + ExoPlayerVideoTrackData( + groupIndex: 0, + trackIndex: 1, + label: '720p', + isSelected: false, + bitrate: 2500000, + width: 1280, + height: 720, + frameRate: 30.0, + codec: 'avc1.64001f', + ), + ], + ), + ); + + final List tracks = await player.getVideoTracks(1); + + expect(tracks.length, 2); + + expect(tracks[0].id, '0_0'); + expect(tracks[0].label, '1080p'); + expect(tracks[0].isSelected, true); + expect(tracks[0].bitrate, 5000000); + expect(tracks[0].width, 1920); + expect(tracks[0].height, 1080); + expect(tracks[0].frameRate, 30.0); + expect(tracks[0].codec, 'avc1.64001f'); + + expect(tracks[1].id, '0_1'); + expect(tracks[1].label, '720p'); + expect(tracks[1].isSelected, false); + expect(tracks[1].bitrate, 2500000); + expect(tracks[1].width, 1280); + expect(tracks[1].height, 720); + }); + + test('getVideoTracks generates label from resolution if not provided', () async { + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = + setUpMockPlayer(playerId: 1); + when(api.getVideoTracks()).thenAnswer( + (_) async => NativeVideoTrackData( + exoPlayerTracks: [ + ExoPlayerVideoTrackData( + groupIndex: 0, + trackIndex: 0, + isSelected: true, + width: 1920, + height: 1080, + ), + ], + ), + ); + + final List tracks = await player.getVideoTracks(1); + + expect(tracks.length, 1); + expect(tracks[0].label, '1080p'); + }); + + test('getVideoTracks handles null exoPlayerTracks', () async { + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = + setUpMockPlayer(playerId: 1); + when(api.getVideoTracks()).thenAnswer((_) async => NativeVideoTrackData()); + + final List tracks = await player.getVideoTracks(1); + + expect(tracks, isEmpty); + }); + + test('selectVideoTrack with null clears override (auto quality)', () async { + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = + setUpMockPlayer(playerId: 1); + when(api.selectVideoTrack(-1, -1)).thenAnswer((_) async {}); + + await player.selectVideoTrack(1, null); + + verify(api.selectVideoTrack(-1, -1)); + }); + + test('selectVideoTrack parses track id and calls API', () async { + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = + setUpMockPlayer(playerId: 1); + when(api.selectVideoTrack(0, 2)).thenAnswer((_) async {}); + + const track = VideoTrack(id: '0_2', isSelected: false); + await player.selectVideoTrack(1, track); + + verify(api.selectVideoTrack(0, 2)); + }); + + test('selectVideoTrack throws on invalid track id format', () async { + final (AndroidVideoPlayer player, _, _) = setUpMockPlayer(playerId: 1); + + const track = VideoTrack(id: 'invalid', isSelected: false); + expect(() => player.selectVideoTrack(1, track), throwsA(isA())); + }); + + test('selectVideoTrack throws on track id with too many parts', () async { + final (AndroidVideoPlayer player, _, _) = setUpMockPlayer(playerId: 1); + + const track = VideoTrack(id: '1_2_3', isSelected: false); + expect(() => player.selectVideoTrack(1, track), throwsA(isA())); + }); + }); }); } diff --git a/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart b/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart index 212c9bde40c..b3056d1ba71 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart @@ -22,6 +22,7 @@ import 'package:video_player_android/src/messages.g.dart' as _i2; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeTexturePlayerIds_0 extends _i1.SmartFake implements _i2.TexturePlayerIds { @@ -35,6 +36,12 @@ class _FakeNativeAudioTrackData_1 extends _i1.SmartFake : super(parent, parentInvocation); } +class _FakeNativeVideoTrackData_2 extends _i1.SmartFake + implements _i2.NativeVideoTrackData { + _FakeNativeVideoTrackData_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + /// A class which mocks [AndroidVideoPlayerApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -252,4 +259,33 @@ class MockVideoPlayerInstanceApi extends _i1.Mock returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + + @override + _i4.Future<_i2.NativeVideoTrackData> getVideoTracks() => + (super.noSuchMethod( + Invocation.method(#getVideoTracks, []), + returnValue: _i4.Future<_i2.NativeVideoTrackData>.value( + _FakeNativeVideoTrackData_2( + this, + Invocation.method(#getVideoTracks, []), + ), + ), + returnValueForMissingStub: + _i4.Future<_i2.NativeVideoTrackData>.value( + _FakeNativeVideoTrackData_2( + this, + Invocation.method(#getVideoTracks, []), + ), + ), + ) + as _i4.Future<_i2.NativeVideoTrackData>); + + @override + _i4.Future selectVideoTrack(int? groupIndex, int? trackIndex) => + (super.noSuchMethod( + Invocation.method(#selectVideoTrack, [groupIndex, trackIndex]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); } diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index a0e403be5dc..a073d786e79 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Updates minimum supported SDK version to Flutter 3.32/Dart 3.8. +* Adds `VideoTrack` class and `getVideoTracks()`, `selectVideoTrack()`, `isVideoTrackSupportAvailable()` methods for video track (quality) selection. ## 6.6.0 diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 1cec5f42c21..7cae85fbc00 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -119,9 +119,7 @@ abstract class VideoPlayerPlatform extends PlatformInterface { /// Sets whether the video should continue to play in the background. Future setAllowBackgroundPlayback(bool allowBackgroundPlayback) { - throw UnimplementedError( - 'setAllowBackgroundPlayback() has not been implemented.', - ); + throw UnimplementedError('setAllowBackgroundPlayback() has not been implemented.'); } /// Sets additional options on web. @@ -153,6 +151,46 @@ abstract class VideoPlayerPlatform extends PlatformInterface { bool isAudioTrackSupportAvailable() { return false; } + + /// Gets the available video tracks (quality variants) for the video. + /// + /// Returns a list of [VideoTrack] objects representing the available + /// video quality variants. For HLS/DASH streams, this returns the different + /// quality levels available. For regular videos, this may return a single + /// track or an empty list. + /// + /// Note: On iOS 13-14, this returns an empty list as the AVAssetVariant API + /// requires iOS 15+. + Future> getVideoTracks(int playerId) { + throw UnimplementedError('getVideoTracks() has not been implemented.'); + } + + /// Selects which video track (quality variant) is chosen for playback. + /// + /// Pass a [VideoTrack] to select a specific quality. + /// Pass `null` to enable automatic quality selection (adaptive streaming). + /// + /// On iOS, this sets `preferredPeakBitRate` on the AVPlayerItem. + /// On Android, this uses ExoPlayer's track selection override. + Future selectVideoTrack(int playerId, VideoTrack? track) { + throw UnimplementedError('selectVideoTrack() has not been implemented.'); + } + + /// Returns whether video track selection is supported on this platform. + /// + /// This method allows developers to query at runtime whether the current + /// platform supports video track (quality) selection functionality. This is + /// useful for platforms like web where video track selection may not be + /// available. + /// + /// Returns `true` if [getVideoTracks] and [selectVideoTrack] are supported, + /// `false` otherwise. + /// + /// The default implementation returns `false`. Platform implementations + /// should override this to return `true` if they support video track selection. + bool isVideoTrackSupportAvailable() { + return false; + } } class _PlaceholderImplementation extends VideoPlayerPlatform {} @@ -317,14 +355,8 @@ class VideoEvent { } @override - int get hashCode => Object.hash( - eventType, - duration, - size, - rotationCorrection, - buffered, - isPlaying, - ); + int get hashCode => + Object.hash(eventType, duration, size, rotationCorrection, buffered, isPlaying); } /// Type of the event. @@ -550,10 +582,7 @@ class VideoViewOptions { @immutable class VideoCreationOptions { /// Constructs an instance of [VideoCreationOptions]. - const VideoCreationOptions({ - required this.dataSource, - required this.viewType, - }); + const VideoCreationOptions({required this.dataSource, required this.viewType}); /// The data source used to create the player. final DataSource dataSource; @@ -652,3 +681,94 @@ class VideoAudioTrack { 'channelCount: $channelCount, ' 'codec: $codec)'; } + +/// Represents a video track (quality variant) in a video with its metadata. +/// +/// For HLS/DASH streams, each [VideoTrack] represents a different quality +/// level (e.g., 1080p, 720p, 480p). For regular videos, there may be only +/// one track or none available. +@immutable +class VideoTrack { + /// Constructs an instance of [VideoTrack]. + const VideoTrack({ + required this.id, + required this.isSelected, + this.label, + this.bitrate, + this.width, + this.height, + this.frameRate, + this.codec, + }); + + /// Unique identifier for the video track. + /// + /// The format is platform-specific: + /// - Android: `"{groupIndex}_{trackIndex}"` (e.g., `"0_2"`) + /// - iOS: `"variant_{bitrate}"` for HLS, `"asset_{trackID}"` for regular videos + final String id; + + /// Whether this track is currently selected. + final bool isSelected; + + /// Human-readable label for the track (e.g., "1080p", "720p"). + /// + /// May be null if not available from the platform. + final String? label; + + /// Bitrate of the video track in bits per second. + /// + /// May be null if not available from the platform. + final int? bitrate; + + /// Video width in pixels. + /// + /// May be null if not available from the platform. + final int? width; + + /// Video height in pixels. + /// + /// May be null if not available from the platform. + final int? height; + + /// Frame rate in frames per second. + /// + /// May be null if not available from the platform. + final double? frameRate; + + /// Video codec used (e.g., "avc1", "hevc", "vp9"). + /// + /// May be null if not available from the platform. + final String? codec; + + @override + bool operator ==(Object other) { + return identical(this, other) || + other is VideoTrack && + runtimeType == other.runtimeType && + id == other.id && + isSelected == other.isSelected && + label == other.label && + bitrate == other.bitrate && + width == other.width && + height == other.height && + frameRate == other.frameRate && + codec == other.codec; + } + + @override + int get hashCode => + Object.hash(id, isSelected, label, bitrate, width, height, frameRate, codec); + + @override + String toString() => + 'VideoTrack(' + 'id: $id, ' + 'isSelected: $isSelected, ' + 'label: $label, ' + 'bitrate: $bitrate, ' + 'width: $width, ' + 'height: $height, ' + 'frameRate: $frameRate, ' + 'codec: $codec)'; +} diff --git a/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart b/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart index 2d920161ec9..454576cf166 100644 --- a/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart +++ b/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart @@ -40,4 +40,124 @@ void main() { test('default implementation isAudioTrackSupportAvailable returns false', () { expect(initialInstance.isAudioTrackSupportAvailable(), false); }); + + test('default implementation getVideoTracks throws unimplemented', () async { + await expectLater( + () => initialInstance.getVideoTracks(1), + throwsUnimplementedError, + ); + }); + + test( + 'default implementation selectVideoTrack throws unimplemented', + () async { + await expectLater( + () => initialInstance.selectVideoTrack( + 1, + const VideoTrack(id: 'test', isSelected: false), + ), + throwsUnimplementedError, + ); + }, + ); + + test('default implementation isVideoTrackSupportAvailable returns false', () { + expect(initialInstance.isVideoTrackSupportAvailable(), false); + }); + + group('VideoTrack', () { + test('constructor creates instance with required fields', () { + const track = VideoTrack(id: 'track_1', isSelected: true); + expect(track.id, 'track_1'); + expect(track.isSelected, true); + expect(track.label, isNull); + expect(track.bitrate, isNull); + expect(track.width, isNull); + expect(track.height, isNull); + expect(track.frameRate, isNull); + expect(track.codec, isNull); + }); + + test('constructor creates instance with all fields', () { + const track = VideoTrack( + id: 'track_1', + isSelected: true, + label: '1080p', + bitrate: 5000000, + width: 1920, + height: 1080, + frameRate: 30.0, + codec: 'avc1', + ); + expect(track.id, 'track_1'); + expect(track.isSelected, true); + expect(track.label, '1080p'); + expect(track.bitrate, 5000000); + expect(track.width, 1920); + expect(track.height, 1080); + expect(track.frameRate, 30.0); + expect(track.codec, 'avc1'); + }); + + test('equality works correctly', () { + const track1 = VideoTrack( + id: 'track_1', + isSelected: true, + label: '1080p', + bitrate: 5000000, + ); + const track2 = VideoTrack( + id: 'track_1', + isSelected: true, + label: '1080p', + bitrate: 5000000, + ); + const track3 = VideoTrack( + id: 'track_2', + isSelected: false, + ); + + expect(track1, equals(track2)); + expect(track1, isNot(equals(track3))); + }); + + test('hashCode is consistent with equality', () { + const track1 = VideoTrack( + id: 'track_1', + isSelected: true, + label: '1080p', + ); + const track2 = VideoTrack( + id: 'track_1', + isSelected: true, + label: '1080p', + ); + + expect(track1.hashCode, equals(track2.hashCode)); + }); + + test('toString returns expected format', () { + const track = VideoTrack( + id: 'track_1', + isSelected: true, + label: '1080p', + bitrate: 5000000, + width: 1920, + height: 1080, + frameRate: 30.0, + codec: 'avc1', + ); + + final str = track.toString(); + expect(str, contains('VideoTrack')); + expect(str, contains('id: track_1')); + expect(str, contains('isSelected: true')); + expect(str, contains('label: 1080p')); + expect(str, contains('bitrate: 5000000')); + expect(str, contains('width: 1920')); + expect(str, contains('height: 1080')); + expect(str, contains('frameRate: 30.0')); + expect(str, contains('codec: avc1')); + }); + }); } From 36626305370f77b15a09f99f1f1d06d10961a8eb Mon Sep 17 00:00:00 2001 From: nateshmbhat Date: Sat, 27 Dec 2025 21:25:48 +0530 Subject: [PATCH 2/7] feat(ios) : adds ios implementation for selecting video track --- .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../video_player/example/ios/Podfile | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 24 +- .../video_player/example/macos/Podfile | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 24 +- .../plugins/videoplayer/VideoPlayerTest.java | 397 ++++++++++++ .../video_player_avfoundation/CHANGELOG.md | 5 + .../darwin/RunnerTests/VideoPlayerTests.m | 222 +++++++ .../FVPVideoPlayer.m | 129 ++++ .../video_player_avfoundation/messages.g.h | 104 +++- .../video_player_avfoundation/messages.g.m | 441 +++++++++----- .../example/pubspec.yaml | 3 +- .../lib/src/avfoundation_video_player.dart | 107 +++- .../lib/src/messages.g.dart | 574 ++++++++++++------ .../pigeons/messages.dart | 65 ++ .../video_player_avfoundation/pubspec.yaml | 3 +- .../test/avfoundation_video_player_test.dart | 166 +++++ .../avfoundation_video_player_test.mocks.dart | 35 ++ .../video_player_web/CHANGELOG.md | 1 + .../video_player_web/example/pubspec.yaml | 3 +- .../lib/video_player_web.dart | 19 +- .../video_player_web/pubspec.yaml | 3 +- 22 files changed, 1940 insertions(+), 391 deletions(-) diff --git a/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist b/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist index 7c569640062..1dc6cf7652b 100644 --- a/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/packages/video_player/video_player/example/ios/Podfile b/packages/video_player/video_player/example/ios/Podfile index 01d4aa611bb..17adeb14132 100644 --- a/packages/video_player/video_player/example/ios/Podfile +++ b/packages/video_player/video_player/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj index 2ab10fb9081..f6c041ca40b 100644 --- a/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj @@ -140,6 +140,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 32E9BFFE171C16A1A344FF4F /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -205,6 +206,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 32E9BFFE171C16A1A344FF4F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -335,7 +353,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -414,7 +432,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -465,7 +483,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/video_player/video_player/example/macos/Podfile b/packages/video_player/video_player/example/macos/Podfile index ae77cc1d426..66f6172bbb3 100644 --- a/packages/video_player/video_player/example/macos/Podfile +++ b/packages/video_player/video_player/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/video_player/video_player/example/macos/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player/example/macos/Runner.xcodeproj/project.pbxproj index e6fa40d2ed6..91fff87add8 100644 --- a/packages/video_player/video_player/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player/example/macos/Runner.xcodeproj/project.pbxproj @@ -193,6 +193,7 @@ 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + CC40D77B687270FD3E1BD701 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -306,6 +307,23 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + CC40D77B687270FD3E1BD701 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; D3E396DFBCC51886820113AA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -402,7 +420,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -481,7 +499,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -528,7 +546,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java index 92c2ff5f156..cfb271b7085 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -620,4 +620,401 @@ public void testSelectAudioTrack_negativeIndices() { videoPlayer.dispose(); } + + // ==================== Video Track Tests ==================== + + @Test + public void testGetVideoTracks_withMultipleVideoTracks() { + Tracks mockTracks = mock(Tracks.class); + Tracks.Group mockVideoGroup1 = mock(Tracks.Group.class); + Tracks.Group mockVideoGroup2 = mock(Tracks.Group.class); + Tracks.Group mockAudioGroup = mock(Tracks.Group.class); + + // Create mock formats for video tracks + Format videoFormat1 = + new Format.Builder() + .setId("video_track_1") + .setLabel("1080p") + .setAverageBitrate(5000000) + .setWidth(1920) + .setHeight(1080) + .setFrameRate(30.0f) + .setCodecs("avc1.64001f") + .build(); + + Format videoFormat2 = + new Format.Builder() + .setId("video_track_2") + .setLabel("720p") + .setAverageBitrate(2500000) + .setWidth(1280) + .setHeight(720) + .setFrameRate(24.0f) + .setCodecs("avc1.4d401f") + .build(); + + // Mock video groups and set length field + setGroupLength(mockVideoGroup1, 1); + setGroupLength(mockVideoGroup2, 1); + + when(mockVideoGroup1.getType()).thenReturn(C.TRACK_TYPE_VIDEO); + when(mockVideoGroup1.getTrackFormat(0)).thenReturn(videoFormat1); + when(mockVideoGroup1.isTrackSelected(0)).thenReturn(true); + + when(mockVideoGroup2.getType()).thenReturn(C.TRACK_TYPE_VIDEO); + when(mockVideoGroup2.getTrackFormat(0)).thenReturn(videoFormat2); + when(mockVideoGroup2.isTrackSelected(0)).thenReturn(false); + + when(mockAudioGroup.getType()).thenReturn(C.TRACK_TYPE_AUDIO); + + // Mock tracks + ImmutableList groups = + ImmutableList.of(mockVideoGroup1, mockVideoGroup2, mockAudioGroup); + when(mockTracks.getGroups()).thenReturn(groups); + when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks); + + VideoPlayer videoPlayer = createVideoPlayer(); + + // Test the method + NativeVideoTrackData nativeData = videoPlayer.getVideoTracks(); + List result = nativeData.getExoPlayerTracks(); + + // Verify results + assertNotNull(result); + assertEquals(2, result.size()); + + // Verify first track + ExoPlayerVideoTrackData track1 = result.get(0); + assertEquals(0L, track1.getGroupIndex()); + assertEquals(0L, track1.getTrackIndex()); + assertEquals("1080p", track1.getLabel()); + assertTrue(track1.isSelected()); + assertEquals(Long.valueOf(5000000), track1.getBitrate()); + assertEquals(Long.valueOf(1920), track1.getWidth()); + assertEquals(Long.valueOf(1080), track1.getHeight()); + assertEquals(Double.valueOf(30.0), track1.getFrameRate()); + assertEquals("avc1.64001f", track1.getCodec()); + + // Verify second track + ExoPlayerVideoTrackData track2 = result.get(1); + assertEquals(1L, track2.getGroupIndex()); + assertEquals(0L, track2.getTrackIndex()); + assertEquals("720p", track2.getLabel()); + assertFalse(track2.isSelected()); + assertEquals(Long.valueOf(2500000), track2.getBitrate()); + assertEquals(Long.valueOf(1280), track2.getWidth()); + assertEquals(Long.valueOf(720), track2.getHeight()); + assertEquals(Double.valueOf(24.0), track2.getFrameRate()); + assertEquals("avc1.4d401f", track2.getCodec()); + + videoPlayer.dispose(); + } + + @Test + public void testGetVideoTracks_withNoVideoTracks() { + Tracks mockTracks = mock(Tracks.class); + Tracks.Group mockAudioGroup = mock(Tracks.Group.class); + + // Mock audio group only (no video tracks) + when(mockAudioGroup.getType()).thenReturn(C.TRACK_TYPE_AUDIO); + + ImmutableList groups = ImmutableList.of(mockAudioGroup); + when(mockTracks.getGroups()).thenReturn(groups); + when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks); + + VideoPlayer videoPlayer = createVideoPlayer(); + + // Test the method + NativeVideoTrackData nativeData = videoPlayer.getVideoTracks(); + List result = nativeData.getExoPlayerTracks(); + + // Verify results + assertNotNull(result); + assertEquals(0, result.size()); + + videoPlayer.dispose(); + } + + @Test + public void testGetVideoTracks_withNullValues() { + Tracks mockTracks = mock(Tracks.class); + Tracks.Group mockVideoGroup = mock(Tracks.Group.class); + + // Create format with null/missing values + Format videoFormat = + new Format.Builder() + .setId("video_track_null") + .setLabel(null) // Null label + .setAverageBitrate(Format.NO_VALUE) // No bitrate + .setWidth(Format.NO_VALUE) // No width + .setHeight(Format.NO_VALUE) // No height + .setFrameRate(Format.NO_VALUE) // No frame rate + .setCodecs(null) // Null codec + .build(); + + // Mock video group and set length field + setGroupLength(mockVideoGroup, 1); + when(mockVideoGroup.getType()).thenReturn(C.TRACK_TYPE_VIDEO); + when(mockVideoGroup.getTrackFormat(0)).thenReturn(videoFormat); + when(mockVideoGroup.isTrackSelected(0)).thenReturn(false); + + ImmutableList groups = ImmutableList.of(mockVideoGroup); + when(mockTracks.getGroups()).thenReturn(groups); + when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks); + + VideoPlayer videoPlayer = createVideoPlayer(); + + // Test the method + NativeVideoTrackData nativeData = videoPlayer.getVideoTracks(); + List result = nativeData.getExoPlayerTracks(); + + // Verify results + assertNotNull(result); + assertEquals(1, result.size()); + + ExoPlayerVideoTrackData track = result.get(0); + assertEquals(0L, track.getGroupIndex()); + assertEquals(0L, track.getTrackIndex()); + assertNull(track.getLabel()); // Null values should be preserved + assertFalse(track.isSelected()); + assertNull(track.getBitrate()); + assertNull(track.getWidth()); + assertNull(track.getHeight()); + assertNull(track.getFrameRate()); + assertNull(track.getCodec()); + + videoPlayer.dispose(); + } + + @Test + public void testGetVideoTracks_withMultipleTracksInSameGroup() { + Tracks mockTracks = mock(Tracks.class); + Tracks.Group mockVideoGroup = mock(Tracks.Group.class); + + // Create formats for group with multiple tracks (adaptive streaming scenario) + Format videoFormat1 = + new Format.Builder() + .setId("video_track_1") + .setLabel("1080p") + .setWidth(1920) + .setHeight(1080) + .setAverageBitrate(5000000) + .build(); + + Format videoFormat2 = + new Format.Builder() + .setId("video_track_2") + .setLabel("720p") + .setWidth(1280) + .setHeight(720) + .setAverageBitrate(2500000) + .build(); + + // Mock video group with multiple tracks + setGroupLength(mockVideoGroup, 2); + when(mockVideoGroup.getType()).thenReturn(C.TRACK_TYPE_VIDEO); + when(mockVideoGroup.getTrackFormat(0)).thenReturn(videoFormat1); + when(mockVideoGroup.getTrackFormat(1)).thenReturn(videoFormat2); + when(mockVideoGroup.isTrackSelected(0)).thenReturn(true); + when(mockVideoGroup.isTrackSelected(1)).thenReturn(false); + + ImmutableList groups = ImmutableList.of(mockVideoGroup); + when(mockTracks.getGroups()).thenReturn(groups); + when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks); + + VideoPlayer videoPlayer = createVideoPlayer(); + + // Test the method + NativeVideoTrackData nativeData = videoPlayer.getVideoTracks(); + List result = nativeData.getExoPlayerTracks(); + + // Verify results + assertNotNull(result); + assertEquals(2, result.size()); + + // Verify track indices are correct + ExoPlayerVideoTrackData track1 = result.get(0); + ExoPlayerVideoTrackData track2 = result.get(1); + assertEquals(0L, track1.getGroupIndex()); + assertEquals(0L, track1.getTrackIndex()); + assertEquals(0L, track2.getGroupIndex()); + assertEquals(1L, track2.getTrackIndex()); + // Tracks have same group but different track indices + assertEquals(track1.getGroupIndex(), track2.getGroupIndex()); + assertNotEquals(track1.getTrackIndex(), track2.getTrackIndex()); + + videoPlayer.dispose(); + } + + @Test + public void testSelectVideoTrack_validIndices() { + DefaultTrackSelector mockTrackSelector = mock(DefaultTrackSelector.class); + DefaultTrackSelector.Parameters mockParameters = mock(DefaultTrackSelector.Parameters.class); + DefaultTrackSelector.Parameters.Builder mockBuilder = + mock(DefaultTrackSelector.Parameters.Builder.class); + + Tracks mockTracks = mock(Tracks.class); + Tracks.Group mockVideoGroup = mock(Tracks.Group.class); + + Format videoFormat = + new Format.Builder() + .setId("video_track_1") + .setLabel("1080p") + .setWidth(1920) + .setHeight(1080) + .build(); + + // Create a real TrackGroup with the format + TrackGroup trackGroup = new TrackGroup(videoFormat); + + // Mock video group with 2 tracks + setGroupLength(mockVideoGroup, 2); + when(mockVideoGroup.getType()).thenReturn(C.TRACK_TYPE_VIDEO); + when(mockVideoGroup.getMediaTrackGroup()).thenReturn(trackGroup); + + ImmutableList groups = ImmutableList.of(mockVideoGroup); + when(mockTracks.getGroups()).thenReturn(groups); + + // Set up track selector BEFORE creating VideoPlayer + when(mockExoPlayer.getTrackSelector()).thenReturn(mockTrackSelector); + when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks); + when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat); + when(mockTrackSelector.buildUponParameters()).thenReturn(mockBuilder); + when(mockBuilder.setOverrideForType(any(TrackSelectionOverride.class))).thenReturn(mockBuilder); + when(mockBuilder.setTrackTypeDisabled(anyInt(), anyBoolean())).thenReturn(mockBuilder); + when(mockBuilder.build()).thenReturn(mockParameters); + + VideoPlayer videoPlayer = createVideoPlayer(); + + // Test selecting a valid video track + videoPlayer.selectVideoTrack(0, 0); + + // Verify track selector was called + verify(mockTrackSelector, atLeastOnce()).buildUponParameters(); + verify(mockBuilder, atLeastOnce()).build(); + verify(mockTrackSelector, atLeastOnce()).setParameters(mockParameters); + + videoPlayer.dispose(); + } + + @Test + public void testSelectVideoTrack_nullTrackSelector() { + // Track selector is null by default in mock + VideoPlayer videoPlayer = createVideoPlayer(); + + assertThrows(IllegalStateException.class, () -> videoPlayer.selectVideoTrack(0, 0)); + + videoPlayer.dispose(); + } + + @Test + public void testSelectVideoTrack_invalidGroupIndex() { + DefaultTrackSelector mockTrackSelector = mock(DefaultTrackSelector.class); + Tracks mockTracks = mock(Tracks.class); + Tracks.Group mockVideoGroup = mock(Tracks.Group.class); + + ImmutableList groups = ImmutableList.of(mockVideoGroup); + when(mockTracks.getGroups()).thenReturn(groups); + when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks); + when(mockExoPlayer.getTrackSelector()).thenReturn(mockTrackSelector); + + VideoPlayer videoPlayer = createVideoPlayer(); + + // Test with invalid group index (only 1 group exists at index 0) + assertThrows(IllegalArgumentException.class, () -> videoPlayer.selectVideoTrack(5, 0)); + + videoPlayer.dispose(); + } + + @Test + public void testSelectVideoTrack_invalidTrackIndex() { + DefaultTrackSelector mockTrackSelector = mock(DefaultTrackSelector.class); + Tracks mockTracks = mock(Tracks.class); + Tracks.Group mockVideoGroup = mock(Tracks.Group.class); + + // Mock video group with only 1 track + setGroupLength(mockVideoGroup, 1); + when(mockVideoGroup.getType()).thenReturn(C.TRACK_TYPE_VIDEO); + + ImmutableList groups = ImmutableList.of(mockVideoGroup); + when(mockTracks.getGroups()).thenReturn(groups); + when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks); + when(mockExoPlayer.getTrackSelector()).thenReturn(mockTrackSelector); + + VideoPlayer videoPlayer = createVideoPlayer(); + + // Test with invalid track index (only 1 track exists at index 0) + assertThrows(IllegalArgumentException.class, () -> videoPlayer.selectVideoTrack(0, 5)); + + videoPlayer.dispose(); + } + + @Test + public void testSelectVideoTrack_nonVideoGroup() { + DefaultTrackSelector mockTrackSelector = mock(DefaultTrackSelector.class); + Tracks mockTracks = mock(Tracks.class); + Tracks.Group mockAudioGroup = mock(Tracks.Group.class); + + // Mock audio group (not video) + setGroupLength(mockAudioGroup, 1); + when(mockAudioGroup.getType()).thenReturn(C.TRACK_TYPE_AUDIO); + + ImmutableList groups = ImmutableList.of(mockAudioGroup); + when(mockTracks.getGroups()).thenReturn(groups); + when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks); + when(mockExoPlayer.getTrackSelector()).thenReturn(mockTrackSelector); + + VideoPlayer videoPlayer = createVideoPlayer(); + + // Test selecting from a non-video group + assertThrows(IllegalArgumentException.class, () -> videoPlayer.selectVideoTrack(0, 0)); + + videoPlayer.dispose(); + } + + @Test + public void testSelectVideoTrack_negativeIndices() { + DefaultTrackSelector mockTrackSelector = mock(DefaultTrackSelector.class); + Tracks mockTracks = mock(Tracks.class); + Tracks.Group mockVideoGroup = mock(Tracks.Group.class); + + ImmutableList groups = ImmutableList.of(mockVideoGroup); + when(mockTracks.getGroups()).thenReturn(groups); + when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks); + when(mockExoPlayer.getTrackSelector()).thenReturn(mockTrackSelector); + + VideoPlayer videoPlayer = createVideoPlayer(); + + // Test with negative group index only (not both -1) + assertThrows(IllegalArgumentException.class, () -> videoPlayer.selectVideoTrack(-1, 0)); + + videoPlayer.dispose(); + } + + @Test + public void testSelectVideoTrack_autoQuality() { + DefaultTrackSelector mockTrackSelector = mock(DefaultTrackSelector.class); + DefaultTrackSelector.Parameters mockParameters = mock(DefaultTrackSelector.Parameters.class); + DefaultTrackSelector.Parameters.Builder mockBuilder = + mock(DefaultTrackSelector.Parameters.Builder.class); + + // Set up track selector + when(mockExoPlayer.getTrackSelector()).thenReturn(mockTrackSelector); + when(mockTrackSelector.buildUponParameters()).thenReturn(mockBuilder); + when(mockBuilder.clearOverridesOfType(C.TRACK_TYPE_VIDEO)).thenReturn(mockBuilder); + when(mockBuilder.build()).thenReturn(mockParameters); + + VideoPlayer videoPlayer = createVideoPlayer(); + + // Test selecting auto quality (both indices -1) + videoPlayer.selectVideoTrack(-1, -1); + + // Verify track selector cleared video overrides + verify(mockTrackSelector).buildUponParameters(); + verify(mockBuilder).clearOverridesOfType(C.TRACK_TYPE_VIDEO); + verify(mockBuilder).build(); + verify(mockTrackSelector).setParameters(mockParameters); + + videoPlayer.dispose(); + } } diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 7f509fcc462..36986375855 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Implements `getVideoTracks()` and `selectVideoTrack()` methods for video track (quality) selection using AVFoundation. +* Video track selection requires iOS 15+ / macOS 12+ for HLS streams. + ## 2.8.8 * Refactors Dart internals for maintainability. diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m index b095dbf33ae..5c3d096b01d 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m @@ -1051,4 +1051,226 @@ - (nonnull AVPlayerItem *)playerItemWithURL:(NSURL *)url { return [AVPlayerItem playerItemWithAsset:[AVURLAsset URLAssetWithURL:url options:nil]]; } +#pragma mark - Video Track Tests + +// Tests getVideoTracks with a regular MP4 video file using real AVFoundation. +// Regular MP4 files don't have HLS variants, so we expect empty media selection tracks. +- (void)testGetVideoTracksWithRealMP4Video { + FVPVideoPlayer *player = + [[FVPVideoPlayer alloc] initWithPlayerItem:[self playerItemWithURL:self.mp4TestURL] + avFactory:[[FVPDefaultAVFactory alloc] init] + viewProvider:[[StubViewProvider alloc] initWithView:nil]]; + XCTAssertNotNil(player); + + XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; + StubEventListener *listener = + [[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation]; + player.eventListener = listener; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + // Now test getVideoTracks + XCTestExpectation *tracksExpectation = + [self expectationWithDescription:@"getVideoTracks completes"]; + [player + getVideoTracks:^(FVPNativeVideoTrackData *_Nullable result, FlutterError *_Nullable error) { + XCTAssertNil(error); + XCTAssertNotNil(result); + // For regular MP4 files, media selection tracks should be nil (no HLS variants) + // The method returns empty data for non-HLS content + [tracksExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + FlutterError *disposeError; + [player disposeWithError:&disposeError]; +} + +// Tests getVideoTracks with an HLS stream using real AVFoundation. +// HLS streams use AVAssetVariant API (iOS 15+) for video track selection. +- (void)testGetVideoTracksWithRealHLSStream { + NSURL *hlsURL = [NSURL + URLWithString:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8"]; + XCTAssertNotNil(hlsURL); + + FVPVideoPlayer *player = + [[FVPVideoPlayer alloc] initWithPlayerItem:[self playerItemWithURL:hlsURL] + avFactory:[[FVPDefaultAVFactory alloc] init] + viewProvider:[[StubViewProvider alloc] initWithView:nil]]; + XCTAssertNotNil(player); + + XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; + StubEventListener *listener = + [[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation]; + player.eventListener = listener; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + // Now test getVideoTracks + XCTestExpectation *tracksExpectation = + [self expectationWithDescription:@"getVideoTracks completes"]; + [player + getVideoTracks:^(FVPNativeVideoTrackData *_Nullable result, FlutterError *_Nullable error) { + XCTAssertNil(error); + XCTAssertNotNil(result); + + // For HLS streams on iOS 15+, we may have media selection tracks (variants) + if (@available(iOS 15.0, macOS 12.0, *)) { + // The bee.m3u8 stream may or may not have multiple video variants. + // We verify the method returns valid data without crashing. + if (result.mediaSelectionTracks) { + // If media selection tracks exist, they should have valid structure + for (FVPMediaSelectionVideoTrackData *track in result.mediaSelectionTracks) { + XCTAssertGreaterThanOrEqual(track.variantIndex, 0); + // Bitrate should be positive if present + if (track.bitrate) { + XCTAssertGreaterThan(track.bitrate.integerValue, 0); + } + } + } + } + [tracksExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + FlutterError *disposeError; + [player disposeWithError:&disposeError]; +} + +// Tests selectVideoTrack sets preferredPeakBitRate correctly. +- (void)testSelectVideoTrackSetsBitrate { + FVPVideoPlayer *player = + [[FVPVideoPlayer alloc] initWithPlayerItem:[self playerItemWithURL:self.mp4TestURL] + avFactory:[[FVPDefaultAVFactory alloc] init] + viewProvider:[[StubViewProvider alloc] initWithView:nil]]; + XCTAssertNotNil(player); + + XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; + StubEventListener *listener = + [[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation]; + player.eventListener = listener; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + FlutterError *error; + // Set a specific bitrate + [player selectVideoTrackWithBitrate:5000000 error:&error]; + XCTAssertNil(error); + XCTAssertEqual(player.player.currentItem.preferredPeakBitRate, 5000000); + + [player disposeWithError:&error]; +} + +// Tests selectVideoTrack with 0 bitrate enables auto quality selection. +- (void)testSelectVideoTrackAutoQuality { + FVPVideoPlayer *player = + [[FVPVideoPlayer alloc] initWithPlayerItem:[self playerItemWithURL:self.mp4TestURL] + avFactory:[[FVPDefaultAVFactory alloc] init] + viewProvider:[[StubViewProvider alloc] initWithView:nil]]; + XCTAssertNotNil(player); + + XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; + StubEventListener *listener = + [[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation]; + player.eventListener = listener; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + FlutterError *error; + // First set a specific bitrate + [player selectVideoTrackWithBitrate:5000000 error:&error]; + XCTAssertNil(error); + XCTAssertEqual(player.player.currentItem.preferredPeakBitRate, 5000000); + + // Then set to auto quality (0) + [player selectVideoTrackWithBitrate:0 error:&error]; + XCTAssertNil(error); + XCTAssertEqual(player.player.currentItem.preferredPeakBitRate, 0); + + [player disposeWithError:&error]; +} + +// Tests that getVideoTracks works correctly through the plugin API with a real video. +- (void)testGetVideoTracksViaPluginWithRealVideo { + NSObject *registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + FVPVideoPlayerPlugin *videoPlayerPlugin = + [[FVPVideoPlayerPlugin alloc] initWithRegistrar:registrar]; + + FlutterError *error; + [videoPlayerPlugin initialize:&error]; + XCTAssertNil(error); + + FVPCreationOptions *create = [FVPCreationOptions + makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" + httpHeaders:@{}]; + FVPTexturePlayerIds *identifiers = [videoPlayerPlugin createTexturePlayerWithOptions:create + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(identifiers); + + FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; + XCTAssertNotNil(player); + + // Wait for player item to become ready + AVPlayerItem *item = player.player.currentItem; + [self keyValueObservingExpectationForObject:(id)item + keyPath:@"status" + expectedValue:@(AVPlayerItemStatusReadyToPlay)]; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + // Now test getVideoTracks + XCTestExpectation *tracksExpectation = + [self expectationWithDescription:@"getVideoTracks completes"]; + [player + getVideoTracks:^(FVPNativeVideoTrackData *_Nullable result, FlutterError *_Nullable error) { + XCTAssertNil(error); + XCTAssertNotNil(result); + [tracksExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + [player disposeWithError:&error]; +} + +// Tests selectVideoTrack via plugin API with HLS stream. +- (void)testSelectVideoTrackViaPluginWithHLSStream { + NSObject *registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + FVPVideoPlayerPlugin *videoPlayerPlugin = + [[FVPVideoPlayerPlugin alloc] initWithRegistrar:registrar]; + + FlutterError *error; + [videoPlayerPlugin initialize:&error]; + XCTAssertNil(error); + + // Use HLS stream which supports adaptive bitrate + FVPCreationOptions *create = [FVPCreationOptions + makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" + httpHeaders:@{}]; + FVPTexturePlayerIds *identifiers = [videoPlayerPlugin createTexturePlayerWithOptions:create + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(identifiers); + + FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; + XCTAssertNotNil(player); + + // Wait for player item to become ready + AVPlayerItem *item = player.player.currentItem; + [self keyValueObservingExpectationForObject:(id)item + keyPath:@"status" + expectedValue:@(AVPlayerItemStatusReadyToPlay)]; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + // Test setting a specific bitrate + [player selectVideoTrackWithBitrate:1000000 error:&error]; + XCTAssertNil(error); + XCTAssertEqual(player.player.currentItem.preferredPeakBitRate, 1000000); + + // Test setting auto quality + [player selectVideoTrackWithBitrate:0 error:&error]; + XCTAssertNil(error); + XCTAssertEqual(player.player.currentItem.preferredPeakBitRate, 0); + + [player disposeWithError:&error]; +} + @end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 9da957fbc8c..484d51246b1 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -5,6 +5,7 @@ #import "./include/video_player_avfoundation/FVPVideoPlayer.h" #import "./include/video_player_avfoundation/FVPVideoPlayer_Internal.h" +#import #import #import "./include/video_player_avfoundation/AVAssetTrackUtils.h" @@ -421,6 +422,134 @@ - (void)setPlaybackSpeed:(double)speed error:(FlutterError *_Nullable *_Nonnull) [self updatePlayingState]; } +- (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, + FlutterError *_Nullable))completion { + NSMutableArray *mediaSelectionTracks = [NSMutableArray array]; + + AVPlayerItem *currentItem = _player.currentItem; + if (!currentItem) { + completion([[FVPNativeVideoTrackData alloc] init], nil); + return; + } + + AVURLAsset *urlAsset = (AVURLAsset *)currentItem.asset; + if (![urlAsset isKindOfClass:[AVURLAsset class]]) { + completion([[FVPNativeVideoTrackData alloc] init], nil); + return; + } + + // Use AVAssetVariant API for iOS 15+ to get HLS variants + if (@available(iOS 15.0, macOS 12.0, *)) { + [urlAsset loadValuesAsynchronouslyForKeys:@[ @"variants" ] + completionHandler:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error = nil; + AVKeyValueStatus status = [urlAsset statusOfValueForKey:@"variants" + error:&error]; + + if (status == AVKeyValueStatusLoaded) { + NSArray *variants = urlAsset.variants; + double currentBitrate = + currentItem.preferredPeakBitRate > 0 + ? currentItem.preferredPeakBitRate + : 0; + + for (NSInteger i = 0; i < variants.count; i++) { + AVAssetVariant *variant = variants[i]; + double peakBitRate = variant.peakBitRate; + CGSize videoSize = CGSizeZero; + double frameRate = 0; + NSString *codec = nil; + + // Get video attributes if available + AVAssetVariantVideoAttributes *videoAttrs = + variant.videoAttributes; + if (videoAttrs) { + videoSize = videoAttrs.presentationSize; + frameRate = videoAttrs.nominalFrameRate; + // Get codec from media sub types + NSArray *codecTypes = videoAttrs.codecTypes; + if (codecTypes.count > 0) { + FourCharCode codecType = + [codecTypes[0] unsignedIntValue]; + codec = [self codecStringFromFourCharCode:codecType]; + } + } + + // Determine if this variant is selected (approximate match by + // bitrate) + BOOL isSelected = (currentBitrate > 0 && + fabs(peakBitRate - currentBitrate) < + peakBitRate * 0.1); + + // Generate label from resolution + NSString *label = nil; + if (videoSize.height > 0) { + label = + [NSString stringWithFormat:@"%.0fp", videoSize.height]; + } + + FVPMediaSelectionVideoTrackData *trackData = + [FVPMediaSelectionVideoTrackData + makeWithVariantIndex:i + label:label + bitrate:peakBitRate > 0 + ? @((NSInteger)peakBitRate) + : nil + width:videoSize.width > 0 + ? @((NSInteger)videoSize.width) + : nil + height:videoSize.height > 0 + ? @((NSInteger)videoSize + .height) + : nil + frameRate:frameRate > 0 ? @(frameRate) : nil + codec:codec + isSelected:isSelected]; + [mediaSelectionTracks addObject:trackData]; + } + } + + FVPNativeVideoTrackData *result = [FVPNativeVideoTrackData + makeWithAssetTracks:nil + mediaSelectionTracks:mediaSelectionTracks.count > 0 + ? mediaSelectionTracks + : nil]; + completion(result, nil); + }); + }]; + } else { + // For iOS < 15, return empty list as AVAssetVariant is not available + completion([[FVPNativeVideoTrackData alloc] init], nil); + } +} + +- (NSString *)codecStringFromFourCharCode:(FourCharCode)code { + // Convert common video codec FourCharCodes to readable strings + switch (code) { + case kCMVideoCodecType_H264: + return @"avc1"; + case kCMVideoCodecType_HEVC: + return @"hevc"; + case kCMVideoCodecType_VP9: + return @"vp9"; + default: + return nil; + } +} + +- (void)selectVideoTrackWithBitrate:(NSInteger)bitrate + error:(FlutterError *_Nullable *_Nonnull)error { + AVPlayerItem *currentItem = _player.currentItem; + if (!currentItem) { + return; + } + + // Set preferredPeakBitRate to select the quality + // 0 means auto quality (adaptive streaming) + currentItem.preferredPeakBitRate = (double)bitrate; +} + #pragma mark - Private - (int64_t)duration { diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h index d06c3fd0179..64a65673e90 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h @@ -16,30 +16,86 @@ NS_ASSUME_NONNULL_BEGIN @class FVPPlatformVideoViewCreationParams; @class FVPCreationOptions; @class FVPTexturePlayerIds; +@class FVPMediaSelectionVideoTrackData; +@class FVPAssetVideoTrackData; +@class FVPNativeVideoTrackData; /// Information passed to the platform view creation. @interface FVPPlatformVideoViewCreationParams : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithPlayerId:(NSInteger)playerId; -@property(nonatomic, assign) NSInteger playerId; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId; +@property(nonatomic, assign) NSInteger playerId; @end @interface FVPCreationOptions : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; + (instancetype)makeWithUri:(NSString *)uri - httpHeaders:(NSDictionary *)httpHeaders; -@property(nonatomic, copy) NSString *uri; -@property(nonatomic, copy) NSDictionary *httpHeaders; + httpHeaders:(NSDictionary *)httpHeaders; +@property(nonatomic, copy) NSString * uri; +@property(nonatomic, copy) NSDictionary * httpHeaders; @end @interface FVPTexturePlayerIds : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithPlayerId:(NSInteger)playerId textureId:(NSInteger)textureId; -@property(nonatomic, assign) NSInteger playerId; -@property(nonatomic, assign) NSInteger textureId; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId + textureId:(NSInteger )textureId; +@property(nonatomic, assign) NSInteger playerId; +@property(nonatomic, assign) NSInteger textureId; +@end + +/// Video track data from AVAssetVariant (HLS variants) for iOS 15+. +@interface FVPMediaSelectionVideoTrackData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithVariantIndex:(NSInteger )variantIndex + label:(nullable NSString *)label + bitrate:(nullable NSNumber *)bitrate + width:(nullable NSNumber *)width + height:(nullable NSNumber *)height + frameRate:(nullable NSNumber *)frameRate + codec:(nullable NSString *)codec + isSelected:(BOOL )isSelected; +@property(nonatomic, assign) NSInteger variantIndex; +@property(nonatomic, copy, nullable) NSString * label; +@property(nonatomic, strong, nullable) NSNumber * bitrate; +@property(nonatomic, strong, nullable) NSNumber * width; +@property(nonatomic, strong, nullable) NSNumber * height; +@property(nonatomic, strong, nullable) NSNumber * frameRate; +@property(nonatomic, copy, nullable) NSString * codec; +@property(nonatomic, assign) BOOL isSelected; +@end + +/// Video track data from AVAssetTrack (regular videos). +@interface FVPAssetVideoTrackData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTrackId:(NSInteger )trackId + label:(nullable NSString *)label + width:(nullable NSNumber *)width + height:(nullable NSNumber *)height + frameRate:(nullable NSNumber *)frameRate + codec:(nullable NSString *)codec + isSelected:(BOOL )isSelected; +@property(nonatomic, assign) NSInteger trackId; +@property(nonatomic, copy, nullable) NSString * label; +@property(nonatomic, strong, nullable) NSNumber * width; +@property(nonatomic, strong, nullable) NSNumber * height; +@property(nonatomic, strong, nullable) NSNumber * frameRate; +@property(nonatomic, copy, nullable) NSString * codec; +@property(nonatomic, assign) BOOL isSelected; +@end + +/// Container for video track data from iOS. +@interface FVPNativeVideoTrackData : NSObject ++ (instancetype)makeWithAssetTracks:(nullable NSArray *)assetTracks + mediaSelectionTracks:(nullable NSArray *)mediaSelectionTracks; +/// Asset-based tracks (for regular videos) +@property(nonatomic, copy, nullable) NSArray * assetTracks; +/// Media selection tracks (for HLS variants on iOS 15+) +@property(nonatomic, copy, nullable) NSArray * mediaSelectionTracks; @end /// The codec used by all APIs. @@ -48,25 +104,17 @@ NSObject *FVPGetMessagesCodec(void); @protocol FVPAVFoundationVideoPlayerApi - (void)initialize:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(FVPCreationOptions *)params - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(FVPCreationOptions *)params error:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable FVPTexturePlayerIds *) - createTexturePlayerWithOptions:(FVPCreationOptions *)creationOptions - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable FVPTexturePlayerIds *)createTexturePlayerWithOptions:(FVPCreationOptions *)creationOptions error:(FlutterError *_Nullable *_Nonnull)error; - (void)setMixWithOthers:(BOOL)mixWithOthers error:(FlutterError *_Nullable *_Nonnull)error; -- (nullable NSString *)fileURLForAssetWithName:(NSString *)asset - package:(nullable NSString *)package - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)fileURLForAssetWithName:(NSString *)asset package:(nullable NSString *)package error:(FlutterError *_Nullable *_Nonnull)error; @end -extern void SetUpFVPAVFoundationVideoPlayerApi( - id binaryMessenger, - NSObject *_Nullable api); +extern void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, NSObject *_Nullable api); + +extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); -extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix( - id binaryMessenger, - NSObject *_Nullable api, NSString *messageChannelSuffix); @protocol FVPVideoPlayerInstanceApi - (void)setLooping:(BOOL)looping error:(FlutterError *_Nullable *_Nonnull)error; @@ -78,13 +126,15 @@ extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix( - (void)seekTo:(NSInteger)position completion:(void (^)(FlutterError *_Nullable))completion; - (void)pauseWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)disposeWithError:(FlutterError *_Nullable *_Nonnull)error; +/// Gets the available video tracks for the video. +- (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, FlutterError *_Nullable))completion; +/// Selects a video track by setting preferredPeakBitRate. +/// Pass 0 to enable auto quality selection. +- (void)selectVideoTrackWithBitrate:(NSInteger)bitrate error:(FlutterError *_Nullable *_Nonnull)error; @end -extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, - NSObject *_Nullable api); +extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, NSObject *_Nullable api); -extern void SetUpFVPVideoPlayerInstanceApiWithSuffix( - id binaryMessenger, NSObject *_Nullable api, - NSString *messageChannelSuffix); +extern void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m index 155ac2bacad..965bfbdc0a9 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m @@ -48,16 +48,32 @@ + (nullable FVPTexturePlayerIds *)nullableFromList:(NSArray *)list; - (NSArray *)toList; @end +@interface FVPMediaSelectionVideoTrackData () ++ (FVPMediaSelectionVideoTrackData *)fromList:(NSArray *)list; ++ (nullable FVPMediaSelectionVideoTrackData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FVPAssetVideoTrackData () ++ (FVPAssetVideoTrackData *)fromList:(NSArray *)list; ++ (nullable FVPAssetVideoTrackData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FVPNativeVideoTrackData () ++ (FVPNativeVideoTrackData *)fromList:(NSArray *)list; ++ (nullable FVPNativeVideoTrackData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @implementation FVPPlatformVideoViewCreationParams -+ (instancetype)makeWithPlayerId:(NSInteger)playerId { - FVPPlatformVideoViewCreationParams *pigeonResult = - [[FVPPlatformVideoViewCreationParams alloc] init]; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId { + FVPPlatformVideoViewCreationParams* pigeonResult = [[FVPPlatformVideoViewCreationParams alloc] init]; pigeonResult.playerId = playerId; return pigeonResult; } + (FVPPlatformVideoViewCreationParams *)fromList:(NSArray *)list { - FVPPlatformVideoViewCreationParams *pigeonResult = - [[FVPPlatformVideoViewCreationParams alloc] init]; + FVPPlatformVideoViewCreationParams *pigeonResult = [[FVPPlatformVideoViewCreationParams alloc] init]; pigeonResult.playerId = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } @@ -73,8 +89,8 @@ + (nullable FVPPlatformVideoViewCreationParams *)nullableFromList:(NSArray * @implementation FVPCreationOptions + (instancetype)makeWithUri:(NSString *)uri - httpHeaders:(NSDictionary *)httpHeaders { - FVPCreationOptions *pigeonResult = [[FVPCreationOptions alloc] init]; + httpHeaders:(NSDictionary *)httpHeaders { + FVPCreationOptions* pigeonResult = [[FVPCreationOptions alloc] init]; pigeonResult.uri = uri; pigeonResult.httpHeaders = httpHeaders; return pigeonResult; @@ -97,8 +113,9 @@ + (nullable FVPCreationOptions *)nullableFromList:(NSArray *)list { @end @implementation FVPTexturePlayerIds -+ (instancetype)makeWithPlayerId:(NSInteger)playerId textureId:(NSInteger)textureId { - FVPTexturePlayerIds *pigeonResult = [[FVPTexturePlayerIds alloc] init]; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId + textureId:(NSInteger )textureId { + FVPTexturePlayerIds* pigeonResult = [[FVPTexturePlayerIds alloc] init]; pigeonResult.playerId = playerId; pigeonResult.textureId = textureId; return pigeonResult; @@ -120,17 +137,142 @@ + (nullable FVPTexturePlayerIds *)nullableFromList:(NSArray *)list { } @end +@implementation FVPMediaSelectionVideoTrackData ++ (instancetype)makeWithVariantIndex:(NSInteger )variantIndex + label:(nullable NSString *)label + bitrate:(nullable NSNumber *)bitrate + width:(nullable NSNumber *)width + height:(nullable NSNumber *)height + frameRate:(nullable NSNumber *)frameRate + codec:(nullable NSString *)codec + isSelected:(BOOL )isSelected { + FVPMediaSelectionVideoTrackData* pigeonResult = [[FVPMediaSelectionVideoTrackData alloc] init]; + pigeonResult.variantIndex = variantIndex; + pigeonResult.label = label; + pigeonResult.bitrate = bitrate; + pigeonResult.width = width; + pigeonResult.height = height; + pigeonResult.frameRate = frameRate; + pigeonResult.codec = codec; + pigeonResult.isSelected = isSelected; + return pigeonResult; +} ++ (FVPMediaSelectionVideoTrackData *)fromList:(NSArray *)list { + FVPMediaSelectionVideoTrackData *pigeonResult = [[FVPMediaSelectionVideoTrackData alloc] init]; + pigeonResult.variantIndex = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.label = GetNullableObjectAtIndex(list, 1); + pigeonResult.bitrate = GetNullableObjectAtIndex(list, 2); + pigeonResult.width = GetNullableObjectAtIndex(list, 3); + pigeonResult.height = GetNullableObjectAtIndex(list, 4); + pigeonResult.frameRate = GetNullableObjectAtIndex(list, 5); + pigeonResult.codec = GetNullableObjectAtIndex(list, 6); + pigeonResult.isSelected = [GetNullableObjectAtIndex(list, 7) boolValue]; + return pigeonResult; +} ++ (nullable FVPMediaSelectionVideoTrackData *)nullableFromList:(NSArray *)list { + return (list) ? [FVPMediaSelectionVideoTrackData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.variantIndex), + self.label ?: [NSNull null], + self.bitrate ?: [NSNull null], + self.width ?: [NSNull null], + self.height ?: [NSNull null], + self.frameRate ?: [NSNull null], + self.codec ?: [NSNull null], + @(self.isSelected), + ]; +} +@end + +@implementation FVPAssetVideoTrackData ++ (instancetype)makeWithTrackId:(NSInteger )trackId + label:(nullable NSString *)label + width:(nullable NSNumber *)width + height:(nullable NSNumber *)height + frameRate:(nullable NSNumber *)frameRate + codec:(nullable NSString *)codec + isSelected:(BOOL )isSelected { + FVPAssetVideoTrackData* pigeonResult = [[FVPAssetVideoTrackData alloc] init]; + pigeonResult.trackId = trackId; + pigeonResult.label = label; + pigeonResult.width = width; + pigeonResult.height = height; + pigeonResult.frameRate = frameRate; + pigeonResult.codec = codec; + pigeonResult.isSelected = isSelected; + return pigeonResult; +} ++ (FVPAssetVideoTrackData *)fromList:(NSArray *)list { + FVPAssetVideoTrackData *pigeonResult = [[FVPAssetVideoTrackData alloc] init]; + pigeonResult.trackId = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.label = GetNullableObjectAtIndex(list, 1); + pigeonResult.width = GetNullableObjectAtIndex(list, 2); + pigeonResult.height = GetNullableObjectAtIndex(list, 3); + pigeonResult.frameRate = GetNullableObjectAtIndex(list, 4); + pigeonResult.codec = GetNullableObjectAtIndex(list, 5); + pigeonResult.isSelected = [GetNullableObjectAtIndex(list, 6) boolValue]; + return pigeonResult; +} ++ (nullable FVPAssetVideoTrackData *)nullableFromList:(NSArray *)list { + return (list) ? [FVPAssetVideoTrackData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.trackId), + self.label ?: [NSNull null], + self.width ?: [NSNull null], + self.height ?: [NSNull null], + self.frameRate ?: [NSNull null], + self.codec ?: [NSNull null], + @(self.isSelected), + ]; +} +@end + +@implementation FVPNativeVideoTrackData ++ (instancetype)makeWithAssetTracks:(nullable NSArray *)assetTracks + mediaSelectionTracks:(nullable NSArray *)mediaSelectionTracks { + FVPNativeVideoTrackData* pigeonResult = [[FVPNativeVideoTrackData alloc] init]; + pigeonResult.assetTracks = assetTracks; + pigeonResult.mediaSelectionTracks = mediaSelectionTracks; + return pigeonResult; +} ++ (FVPNativeVideoTrackData *)fromList:(NSArray *)list { + FVPNativeVideoTrackData *pigeonResult = [[FVPNativeVideoTrackData alloc] init]; + pigeonResult.assetTracks = GetNullableObjectAtIndex(list, 0); + pigeonResult.mediaSelectionTracks = GetNullableObjectAtIndex(list, 1); + return pigeonResult; +} ++ (nullable FVPNativeVideoTrackData *)nullableFromList:(NSArray *)list { + return (list) ? [FVPNativeVideoTrackData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + self.assetTracks ?: [NSNull null], + self.mediaSelectionTracks ?: [NSNull null], + ]; +} +@end + @interface FVPMessagesPigeonCodecReader : FlutterStandardReader @end @implementation FVPMessagesPigeonCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 129: + case 129: return [FVPPlatformVideoViewCreationParams fromList:[self readValue]]; - case 130: + case 130: return [FVPCreationOptions fromList:[self readValue]]; - case 131: + case 131: return [FVPTexturePlayerIds fromList:[self readValue]]; + case 132: + return [FVPMediaSelectionVideoTrackData fromList:[self readValue]]; + case 133: + return [FVPAssetVideoTrackData fromList:[self readValue]]; + case 134: + return [FVPNativeVideoTrackData fromList:[self readValue]]; default: return [super readValueOfType:type]; } @@ -150,6 +292,15 @@ - (void)writeValue:(id)value { } else if ([value isKindOfClass:[FVPTexturePlayerIds class]]) { [self writeByte:131]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPMediaSelectionVideoTrackData class]]) { + [self writeByte:132]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPAssetVideoTrackData class]]) { + [self writeByte:133]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPNativeVideoTrackData class]]) { + [self writeByte:134]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -171,35 +322,25 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { static FlutterStandardMessageCodec *sSharedObject = nil; static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ - FVPMessagesPigeonCodecReaderWriter *readerWriter = - [[FVPMessagesPigeonCodecReaderWriter alloc] init]; + FVPMessagesPigeonCodecReaderWriter *readerWriter = [[FVPMessagesPigeonCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; }); return sSharedObject; } -void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, - NSObject *api) { +void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, NSObject *api) { SetUpFVPAVFoundationVideoPlayerApiWithSuffix(binaryMessenger, api, @""); } -void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, - NSObject *api, - NSString *messageChannelSuffix) { - messageChannelSuffix = messageChannelSuffix.length > 0 - ? [NSString stringWithFormat:@".%@", messageChannelSuffix] - : @""; +void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *api, NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @""; { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.initialize", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(initialize:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", - api); + NSCAssert([api respondsToSelector:@selector(initialize:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api initialize:&error]; @@ -210,19 +351,13 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString - stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.createForPlatformView", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(createPlatformViewPlayerWithOptions:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(createPlatformViewPlayerWithOptions:error:)", - api); + NSCAssert([api respondsToSelector:@selector(createPlatformViewPlayerWithOptions:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(createPlatformViewPlayerWithOptions:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FVPCreationOptions *arg_params = GetNullableObjectAtIndex(args, 0); @@ -235,25 +370,18 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString - stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.createForTextureView", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(createTexturePlayerWithOptions:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(createTexturePlayerWithOptions:error:)", - api); + NSCAssert([api respondsToSelector:@selector(createTexturePlayerWithOptions:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(createTexturePlayerWithOptions:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FVPCreationOptions *arg_creationOptions = GetNullableObjectAtIndex(args, 0); FlutterError *error; - FVPTexturePlayerIds *output = [api createTexturePlayerWithOptions:arg_creationOptions - error:&error]; + FVPTexturePlayerIds *output = [api createTexturePlayerWithOptions:arg_creationOptions error:&error]; callback(wrapResult(output, error)); }]; } else { @@ -261,18 +389,13 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.setMixWithOthers", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(setMixWithOthers:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(setMixWithOthers:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_mixWithOthers = [GetNullableObjectAtIndex(args, 0) boolValue]; @@ -285,18 +408,13 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.getAssetUrl", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(fileURLForAssetWithName:package:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(fileURLForAssetWithName:package:error:)", - api); + NSCAssert([api respondsToSelector:@selector(fileURLForAssetWithName:package:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(fileURLForAssetWithName:package:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSString *arg_asset = GetNullableObjectAtIndex(args, 0); @@ -310,30 +428,20 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } } -void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, - NSObject *api) { +void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, NSObject *api) { SetUpFVPVideoPlayerInstanceApiWithSuffix(binaryMessenger, api, @""); } -void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, - NSObject *api, - NSString *messageChannelSuffix) { - messageChannelSuffix = messageChannelSuffix.length > 0 - ? [NSString stringWithFormat:@".%@", messageChannelSuffix] - : @""; +void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, NSObject *api, NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @""; { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.setLooping", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(setLooping:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setLooping:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setLooping:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setLooping:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_looping = [GetNullableObjectAtIndex(args, 0) boolValue]; @@ -346,18 +454,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.setVolume", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(setVolume:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setVolume:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setVolume:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setVolume:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_volume = [GetNullableObjectAtIndex(args, 0) doubleValue]; @@ -370,18 +473,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.setPlaybackSpeed", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to " - @"@selector(setPlaybackSpeed:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setPlaybackSpeed:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_speed = [GetNullableObjectAtIndex(args, 0) doubleValue]; @@ -394,17 +492,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.play", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(playWithError:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(playWithError:)", - api); + NSCAssert([api respondsToSelector:@selector(playWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(playWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api playWithError:&error]; @@ -415,16 +509,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.getPosition", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(position:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(position:)", api); + NSCAssert([api respondsToSelector:@selector(position:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(position:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; NSNumber *output = [api position:&error]; @@ -435,42 +526,32 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.seekTo", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(seekTo:completion:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(seekTo:completion:)", - api); + NSCAssert([api respondsToSelector:@selector(seekTo:completion:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(seekTo:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSInteger arg_position = [GetNullableObjectAtIndex(args, 0) integerValue]; - [api seekTo:arg_position - completion:^(FlutterError *_Nullable error) { - callback(wrapResult(nil, error)); - }]; + [api seekTo:arg_position completion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; }]; } else { [channel setMessageHandler:nil]; } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.pause", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(pauseWithError:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(pauseWithError:)", - api); + NSCAssert([api respondsToSelector:@selector(pauseWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(pauseWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api pauseWithError:&error]; @@ -481,18 +562,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.dispose", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(disposeWithError:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(disposeWithError:)", - api); + NSCAssert([api respondsToSelector:@selector(disposeWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(disposeWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api disposeWithError:&error]; @@ -502,4 +578,43 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM [channel setMessageHandler:nil]; } } + /// Gets the available video tracks for the video. + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getVideoTracks", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getVideoTracks:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getVideoTracks:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + [api getVideoTracks:^(FVPNativeVideoTrackData *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } + /// Selects a video track by setting preferredPeakBitRate. + /// Pass 0 to enable auto quality selection. + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectVideoTrack", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(selectVideoTrackWithBitrate:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(selectVideoTrackWithBitrate:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_bitrate = [GetNullableObjectAtIndex(args, 0) integerValue]; + FlutterError *error; + [api selectVideoTrackWithBitrate:arg_bitrate error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index cc176e75c3f..53cfd8b7452 100644 --- a/packages/video_player/video_player_avfoundation/example/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml @@ -16,7 +16,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - video_player_platform_interface: ^6.3.0 + video_player_platform_interface: + path: ../../video_player_platform_interface dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index 834b36ed6b0..26b87cd1a3b 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -21,8 +21,7 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { /// Creates a new AVFoundation-based video player implementation instance. AVFoundationVideoPlayer({ @visibleForTesting AVFoundationVideoPlayerApi? pluginApi, - @visibleForTesting - VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, + @visibleForTesting VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, }) : _api = pluginApi ?? AVFoundationVideoPlayerApi(), _playerApiProvider = playerApiProvider ?? _productionApiProvider; @@ -71,9 +70,7 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { case DataSourceType.asset: final String? asset = dataSource.asset; if (asset == null) { - throw ArgumentError( - '"asset" must be non-null for an asset data source', - ); + throw ArgumentError('"asset" must be non-null for an asset data source'); } uri = await _api.getAssetUrl(asset, dataSource.package); if (uri == null) { @@ -178,6 +175,87 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { return _api.setMixWithOthers(mixWithOthers); } + @override + Future> getVideoTracks(int playerId) async { + final NativeVideoTrackData nativeData = await _playerWith( + id: playerId, + ).getVideoTracks(); + final tracks = []; + + // Convert HLS variant tracks (iOS 15+) + if (nativeData.mediaSelectionTracks != null) { + for (final MediaSelectionVideoTrackData track in nativeData.mediaSelectionTracks!) { + // Use bitrate as the track ID for HLS variants + final String trackId = 'variant_${track.bitrate ?? track.variantIndex}'; + // Generate label from resolution if not provided + final String? label = + track.label ?? + (track.width != null && track.height != null ? '${track.height}p' : null); + tracks.add( + VideoTrack( + id: trackId, + isSelected: track.isSelected, + label: label, + bitrate: track.bitrate, + width: track.width, + height: track.height, + frameRate: track.frameRate, + codec: track.codec, + ), + ); + } + } + + // Convert asset tracks (for regular videos) + if (nativeData.assetTracks != null) { + for (final AssetVideoTrackData track in nativeData.assetTracks!) { + final String trackId = 'asset_${track.trackId}'; + // Generate label from resolution if not provided + final String? label = + track.label ?? + (track.width != null && track.height != null ? '${track.height}p' : null); + tracks.add( + VideoTrack( + id: trackId, + isSelected: track.isSelected, + label: label, + bitrate: null, + width: track.width, + height: track.height, + frameRate: track.frameRate, + codec: track.codec, + ), + ); + } + } + + return tracks; + } + + @override + Future selectVideoTrack(int playerId, VideoTrack? track) async { + if (track == null) { + // Auto quality - pass 0 to clear preferredPeakBitRate + await _playerWith(id: playerId).selectVideoTrack(0); + return; + } + + // Use bitrate directly from the track for HLS quality selection + if (track.bitrate != null) { + await _playerWith(id: playerId).selectVideoTrack(track.bitrate!); + return; + } + + // For asset tracks without bitrate, we can't really select them differently + // Just ignore the selection for non-HLS content + } + + @override + bool isVideoTrackSupportAvailable() { + // iOS with AVFoundation supports video track selection + return true; + } + @override Widget buildView(int playerId) { return buildViewWithOptions(VideoViewOptions(playerId: playerId)); @@ -189,9 +267,7 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { final VideoPlayerViewState viewState = _playerWith(id: playerId).viewState; return switch (viewState) { - VideoPlayerTextureViewState(:final int textureId) => Texture( - textureId: textureId, - ), + VideoPlayerTextureViewState(:final int textureId) => Texture(textureId: textureId), VideoPlayerPlatformViewState() => _buildPlatformView(playerId), }; } @@ -218,11 +294,8 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { /// An instance of a video player, corresponding to a single player ID in /// [AVFoundationVideoPlayer]. class _PlayerInstance { - _PlayerInstance( - this._api, - this.viewState, { - required EventChannel eventChannel, - }) : _eventChannel = eventChannel; + _PlayerInstance(this._api, this.viewState, {required EventChannel eventChannel}) + : _eventChannel = eventChannel; final VideoPlayerInstanceApi _api; final VideoPlayerViewState viewState; @@ -260,6 +333,14 @@ class _PlayerInstance { return _eventStreamController.stream; } + Future getVideoTracks() { + return _api.getVideoTracks(); + } + + Future selectVideoTrack(int bitrate) { + return _api.selectVideoTrack(bitrate); + } + Future dispose() async { await _eventSubscription?.cancel(); unawaited(_eventStreamController.close()); diff --git a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart index 9072c153f95..e5e88ec215a 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart @@ -17,49 +17,49 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } - bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && - a.indexed.every( - ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), - ); + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every( - (MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key]), - ); + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); } return a == b; } + /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { - PlatformVideoViewCreationParams({required this.playerId}); + PlatformVideoViewCreationParams({ + required this.playerId, + }); int playerId; List _toList() { - return [playerId]; + return [ + playerId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlatformVideoViewCreationParams decode(Object result) { result as List; - return PlatformVideoViewCreationParams(playerId: result[0]! as int); + return PlatformVideoViewCreationParams( + playerId: result[0]! as int, + ); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlatformVideoViewCreationParams || - other.runtimeType != runtimeType) { + if (other is! PlatformVideoViewCreationParams || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -70,30 +70,35 @@ class PlatformVideoViewCreationParams { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class CreationOptions { - CreationOptions({required this.uri, required this.httpHeaders}); + CreationOptions({ + required this.uri, + required this.httpHeaders, + }); String uri; Map httpHeaders; List _toList() { - return [uri, httpHeaders]; + return [ + uri, + httpHeaders, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static CreationOptions decode(Object result) { result as List; return CreationOptions( uri: result[0]! as String, - httpHeaders: (result[1] as Map?)! - .cast(), + httpHeaders: (result[1] as Map?)!.cast(), ); } @@ -111,23 +116,29 @@ class CreationOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class TexturePlayerIds { - TexturePlayerIds({required this.playerId, required this.textureId}); + TexturePlayerIds({ + required this.playerId, + required this.textureId, + }); int playerId; int textureId; List _toList() { - return [playerId, textureId]; + return [ + playerId, + textureId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static TexturePlayerIds decode(Object result) { result as List; @@ -151,9 +162,209 @@ class TexturePlayerIds { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Video track data from AVAssetVariant (HLS variants) for iOS 15+. +class MediaSelectionVideoTrackData { + MediaSelectionVideoTrackData({ + required this.variantIndex, + this.label, + this.bitrate, + this.width, + this.height, + this.frameRate, + this.codec, + required this.isSelected, + }); + + int variantIndex; + + String? label; + + int? bitrate; + + int? width; + + int? height; + + double? frameRate; + + String? codec; + + bool isSelected; + + List _toList() { + return [ + variantIndex, + label, + bitrate, + width, + height, + frameRate, + codec, + isSelected, + ]; + } + + Object encode() { + return _toList(); } + + static MediaSelectionVideoTrackData decode(Object result) { + result as List; + return MediaSelectionVideoTrackData( + variantIndex: result[0]! as int, + label: result[1] as String?, + bitrate: result[2] as int?, + width: result[3] as int?, + height: result[4] as int?, + frameRate: result[5] as double?, + codec: result[6] as String?, + isSelected: result[7]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! MediaSelectionVideoTrackData || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Video track data from AVAssetTrack (regular videos). +class AssetVideoTrackData { + AssetVideoTrackData({ + required this.trackId, + this.label, + this.width, + this.height, + this.frameRate, + this.codec, + required this.isSelected, + }); + + int trackId; + + String? label; + + int? width; + + int? height; + + double? frameRate; + + String? codec; + + bool isSelected; + + List _toList() { + return [ + trackId, + label, + width, + height, + frameRate, + codec, + isSelected, + ]; + } + + Object encode() { + return _toList(); } + + static AssetVideoTrackData decode(Object result) { + result as List; + return AssetVideoTrackData( + trackId: result[0]! as int, + label: result[1] as String?, + width: result[2] as int?, + height: result[3] as int?, + frameRate: result[4] as double?, + codec: result[5] as String?, + isSelected: result[6]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! AssetVideoTrackData || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; } +/// Container for video track data from iOS. +class NativeVideoTrackData { + NativeVideoTrackData({ + this.assetTracks, + this.mediaSelectionTracks, + }); + + /// Asset-based tracks (for regular videos) + List? assetTracks; + + /// Media selection tracks (for HLS variants on iOS 15+) + List? mediaSelectionTracks; + + List _toList() { + return [ + assetTracks, + mediaSelectionTracks, + ]; + } + + Object encode() { + return _toList(); } + + static NativeVideoTrackData decode(Object result) { + result as List; + return NativeVideoTrackData( + assetTracks: (result[0] as List?)?.cast(), + mediaSelectionTracks: (result[1] as List?)?.cast(), + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! NativeVideoTrackData || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -161,15 +372,24 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PlatformVideoViewCreationParams) { + } else if (value is PlatformVideoViewCreationParams) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is CreationOptions) { + } else if (value is CreationOptions) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is TexturePlayerIds) { + } else if (value is TexturePlayerIds) { buffer.putUint8(131); writeValue(buffer, value.encode()); + } else if (value is MediaSelectionVideoTrackData) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is AssetVideoTrackData) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is NativeVideoTrackData) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -178,12 +398,18 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: return PlatformVideoViewCreationParams.decode(readValue(buffer)!); - case 130: + case 130: return CreationOptions.decode(readValue(buffer)!); - case 131: + case 131: return TexturePlayerIds.decode(readValue(buffer)!); + case 132: + return MediaSelectionVideoTrackData.decode(readValue(buffer)!); + case 133: + return AssetVideoTrackData.decode(readValue(buffer)!); + case 134: + return NativeVideoTrackData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -194,13 +420,9 @@ class AVFoundationVideoPlayerApi { /// Constructor for [AVFoundationVideoPlayerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AVFoundationVideoPlayerApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + AVFoundationVideoPlayerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -208,14 +430,12 @@ class AVFoundationVideoPlayerApi { final String pigeonVar_messageChannelSuffix; Future initialize() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; @@ -233,17 +453,13 @@ class AVFoundationVideoPlayerApi { } Future createForPlatformView(CreationOptions params) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [params], + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([params]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -264,20 +480,14 @@ class AVFoundationVideoPlayerApi { } } - Future createForTextureView( - CreationOptions creationOptions, - ) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [creationOptions], + Future createForTextureView(CreationOptions creationOptions) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([creationOptions]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -299,17 +509,13 @@ class AVFoundationVideoPlayerApi { } Future setMixWithOthers(bool mixWithOthers) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [mixWithOthers], + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -326,17 +532,13 @@ class AVFoundationVideoPlayerApi { } Future getAssetUrl(String asset, String? package) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [asset, package], + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([asset, package]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -357,13 +559,9 @@ class VideoPlayerInstanceApi { /// Constructor for [VideoPlayerInstanceApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - VideoPlayerInstanceApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + VideoPlayerInstanceApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -371,17 +569,13 @@ class VideoPlayerInstanceApi { final String pigeonVar_messageChannelSuffix; Future setLooping(bool looping) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [looping], + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([looping]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -398,17 +592,13 @@ class VideoPlayerInstanceApi { } Future setVolume(double volume) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [volume], + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -425,17 +615,13 @@ class VideoPlayerInstanceApi { } Future setPlaybackSpeed(double speed) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [speed], + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([speed]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -452,14 +638,12 @@ class VideoPlayerInstanceApi { } Future play() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; @@ -477,14 +661,12 @@ class VideoPlayerInstanceApi { } Future getPosition() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; @@ -507,17 +689,13 @@ class VideoPlayerInstanceApi { } Future seekTo(int position) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [position], + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([position]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -534,14 +712,12 @@ class VideoPlayerInstanceApi { } Future pause() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; @@ -559,14 +735,12 @@ class VideoPlayerInstanceApi { } Future dispose() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; @@ -582,4 +756,58 @@ class VideoPlayerInstanceApi { return; } } + + /// Gets the available video tracks for the video. + Future getVideoTracks() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getVideoTracks$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as NativeVideoTrackData?)!; + } + } + + /// Selects a video track by setting preferredPeakBitRate. + /// Pass 0 to enable auto quality selection. + Future selectVideoTrack(int bitrate) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectVideoTrack$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([bitrate]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } } diff --git a/packages/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart index 6e872dec145..fc9fe59f8a4 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -56,6 +56,61 @@ abstract class AVFoundationVideoPlayerApi { String? getAssetUrl(String asset, String? package); } +/// Video track data from AVAssetVariant (HLS variants) for iOS 15+. +class MediaSelectionVideoTrackData { + MediaSelectionVideoTrackData({ + required this.variantIndex, + this.label, + this.bitrate, + this.width, + this.height, + this.frameRate, + this.codec, + required this.isSelected, + }); + + int variantIndex; + String? label; + int? bitrate; + int? width; + int? height; + double? frameRate; + String? codec; + bool isSelected; +} + +/// Video track data from AVAssetTrack (regular videos). +class AssetVideoTrackData { + AssetVideoTrackData({ + required this.trackId, + this.label, + this.width, + this.height, + this.frameRate, + this.codec, + required this.isSelected, + }); + + int trackId; + String? label; + int? width; + int? height; + double? frameRate; + String? codec; + bool isSelected; +} + +/// Container for video track data from iOS. +class NativeVideoTrackData { + NativeVideoTrackData({this.assetTracks, this.mediaSelectionTracks}); + + /// Asset-based tracks (for regular videos) + List? assetTracks; + + /// Media selection tracks (for HLS variants on iOS 15+) + List? mediaSelectionTracks; +} + @HostApi() abstract class VideoPlayerInstanceApi { @ObjCSelector('setLooping:') @@ -72,4 +127,14 @@ abstract class VideoPlayerInstanceApi { void seekTo(int position); void pause(); void dispose(); + + /// Gets the available video tracks for the video. + @async + @ObjCSelector('getVideoTracks') + NativeVideoTrackData getVideoTracks(); + + /// Selects a video track by setting preferredPeakBitRate. + /// Pass 0 to enable auto quality selection. + @ObjCSelector('selectVideoTrackWithBitrate:') + void selectVideoTrack(int bitrate); } diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 9c326136d92..9f26f8e5b31 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -24,7 +24,8 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^6.3.0 + video_player_platform_interface: + path: ../video_player_platform_interface dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index c16d5bcb08e..11949fc4865 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -690,5 +690,171 @@ void main() { ]), ); }); + + group('video tracks', () { + test('isVideoTrackSupportAvailable returns true', () { + final (AVFoundationVideoPlayer player, _, _) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + + expect(player.isVideoTrackSupportAvailable(), true); + }); + + test('getVideoTracks returns empty list when no tracks', () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when(api.getVideoTracks()).thenAnswer((_) async => NativeVideoTrackData()); + + final List tracks = await player.getVideoTracks(1); + + expect(tracks, isEmpty); + }); + + test('getVideoTracks converts HLS variant tracks to VideoTrack', () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when(api.getVideoTracks()).thenAnswer( + (_) async => NativeVideoTrackData( + mediaSelectionTracks: [ + MediaSelectionVideoTrackData( + variantIndex: 0, + label: '1080p', + isSelected: true, + bitrate: 5000000, + width: 1920, + height: 1080, + frameRate: 30.0, + codec: 'avc1', + ), + MediaSelectionVideoTrackData( + variantIndex: 1, + label: '720p', + isSelected: false, + bitrate: 2500000, + width: 1280, + height: 720, + frameRate: 30.0, + codec: 'avc1', + ), + ], + ), + ); + + final List tracks = await player.getVideoTracks(1); + + expect(tracks.length, 2); + + expect(tracks[0].id, 'variant_5000000'); + expect(tracks[0].label, '1080p'); + expect(tracks[0].isSelected, true); + expect(tracks[0].bitrate, 5000000); + expect(tracks[0].width, 1920); + expect(tracks[0].height, 1080); + expect(tracks[0].frameRate, 30.0); + expect(tracks[0].codec, 'avc1'); + + expect(tracks[1].id, 'variant_2500000'); + expect(tracks[1].label, '720p'); + expect(tracks[1].isSelected, false); + }); + + test('getVideoTracks generates label from resolution if not provided', () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when(api.getVideoTracks()).thenAnswer( + (_) async => NativeVideoTrackData( + mediaSelectionTracks: [ + MediaSelectionVideoTrackData( + variantIndex: 0, + isSelected: true, + bitrate: 5000000, + width: 1920, + height: 1080, + ), + ], + ), + ); + + final List tracks = await player.getVideoTracks(1); + + expect(tracks.length, 1); + expect(tracks[0].label, '1080p'); + }); + + test('selectVideoTrack with null sets auto quality (bitrate 0)', () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when(api.selectVideoTrack(0)).thenAnswer((_) async {}); + + await player.selectVideoTrack(1, null); + + verify(api.selectVideoTrack(0)); + }); + + test('selectVideoTrack with track uses bitrate', () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when(api.selectVideoTrack(5000000)).thenAnswer((_) async {}); + + const track = VideoTrack( + id: 'variant_5000000', + isSelected: false, + bitrate: 5000000, + ); + await player.selectVideoTrack(1, track); + + verify(api.selectVideoTrack(5000000)); + }); + + test('selectVideoTrack ignores track without bitrate', () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + + const track = VideoTrack( + id: 'asset_123', + isSelected: false, + ); + await player.selectVideoTrack(1, track); + + verifyNever(api.selectVideoTrack(any)); + }); + }); }); } diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart index 8caf6ad8dc4..6afa3dd8dcb 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart @@ -29,6 +29,12 @@ class _FakeTexturePlayerIds_0 extends _i1.SmartFake : super(parent, parentInvocation); } +class _FakeNativeVideoTrackData_1 extends _i1.SmartFake + implements _i2.NativeVideoTrackData { + _FakeNativeVideoTrackData_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + /// A class which mocks [AVFoundationVideoPlayerApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -198,4 +204,33 @@ class MockVideoPlayerInstanceApi extends _i1.Mock returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + + @override + _i4.Future<_i2.NativeVideoTrackData> getVideoTracks() => + (super.noSuchMethod( + Invocation.method(#getVideoTracks, []), + returnValue: _i4.Future<_i2.NativeVideoTrackData>.value( + _FakeNativeVideoTrackData_1( + this, + Invocation.method(#getVideoTracks, []), + ), + ), + returnValueForMissingStub: + _i4.Future<_i2.NativeVideoTrackData>.value( + _FakeNativeVideoTrackData_1( + this, + Invocation.method(#getVideoTracks, []), + ), + ), + ) + as _i4.Future<_i2.NativeVideoTrackData>); + + @override + _i4.Future selectVideoTrack(int? bitrate) => + (super.noSuchMethod( + Invocation.method(#selectVideoTrack, [bitrate]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); } diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 2f383e86f2d..47eb400127a 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Updates minimum supported SDK version to Flutter 3.32/Dart 3.8. +* Adds stub implementation for video track selection (not supported on web). ## 2.4.0 diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index 92f965e988d..8b823ecf53a 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -8,7 +8,8 @@ environment: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^6.3.0 + video_player_platform_interface: + path: ../../video_player_platform_interface video_player_web: path: ../ web: ^1.0.0 diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart index ecc8e427d2d..b45b3770630 100644 --- a/packages/video_player/video_player_web/lib/video_player_web.dart +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -102,8 +102,7 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { (int viewId) => videoElement, ); - final player = VideoPlayer(videoElement: videoElement) - ..initialize(src: uri); + final player = VideoPlayer(videoElement: videoElement)..initialize(src: uri); _videoPlayers[playerId] = player; @@ -169,4 +168,20 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { /// Sets the audio mode to mix with other sources (ignored). @override Future setMixWithOthers(bool mixWithOthers) => Future.value(); + + @override + Future> getVideoTracks(int playerId) { + throw UnimplementedError('getVideoTracks() is not supported on web'); + } + + @override + Future selectVideoTrack(int playerId, VideoTrack? track) { + throw UnimplementedError('selectVideoTrack() is not supported on web'); + } + + @override + bool isVideoTrackSupportAvailable() { + // Web does not support video track selection + return false; + } } diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index 39148c8fd3c..a974f97a8bb 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -21,7 +21,8 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - video_player_platform_interface: ^6.4.0 + video_player_platform_interface: + path: ../video_player_platform_interface web: ">=0.5.1 <2.0.0" dev_dependencies: From 19fbb6fb47f28d97ff8a4f456418f0cdf1ab84b4 Mon Sep 17 00:00:00 2001 From: nateshmbhat Date: Sat, 27 Dec 2025 21:42:55 +0530 Subject: [PATCH 3/7] formatted the files --- .../example/lib/video_tracks_demo.dart | 27 +- .../video_player/lib/video_player.dart | 58 +- .../video_player/test/video_player_test.dart | 380 +++++--- .../plugins/videoplayer/VideoPlayer.java | 3 +- .../flutter/plugins/videoplayer/Messages.kt | 878 ++++++++++-------- .../lib/src/android_video_player.dart | 32 +- .../lib/src/messages.g.dart | 438 +++++---- .../pigeons/messages.dart | 3 +- .../test/android_video_player_test.dart | 401 +++++--- .../FVPVideoPlayer.m | 150 ++- .../video_player_avfoundation/messages.g.h | 118 +-- .../video_player_avfoundation/messages.g.m | 327 ++++--- .../lib/src/avfoundation_video_player.dart | 29 +- .../lib/src/messages.g.dart | 383 ++++---- .../test/avfoundation_video_player_test.dart | 204 ++-- .../lib/video_player_platform_interface.dart | 31 +- .../video_player_platform_interface_test.dart | 5 +- .../lib/video_player_web.dart | 3 +- 18 files changed, 2014 insertions(+), 1456 deletions(-) diff --git a/packages/video_player/video_player/example/lib/video_tracks_demo.dart b/packages/video_player/video_player/example/lib/video_tracks_demo.dart index 811a52b7616..fe59fa176d0 100644 --- a/packages/video_player/video_player/example/lib/video_tracks_demo.dart +++ b/packages/video_player/video_player/example/lib/video_tracks_demo.dart @@ -141,14 +141,16 @@ class _VideoTracksDemoState extends State { final String message = track == null ? 'Switched to automatic quality' : 'Selected video track: ${_getTrackLabel(track)}'; - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); } catch (e) { if (!mounted) { return; } - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Failed to select video track: $e'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to select video track: $e')), + ); } } @@ -228,7 +230,9 @@ class _VideoTracksDemoState extends State { inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), ), - dropdownMenuEntries: _sampleVideos.indexed.map(((int, String) record) { + dropdownMenuEntries: _sampleVideos.indexed.map(( + (int, String) record, + ) { final (int index, String url) = record; final String label = url.contains('.m3u8') ? 'HLS Stream ${index + 1}' @@ -285,7 +289,10 @@ class _VideoTracksDemoState extends State { ), ), const SizedBox(height: 16), - ElevatedButton(onPressed: _initializeVideo, child: const Text('Retry')), + ElevatedButton( + onPressed: _initializeVideo, + child: const Text('Retry'), + ), ], ), ); @@ -374,7 +381,9 @@ class _VideoTracksDemoState extends State { title: Text( 'Automatic Quality', style: TextStyle( - fontWeight: _isAutoQuality ? FontWeight.bold : FontWeight.normal, + fontWeight: _isAutoQuality + ? FontWeight.bold + : FontWeight.normal, ), ), subtitle: const Text('Let the player choose the best quality'), @@ -434,7 +443,9 @@ class _VideoTracksDemoState extends State { ), title: Text( _getTrackLabel(track), - style: TextStyle(fontWeight: isSelected ? FontWeight.bold : FontWeight.normal), + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 0ba94b6c747..a4396ef5dd6 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -467,7 +467,9 @@ class VideoPlayerController extends ValueNotifier { ); if (videoPlayerOptions?.mixWithOthers != null) { - await _videoPlayerPlatform.setMixWithOthers(videoPlayerOptions!.mixWithOthers); + await _videoPlayerPlatform.setMixWithOthers( + videoPlayerOptions!.mixWithOthers, + ); } _playerId = @@ -528,7 +530,10 @@ class VideoPlayerController extends ValueNotifier { value = value.copyWith(isBuffering: false); case VideoEventType.isPlayingStateUpdate: if (event.isPlaying ?? false) { - value = value.copyWith(isPlaying: event.isPlaying, isCompleted: false); + value = value.copyWith( + isPlaying: event.isPlaying, + isCompleted: false, + ); } else { value = value.copyWith(isPlaying: event.isPlaying); } @@ -619,7 +624,9 @@ class VideoPlayerController extends ValueNotifier { await _videoPlayerPlatform.play(_playerId); _timer?.cancel(); - _timer = Timer.periodic(const Duration(milliseconds: 100), (Timer timer) async { + _timer = Timer.periodic(const Duration(milliseconds: 100), ( + Timer timer, + ) async { if (_isDisposed) { return; } @@ -741,7 +748,10 @@ class VideoPlayerController extends ValueNotifier { /// * >0: The caption will have a negative offset. So you will get caption text from the past. /// * <0: The caption will have a positive offset. So you will get caption text from the future. void setCaptionOffset(Duration offset) { - value = value.copyWith(captionOffset: offset, caption: _getCaptionAt(value.position)); + value = value.copyWith( + captionOffset: offset, + caption: _getCaptionAt(value.position), + ); } /// The closed caption based on the current [position] in the video. @@ -775,7 +785,9 @@ class VideoPlayerController extends ValueNotifier { /// Sets a closed caption file. /// /// If [closedCaptionFile] is null, closed captions will be removed. - Future setClosedCaptionFile(Future? closedCaptionFile) async { + Future setClosedCaptionFile( + Future? closedCaptionFile, + ) async { await _updateClosedCaptionWithFuture(closedCaptionFile); _closedCaptionFileFuture = closedCaptionFile; } @@ -828,10 +840,13 @@ class VideoPlayerController extends ValueNotifier { if (_isDisposedOrNotInitialized) { return []; } - final List platformTracks = await _videoPlayerPlatform - .getVideoTracks(_playerId); + final List platformTracks = + await _videoPlayerPlatform.getVideoTracks(_playerId); return platformTracks - .map((platform_interface.VideoTrack track) => VideoTrack._fromPlatform(track)) + .map( + (platform_interface.VideoTrack track) => + VideoTrack._fromPlatform(track), + ) .toList(); } @@ -1026,7 +1041,11 @@ class VideoScrubber extends StatefulWidget { /// /// [controller] is the [VideoPlayerController] that will be controlled by /// this scrubber. - const VideoScrubber({super.key, required this.child, required this.controller}); + const VideoScrubber({ + super.key, + required this.child, + required this.controller, + }); /// The widget that will be displayed inside the gesture detector. final Widget child; @@ -1196,7 +1215,10 @@ class _VideoProgressIndicatorState extends State { child: progressIndicator, ); if (widget.allowScrubbing) { - return VideoScrubber(controller: controller, child: paddedProgressIndicator); + return VideoScrubber( + controller: controller, + child: paddedProgressIndicator, + ); } else { return paddedProgressIndicator; } @@ -1248,7 +1270,9 @@ class ClosedCaption extends StatelessWidget { final TextStyle effectiveTextStyle = textStyle ?? - DefaultTextStyle.of(context).style.copyWith(fontSize: 36.0, color: Colors.white); + DefaultTextStyle.of( + context, + ).style.copyWith(fontSize: 36.0, color: Colors.white); return Align( alignment: Alignment.bottomCenter, @@ -1358,8 +1382,16 @@ class VideoTrack { } @override - int get hashCode => - Object.hash(id, isSelected, label, bitrate, width, height, frameRate, codec); + int get hashCode => Object.hash( + id, + isSelected, + label, + bitrate, + width, + height, + frameRate, + codec, + ); @override String toString() => diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index c0b3c4c277f..1d9c91232f8 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -85,7 +85,9 @@ class FakeController extends ValueNotifier void setCaptionOffset(Duration delay) {} @override - Future setClosedCaptionFile(Future? closedCaptionFile) async {} + Future setClosedCaptionFile( + Future? closedCaptionFile, + ) async {} @override Future> getVideoTracks() async => []; @@ -97,7 +99,8 @@ class FakeController extends ValueNotifier bool isVideoTrackSupportAvailable() => false; } -Future _loadClosedCaption() async => _FakeClosedCaptionFile(); +Future _loadClosedCaption() async => + _FakeClosedCaptionFile(); class _FakeClosedCaptionFile extends ClosedCaptionFile { @override @@ -132,9 +135,13 @@ void main() { required bool shouldPlayInBackground, }) { expect(controller.value.isPlaying, true); - WidgetsBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.paused); + WidgetsBinding.instance.handleAppLifecycleStateChanged( + AppLifecycleState.paused, + ); expect(controller.value.isPlaying, shouldPlayInBackground); - WidgetsBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + WidgetsBinding.instance.handleAppLifecycleStateChanged( + AppLifecycleState.resumed, + ); expect(controller.value.isPlaying, true); } @@ -178,37 +185,38 @@ void main() { ); }); - testWidgets('VideoPlayer still listens for controller changes when reparented', ( - WidgetTester tester, - ) async { - final controller = FakeController(); - addTearDown(controller.dispose); - final GlobalKey videoKey = GlobalKey(); - final Widget videoPlayer = KeyedSubtree( - key: videoKey, - child: VideoPlayer(controller), - ); + testWidgets( + 'VideoPlayer still listens for controller changes when reparented', + (WidgetTester tester) async { + final controller = FakeController(); + addTearDown(controller.dispose); + final GlobalKey videoKey = GlobalKey(); + final Widget videoPlayer = KeyedSubtree( + key: videoKey, + child: VideoPlayer(controller), + ); - await tester.pumpWidget(videoPlayer); - expect(find.byType(Texture), findsNothing); + await tester.pumpWidget(videoPlayer); + expect(find.byType(Texture), findsNothing); - // The VideoPlayer is reparented in the widget tree, before the - // underlying player is initialized. - await tester.pumpWidget(SizedBox(child: videoPlayer)); - controller.playerId = 321; - controller.value = controller.value.copyWith( - duration: const Duration(milliseconds: 100), - isInitialized: true, - ); + // The VideoPlayer is reparented in the widget tree, before the + // underlying player is initialized. + await tester.pumpWidget(SizedBox(child: videoPlayer)); + controller.playerId = 321; + controller.value = controller.value.copyWith( + duration: const Duration(milliseconds: 100), + isInitialized: true, + ); - await tester.pump(); - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Texture && widget.textureId == 321, - ), - findsOneWidget, - ); - }); + await tester.pump(); + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Texture && widget.textureId == 321, + ), + findsOneWidget, + ); + }, + ); testWidgets( 'VideoProgressIndicator still listens for controller changes after reparenting', @@ -229,7 +237,9 @@ void main() { ); await tester.pumpWidget(MaterialApp(home: progressIndicator)); await tester.pump(); - await tester.pumpWidget(MaterialApp(home: SizedBox(child: progressIndicator))); + await tester.pumpWidget( + MaterialApp(home: SizedBox(child: progressIndicator)), + ); expect((key.currentContext! as Element).dirty, isFalse); // Verify that changing value dirties the widget tree. controller.value = controller.value.copyWith( @@ -249,12 +259,16 @@ void main() { isInitialized: true, ); await tester.pumpWidget( - MaterialApp(home: VideoProgressIndicator(controller, allowScrubbing: false)), + MaterialApp( + home: VideoProgressIndicator(controller, allowScrubbing: false), + ), ); expect(tester.takeException(), isNull); }); - testWidgets('non-zero rotationCorrection value is used', (WidgetTester tester) async { + testWidgets('non-zero rotationCorrection value is used', ( + WidgetTester tester, + ) async { final controller = FakeController.value( const VideoPlayerValue(duration: Duration.zero, rotationCorrection: 180), ); @@ -282,7 +296,9 @@ void main() { group('ClosedCaption widget', () { testWidgets('uses a default text style', (WidgetTester tester) async { const text = 'foo'; - await tester.pumpWidget(const MaterialApp(home: ClosedCaption(text: text))); + await tester.pumpWidget( + const MaterialApp(home: ClosedCaption(text: text)), + ); final Text textWidget = tester.widget(find.text(text)); expect(textWidget.style!.fontSize, 36.0); @@ -313,7 +329,9 @@ void main() { expect(find.byType(Text), findsNothing); }); - testWidgets('Passes text contrast ratio guidelines', (WidgetTester tester) async { + testWidgets('Passes text contrast ratio guidelines', ( + WidgetTester tester, + ) async { const text = 'foo'; await tester.pumpWidget( const MaterialApp( @@ -337,7 +355,10 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {}, + ); }); test('network with hint', () async { @@ -348,8 +369,14 @@ void main() { await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); - expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, VideoFormat.dash); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); + expect( + fakeVideoPlayerPlatform.dataSources[0].formatHint, + VideoFormat.dash, + ); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {}, + ); }); test('network with some headers', () async { @@ -361,9 +388,10 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, { - 'Authorization': 'Bearer token', - }); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {'Authorization': 'Bearer token'}, + ); }); }); @@ -375,7 +403,10 @@ void main() { addTearDown(controller.dispose); await controller.initialize(); await controller.play(); - verifyPlayStateRespondsToLifecycle(controller, shouldPlayInBackground: false); + verifyPlayStateRespondsToLifecycle( + controller, + shouldPlayInBackground: false, + ); }); test('asset', () async { @@ -395,7 +426,10 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {}, + ); }); test('network url with hint', () async { @@ -407,8 +441,14 @@ void main() { await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); - expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, VideoFormat.dash); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); + expect( + fakeVideoPlayerPlatform.dataSources[0].formatHint, + VideoFormat.dash, + ); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {}, + ); }); test('network url with some headers', () async { @@ -421,9 +461,10 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, { - 'Authorization': 'Bearer token', - }); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {'Authorization': 'Bearer token'}, + ); }); test( @@ -451,40 +492,64 @@ void main() { expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); }, skip: kIsWeb /* Web does not support file assets. */); - test('file with special characters', () async { - final controller = VideoPlayerController.file(File('A #1 Hit.avi')); - await controller.initialize(); + test( + 'file with special characters', + () async { + final controller = VideoPlayerController.file(File('A #1 Hit.avi')); + await controller.initialize(); - final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; - expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); - expect(uri.endsWith('/A%20%231%20Hit.avi'), true, reason: 'Actual string: $uri'); - }, skip: kIsWeb /* Web does not support file assets. */); + final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; + expect( + uri.startsWith('file:///'), + true, + reason: 'Actual string: $uri', + ); + expect( + uri.endsWith('/A%20%231%20Hit.avi'), + true, + reason: 'Actual string: $uri', + ); + }, + skip: kIsWeb /* Web does not support file assets. */, + ); - test('file with headers (m3u8)', () async { - final controller = VideoPlayerController.file( - File('a.avi'), - httpHeaders: {'Authorization': 'Bearer token'}, - ); - await controller.initialize(); + test( + 'file with headers (m3u8)', + () async { + final controller = VideoPlayerController.file( + File('a.avi'), + httpHeaders: {'Authorization': 'Bearer token'}, + ); + await controller.initialize(); - final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; - expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); - expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); + final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; + expect( + uri.startsWith('file:///'), + true, + reason: 'Actual string: $uri', + ); + expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, { - 'Authorization': 'Bearer token', - }); - }, skip: kIsWeb /* Web does not support file assets. */); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {'Authorization': 'Bearer token'}, + ); + }, + skip: kIsWeb /* Web does not support file assets. */, + ); - test('successful initialize on controller with error clears error', () async { - final controller = VideoPlayerController.network('https://127.0.0.1'); - fakeVideoPlayerPlatform.forceInitError = true; - await controller.initialize().catchError((dynamic e) {}); - expect(controller.value.hasError, equals(true)); - fakeVideoPlayerPlatform.forceInitError = false; - await controller.initialize(); - expect(controller.value.hasError, equals(false)); - }); + test( + 'successful initialize on controller with error clears error', + () async { + final controller = VideoPlayerController.network('https://127.0.0.1'); + fakeVideoPlayerPlatform.forceInitError = true; + await controller.initialize().catchError((dynamic e) {}); + expect(controller.value.hasError, equals(true)); + fakeVideoPlayerPlatform.forceInitError = false; + await controller.initialize(); + expect(controller.value.hasError, equals(false)); + }, + ); test( 'given controller with error when initialization succeeds it should clear error', @@ -503,7 +568,9 @@ void main() { }); test('contentUri', () async { - final controller = VideoPlayerController.contentUri(Uri.parse('content://video')); + final controller = VideoPlayerController.contentUri( + Uri.parse('content://video'), + ); await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'content://video'); @@ -534,7 +601,9 @@ void main() { }); test('play', () async { - final controller = VideoPlayerController.networkUrl(Uri.parse('https://127.0.0.1')); + final controller = VideoPlayerController.networkUrl( + Uri.parse('https://127.0.0.1'), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -699,14 +768,22 @@ void main() { }); group('scrubbing', () { - testWidgets('restarts on release if already playing', (WidgetTester tester) async { + testWidgets('restarts on release if already playing', ( + WidgetTester tester, + ) async { final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - final progressWidget = VideoProgressIndicator(controller, allowScrubbing: true); + final progressWidget = VideoProgressIndicator( + controller, + allowScrubbing: true, + ); await tester.pumpWidget( - Directionality(textDirection: TextDirection.ltr, child: progressWidget), + Directionality( + textDirection: TextDirection.ltr, + child: progressWidget, + ), ); await controller.play(); @@ -723,14 +800,22 @@ void main() { await tester.runAsync(controller.dispose); }); - testWidgets('does not restart when dragging to end', (WidgetTester tester) async { + testWidgets('does not restart when dragging to end', ( + WidgetTester tester, + ) async { final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - final progressWidget = VideoProgressIndicator(controller, allowScrubbing: true); + final progressWidget = VideoProgressIndicator( + controller, + allowScrubbing: true, + ); await tester.pumpWidget( - Directionality(textDirection: TextDirection.ltr, child: progressWidget), + Directionality( + textDirection: TextDirection.ltr, + child: progressWidget, + ), ); await controller.play(); @@ -941,7 +1026,9 @@ void main() { final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.playerId]!; - fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed)); + fakeVideoEventStream.add( + VideoEvent(eventType: VideoEventType.completed), + ); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); @@ -957,13 +1044,19 @@ void main() { fakeVideoPlayerPlatform.streams[controller.playerId]!; fakeVideoEventStream.add( - VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: true), + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + ), ); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isTrue); fakeVideoEventStream.add( - VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: false), + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: false, + ), ); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); @@ -979,7 +1072,9 @@ void main() { final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.playerId]!; - fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.bufferingStart)); + fakeVideoEventStream.add( + VideoEvent(eventType: VideoEventType.bufferingStart), + ); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isTrue); @@ -999,7 +1094,9 @@ void main() { DurationRange(bufferStart, bufferEnd).toString(), ); - fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.bufferingEnd)); + fakeVideoEventStream.add( + VideoEvent(eventType: VideoEventType.bufferingEnd), + ); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isFalse); await tester.runAsync(controller.dispose); @@ -1179,13 +1276,17 @@ void main() { }); test('errorDescription is changed when copy with another error', () { const original = VideoPlayerValue.erroneous('error'); - final VideoPlayerValue copy = original.copyWith(errorDescription: 'new error'); + final VideoPlayerValue copy = original.copyWith( + errorDescription: 'new error', + ); expect(copy.errorDescription, 'new error'); }); test('errorDescription is changed when copy with error', () { const original = VideoPlayerValue.uninitialized(); - final VideoPlayerValue copy = original.copyWith(errorDescription: 'new error'); + final VideoPlayerValue copy = original.copyWith( + errorDescription: 'new error', + ); expect(copy.errorDescription, 'new error'); }); @@ -1259,7 +1360,10 @@ void main() { await controller.initialize(); await controller.play(); - verifyPlayStateRespondsToLifecycle(controller, shouldPlayInBackground: true); + verifyPlayStateRespondsToLifecycle( + controller, + shouldPlayInBackground: true, + ); }); test('false allowBackgroundPlayback pauses playback', () async { @@ -1271,7 +1375,10 @@ void main() { await controller.initialize(); await controller.play(); - verifyPlayStateRespondsToLifecycle(controller, shouldPlayInBackground: false); + verifyPlayStateRespondsToLifecycle( + controller, + shouldPlayInBackground: false, + ); }); }); @@ -1344,7 +1451,10 @@ void main() { isCompletedTest(); if (!hasLooped) { fakeVideoEventStream.add( - VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: true), + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + ), ); hasLooped = !hasLooped; } @@ -1370,7 +1480,9 @@ void main() { final void Function() isCompletedTest = expectAsync0(() {}); - controller.value = controller.value.copyWith(duration: const Duration(seconds: 10)); + controller.value = controller.value.copyWith( + duration: const Duration(seconds: 10), + ); controller.addListener(() async { if (currentIsCompleted != controller.value.isCompleted) { @@ -1408,25 +1520,27 @@ void main() { final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - fakeVideoPlayerPlatform - .setVideoTracksForPlayer(controller.playerId, [ - const platform_interface.VideoTrack( - id: '0_0', - isSelected: true, - label: '1080p', - bitrate: 5000000, - width: 1920, - height: 1080, - ), - const platform_interface.VideoTrack( - id: '0_1', - isSelected: false, - label: '720p', - bitrate: 2500000, - width: 1280, - height: 720, - ), - ]); + fakeVideoPlayerPlatform.setVideoTracksForPlayer( + controller.playerId, + [ + const platform_interface.VideoTrack( + id: '0_0', + isSelected: true, + label: '1080p', + bitrate: 5000000, + width: 1920, + height: 1080, + ), + const platform_interface.VideoTrack( + id: '0_1', + isSelected: false, + label: '720p', + bitrate: 2500000, + width: 1280, + height: 720, + ), + ], + ); final tracks = await controller.getVideoTracks(); @@ -1469,7 +1583,10 @@ void main() { await controller.selectVideoTrack(null); - expect(fakeVideoPlayerPlatform.calls, isNot(contains('selectVideoTrack'))); + expect( + fakeVideoPlayerPlatform.calls, + isNot(contains('selectVideoTrack')), + ); }); }); } @@ -1484,7 +1601,8 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { bool forceInitError = false; int nextPlayerId = 0; final Map _positions = {}; - final Map webOptions = {}; + final Map webOptions = + {}; @override Future create(DataSource dataSource) async { @@ -1493,7 +1611,10 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { streams[nextPlayerId] = stream; if (forceInitError) { stream.addError( - PlatformException(code: 'VideoError', message: 'Video player had error XYZ'), + PlatformException( + code: 'VideoError', + message: 'Video player had error XYZ', + ), ); } else { stream.add( @@ -1515,7 +1636,10 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { streams[nextPlayerId] = stream; if (forceInitError) { stream.addError( - PlatformException(code: 'VideoError', message: 'Video player had error XYZ'), + PlatformException( + code: 'VideoError', + message: 'Video player had error XYZ', + ), ); } else { stream.add( @@ -1595,7 +1719,10 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { } @override - Future setWebOptions(int playerId, VideoPlayerWebOptions options) async { + Future setWebOptions( + int playerId, + VideoPlayerWebOptions options, + ) async { if (!kIsWeb) { throw UnimplementedError('setWebOptions() is only available in the web.'); } @@ -1608,12 +1735,17 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { >{}; platform_interface.VideoTrack? _selectedVideoTrack; - void setVideoTracksForPlayer(int playerId, List tracks) { + void setVideoTracksForPlayer( + int playerId, + List tracks, + ) { _videoTracks[playerId] = tracks; } @override - Future> getVideoTracks(int playerId) async { + Future> getVideoTracks( + int playerId, + ) async { calls.add('getVideoTracks'); return _videoTracks[playerId] ?? []; } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 4758971e225..d22152ca2cf 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -331,8 +331,7 @@ public void selectVideoTrack(long groupIndex, long trackIndex) { Format newFormat = trackGroup.getFormat((int) trackIndex); boolean dimensionsChanged = currentFormat != null - && (currentFormat.width != newFormat.width - || currentFormat.height != newFormat.height); + && (currentFormat.width != newFormat.width || currentFormat.height != newFormat.height); // When video dimensions change, we need to force a complete renderer reset to avoid // surface rendering issues. We do this by temporarily disabling the video track type, diff --git a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt index 20d0c7f6b9f..6faff6e1abe 100644 --- a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt +++ b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt @@ -12,10 +12,11 @@ import io.flutter.plugin.common.BasicMessageChannel import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec import io.flutter.plugin.common.StandardMessageCodec +import io.flutter.plugin.common.StandardMethodCodec import java.io.ByteArrayOutputStream import java.nio.ByteBuffer + private object MessagesPigeonUtils { fun wrapResult(result: Any?): List { @@ -24,61 +25,53 @@ private object MessagesPigeonUtils { fun wrapError(exception: Throwable): List { return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) + listOf(exception.code, exception.message, exception.details) } else { listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)) } } + fun deepEquals(a: Any?, b: Any?): Boolean { if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is IntArray && b is IntArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is LongArray && b is LongArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is DoubleArray && b is DoubleArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is Array<*> && b is Array<*>) { - return a.size == b.size && - a.indices.all{ deepEquals(a[it], b[it]) } + return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } } if (a is List<*> && b is List<*>) { - return a.size == b.size && - a.indices.all{ deepEquals(a[it], b[it]) } + return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } } if (a is Map<*, *> && b is Map<*, *>) { - return a.size == b.size && a.all { - (b as Map).contains(it.key) && - deepEquals(it.value, b[it.key]) - } + return a.size == b.size && + a.all { (b as Map).contains(it.key) && deepEquals(it.value, b[it.key]) } } return a == b } - } /** * Error class for passing custom error details to Flutter via a thrown PlatformException. + * * @property code The error code. * @property message The error message. * @property details The error details. Must be a datatype supported by the api codec. */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null +class FlutterError( + val code: String, + override val message: String? = null, + val details: Any? = null ) : Throwable() /** Pigeon equivalent of video_platform_interface's VideoFormat. */ @@ -113,26 +106,25 @@ enum class PlatformPlaybackState(val raw: Int) { } /** - * Generated class from Pigeon that represents data sent in messages. - * This class should not be extended by any user class outside of the generated file. + * Generated class from Pigeon that represents data sent in messages. This class should not be + * extended by any user class outside of the generated file. */ -sealed class PlatformVideoEvent +sealed class PlatformVideoEvent /** * Sent when the video is initialized and ready to play. * * Generated class from Pigeon that represents data sent in messages. */ -data class InitializationEvent ( - /** The video duration in milliseconds. */ - val duration: Long, - /** The width of the video in pixels. */ - val width: Long, - /** The height of the video in pixels. */ - val height: Long, - /** The rotation that should be applied during playback. */ - val rotationCorrection: Long -) : PlatformVideoEvent() - { +data class InitializationEvent( + /** The video duration in milliseconds. */ + val duration: Long, + /** The width of the video in pixels. */ + val width: Long, + /** The height of the video in pixels. */ + val height: Long, + /** The rotation that should be applied during playback. */ + val rotationCorrection: Long +) : PlatformVideoEvent() { companion object { fun fromList(pigeonVar_list: List): InitializationEvent { val duration = pigeonVar_list[0] as Long @@ -142,14 +134,16 @@ data class InitializationEvent ( return InitializationEvent(duration, width, height, rotationCorrection) } } + fun toList(): List { return listOf( - duration, - width, - height, - rotationCorrection, + duration, + width, + height, + rotationCorrection, ) } + override fun equals(other: Any?): Boolean { if (other !is InitializationEvent) { return false @@ -157,7 +151,8 @@ data class InitializationEvent ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } @@ -169,21 +164,20 @@ data class InitializationEvent ( * * Generated class from Pigeon that represents data sent in messages. */ -data class PlaybackStateChangeEvent ( - val state: PlatformPlaybackState -) : PlatformVideoEvent() - { +data class PlaybackStateChangeEvent(val state: PlatformPlaybackState) : PlatformVideoEvent() { companion object { fun fromList(pigeonVar_list: List): PlaybackStateChangeEvent { val state = pigeonVar_list[0] as PlatformPlaybackState return PlaybackStateChangeEvent(state) } } + fun toList(): List { return listOf( - state, + state, ) } + override fun equals(other: Any?): Boolean { if (other !is PlaybackStateChangeEvent) { return false @@ -191,7 +185,8 @@ data class PlaybackStateChangeEvent ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } @@ -203,21 +198,20 @@ data class PlaybackStateChangeEvent ( * * Generated class from Pigeon that represents data sent in messages. */ -data class IsPlayingStateEvent ( - val isPlaying: Boolean -) : PlatformVideoEvent() - { +data class IsPlayingStateEvent(val isPlaying: Boolean) : PlatformVideoEvent() { companion object { fun fromList(pigeonVar_list: List): IsPlayingStateEvent { val isPlaying = pigeonVar_list[0] as Boolean return IsPlayingStateEvent(isPlaying) } } + fun toList(): List { return listOf( - isPlaying, + isPlaying, ) } + override fun equals(other: Any?): Boolean { if (other !is IsPlayingStateEvent) { return false @@ -225,7 +219,8 @@ data class IsPlayingStateEvent ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } @@ -233,27 +228,28 @@ data class IsPlayingStateEvent ( /** * Sent when audio tracks change. * - * This includes when the selected audio track changes after calling selectAudioTrack. - * Corresponds to ExoPlayer's onTracksChanged. + * This includes when the selected audio track changes after calling selectAudioTrack. Corresponds + * to ExoPlayer's onTracksChanged. * * Generated class from Pigeon that represents data sent in messages. */ -data class AudioTrackChangedEvent ( - /** The ID of the newly selected audio track, if any. */ - val selectedTrackId: String? = null -) : PlatformVideoEvent() - { +data class AudioTrackChangedEvent( + /** The ID of the newly selected audio track, if any. */ + val selectedTrackId: String? = null +) : PlatformVideoEvent() { companion object { fun fromList(pigeonVar_list: List): AudioTrackChangedEvent { val selectedTrackId = pigeonVar_list[0] as String? return AudioTrackChangedEvent(selectedTrackId) } } + fun toList(): List { return listOf( - selectedTrackId, + selectedTrackId, ) } + override fun equals(other: Any?): Boolean { if (other !is AudioTrackChangedEvent) { return false @@ -261,7 +257,8 @@ data class AudioTrackChangedEvent ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } @@ -269,30 +266,31 @@ data class AudioTrackChangedEvent ( /** * Sent when video tracks change. * - * This includes when the selected video track changes after calling selectVideoTrack. - * Corresponds to ExoPlayer's onTracksChanged. + * This includes when the selected video track changes after calling selectVideoTrack. Corresponds + * to ExoPlayer's onTracksChanged. * * Generated class from Pigeon that represents data sent in messages. */ -data class VideoTrackChangedEvent ( - /** - * The ID of the newly selected video track, if any. - * Will be null when auto quality selection is enabled. - */ - val selectedTrackId: String? = null -) : PlatformVideoEvent() - { +data class VideoTrackChangedEvent( + /** + * The ID of the newly selected video track, if any. Will be null when auto quality selection is + * enabled. + */ + val selectedTrackId: String? = null +) : PlatformVideoEvent() { companion object { fun fromList(pigeonVar_list: List): VideoTrackChangedEvent { val selectedTrackId = pigeonVar_list[0] as String? return VideoTrackChangedEvent(selectedTrackId) } } + fun toList(): List { return listOf( - selectedTrackId, + selectedTrackId, ) } + override fun equals(other: Any?): Boolean { if (other !is VideoTrackChangedEvent) { return false @@ -300,7 +298,8 @@ data class VideoTrackChangedEvent ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } @@ -310,21 +309,20 @@ data class VideoTrackChangedEvent ( * * Generated class from Pigeon that represents data sent in messages. */ -data class PlatformVideoViewCreationParams ( - val playerId: Long -) - { +data class PlatformVideoViewCreationParams(val playerId: Long) { companion object { fun fromList(pigeonVar_list: List): PlatformVideoViewCreationParams { val playerId = pigeonVar_list[0] as Long return PlatformVideoViewCreationParams(playerId) } } + fun toList(): List { return listOf( - playerId, + playerId, ) } + override fun equals(other: Any?): Boolean { if (other !is PlatformVideoViewCreationParams) { return false @@ -332,19 +330,19 @@ data class PlatformVideoViewCreationParams ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ -data class CreationOptions ( - val uri: String, - val formatHint: PlatformVideoFormat? = null, - val httpHeaders: Map, - val userAgent: String? = null -) - { +data class CreationOptions( + val uri: String, + val formatHint: PlatformVideoFormat? = null, + val httpHeaders: Map, + val userAgent: String? = null +) { companion object { fun fromList(pigeonVar_list: List): CreationOptions { val uri = pigeonVar_list[0] as String @@ -354,14 +352,16 @@ data class CreationOptions ( return CreationOptions(uri, formatHint, httpHeaders, userAgent) } } + fun toList(): List { return listOf( - uri, - formatHint, - httpHeaders, - userAgent, + uri, + formatHint, + httpHeaders, + userAgent, ) } + override fun equals(other: Any?): Boolean { if (other !is CreationOptions) { return false @@ -369,17 +369,14 @@ data class CreationOptions ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ -data class TexturePlayerIds ( - val playerId: Long, - val textureId: Long -) - { +data class TexturePlayerIds(val playerId: Long, val textureId: Long) { companion object { fun fromList(pigeonVar_list: List): TexturePlayerIds { val playerId = pigeonVar_list[0] as Long @@ -387,12 +384,14 @@ data class TexturePlayerIds ( return TexturePlayerIds(playerId, textureId) } } + fun toList(): List { return listOf( - playerId, - textureId, + playerId, + textureId, ) } + override fun equals(other: Any?): Boolean { if (other !is TexturePlayerIds) { return false @@ -400,19 +399,19 @@ data class TexturePlayerIds ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ -data class PlaybackState ( - /** The current playback position, in milliseconds. */ - val playPosition: Long, - /** The current buffer position, in milliseconds. */ - val bufferPosition: Long -) - { +data class PlaybackState( + /** The current playback position, in milliseconds. */ + val playPosition: Long, + /** The current buffer position, in milliseconds. */ + val bufferPosition: Long +) { companion object { fun fromList(pigeonVar_list: List): PlaybackState { val playPosition = pigeonVar_list[0] as Long @@ -420,12 +419,14 @@ data class PlaybackState ( return PlaybackState(playPosition, bufferPosition) } } + fun toList(): List { return listOf( - playPosition, - bufferPosition, + playPosition, + bufferPosition, ) } + override fun equals(other: Any?): Boolean { if (other !is PlaybackState) { return false @@ -433,7 +434,8 @@ data class PlaybackState ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } @@ -443,17 +445,16 @@ data class PlaybackState ( * * Generated class from Pigeon that represents data sent in messages. */ -data class AudioTrackMessage ( - val id: String, - val label: String, - val language: String, - val isSelected: Boolean, - val bitrate: Long? = null, - val sampleRate: Long? = null, - val channelCount: Long? = null, - val codec: String? = null -) - { +data class AudioTrackMessage( + val id: String, + val label: String, + val language: String, + val isSelected: Boolean, + val bitrate: Long? = null, + val sampleRate: Long? = null, + val channelCount: Long? = null, + val codec: String? = null +) { companion object { fun fromList(pigeonVar_list: List): AudioTrackMessage { val id = pigeonVar_list[0] as String @@ -464,21 +465,24 @@ data class AudioTrackMessage ( val sampleRate = pigeonVar_list[5] as Long? val channelCount = pigeonVar_list[6] as Long? val codec = pigeonVar_list[7] as String? - return AudioTrackMessage(id, label, language, isSelected, bitrate, sampleRate, channelCount, codec) + return AudioTrackMessage( + id, label, language, isSelected, bitrate, sampleRate, channelCount, codec) } } + fun toList(): List { return listOf( - id, - label, - language, - isSelected, - bitrate, - sampleRate, - channelCount, - codec, + id, + label, + language, + isSelected, + bitrate, + sampleRate, + channelCount, + codec, ) } + override fun equals(other: Any?): Boolean { if (other !is AudioTrackMessage) { return false @@ -486,7 +490,8 @@ data class AudioTrackMessage ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } @@ -496,18 +501,17 @@ data class AudioTrackMessage ( * * Generated class from Pigeon that represents data sent in messages. */ -data class ExoPlayerAudioTrackData ( - val groupIndex: Long, - val trackIndex: Long, - val label: String? = null, - val language: String? = null, - val isSelected: Boolean, - val bitrate: Long? = null, - val sampleRate: Long? = null, - val channelCount: Long? = null, - val codec: String? = null -) - { +data class ExoPlayerAudioTrackData( + val groupIndex: Long, + val trackIndex: Long, + val label: String? = null, + val language: String? = null, + val isSelected: Boolean, + val bitrate: Long? = null, + val sampleRate: Long? = null, + val channelCount: Long? = null, + val codec: String? = null +) { companion object { fun fromList(pigeonVar_list: List): ExoPlayerAudioTrackData { val groupIndex = pigeonVar_list[0] as Long @@ -519,22 +523,33 @@ data class ExoPlayerAudioTrackData ( val sampleRate = pigeonVar_list[6] as Long? val channelCount = pigeonVar_list[7] as Long? val codec = pigeonVar_list[8] as String? - return ExoPlayerAudioTrackData(groupIndex, trackIndex, label, language, isSelected, bitrate, sampleRate, channelCount, codec) + return ExoPlayerAudioTrackData( + groupIndex, + trackIndex, + label, + language, + isSelected, + bitrate, + sampleRate, + channelCount, + codec) } } + fun toList(): List { return listOf( - groupIndex, - trackIndex, - label, - language, - isSelected, - bitrate, - sampleRate, - channelCount, - codec, + groupIndex, + trackIndex, + label, + language, + isSelected, + bitrate, + sampleRate, + channelCount, + codec, ) } + override fun equals(other: Any?): Boolean { if (other !is ExoPlayerAudioTrackData) { return false @@ -542,7 +557,8 @@ data class ExoPlayerAudioTrackData ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } @@ -552,22 +568,23 @@ data class ExoPlayerAudioTrackData ( * * Generated class from Pigeon that represents data sent in messages. */ -data class NativeAudioTrackData ( - /** ExoPlayer-based tracks */ - val exoPlayerTracks: List? = null -) - { +data class NativeAudioTrackData( + /** ExoPlayer-based tracks */ + val exoPlayerTracks: List? = null +) { companion object { fun fromList(pigeonVar_list: List): NativeAudioTrackData { val exoPlayerTracks = pigeonVar_list[0] as List? return NativeAudioTrackData(exoPlayerTracks) } } + fun toList(): List { return listOf( - exoPlayerTracks, + exoPlayerTracks, ) } + override fun equals(other: Any?): Boolean { if (other !is NativeAudioTrackData) { return false @@ -575,7 +592,8 @@ data class NativeAudioTrackData ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } @@ -585,18 +603,17 @@ data class NativeAudioTrackData ( * * Generated class from Pigeon that represents data sent in messages. */ -data class ExoPlayerVideoTrackData ( - val groupIndex: Long, - val trackIndex: Long, - val label: String? = null, - val isSelected: Boolean, - val bitrate: Long? = null, - val width: Long? = null, - val height: Long? = null, - val frameRate: Double? = null, - val codec: String? = null -) - { +data class ExoPlayerVideoTrackData( + val groupIndex: Long, + val trackIndex: Long, + val label: String? = null, + val isSelected: Boolean, + val bitrate: Long? = null, + val width: Long? = null, + val height: Long? = null, + val frameRate: Double? = null, + val codec: String? = null +) { companion object { fun fromList(pigeonVar_list: List): ExoPlayerVideoTrackData { val groupIndex = pigeonVar_list[0] as Long @@ -608,22 +625,25 @@ data class ExoPlayerVideoTrackData ( val height = pigeonVar_list[6] as Long? val frameRate = pigeonVar_list[7] as Double? val codec = pigeonVar_list[8] as String? - return ExoPlayerVideoTrackData(groupIndex, trackIndex, label, isSelected, bitrate, width, height, frameRate, codec) + return ExoPlayerVideoTrackData( + groupIndex, trackIndex, label, isSelected, bitrate, width, height, frameRate, codec) } } + fun toList(): List { return listOf( - groupIndex, - trackIndex, - label, - isSelected, - bitrate, - width, - height, - frameRate, - codec, + groupIndex, + trackIndex, + label, + isSelected, + bitrate, + width, + height, + frameRate, + codec, ) } + override fun equals(other: Any?): Boolean { if (other !is ExoPlayerVideoTrackData) { return false @@ -631,7 +651,8 @@ data class ExoPlayerVideoTrackData ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } @@ -641,22 +662,23 @@ data class ExoPlayerVideoTrackData ( * * Generated class from Pigeon that represents data sent in messages. */ -data class NativeVideoTrackData ( - /** ExoPlayer-based tracks */ - val exoPlayerTracks: List? = null -) - { +data class NativeVideoTrackData( + /** ExoPlayer-based tracks */ + val exoPlayerTracks: List? = null +) { companion object { fun fromList(pigeonVar_list: List): NativeVideoTrackData { val exoPlayerTracks = pigeonVar_list[0] as List? return NativeVideoTrackData(exoPlayerTracks) } } + fun toList(): List { return listOf( - exoPlayerTracks, + exoPlayerTracks, ) } + override fun equals(other: Any?): Boolean { if (other !is NativeVideoTrackData) { return false @@ -664,47 +686,35 @@ data class NativeVideoTrackData ( if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } override fun hashCode(): Int = toList().hashCode() } + private open class MessagesPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { 129.toByte() -> { - return (readValue(buffer) as Long?)?.let { - PlatformVideoFormat.ofRaw(it.toInt()) - } + return (readValue(buffer) as Long?)?.let { PlatformVideoFormat.ofRaw(it.toInt()) } } 130.toByte() -> { - return (readValue(buffer) as Long?)?.let { - PlatformPlaybackState.ofRaw(it.toInt()) - } + return (readValue(buffer) as Long?)?.let { PlatformPlaybackState.ofRaw(it.toInt()) } } 131.toByte() -> { - return (readValue(buffer) as? List)?.let { - InitializationEvent.fromList(it) - } + return (readValue(buffer) as? List)?.let { InitializationEvent.fromList(it) } } 132.toByte() -> { - return (readValue(buffer) as? List)?.let { - PlaybackStateChangeEvent.fromList(it) - } + return (readValue(buffer) as? List)?.let { PlaybackStateChangeEvent.fromList(it) } } 133.toByte() -> { - return (readValue(buffer) as? List)?.let { - IsPlayingStateEvent.fromList(it) - } + return (readValue(buffer) as? List)?.let { IsPlayingStateEvent.fromList(it) } } 134.toByte() -> { - return (readValue(buffer) as? List)?.let { - AudioTrackChangedEvent.fromList(it) - } + return (readValue(buffer) as? List)?.let { AudioTrackChangedEvent.fromList(it) } } 135.toByte() -> { - return (readValue(buffer) as? List)?.let { - VideoTrackChangedEvent.fromList(it) - } + return (readValue(buffer) as? List)?.let { VideoTrackChangedEvent.fromList(it) } } 136.toByte() -> { return (readValue(buffer) as? List)?.let { @@ -712,49 +722,34 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { } } 137.toByte() -> { - return (readValue(buffer) as? List)?.let { - CreationOptions.fromList(it) - } + return (readValue(buffer) as? List)?.let { CreationOptions.fromList(it) } } 138.toByte() -> { - return (readValue(buffer) as? List)?.let { - TexturePlayerIds.fromList(it) - } + return (readValue(buffer) as? List)?.let { TexturePlayerIds.fromList(it) } } 139.toByte() -> { - return (readValue(buffer) as? List)?.let { - PlaybackState.fromList(it) - } + return (readValue(buffer) as? List)?.let { PlaybackState.fromList(it) } } 140.toByte() -> { - return (readValue(buffer) as? List)?.let { - AudioTrackMessage.fromList(it) - } + return (readValue(buffer) as? List)?.let { AudioTrackMessage.fromList(it) } } 141.toByte() -> { - return (readValue(buffer) as? List)?.let { - ExoPlayerAudioTrackData.fromList(it) - } + return (readValue(buffer) as? List)?.let { ExoPlayerAudioTrackData.fromList(it) } } 142.toByte() -> { - return (readValue(buffer) as? List)?.let { - NativeAudioTrackData.fromList(it) - } + return (readValue(buffer) as? List)?.let { NativeAudioTrackData.fromList(it) } } 143.toByte() -> { - return (readValue(buffer) as? List)?.let { - ExoPlayerVideoTrackData.fromList(it) - } + return (readValue(buffer) as? List)?.let { ExoPlayerVideoTrackData.fromList(it) } } 144.toByte() -> { - return (readValue(buffer) as? List)?.let { - NativeVideoTrackData.fromList(it) - } + return (readValue(buffer) as? List)?.let { NativeVideoTrackData.fromList(it) } } else -> super.readValueOfType(type, buffer) } } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { when (value) { is PlatformVideoFormat -> { stream.write(129) @@ -830,31 +825,47 @@ val MessagesPigeonMethodCodec = StandardMethodCodec(MessagesPigeonCodec()) /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface AndroidVideoPlayerApi { fun initialize() + fun createForPlatformView(options: CreationOptions): Long + fun createForTextureView(options: CreationOptions): TexturePlayerIds + fun dispose(playerId: Long) + fun setMixWithOthers(mixWithOthers: Boolean) + fun getLookupKeyForAsset(asset: String, packageName: String?): String companion object { /** The codec used by AndroidVideoPlayerApi. */ - val codec: MessageCodec by lazy { - MessagesPigeonCodec() - } - /** Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the `binaryMessenger`. */ + val codec: MessageCodec by lazy { MessagesPigeonCodec() } + /** + * Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the + * `binaryMessenger`. + */ @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: AndroidVideoPlayerApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + fun setUp( + binaryMessenger: BinaryMessenger, + api: AndroidVideoPlayerApi?, + messageChannelSuffix: String = "" + ) { + val separatedMessageChannelSuffix = + if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.initialize() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + api.initialize() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -862,16 +873,21 @@ interface AndroidVideoPlayerApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val optionsArg = args[0] as CreationOptions - val wrapped: List = try { - listOf(api.createForPlatformView(optionsArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + listOf(api.createForPlatformView(optionsArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -879,16 +895,21 @@ interface AndroidVideoPlayerApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val optionsArg = args[0] as CreationOptions - val wrapped: List = try { - listOf(api.createForTextureView(optionsArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + listOf(api.createForTextureView(optionsArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -896,17 +917,22 @@ interface AndroidVideoPlayerApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val playerIdArg = args[0] as Long - val wrapped: List = try { - api.dispose(playerIdArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + api.dispose(playerIdArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -914,17 +940,22 @@ interface AndroidVideoPlayerApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val mixWithOthersArg = args[0] as Boolean - val wrapped: List = try { - api.setMixWithOthers(mixWithOthersArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + api.setMixWithOthers(mixWithOthersArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -932,17 +963,22 @@ interface AndroidVideoPlayerApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val assetArg = args[0] as String val packageNameArg = args[1] as String? - val wrapped: List = try { - listOf(api.getLookupKeyForAsset(assetArg, packageNameArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + listOf(api.getLookupKeyForAsset(assetArg, packageNameArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -977,32 +1013,43 @@ interface VideoPlayerInstanceApi { /** Gets the available video tracks for the video. */ fun getVideoTracks(): NativeVideoTrackData /** - * Selects which video track is chosen for playback from its [groupIndex] and [trackIndex]. - * Pass -1 for both indices to enable auto quality selection. + * Selects which video track is chosen for playback from its [groupIndex] and [trackIndex]. Pass + * -1 for both indices to enable auto quality selection. */ fun selectVideoTrack(groupIndex: Long, trackIndex: Long) companion object { /** The codec used by VideoPlayerInstanceApi. */ - val codec: MessageCodec by lazy { - MessagesPigeonCodec() - } - /** Sets up an instance of `VideoPlayerInstanceApi` to handle messages through the `binaryMessenger`. */ + val codec: MessageCodec by lazy { MessagesPigeonCodec() } + /** + * Sets up an instance of `VideoPlayerInstanceApi` to handle messages through the + * `binaryMessenger`. + */ @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: VideoPlayerInstanceApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + fun setUp( + binaryMessenger: BinaryMessenger, + api: VideoPlayerInstanceApi?, + messageChannelSuffix: String = "" + ) { + val separatedMessageChannelSuffix = + if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val loopingArg = args[0] as Boolean - val wrapped: List = try { - api.setLooping(loopingArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + api.setLooping(loopingArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1010,17 +1057,22 @@ interface VideoPlayerInstanceApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val volumeArg = args[0] as Double - val wrapped: List = try { - api.setVolume(volumeArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + api.setVolume(volumeArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1028,17 +1080,22 @@ interface VideoPlayerInstanceApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val speedArg = args[0] as Double - val wrapped: List = try { - api.setPlaybackSpeed(speedArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + api.setPlaybackSpeed(speedArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1046,15 +1103,20 @@ interface VideoPlayerInstanceApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.play() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + api.play() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1062,15 +1124,20 @@ interface VideoPlayerInstanceApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.pause() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + api.pause() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1078,17 +1145,22 @@ interface VideoPlayerInstanceApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val positionArg = args[0] as Long - val wrapped: List = try { - api.seekTo(positionArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + api.seekTo(positionArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1096,14 +1168,19 @@ interface VideoPlayerInstanceApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getCurrentPosition()) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + listOf(api.getCurrentPosition()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1111,14 +1188,19 @@ interface VideoPlayerInstanceApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getBufferedPosition()) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + listOf(api.getBufferedPosition()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1126,14 +1208,19 @@ interface VideoPlayerInstanceApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getAudioTracks()) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + listOf(api.getAudioTracks()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1141,18 +1228,23 @@ interface VideoPlayerInstanceApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val groupIndexArg = args[0] as Long val trackIndexArg = args[1] as Long - val wrapped: List = try { - api.selectAudioTrack(groupIndexArg, trackIndexArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + api.selectAudioTrack(groupIndexArg, trackIndexArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1160,14 +1252,19 @@ interface VideoPlayerInstanceApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getVideoTracks$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getVideoTracks$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getVideoTracks()) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + listOf(api.getVideoTracks()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1175,18 +1272,23 @@ interface VideoPlayerInstanceApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectVideoTrack$separatedMessageChannelSuffix", codec) + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectVideoTrack$separatedMessageChannelSuffix", + codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val groupIndexArg = args[0] as Long val trackIndexArg = args[1] as Long - val wrapped: List = try { - api.selectVideoTrack(groupIndexArg, trackIndexArg) - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } + val wrapped: List = + try { + api.selectVideoTrack(groupIndexArg, trackIndexArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } reply.reply(wrapped) } } else { @@ -1197,9 +1299,8 @@ interface VideoPlayerInstanceApi { } } -private class MessagesPigeonStreamHandler( - val wrapper: MessagesPigeonEventChannelWrapper -) : EventChannel.StreamHandler { +private class MessagesPigeonStreamHandler(val wrapper: MessagesPigeonEventChannelWrapper) : + EventChannel.StreamHandler { var pigeonSink: PigeonEventSink? = null override fun onListen(p0: Any?, sink: EventChannel.EventSink) { @@ -1232,21 +1333,26 @@ class PigeonEventSink(private val sink: EventChannel.EventSink) { sink.endOfStream() } } - + abstract class VideoEventsStreamHandler : MessagesPigeonEventChannelWrapper { companion object { - fun register(messenger: BinaryMessenger, streamHandler: VideoEventsStreamHandler, instanceName: String = "") { - var channelName: String = "dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents" + fun register( + messenger: BinaryMessenger, + streamHandler: VideoEventsStreamHandler, + instanceName: String = "" + ) { + var channelName: String = + "dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents" if (instanceName.isNotEmpty()) { channelName += ".$instanceName" } val internalStreamHandler = MessagesPigeonStreamHandler(streamHandler) - EventChannel(messenger, channelName, MessagesPigeonMethodCodec).setStreamHandler(internalStreamHandler) + EventChannel(messenger, channelName, MessagesPigeonMethodCodec) + .setStreamHandler(internalStreamHandler) } } -// Implement methods from MessagesPigeonEventChannelWrapper -override fun onListen(p0: Any?, sink: PigeonEventSink) {} + // Implement methods from MessagesPigeonEventChannelWrapper + override fun onListen(p0: Any?, sink: PigeonEventSink) {} -override fun onCancel(p0: Any?) {} + override fun onCancel(p0: Any?) {} } - diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index 22ac7fbe81d..d8331267a1a 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -17,7 +17,9 @@ VideoPlayerInstanceApi _productionApiProvider(int playerId) { } /// The non-test implementation of `_videoEventStreamProvider`. -Stream _productionVideoEventStreamProvider(String streamIdentifier) { +Stream _productionVideoEventStreamProvider( + String streamIdentifier, +) { return pigeon.videoEvents(instanceName: streamIdentifier); } @@ -27,7 +29,8 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { /// Creates a new Android video player implementation instance. AndroidVideoPlayer({ @visibleForTesting AndroidVideoPlayerApi? pluginApi, - @visibleForTesting VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, + @visibleForTesting + VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, Stream Function(String streamIdentifier)? videoEventStreamProvider, }) : _api = pluginApi ?? AndroidVideoPlayerApi(), @@ -87,9 +90,14 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { case DataSourceType.asset: final String? asset = dataSource.asset; if (asset == null) { - throw ArgumentError('"asset" must be non-null for an asset data source'); + throw ArgumentError( + '"asset" must be non-null for an asset data source', + ); } - final String key = await _api.getLookupKeyForAsset(asset, dataSource.package); + final String key = await _api.getLookupKeyForAsset( + asset, + dataSource.package, + ); uri = 'asset:///$key'; case DataSourceType.network: uri = dataSource.uri; @@ -205,7 +213,9 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { final VideoPlayerViewState viewState = _playerWith(id: playerId).viewState; return switch (viewState) { - VideoPlayerTextureViewState(:final int textureId) => Texture(textureId: textureId), + VideoPlayerTextureViewState(:final int textureId) => Texture( + textureId: textureId, + ), VideoPlayerPlatformViewState() => PlatformViewPlayer(playerId: playerId), }; } @@ -271,7 +281,9 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { // Generate label from resolution if not provided final String? label = track.label ?? - (track.width != null && track.height != null ? '${track.height}p' : null); + (track.width != null && track.height != null + ? '${track.height}p' + : null); tracks.add( VideoTrack( id: trackId, @@ -306,7 +318,9 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { return player ?? (throw StateError('No active player with ID $id.')); } - PlatformVideoFormat? _platformVideoFormatFromVideoFormat(VideoFormat? format) { + PlatformVideoFormat? _platformVideoFormatFromVideoFormat( + VideoFormat? format, + ) { return switch (format) { VideoFormat.dash => PlatformVideoFormat.dash, VideoFormat.hls => PlatformVideoFormat.hls, @@ -560,7 +574,9 @@ class _PlayerInstance { // should be synchronous with the state change. break; case PlatformPlaybackState.ended: - _eventStreamController.add(VideoEvent(eventType: VideoEventType.completed)); + _eventStreamController.add( + VideoEvent(eventType: VideoEventType.completed), + ); case PlatformPlaybackState.unknown: // Ignore unknown states. This isn't an error since the media // framework could add new states in the future. diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index 97571a2a499..765791594b8 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -17,40 +17,33 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } + bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && - a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + a.indexed.every( + ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), + ); } if (a is Map && b is Map) { - return a.length == b.length && a.entries.every((MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key])); + return a.length == b.length && + a.entries.every( + (MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key]), + ); } return a == b; } - /// Pigeon equivalent of video_platform_interface's VideoFormat. -enum PlatformVideoFormat { - dash, - hls, - ss, -} +enum PlatformVideoFormat { dash, hls, ss } /// Pigeon equivalent of Player's playback state. /// https://developer.android.com/media/media3/exoplayer/listening-to-player-events#playback-state -enum PlatformPlaybackState { - idle, - buffering, - ready, - ended, - unknown, -} +enum PlatformPlaybackState { idle, buffering, ready, ended, unknown } -sealed class PlatformVideoEvent { -} +sealed class PlatformVideoEvent {} /// Sent when the video is initialized and ready to play. class InitializationEvent extends PlatformVideoEvent { @@ -74,16 +67,12 @@ class InitializationEvent extends PlatformVideoEvent { int rotationCorrection; List _toList() { - return [ - duration, - width, - height, - rotationCorrection, - ]; + return [duration, width, height, rotationCorrection]; } Object encode() { - return _toList(); } + return _toList(); + } static InitializationEvent decode(Object result) { result as List; @@ -109,40 +98,35 @@ class InitializationEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Sent when the video state changes. /// /// Corresponds to ExoPlayer's onPlaybackStateChanged. class PlaybackStateChangeEvent extends PlatformVideoEvent { - PlaybackStateChangeEvent({ - required this.state, - }); + PlaybackStateChangeEvent({required this.state}); PlatformPlaybackState state; List _toList() { - return [ - state, - ]; + return [state]; } Object encode() { - return _toList(); } + return _toList(); + } static PlaybackStateChangeEvent decode(Object result) { result as List; - return PlaybackStateChangeEvent( - state: result[0]! as PlatformPlaybackState, - ); + return PlaybackStateChangeEvent(state: result[0]! as PlatformPlaybackState); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlaybackStateChangeEvent || other.runtimeType != runtimeType) { + if (other is! PlaybackStateChangeEvent || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -153,34 +137,28 @@ class PlaybackStateChangeEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Sent when the video starts or stops playing. /// /// Corresponds to ExoPlayer's onIsPlayingChanged. class IsPlayingStateEvent extends PlatformVideoEvent { - IsPlayingStateEvent({ - required this.isPlaying, - }); + IsPlayingStateEvent({required this.isPlaying}); bool isPlaying; List _toList() { - return [ - isPlaying, - ]; + return [isPlaying]; } Object encode() { - return _toList(); } + return _toList(); + } static IsPlayingStateEvent decode(Object result) { result as List; - return IsPlayingStateEvent( - isPlaying: result[0]! as bool, - ); + return IsPlayingStateEvent(isPlaying: result[0]! as bool); } @override @@ -197,8 +175,7 @@ class IsPlayingStateEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Sent when audio tracks change. @@ -206,27 +183,22 @@ class IsPlayingStateEvent extends PlatformVideoEvent { /// This includes when the selected audio track changes after calling selectAudioTrack. /// Corresponds to ExoPlayer's onTracksChanged. class AudioTrackChangedEvent extends PlatformVideoEvent { - AudioTrackChangedEvent({ - this.selectedTrackId, - }); + AudioTrackChangedEvent({this.selectedTrackId}); /// The ID of the newly selected audio track, if any. String? selectedTrackId; List _toList() { - return [ - selectedTrackId, - ]; + return [selectedTrackId]; } Object encode() { - return _toList(); } + return _toList(); + } static AudioTrackChangedEvent decode(Object result) { result as List; - return AudioTrackChangedEvent( - selectedTrackId: result[0] as String?, - ); + return AudioTrackChangedEvent(selectedTrackId: result[0] as String?); } @override @@ -243,8 +215,7 @@ class AudioTrackChangedEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Sent when video tracks change. @@ -252,28 +223,23 @@ class AudioTrackChangedEvent extends PlatformVideoEvent { /// This includes when the selected video track changes after calling selectVideoTrack. /// Corresponds to ExoPlayer's onTracksChanged. class VideoTrackChangedEvent extends PlatformVideoEvent { - VideoTrackChangedEvent({ - this.selectedTrackId, - }); + VideoTrackChangedEvent({this.selectedTrackId}); /// The ID of the newly selected video track, if any. /// Will be null when auto quality selection is enabled. String? selectedTrackId; List _toList() { - return [ - selectedTrackId, - ]; + return [selectedTrackId]; } Object encode() { - return _toList(); } + return _toList(); + } static VideoTrackChangedEvent decode(Object result) { result as List; - return VideoTrackChangedEvent( - selectedTrackId: result[0] as String?, - ); + return VideoTrackChangedEvent(selectedTrackId: result[0] as String?); } @override @@ -290,38 +256,33 @@ class VideoTrackChangedEvent extends PlatformVideoEvent { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { - PlatformVideoViewCreationParams({ - required this.playerId, - }); + PlatformVideoViewCreationParams({required this.playerId}); int playerId; List _toList() { - return [ - playerId, - ]; + return [playerId]; } Object encode() { - return _toList(); } + return _toList(); + } static PlatformVideoViewCreationParams decode(Object result) { result as List; - return PlatformVideoViewCreationParams( - playerId: result[0]! as int, - ); + return PlatformVideoViewCreationParams(playerId: result[0]! as int); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlatformVideoViewCreationParams || other.runtimeType != runtimeType) { + if (other is! PlatformVideoViewCreationParams || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -332,8 +293,7 @@ class PlatformVideoViewCreationParams { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } class CreationOptions { @@ -353,23 +313,20 @@ class CreationOptions { String? userAgent; List _toList() { - return [ - uri, - formatHint, - httpHeaders, - userAgent, - ]; + return [uri, formatHint, httpHeaders, userAgent]; } Object encode() { - return _toList(); } + return _toList(); + } static CreationOptions decode(Object result) { result as List; return CreationOptions( uri: result[0]! as String, formatHint: result[1] as PlatformVideoFormat?, - httpHeaders: (result[2] as Map?)!.cast(), + httpHeaders: (result[2] as Map?)! + .cast(), userAgent: result[3] as String?, ); } @@ -388,29 +345,23 @@ class CreationOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } class TexturePlayerIds { - TexturePlayerIds({ - required this.playerId, - required this.textureId, - }); + TexturePlayerIds({required this.playerId, required this.textureId}); int playerId; int textureId; List _toList() { - return [ - playerId, - textureId, - ]; + return [playerId, textureId]; } Object encode() { - return _toList(); } + return _toList(); + } static TexturePlayerIds decode(Object result) { result as List; @@ -434,15 +385,11 @@ class TexturePlayerIds { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } class PlaybackState { - PlaybackState({ - required this.playPosition, - required this.bufferPosition, - }); + PlaybackState({required this.playPosition, required this.bufferPosition}); /// The current playback position, in milliseconds. int playPosition; @@ -451,14 +398,12 @@ class PlaybackState { int bufferPosition; List _toList() { - return [ - playPosition, - bufferPosition, - ]; + return [playPosition, bufferPosition]; } Object encode() { - return _toList(); } + return _toList(); + } static PlaybackState decode(Object result) { result as List; @@ -482,8 +427,7 @@ class PlaybackState { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Represents an audio track in a video. @@ -529,7 +473,8 @@ class AudioTrackMessage { } Object encode() { - return _toList(); } + return _toList(); + } static AudioTrackMessage decode(Object result) { result as List; @@ -559,8 +504,7 @@ class AudioTrackMessage { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Raw audio track data from ExoPlayer Format objects. @@ -610,7 +554,8 @@ class ExoPlayerAudioTrackData { } Object encode() { - return _toList(); } + return _toList(); + } static ExoPlayerAudioTrackData decode(Object result) { result as List; @@ -641,32 +586,29 @@ class ExoPlayerAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Container for raw audio track data from Android ExoPlayer. class NativeAudioTrackData { - NativeAudioTrackData({ - this.exoPlayerTracks, - }); + NativeAudioTrackData({this.exoPlayerTracks}); /// ExoPlayer-based tracks List? exoPlayerTracks; List _toList() { - return [ - exoPlayerTracks, - ]; + return [exoPlayerTracks]; } Object encode() { - return _toList(); } + return _toList(); + } static NativeAudioTrackData decode(Object result) { result as List; return NativeAudioTrackData( - exoPlayerTracks: (result[0] as List?)?.cast(), + exoPlayerTracks: (result[0] as List?) + ?.cast(), ); } @@ -684,8 +626,7 @@ class NativeAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Raw video track data from ExoPlayer Format objects. @@ -735,7 +676,8 @@ class ExoPlayerVideoTrackData { } Object encode() { - return _toList(); } + return _toList(); + } static ExoPlayerVideoTrackData decode(Object result) { result as List; @@ -766,32 +708,29 @@ class ExoPlayerVideoTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Container for raw video track data from Android ExoPlayer. class NativeVideoTrackData { - NativeVideoTrackData({ - this.exoPlayerTracks, - }); + NativeVideoTrackData({this.exoPlayerTracks}); /// ExoPlayer-based tracks List? exoPlayerTracks; List _toList() { - return [ - exoPlayerTracks, - ]; + return [exoPlayerTracks]; } Object encode() { - return _toList(); } + return _toList(); + } static NativeVideoTrackData decode(Object result) { result as List; return NativeVideoTrackData( - exoPlayerTracks: (result[0] as List?)?.cast(), + exoPlayerTracks: (result[0] as List?) + ?.cast(), ); } @@ -809,11 +748,9 @@ class NativeVideoTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -821,52 +758,52 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PlatformVideoFormat) { + } else if (value is PlatformVideoFormat) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is PlatformPlaybackState) { + } else if (value is PlatformPlaybackState) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is InitializationEvent) { + } else if (value is InitializationEvent) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is PlaybackStateChangeEvent) { + } else if (value is PlaybackStateChangeEvent) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is IsPlayingStateEvent) { + } else if (value is IsPlayingStateEvent) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is AudioTrackChangedEvent) { + } else if (value is AudioTrackChangedEvent) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is VideoTrackChangedEvent) { + } else if (value is VideoTrackChangedEvent) { buffer.putUint8(135); writeValue(buffer, value.encode()); - } else if (value is PlatformVideoViewCreationParams) { + } else if (value is PlatformVideoViewCreationParams) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is CreationOptions) { + } else if (value is CreationOptions) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is TexturePlayerIds) { + } else if (value is TexturePlayerIds) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is PlaybackState) { + } else if (value is PlaybackState) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is AudioTrackMessage) { + } else if (value is AudioTrackMessage) { buffer.putUint8(140); writeValue(buffer, value.encode()); - } else if (value is ExoPlayerAudioTrackData) { + } else if (value is ExoPlayerAudioTrackData) { buffer.putUint8(141); writeValue(buffer, value.encode()); - } else if (value is NativeAudioTrackData) { + } else if (value is NativeAudioTrackData) { buffer.putUint8(142); writeValue(buffer, value.encode()); - } else if (value is ExoPlayerVideoTrackData) { + } else if (value is ExoPlayerVideoTrackData) { buffer.putUint8(143); writeValue(buffer, value.encode()); - } else if (value is NativeVideoTrackData) { + } else if (value is NativeVideoTrackData) { buffer.putUint8(144); writeValue(buffer, value.encode()); } else { @@ -877,39 +814,39 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: final value = readValue(buffer) as int?; return value == null ? null : PlatformVideoFormat.values[value]; - case 130: + case 130: final value = readValue(buffer) as int?; return value == null ? null : PlatformPlaybackState.values[value]; - case 131: + case 131: return InitializationEvent.decode(readValue(buffer)!); - case 132: + case 132: return PlaybackStateChangeEvent.decode(readValue(buffer)!); - case 133: + case 133: return IsPlayingStateEvent.decode(readValue(buffer)!); - case 134: + case 134: return AudioTrackChangedEvent.decode(readValue(buffer)!); - case 135: + case 135: return VideoTrackChangedEvent.decode(readValue(buffer)!); - case 136: + case 136: return PlatformVideoViewCreationParams.decode(readValue(buffer)!); - case 137: + case 137: return CreationOptions.decode(readValue(buffer)!); - case 138: + case 138: return TexturePlayerIds.decode(readValue(buffer)!); - case 139: + case 139: return PlaybackState.decode(readValue(buffer)!); - case 140: + case 140: return AudioTrackMessage.decode(readValue(buffer)!); - case 141: + case 141: return ExoPlayerAudioTrackData.decode(readValue(buffer)!); - case 142: + case 142: return NativeAudioTrackData.decode(readValue(buffer)!); - case 143: + case 143: return ExoPlayerVideoTrackData.decode(readValue(buffer)!); - case 144: + case 144: return NativeVideoTrackData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -917,15 +854,21 @@ class _PigeonCodec extends StandardMessageCodec { } } -const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec(_PigeonCodec()); +const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec( + _PigeonCodec(), +); class AndroidVideoPlayerApi { /// Constructor for [AndroidVideoPlayerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AndroidVideoPlayerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + AndroidVideoPlayerApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -933,7 +876,8 @@ class AndroidVideoPlayerApi { final String pigeonVar_messageChannelSuffix; Future initialize() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -955,13 +899,16 @@ class AndroidVideoPlayerApi { } Future createForPlatformView(CreationOptions options) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [options], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -982,13 +929,16 @@ class AndroidVideoPlayerApi { } Future createForTextureView(CreationOptions options) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [options], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1009,13 +959,16 @@ class AndroidVideoPlayerApi { } Future dispose(int playerId) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [playerId], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1031,13 +984,16 @@ class AndroidVideoPlayerApi { } Future setMixWithOthers(bool mixWithOthers) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [mixWithOthers], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1053,13 +1009,16 @@ class AndroidVideoPlayerApi { } Future getLookupKeyForAsset(String asset, String? packageName) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([asset, packageName]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [asset, packageName], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1084,9 +1043,13 @@ class VideoPlayerInstanceApi { /// Constructor for [VideoPlayerInstanceApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - VideoPlayerInstanceApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + VideoPlayerInstanceApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -1095,13 +1058,16 @@ class VideoPlayerInstanceApi { /// Sets whether to automatically loop playback of the video. Future setLooping(bool looping) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([looping]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [looping], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1118,13 +1084,16 @@ class VideoPlayerInstanceApi { /// Sets the volume, with 0.0 being muted and 1.0 being full volume. Future setVolume(double volume) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [volume], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1141,13 +1110,16 @@ class VideoPlayerInstanceApi { /// Sets the playback speed as a multiple of normal speed. Future setPlaybackSpeed(double speed) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([speed]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [speed], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1164,7 +1136,8 @@ class VideoPlayerInstanceApi { /// Begins playback if the video is not currently playing. Future play() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1187,7 +1160,8 @@ class VideoPlayerInstanceApi { /// Pauses playback if the video is currently playing. Future pause() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1210,13 +1184,16 @@ class VideoPlayerInstanceApi { /// Seeks to the given playback position, in milliseconds. Future seekTo(int position) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([position]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [position], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1233,7 +1210,8 @@ class VideoPlayerInstanceApi { /// Returns the current playback position, in milliseconds. Future getCurrentPosition() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1261,7 +1239,8 @@ class VideoPlayerInstanceApi { /// Returns the current buffer position, in milliseconds. Future getBufferedPosition() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1289,7 +1268,8 @@ class VideoPlayerInstanceApi { /// Gets the available audio tracks for the video. Future getAudioTracks() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1317,13 +1297,16 @@ class VideoPlayerInstanceApi { /// Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] Future selectAudioTrack(int groupIndex, int trackIndex) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([groupIndex, trackIndex]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [groupIndex, trackIndex], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1340,7 +1323,8 @@ class VideoPlayerInstanceApi { /// Gets the available video tracks for the video. Future getVideoTracks() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getVideoTracks$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getVideoTracks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -1369,13 +1353,16 @@ class VideoPlayerInstanceApi { /// Selects which video track is chosen for playback from its [groupIndex] and [trackIndex]. /// Pass -1 for both indices to enable auto quality selection. Future selectVideoTrack(int groupIndex, int trackIndex) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectVideoTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.selectVideoTrack$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([groupIndex, trackIndex]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [groupIndex, trackIndex], + ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -1391,14 +1378,15 @@ class VideoPlayerInstanceApi { } } -Stream videoEvents( {String instanceName = ''}) { +Stream videoEvents({String instanceName = ''}) { if (instanceName.isNotEmpty) { instanceName = '.$instanceName'; } - final EventChannel videoEventsChannel = - EventChannel('dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents$instanceName', pigeonMethodCodec); + final EventChannel videoEventsChannel = EventChannel( + 'dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents$instanceName', + pigeonMethodCodec, + ); return videoEventsChannel.receiveBroadcastStream().map((dynamic event) { return event as PlatformVideoEvent; }); } - diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index a9904dd6253..07323dbc114 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -7,7 +7,8 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( dartOut: 'lib/src/messages.g.dart', - kotlinOut: 'android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt', + kotlinOut: + 'android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt', kotlinOptions: KotlinOptions(package: 'io.flutter.plugins.videoplayer'), copyrightHeader: 'pigeons/copyright.txt', ), diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart index a42679bc0cd..fad08644da1 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -72,7 +72,8 @@ void main() { final player = AndroidVideoPlayer( pluginApi: pluginApi, playerApiProvider: (_) => instanceApi, - videoEventStreamProvider: (_) => streamController.stream.asBroadcastStream(), + videoEventStreamProvider: (_) => + streamController.stream.asBroadcastStream(), ); player.ensurePlayerInitialized( playerId, @@ -109,17 +110,23 @@ void main() { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 100); const newPlayerId = 2; - when( - api.createForTextureView(any), - ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100)); + when(api.createForTextureView(any)).thenAnswer( + (_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100), + ); const asset = 'someAsset'; const package = 'somePackage'; const assetKey = 'resultingAssetKey'; - when(api.getLookupKeyForAsset(asset, package)).thenAnswer((_) async => assetKey); + when( + api.getLookupKeyForAsset(asset, package), + ).thenAnswer((_) async => assetKey); final int? playerId = await player.create( - DataSource(sourceType: DataSourceType.asset, asset: asset, package: package), + DataSource( + sourceType: DataSourceType.asset, + asset: asset, + package: package, + ), ); final VerificationResult verification = verify( @@ -138,9 +145,9 @@ void main() { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 100); const newPlayerId = 2; - when( - api.createForTextureView(any), - ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100)); + when(api.createForTextureView(any)).thenAnswer( + (_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100), + ); const uri = 'https://example.com'; final int? playerId = await player.create( @@ -263,7 +270,11 @@ void main() { const fileUri = 'file:///foo/bar'; const headers = {'Authorization': 'Bearer token'}; await player.create( - DataSource(sourceType: DataSourceType.file, uri: fileUri, httpHeaders: headers), + DataSource( + sourceType: DataSourceType.file, + uri: fileUri, + httpHeaders: headers, + ), ); final VerificationResult verification = verify( api.createForTextureView(captureAny), @@ -276,14 +287,16 @@ void main() { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 100); const newPlayerId = 2; - when( - api.createForTextureView(any), - ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100)); + when(api.createForTextureView(any)).thenAnswer( + (_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100), + ); const asset = 'someAsset'; const package = 'somePackage'; const assetKey = 'resultingAssetKey'; - when(api.getLookupKeyForAsset(asset, package)).thenAnswer((_) async => assetKey); + when( + api.getLookupKeyForAsset(asset, package), + ).thenAnswer((_) async => assetKey); final int? playerId = await player.createWithOptions( VideoCreationOptions( @@ -312,9 +325,9 @@ void main() { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 100); const newPlayerId = 2; - when( - api.createForTextureView(any), - ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100)); + when(api.createForTextureView(any)).thenAnswer( + (_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100), + ); const uri = 'https://example.com'; final int? playerId = await player.createWithOptions( @@ -346,9 +359,9 @@ void main() { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 100); const newPlayerId = 2; - when( - api.createForTextureView(any), - ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100)); + when(api.createForTextureView(any)).thenAnswer( + (_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100), + ); const headers = {'Authorization': 'Bearer token'}; final int? playerId = await player.createWithOptions( @@ -374,9 +387,9 @@ void main() { final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer(playerId: 1, textureId: 100); const newPlayerId = 2; - when( - api.createForTextureView(any), - ).thenAnswer((_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100)); + when(api.createForTextureView(any)).thenAnswer( + (_) async => TexturePlayerIds(playerId: newPlayerId, textureId: 100), + ); const fileUri = 'file:///foo/bar'; final int? playerId = await player.createWithOptions( @@ -452,24 +465,39 @@ void main() { }); test('setLooping', () async { - final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = - setUpMockPlayer(playerId: 1); + final ( + AndroidVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); await player.setLooping(1, true); verify(playerApi.setLooping(true)); }); test('play', () async { - final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = - setUpMockPlayer(playerId: 1); + final ( + AndroidVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); await player.play(1); verify(playerApi.play()); }); test('pause', () async { - final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = - setUpMockPlayer(playerId: 1); + final ( + AndroidVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); await player.pause(1); verify(playerApi.pause()); @@ -494,8 +522,13 @@ void main() { }); test('setVolume', () async { - final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = - setUpMockPlayer(playerId: 1); + final ( + AndroidVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); const volume = 0.7; await player.setVolume(1, volume); @@ -503,8 +536,13 @@ void main() { }); test('setPlaybackSpeed', () async { - final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = - setUpMockPlayer(playerId: 1); + final ( + AndroidVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); const speed = 1.5; await player.setPlaybackSpeed(1, speed); @@ -512,19 +550,34 @@ void main() { }); test('seekTo', () async { - final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = - setUpMockPlayer(playerId: 1); + final ( + AndroidVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); const positionMilliseconds = 12345; - await player.seekTo(1, const Duration(milliseconds: positionMilliseconds)); + await player.seekTo( + 1, + const Duration(milliseconds: positionMilliseconds), + ); verify(playerApi.seekTo(positionMilliseconds)); }); test('getPosition', () async { - final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = - setUpMockPlayer(playerId: 1); + final ( + AndroidVideoPlayer player, + _, + MockVideoPlayerInstanceApi playerApi, + ) = setUpMockPlayer( + playerId: 1, + ); const positionMilliseconds = 12345; - when(playerApi.getCurrentPosition()).thenAnswer((_) async => positionMilliseconds); + when( + playerApi.getCurrentPosition(), + ).thenAnswer((_) async => positionMilliseconds); final Duration position = await player.getPosition(1); expect(position, const Duration(milliseconds: positionMilliseconds)); @@ -534,7 +587,9 @@ void main() { // Sets up a mock player that emits the given event structure as a success // callback on the internal platform channel event stream, and returns // the player's videoEventsFor(...) stream. - Stream mockPlayerEmitingEvents(List events) { + Stream mockPlayerEmitingEvents( + List events, + ) { const playerId = 1; final ( AndroidVideoPlayer player, @@ -551,16 +606,15 @@ void main() { } test('initialize', () async { - final Stream eventStream = mockPlayerEmitingEvents( - [ - InitializationEvent( - duration: 98765, - width: 1920, - height: 1080, - rotationCorrection: 90, - ), - ], - ); + final Stream eventStream = + mockPlayerEmitingEvents([ + InitializationEvent( + duration: 98765, + width: 1920, + height: 1080, + rotationCorrection: 90, + ), + ]); expect( eventStream, @@ -576,16 +630,15 @@ void main() { }); test('initialization triggers buffer update polling', () async { - final Stream eventStream = mockPlayerEmitingEvents( - [ - InitializationEvent( - duration: 98765, - width: 1920, - height: 1080, - rotationCorrection: 90, - ), - ], - ); + final Stream eventStream = + mockPlayerEmitingEvents([ + InitializationEvent( + duration: 98765, + width: 1920, + height: 1080, + rotationCorrection: 90, + ), + ]); expect( eventStream, @@ -598,7 +651,9 @@ void main() { ), VideoEvent( eventType: VideoEventType.bufferingUpdate, - buffered: [DurationRange(Duration.zero, Duration.zero)], + buffered: [ + DurationRange(Duration.zero, Duration.zero), + ], ), ]), ); @@ -613,7 +668,9 @@ void main() { expect( eventStream, - emitsInOrder([VideoEvent(eventType: VideoEventType.completed)]), + emitsInOrder([ + VideoEvent(eventType: VideoEventType.completed), + ]), ); }); @@ -631,7 +688,9 @@ void main() { // A buffer start should trigger a buffer update as well. VideoEvent( eventType: VideoEventType.bufferingUpdate, - buffered: [DurationRange(Duration.zero, Duration.zero)], + buffered: [ + DurationRange(Duration.zero, Duration.zero), + ], ), ]), ); @@ -654,7 +713,9 @@ void main() { VideoEvent(eventType: VideoEventType.bufferingStart), VideoEvent( eventType: VideoEventType.bufferingUpdate, - buffered: [DurationRange(Duration.zero, Duration.zero)], + buffered: [ + DurationRange(Duration.zero, Duration.zero), + ], ), // Emitted by ready. VideoEvent(eventType: VideoEventType.bufferingEnd), @@ -679,7 +740,9 @@ void main() { VideoEvent(eventType: VideoEventType.bufferingStart), VideoEvent( eventType: VideoEventType.bufferingUpdate, - buffered: [DurationRange(Duration.zero, Duration.zero)], + buffered: [ + DurationRange(Duration.zero, Duration.zero), + ], ), // Emitted by ready. VideoEvent(eventType: VideoEventType.bufferingEnd), @@ -704,7 +767,9 @@ void main() { VideoEvent(eventType: VideoEventType.bufferingStart), VideoEvent( eventType: VideoEventType.bufferingUpdate, - buffered: [DurationRange(Duration.zero, Duration.zero)], + buffered: [ + DurationRange(Duration.zero, Duration.zero), + ], ), // Emitted by ended. VideoEvent(eventType: VideoEventType.completed), @@ -721,7 +786,10 @@ void main() { expect( eventStream, emitsInOrder([ - VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: true), + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + ), ]), ); }); @@ -734,7 +802,10 @@ void main() { expect( eventStream, emitsInOrder([ - VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: false), + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: false, + ), ]), ); }); @@ -751,7 +822,9 @@ void main() { final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = setUpMockPlayer(playerId: 1); when(api.getAudioTracks()).thenAnswer( - (_) async => NativeAudioTrackData(exoPlayerTracks: []), + (_) async => NativeAudioTrackData( + exoPlayerTracks: [], + ), ); final List tracks = await player.getAudioTracks(1); @@ -760,61 +833,66 @@ void main() { verify(api.getAudioTracks()); }); - test('getAudioTracks converts native tracks to VideoAudioTrack', () async { - final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = - setUpMockPlayer(playerId: 1); - when(api.getAudioTracks()).thenAnswer( - (_) async => NativeAudioTrackData( - exoPlayerTracks: [ - ExoPlayerAudioTrackData( - groupIndex: 0, - trackIndex: 1, - label: 'English', - language: 'en', - isSelected: true, - bitrate: 128000, - sampleRate: 44100, - channelCount: 2, - codec: 'mp4a.40.2', - ), - ExoPlayerAudioTrackData( - groupIndex: 0, - trackIndex: 2, - label: 'Spanish', - language: 'es', - isSelected: false, - bitrate: 128000, - sampleRate: 44100, - channelCount: 2, - codec: 'mp4a.40.2', - ), - ], - ), - ); - - final List tracks = await player.getAudioTracks(1); - - expect(tracks.length, 2); - - expect(tracks[0].id, '0_1'); - expect(tracks[0].label, 'English'); - expect(tracks[0].language, 'en'); - expect(tracks[0].isSelected, true); - expect(tracks[0].bitrate, 128000); - expect(tracks[0].sampleRate, 44100); - expect(tracks[0].channelCount, 2); - expect(tracks[0].codec, 'mp4a.40.2'); - - expect(tracks[1].id, '0_2'); - expect(tracks[1].label, 'Spanish'); - expect(tracks[1].language, 'es'); - expect(tracks[1].isSelected, false); - }); + test( + 'getAudioTracks converts native tracks to VideoAudioTrack', + () async { + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = + setUpMockPlayer(playerId: 1); + when(api.getAudioTracks()).thenAnswer( + (_) async => NativeAudioTrackData( + exoPlayerTracks: [ + ExoPlayerAudioTrackData( + groupIndex: 0, + trackIndex: 1, + label: 'English', + language: 'en', + isSelected: true, + bitrate: 128000, + sampleRate: 44100, + channelCount: 2, + codec: 'mp4a.40.2', + ), + ExoPlayerAudioTrackData( + groupIndex: 0, + trackIndex: 2, + label: 'Spanish', + language: 'es', + isSelected: false, + bitrate: 128000, + sampleRate: 44100, + channelCount: 2, + codec: 'mp4a.40.2', + ), + ], + ), + ); + + final List tracks = await player.getAudioTracks(1); + + expect(tracks.length, 2); + + expect(tracks[0].id, '0_1'); + expect(tracks[0].label, 'English'); + expect(tracks[0].language, 'en'); + expect(tracks[0].isSelected, true); + expect(tracks[0].bitrate, 128000); + expect(tracks[0].sampleRate, 44100); + expect(tracks[0].channelCount, 2); + expect(tracks[0].codec, 'mp4a.40.2'); + + expect(tracks[1].id, '0_2'); + expect(tracks[1].label, 'Spanish'); + expect(tracks[1].language, 'es'); + expect(tracks[1].isSelected, false); + }, + ); test('getAudioTracks handles null exoPlayerTracks', () async { final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = setUpMockPlayer(playerId: 1); - when(api.getAudioTracks()).thenAnswer((_) async => NativeAudioTrackData()); + when( + api.getAudioTracks(), + ).thenAnswer((_) async => NativeAudioTrackData()); final List tracks = await player.getAudioTracks(1); @@ -852,7 +930,10 @@ void main() { test('selectAudioTrack throws on trackId with too many parts', () async { final (AndroidVideoPlayer player, _, _) = setUpMockPlayer(playerId: 1); - expect(() => player.selectAudioTrack(1, '1_2_3'), throwsA(isA())); + expect( + () => player.selectAudioTrack(1, '1_2_3'), + throwsA(isA()), + ); }); test('selectAudioTrack completes on AudioTrackChangedEvent', () async { @@ -889,7 +970,9 @@ void main() { test('getVideoTracks returns empty list when no tracks', () async { final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = setUpMockPlayer(playerId: 1); - when(api.getVideoTracks()).thenAnswer((_) async => NativeVideoTrackData()); + when( + api.getVideoTracks(), + ).thenAnswer((_) async => NativeVideoTrackData()); final List tracks = await player.getVideoTracks(1); @@ -949,48 +1032,56 @@ void main() { expect(tracks[1].height, 720); }); - test('getVideoTracks generates label from resolution if not provided', () async { - final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = - setUpMockPlayer(playerId: 1); - when(api.getVideoTracks()).thenAnswer( - (_) async => NativeVideoTrackData( - exoPlayerTracks: [ - ExoPlayerVideoTrackData( - groupIndex: 0, - trackIndex: 0, - isSelected: true, - width: 1920, - height: 1080, - ), - ], - ), - ); + test( + 'getVideoTracks generates label from resolution if not provided', + () async { + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = + setUpMockPlayer(playerId: 1); + when(api.getVideoTracks()).thenAnswer( + (_) async => NativeVideoTrackData( + exoPlayerTracks: [ + ExoPlayerVideoTrackData( + groupIndex: 0, + trackIndex: 0, + isSelected: true, + width: 1920, + height: 1080, + ), + ], + ), + ); - final List tracks = await player.getVideoTracks(1); + final List tracks = await player.getVideoTracks(1); - expect(tracks.length, 1); - expect(tracks[0].label, '1080p'); - }); + expect(tracks.length, 1); + expect(tracks[0].label, '1080p'); + }, + ); test('getVideoTracks handles null exoPlayerTracks', () async { final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = setUpMockPlayer(playerId: 1); - when(api.getVideoTracks()).thenAnswer((_) async => NativeVideoTrackData()); + when( + api.getVideoTracks(), + ).thenAnswer((_) async => NativeVideoTrackData()); final List tracks = await player.getVideoTracks(1); expect(tracks, isEmpty); }); - test('selectVideoTrack with null clears override (auto quality)', () async { - final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = - setUpMockPlayer(playerId: 1); - when(api.selectVideoTrack(-1, -1)).thenAnswer((_) async {}); + test( + 'selectVideoTrack with null clears override (auto quality)', + () async { + final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = + setUpMockPlayer(playerId: 1); + when(api.selectVideoTrack(-1, -1)).thenAnswer((_) async {}); - await player.selectVideoTrack(1, null); + await player.selectVideoTrack(1, null); - verify(api.selectVideoTrack(-1, -1)); - }); + verify(api.selectVideoTrack(-1, -1)); + }, + ); test('selectVideoTrack parses track id and calls API', () async { final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi api) = @@ -1007,14 +1098,20 @@ void main() { final (AndroidVideoPlayer player, _, _) = setUpMockPlayer(playerId: 1); const track = VideoTrack(id: 'invalid', isSelected: false); - expect(() => player.selectVideoTrack(1, track), throwsA(isA())); + expect( + () => player.selectVideoTrack(1, track), + throwsA(isA()), + ); }); test('selectVideoTrack throws on track id with too many parts', () async { final (AndroidVideoPlayer player, _, _) = setUpMockPlayer(playerId: 1); const track = VideoTrack(id: '1_2_3', isSelected: false); - expect(() => player.selectVideoTrack(1, track), throwsA(isA())); + expect( + () => player.selectVideoTrack(1, track), + throwsA(isA()), + ); }); }); }); diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 484d51246b1..061408dc3b3 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -440,84 +440,80 @@ - (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, // Use AVAssetVariant API for iOS 15+ to get HLS variants if (@available(iOS 15.0, macOS 12.0, *)) { - [urlAsset loadValuesAsynchronouslyForKeys:@[ @"variants" ] - completionHandler:^{ - dispatch_async(dispatch_get_main_queue(), ^{ - NSError *error = nil; - AVKeyValueStatus status = [urlAsset statusOfValueForKey:@"variants" - error:&error]; - - if (status == AVKeyValueStatusLoaded) { - NSArray *variants = urlAsset.variants; - double currentBitrate = - currentItem.preferredPeakBitRate > 0 - ? currentItem.preferredPeakBitRate - : 0; - - for (NSInteger i = 0; i < variants.count; i++) { - AVAssetVariant *variant = variants[i]; - double peakBitRate = variant.peakBitRate; - CGSize videoSize = CGSizeZero; - double frameRate = 0; - NSString *codec = nil; - - // Get video attributes if available - AVAssetVariantVideoAttributes *videoAttrs = - variant.videoAttributes; - if (videoAttrs) { - videoSize = videoAttrs.presentationSize; - frameRate = videoAttrs.nominalFrameRate; - // Get codec from media sub types - NSArray *codecTypes = videoAttrs.codecTypes; - if (codecTypes.count > 0) { - FourCharCode codecType = - [codecTypes[0] unsignedIntValue]; - codec = [self codecStringFromFourCharCode:codecType]; - } - } - - // Determine if this variant is selected (approximate match by - // bitrate) - BOOL isSelected = (currentBitrate > 0 && - fabs(peakBitRate - currentBitrate) < - peakBitRate * 0.1); - - // Generate label from resolution - NSString *label = nil; - if (videoSize.height > 0) { - label = - [NSString stringWithFormat:@"%.0fp", videoSize.height]; - } - - FVPMediaSelectionVideoTrackData *trackData = - [FVPMediaSelectionVideoTrackData - makeWithVariantIndex:i - label:label - bitrate:peakBitRate > 0 - ? @((NSInteger)peakBitRate) - : nil - width:videoSize.width > 0 - ? @((NSInteger)videoSize.width) - : nil - height:videoSize.height > 0 - ? @((NSInteger)videoSize - .height) - : nil - frameRate:frameRate > 0 ? @(frameRate) : nil - codec:codec - isSelected:isSelected]; - [mediaSelectionTracks addObject:trackData]; - } + [urlAsset + loadValuesAsynchronouslyForKeys:@[ @"variants" ] + completionHandler:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error = nil; + AVKeyValueStatus status = [urlAsset statusOfValueForKey:@"variants" + error:&error]; + + if (status == AVKeyValueStatusLoaded) { + NSArray *variants = urlAsset.variants; + double currentBitrate = currentItem.preferredPeakBitRate > 0 + ? currentItem.preferredPeakBitRate + : 0; + + for (NSInteger i = 0; i < variants.count; i++) { + AVAssetVariant *variant = variants[i]; + double peakBitRate = variant.peakBitRate; + CGSize videoSize = CGSizeZero; + double frameRate = 0; + NSString *codec = nil; + + // Get video attributes if available + AVAssetVariantVideoAttributes *videoAttrs = variant.videoAttributes; + if (videoAttrs) { + videoSize = videoAttrs.presentationSize; + frameRate = videoAttrs.nominalFrameRate; + // Get codec from media sub types + NSArray *codecTypes = videoAttrs.codecTypes; + if (codecTypes.count > 0) { + FourCharCode codecType = [codecTypes[0] unsignedIntValue]; + codec = [self codecStringFromFourCharCode:codecType]; } - - FVPNativeVideoTrackData *result = [FVPNativeVideoTrackData - makeWithAssetTracks:nil - mediaSelectionTracks:mediaSelectionTracks.count > 0 - ? mediaSelectionTracks - : nil]; - completion(result, nil); - }); - }]; + } + + // Determine if this variant is selected (approximate match by + // bitrate) + BOOL isSelected = + (currentBitrate > 0 && + fabs(peakBitRate - currentBitrate) < peakBitRate * 0.1); + + // Generate label from resolution + NSString *label = nil; + if (videoSize.height > 0) { + label = [NSString stringWithFormat:@"%.0fp", videoSize.height]; + } + + FVPMediaSelectionVideoTrackData *trackData = + [FVPMediaSelectionVideoTrackData + makeWithVariantIndex:i + label:label + bitrate:peakBitRate > 0 + ? @((NSInteger)peakBitRate) + : nil + width:videoSize.width > 0 + ? @((NSInteger)videoSize.width) + : nil + height:videoSize.height > 0 + ? @((NSInteger)videoSize.height) + : nil + frameRate:frameRate > 0 ? @(frameRate) : nil + codec:codec + isSelected:isSelected]; + [mediaSelectionTracks addObject:trackData]; + } + } + + FVPNativeVideoTrackData *result = [FVPNativeVideoTrackData + makeWithAssetTracks:nil + mediaSelectionTracks:mediaSelectionTracks.count > 0 + ? mediaSelectionTracks + : nil]; + completion(result, nil); + }); + }]; } else { // For iOS < 15, return empty list as AVAssetVariant is not available completion([[FVPNativeVideoTrackData alloc] init], nil); diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h index 64a65673e90..5806521efb3 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h @@ -24,78 +24,79 @@ NS_ASSUME_NONNULL_BEGIN @interface FVPPlatformVideoViewCreationParams : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithPlayerId:(NSInteger )playerId; -@property(nonatomic, assign) NSInteger playerId; ++ (instancetype)makeWithPlayerId:(NSInteger)playerId; +@property(nonatomic, assign) NSInteger playerId; @end @interface FVPCreationOptions : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; + (instancetype)makeWithUri:(NSString *)uri - httpHeaders:(NSDictionary *)httpHeaders; -@property(nonatomic, copy) NSString * uri; -@property(nonatomic, copy) NSDictionary * httpHeaders; + httpHeaders:(NSDictionary *)httpHeaders; +@property(nonatomic, copy) NSString *uri; +@property(nonatomic, copy) NSDictionary *httpHeaders; @end @interface FVPTexturePlayerIds : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithPlayerId:(NSInteger )playerId - textureId:(NSInteger )textureId; -@property(nonatomic, assign) NSInteger playerId; -@property(nonatomic, assign) NSInteger textureId; ++ (instancetype)makeWithPlayerId:(NSInteger)playerId textureId:(NSInteger)textureId; +@property(nonatomic, assign) NSInteger playerId; +@property(nonatomic, assign) NSInteger textureId; @end /// Video track data from AVAssetVariant (HLS variants) for iOS 15+. @interface FVPMediaSelectionVideoTrackData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithVariantIndex:(NSInteger )variantIndex - label:(nullable NSString *)label - bitrate:(nullable NSNumber *)bitrate - width:(nullable NSNumber *)width - height:(nullable NSNumber *)height - frameRate:(nullable NSNumber *)frameRate - codec:(nullable NSString *)codec - isSelected:(BOOL )isSelected; -@property(nonatomic, assign) NSInteger variantIndex; -@property(nonatomic, copy, nullable) NSString * label; -@property(nonatomic, strong, nullable) NSNumber * bitrate; -@property(nonatomic, strong, nullable) NSNumber * width; -@property(nonatomic, strong, nullable) NSNumber * height; -@property(nonatomic, strong, nullable) NSNumber * frameRate; -@property(nonatomic, copy, nullable) NSString * codec; -@property(nonatomic, assign) BOOL isSelected; ++ (instancetype)makeWithVariantIndex:(NSInteger)variantIndex + label:(nullable NSString *)label + bitrate:(nullable NSNumber *)bitrate + width:(nullable NSNumber *)width + height:(nullable NSNumber *)height + frameRate:(nullable NSNumber *)frameRate + codec:(nullable NSString *)codec + isSelected:(BOOL)isSelected; +@property(nonatomic, assign) NSInteger variantIndex; +@property(nonatomic, copy, nullable) NSString *label; +@property(nonatomic, strong, nullable) NSNumber *bitrate; +@property(nonatomic, strong, nullable) NSNumber *width; +@property(nonatomic, strong, nullable) NSNumber *height; +@property(nonatomic, strong, nullable) NSNumber *frameRate; +@property(nonatomic, copy, nullable) NSString *codec; +@property(nonatomic, assign) BOOL isSelected; @end /// Video track data from AVAssetTrack (regular videos). @interface FVPAssetVideoTrackData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithTrackId:(NSInteger )trackId - label:(nullable NSString *)label - width:(nullable NSNumber *)width - height:(nullable NSNumber *)height - frameRate:(nullable NSNumber *)frameRate - codec:(nullable NSString *)codec - isSelected:(BOOL )isSelected; -@property(nonatomic, assign) NSInteger trackId; -@property(nonatomic, copy, nullable) NSString * label; -@property(nonatomic, strong, nullable) NSNumber * width; -@property(nonatomic, strong, nullable) NSNumber * height; -@property(nonatomic, strong, nullable) NSNumber * frameRate; -@property(nonatomic, copy, nullable) NSString * codec; -@property(nonatomic, assign) BOOL isSelected; ++ (instancetype)makeWithTrackId:(NSInteger)trackId + label:(nullable NSString *)label + width:(nullable NSNumber *)width + height:(nullable NSNumber *)height + frameRate:(nullable NSNumber *)frameRate + codec:(nullable NSString *)codec + isSelected:(BOOL)isSelected; +@property(nonatomic, assign) NSInteger trackId; +@property(nonatomic, copy, nullable) NSString *label; +@property(nonatomic, strong, nullable) NSNumber *width; +@property(nonatomic, strong, nullable) NSNumber *height; +@property(nonatomic, strong, nullable) NSNumber *frameRate; +@property(nonatomic, copy, nullable) NSString *codec; +@property(nonatomic, assign) BOOL isSelected; @end /// Container for video track data from iOS. @interface FVPNativeVideoTrackData : NSObject + (instancetype)makeWithAssetTracks:(nullable NSArray *)assetTracks - mediaSelectionTracks:(nullable NSArray *)mediaSelectionTracks; + mediaSelectionTracks: + (nullable NSArray *)mediaSelectionTracks; /// Asset-based tracks (for regular videos) -@property(nonatomic, copy, nullable) NSArray * assetTracks; +@property(nonatomic, copy, nullable) NSArray *assetTracks; /// Media selection tracks (for HLS variants on iOS 15+) -@property(nonatomic, copy, nullable) NSArray * mediaSelectionTracks; +@property(nonatomic, copy, nullable) + NSArray *mediaSelectionTracks; @end /// The codec used by all APIs. @@ -104,17 +105,25 @@ NSObject *FVPGetMessagesCodec(void); @protocol FVPAVFoundationVideoPlayerApi - (void)initialize:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(FVPCreationOptions *)params error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(FVPCreationOptions *)params + error:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable FVPTexturePlayerIds *)createTexturePlayerWithOptions:(FVPCreationOptions *)creationOptions error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable FVPTexturePlayerIds *) + createTexturePlayerWithOptions:(FVPCreationOptions *)creationOptions + error:(FlutterError *_Nullable *_Nonnull)error; - (void)setMixWithOthers:(BOOL)mixWithOthers error:(FlutterError *_Nullable *_Nonnull)error; -- (nullable NSString *)fileURLForAssetWithName:(NSString *)asset package:(nullable NSString *)package error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)fileURLForAssetWithName:(NSString *)asset + package:(nullable NSString *)package + error:(FlutterError *_Nullable *_Nonnull)error; @end -extern void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, NSObject *_Nullable api); - -extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); +extern void SetUpFVPAVFoundationVideoPlayerApi( + id binaryMessenger, + NSObject *_Nullable api); +extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix( + id binaryMessenger, + NSObject *_Nullable api, NSString *messageChannelSuffix); @protocol FVPVideoPlayerInstanceApi - (void)setLooping:(BOOL)looping error:(FlutterError *_Nullable *_Nonnull)error; @@ -127,14 +136,19 @@ extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *_Nullable api); +extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, + NSObject *_Nullable api); -extern void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); +extern void SetUpFVPVideoPlayerInstanceApiWithSuffix( + id binaryMessenger, NSObject *_Nullable api, + NSString *messageChannelSuffix); NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m index 965bfbdc0a9..b4efb46c9e2 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m @@ -67,13 +67,15 @@ + (nullable FVPNativeVideoTrackData *)nullableFromList:(NSArray *)list; @end @implementation FVPPlatformVideoViewCreationParams -+ (instancetype)makeWithPlayerId:(NSInteger )playerId { - FVPPlatformVideoViewCreationParams* pigeonResult = [[FVPPlatformVideoViewCreationParams alloc] init]; ++ (instancetype)makeWithPlayerId:(NSInteger)playerId { + FVPPlatformVideoViewCreationParams *pigeonResult = + [[FVPPlatformVideoViewCreationParams alloc] init]; pigeonResult.playerId = playerId; return pigeonResult; } + (FVPPlatformVideoViewCreationParams *)fromList:(NSArray *)list { - FVPPlatformVideoViewCreationParams *pigeonResult = [[FVPPlatformVideoViewCreationParams alloc] init]; + FVPPlatformVideoViewCreationParams *pigeonResult = + [[FVPPlatformVideoViewCreationParams alloc] init]; pigeonResult.playerId = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } @@ -89,8 +91,8 @@ + (nullable FVPPlatformVideoViewCreationParams *)nullableFromList:(NSArray * @implementation FVPCreationOptions + (instancetype)makeWithUri:(NSString *)uri - httpHeaders:(NSDictionary *)httpHeaders { - FVPCreationOptions* pigeonResult = [[FVPCreationOptions alloc] init]; + httpHeaders:(NSDictionary *)httpHeaders { + FVPCreationOptions *pigeonResult = [[FVPCreationOptions alloc] init]; pigeonResult.uri = uri; pigeonResult.httpHeaders = httpHeaders; return pigeonResult; @@ -113,9 +115,8 @@ + (nullable FVPCreationOptions *)nullableFromList:(NSArray *)list { @end @implementation FVPTexturePlayerIds -+ (instancetype)makeWithPlayerId:(NSInteger )playerId - textureId:(NSInteger )textureId { - FVPTexturePlayerIds* pigeonResult = [[FVPTexturePlayerIds alloc] init]; ++ (instancetype)makeWithPlayerId:(NSInteger)playerId textureId:(NSInteger)textureId { + FVPTexturePlayerIds *pigeonResult = [[FVPTexturePlayerIds alloc] init]; pigeonResult.playerId = playerId; pigeonResult.textureId = textureId; return pigeonResult; @@ -138,15 +139,15 @@ + (nullable FVPTexturePlayerIds *)nullableFromList:(NSArray *)list { @end @implementation FVPMediaSelectionVideoTrackData -+ (instancetype)makeWithVariantIndex:(NSInteger )variantIndex - label:(nullable NSString *)label - bitrate:(nullable NSNumber *)bitrate - width:(nullable NSNumber *)width - height:(nullable NSNumber *)height - frameRate:(nullable NSNumber *)frameRate - codec:(nullable NSString *)codec - isSelected:(BOOL )isSelected { - FVPMediaSelectionVideoTrackData* pigeonResult = [[FVPMediaSelectionVideoTrackData alloc] init]; ++ (instancetype)makeWithVariantIndex:(NSInteger)variantIndex + label:(nullable NSString *)label + bitrate:(nullable NSNumber *)bitrate + width:(nullable NSNumber *)width + height:(nullable NSNumber *)height + frameRate:(nullable NSNumber *)frameRate + codec:(nullable NSString *)codec + isSelected:(BOOL)isSelected { + FVPMediaSelectionVideoTrackData *pigeonResult = [[FVPMediaSelectionVideoTrackData alloc] init]; pigeonResult.variantIndex = variantIndex; pigeonResult.label = label; pigeonResult.bitrate = bitrate; @@ -187,14 +188,14 @@ + (nullable FVPMediaSelectionVideoTrackData *)nullableFromList:(NSArray *)li @end @implementation FVPAssetVideoTrackData -+ (instancetype)makeWithTrackId:(NSInteger )trackId - label:(nullable NSString *)label - width:(nullable NSNumber *)width - height:(nullable NSNumber *)height - frameRate:(nullable NSNumber *)frameRate - codec:(nullable NSString *)codec - isSelected:(BOOL )isSelected { - FVPAssetVideoTrackData* pigeonResult = [[FVPAssetVideoTrackData alloc] init]; ++ (instancetype)makeWithTrackId:(NSInteger)trackId + label:(nullable NSString *)label + width:(nullable NSNumber *)width + height:(nullable NSNumber *)height + frameRate:(nullable NSNumber *)frameRate + codec:(nullable NSString *)codec + isSelected:(BOOL)isSelected { + FVPAssetVideoTrackData *pigeonResult = [[FVPAssetVideoTrackData alloc] init]; pigeonResult.trackId = trackId; pigeonResult.label = label; pigeonResult.width = width; @@ -233,8 +234,9 @@ + (nullable FVPAssetVideoTrackData *)nullableFromList:(NSArray *)list { @implementation FVPNativeVideoTrackData + (instancetype)makeWithAssetTracks:(nullable NSArray *)assetTracks - mediaSelectionTracks:(nullable NSArray *)mediaSelectionTracks { - FVPNativeVideoTrackData* pigeonResult = [[FVPNativeVideoTrackData alloc] init]; + mediaSelectionTracks: + (nullable NSArray *)mediaSelectionTracks { + FVPNativeVideoTrackData *pigeonResult = [[FVPNativeVideoTrackData alloc] init]; pigeonResult.assetTracks = assetTracks; pigeonResult.mediaSelectionTracks = mediaSelectionTracks; return pigeonResult; @@ -261,17 +263,17 @@ @interface FVPMessagesPigeonCodecReader : FlutterStandardReader @implementation FVPMessagesPigeonCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 129: + case 129: return [FVPPlatformVideoViewCreationParams fromList:[self readValue]]; - case 130: + case 130: return [FVPCreationOptions fromList:[self readValue]]; - case 131: + case 131: return [FVPTexturePlayerIds fromList:[self readValue]]; - case 132: + case 132: return [FVPMediaSelectionVideoTrackData fromList:[self readValue]]; - case 133: + case 133: return [FVPAssetVideoTrackData fromList:[self readValue]]; - case 134: + case 134: return [FVPNativeVideoTrackData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -322,25 +324,35 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { static FlutterStandardMessageCodec *sSharedObject = nil; static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ - FVPMessagesPigeonCodecReaderWriter *readerWriter = [[FVPMessagesPigeonCodecReaderWriter alloc] init]; + FVPMessagesPigeonCodecReaderWriter *readerWriter = + [[FVPMessagesPigeonCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; }); return sSharedObject; } -void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, NSObject *api) { +void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, + NSObject *api) { SetUpFVPAVFoundationVideoPlayerApiWithSuffix(binaryMessenger, api, @""); } -void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *api, NSString *messageChannelSuffix) { - messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @""; +void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, + NSObject *api, + NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 + ? [NSString stringWithFormat:@".%@", messageChannelSuffix] + : @""; { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.initialize", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(initialize:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", api); + NSCAssert([api respondsToSelector:@selector(initialize:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api initialize:&error]; @@ -351,13 +363,19 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString + stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.createForPlatformView", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(createPlatformViewPlayerWithOptions:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(createPlatformViewPlayerWithOptions:error:)", api); + NSCAssert([api respondsToSelector:@selector(createPlatformViewPlayerWithOptions:error:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(createPlatformViewPlayerWithOptions:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FVPCreationOptions *arg_params = GetNullableObjectAtIndex(args, 0); @@ -370,18 +388,25 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString + stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.createForTextureView", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(createTexturePlayerWithOptions:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(createTexturePlayerWithOptions:error:)", api); + NSCAssert([api respondsToSelector:@selector(createTexturePlayerWithOptions:error:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(createTexturePlayerWithOptions:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FVPCreationOptions *arg_creationOptions = GetNullableObjectAtIndex(args, 0); FlutterError *error; - FVPTexturePlayerIds *output = [api createTexturePlayerWithOptions:arg_creationOptions error:&error]; + FVPTexturePlayerIds *output = [api createTexturePlayerWithOptions:arg_creationOptions + error:&error]; callback(wrapResult(output, error)); }]; } else { @@ -389,13 +414,18 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.setMixWithOthers", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(setMixWithOthers:error:)", api); + NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(setMixWithOthers:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_mixWithOthers = [GetNullableObjectAtIndex(args, 0) boolValue]; @@ -408,13 +438,18 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.getAssetUrl", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(fileURLForAssetWithName:package:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(fileURLForAssetWithName:package:error:)", api); + NSCAssert([api respondsToSelector:@selector(fileURLForAssetWithName:package:error:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(fileURLForAssetWithName:package:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSString *arg_asset = GetNullableObjectAtIndex(args, 0); @@ -428,20 +463,30 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } } -void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, NSObject *api) { +void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, + NSObject *api) { SetUpFVPVideoPlayerInstanceApiWithSuffix(binaryMessenger, api, @""); } -void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, NSObject *api, NSString *messageChannelSuffix) { - messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @""; +void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, + NSObject *api, + NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 + ? [NSString stringWithFormat:@".%@", messageChannelSuffix] + : @""; { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.setLooping", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setLooping:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setLooping:error:)", api); + NSCAssert( + [api respondsToSelector:@selector(setLooping:error:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setLooping:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_looping = [GetNullableObjectAtIndex(args, 0) boolValue]; @@ -454,13 +499,18 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.setVolume", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setVolume:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setVolume:error:)", api); + NSCAssert( + [api respondsToSelector:@selector(setVolume:error:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setVolume:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_volume = [GetNullableObjectAtIndex(args, 0) doubleValue]; @@ -473,13 +523,18 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.setPlaybackSpeed", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setPlaybackSpeed:error:)", api); + NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to " + @"@selector(setPlaybackSpeed:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_speed = [GetNullableObjectAtIndex(args, 0) doubleValue]; @@ -492,13 +547,17 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.play", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(playWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(playWithError:)", api); + NSCAssert([api respondsToSelector:@selector(playWithError:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(playWithError:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api playWithError:&error]; @@ -509,13 +568,16 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.getPosition", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(position:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(position:)", api); + NSCAssert([api respondsToSelector:@selector(position:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(position:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; NSNumber *output = [api position:&error]; @@ -526,32 +588,42 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.seekTo", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(seekTo:completion:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(seekTo:completion:)", api); + NSCAssert( + [api respondsToSelector:@selector(seekTo:completion:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(seekTo:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSInteger arg_position = [GetNullableObjectAtIndex(args, 0) integerValue]; - [api seekTo:arg_position completion:^(FlutterError *_Nullable error) { - callback(wrapResult(nil, error)); - }]; + [api seekTo:arg_position + completion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; }]; } else { [channel setMessageHandler:nil]; } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.pause", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(pauseWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(pauseWithError:)", api); + NSCAssert([api respondsToSelector:@selector(pauseWithError:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(pauseWithError:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api pauseWithError:&error]; @@ -562,13 +634,18 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.dispose", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(disposeWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(disposeWithError:)", api); + NSCAssert( + [api respondsToSelector:@selector(disposeWithError:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(disposeWithError:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api disposeWithError:&error]; @@ -580,15 +657,20 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } /// Gets the available video tracks for the video. { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getVideoTracks", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.getVideoTracks", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(getVideoTracks:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getVideoTracks:)", api); + NSCAssert([api respondsToSelector:@selector(getVideoTracks:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getVideoTracks:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - [api getVideoTracks:^(FVPNativeVideoTrackData *_Nullable output, FlutterError *_Nullable error) { + [api getVideoTracks:^(FVPNativeVideoTrackData *_Nullable output, + FlutterError *_Nullable error) { callback(wrapResult(output, error)); }]; }]; @@ -599,13 +681,18 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM /// Selects a video track by setting preferredPeakBitRate. /// Pass 0 to enable auto quality selection. { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectVideoTrack", messageChannelSuffix] + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.selectVideoTrack", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(selectVideoTrackWithBitrate:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(selectVideoTrackWithBitrate:error:)", api); + NSCAssert([api respondsToSelector:@selector(selectVideoTrackWithBitrate:error:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to " + @"@selector(selectVideoTrackWithBitrate:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSInteger arg_bitrate = [GetNullableObjectAtIndex(args, 0) integerValue]; diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index 26b87cd1a3b..230aa82cbda 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -21,7 +21,8 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { /// Creates a new AVFoundation-based video player implementation instance. AVFoundationVideoPlayer({ @visibleForTesting AVFoundationVideoPlayerApi? pluginApi, - @visibleForTesting VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, + @visibleForTesting + VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, }) : _api = pluginApi ?? AVFoundationVideoPlayerApi(), _playerApiProvider = playerApiProvider ?? _productionApiProvider; @@ -70,7 +71,9 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { case DataSourceType.asset: final String? asset = dataSource.asset; if (asset == null) { - throw ArgumentError('"asset" must be non-null for an asset data source'); + throw ArgumentError( + '"asset" must be non-null for an asset data source', + ); } uri = await _api.getAssetUrl(asset, dataSource.package); if (uri == null) { @@ -184,13 +187,16 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { // Convert HLS variant tracks (iOS 15+) if (nativeData.mediaSelectionTracks != null) { - for (final MediaSelectionVideoTrackData track in nativeData.mediaSelectionTracks!) { + for (final MediaSelectionVideoTrackData track + in nativeData.mediaSelectionTracks!) { // Use bitrate as the track ID for HLS variants final String trackId = 'variant_${track.bitrate ?? track.variantIndex}'; // Generate label from resolution if not provided final String? label = track.label ?? - (track.width != null && track.height != null ? '${track.height}p' : null); + (track.width != null && track.height != null + ? '${track.height}p' + : null); tracks.add( VideoTrack( id: trackId, @@ -213,7 +219,9 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { // Generate label from resolution if not provided final String? label = track.label ?? - (track.width != null && track.height != null ? '${track.height}p' : null); + (track.width != null && track.height != null + ? '${track.height}p' + : null); tracks.add( VideoTrack( id: trackId, @@ -267,7 +275,9 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { final VideoPlayerViewState viewState = _playerWith(id: playerId).viewState; return switch (viewState) { - VideoPlayerTextureViewState(:final int textureId) => Texture(textureId: textureId), + VideoPlayerTextureViewState(:final int textureId) => Texture( + textureId: textureId, + ), VideoPlayerPlatformViewState() => _buildPlatformView(playerId), }; } @@ -294,8 +304,11 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { /// An instance of a video player, corresponding to a single player ID in /// [AVFoundationVideoPlayer]. class _PlayerInstance { - _PlayerInstance(this._api, this.viewState, {required EventChannel eventChannel}) - : _eventChannel = eventChannel; + _PlayerInstance( + this._api, + this.viewState, { + required EventChannel eventChannel, + }) : _eventChannel = eventChannel; final VideoPlayerInstanceApi _api; final VideoPlayerViewState viewState; diff --git a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart index e5e88ec215a..84cb91c7b61 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart @@ -17,49 +17,49 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } + bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && - a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + a.indexed.every( + ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), + ); } if (a is Map && b is Map) { - return a.length == b.length && a.entries.every((MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key])); + return a.length == b.length && + a.entries.every( + (MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key]), + ); } return a == b; } - /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { - PlatformVideoViewCreationParams({ - required this.playerId, - }); + PlatformVideoViewCreationParams({required this.playerId}); int playerId; List _toList() { - return [ - playerId, - ]; + return [playerId]; } Object encode() { - return _toList(); } + return _toList(); + } static PlatformVideoViewCreationParams decode(Object result) { result as List; - return PlatformVideoViewCreationParams( - playerId: result[0]! as int, - ); + return PlatformVideoViewCreationParams(playerId: result[0]! as int); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlatformVideoViewCreationParams || other.runtimeType != runtimeType) { + if (other is! PlatformVideoViewCreationParams || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -70,35 +70,30 @@ class PlatformVideoViewCreationParams { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } class CreationOptions { - CreationOptions({ - required this.uri, - required this.httpHeaders, - }); + CreationOptions({required this.uri, required this.httpHeaders}); String uri; Map httpHeaders; List _toList() { - return [ - uri, - httpHeaders, - ]; + return [uri, httpHeaders]; } Object encode() { - return _toList(); } + return _toList(); + } static CreationOptions decode(Object result) { result as List; return CreationOptions( uri: result[0]! as String, - httpHeaders: (result[1] as Map?)!.cast(), + httpHeaders: (result[1] as Map?)! + .cast(), ); } @@ -116,29 +111,23 @@ class CreationOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } class TexturePlayerIds { - TexturePlayerIds({ - required this.playerId, - required this.textureId, - }); + TexturePlayerIds({required this.playerId, required this.textureId}); int playerId; int textureId; List _toList() { - return [ - playerId, - textureId, - ]; + return [playerId, textureId]; } Object encode() { - return _toList(); } + return _toList(); + } static TexturePlayerIds decode(Object result) { result as List; @@ -162,8 +151,7 @@ class TexturePlayerIds { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Video track data from AVAssetVariant (HLS variants) for iOS 15+. @@ -209,7 +197,8 @@ class MediaSelectionVideoTrackData { } Object encode() { - return _toList(); } + return _toList(); + } static MediaSelectionVideoTrackData decode(Object result) { result as List; @@ -228,7 +217,8 @@ class MediaSelectionVideoTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! MediaSelectionVideoTrackData || other.runtimeType != runtimeType) { + if (other is! MediaSelectionVideoTrackData || + other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -239,8 +229,7 @@ class MediaSelectionVideoTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Video track data from AVAssetTrack (regular videos). @@ -282,7 +271,8 @@ class AssetVideoTrackData { } Object encode() { - return _toList(); } + return _toList(); + } static AssetVideoTrackData decode(Object result) { result as List; @@ -311,16 +301,12 @@ class AssetVideoTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } /// Container for video track data from iOS. class NativeVideoTrackData { - NativeVideoTrackData({ - this.assetTracks, - this.mediaSelectionTracks, - }); + NativeVideoTrackData({this.assetTracks, this.mediaSelectionTracks}); /// Asset-based tracks (for regular videos) List? assetTracks; @@ -329,20 +315,19 @@ class NativeVideoTrackData { List? mediaSelectionTracks; List _toList() { - return [ - assetTracks, - mediaSelectionTracks, - ]; + return [assetTracks, mediaSelectionTracks]; } Object encode() { - return _toList(); } + return _toList(); + } static NativeVideoTrackData decode(Object result) { result as List; return NativeVideoTrackData( assetTracks: (result[0] as List?)?.cast(), - mediaSelectionTracks: (result[1] as List?)?.cast(), + mediaSelectionTracks: (result[1] as List?) + ?.cast(), ); } @@ -360,11 +345,9 @@ class NativeVideoTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -372,22 +355,22 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PlatformVideoViewCreationParams) { + } else if (value is PlatformVideoViewCreationParams) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is CreationOptions) { + } else if (value is CreationOptions) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is TexturePlayerIds) { + } else if (value is TexturePlayerIds) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is MediaSelectionVideoTrackData) { + } else if (value is MediaSelectionVideoTrackData) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is AssetVideoTrackData) { + } else if (value is AssetVideoTrackData) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is NativeVideoTrackData) { + } else if (value is NativeVideoTrackData) { buffer.putUint8(134); writeValue(buffer, value.encode()); } else { @@ -398,17 +381,17 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: return PlatformVideoViewCreationParams.decode(readValue(buffer)!); - case 130: + case 130: return CreationOptions.decode(readValue(buffer)!); - case 131: + case 131: return TexturePlayerIds.decode(readValue(buffer)!); - case 132: + case 132: return MediaSelectionVideoTrackData.decode(readValue(buffer)!); - case 133: + case 133: return AssetVideoTrackData.decode(readValue(buffer)!); - case 134: + case 134: return NativeVideoTrackData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -420,9 +403,13 @@ class AVFoundationVideoPlayerApi { /// Constructor for [AVFoundationVideoPlayerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AVFoundationVideoPlayerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + AVFoundationVideoPlayerApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -430,12 +417,14 @@ class AVFoundationVideoPlayerApi { final String pigeonVar_messageChannelSuffix; Future initialize() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; @@ -453,13 +442,17 @@ class AVFoundationVideoPlayerApi { } Future createForPlatformView(CreationOptions params) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [params], ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([params]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -480,14 +473,20 @@ class AVFoundationVideoPlayerApi { } } - Future createForTextureView(CreationOptions creationOptions) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, + Future createForTextureView( + CreationOptions creationOptions, + ) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [creationOptions], ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([creationOptions]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -509,13 +508,17 @@ class AVFoundationVideoPlayerApi { } Future setMixWithOthers(bool mixWithOthers) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [mixWithOthers], ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -532,13 +535,17 @@ class AVFoundationVideoPlayerApi { } Future getAssetUrl(String asset, String? package) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [asset, package], ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([asset, package]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -559,9 +566,13 @@ class VideoPlayerInstanceApi { /// Constructor for [VideoPlayerInstanceApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - VideoPlayerInstanceApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + VideoPlayerInstanceApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -569,13 +580,17 @@ class VideoPlayerInstanceApi { final String pigeonVar_messageChannelSuffix; Future setLooping(bool looping) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [looping], ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([looping]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -592,13 +607,17 @@ class VideoPlayerInstanceApi { } Future setVolume(double volume) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [volume], ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -615,13 +634,17 @@ class VideoPlayerInstanceApi { } Future setPlaybackSpeed(double speed) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [speed], ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([speed]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -638,12 +661,14 @@ class VideoPlayerInstanceApi { } Future play() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; @@ -661,12 +686,14 @@ class VideoPlayerInstanceApi { } Future getPosition() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; @@ -689,13 +716,17 @@ class VideoPlayerInstanceApi { } Future seekTo(int position) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [position], ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([position]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -712,12 +743,14 @@ class VideoPlayerInstanceApi { } Future pause() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; @@ -735,12 +768,14 @@ class VideoPlayerInstanceApi { } Future dispose() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; @@ -759,12 +794,14 @@ class VideoPlayerInstanceApi { /// Gets the available video tracks for the video. Future getVideoTracks() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getVideoTracks$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getVideoTracks$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; @@ -789,13 +826,17 @@ class VideoPlayerInstanceApi { /// Selects a video track by setting preferredPeakBitRate. /// Pass 0 to enable auto quality selection. Future selectVideoTrack(int bitrate) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectVideoTrack$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectVideoTrack$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [bitrate], ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([bitrate]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index 11949fc4865..e9109cd0df8 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -710,111 +710,122 @@ void main() { playerId: 1, textureId: 101, ); - when(api.getVideoTracks()).thenAnswer((_) async => NativeVideoTrackData()); + when( + api.getVideoTracks(), + ).thenAnswer((_) async => NativeVideoTrackData()); final List tracks = await player.getVideoTracks(1); expect(tracks, isEmpty); }); - test('getVideoTracks converts HLS variant tracks to VideoTrack', () async { - final ( - AVFoundationVideoPlayer player, - _, - MockVideoPlayerInstanceApi api, - ) = setUpMockPlayer( - playerId: 1, - textureId: 101, - ); - when(api.getVideoTracks()).thenAnswer( - (_) async => NativeVideoTrackData( - mediaSelectionTracks: [ - MediaSelectionVideoTrackData( - variantIndex: 0, - label: '1080p', - isSelected: true, - bitrate: 5000000, - width: 1920, - height: 1080, - frameRate: 30.0, - codec: 'avc1', - ), - MediaSelectionVideoTrackData( - variantIndex: 1, - label: '720p', - isSelected: false, - bitrate: 2500000, - width: 1280, - height: 720, - frameRate: 30.0, - codec: 'avc1', - ), - ], - ), - ); - - final List tracks = await player.getVideoTracks(1); - - expect(tracks.length, 2); - - expect(tracks[0].id, 'variant_5000000'); - expect(tracks[0].label, '1080p'); - expect(tracks[0].isSelected, true); - expect(tracks[0].bitrate, 5000000); - expect(tracks[0].width, 1920); - expect(tracks[0].height, 1080); - expect(tracks[0].frameRate, 30.0); - expect(tracks[0].codec, 'avc1'); - - expect(tracks[1].id, 'variant_2500000'); - expect(tracks[1].label, '720p'); - expect(tracks[1].isSelected, false); - }); - - test('getVideoTracks generates label from resolution if not provided', () async { - final ( - AVFoundationVideoPlayer player, - _, - MockVideoPlayerInstanceApi api, - ) = setUpMockPlayer( - playerId: 1, - textureId: 101, - ); - when(api.getVideoTracks()).thenAnswer( - (_) async => NativeVideoTrackData( - mediaSelectionTracks: [ - MediaSelectionVideoTrackData( - variantIndex: 0, - isSelected: true, - bitrate: 5000000, - width: 1920, - height: 1080, - ), - ], - ), - ); + test( + 'getVideoTracks converts HLS variant tracks to VideoTrack', + () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when(api.getVideoTracks()).thenAnswer( + (_) async => NativeVideoTrackData( + mediaSelectionTracks: [ + MediaSelectionVideoTrackData( + variantIndex: 0, + label: '1080p', + isSelected: true, + bitrate: 5000000, + width: 1920, + height: 1080, + frameRate: 30.0, + codec: 'avc1', + ), + MediaSelectionVideoTrackData( + variantIndex: 1, + label: '720p', + isSelected: false, + bitrate: 2500000, + width: 1280, + height: 720, + frameRate: 30.0, + codec: 'avc1', + ), + ], + ), + ); + + final List tracks = await player.getVideoTracks(1); + + expect(tracks.length, 2); + + expect(tracks[0].id, 'variant_5000000'); + expect(tracks[0].label, '1080p'); + expect(tracks[0].isSelected, true); + expect(tracks[0].bitrate, 5000000); + expect(tracks[0].width, 1920); + expect(tracks[0].height, 1080); + expect(tracks[0].frameRate, 30.0); + expect(tracks[0].codec, 'avc1'); + + expect(tracks[1].id, 'variant_2500000'); + expect(tracks[1].label, '720p'); + expect(tracks[1].isSelected, false); + }, + ); + + test( + 'getVideoTracks generates label from resolution if not provided', + () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when(api.getVideoTracks()).thenAnswer( + (_) async => NativeVideoTrackData( + mediaSelectionTracks: [ + MediaSelectionVideoTrackData( + variantIndex: 0, + isSelected: true, + bitrate: 5000000, + width: 1920, + height: 1080, + ), + ], + ), + ); - final List tracks = await player.getVideoTracks(1); + final List tracks = await player.getVideoTracks(1); - expect(tracks.length, 1); - expect(tracks[0].label, '1080p'); - }); + expect(tracks.length, 1); + expect(tracks[0].label, '1080p'); + }, + ); - test('selectVideoTrack with null sets auto quality (bitrate 0)', () async { - final ( - AVFoundationVideoPlayer player, - _, - MockVideoPlayerInstanceApi api, - ) = setUpMockPlayer( - playerId: 1, - textureId: 101, - ); - when(api.selectVideoTrack(0)).thenAnswer((_) async {}); + test( + 'selectVideoTrack with null sets auto quality (bitrate 0)', + () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when(api.selectVideoTrack(0)).thenAnswer((_) async {}); - await player.selectVideoTrack(1, null); + await player.selectVideoTrack(1, null); - verify(api.selectVideoTrack(0)); - }); + verify(api.selectVideoTrack(0)); + }, + ); test('selectVideoTrack with track uses bitrate', () async { final ( @@ -847,10 +858,7 @@ void main() { textureId: 101, ); - const track = VideoTrack( - id: 'asset_123', - isSelected: false, - ); + const track = VideoTrack(id: 'asset_123', isSelected: false); await player.selectVideoTrack(1, track); verifyNever(api.selectVideoTrack(any)); diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 7cae85fbc00..ca75be5bb8a 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -119,7 +119,9 @@ abstract class VideoPlayerPlatform extends PlatformInterface { /// Sets whether the video should continue to play in the background. Future setAllowBackgroundPlayback(bool allowBackgroundPlayback) { - throw UnimplementedError('setAllowBackgroundPlayback() has not been implemented.'); + throw UnimplementedError( + 'setAllowBackgroundPlayback() has not been implemented.', + ); } /// Sets additional options on web. @@ -355,8 +357,14 @@ class VideoEvent { } @override - int get hashCode => - Object.hash(eventType, duration, size, rotationCorrection, buffered, isPlaying); + int get hashCode => Object.hash( + eventType, + duration, + size, + rotationCorrection, + buffered, + isPlaying, + ); } /// Type of the event. @@ -582,7 +590,10 @@ class VideoViewOptions { @immutable class VideoCreationOptions { /// Constructs an instance of [VideoCreationOptions]. - const VideoCreationOptions({required this.dataSource, required this.viewType}); + const VideoCreationOptions({ + required this.dataSource, + required this.viewType, + }); /// The data source used to create the player. final DataSource dataSource; @@ -757,8 +768,16 @@ class VideoTrack { } @override - int get hashCode => - Object.hash(id, isSelected, label, bitrate, width, height, frameRate, codec); + int get hashCode => Object.hash( + id, + isSelected, + label, + bitrate, + width, + height, + frameRate, + codec, + ); @override String toString() => diff --git a/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart b/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart index 454576cf166..74349b1d491 100644 --- a/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart +++ b/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart @@ -112,10 +112,7 @@ void main() { label: '1080p', bitrate: 5000000, ); - const track3 = VideoTrack( - id: 'track_2', - isSelected: false, - ); + const track3 = VideoTrack(id: 'track_2', isSelected: false); expect(track1, equals(track2)); expect(track1, isNot(equals(track3))); diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart index b45b3770630..3423666082c 100644 --- a/packages/video_player/video_player_web/lib/video_player_web.dart +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -102,7 +102,8 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { (int viewId) => videoElement, ); - final player = VideoPlayer(videoElement: videoElement)..initialize(src: uri); + final player = VideoPlayer(videoElement: videoElement) + ..initialize(src: uri); _videoPlayers[playerId] = player; From 6980c1346ebae254bcf75b9943abc811791e9a6f Mon Sep 17 00:00:00 2001 From: nateshmbhat Date: Sat, 27 Dec 2025 21:46:58 +0530 Subject: [PATCH 4/7] fixed analysis errors --- .../example/lib/video_tracks_demo.dart | 39 +- .../video_player/test/video_player_test.dart | 389 ++++++------------ .../lib/src/android_video_player.dart | 34 +- .../lib/src/avfoundation_video_player.dart | 34 +- 4 files changed, 160 insertions(+), 336 deletions(-) diff --git a/packages/video_player/video_player/example/lib/video_tracks_demo.dart b/packages/video_player/video_player/example/lib/video_tracks_demo.dart index fe59fa176d0..6a897c79e96 100644 --- a/packages/video_player/video_player/example/lib/video_tracks_demo.dart +++ b/packages/video_player/video_player/example/lib/video_tracks_demo.dart @@ -50,7 +50,7 @@ class _VideoTracksDemoState extends State { try { await _controller?.dispose(); - final VideoPlayerController controller = VideoPlayerController.networkUrl( + final controller = VideoPlayerController.networkUrl( Uri.parse(_sampleVideos[_selectedVideoIndex]), ); _controller = controller; @@ -138,19 +138,17 @@ class _VideoTracksDemoState extends State { if (!mounted) { return; } - final String message = track == null + final message = track == null ? 'Switched to automatic quality' : 'Selected video track: ${_getTrackLabel(track)}'; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); } catch (e) { if (!mounted) { return; } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to select video track: $e')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Failed to select video track: $e'))); } } @@ -186,7 +184,7 @@ class _VideoTracksDemoState extends State { } final VideoPlayerValue currentValue = _controller!.value; - bool shouldUpdate = false; + var shouldUpdate = false; // Check for relevant state changes that affect UI if (currentValue.isPlaying != _wasPlaying) { @@ -230,11 +228,9 @@ class _VideoTracksDemoState extends State { inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), ), - dropdownMenuEntries: _sampleVideos.indexed.map(( - (int, String) record, - ) { - final (int index, String url) = record; - final String label = url.contains('.m3u8') + dropdownMenuEntries: _sampleVideos.indexed.map(((int, String) record) { + final (index, url) = record; + final label = url.contains('.m3u8') ? 'HLS Stream ${index + 1}' : 'MP4 Video ${index + 1}'; return DropdownMenuEntry(value: index, label: label); @@ -289,10 +285,7 @@ class _VideoTracksDemoState extends State { ), ), const SizedBox(height: 16), - ElevatedButton( - onPressed: _initializeVideo, - child: const Text('Retry'), - ), + ElevatedButton(onPressed: _initializeVideo, child: const Text('Retry')), ], ), ); @@ -381,9 +374,7 @@ class _VideoTracksDemoState extends State { title: Text( 'Automatic Quality', style: TextStyle( - fontWeight: _isAutoQuality - ? FontWeight.bold - : FontWeight.normal, + fontWeight: _isAutoQuality ? FontWeight.bold : FontWeight.normal, ), ), subtitle: const Text('Let the player choose the best quality'), @@ -406,7 +397,7 @@ class _VideoTracksDemoState extends State { ), ), ) - else if (_error != null && _controller?.value.isInitialized == true) + else if (_error != null && (_controller?.value.isInitialized ?? false)) Expanded( child: Center( child: Text( @@ -443,9 +434,7 @@ class _VideoTracksDemoState extends State { ), title: Text( _getTrackLabel(track), - style: TextStyle( - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - ), + style: TextStyle(fontWeight: isSelected ? FontWeight.bold : FontWeight.normal), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 1d9c91232f8..1c60063d2c9 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -85,9 +85,7 @@ class FakeController extends ValueNotifier void setCaptionOffset(Duration delay) {} @override - Future setClosedCaptionFile( - Future? closedCaptionFile, - ) async {} + Future setClosedCaptionFile(Future? closedCaptionFile) async {} @override Future> getVideoTracks() async => []; @@ -99,8 +97,7 @@ class FakeController extends ValueNotifier bool isVideoTrackSupportAvailable() => false; } -Future _loadClosedCaption() async => - _FakeClosedCaptionFile(); +Future _loadClosedCaption() async => _FakeClosedCaptionFile(); class _FakeClosedCaptionFile extends ClosedCaptionFile { @override @@ -135,13 +132,9 @@ void main() { required bool shouldPlayInBackground, }) { expect(controller.value.isPlaying, true); - WidgetsBinding.instance.handleAppLifecycleStateChanged( - AppLifecycleState.paused, - ); + WidgetsBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.paused); expect(controller.value.isPlaying, shouldPlayInBackground); - WidgetsBinding.instance.handleAppLifecycleStateChanged( - AppLifecycleState.resumed, - ); + WidgetsBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.resumed); expect(controller.value.isPlaying, true); } @@ -185,38 +178,37 @@ void main() { ); }); - testWidgets( - 'VideoPlayer still listens for controller changes when reparented', - (WidgetTester tester) async { - final controller = FakeController(); - addTearDown(controller.dispose); - final GlobalKey videoKey = GlobalKey(); - final Widget videoPlayer = KeyedSubtree( - key: videoKey, - child: VideoPlayer(controller), - ); + testWidgets('VideoPlayer still listens for controller changes when reparented', ( + WidgetTester tester, + ) async { + final controller = FakeController(); + addTearDown(controller.dispose); + final GlobalKey videoKey = GlobalKey(); + final Widget videoPlayer = KeyedSubtree( + key: videoKey, + child: VideoPlayer(controller), + ); - await tester.pumpWidget(videoPlayer); - expect(find.byType(Texture), findsNothing); + await tester.pumpWidget(videoPlayer); + expect(find.byType(Texture), findsNothing); - // The VideoPlayer is reparented in the widget tree, before the - // underlying player is initialized. - await tester.pumpWidget(SizedBox(child: videoPlayer)); - controller.playerId = 321; - controller.value = controller.value.copyWith( - duration: const Duration(milliseconds: 100), - isInitialized: true, - ); + // The VideoPlayer is reparented in the widget tree, before the + // underlying player is initialized. + await tester.pumpWidget(SizedBox(child: videoPlayer)); + controller.playerId = 321; + controller.value = controller.value.copyWith( + duration: const Duration(milliseconds: 100), + isInitialized: true, + ); - await tester.pump(); - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Texture && widget.textureId == 321, - ), - findsOneWidget, - ); - }, - ); + await tester.pump(); + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Texture && widget.textureId == 321, + ), + findsOneWidget, + ); + }); testWidgets( 'VideoProgressIndicator still listens for controller changes after reparenting', @@ -237,9 +229,7 @@ void main() { ); await tester.pumpWidget(MaterialApp(home: progressIndicator)); await tester.pump(); - await tester.pumpWidget( - MaterialApp(home: SizedBox(child: progressIndicator)), - ); + await tester.pumpWidget(MaterialApp(home: SizedBox(child: progressIndicator))); expect((key.currentContext! as Element).dirty, isFalse); // Verify that changing value dirties the widget tree. controller.value = controller.value.copyWith( @@ -259,16 +249,12 @@ void main() { isInitialized: true, ); await tester.pumpWidget( - MaterialApp( - home: VideoProgressIndicator(controller, allowScrubbing: false), - ), + MaterialApp(home: VideoProgressIndicator(controller, allowScrubbing: false)), ); expect(tester.takeException(), isNull); }); - testWidgets('non-zero rotationCorrection value is used', ( - WidgetTester tester, - ) async { + testWidgets('non-zero rotationCorrection value is used', (WidgetTester tester) async { final controller = FakeController.value( const VideoPlayerValue(duration: Duration.zero, rotationCorrection: 180), ); @@ -296,9 +282,7 @@ void main() { group('ClosedCaption widget', () { testWidgets('uses a default text style', (WidgetTester tester) async { const text = 'foo'; - await tester.pumpWidget( - const MaterialApp(home: ClosedCaption(text: text)), - ); + await tester.pumpWidget(const MaterialApp(home: ClosedCaption(text: text))); final Text textWidget = tester.widget(find.text(text)); expect(textWidget.style!.fontSize, 36.0); @@ -329,9 +313,7 @@ void main() { expect(find.byType(Text), findsNothing); }); - testWidgets('Passes text contrast ratio guidelines', ( - WidgetTester tester, - ) async { + testWidgets('Passes text contrast ratio guidelines', (WidgetTester tester) async { const text = 'foo'; await tester.pumpWidget( const MaterialApp( @@ -355,10 +337,7 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {}, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); }); test('network with hint', () async { @@ -369,14 +348,8 @@ void main() { await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); - expect( - fakeVideoPlayerPlatform.dataSources[0].formatHint, - VideoFormat.dash, - ); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {}, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, VideoFormat.dash); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); }); test('network with some headers', () async { @@ -388,10 +361,9 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {'Authorization': 'Bearer token'}, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, { + 'Authorization': 'Bearer token', + }); }); }); @@ -403,10 +375,7 @@ void main() { addTearDown(controller.dispose); await controller.initialize(); await controller.play(); - verifyPlayStateRespondsToLifecycle( - controller, - shouldPlayInBackground: false, - ); + verifyPlayStateRespondsToLifecycle(controller, shouldPlayInBackground: false); }); test('asset', () async { @@ -426,10 +395,7 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {}, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); }); test('network url with hint', () async { @@ -441,14 +407,8 @@ void main() { await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); - expect( - fakeVideoPlayerPlatform.dataSources[0].formatHint, - VideoFormat.dash, - ); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {}, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, VideoFormat.dash); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); }); test('network url with some headers', () async { @@ -461,10 +421,9 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {'Authorization': 'Bearer token'}, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, { + 'Authorization': 'Bearer token', + }); }); test( @@ -492,64 +451,40 @@ void main() { expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); }, skip: kIsWeb /* Web does not support file assets. */); - test( - 'file with special characters', - () async { - final controller = VideoPlayerController.file(File('A #1 Hit.avi')); - await controller.initialize(); + test('file with special characters', () async { + final controller = VideoPlayerController.file(File('A #1 Hit.avi')); + await controller.initialize(); - final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; - expect( - uri.startsWith('file:///'), - true, - reason: 'Actual string: $uri', - ); - expect( - uri.endsWith('/A%20%231%20Hit.avi'), - true, - reason: 'Actual string: $uri', - ); - }, - skip: kIsWeb /* Web does not support file assets. */, - ); + final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; + expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); + expect(uri.endsWith('/A%20%231%20Hit.avi'), true, reason: 'Actual string: $uri'); + }, skip: kIsWeb /* Web does not support file assets. */); - test( - 'file with headers (m3u8)', - () async { - final controller = VideoPlayerController.file( - File('a.avi'), - httpHeaders: {'Authorization': 'Bearer token'}, - ); - await controller.initialize(); + test('file with headers (m3u8)', () async { + final controller = VideoPlayerController.file( + File('a.avi'), + httpHeaders: {'Authorization': 'Bearer token'}, + ); + await controller.initialize(); - final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; - expect( - uri.startsWith('file:///'), - true, - reason: 'Actual string: $uri', - ); - expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); + final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; + expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); + expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); - expect( - fakeVideoPlayerPlatform.dataSources[0].httpHeaders, - {'Authorization': 'Bearer token'}, - ); - }, - skip: kIsWeb /* Web does not support file assets. */, - ); + expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, { + 'Authorization': 'Bearer token', + }); + }, skip: kIsWeb /* Web does not support file assets. */); - test( - 'successful initialize on controller with error clears error', - () async { - final controller = VideoPlayerController.network('https://127.0.0.1'); - fakeVideoPlayerPlatform.forceInitError = true; - await controller.initialize().catchError((dynamic e) {}); - expect(controller.value.hasError, equals(true)); - fakeVideoPlayerPlatform.forceInitError = false; - await controller.initialize(); - expect(controller.value.hasError, equals(false)); - }, - ); + test('successful initialize on controller with error clears error', () async { + final controller = VideoPlayerController.network('https://127.0.0.1'); + fakeVideoPlayerPlatform.forceInitError = true; + await controller.initialize().catchError((dynamic e) {}); + expect(controller.value.hasError, equals(true)); + fakeVideoPlayerPlatform.forceInitError = false; + await controller.initialize(); + expect(controller.value.hasError, equals(false)); + }); test( 'given controller with error when initialization succeeds it should clear error', @@ -568,9 +503,7 @@ void main() { }); test('contentUri', () async { - final controller = VideoPlayerController.contentUri( - Uri.parse('content://video'), - ); + final controller = VideoPlayerController.contentUri(Uri.parse('content://video')); await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'content://video'); @@ -601,9 +534,7 @@ void main() { }); test('play', () async { - final controller = VideoPlayerController.networkUrl( - Uri.parse('https://127.0.0.1'), - ); + final controller = VideoPlayerController.networkUrl(Uri.parse('https://127.0.0.1')); addTearDown(controller.dispose); await controller.initialize(); @@ -768,22 +699,14 @@ void main() { }); group('scrubbing', () { - testWidgets('restarts on release if already playing', ( - WidgetTester tester, - ) async { + testWidgets('restarts on release if already playing', (WidgetTester tester) async { final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - final progressWidget = VideoProgressIndicator( - controller, - allowScrubbing: true, - ); + final progressWidget = VideoProgressIndicator(controller, allowScrubbing: true); await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: progressWidget, - ), + Directionality(textDirection: TextDirection.ltr, child: progressWidget), ); await controller.play(); @@ -800,22 +723,14 @@ void main() { await tester.runAsync(controller.dispose); }); - testWidgets('does not restart when dragging to end', ( - WidgetTester tester, - ) async { + testWidgets('does not restart when dragging to end', (WidgetTester tester) async { final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - final progressWidget = VideoProgressIndicator( - controller, - allowScrubbing: true, - ); + final progressWidget = VideoProgressIndicator(controller, allowScrubbing: true); await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: progressWidget, - ), + Directionality(textDirection: TextDirection.ltr, child: progressWidget), ); await controller.play(); @@ -1026,9 +941,7 @@ void main() { final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.playerId]!; - fakeVideoEventStream.add( - VideoEvent(eventType: VideoEventType.completed), - ); + fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed)); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); @@ -1044,19 +957,13 @@ void main() { fakeVideoPlayerPlatform.streams[controller.playerId]!; fakeVideoEventStream.add( - VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: true, - ), + VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: true), ); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isTrue); fakeVideoEventStream.add( - VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: false, - ), + VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: false), ); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); @@ -1072,9 +979,7 @@ void main() { final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.playerId]!; - fakeVideoEventStream.add( - VideoEvent(eventType: VideoEventType.bufferingStart), - ); + fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.bufferingStart)); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isTrue); @@ -1094,9 +999,7 @@ void main() { DurationRange(bufferStart, bufferEnd).toString(), ); - fakeVideoEventStream.add( - VideoEvent(eventType: VideoEventType.bufferingEnd), - ); + fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.bufferingEnd)); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isFalse); await tester.runAsync(controller.dispose); @@ -1276,17 +1179,13 @@ void main() { }); test('errorDescription is changed when copy with another error', () { const original = VideoPlayerValue.erroneous('error'); - final VideoPlayerValue copy = original.copyWith( - errorDescription: 'new error', - ); + final VideoPlayerValue copy = original.copyWith(errorDescription: 'new error'); expect(copy.errorDescription, 'new error'); }); test('errorDescription is changed when copy with error', () { const original = VideoPlayerValue.uninitialized(); - final VideoPlayerValue copy = original.copyWith( - errorDescription: 'new error', - ); + final VideoPlayerValue copy = original.copyWith(errorDescription: 'new error'); expect(copy.errorDescription, 'new error'); }); @@ -1360,10 +1259,7 @@ void main() { await controller.initialize(); await controller.play(); - verifyPlayStateRespondsToLifecycle( - controller, - shouldPlayInBackground: true, - ); + verifyPlayStateRespondsToLifecycle(controller, shouldPlayInBackground: true); }); test('false allowBackgroundPlayback pauses playback', () async { @@ -1375,10 +1271,7 @@ void main() { await controller.initialize(); await controller.play(); - verifyPlayStateRespondsToLifecycle( - controller, - shouldPlayInBackground: false, - ); + verifyPlayStateRespondsToLifecycle(controller, shouldPlayInBackground: false); }); }); @@ -1451,10 +1344,7 @@ void main() { isCompletedTest(); if (!hasLooped) { fakeVideoEventStream.add( - VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: true, - ), + VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: true), ); hasLooped = !hasLooped; } @@ -1480,9 +1370,7 @@ void main() { final void Function() isCompletedTest = expectAsync0(() {}); - controller.value = controller.value.copyWith( - duration: const Duration(seconds: 10), - ); + controller.value = controller.value.copyWith(duration: const Duration(seconds: 10)); controller.addListener(() async { if (currentIsCompleted != controller.value.isCompleted) { @@ -1511,7 +1399,7 @@ void main() { test('getVideoTracks returns empty list when not initialized', () async { final controller = VideoPlayerController.networkUrl(_localhostUri); - final tracks = await controller.getVideoTracks(); + final List tracks = await controller.getVideoTracks(); expect(tracks, isEmpty); }); @@ -1520,29 +1408,27 @@ void main() { final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - fakeVideoPlayerPlatform.setVideoTracksForPlayer( - controller.playerId, - [ - const platform_interface.VideoTrack( - id: '0_0', - isSelected: true, - label: '1080p', - bitrate: 5000000, - width: 1920, - height: 1080, - ), - const platform_interface.VideoTrack( - id: '0_1', - isSelected: false, - label: '720p', - bitrate: 2500000, - width: 1280, - height: 720, - ), - ], - ); - - final tracks = await controller.getVideoTracks(); + fakeVideoPlayerPlatform + .setVideoTracksForPlayer(controller.playerId, [ + const platform_interface.VideoTrack( + id: '0_0', + isSelected: true, + label: '1080p', + bitrate: 5000000, + width: 1920, + height: 1080, + ), + const platform_interface.VideoTrack( + id: '0_1', + isSelected: false, + label: '720p', + bitrate: 2500000, + width: 1280, + height: 720, + ), + ]); + + final List tracks = await controller.getVideoTracks(); expect(tracks.length, 2); expect(tracks[0].id, '0_0'); @@ -1583,10 +1469,7 @@ void main() { await controller.selectVideoTrack(null); - expect( - fakeVideoPlayerPlatform.calls, - isNot(contains('selectVideoTrack')), - ); + expect(fakeVideoPlayerPlatform.calls, isNot(contains('selectVideoTrack'))); }); }); } @@ -1601,8 +1484,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { bool forceInitError = false; int nextPlayerId = 0; final Map _positions = {}; - final Map webOptions = - {}; + final Map webOptions = {}; @override Future create(DataSource dataSource) async { @@ -1611,10 +1493,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { streams[nextPlayerId] = stream; if (forceInitError) { stream.addError( - PlatformException( - code: 'VideoError', - message: 'Video player had error XYZ', - ), + PlatformException(code: 'VideoError', message: 'Video player had error XYZ'), ); } else { stream.add( @@ -1636,10 +1515,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { streams[nextPlayerId] = stream; if (forceInitError) { stream.addError( - PlatformException( - code: 'VideoError', - message: 'Video player had error XYZ', - ), + PlatformException(code: 'VideoError', message: 'Video player had error XYZ'), ); } else { stream.add( @@ -1719,10 +1595,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { } @override - Future setWebOptions( - int playerId, - VideoPlayerWebOptions options, - ) async { + Future setWebOptions(int playerId, VideoPlayerWebOptions options) async { if (!kIsWeb) { throw UnimplementedError('setWebOptions() is only available in the web.'); } @@ -1733,19 +1606,12 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { // Video track selection support final Map> _videoTracks = >{}; - platform_interface.VideoTrack? _selectedVideoTrack; - - void setVideoTracksForPlayer( - int playerId, - List tracks, - ) { + void setVideoTracksForPlayer(int playerId, List tracks) { _videoTracks[playerId] = tracks; } @override - Future> getVideoTracks( - int playerId, - ) async { + Future> getVideoTracks(int playerId) async { calls.add('getVideoTracks'); return _videoTracks[playerId] ?? []; } @@ -1756,7 +1622,6 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { platform_interface.VideoTrack? track, ) async { calls.add('selectVideoTrack'); - _selectedVideoTrack = track; } @override diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index d8331267a1a..802272c13e3 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -17,9 +17,7 @@ VideoPlayerInstanceApi _productionApiProvider(int playerId) { } /// The non-test implementation of `_videoEventStreamProvider`. -Stream _productionVideoEventStreamProvider( - String streamIdentifier, -) { +Stream _productionVideoEventStreamProvider(String streamIdentifier) { return pigeon.videoEvents(instanceName: streamIdentifier); } @@ -29,8 +27,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { /// Creates a new Android video player implementation instance. AndroidVideoPlayer({ @visibleForTesting AndroidVideoPlayerApi? pluginApi, - @visibleForTesting - VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, + @visibleForTesting VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, Stream Function(String streamIdentifier)? videoEventStreamProvider, }) : _api = pluginApi ?? AndroidVideoPlayerApi(), @@ -90,14 +87,9 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { case DataSourceType.asset: final String? asset = dataSource.asset; if (asset == null) { - throw ArgumentError( - '"asset" must be non-null for an asset data source', - ); + throw ArgumentError('"asset" must be non-null for an asset data source'); } - final String key = await _api.getLookupKeyForAsset( - asset, - dataSource.package, - ); + final String key = await _api.getLookupKeyForAsset(asset, dataSource.package); uri = 'asset:///$key'; case DataSourceType.network: uri = dataSource.uri; @@ -213,9 +205,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { final VideoPlayerViewState viewState = _playerWith(id: playerId).viewState; return switch (viewState) { - VideoPlayerTextureViewState(:final int textureId) => Texture( - textureId: textureId, - ), + VideoPlayerTextureViewState(:final int textureId) => Texture(textureId: textureId), VideoPlayerPlatformViewState() => PlatformViewPlayer(playerId: playerId), }; } @@ -277,13 +267,11 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { if (nativeData.exoPlayerTracks != null) { for (final ExoPlayerVideoTrackData track in nativeData.exoPlayerTracks!) { // Construct a string ID from groupIndex and trackIndex for compatibility - final String trackId = '${track.groupIndex}_${track.trackIndex}'; + final trackId = '${track.groupIndex}_${track.trackIndex}'; // Generate label from resolution if not provided final String? label = track.label ?? - (track.width != null && track.height != null - ? '${track.height}p' - : null); + (track.width != null && track.height != null ? '${track.height}p' : null); tracks.add( VideoTrack( id: trackId, @@ -318,9 +306,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { return player ?? (throw StateError('No active player with ID $id.')); } - PlatformVideoFormat? _platformVideoFormatFromVideoFormat( - VideoFormat? format, - ) { + PlatformVideoFormat? _platformVideoFormatFromVideoFormat(VideoFormat? format) { return switch (format) { VideoFormat.dash => PlatformVideoFormat.dash, VideoFormat.hls => PlatformVideoFormat.hls, @@ -574,9 +560,7 @@ class _PlayerInstance { // should be synchronous with the state change. break; case PlatformPlaybackState.ended: - _eventStreamController.add( - VideoEvent(eventType: VideoEventType.completed), - ); + _eventStreamController.add(VideoEvent(eventType: VideoEventType.completed)); case PlatformPlaybackState.unknown: // Ignore unknown states. This isn't an error since the media // framework could add new states in the future. diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index 230aa82cbda..54ef598a51d 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -21,8 +21,7 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { /// Creates a new AVFoundation-based video player implementation instance. AVFoundationVideoPlayer({ @visibleForTesting AVFoundationVideoPlayerApi? pluginApi, - @visibleForTesting - VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, + @visibleForTesting VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, }) : _api = pluginApi ?? AVFoundationVideoPlayerApi(), _playerApiProvider = playerApiProvider ?? _productionApiProvider; @@ -71,9 +70,7 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { case DataSourceType.asset: final String? asset = dataSource.asset; if (asset == null) { - throw ArgumentError( - '"asset" must be non-null for an asset data source', - ); + throw ArgumentError('"asset" must be non-null for an asset data source'); } uri = await _api.getAssetUrl(asset, dataSource.package); if (uri == null) { @@ -187,16 +184,13 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { // Convert HLS variant tracks (iOS 15+) if (nativeData.mediaSelectionTracks != null) { - for (final MediaSelectionVideoTrackData track - in nativeData.mediaSelectionTracks!) { + for (final MediaSelectionVideoTrackData track in nativeData.mediaSelectionTracks!) { // Use bitrate as the track ID for HLS variants - final String trackId = 'variant_${track.bitrate ?? track.variantIndex}'; + final trackId = 'variant_${track.bitrate ?? track.variantIndex}'; // Generate label from resolution if not provided final String? label = track.label ?? - (track.width != null && track.height != null - ? '${track.height}p' - : null); + (track.width != null && track.height != null ? '${track.height}p' : null); tracks.add( VideoTrack( id: trackId, @@ -215,19 +209,16 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { // Convert asset tracks (for regular videos) if (nativeData.assetTracks != null) { for (final AssetVideoTrackData track in nativeData.assetTracks!) { - final String trackId = 'asset_${track.trackId}'; + final trackId = 'asset_${track.trackId}'; // Generate label from resolution if not provided final String? label = track.label ?? - (track.width != null && track.height != null - ? '${track.height}p' - : null); + (track.width != null && track.height != null ? '${track.height}p' : null); tracks.add( VideoTrack( id: trackId, isSelected: track.isSelected, label: label, - bitrate: null, width: track.width, height: track.height, frameRate: track.frameRate, @@ -275,9 +266,7 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { final VideoPlayerViewState viewState = _playerWith(id: playerId).viewState; return switch (viewState) { - VideoPlayerTextureViewState(:final int textureId) => Texture( - textureId: textureId, - ), + VideoPlayerTextureViewState(:final int textureId) => Texture(textureId: textureId), VideoPlayerPlatformViewState() => _buildPlatformView(playerId), }; } @@ -304,11 +293,8 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { /// An instance of a video player, corresponding to a single player ID in /// [AVFoundationVideoPlayer]. class _PlayerInstance { - _PlayerInstance( - this._api, - this.viewState, { - required EventChannel eventChannel, - }) : _eventChannel = eventChannel; + _PlayerInstance(this._api, this.viewState, {required EventChannel eventChannel}) + : _eventChannel = eventChannel; final VideoPlayerInstanceApi _api; final VideoPlayerViewState viewState; From 57f9b9ace5640769f5ca8816a9fc575e52b9b6e5 Mon Sep 17 00:00:00 2001 From: nateshmbhat Date: Wed, 7 Jan 2026 21:39:25 +0530 Subject: [PATCH 5/7] formatted all dart files --- .../video_player/example/lib/main.dart | 1 - .../example/lib/video_tracks_demo.dart | 30 +- .../video_player/test/video_player_test.dart | 380 ++++++++++++------ .../lib/src/android_video_player.dart | 32 +- .../lib/src/avfoundation_video_player.dart | 29 +- 5 files changed, 322 insertions(+), 150 deletions(-) diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index 5b08c984d24..27557ada553 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -10,7 +10,6 @@ library; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; - import 'video_tracks_demo.dart'; void main() { diff --git a/packages/video_player/video_player/example/lib/video_tracks_demo.dart b/packages/video_player/video_player/example/lib/video_tracks_demo.dart index 6a897c79e96..4d7d3df3d2a 100644 --- a/packages/video_player/video_player/example/lib/video_tracks_demo.dart +++ b/packages/video_player/video_player/example/lib/video_tracks_demo.dart @@ -141,14 +141,16 @@ class _VideoTracksDemoState extends State { final message = track == null ? 'Switched to automatic quality' : 'Selected video track: ${_getTrackLabel(track)}'; - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); } catch (e) { if (!mounted) { return; } - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Failed to select video track: $e'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to select video track: $e')), + ); } } @@ -228,7 +230,9 @@ class _VideoTracksDemoState extends State { inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), ), - dropdownMenuEntries: _sampleVideos.indexed.map(((int, String) record) { + dropdownMenuEntries: _sampleVideos.indexed.map(( + (int, String) record, + ) { final (index, url) = record; final label = url.contains('.m3u8') ? 'HLS Stream ${index + 1}' @@ -285,7 +289,10 @@ class _VideoTracksDemoState extends State { ), ), const SizedBox(height: 16), - ElevatedButton(onPressed: _initializeVideo, child: const Text('Retry')), + ElevatedButton( + onPressed: _initializeVideo, + child: const Text('Retry'), + ), ], ), ); @@ -374,7 +381,9 @@ class _VideoTracksDemoState extends State { title: Text( 'Automatic Quality', style: TextStyle( - fontWeight: _isAutoQuality ? FontWeight.bold : FontWeight.normal, + fontWeight: _isAutoQuality + ? FontWeight.bold + : FontWeight.normal, ), ), subtitle: const Text('Let the player choose the best quality'), @@ -397,7 +406,8 @@ class _VideoTracksDemoState extends State { ), ), ) - else if (_error != null && (_controller?.value.isInitialized ?? false)) + else if (_error != null && + (_controller?.value.isInitialized ?? false)) Expanded( child: Center( child: Text( @@ -434,7 +444,9 @@ class _VideoTracksDemoState extends State { ), title: Text( _getTrackLabel(track), - style: TextStyle(fontWeight: isSelected ? FontWeight.bold : FontWeight.normal), + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 1c60063d2c9..7f4d242bf8b 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -85,7 +85,9 @@ class FakeController extends ValueNotifier void setCaptionOffset(Duration delay) {} @override - Future setClosedCaptionFile(Future? closedCaptionFile) async {} + Future setClosedCaptionFile( + Future? closedCaptionFile, + ) async {} @override Future> getVideoTracks() async => []; @@ -97,7 +99,8 @@ class FakeController extends ValueNotifier bool isVideoTrackSupportAvailable() => false; } -Future _loadClosedCaption() async => _FakeClosedCaptionFile(); +Future _loadClosedCaption() async => + _FakeClosedCaptionFile(); class _FakeClosedCaptionFile extends ClosedCaptionFile { @override @@ -132,9 +135,13 @@ void main() { required bool shouldPlayInBackground, }) { expect(controller.value.isPlaying, true); - WidgetsBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.paused); + WidgetsBinding.instance.handleAppLifecycleStateChanged( + AppLifecycleState.paused, + ); expect(controller.value.isPlaying, shouldPlayInBackground); - WidgetsBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + WidgetsBinding.instance.handleAppLifecycleStateChanged( + AppLifecycleState.resumed, + ); expect(controller.value.isPlaying, true); } @@ -178,37 +185,38 @@ void main() { ); }); - testWidgets('VideoPlayer still listens for controller changes when reparented', ( - WidgetTester tester, - ) async { - final controller = FakeController(); - addTearDown(controller.dispose); - final GlobalKey videoKey = GlobalKey(); - final Widget videoPlayer = KeyedSubtree( - key: videoKey, - child: VideoPlayer(controller), - ); + testWidgets( + 'VideoPlayer still listens for controller changes when reparented', + (WidgetTester tester) async { + final controller = FakeController(); + addTearDown(controller.dispose); + final GlobalKey videoKey = GlobalKey(); + final Widget videoPlayer = KeyedSubtree( + key: videoKey, + child: VideoPlayer(controller), + ); - await tester.pumpWidget(videoPlayer); - expect(find.byType(Texture), findsNothing); + await tester.pumpWidget(videoPlayer); + expect(find.byType(Texture), findsNothing); - // The VideoPlayer is reparented in the widget tree, before the - // underlying player is initialized. - await tester.pumpWidget(SizedBox(child: videoPlayer)); - controller.playerId = 321; - controller.value = controller.value.copyWith( - duration: const Duration(milliseconds: 100), - isInitialized: true, - ); + // The VideoPlayer is reparented in the widget tree, before the + // underlying player is initialized. + await tester.pumpWidget(SizedBox(child: videoPlayer)); + controller.playerId = 321; + controller.value = controller.value.copyWith( + duration: const Duration(milliseconds: 100), + isInitialized: true, + ); - await tester.pump(); - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Texture && widget.textureId == 321, - ), - findsOneWidget, - ); - }); + await tester.pump(); + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Texture && widget.textureId == 321, + ), + findsOneWidget, + ); + }, + ); testWidgets( 'VideoProgressIndicator still listens for controller changes after reparenting', @@ -229,7 +237,9 @@ void main() { ); await tester.pumpWidget(MaterialApp(home: progressIndicator)); await tester.pump(); - await tester.pumpWidget(MaterialApp(home: SizedBox(child: progressIndicator))); + await tester.pumpWidget( + MaterialApp(home: SizedBox(child: progressIndicator)), + ); expect((key.currentContext! as Element).dirty, isFalse); // Verify that changing value dirties the widget tree. controller.value = controller.value.copyWith( @@ -249,12 +259,16 @@ void main() { isInitialized: true, ); await tester.pumpWidget( - MaterialApp(home: VideoProgressIndicator(controller, allowScrubbing: false)), + MaterialApp( + home: VideoProgressIndicator(controller, allowScrubbing: false), + ), ); expect(tester.takeException(), isNull); }); - testWidgets('non-zero rotationCorrection value is used', (WidgetTester tester) async { + testWidgets('non-zero rotationCorrection value is used', ( + WidgetTester tester, + ) async { final controller = FakeController.value( const VideoPlayerValue(duration: Duration.zero, rotationCorrection: 180), ); @@ -282,7 +296,9 @@ void main() { group('ClosedCaption widget', () { testWidgets('uses a default text style', (WidgetTester tester) async { const text = 'foo'; - await tester.pumpWidget(const MaterialApp(home: ClosedCaption(text: text))); + await tester.pumpWidget( + const MaterialApp(home: ClosedCaption(text: text)), + ); final Text textWidget = tester.widget(find.text(text)); expect(textWidget.style!.fontSize, 36.0); @@ -313,7 +329,9 @@ void main() { expect(find.byType(Text), findsNothing); }); - testWidgets('Passes text contrast ratio guidelines', (WidgetTester tester) async { + testWidgets('Passes text contrast ratio guidelines', ( + WidgetTester tester, + ) async { const text = 'foo'; await tester.pumpWidget( const MaterialApp( @@ -337,7 +355,10 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {}, + ); }); test('network with hint', () async { @@ -348,8 +369,14 @@ void main() { await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); - expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, VideoFormat.dash); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); + expect( + fakeVideoPlayerPlatform.dataSources[0].formatHint, + VideoFormat.dash, + ); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {}, + ); }); test('network with some headers', () async { @@ -361,9 +388,10 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, { - 'Authorization': 'Bearer token', - }); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {'Authorization': 'Bearer token'}, + ); }); }); @@ -375,7 +403,10 @@ void main() { addTearDown(controller.dispose); await controller.initialize(); await controller.play(); - verifyPlayStateRespondsToLifecycle(controller, shouldPlayInBackground: false); + verifyPlayStateRespondsToLifecycle( + controller, + shouldPlayInBackground: false, + ); }); test('asset', () async { @@ -395,7 +426,10 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {}, + ); }); test('network url with hint', () async { @@ -407,8 +441,14 @@ void main() { await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); - expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, VideoFormat.dash); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}); + expect( + fakeVideoPlayerPlatform.dataSources[0].formatHint, + VideoFormat.dash, + ); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {}, + ); }); test('network url with some headers', () async { @@ -421,9 +461,10 @@ void main() { expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1'); expect(fakeVideoPlayerPlatform.dataSources[0].formatHint, null); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, { - 'Authorization': 'Bearer token', - }); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {'Authorization': 'Bearer token'}, + ); }); test( @@ -451,40 +492,64 @@ void main() { expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); }, skip: kIsWeb /* Web does not support file assets. */); - test('file with special characters', () async { - final controller = VideoPlayerController.file(File('A #1 Hit.avi')); - await controller.initialize(); + test( + 'file with special characters', + () async { + final controller = VideoPlayerController.file(File('A #1 Hit.avi')); + await controller.initialize(); - final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; - expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); - expect(uri.endsWith('/A%20%231%20Hit.avi'), true, reason: 'Actual string: $uri'); - }, skip: kIsWeb /* Web does not support file assets. */); + final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; + expect( + uri.startsWith('file:///'), + true, + reason: 'Actual string: $uri', + ); + expect( + uri.endsWith('/A%20%231%20Hit.avi'), + true, + reason: 'Actual string: $uri', + ); + }, + skip: kIsWeb /* Web does not support file assets. */, + ); - test('file with headers (m3u8)', () async { - final controller = VideoPlayerController.file( - File('a.avi'), - httpHeaders: {'Authorization': 'Bearer token'}, - ); - await controller.initialize(); + test( + 'file with headers (m3u8)', + () async { + final controller = VideoPlayerController.file( + File('a.avi'), + httpHeaders: {'Authorization': 'Bearer token'}, + ); + await controller.initialize(); - final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; - expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); - expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); + final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; + expect( + uri.startsWith('file:///'), + true, + reason: 'Actual string: $uri', + ); + expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); - expect(fakeVideoPlayerPlatform.dataSources[0].httpHeaders, { - 'Authorization': 'Bearer token', - }); - }, skip: kIsWeb /* Web does not support file assets. */); + expect( + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {'Authorization': 'Bearer token'}, + ); + }, + skip: kIsWeb /* Web does not support file assets. */, + ); - test('successful initialize on controller with error clears error', () async { - final controller = VideoPlayerController.network('https://127.0.0.1'); - fakeVideoPlayerPlatform.forceInitError = true; - await controller.initialize().catchError((dynamic e) {}); - expect(controller.value.hasError, equals(true)); - fakeVideoPlayerPlatform.forceInitError = false; - await controller.initialize(); - expect(controller.value.hasError, equals(false)); - }); + test( + 'successful initialize on controller with error clears error', + () async { + final controller = VideoPlayerController.network('https://127.0.0.1'); + fakeVideoPlayerPlatform.forceInitError = true; + await controller.initialize().catchError((dynamic e) {}); + expect(controller.value.hasError, equals(true)); + fakeVideoPlayerPlatform.forceInitError = false; + await controller.initialize(); + expect(controller.value.hasError, equals(false)); + }, + ); test( 'given controller with error when initialization succeeds it should clear error', @@ -503,7 +568,9 @@ void main() { }); test('contentUri', () async { - final controller = VideoPlayerController.contentUri(Uri.parse('content://video')); + final controller = VideoPlayerController.contentUri( + Uri.parse('content://video'), + ); await controller.initialize(); expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'content://video'); @@ -534,7 +601,9 @@ void main() { }); test('play', () async { - final controller = VideoPlayerController.networkUrl(Uri.parse('https://127.0.0.1')); + final controller = VideoPlayerController.networkUrl( + Uri.parse('https://127.0.0.1'), + ); addTearDown(controller.dispose); await controller.initialize(); @@ -699,14 +768,22 @@ void main() { }); group('scrubbing', () { - testWidgets('restarts on release if already playing', (WidgetTester tester) async { + testWidgets('restarts on release if already playing', ( + WidgetTester tester, + ) async { final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - final progressWidget = VideoProgressIndicator(controller, allowScrubbing: true); + final progressWidget = VideoProgressIndicator( + controller, + allowScrubbing: true, + ); await tester.pumpWidget( - Directionality(textDirection: TextDirection.ltr, child: progressWidget), + Directionality( + textDirection: TextDirection.ltr, + child: progressWidget, + ), ); await controller.play(); @@ -723,14 +800,22 @@ void main() { await tester.runAsync(controller.dispose); }); - testWidgets('does not restart when dragging to end', (WidgetTester tester) async { + testWidgets('does not restart when dragging to end', ( + WidgetTester tester, + ) async { final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - final progressWidget = VideoProgressIndicator(controller, allowScrubbing: true); + final progressWidget = VideoProgressIndicator( + controller, + allowScrubbing: true, + ); await tester.pumpWidget( - Directionality(textDirection: TextDirection.ltr, child: progressWidget), + Directionality( + textDirection: TextDirection.ltr, + child: progressWidget, + ), ); await controller.play(); @@ -941,7 +1026,9 @@ void main() { final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.playerId]!; - fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.completed)); + fakeVideoEventStream.add( + VideoEvent(eventType: VideoEventType.completed), + ); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); @@ -957,13 +1044,19 @@ void main() { fakeVideoPlayerPlatform.streams[controller.playerId]!; fakeVideoEventStream.add( - VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: true), + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + ), ); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isTrue); fakeVideoEventStream.add( - VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: false), + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: false, + ), ); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); @@ -979,7 +1072,9 @@ void main() { final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.playerId]!; - fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.bufferingStart)); + fakeVideoEventStream.add( + VideoEvent(eventType: VideoEventType.bufferingStart), + ); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isTrue); @@ -999,7 +1094,9 @@ void main() { DurationRange(bufferStart, bufferEnd).toString(), ); - fakeVideoEventStream.add(VideoEvent(eventType: VideoEventType.bufferingEnd)); + fakeVideoEventStream.add( + VideoEvent(eventType: VideoEventType.bufferingEnd), + ); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isFalse); await tester.runAsync(controller.dispose); @@ -1179,13 +1276,17 @@ void main() { }); test('errorDescription is changed when copy with another error', () { const original = VideoPlayerValue.erroneous('error'); - final VideoPlayerValue copy = original.copyWith(errorDescription: 'new error'); + final VideoPlayerValue copy = original.copyWith( + errorDescription: 'new error', + ); expect(copy.errorDescription, 'new error'); }); test('errorDescription is changed when copy with error', () { const original = VideoPlayerValue.uninitialized(); - final VideoPlayerValue copy = original.copyWith(errorDescription: 'new error'); + final VideoPlayerValue copy = original.copyWith( + errorDescription: 'new error', + ); expect(copy.errorDescription, 'new error'); }); @@ -1259,7 +1360,10 @@ void main() { await controller.initialize(); await controller.play(); - verifyPlayStateRespondsToLifecycle(controller, shouldPlayInBackground: true); + verifyPlayStateRespondsToLifecycle( + controller, + shouldPlayInBackground: true, + ); }); test('false allowBackgroundPlayback pauses playback', () async { @@ -1271,7 +1375,10 @@ void main() { await controller.initialize(); await controller.play(); - verifyPlayStateRespondsToLifecycle(controller, shouldPlayInBackground: false); + verifyPlayStateRespondsToLifecycle( + controller, + shouldPlayInBackground: false, + ); }); }); @@ -1344,7 +1451,10 @@ void main() { isCompletedTest(); if (!hasLooped) { fakeVideoEventStream.add( - VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: true), + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + ), ); hasLooped = !hasLooped; } @@ -1370,7 +1480,9 @@ void main() { final void Function() isCompletedTest = expectAsync0(() {}); - controller.value = controller.value.copyWith(duration: const Duration(seconds: 10)); + controller.value = controller.value.copyWith( + duration: const Duration(seconds: 10), + ); controller.addListener(() async { if (currentIsCompleted != controller.value.isCompleted) { @@ -1408,25 +1520,27 @@ void main() { final controller = VideoPlayerController.networkUrl(_localhostUri); await controller.initialize(); - fakeVideoPlayerPlatform - .setVideoTracksForPlayer(controller.playerId, [ - const platform_interface.VideoTrack( - id: '0_0', - isSelected: true, - label: '1080p', - bitrate: 5000000, - width: 1920, - height: 1080, - ), - const platform_interface.VideoTrack( - id: '0_1', - isSelected: false, - label: '720p', - bitrate: 2500000, - width: 1280, - height: 720, - ), - ]); + fakeVideoPlayerPlatform.setVideoTracksForPlayer( + controller.playerId, + [ + const platform_interface.VideoTrack( + id: '0_0', + isSelected: true, + label: '1080p', + bitrate: 5000000, + width: 1920, + height: 1080, + ), + const platform_interface.VideoTrack( + id: '0_1', + isSelected: false, + label: '720p', + bitrate: 2500000, + width: 1280, + height: 720, + ), + ], + ); final List tracks = await controller.getVideoTracks(); @@ -1469,7 +1583,10 @@ void main() { await controller.selectVideoTrack(null); - expect(fakeVideoPlayerPlatform.calls, isNot(contains('selectVideoTrack'))); + expect( + fakeVideoPlayerPlatform.calls, + isNot(contains('selectVideoTrack')), + ); }); }); } @@ -1484,7 +1601,8 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { bool forceInitError = false; int nextPlayerId = 0; final Map _positions = {}; - final Map webOptions = {}; + final Map webOptions = + {}; @override Future create(DataSource dataSource) async { @@ -1493,7 +1611,10 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { streams[nextPlayerId] = stream; if (forceInitError) { stream.addError( - PlatformException(code: 'VideoError', message: 'Video player had error XYZ'), + PlatformException( + code: 'VideoError', + message: 'Video player had error XYZ', + ), ); } else { stream.add( @@ -1515,7 +1636,10 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { streams[nextPlayerId] = stream; if (forceInitError) { stream.addError( - PlatformException(code: 'VideoError', message: 'Video player had error XYZ'), + PlatformException( + code: 'VideoError', + message: 'Video player had error XYZ', + ), ); } else { stream.add( @@ -1595,7 +1719,10 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { } @override - Future setWebOptions(int playerId, VideoPlayerWebOptions options) async { + Future setWebOptions( + int playerId, + VideoPlayerWebOptions options, + ) async { if (!kIsWeb) { throw UnimplementedError('setWebOptions() is only available in the web.'); } @@ -1606,12 +1733,17 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { // Video track selection support final Map> _videoTracks = >{}; - void setVideoTracksForPlayer(int playerId, List tracks) { + void setVideoTracksForPlayer( + int playerId, + List tracks, + ) { _videoTracks[playerId] = tracks; } @override - Future> getVideoTracks(int playerId) async { + Future> getVideoTracks( + int playerId, + ) async { calls.add('getVideoTracks'); return _videoTracks[playerId] ?? []; } diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index 802272c13e3..27d6de8c908 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -17,7 +17,9 @@ VideoPlayerInstanceApi _productionApiProvider(int playerId) { } /// The non-test implementation of `_videoEventStreamProvider`. -Stream _productionVideoEventStreamProvider(String streamIdentifier) { +Stream _productionVideoEventStreamProvider( + String streamIdentifier, +) { return pigeon.videoEvents(instanceName: streamIdentifier); } @@ -27,7 +29,8 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { /// Creates a new Android video player implementation instance. AndroidVideoPlayer({ @visibleForTesting AndroidVideoPlayerApi? pluginApi, - @visibleForTesting VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, + @visibleForTesting + VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, Stream Function(String streamIdentifier)? videoEventStreamProvider, }) : _api = pluginApi ?? AndroidVideoPlayerApi(), @@ -87,9 +90,14 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { case DataSourceType.asset: final String? asset = dataSource.asset; if (asset == null) { - throw ArgumentError('"asset" must be non-null for an asset data source'); + throw ArgumentError( + '"asset" must be non-null for an asset data source', + ); } - final String key = await _api.getLookupKeyForAsset(asset, dataSource.package); + final String key = await _api.getLookupKeyForAsset( + asset, + dataSource.package, + ); uri = 'asset:///$key'; case DataSourceType.network: uri = dataSource.uri; @@ -205,7 +213,9 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { final VideoPlayerViewState viewState = _playerWith(id: playerId).viewState; return switch (viewState) { - VideoPlayerTextureViewState(:final int textureId) => Texture(textureId: textureId), + VideoPlayerTextureViewState(:final int textureId) => Texture( + textureId: textureId, + ), VideoPlayerPlatformViewState() => PlatformViewPlayer(playerId: playerId), }; } @@ -271,7 +281,9 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { // Generate label from resolution if not provided final String? label = track.label ?? - (track.width != null && track.height != null ? '${track.height}p' : null); + (track.width != null && track.height != null + ? '${track.height}p' + : null); tracks.add( VideoTrack( id: trackId, @@ -306,7 +318,9 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { return player ?? (throw StateError('No active player with ID $id.')); } - PlatformVideoFormat? _platformVideoFormatFromVideoFormat(VideoFormat? format) { + PlatformVideoFormat? _platformVideoFormatFromVideoFormat( + VideoFormat? format, + ) { return switch (format) { VideoFormat.dash => PlatformVideoFormat.dash, VideoFormat.hls => PlatformVideoFormat.hls, @@ -560,7 +574,9 @@ class _PlayerInstance { // should be synchronous with the state change. break; case PlatformPlaybackState.ended: - _eventStreamController.add(VideoEvent(eventType: VideoEventType.completed)); + _eventStreamController.add( + VideoEvent(eventType: VideoEventType.completed), + ); case PlatformPlaybackState.unknown: // Ignore unknown states. This isn't an error since the media // framework could add new states in the future. diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index 54ef598a51d..94a5eb54b99 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -21,7 +21,8 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { /// Creates a new AVFoundation-based video player implementation instance. AVFoundationVideoPlayer({ @visibleForTesting AVFoundationVideoPlayerApi? pluginApi, - @visibleForTesting VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, + @visibleForTesting + VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, }) : _api = pluginApi ?? AVFoundationVideoPlayerApi(), _playerApiProvider = playerApiProvider ?? _productionApiProvider; @@ -70,7 +71,9 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { case DataSourceType.asset: final String? asset = dataSource.asset; if (asset == null) { - throw ArgumentError('"asset" must be non-null for an asset data source'); + throw ArgumentError( + '"asset" must be non-null for an asset data source', + ); } uri = await _api.getAssetUrl(asset, dataSource.package); if (uri == null) { @@ -184,13 +187,16 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { // Convert HLS variant tracks (iOS 15+) if (nativeData.mediaSelectionTracks != null) { - for (final MediaSelectionVideoTrackData track in nativeData.mediaSelectionTracks!) { + for (final MediaSelectionVideoTrackData track + in nativeData.mediaSelectionTracks!) { // Use bitrate as the track ID for HLS variants final trackId = 'variant_${track.bitrate ?? track.variantIndex}'; // Generate label from resolution if not provided final String? label = track.label ?? - (track.width != null && track.height != null ? '${track.height}p' : null); + (track.width != null && track.height != null + ? '${track.height}p' + : null); tracks.add( VideoTrack( id: trackId, @@ -213,7 +219,9 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { // Generate label from resolution if not provided final String? label = track.label ?? - (track.width != null && track.height != null ? '${track.height}p' : null); + (track.width != null && track.height != null + ? '${track.height}p' + : null); tracks.add( VideoTrack( id: trackId, @@ -266,7 +274,9 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { final VideoPlayerViewState viewState = _playerWith(id: playerId).viewState; return switch (viewState) { - VideoPlayerTextureViewState(:final int textureId) => Texture(textureId: textureId), + VideoPlayerTextureViewState(:final int textureId) => Texture( + textureId: textureId, + ), VideoPlayerPlatformViewState() => _buildPlatformView(playerId), }; } @@ -293,8 +303,11 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { /// An instance of a video player, corresponding to a single player ID in /// [AVFoundationVideoPlayer]. class _PlayerInstance { - _PlayerInstance(this._api, this.viewState, {required EventChannel eventChannel}) - : _eventChannel = eventChannel; + _PlayerInstance( + this._api, + this.viewState, { + required EventChannel eventChannel, + }) : _eventChannel = eventChannel; final VideoPlayerInstanceApi _api; final VideoPlayerViewState viewState; From 5c06f478452067192068560ae854591a6c33fa10 Mon Sep 17 00:00:00 2001 From: nateshmbhat Date: Wed, 7 Jan 2026 21:47:40 +0530 Subject: [PATCH 6/7] addressed some PR comments --- .../video_player/video_player/CHANGELOG.md | 4 +-- .../example/lib/video_tracks_demo.dart | 28 ++++++------------- .../video_player/lib/video_player.dart | 15 +++++----- .../video_player/video_player/pubspec.yaml | 2 +- .../FVPVideoPlayer.m | 26 ++++++++--------- 5 files changed, 32 insertions(+), 43 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 74d50ef0a7d..5a6940d5132 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,11 +1,11 @@ -## NEXT +## 2.11.0 * Updates minimum supported SDK version to Flutter 3.32/Dart 3.8. * Updates README to reflect currently supported OS versions for the latest versions of the endorsed platform implementations. * Applications built with older versions of Flutter will continue to use compatible versions of the platform implementations. -* Adds video track (quality) selection support via `getVideoTracks()`, `selectVideoTrack()`, and `isVideoTrackSupportAvailable()` methods. +* Adds video quality selection support for HLS/DASH adaptive streams via `getVideoTracks()`, `selectVideoTrack()`, and `isVideoTrackSupportAvailable()` methods. This enables programmatic switching between different resolution/bitrate variants. ## 2.10.1 diff --git a/packages/video_player/video_player/example/lib/video_tracks_demo.dart b/packages/video_player/video_player/example/lib/video_tracks_demo.dart index 4d7d3df3d2a..a1b2f5f76e2 100644 --- a/packages/video_player/video_player/example/lib/video_tracks_demo.dart +++ b/packages/video_player/video_player/example/lib/video_tracks_demo.dart @@ -125,6 +125,8 @@ class _VideoTracksDemoState extends State { return; } + final ScaffoldMessengerState scaffoldMessenger = ScaffoldMessenger.of(context); + try { await controller.selectVideoTrack(track); @@ -141,14 +143,12 @@ class _VideoTracksDemoState extends State { final message = track == null ? 'Switched to automatic quality' : 'Selected video track: ${_getTrackLabel(track)}'; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + scaffoldMessenger.showSnackBar(SnackBar(content: Text(message))); } catch (e) { if (!mounted) { return; } - ScaffoldMessenger.of(context).showSnackBar( + scaffoldMessenger.showSnackBar( SnackBar(content: Text('Failed to select video track: $e')), ); } @@ -230,9 +230,7 @@ class _VideoTracksDemoState extends State { inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), ), - dropdownMenuEntries: _sampleVideos.indexed.map(( - (int, String) record, - ) { + dropdownMenuEntries: _sampleVideos.indexed.map(((int, String) record) { final (index, url) = record; final label = url.contains('.m3u8') ? 'HLS Stream ${index + 1}' @@ -289,10 +287,7 @@ class _VideoTracksDemoState extends State { ), ), const SizedBox(height: 16), - ElevatedButton( - onPressed: _initializeVideo, - child: const Text('Retry'), - ), + ElevatedButton(onPressed: _initializeVideo, child: const Text('Retry')), ], ), ); @@ -381,9 +376,7 @@ class _VideoTracksDemoState extends State { title: Text( 'Automatic Quality', style: TextStyle( - fontWeight: _isAutoQuality - ? FontWeight.bold - : FontWeight.normal, + fontWeight: _isAutoQuality ? FontWeight.bold : FontWeight.normal, ), ), subtitle: const Text('Let the player choose the best quality'), @@ -406,8 +399,7 @@ class _VideoTracksDemoState extends State { ), ), ) - else if (_error != null && - (_controller?.value.isInitialized ?? false)) + else if (_error != null && (_controller?.value.isInitialized ?? false)) Expanded( child: Center( child: Text( @@ -444,9 +436,7 @@ class _VideoTracksDemoState extends State { ), title: Text( _getTrackLabel(track), - style: TextStyle( - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - ), + style: TextStyle(fontWeight: isSelected ? FontWeight.bold : FontWeight.normal), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index a4396ef5dd6..2a2ee098c2b 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -827,9 +827,10 @@ class VideoPlayerController extends ValueNotifier { /// Gets the available video tracks (quality variants) for the video. /// /// Returns a list of [VideoTrack] objects representing the available - /// video quality variants. For HLS/DASH streams, this returns the different - /// quality levels available. For regular videos, this may return a single - /// track or an empty list. + /// video quality variants. For HLS/DASH adaptive streams, this returns the + /// different quality levels (resolution/bitrate variants) available. + /// For non-adaptive videos (MP4, MOV, etc.), this returns an empty list + /// as they have a single fixed quality that cannot be switched. /// /// Note: On iOS 13-14, this returns an empty list as the AVAssetVariant API /// requires iOS 15+. On web, this throws an [UnimplementedError]. @@ -1295,9 +1296,9 @@ class ClosedCaption extends StatelessWidget { /// Represents a video track (quality variant) in a video with its metadata. /// -/// For HLS/DASH streams, each [VideoTrack] represents a different quality -/// level (e.g., 1080p, 720p, 480p). For regular videos, there may be only -/// one track or none available. +/// For HLS/DASH adaptive streams, each [VideoTrack] represents a different +/// quality level (e.g., 1080p, 720p, 480p). For non-adaptive videos (MP4, +/// MOV, etc.), no tracks are returned as they have a single fixed quality. @immutable class VideoTrack { /// Constructs an instance of [VideoTrack]. @@ -1330,7 +1331,7 @@ class VideoTrack { /// /// The format is platform-specific: /// - Android: `"{groupIndex}_{trackIndex}"` (e.g., `"0_2"`) - /// - iOS: `"variant_{bitrate}"` for HLS, `"asset_{trackID}"` for regular videos + /// - iOS: `"variant_{bitrate}"` for HLS adaptive streams final String id; /// Whether this track is currently selected. diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 61cb3cb6ba6..9b1bef5025d 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, macOS and web. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.10.1 +version: 2.11.0 environment: sdk: ^3.8.0 diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 061408dc3b3..b97e8064b8e 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -450,12 +450,10 @@ - (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, if (status == AVKeyValueStatusLoaded) { NSArray *variants = urlAsset.variants; - double currentBitrate = currentItem.preferredPeakBitRate > 0 - ? currentItem.preferredPeakBitRate - : 0; + double currentBitrate = MAX(currentItem.preferredPeakBitRate, 0); - for (NSInteger i = 0; i < variants.count; i++) { - AVAssetVariant *variant = variants[i]; + NSInteger variantIndex = 0; + for (AVAssetVariant *variant in variants) { double peakBitRate = variant.peakBitRate; CGSize videoSize = CGSizeZero; double frameRate = 0; @@ -480,16 +478,16 @@ - (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, (currentBitrate > 0 && fabs(peakBitRate - currentBitrate) < peakBitRate * 0.1); - // Generate label from resolution - NSString *label = nil; + // Generate a human-readable resolution label (e.g., "1080p") + NSString *resolutionLabel = nil; if (videoSize.height > 0) { - label = [NSString stringWithFormat:@"%.0fp", videoSize.height]; + resolutionLabel = [NSString stringWithFormat:@"%.0fp", videoSize.height]; } FVPMediaSelectionVideoTrackData *trackData = [FVPMediaSelectionVideoTrackData - makeWithVariantIndex:i - label:label + makeWithVariantIndex:variantIndex + label:resolutionLabel bitrate:peakBitRate > 0 ? @((NSInteger)peakBitRate) : nil @@ -503,14 +501,13 @@ - (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, codec:codec isSelected:isSelected]; [mediaSelectionTracks addObject:trackData]; + variantIndex++; } } FVPNativeVideoTrackData *result = [FVPNativeVideoTrackData makeWithAssetTracks:nil - mediaSelectionTracks:mediaSelectionTracks.count > 0 - ? mediaSelectionTracks - : nil]; + mediaSelectionTracks:mediaSelectionTracks]; completion(result, nil); }); }]; @@ -520,8 +517,9 @@ - (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, } } +/// Converts a FourCharCode codec type to a human-readable string for display in the UI. +/// These codec names help users understand the video encoding format of each quality variant. - (NSString *)codecStringFromFourCharCode:(FourCharCode)code { - // Convert common video codec FourCharCodes to readable strings switch (code) { case kCMVideoCodecType_H264: return @"avc1"; From 15d211f6f6fee3dea8b0176b521336142eec5963 Mon Sep 17 00:00:00 2001 From: nateshmbhat Date: Wed, 7 Jan 2026 22:08:44 +0530 Subject: [PATCH 7/7] addressed pr comments --- .../darwin/RunnerTests/VideoPlayerTests.m | 8 +-- .../FVPVideoPlayer.m | 58 ++++++++++++------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m index 5c3d096b01d..63d79d60c9c 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m @@ -1053,8 +1053,8 @@ - (nonnull AVPlayerItem *)playerItemWithURL:(NSURL *)url { #pragma mark - Video Track Tests -// Tests getVideoTracks with a regular MP4 video file using real AVFoundation. -// Regular MP4 files don't have HLS variants, so we expect empty media selection tracks. +// Integration test for getVideoTracks with a non-HLS MP4 video over a live network request. +// Non-HLS MP4 files don't have adaptive bitrate variants, so we expect empty media selection tracks. - (void)testGetVideoTracksWithRealMP4Video { FVPVideoPlayer *player = [[FVPVideoPlayer alloc] initWithPlayerItem:[self playerItemWithURL:self.mp4TestURL] @@ -1086,8 +1086,8 @@ - (void)testGetVideoTracksWithRealMP4Video { [player disposeWithError:&disposeError]; } -// Tests getVideoTracks with an HLS stream using real AVFoundation. -// HLS streams use AVAssetVariant API (iOS 15+) for video track selection. +// Integration test for getVideoTracks with an HLS stream over a live network request. +// HLS streams use AVAssetVariant API (iOS 15+) to enumerate available quality variants. - (void)testGetVideoTracksWithRealHLSStream { NSURL *hlsURL = [NSURL URLWithString:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8"]; diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index b97e8064b8e..bf46798d3ac 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -15,6 +15,10 @@ static void *playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext; static void *rateContext = &rateContext; +/// The key name for loading AVURLAsset variants property asynchronously. +/// Note: Apple does not provide a constant for this key; it is documented in the AVURLAsset API. +static NSString *const kFVPAssetVariantsKey = @"variants"; + /// Registers KVO observers on 'object' for each entry in 'observations', which must be a /// dictionary mapping KVO keys to NSValue-wrapped context pointers. /// @@ -441,11 +445,11 @@ - (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, // Use AVAssetVariant API for iOS 15+ to get HLS variants if (@available(iOS 15.0, macOS 12.0, *)) { [urlAsset - loadValuesAsynchronouslyForKeys:@[ @"variants" ] + loadValuesAsynchronouslyForKeys:@[ kFVPAssetVariantsKey ] completionHandler:^{ dispatch_async(dispatch_get_main_queue(), ^{ NSError *error = nil; - AVKeyValueStatus status = [urlAsset statusOfValueForKey:@"variants" + AVKeyValueStatus status = [urlAsset statusOfValueForKey:kFVPAssetVariantsKey error:&error]; if (status == AVKeyValueStatusLoaded) { @@ -472,8 +476,9 @@ - (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, } } - // Determine if this variant is selected (approximate match by - // bitrate) + // Determine if this variant is currently selected by comparing bitrates. + // Since AVPlayer doesn't expose the exact selected variant, we use a 10% + // tolerance to account for minor bitrate variations in adaptive streaming. BOOL isSelected = (currentBitrate > 0 && fabs(peakBitRate - currentBitrate) < peakBitRate * 0.1); @@ -485,21 +490,13 @@ - (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, } FVPMediaSelectionVideoTrackData *trackData = - [FVPMediaSelectionVideoTrackData - makeWithVariantIndex:variantIndex - label:resolutionLabel - bitrate:peakBitRate > 0 - ? @((NSInteger)peakBitRate) - : nil - width:videoSize.width > 0 - ? @((NSInteger)videoSize.width) - : nil - height:videoSize.height > 0 - ? @((NSInteger)videoSize.height) - : nil - frameRate:frameRate > 0 ? @(frameRate) : nil - codec:codec - isSelected:isSelected]; + [self createVideoTrackDataWithIndex:variantIndex + label:resolutionLabel + peakBitRate:peakBitRate + videoSize:videoSize + frameRate:frameRate + codec:codec + isSelected:isSelected]; [mediaSelectionTracks addObject:trackData]; variantIndex++; } @@ -512,11 +509,32 @@ - (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, }); }]; } else { - // For iOS < 15, return empty list as AVAssetVariant is not available + // For iOS < 15, AVAssetVariant API is not available. Return an empty result (not an error) + // since the absence of variant data is expected on older OS versions. completion([[FVPNativeVideoTrackData alloc] init], nil); } } +/// Creates a video track data object with the given parameters, converting values to NSNumber +/// where appropriate and returning nil for invalid/zero values. +- (FVPMediaSelectionVideoTrackData *)createVideoTrackDataWithIndex:(NSInteger)index + label:(NSString *)label + peakBitRate:(double)peakBitRate + videoSize:(CGSize)videoSize + frameRate:(double)frameRate + codec:(NSString *)codec + isSelected:(BOOL)isSelected { + return [FVPMediaSelectionVideoTrackData + makeWithVariantIndex:index + label:label + bitrate:peakBitRate > 0 ? @((NSInteger)peakBitRate) : nil + width:videoSize.width > 0 ? @((NSInteger)videoSize.width) : nil + height:videoSize.height > 0 ? @((NSInteger)videoSize.height) : nil + frameRate:frameRate > 0 ? @(frameRate) : nil + codec:codec + isSelected:isSelected]; +} + /// Converts a FourCharCode codec type to a human-readable string for display in the UI. /// These codec names help users understand the video encoding format of each quality variant. - (NSString *)codecStringFromFourCharCode:(FourCharCode)code {