fix: added external player support
Some checks are pending
Build and Deploy / build_windows (push) Waiting to run
Build and Deploy / build_android (push) Waiting to run
Build and Deploy / build_android_tv (push) Waiting to run
Build and Deploy / build_ipa (push) Waiting to run
Build and Deploy / build_linux (push) Waiting to run
Build and Deploy / build_macos (push) Waiting to run

This commit is contained in:
omkar 2025-01-31 19:38:57 +05:30
parent 4b95941e4a
commit f8af47b165
5 changed files with 281 additions and 39 deletions

View file

@ -0,0 +1,152 @@
import 'dart:io';
import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:universal_platform/universal_platform.dart';
import 'package:url_launcher/url_launcher.dart';
final _logger = Logger("ExternalPlayerService");
class ExternalPlayerService {
static Future<void> openInExternalPlayer({
required String videoUrl,
String? playerPackage,
}) async {
switch (UniversalPlatform.value) {
case UniversalPlatformType.Android:
await _openAndroid(videoUrl, playerPackage);
break;
case UniversalPlatformType.IOS:
await _openIOS(videoUrl, playerPackage);
break;
case UniversalPlatformType.MacOS:
await _openMacOS(videoUrl, playerPackage);
break;
case UniversalPlatformType.Windows:
await _openWindows(videoUrl);
break;
case UniversalPlatformType.Linux:
await _openLinux(videoUrl);
break;
default:
throw PlatformException(
code: 'UNSUPPORTED_PLATFORM',
message: 'Platform ${UniversalPlatform.value} is not supported',
);
}
}
static Future<void> _openAndroid(
String videoUrl, String? playerPackage) async {
final AndroidIntent intent = AndroidIntent(
action: 'action_view',
type: "video/*",
package: playerPackage,
data: videoUrl,
flags: const <int>[268435456],
arguments: {},
);
await intent.launch();
}
static Future<void> _openIOS(String videoUrl, String? customScheme) async {
if (customScheme != null) {
final encodedUrl = Uri.encodeComponent(videoUrl);
String customUrl = '$customScheme://$encodedUrl';
switch (customScheme) {
case "infuse":
customUrl = "infuse://x-callback-url/play?url=$encodedUrl";
break;
case "open-vidhub":
customUrl = "open-vidhub://x-callback-url/open?url=$encodedUrl";
break;
case "vlc":
customUrl = "vlc://$encodedUrl";
break;
case "outplayer":
customUrl = "outplayer://$encodedUrl";
break;
}
_logger.info("External player $customUrl");
if (await canLaunchUrl(Uri.parse(customUrl))) {
await launchUrl(Uri.parse(customUrl));
return;
}
}
await launchUrl(Uri.parse(videoUrl));
}
static Future<void> _openMacOS(String videoUrl, String? customScheme) async {
if (customScheme != null) {
final encodedUrl = Uri.encodeComponent(videoUrl);
String customUrl = '$customScheme://$encodedUrl';
switch (customScheme) {
case "infuse":
customUrl = "infuse://x-callback-url/play?url=$encodedUrl";
break;
case "open-vidhub":
customUrl = "open-vidhub://x-callback-url/open?url=$encodedUrl";
break;
case "iina":
customUrl = "iina://weblink?url=$encodedUrl";
break;
case "omniplayer":
customUrl = "omniplayer://$encodedUrl";
break;
case "nplayer-mac":
customUrl = "nplayer-mac://$encodedUrl";
break;
}
_logger.info("External player $customUrl for $customScheme");
if (await canLaunchUrl(Uri.parse(customUrl))) {
await launchUrl(Uri.parse(customUrl));
return;
}
}
await Process.run('open', [videoUrl]);
}
static Future<void> _openWindows(String videoUrl) async {
await Process.run('cmd', ['/c', 'start', videoUrl]);
}
static Future<void> _openLinux(String videoUrl) async {
try {
await Process.run('xdg-open', [videoUrl]);
} catch (e) {
final players = ['vlc', 'mpv', 'mplayer'];
bool launched = false;
for (final player in players) {
try {
final result = await Process.run('which', [player]);
if (result.exitCode == 0) {
await Process.run(player, [videoUrl]);
launched = true;
break;
}
} catch (e) {
continue;
}
}
if (!launched) {
throw PlatformException(
code: 'NO_PLAYER_FOUND',
message: 'No suitable video player found on Linux',
);
}
}
}
}

View file

@ -15,6 +15,7 @@ class AddToListButton extends StatefulWidget {
final Function()? onRemoved;
final String? listName;
final Widget? label;
final bool minimal;
const AddToListButton({
super.key,
@ -26,6 +27,7 @@ class AddToListButton extends StatefulWidget {
this.onRemoved,
this.listName,
this.label,
this.minimal = false,
}) : assert(
listName != null || child != null || icon != null,
'Either listName, child, or icon must be provided',
@ -320,6 +322,51 @@ class _AddToListButtonState extends State<AddToListButton> {
final colorScheme = theme.colorScheme;
if (widget.listName != null) {
final icon = _getListIcon(widget.listName!);
// Minimal mode UI
if (widget.minimal) {
return IconButton(
onPressed: _isLoading
? null
: () {
if (_existsInList &&
_existingItemId != null &&
_existingList != null) {
_removeFromList(context, _existingItemId!, _existingList!);
} else {
_createAndAddToList(context, widget.listName!);
}
},
icon: _isLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Icon(
_existsInList
? _getListIconUnselected(widget.listName!)
: icon,
color: _existsInList
? widget.listName?.toLowerCase() == 'favourites'
? Colors.red
: colorScheme.primary
: colorScheme.onSurfaceVariant,
),
style: IconButton.styleFrom(
backgroundColor: _existsInList
? widget.listName?.toLowerCase() == 'favourites'
? Colors.red.withOpacity(0.1)
: colorScheme.primary.withOpacity(0.1)
: null,
),
);
}
// Original UI for non-minimal mode
return ElevatedButton.icon(
onPressed: _isLoading
? null
@ -425,18 +472,33 @@ class _AddToListButtonState extends State<AddToListButton> {
_getListIcon(list.name),
size: 18,
),
child: Text(list.name),
child: _getListIcon(list.name) == Icons.folder_outlined
? Text(list.name)
: null,
),
],
);
}
IconData _getListIconUnselected(String name) {
switch (name.toLowerCase()) {
case 'watchlist':
return Icons.bookmark;
case 'favourites':
return Icons.favorite;
case 'watch later':
return Icons.watch_later;
default:
return Icons.folder;
}
}
IconData _getListIcon(String name) {
switch (name.toLowerCase()) {
case 'watchlist':
return Icons.bookmark_outlined;
return Icons.bookmark_add_outlined;
case 'favourites':
return Icons.favorite;
return Icons.favorite_outline;
case 'watch later':
return Icons.watch_later_outlined;
default:

View file

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import 'package:madari_client/features/external_player/service/external_player.dart';
import 'package:madari_client/features/settings/service/playback_setting_service.dart';
import 'package:madari_client/features/streamio_addons/extension/query_extension.dart';
import '../../../../streamio_addons/models/stremio_base_types.dart';
@ -267,8 +269,20 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
return InkWell(
onTap: stream.url != null
? () {
? () async {
if (stream.url != null) {
final settings =
await PlaybackSettingsService.instance.getSettings();
if (settings.externalPlayer) {
await ExternalPlayerService.openInExternalPlayer(
videoUrl: stream.url!,
playerPackage: settings.selectedExternalPlayer,
);
return;
}
String url =
'/player/${widget.meta.type}/${widget.meta.id}/${Uri.encodeQueryComponent(stream.url!)}?';
@ -399,7 +413,9 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
Widget build(BuildContext context) {
if (_isLoading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
body: Center(
child: CircularProgressIndicator(),
),
);
}

View file

@ -174,20 +174,57 @@ class StreamioHeroSection extends StatelessWidget {
const SizedBox(
height: 12,
),
AddToListButton(
label: const Row(
mainAxisSize: MainAxisSize.min,
OutlinedButton.icon(
onPressed: () {
openVideoStream(
context,
meta,
);
},
icon: const Icon(Icons.play_arrow),
label: const Text("Play"),
),
const SizedBox(
height: 12,
),
SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
Icon(Icons.playlist_add_outlined),
SizedBox(
AddToListButton(
meta: meta,
listName: "Favourites",
minimal: true,
),
const SizedBox(
width: 8,
),
Text("Add to list"),
AddToListButton(
meta: meta,
listName: "Watchlist",
minimal: true,
),
const SizedBox(
width: 8,
),
AddToListButton(
label: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.playlist_add_outlined),
SizedBox(
width: 8,
),
Text("Add to list"),
],
),
meta: meta,
icon: Icons.add,
),
],
),
meta: meta,
icon: Icons.add,
)
),
],
),
),

View file

@ -6,8 +6,6 @@ import 'package:madari_client/features/widgetter/plugins/stremio/containers/stre
import 'package:madari_client/features/widgetter/plugins/stremio/containers/streamio_trailer_section.dart';
import 'package:madari_client/features/widgetter/plugins/stremio/containers/streamio_video_list.dart';
import '../../../../library/container/add_to_list_button.dart';
final _logger = Logger('StreamioViewerContent');
class StreamioViewerContent extends StatefulWidget {
@ -59,29 +57,6 @@ class _StreamioViewerContentState extends State<StreamioViewerContent> {
prefix: widget.prefix,
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
AddToListButton(
meta: widget.meta,
listName: "Favourites",
),
const SizedBox(
width: 8,
),
AddToListButton(
meta: widget.meta,
listName: "Watchlist",
),
],
),
),
),
const SizedBox(height: 16),
if (widget.meta.description != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),