Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
39 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
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
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,310 @@
// 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 {
const AudioTracksDemo({super.key});

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

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

// Sample video URLs with multiple audio tracks
final List<String> _sampleVideos = [
'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
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.

'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(
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: here and everywhere (especially in _loadAudioTracks): consider using a local var so you don't have to use ! to force unwrap the controller every time.

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 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);
// Reload tracks to update selection status
await _loadAudioTracks();

ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Selected audio track: $trackId')));
} catch (e) {
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: [
// Video selection dropdown
Padding(
padding: const EdgeInsets.all(16.0),
child: DropdownButtonFormField<int>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using https://api.flutter.dev/flutter/material/DropdownMenuFormField-class.html instead, IIRC the Dropdown* widgets are going to be replaced.

value: _selectedVideoIndex,
decoration: const InputDecoration(
labelText: 'Select Video',
border: OutlineInputBorder(),
),
items:
_sampleVideos.asMap().entries.map((entry) {
return DropdownMenuItem<int>(
value: entry.key,
child: Text('Video ${entry.key + 1}'),
);
}).toList(),
onChanged: (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: Container(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
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.

return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 48, color: Colors.red[300]),
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 == true) {
return Stack(
alignment: Alignment.center,
children: [
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(() {});
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this setState needed here? Can we listen to the controller?

Copy link
Author

Choose a reason for hiding this comment

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

yes we can , will update

},
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: [
Row(
children: [
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: (context, index) {
final 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: [
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
6 changes: 6 additions & 0 deletions packages/video_player/video_player/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ flutter:
- assets/bumble_bee_captions.srt
- assets/bumble_bee_captions.vtt
- assets/Audio.mp3
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
video_player_android: {path: ../../../../packages/video_player/video_player_android}
video_player_avfoundation: {path: ../../../../packages/video_player/video_player_avfoundation}
video_player_platform_interface: {path: ../../../../packages/video_player/video_player_platform_interface}
Loading