diff options
| -rw-r--r-- | player-android/android/app/src/main/AndroidManifest.xml | 31 | ||||
| -rw-r--r-- | player-android/lib/app_routes.dart | 14 | ||||
| -rw-r--r-- | player-android/lib/router.dart | 26 | ||||
| -rw-r--r-- | player-android/lib/screens/audio_player_screen.dart | 56 | ||||
| -rw-r--r-- | player-android/lib/screens/video_player_screen.dart | 56 | ||||
| -rw-r--r-- | player-android/pubspec.lock | 200 | ||||
| -rw-r--r-- | player-android/pubspec.yaml | 12 |
7 files changed, 395 insertions, 0 deletions
diff --git a/player-android/android/app/src/main/AndroidManifest.xml b/player-android/android/app/src/main/AndroidManifest.xml index 4ca1088..602051c 100644 --- a/player-android/android/app/src/main/AndroidManifest.xml +++ b/player-android/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,13 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Required for all network calls (API, streaming). --> <uses-permission android:name="android.permission.INTERNET" /> + <!-- Required by audio_service to keep audio playing when the app is in the + background. Without this the OS may kill the media playback process. --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <!-- Required to declare mediaPlayback foreground service type on API 34+. --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> + <!-- Prevents the CPU from sleeping while audio is actively playing. --> + <uses-permission android:name="android.permission.WAKE_LOCK" /> <application android:label="Player" @@ -25,5 +33,28 @@ <meta-data android:name="flutterEmbedding" android:value="2" /> + <!-- audio_service background playback service. + android:foregroundServiceType="mediaPlayback" is mandatory on + Android 10+ (API 29+) to grant media-playback foreground service + privileges. This overrides the entry auto-merged from + audio_service's own manifest to explicitly pin the foreground + service type. Class names match audio_service 0.18.x internals. --> + <service + android:name="com.ryanheise.audioservice.AudioService" + android:exported="true" + android:foregroundServiceType="mediaPlayback"> + <intent-filter> + <action android:name="android.media.browse.MediaBrowserService" /> + </intent-filter> + </service> + <!-- Receives hardware media button events (headset play/pause, etc.) + and forwards them to audio_service. --> + <receiver + android:name="com.ryanheise.audioservice.MediaButtonReceiver" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MEDIA_BUTTON" /> + </intent-filter> + </receiver> </application> </manifest> diff --git a/player-android/lib/app_routes.dart b/player-android/lib/app_routes.dart index 746ebb9..d64c325 100644 --- a/player-android/lib/app_routes.dart +++ b/player-android/lib/app_routes.dart @@ -17,9 +17,23 @@ abstract final class AppRoutes { /// The ':setId' segment is a numeric set identifier. static const mediaGrid = '/sets/:setId'; + /// Route for the video player screen. + /// The ':mediaId' segment identifies the media item to play. + static const videoPlayer = '/video/:mediaId'; + + /// Route for the audio player screen. + /// The ':mediaId' segment identifies the media item to play. + static const audioPlayer = '/audio/:mediaId'; + /// Returns the concrete path for a media-detail page given a numeric [id]. static String mediaDetailPath(int id) => '/media/$id'; /// Returns the concrete path for the media-grid page of a given [setId]. static String mediaGridPath(int setId) => '/sets/$setId'; + + /// Returns the concrete path for the video player of a given [mediaId]. + static String videoPlayerPath(String mediaId) => '/video/$mediaId'; + + /// Returns the concrete path for the audio player of a given [mediaId]. + static String audioPlayerPath(String mediaId) => '/audio/$mediaId'; } diff --git a/player-android/lib/router.dart b/player-android/lib/router.dart index ee101a5..bd45a60 100644 --- a/player-android/lib/router.dart +++ b/player-android/lib/router.dart @@ -6,6 +6,7 @@ import 'app_routes.dart'; import 'navigation_key.dart'; import 'providers/auth_state_provider.dart'; import 'providers/first_run_provider.dart'; +import 'screens/audio_player_screen.dart'; import 'screens/bootstrap_screen.dart'; import 'screens/home_screen.dart'; import 'screens/login_screen.dart'; @@ -13,6 +14,7 @@ import 'screens/media_detail_screen.dart'; import 'screens/media_grid_screen.dart'; import 'screens/settings_screen.dart'; import 'screens/share_screen.dart'; +import 'screens/video_player_screen.dart'; // Re-export AppRoutes so existing callers that import router.dart for routes // do not need to change their import path. @@ -129,6 +131,30 @@ final routerProvider = Provider<GoRouter>((ref) { path: AppRoutes.settings, builder: (context, state) => const SettingsScreen(), ), + GoRoute( + path: AppRoutes.videoPlayer, + builder: (context, state) { + // ':mediaId' is guaranteed present by the route pattern. + final mediaId = state.pathParameters['mediaId']!; + // The resolved stream URL is optionally forwarded as a route extra + // (String) by the calling screen (e.g. MediaDetailScreen). + final mediaUrl = + state.extra is String ? state.extra as String : null; + return VideoPlayerScreen(mediaId: mediaId, mediaUrl: mediaUrl); + }, + ), + GoRoute( + path: AppRoutes.audioPlayer, + builder: (context, state) { + // ':mediaId' is guaranteed present by the route pattern. + final mediaId = state.pathParameters['mediaId']!; + // The resolved stream URL is optionally forwarded as a route extra + // (String) by the calling screen (e.g. MediaDetailScreen). + final mediaUrl = + state.extra is String ? state.extra as String : null; + return AudioPlayerScreen(mediaId: mediaId, mediaUrl: mediaUrl); + }, + ), ], ); }); diff --git a/player-android/lib/screens/audio_player_screen.dart b/player-android/lib/screens/audio_player_screen.dart new file mode 100644 index 0000000..cbf8f96 --- /dev/null +++ b/player-android/lib/screens/audio_player_screen.dart @@ -0,0 +1,56 @@ +// ignore_for_file: unused_import +// The audio_service and just_audio imports are intentionally present even in +// this placeholder so that package resolution is verified at analysis time and +// the import graph is established before feature implementation begins. +import 'package:audio_service/audio_service.dart'; +import 'package:flutter/material.dart'; +import 'package:just_audio/just_audio.dart'; + +/// Placeholder audio player screen — full implementation is deferred. +/// +/// Accepts [mediaId] (the route path parameter) and [mediaUrl] (the resolved +/// stream URL, passed as route extra) so the router wiring is established and +/// the package imports are verified before feature work begins. +/// +/// TODO(audio-player): Initialise a custom [AudioHandler] that extends +/// [BaseAudioHandler]. Register it via [AudioService.init] in `main.dart` +/// and inject it through Riverpod. Inside the handler call +/// [AudioPlayer.setUrl] with [mediaUrl] to start buffering. +/// See: https://pub.dev/packages/audio_service +/// https://pub.dev/packages/just_audio +class AudioPlayerScreen extends StatelessWidget { + const AudioPlayerScreen({ + super.key, + required this.mediaId, + this.mediaUrl, + }); + + /// The media item identifier extracted from the '/audio/:mediaId' route path. + final String mediaId; + + /// The resolved stream URL, optionally provided as a route extra. + /// Will be required once real playback is wired up. + final String? mediaUrl; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Audio – $mediaId')), + body: const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.headphones_outlined, size: 64), + SizedBox(height: 16), + Text('Audio player TODO', style: TextStyle(fontSize: 18)), + SizedBox(height: 8), + Text( + 'Will use just_audio + audio_service for background playback.', + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } +} diff --git a/player-android/lib/screens/video_player_screen.dart b/player-android/lib/screens/video_player_screen.dart new file mode 100644 index 0000000..280a4b9 --- /dev/null +++ b/player-android/lib/screens/video_player_screen.dart @@ -0,0 +1,56 @@ +// ignore_for_file: unused_import +// The chewie and video_player imports are intentionally present even in this +// placeholder so that package resolution is verified at analysis time and the +// import graph is established before feature implementation begins. +import 'package:chewie/chewie.dart'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +/// Placeholder video player screen — full implementation is deferred. +/// +/// Accepts [mediaId] (the route path parameter) and [mediaUrl] (the resolved +/// stream URL, passed as route extra) so the router wiring is established and +/// the package imports are verified before feature work begins. +/// +/// TODO(video-player): Convert to [StatefulWidget]. In [State.initState] +/// create [VideoPlayerController.networkUrl] from [mediaUrl], then wrap it +/// in a [ChewieController] with `aspectRatio`, `autoPlay`, etc. Dispose +/// both controllers in [State.dispose]. +/// See: https://pub.dev/packages/chewie +/// https://pub.dev/packages/video_player +class VideoPlayerScreen extends StatelessWidget { + const VideoPlayerScreen({ + super.key, + required this.mediaId, + this.mediaUrl, + }); + + /// The media item identifier extracted from the '/video/:mediaId' route path. + final String mediaId; + + /// The resolved HLS/direct stream URL, optionally provided as a route extra. + /// Will be required once real playback is wired up. + final String? mediaUrl; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Video – $mediaId')), + body: const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.videocam_outlined, size: 64), + SizedBox(height: 16), + Text('Video player TODO', style: TextStyle(fontSize: 18)), + SizedBox(height: 8), + Text( + 'Will use video_player + chewie for playback controls.', + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } +} diff --git a/player-android/pubspec.lock b/player-android/pubspec.lock index 91d9b07..47f6b23 100644 --- a/player-android/pubspec.lock +++ b/player-android/pubspec.lock @@ -17,6 +17,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.1" + audio_service: + dependency: "direct main" + description: + name: audio_service + sha256: cb122c7c2639d2a992421ef96b67948ad88c5221da3365ccef1031393a76e044 + url: "https://pub.dev" + source: hosted + version: "0.18.18" + audio_service_platform_interface: + dependency: transitive + description: + name: audio_service_platform_interface + sha256: "6283782851f6c8b501b60904a32fc7199dc631172da0629d7301e66f672ab777" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + audio_service_web: + dependency: transitive + description: + name: audio_service_web + sha256: b8ea9243201ee53383157fbccf13d5d2a866b5dda922ec19d866d1d5d70424df + url: "https://pub.dev" + source: hosted + version: "0.1.4" + audio_session: + dependency: transitive + description: + name: audio_session + sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" + url: "https://pub.dev" + source: hosted + version: "0.1.25" boolean_selector: dependency: transitive description: @@ -57,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + chewie: + dependency: "direct main" + description: + name: chewie + sha256: "53dadd2c5b6748742d7744072b38a417ad22691ca55715850300ee793dc7cb27" + url: "https://pub.dev" + source: hosted + version: "1.13.1" clock: dependency: transitive description: @@ -89,6 +129,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" dio: dependency: "direct main" description: @@ -248,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" http: dependency: transitive description: @@ -296,6 +368,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + just_audio: + dependency: "direct main" + description: + name: just_audio + sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e + url: "https://pub.dev" + source: hosted + version: "0.9.46" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" + url: "https://pub.dev" + source: hosted + version: "0.4.16" leak_tracker: dependency: transitive description: @@ -384,6 +480,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.17.6" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" objective_c: dependency: transitive description: @@ -408,6 +512,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" + url: "https://pub.dev" + source: hosted + version: "9.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: @@ -464,6 +584,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -480,6 +608,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" pub_semver: dependency: transitive description: @@ -701,6 +837,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0" + url: "https://pub.dev" + source: hosted + version: "2.9.5" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: "9338f3ec22774f88146b22f13273a446719b1da010fd200c4d1d97802156ac58" + url: "https://pub.dev" + source: hosted + version: "2.9.7" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "16eaed5268c571c31840dc58ef8da5f0cd4db2a98490c3b8f1cf70122546c6e0" + url: "https://pub.dev" + source: hosted + version: "6.7.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://pub.dev" + source: hosted + version: "2.4.0" vm_service: dependency: transitive description: @@ -709,6 +885,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.2.0" + wakelock_plus: + dependency: transitive + description: + name: wakelock_plus + sha256: ddf3db70eaa10c37558ff817519b85d527dbd21034fd5d8e1c2e85f31588f1c1 + url: "https://pub.dev" + source: hosted + version: "1.5.2" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b + url: "https://pub.dev" + source: hosted + version: "1.5.1" web: dependency: transitive description: @@ -733,6 +925,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" yaml: dependency: transitive description: diff --git a/player-android/pubspec.yaml b/player-android/pubspec.yaml index d66852c..2fcc290 100644 --- a/player-android/pubspec.yaml +++ b/player-android/pubspec.yaml @@ -23,6 +23,18 @@ dependencies: # cached_network_image: downloads and caches remote images (e.g. set covers) # so repeated visits do not re-fetch from the server. cached_network_image: ^3.4.1 + # video_player: low-level Flutter plugin for video playback via platform codecs. + video_player: ^2.9.2 + # chewie: opinionated UI controls (play/pause, seek bar, fullscreen) built on + # top of video_player, so the app gets a polished player without hand-rolling + # all the controls. + chewie: ^1.8.5 + # just_audio: cross-platform audio playback with gapless playlists, stream + # support, and fine-grained playback controls. + just_audio: ^0.9.42 + # audio_service: runs audio in the background as a foreground service, + # integrates with Android media notifications and lock-screen controls. + audio_service: ^0.18.15 dev_dependencies: flutter_test: |
