mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-13 19:22:48 +00:00
feat: background playback (wip)
https://github.com/ryanheise/audio_service/issues/1124
This commit is contained in:
@ -1,5 +1,4 @@
|
||||
import com.android.build.api.dsl.ApkSigningConfig
|
||||
import com.android.build.api.dsl.SigningConfig
|
||||
import org.jetbrains.kotlin.gradle.targets.js.toHex
|
||||
import java.io.FileInputStream
|
||||
import java.util.Base64
|
||||
@ -8,11 +7,9 @@ import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// START: FlutterFire Configuration
|
||||
id("com.google.gms.google-services")
|
||||
// END: FlutterFire Configuration
|
||||
id("kotlin-android")
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
id("com.google.gms.google-services")
|
||||
}
|
||||
|
||||
fun getKeystoreFile(base64String: String?, hash: String, fileName: String): File {
|
||||
|
@ -3,6 +3,8 @@
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
</manifest>
|
||||
|
@ -1,52 +1,73 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
|
||||
<application
|
||||
android:label="zap.stream"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="zap.stream">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" android:host="zap.stream" />
|
||||
|
||||
<data android:host="zap.stream" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="zswc" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<service
|
||||
android:name="com.ryanheise.audioservice.AudioService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:permission="android.permission.FOREGROUND_SERVICE"
|
||||
tools:ignore="Instantiatable">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<receiver
|
||||
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||
android:exported="true"
|
||||
tools:ignore="Instantiatable">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
@ -55,10 +76,11 @@
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<meta-data
|
||||
android:name="firebase_messaging_auto_init_enabled"
|
||||
android:value="false" />
|
||||
|
@ -72,6 +72,7 @@
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
|
@ -25,7 +25,11 @@ void runZapStream() {
|
||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||
theme: ThemeData.localize(
|
||||
ThemeData(colorScheme: ColorScheme.dark(), highlightColor: PRIMARY_1),
|
||||
ThemeData(
|
||||
colorScheme: ColorScheme.dark(),
|
||||
highlightColor: PRIMARY_1,
|
||||
useMaterial3: true,
|
||||
),
|
||||
TextTheme(),
|
||||
),
|
||||
routerConfig: GoRouter(
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
@ -7,6 +8,9 @@ import 'package:zap_stream_flutter/app.dart';
|
||||
import 'package:zap_stream_flutter/const.dart';
|
||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||
import 'package:zap_stream_flutter/notifications.dart';
|
||||
import 'package:zap_stream_flutter/player.dart';
|
||||
|
||||
late final MainPlayer mainPlayer;
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@ -20,5 +24,14 @@ Future<void> main() async {
|
||||
developer.log("Failed to setup notifications: $e");
|
||||
});
|
||||
|
||||
mainPlayer = await AudioService.init(
|
||||
builder: () => MainPlayer(),
|
||||
config: AudioServiceConfig(
|
||||
androidNotificationChannelId: "io.nostrlabs.zap_stream_flutter.player",
|
||||
androidNotificationChannelName: "player",
|
||||
androidNotificationOngoing: true
|
||||
),
|
||||
);
|
||||
|
||||
runZapStream();
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ import 'package:zap_stream_flutter/widgets/notifications_button.dart';
|
||||
import 'package:zap_stream_flutter/widgets/pill.dart';
|
||||
import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||
import 'package:zap_stream_flutter/widgets/stream_info.dart';
|
||||
import 'package:zap_stream_flutter/widgets/video_player.dart';
|
||||
import 'package:zap_stream_flutter/widgets/video_player_main.dart';
|
||||
import 'package:zap_stream_flutter/widgets/zap.dart';
|
||||
|
||||
class StreamPage extends StatefulWidget {
|
||||
@ -141,11 +141,13 @@ class _StreamPage extends State<StreamPage> with RouteAware {
|
||||
aspectRatio: 16 / 9,
|
||||
child:
|
||||
(stream.info.stream != null && !_offScreen)
|
||||
? VideoPlayerWidget(
|
||||
? MainVideoPlayerWidget(
|
||||
url: stream.info.stream!,
|
||||
placeholder: stream.info.image,
|
||||
aspectRatio: 16 / 9,
|
||||
isLive: true,
|
||||
title: stream.info.title,
|
||||
|
||||
)
|
||||
: (stream.info.image?.isNotEmpty ?? false)
|
||||
? ProxyImg(url: stream.info.image)
|
||||
|
100
lib/player.dart
Normal file
100
lib/player.dart
Normal file
@ -0,0 +1,100 @@
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:zap_stream_flutter/const.dart';
|
||||
import 'package:zap_stream_flutter/imgproxy.dart';
|
||||
|
||||
class MainPlayer extends BaseAudioHandler {
|
||||
VideoPlayerController? _controller;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
ChewieController? get chewie {
|
||||
return _chewieController;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() async {
|
||||
await _chewieController?.play();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
await _chewieController?.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
await _chewieController?.pause();
|
||||
}
|
||||
|
||||
void loadUrl(
|
||||
String url, {
|
||||
String? title,
|
||||
bool? autoPlay,
|
||||
double? aspectRatio,
|
||||
bool? isLive,
|
||||
String? placeholder,
|
||||
String? artist,
|
||||
}) {
|
||||
if (_chewieController != null) {
|
||||
_chewieController!.dispose();
|
||||
_controller!.dispose();
|
||||
}
|
||||
|
||||
_controller = VideoPlayerController.networkUrl(
|
||||
Uri.parse(url),
|
||||
httpHeaders: Map.from({"user-agent": userAgent}),
|
||||
);
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _controller!,
|
||||
autoPlay: autoPlay ?? true,
|
||||
aspectRatio: aspectRatio,
|
||||
isLive: isLive ?? false,
|
||||
autoInitialize: true,
|
||||
allowedScreenSleep: false,
|
||||
placeholder:
|
||||
(placeholder?.isNotEmpty ?? false)
|
||||
? ProxyImg(url: placeholder!)
|
||||
: null,
|
||||
);
|
||||
|
||||
// insert media item
|
||||
mediaItem.add(
|
||||
MediaItem(
|
||||
id: url.hashCode.toString(),
|
||||
title: title ?? url,
|
||||
artist: artist,
|
||||
isLive: _chewieController!.isLive,
|
||||
artUri:
|
||||
(placeholder?.isNotEmpty ?? false) ? Uri.parse(placeholder!) : null,
|
||||
),
|
||||
);
|
||||
_chewieController!.videoPlayerController.addListener(updatePlayerState);
|
||||
}
|
||||
|
||||
void updatePlayerState() {
|
||||
final isPlaying =
|
||||
_chewieController?.videoPlayerController.value.isPlaying ?? false;
|
||||
|
||||
playbackState.add(
|
||||
playbackState.value.copyWith(
|
||||
controls: [
|
||||
if (playbackState.value.playing)
|
||||
MediaControl.pause
|
||||
else
|
||||
MediaControl.play,
|
||||
MediaControl.stop,
|
||||
],
|
||||
playing: isPlaying,
|
||||
processingState: switch (_chewieController
|
||||
?.videoPlayerController
|
||||
.value
|
||||
.isInitialized) {
|
||||
true => AudioProcessingState.ready,
|
||||
false => AudioProcessingState.idle,
|
||||
_ => AudioProcessingState.completed,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
53
lib/widgets/video_player_main.dart
Normal file
53
lib/widgets/video_player_main.dart
Normal file
@ -0,0 +1,53 @@
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:zap_stream_flutter/main.dart';
|
||||
|
||||
class MainVideoPlayerWidget extends StatefulWidget {
|
||||
final String url;
|
||||
final String? title;
|
||||
final String? placeholder;
|
||||
final double? aspectRatio;
|
||||
final bool? autoPlay;
|
||||
final bool? isLive;
|
||||
|
||||
const MainVideoPlayerWidget({
|
||||
super.key,
|
||||
required this.url,
|
||||
this.title,
|
||||
this.placeholder,
|
||||
this.aspectRatio,
|
||||
this.autoPlay,
|
||||
this.isLive,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _MainVideoPlayerWidget();
|
||||
}
|
||||
|
||||
class _MainVideoPlayerWidget extends State<MainVideoPlayerWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
mainPlayer.loadUrl(
|
||||
widget.url,
|
||||
title: widget.title,
|
||||
placeholder: widget.placeholder,
|
||||
aspectRatio: widget.aspectRatio,
|
||||
autoPlay: widget.autoPlay,
|
||||
isLive: widget.isLive,
|
||||
artist: "zap.stream"
|
||||
);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
mainPlayer.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Chewie(controller: mainPlayer.chewie!);
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import audio_service
|
||||
import audio_session
|
||||
import emoji_picker_flutter
|
||||
import file_selector_macos
|
||||
import firebase_core
|
||||
@ -23,6 +25,8 @@ import video_player_avfoundation
|
||||
import wakelock_plus
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
|
||||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||
EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
|
32
pubspec.lock
32
pubspec.lock
@ -49,6 +49,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
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: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
bech32:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -23,10 +23,10 @@ dependencies:
|
||||
crypto: ^3.0.6
|
||||
convert: ^3.1.2
|
||||
collection: ^1.19.1
|
||||
video_player: ^2.9.5
|
||||
clipboard: ^0.1.3
|
||||
qr_flutter: ^4.1.0
|
||||
url_launcher: ^6.3.1
|
||||
video_player: ^2.9.5
|
||||
chewie: ^1.11.3
|
||||
image_picker: ^1.1.2
|
||||
emoji_picker_flutter: ^4.3.0
|
||||
@ -45,6 +45,7 @@ dependencies:
|
||||
flutter_local_notifications: ^19.2.1
|
||||
flutter_dotenv: ^5.2.1
|
||||
protocol_handler: ^0.2.0
|
||||
audio_service: ^0.18.18
|
||||
|
||||
dependency_overrides:
|
||||
ndk:
|
||||
|
Reference in New Issue
Block a user