diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 9e76d61..5db1186 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -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 {
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
index 67fffa0..6be01e2 100644
--- a/android/app/src/debug/AndroidManifest.xml
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -3,6 +3,8 @@
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
-
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 587f5a0..cddc286 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,52 +1,73 @@
-
+
+
+
+
+
+
+
+ android:icon="@mipmap/ic_launcher"
+ android:label="zap.stream">
-
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme" />
+
-
-
+
+
+
-
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 3ed80d2..6c247bf 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -72,6 +72,7 @@
fetch
remote-notification
+ audio
UILaunchStoryboardName
LaunchScreen
diff --git a/lib/app.dart b/lib/app.dart
index 8bd6d94..6ae5bdd 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -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(
diff --git a/lib/main.dart b/lib/main.dart
index 76ccaca..23a029f 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -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 main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -20,5 +24,14 @@ Future 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();
}
diff --git a/lib/pages/stream.dart b/lib/pages/stream.dart
index 8bc6fab..ebd9c4b 100644
--- a/lib/pages/stream.dart
+++ b/lib/pages/stream.dart
@@ -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 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)
diff --git a/lib/player.dart b/lib/player.dart
new file mode 100644
index 0000000..3e60a59
--- /dev/null
+++ b/lib/player.dart
@@ -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 play() async {
+ await _chewieController?.play();
+ }
+
+ @override
+ Future pause() async {
+ await _chewieController?.pause();
+ }
+
+ @override
+ Future 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,
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/video_player_main.dart b/lib/widgets/video_player_main.dart
new file mode 100644
index 0000000..e3e6e3f
--- /dev/null
+++ b/lib/widgets/video_player_main.dart
@@ -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 createState() => _MainVideoPlayerWidget();
+}
+
+class _MainVideoPlayerWidget extends State {
+ @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!);
+ }
+}
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 172e45c..a785188 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -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"))
diff --git a/pubspec.lock b/pubspec.lock
index 7a734e2..9a1dcd4 100644
--- a/pubspec.lock
+++ b/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:
diff --git a/pubspec.yaml b/pubspec.yaml
index b895e26..8d25e2e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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: