-
Notifications
You must be signed in to change notification settings - Fork 3.4k
[video_player] Add audio track metadata support (bitrate, sample rate, channels, codec) #9782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
c331d42
870b702
3435c20
4deae93
86ba273
5e09464
6bab30b
2dec18d
d29ad6c
cbb854b
c7488c1
567925d
a3a0884
1dfbf82
638bf43
e507002
d7aa9fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
// 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'; | ||
|
||
/// Demo page showing how to retrieve and display available audio tracks | ||
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; | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
_initializeVideoPlayer(); | ||
} | ||
|
||
Future<void> _initializeVideoPlayer() async { | ||
// Example URL with multiple audio tracks (replace with your test video) | ||
const String videoUrl = | ||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'; | ||
|
||
_controller = VideoPlayerController.networkUrl(Uri.parse(videoUrl)); | ||
|
||
try { | ||
await _controller!.initialize(); | ||
setState(() {}); | ||
|
||
// Get audio tracks after initialization | ||
await _getAudioTracks(); | ||
} catch (e) { | ||
debugPrint('Error initializing video player: $e'); | ||
} | ||
} | ||
|
||
Future<void> _getAudioTracks() async { | ||
if (_controller == null) return; | ||
|
||
setState(() { | ||
_isLoading = true; | ||
}); | ||
|
||
try { | ||
final tracks = await _controller!.getAudioTracks(); | ||
setState(() { | ||
_audioTracks = tracks; | ||
_isLoading = false; | ||
}); | ||
} catch (e) { | ||
debugPrint('Error getting audio tracks: $e'); | ||
setState(() { | ||
_isLoading = false; | ||
}); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
|
||
|
||
@override | ||
void dispose() { | ||
_controller?.dispose(); | ||
super.dispose(); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Scaffold( | ||
appBar: AppBar( | ||
title: const Text('Audio Tracks Demo'), | ||
backgroundColor: Colors.blue, | ||
), | ||
body: Column( | ||
children: [ | ||
// Video Player | ||
if (_controller != null && _controller!.value.isInitialized) | ||
AspectRatio( | ||
aspectRatio: _controller!.value.aspectRatio, | ||
child: VideoPlayer(_controller!), | ||
) | ||
else | ||
const SizedBox( | ||
height: 200, | ||
child: Center( | ||
child: CircularProgressIndicator(), | ||
), | ||
), | ||
|
||
// Video Controls | ||
if (_controller != null && _controller!.value.isInitialized) | ||
Row( | ||
mainAxisAlignment: MainAxisAlignment.center, | ||
children: [ | ||
IconButton( | ||
onPressed: () { | ||
setState(() { | ||
_controller!.value.isPlaying | ||
? _controller!.pause() | ||
: _controller!.play(); | ||
}); | ||
}, | ||
icon: Icon( | ||
_controller!.value.isPlaying | ||
? Icons.pause | ||
: Icons.play_arrow, | ||
), | ||
), | ||
IconButton( | ||
onPressed: _getAudioTracks, | ||
icon: const Icon(Icons.refresh), | ||
tooltip: 'Refresh Audio Tracks', | ||
), | ||
], | ||
), | ||
|
||
const Divider(), | ||
|
||
// Audio Tracks Section | ||
Expanded( | ||
child: Padding( | ||
padding: const EdgeInsets.all(16.0), | ||
child: Column( | ||
crossAxisAlignment: CrossAxisAlignment.start, | ||
children: [ | ||
Row( | ||
children: [ | ||
const Text( | ||
'Available Audio Tracks:', | ||
style: TextStyle( | ||
fontSize: 18, | ||
fontWeight: FontWeight.bold, | ||
), | ||
), | ||
const Spacer(), | ||
if (_isLoading) | ||
const SizedBox( | ||
width: 20, | ||
height: 20, | ||
child: CircularProgressIndicator(strokeWidth: 2), | ||
), | ||
], | ||
), | ||
const SizedBox(height: 16), | ||
if (_audioTracks.isEmpty && !_isLoading) | ||
const Text( | ||
'No audio tracks found or video not initialized.', | ||
style: TextStyle(color: Colors.grey), | ||
) | ||
else | ||
Expanded( | ||
child: ListView.builder( | ||
itemCount: _audioTracks.length, | ||
itemBuilder: (context, index) { | ||
final track = _audioTracks[index]; | ||
return Card( | ||
margin: const EdgeInsets.only(bottom: 8), | ||
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, | ||
style: TextStyle( | ||
fontWeight: track.isSelected | ||
? FontWeight.bold | ||
: FontWeight.normal, | ||
), | ||
), | ||
subtitle: Column( | ||
crossAxisAlignment: CrossAxisAlignment.start, | ||
children: [ | ||
Text('ID: ${track.id}'), | ||
Text('Language: ${track.language}'), | ||
], | ||
), | ||
trailing: track.isSelected | ||
? const Chip( | ||
label: Text('Selected'), | ||
backgroundColor: Colors.green, | ||
labelStyle: | ||
TextStyle(color: Colors.white), | ||
) | ||
: null, | ||
), | ||
); | ||
}, | ||
), | ||
), | ||
], | ||
), | ||
), | ||
), | ||
], | ||
), | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ library; | |
|
||
import 'package:flutter/material.dart'; | ||
import 'package:video_player/video_player.dart'; | ||
import 'package:video_player_platform_interface/video_player_platform_interface.dart'; | ||
|
||
void main() { | ||
runApp( | ||
|
@@ -295,6 +296,8 @@ class _BumbleBeeRemoteVideo extends StatefulWidget { | |
|
||
class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { | ||
late VideoPlayerController _controller; | ||
List<VideoAudioTrack> _audioTracks = []; | ||
bool _isLoadingTracks = false; | ||
|
||
Future<ClosedCaptionFile> _loadCaptions() async { | ||
final String fileContents = await DefaultAssetBundle.of(context) | ||
|
@@ -349,6 +352,96 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { | |
), | ||
), | ||
), | ||
// Audio Tracks Button and Display | ||
Padding( | ||
padding: const EdgeInsets.all(16.0), | ||
child: Column( | ||
children: [ | ||
ElevatedButton.icon( | ||
onPressed: () async { | ||
if (_controller.value.isInitialized) { | ||
final audioTracks = await _controller.getAudioTracks(); | ||
setState(() { | ||
_audioTracks = audioTracks; | ||
_isLoadingTracks = false; | ||
}); | ||
} | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
|
||
icon: _isLoadingTracks | ||
? const SizedBox( | ||
width: 16, | ||
height: 16, | ||
child: CircularProgressIndicator(strokeWidth: 2), | ||
) | ||
: const Icon(Icons.audiotrack), | ||
label: const Text('Get Audio Tracks'), | ||
), | ||
const SizedBox(height: 16), | ||
if (_audioTracks.isNotEmpty) ...[ | ||
const Text( | ||
'Available Audio Tracks:', | ||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), | ||
), | ||
const SizedBox(height: 8), | ||
...(_audioTracks.map((track) => Card( | ||
margin: const EdgeInsets.symmetric(vertical: 4), | ||
child: ListTile( | ||
leading: CircleAvatar( | ||
backgroundColor: | ||
track.isSelected ? Colors.green : Colors.grey, | ||
child: Icon( | ||
track.isSelected ? Icons.check : Icons.audiotrack, | ||
color: Colors.white, | ||
size: 16, | ||
), | ||
), | ||
title: Text( | ||
track.label, | ||
style: TextStyle( | ||
fontWeight: track.isSelected | ||
? FontWeight.bold | ||
: FontWeight.normal, | ||
), | ||
), | ||
subtitle: Column( | ||
crossAxisAlignment: CrossAxisAlignment.start, | ||
children: [ | ||
Text( | ||
'Language: ${track.language} | ID: ${track.id}'), | ||
if (track.bitrate != null || | ||
track.sampleRate != null || | ||
track.channelCount != null || | ||
track.codec != null) | ||
Text( | ||
track.qualityDescription, | ||
style: const TextStyle( | ||
fontSize: 12, | ||
color: Colors.blue, | ||
fontWeight: FontWeight.w500, | ||
), | ||
), | ||
], | ||
), | ||
trailing: track.isSelected | ||
? const Chip( | ||
label: Text('Selected', | ||
style: TextStyle(fontSize: 12)), | ||
backgroundColor: Colors.green, | ||
labelStyle: TextStyle(color: Colors.white), | ||
) | ||
: null, | ||
), | ||
))), | ||
] else if (_audioTracks.isEmpty && !_isLoadingTracks) ...[ | ||
const Text( | ||
'No audio tracks found. Click "Get Audio Tracks" to retrieve them.', | ||
style: TextStyle(color: Colors.grey), | ||
textAlign: TextAlign.center, | ||
), | ||
], | ||
], | ||
), | ||
), | ||
], | ||
), | ||
); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example video URL points to
BigBuckBunny.mp4
, which is unlikely to have multiple audio tracks. This makes the demo less effective at showcasing the new audio track selection feature. Consider using a test stream that is known to have multiple audio tracks, such as an HLS stream, to make the example more helpful for developers.For example, Apple provides test streams that could be used here: