import 'dart:io'; import 'package:injectable/injectable.dart'; @singleton class VideoDownloader { static const cacheDir = 'cache/video'; static const int maxWidth = 480; static const int maxHeight = 320; /// Downloads and resizes a YouTube video /// Returns the path to the resized video file, or null on failure Future downloadAndResize(String videoId) async { final videoDir = Directory('$cacheDir/$videoId'); final originalVideoFile = File('${videoDir.path}/video_original.mp4'); final resizedVideoFile = File('${videoDir.path}/video.mp4'); // Check if resized video already exists in cache if (await resizedVideoFile.exists()) { return resizedVideoFile.path; } // Create cache directory if it doesn't exist if (!await videoDir.exists()) { await videoDir.create(recursive: true); } final youtubeUrl = 'https://www.youtube.com/watch?v=$videoId'; try { // Use yt-dlp to download the video final downloadResult = await Process.run( 'yt-dlp', [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', '-o', originalVideoFile.path, '--merge-output-format', 'mp4', youtubeUrl, ], ); if (downloadResult.exitCode != 0) { print('Failed to download video $videoId: ${downloadResult.stderr}'); await _cleanup(videoDir); return null; } // Resize video to fit within maxWidth x maxHeight using ffmpeg final resizeResult = await Process.run( 'ffmpeg', [ '-i', originalVideoFile.path, '-vf', 'scale=w=$maxWidth:h=$maxHeight:force_original_aspect_ratio=decrease', '-c:v', 'libx264', '-preset', 'medium', '-crf', '23', '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', '-y', resizedVideoFile.path, ], ); if (resizeResult.exitCode != 0) { print('Failed to resize video $videoId: ${resizeResult.stderr}'); await _cleanup(videoDir); return null; } // Delete original video to save space if (await originalVideoFile.exists()) { await originalVideoFile.delete(); } return resizedVideoFile.path; } catch (e) { print('Error processing video $videoId: $e'); await _cleanup(videoDir); return null; } } /// Check if video is already cached Future isCached(String videoId) async { final resizedVideoFile = File('$cacheDir/$videoId/video.mp4'); return resizedVideoFile.exists(); } /// Get all cached video IDs Future> getCachedVideos() async { final dir = Directory(cacheDir); if (!await dir.exists()) { return []; } final videos = []; await for (final entity in dir.list()) { if (entity is Directory) { final videoId = entity.path.split('/').last; videos.add(videoId); } } return videos; } Future _cleanup(Directory videoDir) async { if (await videoDir.exists()) { await videoDir.delete(recursive: true); } } }