device_media_finder 0.0.1
device_media_finder: ^0.0.1 copied to clipboard
A Flutter plugin for accessing media files (videos and audio) on the device with support for various formats and thumbnails.
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