import 'dart:convert'; import 'dart:io'; import 'package:url_launcher/url_launcher.dart'; import '../utils/app_logger.dart'; typedef PlayerLauncher = Future Function(String url); enum CustomPlayerType { command, urlScheme } class ExternalPlayer { final String id; final String name; final String? iconAsset; final bool isAvailable; final PlayerLauncher launch; final bool isCustom; final String? customValue; // Only for custom player serialization final CustomPlayerType? customType; ExternalPlayer({ required this.id, required this.name, this.iconAsset, this.isAvailable = true, required this.launch, this.isCustom = false, this.customValue, this.customType, }); factory ExternalPlayer.custom({ required String id, required String name, required String value, required CustomPlayerType type, }) { return ExternalPlayer( id: id, name: name, isCustom: true, customValue: value, customType: type, launch: (url) => _launchCustom(value, url, type), ); } factory ExternalPlayer.fromJson(Map json) { final id = json['id'] as String; final known = KnownPlayers.findById(id); if (known != null) return known; return ExternalPlayer.custom( id: id, name: json['name'] as String, value: json['customValue'] as String? ?? '', type: json['customType'] == 'urlScheme' ? CustomPlayerType.urlScheme : CustomPlayerType.command, ); } Map toJson() => { 'id': id, 'name': name, if (isCustom) 'isCustom': true, if (customValue != null) 'customValue': customValue, if (customType != null) 'customType': customType == CustomPlayerType.urlScheme ? 'urlScheme' : 'command', }; String toJsonString() => json.encode(toJson()); static ExternalPlayer fromJsonString(String jsonString) => ExternalPlayer.fromJson(json.decode(jsonString) as Map); @override bool operator ==(Object other) => identical(this, other) || other is ExternalPlayer && runtimeType == other.runtimeType && id == other.id; @override int get hashCode => id.hashCode; } // --- Launch helpers --- Future _launchWithUrl(String url) { return launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } Future _launchAndroidIntent(String url, {required String package}) async { final intentUri = Uri.parse( 'intent:$url#Intent;' 'action=android.intent.action.VIEW;' 'type=video/*;' 'package=$package;' 'end', ); try { return await launchUrl(intentUri, mode: LaunchMode.externalApplication); } catch (_) { return _launchWithUrl(url); } } Future _launchUrlScheme(String scheme, String url) async { final playerUrl = scheme.contains('url=') ? '$scheme${Uri.encodeComponent(url)}' : '$scheme$url'; final uri = Uri.parse(playerUrl); if (await canLaunchUrl(uri)) { return launchUrl(uri, mode: LaunchMode.externalApplication); } return false; } Future _launchMacApp(String appName, String url) async { try { await Process.start('open', ['-a', appName, url], mode: ProcessStartMode.detached); return true; } catch (e) { appLogger.w('Failed to launch $appName via open -a', error: e); return false; } } Future _launchCommand(String command, String url) async { try { final result = await Process.start(command, [url], mode: ProcessStartMode.detached); appLogger.d('Launched $command with PID: ${result.pid}'); return true; } catch (e) { appLogger.w('Failed to launch $command', error: e); return false; } } Future _launchCustom(String value, String url, CustomPlayerType type) async { if (type == CustomPlayerType.urlScheme) { return _launchUrlScheme(value, url); } // Command type if (Platform.isAndroid) { return _launchAndroidIntent(url, package: value); } else if (Platform.isMacOS) { // Try PATH first (e.g. mpv), fall back to open -a (e.g. VLC) if (await _launchCommand(value, url)) return true; return _launchMacApp(value, url); } else { return _launchCommand(value, url); } } // --- Known Players --- class KnownPlayers { static final systemDefault = ExternalPlayer(id: 'system_default', name: 'System Default', launch: _launchWithUrl); static final _allPlayers = [ systemDefault, ExternalPlayer( id: 'vlc', name: 'VLC', iconAsset: 'assets/player_icons/vlc.svg', isAvailable: Platform.isAndroid || Platform.isIOS || Platform.isMacOS || Platform.isLinux || Platform.isWindows, launch: (url) { if (Platform.isAndroid) return _launchAndroidIntent(url, package: 'org.videolan.vlc'); if (Platform.isIOS) return _launchUrlScheme('vlc://', url); if (Platform.isMacOS) return _launchMacApp('VLC', url); return _launchCommand('vlc', url); }, ), ExternalPlayer( id: 'mpv', name: 'mpv', iconAsset: 'assets/player_icons/mpv.svg', isAvailable: Platform.isAndroid || Platform.isMacOS || Platform.isLinux || Platform.isWindows, launch: (url) { if (Platform.isAndroid) return _launchAndroidIntent(url, package: 'is.xyz.mpv'); return _launchCommand('mpv', url); }, ), ExternalPlayer( id: 'iina', name: 'IINA', iconAsset: 'assets/player_icons/iina.png', isAvailable: Platform.isMacOS, launch: (url) => _launchUrlScheme('iina://weblink?url=', url), ), ExternalPlayer( id: 'mx_player', name: 'MX Player', iconAsset: 'assets/player_icons/mx_player.svg', isAvailable: Platform.isAndroid, launch: (url) => _launchAndroidIntent(url, package: 'com.mxtech.videoplayer.ad'), ), ExternalPlayer( id: 'just_player', name: 'Just Player', iconAsset: 'assets/player_icons/just_player.png', isAvailable: Platform.isAndroid, launch: (url) => _launchAndroidIntent(url, package: 'com.brouken.player'), ), ExternalPlayer( id: 'infuse', name: 'Infuse', iconAsset: 'assets/player_icons/infuse.png', isAvailable: Platform.isIOS, launch: (url) => _launchUrlScheme('infuse://x-callback-url/play?url=', url), ), ExternalPlayer( id: 'potplayer', name: 'PotPlayer', iconAsset: 'assets/player_icons/potplayer.png', isAvailable: Platform.isWindows, launch: (url) async { if (await _launchUrlScheme('potplayer://', url)) return true; return _launchCommand('PotPlayerMini64', url); }, ), ExternalPlayer( id: 'celluloid', name: 'Celluloid', iconAsset: 'assets/player_icons/celluloid.svg', isAvailable: Platform.isLinux, launch: (url) => _launchCommand('celluloid', url), ), ]; /// Get players available on the current platform static List getForCurrentPlatform() { return _allPlayers.where((p) => p.isAvailable).toList(); } /// Find a known player by ID static ExternalPlayer? findById(String id) { try { return _allPlayers.firstWhere((p) => p.id == id); } catch (_) { return null; } } }