diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 4b44b050047a..a52dcb2c076b 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 6.7.0 +* Adds `VideoTrack` class and `getVideoTracks()`, `selectVideoTrack()`, `isVideoTrackSupportAvailable()` methods for video track (quality) selection. * Updates minimum supported SDK version to Flutter 3.35/Dart 3.9. ## 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 1cec5f42c218..8f1b43439682 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 @@ -153,6 +153,41 @@ 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 non-adaptive videos, platform + /// implementations may return one or more tracks, or an empty list, + /// depending on the media and the metadata available. + 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). + 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 {} @@ -652,3 +687,102 @@ 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/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index b39acce19665..373ddb91f1fb 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/video_player/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 6.6.0 +version: 6.7.0 environment: sdk: ^3.9.0 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 2d920161ec9e..2eeffb9f0176 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,122 @@ 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')); + // Accept both '30' and '30.0' (web JS omits trailing .0 for whole-number doubles) + expect(str, contains('frameRate: 30')); + expect(str, contains('codec: avc1')); + }); + }); }