Files
plezy/lib/services/external_player_service.dart
T

102 lines
3.6 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/external_player_models.dart';
import '../models/plex_metadata.dart';
import '../utils/app_logger.dart';
import '../utils/snackbar_helper.dart';
import '../i18n/strings.g.dart';
import 'plex_client.dart';
import 'settings_service.dart';
const _externalPlayerChannel = MethodChannel('com.plezy/external_player');
class ExternalPlayerService {
/// Launch an external player with either a pre-resolved [videoUrl] (e.g. local
/// file path for downloaded content) or by fetching the streaming URL from [client].
static Future<bool> launch({
required BuildContext context,
PlexMetadata? metadata,
PlexClient? client,
int mediaIndex = 0,
String? videoUrl,
}) async {
try {
String resolvedUrl;
if (videoUrl != null) {
resolvedUrl = videoUrl;
} else if (client != null && metadata != null) {
final playbackData = await client.getVideoPlaybackData(metadata.ratingKey, mediaIndex: mediaIndex);
if (!playbackData.hasValidVideoUrl) {
if (context.mounted) {
showErrorSnackBar(context, t.messages.fileInfoNotAvailable);
}
return false;
}
resolvedUrl = playbackData.videoUrl!;
} else {
appLogger.e('ExternalPlayerService.launch requires either videoUrl or client+metadata');
return false;
}
final settings = await SettingsService.getInstance();
final player = settings.read(SettingsService.selectedExternalPlayer);
// On Android, always use native intent to avoid url_launcher opening in browser
if (Platform.isAndroid && context.mounted) {
return _launchAndroidNative(resolvedUrl, player, context);
}
final launched = await player.launch(resolvedUrl);
if (!launched && context.mounted) {
showErrorSnackBar(context, t.externalPlayer.appNotInstalled(name: player.name));
}
return launched;
} catch (e) {
appLogger.e('Failed to launch external player', error: e);
if (context.mounted) {
showErrorSnackBar(context, t.externalPlayer.launchFailed);
}
return false;
}
}
/// Launch a video on Android using native ACTION_VIEW intent.
/// Handles local files (file://, content://, absolute paths) and remote URLs.
static Future<bool> _launchAndroidNative(String url, ExternalPlayer player, BuildContext context) async {
try {
await _externalPlayerChannel.invokeMethod<bool>('openVideo', {
'filePath': url,
if (player.id != 'system_default') 'package': _getAndroidPackage(player),
});
return true;
} on PlatformException catch (e) {
if (e.code == 'APP_NOT_FOUND' && context.mounted) {
showErrorSnackBar(context, t.externalPlayer.appNotInstalled(name: player.name));
} else if (context.mounted) {
showErrorSnackBar(context, t.externalPlayer.launchFailed);
}
return false;
}
}
/// Map known player IDs to their Android package names.
static String? _getAndroidPackage(ExternalPlayer player) {
const packageMap = {
'vlc': 'org.videolan.vlc',
'mpv': 'is.xyz.mpv',
'mx_player': 'com.mxtech.videoplayer.ad',
'just_player': 'com.brouken.player',
};
// Known players
if (packageMap.containsKey(player.id)) return packageMap[player.id];
// Custom command-type players use the value as package name on Android
if (player.isCustom && player.customType == CustomPlayerType.command) return player.customValue;
return null;
}
}