Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
c8b0071
feat(video_player): add audio track selection support for iOS and And…
nateshmbhat Aug 29, 2025
4e4dc8c
feat(android): implement audio track selection in video player
nateshmbhat Aug 29, 2025
25de26c
feat(video_player): Format Entire Codebase
nateshmbhat Aug 29, 2025
31f9030
feat(video_player): add audio track selection and retrieval functiona…
nateshmbhat Aug 29, 2025
8f711b5
test(video_player): add tests for audio track selection and management
nateshmbhat Aug 29, 2025
e0f6d65
fix(video): address PR review comments
nateshmbhat Aug 30, 2025
fc30013
Merge branch 'main' into develop
nateshmbhat Aug 30, 2025
8a68e76
fix(video_player): add delay after audio track selection to handle Ex…
nateshmbhat Aug 30, 2025
894f516
test(video_player): update audio tracks test to use ImmutableList and…
nateshmbhat Aug 30, 2025
644e08e
Merge branch 'develop' of github.com:nateshmbhat/flutter_packages int…
nateshmbhat Aug 30, 2025
fdde6f8
refactor(tests): move audio track tests from AudioTracksTests.m to Vi…
nateshmbhat Aug 30, 2025
4291609
fix(ios,android): fixed test failure cases (linting and warnings)
nateshmbhat Aug 31, 2025
8dfd8e3
style(audio_tracks): improve code style and add type safety in audio …
nateshmbhat Aug 31, 2025
a892a5e
chore(android): bump compileSdk from 34 to 35 for video player plugin
nateshmbhat Sep 1, 2025
1537778
Merge branch 'main' into develop
nateshmbhat Sep 4, 2025
c222584
Merge branch 'main' into develop
nateshmbhat Sep 9, 2025
f087fe1
refactor(video): improve video player controller handling and code fo…
nateshmbhat Sep 19, 2025
2e4c9ac
Merge branch 'main' into develop
nateshmbhat Sep 19, 2025
652dd48
refactor(video): improve video player state management and UI components
nateshmbhat Sep 19, 2025
80bda36
refactor(ios): improve audio track format parsing with better mock ob…
nateshmbhat Sep 19, 2025
1495a95
refactor(ios): optimize audio track metadata lookup using AVMetadataI…
nateshmbhat Sep 19, 2025
ad558a7
refactor(video): move ExoPlayer delay from demo to controller impleme…
nateshmbhat Sep 19, 2025
6c7fd2b
Merge pull request #1 from nateshmbhat/19-sept-pr-review-changes
nateshmbhat Sep 19, 2025
85a8f54
Merge branch 'main' into develop
nateshmbhat Sep 20, 2025
ac54143
feat(video_player): add platform check for audio track selection support
nateshmbhat Sep 20, 2025
1775e23
Merge pull request #2 from nateshmbhat/19-sept-pr-review-changes
nateshmbhat Sep 20, 2025
6dafd5f
Merge branch 'develop' of github.com:nateshmbhat/flutter_packages int…
nateshmbhat Sep 20, 2025
9440d1b
style(dart): format code and improve readability with proper line breaks
nateshmbhat Sep 20, 2025
a65ebaf
chore(deps): format dependency overrides and add video_player_web path
nateshmbhat Sep 20, 2025
7798aaa
chore(deps): add video_player_web dependency and update package overr…
nateshmbhat Sep 20, 2025
e912f6d
fix(video_player): add web platform check for audio track selection d…
nateshmbhat Sep 20, 2025
3ae0519
Merge branch 'main' into develop
nateshmbhat Sep 21, 2025
99cdbd6
fix(video_player): remove fallback labels for audio tracks in Android…
nateshmbhat Sep 23, 2025
e7895ef
Merge branch 'main' into develop
nateshmbhat Sep 23, 2025
4e9b50b
Merge branch 'main' into develop
nateshmbhat Sep 24, 2025
5c32bf3
chore(license): update copyright headers in video player files
nateshmbhat Sep 24, 2025
5101efc
chore(license): remove trailing period from copyright headers
nateshmbhat Sep 24, 2025
f0bdabd
Merge branch 'main' into develop
nateshmbhat Sep 27, 2025
e7b3da0
chore(release): bump video_player packages to next version with audio…
nateshmbhat Oct 4, 2025
6b0de5f
feat(video_player): add audio track management support to platform in…
nateshmbhat Oct 4, 2025
12fd73f
Update packages/video_player/video_player_platform_interface/CHANGELO…
nateshmbhat Oct 13, 2025
680c5e6
feat(video_player): add isAudioTrackSupportAvailable method to platfo…
nateshmbhat Oct 13, 2025
c06a83d
Merge branch '4-oct-video-player-interface-updates' of github.com:nat…
nateshmbhat Oct 13, 2025
466946a
Merge branch 'main' into 4-oct-video-player-interface-updates
nateshmbhat Oct 13, 2025
9f391de
Merge branch '4-oct-video-player-interface-updates' into develop
nateshmbhat Oct 13, 2025
f85c2bf
refactor(tests): improve formatting of video player test cases
nateshmbhat Oct 13, 2025
5aca457
fix(android): add logging for audio track selection failures in Video…
nateshmbhat Oct 13, 2025
56191a0
docs(video_player): improve documentation for audio track selection m…
nateshmbhat Oct 13, 2025
27336d4
refactor(video_player): move VideoAudioTrack class from platform inte…
nateshmbhat Oct 16, 2025
f202438
refactor(video): normalize null handling for audio track metadata acr…
nateshmbhat Oct 16, 2025
3341e2d
refactor(video): change isAudioTrackSupportAvailable from async to sy…
nateshmbhat Oct 16, 2025
bba24d3
style(video_player): fix code formatting and indentation
nateshmbhat Oct 16, 2025
f89e93b
Merge branch 'main' into develop
nateshmbhat Oct 16, 2025
d69fb63
Merge branch 'main' into develop
nateshmbhat Oct 28, 2025
48f7159
feat(android): add audio track selection support for video player
nateshmbhat Oct 28, 2025
1862a7d
chore(deps): update video_player_platform_interface to 6.6.0
nateshmbhat Oct 28, 2025
8046d22
feat(video): simplify video sources and refactor audio track handling
nateshmbhat Oct 28, 2025
e2b9a1b
refactor(video_player): simplify audio track data class references
nateshmbhat Oct 28, 2025
00b55ed
chore(deps): update video_player_platform_interface to 6.6.0
nateshmbhat Oct 28, 2025
3c82ead
Based on the git diff, I'll create a commit message that summarizes t…
nateshmbhat Oct 28, 2025
5916b33
Merge branch 'main' into develop
nateshmbhat Nov 3, 2025
5b1e5c7
Here's a commit message summarizing the changes:
nateshmbhat Nov 3, 2025
1161332
perf(ios): optimize audio track selection in video player
nateshmbhat Nov 3, 2025
2811ea2
fix(video_player): improve audio track format handling in tests
nateshmbhat Nov 3, 2025
77a28bb
Merge branch 'main' into develop
nateshmbhat Nov 4, 2025
08e20d3
fix(video_player_avfoundation): update audio track selection for macO…
nateshmbhat Nov 4, 2025
c63b050
Merge branch 'main' into develop
nateshmbhat Nov 6, 2025
e3df547
refactor(video_player): change audio track selection from string ID t…
nateshmbhat Nov 7, 2025
8d25e52
Merge branch 'main' into develop
nateshmbhat Nov 7, 2025
9f00bf2
chore(video_player): bump iOS minimum deployment target to 13.0
nateshmbhat Nov 7, 2025
a7118ee
refactor(video_player_android): improve audio track selection error h…
nateshmbhat Nov 19, 2025
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
1 change: 1 addition & 0 deletions packages/video_player/video_player/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT

* Adds `getAudioTracks()` and `selectAudioTrack()` methods to retrieve and select available audio tracks.
* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.

## 2.10.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
40E43985C26639614BC3B419 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
Expand Down Expand Up @@ -221,6 +222,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
40E43985C26639614BC3B419 /* [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;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// 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 audio track functionality.
class AudioTracksDemo extends StatefulWidget {
/// Creates an AudioTracksDemo widget.
const AudioTracksDemo({super.key});

@override
State<AudioTracksDemo> createState() => _AudioTracksDemoState();
}

class _AudioTracksDemoState extends State<AudioTracksDemo> {
VideoPlayerController? _controller;
List<VideoAudioTrack> _audioTracks = <VideoAudioTrack>[];
bool _isLoading = false;
String? _error;

// Sample video URLs with multiple audio tracks
final List<String> _sampleVideos = <String>[
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to use existing videos hosted in the flutter/samples repo (see the other demos), or do they not have tracks? I'm not sure what licenses these samples use.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a const right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to use existing videos hosted in the flutter/samples repo (see the other demos), or do they not have tracks? I'm not sure what licenses these samples use.

I tried to find if such urls are there in flutter/samples but coudn't find it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example:

_controller = MiniController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
viewType: widget.viewType,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or you can upload one with a proper license to https://github.com/flutter/assets-for-api-docs/tree/main/assets/videos

'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8',
// Add HLS stream with multiple audio tracks if available
'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
];

int _selectedVideoIndex = 0;

@override
void initState() {
super.initState();
_initializeVideo();
}

Future<void> _initializeVideo() async {
setState(() {
_isLoading = true;
_error = null;
});

try {
await _controller?.dispose();

_controller = VideoPlayerController.networkUrl(
Uri.parse(_sampleVideos[_selectedVideoIndex]),
);

await _controller!.initialize();

// Get audio tracks after initialization
await _loadAudioTracks();

setState(() {
_isLoading = false;
});
} catch (e) {
setState(() {
_error = 'Failed to initialize video: $e';
_isLoading = false;
});
}
}

Future<void> _loadAudioTracks() async {
if (_controller == null || !_controller!.value.isInitialized) {
return;
}

try {
final List<VideoAudioTrack> tracks = await _controller!.getAudioTracks();
setState(() {
_audioTracks = tracks;
});
} catch (e) {
setState(() {
_error = 'Failed to load audio tracks: $e';
});
}
}

Future<void> _selectAudioTrack(String trackId) async {
if (_controller == null) {
return;
}

try {
await _controller!.selectAudioTrack(trackId);

// Add a small delay to allow ExoPlayer to process the track selection change
// This is needed because ExoPlayer's track selection update is asynchronous
await Future<void>.delayed(const Duration(milliseconds: 100));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this could be a major source of flakiness. Shouldn't selectAudioTrack completes the future only after the new tracks becomes ready?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, a major source of user errors if this isn't documented (still I would prefer that we hide the ugliness within our implementation so the user doesn't have to worry about that).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will move the delay inside the selectAudioTrack dart method itself. that should take care of this.


// Reload tracks to update selection status
await _loadAudioTracks();

if (!mounted) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is also needed whenever you want to call setState I think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will add it after async calls whereever it's needed

return;
}
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Selected audio track: $trackId')));
} catch (e) {
if (!mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to select audio track: $e')),
);
}
}

@override
void dispose() {
_controller?.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Audio Tracks Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Column(
children: <Widget>[
// Video selection dropdown
Padding(
padding: const EdgeInsets.all(16.0),
child: DropdownButtonFormField<int>(
value: _selectedVideoIndex,
decoration: const InputDecoration(
labelText: 'Select Video',
border: OutlineInputBorder(),
),
items:
_sampleVideos.asMap().entries.map((MapEntry<int, String> entry) {
return DropdownMenuItem<int>(
value: entry.key,
child: Text('Video ${entry.key + 1}'),
);
}).toList(),
onChanged: (int? value) {
if (value != null && value != _selectedVideoIndex) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the material DropdownMenu documentation and it doesn't really tell me when the callback would give me a null value. Does it mean no selection was made?

setState(() {
_selectedVideoIndex = value;
});
_initializeVideo();
}
},
),
),

// Video player
Expanded(
flex: 2,
child: ColoredBox(color: Colors.black, child: _buildVideoPlayer()),
),

// Audio tracks list
Expanded(flex: 3, child: _buildAudioTracksList()),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _loadAudioTracks,
tooltip: 'Refresh Audio Tracks',
child: const Icon(Icons.refresh),
),
);
}

Widget _buildVideoPlayer() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}

if (_error != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: final error case _error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't understand this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (_error != null) {
if (_error case final String error?) {

so you don't need to use !.

return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.error, size: 48, color: Colors.red[300]),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can we use the theme based colors throughout this example? Including where we use Colors.white etc?

const SizedBox(height: 16),
Text(
_error!,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _initializeVideo,
child: const Text('Retry'),
),
],
),
);
}

if (_controller?.value.isInitialized ?? false) {
return Stack(
alignment: Alignment.center,
children: <Widget>[
AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: VideoPlayer(_controller!),
),
_buildPlayPauseButton(),
],
);
}

return const Center(
child: Text('No video loaded', style: TextStyle(color: Colors.white)),
);
}

Widget _buildPlayPauseButton() {
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();
}
setState(() {});
},
icon: Icon(
_controller!.value.isPlaying ? Icons.pause : Icons.play_arrow,
),
),
);
}

Widget _buildAudioTracksList() {
return Container(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
const Icon(Icons.audiotrack),
const SizedBox(width: 8),
Text(
'Audio Tracks (${_audioTracks.length})',
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
const SizedBox(height: 16),

if (_audioTracks.isEmpty)
const Expanded(
child: Center(
child: Text(
'No audio tracks available.\nTry loading a video with multiple audio tracks.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
),
)
else
Expanded(
child: ListView.builder(
itemCount: _audioTracks.length,
itemBuilder: (BuildContext context, int index) {
final VideoAudioTrack track = _audioTracks[index];
return _buildAudioTrackTile(track);
},
),
),
],
),
);
}

Widget _buildAudioTrackTile(VideoAudioTrack track) {
return Card(
margin: const EdgeInsets.only(bottom: 8.0),
child: ListTile(
leading: CircleAvatar(
backgroundColor: track.isSelected ? Colors.green : Colors.grey,
child: Icon(
track.isSelected ? Icons.check : Icons.audiotrack,
color: Colors.white,
),
),
title: Text(
track.label.isNotEmpty ? track.label : 'Track ${track.id}',
style: TextStyle(
fontWeight: track.isSelected ? FontWeight.bold : FontWeight.normal,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('ID: ${track.id}'),
Text('Language: ${track.language}'),
if (track.codec != null) Text('Codec: ${track.codec}'),
if (track.bitrate != null) Text('Bitrate: ${track.bitrate} bps'),
if (track.sampleRate != null)
Text('Sample Rate: ${track.sampleRate} Hz'),
if (track.channelCount != null)
Text('Channels: ${track.channelCount}'),
],
),
trailing:
track.isSelected
? const Icon(Icons.radio_button_checked, color: Colors.green)
: const Icon(Icons.radio_button_unchecked),
onTap: track.isSelected ? null : () => _selectAudioTrack(track.id),
),
);
}
}
15 changes: 15 additions & 0 deletions packages/video_player/video_player/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ library;
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';

import 'audio_tracks_demo.dart';

void main() {
runApp(MaterialApp(home: _App()));
}
Expand All @@ -37,6 +39,19 @@ class _App extends StatelessWidget {
);
},
),
IconButton(
key: const ValueKey<String>('audio_tracks_demo'),
icon: const Icon(Icons.audiotrack),
tooltip: 'Audio Tracks Demo',
onPressed: () {
Navigator.push<AudioTracksDemo>(
context,
MaterialPageRoute<AudioTracksDemo>(
builder: (BuildContext context) => const AudioTracksDemo(),
),
);
},
),
],
bottom: const TabBar(
isScrollable: true,
Expand Down
Loading