summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-21 08:25:16 +0300
committerPaul Buetow <paul@buetow.org>2026-05-21 08:25:16 +0300
commit09f0ee28b6a7b7fc588a8a4e387d94ffb6848af3 (patch)
tree4850136006f64f1938dc2a9fae2bde7a28c4f8b7
parent63fc4369917ab548c5cdc4dddccf0ad4ded11a2f (diff)
Add video/audio player packages, routes, and placeholder screens (task xa)
- pubspec.yaml: add video_player ^2.9.2, chewie ^1.8.5, just_audio ^0.9.42, audio_service ^0.18.15 with descriptive comments explaining each package's role. - AndroidManifest.xml: add FOREGROUND_SERVICE, FOREGROUND_SERVICE_MEDIA_PLAYBACK, and WAKE_LOCK permissions; declare AudioService with foregroundServiceType= mediaPlayback and MediaButtonReceiver for hardware media button support. INTERNET was already present — not duplicated. - app_routes.dart: add videoPlayer (/video/:mediaId) and audioPlayer (/audio/:mediaId) route constants plus videoPlayerPath/audioPlayerPath helpers. - router.dart: wire GoRoutes for the two new paths, forwarding mediaId from path params and optional mediaUrl from route extra. - screens/video_player_screen.dart: placeholder VideoPlayerScreen accepting mediaId + mediaUrl, importing chewie and video_player for resolution check. - screens/audio_player_screen.dart: placeholder AudioPlayerScreen accepting mediaId + mediaUrl, importing just_audio and audio_service for resolution check. flutter pub get and flutter analyze both pass clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--player-android/android/app/src/main/AndroidManifest.xml31
-rw-r--r--player-android/lib/app_routes.dart14
-rw-r--r--player-android/lib/router.dart26
-rw-r--r--player-android/lib/screens/audio_player_screen.dart56
-rw-r--r--player-android/lib/screens/video_player_screen.dart56
-rw-r--r--player-android/pubspec.lock200
-rw-r--r--player-android/pubspec.yaml12
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: