Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<VideoTrack>> 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<void> 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 {}
Expand Down Expand Up @@ -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)';
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
});
}