Device Media Finder
A Flutter plugin for accessing media files (videos and audio) on the device with support for various formats and thumbnails.
Features
- Get videos from the device
- Get videos filtered by specific MIME types
- Get audio files from the device
- Generate thumbnails for videos
- Fetch videos with thumbnails in a single call
Installation
Add this to your package's pubspec.yaml file:
dependencies:
device_media_finder: ^0.0.1
Usage
First, import the package:
import 'package:device_media_finder/device_media_finder.dart';
Then create an instance of DeviceMediaFinder:
final deviceMediaFinder = DeviceMediaFinder();
Get All Videos
Retrieve all videos from the device:
Future<void> getAllVideos() async {
try {
final videos = await deviceMediaFinder.getVideos();
print('Found ${videos.length} videos');
// Access video properties
for (final video in videos) {
print('Video name: ${video.name}');
print('Video size: ${(video.size / (1024 * 1024)).toStringAsFixed(2)} MB');
print('Video duration: ${_formatDuration(video.duration)}');
print('Video path: ${video.path}');
print('Video MIME type: ${video.mimeType}');
}
} catch (e) {
print('Error getting videos: $e');
}
}
String _formatDuration(int milliseconds) {
final seconds = (milliseconds / 1000).floor();
final minutes = (seconds / 60).floor();
final hours = (minutes / 60).floor();
final remainingMinutes = minutes % 60;
final remainingSeconds = seconds % 60;
if (hours > 0) {
return '$hours:${remainingMinutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
} else {
return '$minutes:${remainingSeconds.toString().padLeft(2, '0')}';
}
}
Example 2: Display videos in a ListView:
class VideoListScreen extends StatefulWidget {
@override
_VideoListScreenState createState() => _VideoListScreenState();
}
class _VideoListScreenState extends State<VideoListScreen> {
final deviceMediaFinder = DeviceMediaFinder();
List<VideoFile> videos = [];
bool isLoading = false;
@override
void initState() {
super.initState();
_loadVideos();
}
Future<void> _loadVideos() async {
setState(() {
isLoading = true;
});
try {
final result = await deviceMediaFinder.getVideos();
setState(() {
videos = result;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error loading videos: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Videos')),
body: isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: videos.length,
itemBuilder: (context, index) {
final video = videos[index];
return ListTile(
title: Text(video.name),
subtitle: Text('${(video.size / (1024 * 1024)).toStringAsFixed(2)} MB'),
trailing: Text(_formatDuration(video.duration)),
);
},
),
);
}
}
Get Videos by MIME Type
Filter videos by specific MIME types:
Future<void> getVideosByFormat() async {
try {
// Get only MP4 and 3GPP videos
final videos = await deviceMediaFinder.getVideosByMimeType([
'video/mp4',
'video/3gpp',
'video/*', // Fallback to catch any other video types
]);
print('Found ${videos.length} MP4 and 3GPP videos');
// Process the videos
for (final video in videos) {
print('Video name: ${video.name}');
print('Video MIME type: ${video.mimeType}');
}
} catch (e) {
print('Error getting videos by MIME type: $e');
}
}
Example 2: Get videos of multiple formats:
Future<void> getAllVideoFormats() async {
try {
// Get videos of various formats
final videos = await deviceMediaFinder.getVideosByMimeType([
'video/mp4',
'video/3gpp',
'video/webm',
'video/quicktime',
'video/x-matroska',
'video/avi',
'video/mpeg',
'video/x-ms-wmv',
'video/*', // Fallback to catch any other video types
]);
// Group videos by format
final videosByFormat = <String, List<VideoFile>>{};
for (final video in videos) {
final format = video.mimeType;
if (!videosByFormat.containsKey(format)) {
videosByFormat[format] = [];
}
videosByFormat[format]!.add(video);
}
// Print summary
videosByFormat.forEach((format, formatVideos) {
print('Format: $format, Count: ${formatVideos.length}');
});
} catch (e) {
print('Error getting videos by format: $e');
}
}
Get Audio Files
Retrieve audio files from the device:
Future<void> getAudioFiles() async {
try {
final audios = await deviceMediaFinder.getAudios();
print('Found ${audios.length} audio files');
// Access audio properties
for (final audio in audios) {
print('Audio name: ${audio.name}');
print('Artist: ${audio.artist}');
print('Album: ${audio.album}');
print('Duration: ${_formatDuration(audio.duration)}');
print('Size: ${(audio.size / (1024 * 1024)).toStringAsFixed(2)} MB');
}
} catch (e) {
print('Error getting audio files: $e');
}
}
Example 2: Display audio files in a ListView with artist and album information:
class AudioListScreen extends StatefulWidget {
@override
_AudioListScreenState createState() => _AudioListScreenState();
}
class _AudioListScreenState extends State<AudioListScreen> {
final deviceMediaFinder = DeviceMediaFinder();
List<AudioFile> audios = [];
bool isLoading = false;
@override
void initState() {
super.initState();
_loadAudios();
}
Future<void> _loadAudios() async {
setState(() {
isLoading = true;
});
try {
final result = await deviceMediaFinder.getAudios();
setState(() {
audios = result;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error loading audio files: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Music')),
body: isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: audios.length,
itemBuilder: (context, index) {
final audio = audios[index];
return ListTile(
leading: CircleAvatar(child: Icon(Icons.music_note)),
title: Text(audio.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Artist: ${audio.artist}'),
Text('Album: ${audio.album}'),
],
),
trailing: Text(_formatDuration(audio.duration)),
);
},
),
);
}
}
Get Video Thumbnails
Generate thumbnails for videos:
Future<void> getVideoThumbnail(String videoId) async {
try {
// Get thumbnail with default size (128x128)
final thumbnail = await deviceMediaFinder.getVideoThumbnail(videoId);
if (thumbnail != null) {
print('Thumbnail size: ${thumbnail.length} bytes');
// Use the thumbnail in an Image widget
final image = Image.memory(thumbnail);
// Or save it to a file
final file = File('path/to/save/thumbnail.jpg');
await file.writeAsBytes(thumbnail);
} else {
print('Failed to generate thumbnail');
}
} catch (e) {
print('Error getting thumbnail: $e');
}
}
Example 2: Get a larger thumbnail:
Future<Widget> getVideoThumbnailWidget(String videoId) async {
try {
// Get a larger thumbnail (256x256)
final thumbnail = await deviceMediaFinder.getVideoThumbnail(
videoId,
width: 256,
height: 256,
);
if (thumbnail != null) {
// Return an image widget with the thumbnail
return Image.memory(
thumbnail,
fit: BoxFit.cover,
width: 256,
height: 256,
);
} else {
// Return a placeholder if thumbnail generation failed
return Container(
width: 256,
height: 256,
color: Colors.grey,
child: Icon(Icons.video_file, size: 64),
);
}
} catch (e) {
print('Error getting thumbnail: $e');
// Return an error placeholder
return Container(
width: 256,
height: 256,
color: Colors.red.withOpacity(0.3),
child: Icon(Icons.error, size: 64),
);
}
}
Get Video with Thumbnail
Fetch a video and its thumbnail in a single call:
Future<void> getVideoWithThumbnail(String videoId) async {
try {
// Get the video with its thumbnail
final video = await deviceMediaFinder.getVideoWithThumbnail(videoId);
print('Video name: ${video.name}');
print('Video duration: ${_formatDuration(video.duration)}');
if (video.thumbnailData != null) {
print('Thumbnail size: ${video.thumbnailData!.length} bytes');
// Use the thumbnail in an Image widget
final image = Image.memory(video.thumbnailData!);
} else {
print('No thumbnail available');
}
} catch (e) {
print('Error getting video with thumbnail: $e');
}
}
Example 2: Display a video with its thumbnail in a card:
class VideoDetailCard extends StatelessWidget {
final String videoId;
final DeviceMediaFinder deviceMediaFinder = DeviceMediaFinder();
VideoDetailCard({required this.videoId});
@override
Widget build(BuildContext context) {
return FutureBuilder<VideoFile>(
future: deviceMediaFinder.getVideoWithThumbnail(
videoId,
width: 192,
height: 108, // 16:9 aspect ratio
),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Card(
child: Container(
height: 200,
child: Center(child: CircularProgressIndicator()),
),
);
}
if (snapshot.hasError) {
return Card(
child: Container(
height: 200,
child: Center(child: Text('Error loading video')),
),
);
}
if (!snapshot.hasData) {
return Card(
child: Container(
height: 200,
child: Center(child: Text('Video not found')),
),
);
}
final video = snapshot.data!;
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Thumbnail
video.thumbnailData != null
? Image.memory(
video.thumbnailData!,
height: 180,
width: double.infinity,
fit: BoxFit.cover,
)
: Container(
height: 180,
width: double.infinity,
color: Colors.grey,
child: Icon(Icons.video_file, size: 64),
),
// Video details
Padding(
padding: EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
video.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Row(
children: [
Text('Duration: ${_formatDuration(video.duration)}'),
Spacer(),
Text('${(video.size / (1024 * 1024)).toStringAsFixed(2)} MB'),
],
),
],
),
),
],
),
);
},
);
}
}
Get All Videos with Auto-Generated Thumbnails
Fetch all videos with their thumbnails in a single call:
Future<void> loadAllVideosWithThumbnails() async {
try {
// Get all videos with thumbnails (default thumbnail size: 128x128)
final videos = await deviceMediaFinder.getVideosWithAutoThumbnails();
print('Loaded ${videos.length} videos with thumbnails');
// Count videos that have thumbnails
final videosWithThumbnails = videos.where((v) => v.thumbnailData != null).length;
print('$videosWithThumbnails videos have thumbnails');
// Use the videos with thumbnails
for (final video in videos) {
if (video.thumbnailData != null) {
// Use the thumbnail
final thumbnailWidget = Image.memory(video.thumbnailData!);
}
}
} catch (e) {
print('Error loading videos with thumbnails: $e');
}
}
Example 2: Display videos in a grid with custom thumbnail size:
class VideoGridScreen extends StatefulWidget {
@override
_VideoGridScreenState createState() => _VideoGridScreenState();
}
class _VideoGridScreenState extends State<VideoGridScreen> {
final deviceMediaFinder = DeviceMediaFinder();
bool isLoading = true;
List<VideoFile> videos = [];
@override
void initState() {
super.initState();
_loadVideos();
}
Future<void> _loadVideos() async {
setState(() {
isLoading = true;
});
try {
// Get videos with larger thumbnails for better quality in the grid
final result = await deviceMediaFinder.getVideosWithAutoThumbnails(
width: 256,
height: 144, // 16:9 aspect ratio
);
setState(() {
videos = result;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error loading videos: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Video Gallery')),
body: isLoading
? Center(child: CircularProgressIndicator())
: videos.isEmpty
? Center(child: Text('No videos found'))
: GridView.builder(
padding: EdgeInsets.all(8),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 16 / 9,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: videos.length,
itemBuilder: (context, index) {
final video = videos[index];
return GestureDetector(
onTap: () {
// Handle video tap (e.g., play the video)
print('Play video: ${video.path}');
},
child: Stack(
fit: StackFit.expand,
children: [
// Thumbnail
video.thumbnailData != null
? Image.memory(
video.thumbnailData!,
fit: BoxFit.cover,
)
: Container(
color: Colors.grey.shade300,
child: Icon(Icons.video_file, size: 48),
),
// Video name overlay
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
color: Colors.black.withOpacity(0.5),
padding: EdgeInsets.all(4),
child: Text(
video.name,
style: TextStyle(color: Colors.white),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
// Play icon overlay
Center(
child: Icon(
Icons.play_circle_outline,
color: Colors.white.withOpacity(0.7),
size: 48,
),
),
],
),
);
},
),
);
}
}
Generate Thumbnails for a List of Videos
Generate thumbnails for a list of videos you already have:
Future<void> getThumbnailsForFilteredVideos() async {
try {
// First get all videos
final allVideos = await deviceMediaFinder.getVideos();
// Filter videos (e.g., only MP4 videos)
final mp4Videos = allVideos.where(
(video) => video.mimeType == 'video/mp4'
).toList();
print('Found ${mp4Videos.length} MP4 videos');
// Generate thumbnails for the filtered videos
final videosWithThumbnails = await deviceMediaFinder.generateThumbnailsForVideos(
mp4Videos,
width: 192,
height: 108, // 16:9 aspect ratio
);
// Use the videos with thumbnails
for (final video in videosWithThumbnails) {
if (video.thumbnailData != null) {
print('Video ${video.name} has a thumbnail');
}
}
} catch (e) {
print('Error: $e');
}
}
Example 2: Get thumbnails for recent videos:
Future<List<VideoFile>> getRecentVideosWithThumbnails() async {
final deviceMediaFinder = DeviceMediaFinder();
try {
// Get all videos
final allVideos = await deviceMediaFinder.getVideos();
// Filter videos added in the last 7 days
final now = DateTime.now();
final oneWeekAgo = now.subtract(Duration(days: 7));
final recentVideos = allVideos.where((video) {
final dateAdded = DateTime.fromMillisecondsSinceEpoch(video.dateAdded * 1000);
return dateAdded.isAfter(oneWeekAgo);
}).toList();
print('Found ${recentVideos.length} videos from the last 7 days');
// Generate thumbnails for the recent videos
final videosWithThumbnails = await deviceMediaFinder.generateThumbnailsForVideos(
recentVideos,
);
return videosWithThumbnails;
} catch (e) {
print('Error getting recent videos with thumbnails: $e');
return [];
}
}
Permissions
Android
Add the following permissions to your AndroidManifest.xml:
<!-- For Android 13 and above -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- For devices with Android 12 or lower -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
iOS
Add the following to your Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to your photo library to display your videos and generate thumbnails.</string>
Supported Formats
Video Formats
- MP4 (video/mp4)
- 3GPP (video/3gpp)
- WebM (video/webm)
- QuickTime (video/quicktime)
- Matroska (video/x-matroska)
- AVI (video/avi)
- MPEG (video/mpeg)
- WMV (video/x-ms-wmv)
- And others (using video/*)
Audio Formats
- MP3
- AAC
- WAV
- FLAC
- OGG
- And others supported by the device
Playing Media Files
Playing Videos from Thumbnails
When you display thumbnails, you can use the video's path or URI to play it when the user taps on the thumbnail:
import 'package:video_player/video_player.dart';
class VideoThumbnailPlayer extends StatefulWidget {
final VideoFile video;
const VideoThumbnailPlayer({Key? key, required this.video}) : super(key: key);
@override
_VideoThumbnailPlayerState createState() => _VideoThumbnailPlayerState();
}
class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
late VideoPlayerController _controller;
bool _isPlaying = false;
@override
void initState() {
super.initState();
// Initialize the controller with the video file path
_controller = VideoPlayerController.file(File(widget.video.path))
..initialize().then((_) {
setState(() {});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
GestureDetector(
onTap: () {
setState(() {
if (_controller.value.isInitialized) {
if (_isPlaying) {
_controller.pause();
} else {
_controller.play();
}
_isPlaying = !_isPlaying;
}
});
},
child: Stack(
alignment: Alignment.center,
children: [
// Show thumbnail until video is initialized
if (!_controller.value.isInitialized && widget.video.thumbnailData != null)
Image.memory(
widget.video.thumbnailData!,
width: 320,
height: 180,
fit: BoxFit.cover,
)
else if (_controller.value.isInitialized)
AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
else
Container(
width: 320,
height: 180,
color: Colors.black,
child: Center(child: CircularProgressIndicator()),
),
// Show play/pause button overlay
if (!_controller.value.isInitialized || !_isPlaying)
Icon(
Icons.play_circle_fill,
size: 64,
color: Colors.white.withOpacity(0.7),
),
],
),
),
// Video details
Padding(
padding: EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.video.name,
style: TextStyle(fontWeight: FontWeight.bold),
),
Text('Duration: ${_formatDuration(widget.video.duration)}'),
Text('Path: ${widget.video.path}'),
],
),
),
],
);
}
String _formatDuration(int milliseconds) {
final seconds = (milliseconds / 1000).floor();
final minutes = (seconds / 60).floor();
final hours = (minutes / 60).floor();
final remainingMinutes = minutes % 60;
final remainingSeconds = seconds % 60;
if (hours > 0) {
return '$hours:${remainingMinutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
} else {
return '$minutes:${remainingSeconds.toString().padLeft(2, '0')}';
}
}
}
Using Video Thumbnails in a Grid with Playback
Here's an example of how to create a video gallery with thumbnails that can play videos when tapped:
class VideoGalleryScreen extends StatefulWidget {
@override
_VideoGalleryScreenState createState() => _VideoGalleryScreenState();
}
class _VideoGalleryScreenState extends State<VideoGalleryScreen> {
final deviceMediaFinder = DeviceMediaFinder();
List<VideoFile> videos = [];
bool isLoading = true;
@override
void initState() {
super.initState();
_loadVideos();
}
Future<void> _loadVideos() async {
setState(() {
isLoading = true;
});
try {
// Get videos with thumbnails
final result = await deviceMediaFinder.getVideosWithAutoThumbnails(
width: 256,
height: 144,
);
setState(() {
videos = result;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error loading videos: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Video Gallery')),
body: isLoading
? Center(child: CircularProgressIndicator())
: GridView.builder(
padding: EdgeInsets.all(8),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 16 / 9,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: videos.length,
itemBuilder: (context, index) {
final video = videos[index];
return GestureDetector(
onTap: () {
// Navigate to a video player screen when thumbnail is tapped
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => VideoPlayerScreen(video: video),
),
);
},
child: Stack(
fit: StackFit.expand,
children: [
// Thumbnail
video.thumbnailData != null
? Image.memory(
video.thumbnailData!,
fit: BoxFit.cover,
)
: Container(
color: Colors.grey.shade300,
child: Icon(Icons.video_file, size: 48),
),
// Video name overlay
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
color: Colors.black.withOpacity(0.5),
padding: EdgeInsets.all(4),
child: Text(
video.name,
style: TextStyle(color: Colors.white),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
// Play icon overlay
Center(
child: Icon(
Icons.play_circle_outline,
color: Colors.white.withOpacity(0.7),
size: 48,
),
),
],
),
);
},
),
);
}
}
// Video player screen that uses the video path to play the video
class VideoPlayerScreen extends StatefulWidget {
final VideoFile video;
const VideoPlayerScreen({Key? key, required this.video}) : super(key: key);
@override
_VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}
class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
late VideoPlayerController _controller;
bool _isInitialized = false;
@override
void initState() {
super.initState();
// Initialize the controller with the video file path
_controller = VideoPlayerController.file(File(widget.video.path))
..initialize().then((_) {
setState(() {
_isInitialized = true;
// Auto-play when ready
_controller.play();
});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.video.name)),
body: Column(
children: [
// Video player
_isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)
: AspectRatio(
aspectRatio: 16 / 9,
child: Center(child: CircularProgressIndicator()),
),
// Video controls
VideoProgressIndicator(
_controller,
allowScrubbing: true,
padding: EdgeInsets.all(16),
),
// Play/pause button
IconButton(
icon: Icon(
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
),
onPressed: () {
setState(() {
if (_controller.value.isPlaying) {
_controller.pause();
} else {
_controller.play();
}
});
},
),
// Video details
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'File Details',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
SizedBox(height: 8),
Text('Name: ${widget.video.name}'),
Text('Size: ${(widget.video.size / (1024 * 1024)).toStringAsFixed(2)} MB'),
Text('Duration: ${_formatDuration(widget.video.duration)}'),
Text('Path: ${widget.video.path}'),
Text('MIME Type: ${widget.video.mimeType}'),
],
),
),
],
),
);
}
String _formatDuration(int milliseconds) {
final seconds = (milliseconds / 1000).floor();
final minutes = (seconds / 60).floor();
final hours = (minutes / 60).floor();
final remainingMinutes = minutes % 60;
final remainingSeconds = seconds % 60;
if (hours > 0) {
return '$hours:${remainingMinutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
} else {
return '$minutes:${remainingSeconds.toString().padLeft(2, '0')}';
}
}
}
Playing Audio Files
Similarly, you can use the path or URI of audio files to play them:
import 'package:audioplayers/audioplayers.dart';
class AudioPlayerWidget extends StatefulWidget {
final AudioFile audio;
const AudioPlayerWidget({Key? key, required this.audio}) : super(key: key);
@override
_AudioPlayerWidgetState createState() => _AudioPlayerWidgetState();
}
class _AudioPlayerWidgetState extends State<AudioPlayerWidget> {
final AudioPlayer _audioPlayer = AudioPlayer();
bool _isPlaying = false;
Duration _duration = Duration.zero;
Duration _position = Duration.zero;
@override
void initState() {
super.initState();
// Listen to player state changes
_audioPlayer.onPlayerStateChanged.listen((state) {
setState(() {
_isPlaying = state == PlayerState.playing;
});
});
// Listen to duration changes
_audioPlayer.onDurationChanged.listen((newDuration) {
setState(() {
_duration = newDuration;
});
});
// Listen to position changes
_audioPlayer.onPositionChanged.listen((newPosition) {
setState(() {
_position = newPosition;
});
});
// Set the source to the audio file path
_audioPlayer.setSource(DeviceFileSource(widget.audio.path));
}
@override
void dispose() {
_audioPlayer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(16),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Audio info
Row(
children: [
CircleAvatar(
backgroundColor: Colors.blue.shade200,
child: Icon(Icons.music_note),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.audio.name,
style: TextStyle(fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
'${widget.audio.artist} • ${widget.audio.album}',
style: TextStyle(fontSize: 12, color: Colors.grey.shade700),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
SizedBox(height: 16),
// Progress bar
Slider(
min: 0,
max: _duration.inSeconds.toDouble(),
value: _position.inSeconds.toDouble(),
onChanged: (value) {
final position = Duration(seconds: value.toInt());
_audioPlayer.seek(position);
},
),
// Time indicators
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_formatDuration(_position)),
Text(_formatDuration(_duration)),
],
),
SizedBox(height: 16),
// Controls
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.replay_10),
onPressed: () {
_audioPlayer.seek(Duration(seconds: _position.inSeconds - 10));
},
),
IconButton(
iconSize: 48,
icon: Icon(_isPlaying ? Icons.pause_circle_filled : Icons.play_circle_filled),
onPressed: () {
if (_isPlaying) {
_audioPlayer.pause();
} else {
_audioPlayer.resume();
}
},
),
IconButton(
icon: Icon(Icons.forward_10),
onPressed: () {
_audioPlayer.seek(Duration(seconds: _position.inSeconds + 10));
},
),
],
),
SizedBox(height: 16),
// File details
Text('Path: ${widget.audio.path}'),
Text('Size: ${(widget.audio.size / (1024 * 1024)).toStringAsFixed(2)} MB'),
],
),
),
);
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = twoDigits(duration.inHours);
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return [
if (duration.inHours > 0) hours,
minutes,
seconds,
].join(':');
}
}
Troubleshooting
If you're having trouble finding videos or audio files:
- Make sure you've granted the necessary permissions
- Try using the
getVideosByMimeTypemethod with specific formats - Check the logs for any error messages
- Verify that the files exist on the device and are valid media files