✨ Playback server
This commit is contained in:
7
lib/services/artist.dart
Normal file
7
lib/services/artist.dart
Normal file
@ -0,0 +1,7 @@
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
extension ArtistExtension on List<ArtistSimple> {
|
||||
String asString() {
|
||||
return map((e) => e.name?.replaceAll(",", " ")).join(", ");
|
||||
}
|
||||
}
|
108
lib/services/audio_player/state.dart
Normal file
108
lib/services/audio_player/state.dart
Normal file
@ -0,0 +1,108 @@
|
||||
import 'package:media_kit/media_kit.dart' hide Track;
|
||||
import 'package:spotify/spotify.dart' hide Playlist;
|
||||
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
||||
|
||||
class AudioPlayerState {
|
||||
final bool playing;
|
||||
final PlaylistMode loopMode;
|
||||
final bool shuffled;
|
||||
final Playlist playlist;
|
||||
|
||||
final List<Track> tracks;
|
||||
final List<String> collections;
|
||||
|
||||
AudioPlayerState({
|
||||
required this.playing,
|
||||
required this.loopMode,
|
||||
required this.shuffled,
|
||||
required this.playlist,
|
||||
required this.collections,
|
||||
List<Track>? tracks,
|
||||
}) : tracks = tracks ??
|
||||
playlist.medias
|
||||
.map((media) => RhythmMedia.fromMedia(media).track)
|
||||
.toList();
|
||||
|
||||
factory AudioPlayerState.fromJson(Map<String, dynamic> json) {
|
||||
return AudioPlayerState(
|
||||
playing: json['playing'],
|
||||
loopMode: PlaylistMode.values.firstWhere(
|
||||
(e) => e.name == json['loopMode'],
|
||||
orElse: () => audioPlayer.loopMode,
|
||||
),
|
||||
shuffled: json['shuffled'],
|
||||
playlist: Playlist(
|
||||
json['playlist']['medias']
|
||||
.map(
|
||||
(media) => RhythmMedia.fromMedia(Media(
|
||||
media['uri'],
|
||||
extras: media['extras'],
|
||||
httpHeaders: media['httpHeaders'],
|
||||
)),
|
||||
)
|
||||
.cast<Media>()
|
||||
.toList(),
|
||||
index: json['playlist']['index'],
|
||||
),
|
||||
collections: List<String>.from(json['collections']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'playing': playing,
|
||||
'loopMode': loopMode.name,
|
||||
'shuffled': shuffled,
|
||||
'playlist': {
|
||||
'medias': playlist.medias
|
||||
.map((media) => {
|
||||
'uri': media.uri,
|
||||
'extras': media.extras,
|
||||
'httpHeaders': media.httpHeaders,
|
||||
})
|
||||
.toList(),
|
||||
'index': playlist.index,
|
||||
},
|
||||
'collections': collections,
|
||||
};
|
||||
}
|
||||
|
||||
AudioPlayerState copyWith({
|
||||
bool? playing,
|
||||
PlaylistMode? loopMode,
|
||||
bool? shuffled,
|
||||
Playlist? playlist,
|
||||
List<String>? collections,
|
||||
}) {
|
||||
return AudioPlayerState(
|
||||
playing: playing ?? this.playing,
|
||||
loopMode: loopMode ?? this.loopMode,
|
||||
shuffled: shuffled ?? this.shuffled,
|
||||
playlist: playlist ?? this.playlist,
|
||||
collections: collections ?? this.collections,
|
||||
tracks: playlist == null ? tracks : null,
|
||||
);
|
||||
}
|
||||
|
||||
Track? get activeTrack {
|
||||
if (playlist.index == -1) return null;
|
||||
return tracks.elementAtOrNull(playlist.index);
|
||||
}
|
||||
|
||||
Media? get activeMedia {
|
||||
if (playlist.index == -1 || playlist.medias.isEmpty) return null;
|
||||
return playlist.medias.elementAt(playlist.index);
|
||||
}
|
||||
|
||||
bool containsTrack(Track track) {
|
||||
return tracks.any((t) => t.id == track.id);
|
||||
}
|
||||
|
||||
bool containsTracks(List<Track> tracks) {
|
||||
return tracks.every(containsTrack);
|
||||
}
|
||||
|
||||
bool containsCollection(String collectionId) {
|
||||
return collections.contains(collectionId);
|
||||
}
|
||||
}
|
84
lib/services/audio_services/audio_services.dart
Executable file
84
lib/services/audio_services/audio_services.dart
Executable file
@ -0,0 +1,84 @@
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rhythm_box/platform.dart';
|
||||
import 'package:rhythm_box/services/audio_services/image.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:rhythm_box/services/audio_services/mobile_audio_service.dart';
|
||||
import 'package:rhythm_box/services/audio_services/windows_audio_service.dart';
|
||||
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
|
||||
import 'package:rhythm_box/services/artist.dart';
|
||||
|
||||
class AudioServices with WidgetsBindingObserver {
|
||||
final MobileAudioService? mobile;
|
||||
final WindowsAudioService? smtc;
|
||||
|
||||
AudioServices(this.mobile, this.smtc) {
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
static Future<AudioServices> create() async {
|
||||
final mobile =
|
||||
PlatformInfo.isMobile || PlatformInfo.isMacOS || PlatformInfo.isLinux
|
||||
? await AudioService.init(
|
||||
builder: () => MobileAudioService(),
|
||||
config: AudioServiceConfig(
|
||||
androidNotificationChannelId: PlatformInfo.isLinux
|
||||
? 'RhythmBox'
|
||||
: 'dev.solsynth.rhythmBox',
|
||||
androidNotificationChannelName: 'RhythmBox',
|
||||
androidNotificationOngoing: false,
|
||||
androidNotificationIcon: "drawable/ic_launcher_monochrome",
|
||||
androidStopForegroundOnPause: false,
|
||||
androidNotificationChannelDescription: "RhythmBox Music",
|
||||
),
|
||||
)
|
||||
: null;
|
||||
final smtc = PlatformInfo.isWindows ? WindowsAudioService() : null;
|
||||
|
||||
return AudioServices(mobile, smtc);
|
||||
}
|
||||
|
||||
Future<void> addTrack(Track track) async {
|
||||
await smtc?.addTrack(track);
|
||||
mobile?.addItem(MediaItem(
|
||||
id: track.id!,
|
||||
album: track.album?.name ?? "",
|
||||
title: track.name!,
|
||||
artist: (track.artists)?.asString() ?? "",
|
||||
duration: track is SourcedTrack
|
||||
? track.sourceInfo.duration
|
||||
: Duration(milliseconds: track.durationMs ?? 0),
|
||||
artUri: Uri.parse(
|
||||
(track.album?.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
),
|
||||
playable: true,
|
||||
));
|
||||
}
|
||||
|
||||
void activateSession() {
|
||||
mobile?.session?.setActive(true);
|
||||
}
|
||||
|
||||
void deactivateSession() {
|
||||
mobile?.session?.setActive(false);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
switch (state) {
|
||||
case AppLifecycleState.detached:
|
||||
deactivateSession();
|
||||
mobile?.stop();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
smtc?.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
}
|
||||
}
|
34
lib/services/audio_services/image.dart
Normal file
34
lib/services/audio_services/image.dart
Normal file
@ -0,0 +1,34 @@
|
||||
import 'package:rhythm_box/services/primitive.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:rhythm_box/collections/assets.gen.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
enum ImagePlaceholder {
|
||||
albumArt,
|
||||
artist,
|
||||
collection,
|
||||
online,
|
||||
}
|
||||
|
||||
extension SpotifyImageExtensions on List<Image>? {
|
||||
String asUrlString({
|
||||
int index = 1,
|
||||
required ImagePlaceholder placeholder,
|
||||
}) {
|
||||
final String placeholderUrl = {
|
||||
ImagePlaceholder.albumArt: Assets.albumPlaceholder.path,
|
||||
ImagePlaceholder.artist: Assets.userPlaceholder.path,
|
||||
ImagePlaceholder.collection: Assets.placeholder.path,
|
||||
ImagePlaceholder.online:
|
||||
"https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png",
|
||||
}[placeholder]!;
|
||||
|
||||
final sortedImage = this?.sorted((a, b) => a.width!.compareTo(b.width!));
|
||||
|
||||
return sortedImage != null && sortedImage.isNotEmpty
|
||||
? sortedImage[
|
||||
index > sortedImage.length - 1 ? sortedImage.length - 1 : index]
|
||||
.url!
|
||||
: placeholderUrl;
|
||||
}
|
||||
}
|
153
lib/services/audio_services/mobile_audio_service.dart
Executable file
153
lib/services/audio_services/mobile_audio_service.dart
Executable file
@ -0,0 +1,153 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:rhythm_box/providers/audio_player.dart';
|
||||
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
||||
import 'package:media_kit/media_kit.dart' hide Track;
|
||||
import 'package:rhythm_box/services/audio_player/state.dart';
|
||||
|
||||
class MobileAudioService extends BaseAudioHandler {
|
||||
AudioSession? session;
|
||||
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
AudioPlayerState get playlist => Get.find<AudioPlayerProvider>().state.value;
|
||||
|
||||
MobileAudioService() {
|
||||
AudioSession.instance.then((s) {
|
||||
session = s;
|
||||
session?.configure(const AudioSessionConfiguration.music());
|
||||
|
||||
bool wasPausedByBeginEvent = false;
|
||||
|
||||
s.interruptionEventStream.listen((event) async {
|
||||
if (event.begin) {
|
||||
switch (event.type) {
|
||||
case AudioInterruptionType.duck:
|
||||
await audioPlayer.setVolume(0.5);
|
||||
break;
|
||||
case AudioInterruptionType.pause:
|
||||
case AudioInterruptionType.unknown:
|
||||
{
|
||||
wasPausedByBeginEvent = audioPlayer.isPlaying;
|
||||
await audioPlayer.pause();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (event.type) {
|
||||
case AudioInterruptionType.duck:
|
||||
await audioPlayer.setVolume(1.0);
|
||||
break;
|
||||
case AudioInterruptionType.pause when wasPausedByBeginEvent:
|
||||
case AudioInterruptionType.unknown when wasPausedByBeginEvent:
|
||||
await audioPlayer.resume();
|
||||
wasPausedByBeginEvent = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
s.becomingNoisyEventStream.listen((_) {
|
||||
audioPlayer.pause();
|
||||
});
|
||||
});
|
||||
audioPlayer.playerStateStream.listen((state) async {
|
||||
playbackState.add(await _transformEvent());
|
||||
});
|
||||
|
||||
audioPlayer.positionStream.listen((pos) async {
|
||||
playbackState.add(await _transformEvent());
|
||||
});
|
||||
audioPlayer.bufferedPositionStream.listen((pos) async {
|
||||
playbackState.add(await _transformEvent());
|
||||
});
|
||||
}
|
||||
|
||||
void addItem(MediaItem item) {
|
||||
session?.setActive(true);
|
||||
mediaItem.add(item);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() => audioPlayer.resume();
|
||||
|
||||
@override
|
||||
Future<void> pause() => audioPlayer.pause();
|
||||
|
||||
@override
|
||||
Future<void> seek(Duration position) => audioPlayer.seek(position);
|
||||
|
||||
@override
|
||||
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
|
||||
await super.setShuffleMode(shuffleMode);
|
||||
|
||||
audioPlayer.setShuffle(shuffleMode == AudioServiceShuffleMode.all);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
||||
super.setRepeatMode(repeatMode);
|
||||
audioPlayer.setLoopMode(switch (repeatMode) {
|
||||
AudioServiceRepeatMode.all ||
|
||||
AudioServiceRepeatMode.group =>
|
||||
PlaylistMode.loop,
|
||||
AudioServiceRepeatMode.one => PlaylistMode.single,
|
||||
_ => PlaylistMode.none,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
await Get.find<AudioPlayerProvider>().stop();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToNext() async {
|
||||
await audioPlayer.skipToNext();
|
||||
await super.skipToNext();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToPrevious() async {
|
||||
await audioPlayer.skipToPrevious();
|
||||
await super.skipToPrevious();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onTaskRemoved() async {
|
||||
await Get.find<AudioPlayerProvider>().stop();
|
||||
return super.onTaskRemoved();
|
||||
}
|
||||
|
||||
Future<PlaybackState> _transformEvent() async {
|
||||
return PlaybackState(
|
||||
controls: [
|
||||
MediaControl.skipToPrevious,
|
||||
audioPlayer.isPlaying ? MediaControl.pause : MediaControl.play,
|
||||
MediaControl.skipToNext,
|
||||
MediaControl.stop,
|
||||
],
|
||||
systemActions: {
|
||||
MediaAction.seek,
|
||||
},
|
||||
androidCompactActionIndices: const [0, 1, 2],
|
||||
playing: audioPlayer.isPlaying,
|
||||
updatePosition: audioPlayer.position,
|
||||
bufferedPosition: audioPlayer.bufferedPosition,
|
||||
shuffleMode: audioPlayer.isShuffled == true
|
||||
? AudioServiceShuffleMode.all
|
||||
: AudioServiceShuffleMode.none,
|
||||
repeatMode: switch (audioPlayer.loopMode) {
|
||||
PlaylistMode.loop => AudioServiceRepeatMode.all,
|
||||
PlaylistMode.single => AudioServiceRepeatMode.one,
|
||||
_ => AudioServiceRepeatMode.none,
|
||||
},
|
||||
processingState: audioPlayer.isBuffering
|
||||
? AudioProcessingState.loading
|
||||
: AudioProcessingState.ready,
|
||||
);
|
||||
}
|
||||
}
|
101
lib/services/audio_services/windows_audio_service.dart
Executable file
101
lib/services/audio_services/windows_audio_service.dart
Executable file
@ -0,0 +1,101 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:rhythm_box/providers/audio_player.dart';
|
||||
import 'package:rhythm_box/services/audio_services/image.dart';
|
||||
import 'package:smtc_windows/smtc_windows.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
||||
import 'package:rhythm_box/services/audio_player/playback_state.dart';
|
||||
import 'package:rhythm_box/services/artist.dart';
|
||||
|
||||
class WindowsAudioService {
|
||||
final SMTCWindows smtc;
|
||||
|
||||
final subscriptions = <StreamSubscription>[];
|
||||
|
||||
WindowsAudioService() : smtc = SMTCWindows(enabled: false) {
|
||||
smtc.setPlaybackStatus(PlaybackStatus.Stopped);
|
||||
final buttonStream = smtc.buttonPressStream.listen((event) {
|
||||
switch (event) {
|
||||
case PressedButton.play:
|
||||
audioPlayer.resume();
|
||||
break;
|
||||
case PressedButton.pause:
|
||||
audioPlayer.pause();
|
||||
break;
|
||||
case PressedButton.next:
|
||||
audioPlayer.skipToNext();
|
||||
break;
|
||||
case PressedButton.previous:
|
||||
audioPlayer.skipToPrevious();
|
||||
break;
|
||||
case PressedButton.stop:
|
||||
Get.find<AudioPlayerProvider>().stop();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
final playerStateStream =
|
||||
audioPlayer.playerStateStream.listen((state) async {
|
||||
switch (state) {
|
||||
case AudioPlaybackState.playing:
|
||||
await smtc.setPlaybackStatus(PlaybackStatus.Playing);
|
||||
break;
|
||||
case AudioPlaybackState.paused:
|
||||
await smtc.setPlaybackStatus(PlaybackStatus.Paused);
|
||||
break;
|
||||
case AudioPlaybackState.stopped:
|
||||
await smtc.setPlaybackStatus(PlaybackStatus.Stopped);
|
||||
break;
|
||||
case AudioPlaybackState.completed:
|
||||
await smtc.setPlaybackStatus(PlaybackStatus.Changing);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
final positionStream = audioPlayer.positionStream.listen((pos) async {
|
||||
await smtc.setPosition(pos);
|
||||
});
|
||||
|
||||
final durationStream = audioPlayer.durationStream.listen((duration) async {
|
||||
await smtc.setEndTime(duration);
|
||||
});
|
||||
|
||||
subscriptions.addAll([
|
||||
buttonStream,
|
||||
playerStateStream,
|
||||
positionStream,
|
||||
durationStream,
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> addTrack(Track track) async {
|
||||
if (!smtc.enabled) {
|
||||
await smtc.enableSmtc();
|
||||
}
|
||||
await smtc.updateMetadata(
|
||||
MusicMetadata(
|
||||
title: track.name!,
|
||||
albumArtist: track.artists?.firstOrNull?.name ?? "Unknown",
|
||||
artist: track.artists?.asString() ?? "Unknown",
|
||||
album: track.album?.name ?? "Unknown",
|
||||
thumbnail: (track.album?.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
smtc.disableSmtc();
|
||||
smtc.dispose();
|
||||
for (var element in subscriptions) {
|
||||
element.cancel();
|
||||
}
|
||||
}
|
||||
}
|
53
lib/services/primitive.dart
Executable file
53
lib/services/primitive.dart
Executable file
@ -0,0 +1,53 @@
|
||||
import 'dart:math';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
abstract class PrimitiveUtils {
|
||||
static bool containsTextInBracket(String matcher, String text) {
|
||||
final allMatches = RegExp(r"(?<=\().+?(?=\))").allMatches(matcher);
|
||||
if (allMatches.isEmpty) return false;
|
||||
return allMatches
|
||||
.map((e) => e.group(0))
|
||||
.every((match) => match?.contains(text) ?? false);
|
||||
}
|
||||
|
||||
static final Random _random = Random();
|
||||
static T getRandomElement<T>(List<T> list) {
|
||||
return list[_random.nextInt(list.length)];
|
||||
}
|
||||
|
||||
static const uuid = Uuid();
|
||||
|
||||
static String toReadableNumber(double num) {
|
||||
if (num > 999 && num < 99999) {
|
||||
return "${(num / 1000).toStringAsFixed(0)}K";
|
||||
} else if (num > 99999 && num < 999999) {
|
||||
return "${(num / 1000).toStringAsFixed(0)}K";
|
||||
} else if (num > 999999 && num < 999999999) {
|
||||
return "${(num / 1000000).toStringAsFixed(0)}M";
|
||||
} else if (num > 999999999) {
|
||||
return "${(num / 1000000000).toStringAsFixed(0)}B";
|
||||
} else {
|
||||
return num.toStringAsFixed(0);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<T> raceMultiple<T>(
|
||||
Future<T> Function() inner, {
|
||||
Duration timeout = const Duration(milliseconds: 2500),
|
||||
int retryCount = 4,
|
||||
}) async {
|
||||
return Future.any(
|
||||
List.generate(retryCount, (i) {
|
||||
if (i == 0) return inner();
|
||||
return Future.delayed(
|
||||
Duration(milliseconds: timeout.inMilliseconds * i),
|
||||
inner,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static String toSafeFileName(String str) {
|
||||
return str.replaceAll(RegExp(r'[/\?%*:|"<>]'), ' ');
|
||||
}
|
||||
}
|
39
lib/services/server/active_sourced_track.dart
Executable file
39
lib/services/server/active_sourced_track.dart
Executable file
@ -0,0 +1,39 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:rhythm_box/providers/audio_player.dart';
|
||||
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
||||
import 'package:rhythm_box/services/sourced_track/models/source_info.dart';
|
||||
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
|
||||
|
||||
class ActiveSourcedTrackProvider extends GetxController {
|
||||
Rx<SourcedTrack?> state = Rx(null);
|
||||
|
||||
void updateTrack(SourcedTrack? sourcedTrack) {
|
||||
state.value = sourcedTrack;
|
||||
}
|
||||
|
||||
Future<void> populateSibling() async {
|
||||
if (state.value == null) return;
|
||||
state.value = await state.value!.copyWithSibling();
|
||||
}
|
||||
|
||||
Future<void> swapSibling(SourceInfo sibling) async {
|
||||
if (state.value == null) return;
|
||||
await populateSibling();
|
||||
final newTrack = await state.value!.swapWithSibling(sibling);
|
||||
if (newTrack == null) return;
|
||||
|
||||
state.value = newTrack;
|
||||
await audioPlayer.pause();
|
||||
|
||||
final playback = Get.find<AudioPlayerProvider>();
|
||||
final oldActiveIndex = audioPlayer.currentIndex;
|
||||
|
||||
await playback.addTracksAtFirst([newTrack]);
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
await playback.jumpToTrack(newTrack);
|
||||
|
||||
await audioPlayer.removeTrack(oldActiveIndex);
|
||||
|
||||
await audioPlayer.resume();
|
||||
}
|
||||
}
|
66
lib/services/server/routes/playback.dart
Executable file
66
lib/services/server/routes/playback.dart
Executable file
@ -0,0 +1,66 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart' hide Response;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart' hide Response;
|
||||
import 'package:rhythm_box/providers/audio_player.dart';
|
||||
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
||||
import 'package:rhythm_box/services/server/active_sourced_track.dart';
|
||||
import 'package:rhythm_box/services/server/sourced_track.dart';
|
||||
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
|
||||
class ServerPlaybackRoutesProvider {
|
||||
/// @get('/stream/<trackId>')
|
||||
Future<Response> getStreamTrackId(Request request, String trackId) async {
|
||||
final AudioPlayerProvider playback = Get.find();
|
||||
|
||||
try {
|
||||
final track = playback.state.value.tracks
|
||||
.firstWhere((element) => element.id == trackId);
|
||||
|
||||
final ActiveSourcedTrackProvider activeSourcedTrack = Get.find();
|
||||
final sourcedTrack = activeSourcedTrack.state.value?.id == track.id
|
||||
? activeSourcedTrack
|
||||
: await Get.find<SourcedTrackProvider>().fetch(RhythmMedia(track));
|
||||
|
||||
activeSourcedTrack.updateTrack(sourcedTrack as SourcedTrack?);
|
||||
|
||||
final res = await Dio().get(
|
||||
sourcedTrack!.url,
|
||||
options: Options(
|
||||
headers: {
|
||||
...request.headers,
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
||||
"host": Uri.parse(sourcedTrack.url).host,
|
||||
"Cache-Control": "max-age=0",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
responseType: ResponseType.stream,
|
||||
validateStatus: (status) => status! < 500,
|
||||
),
|
||||
);
|
||||
|
||||
final audioStream =
|
||||
(res.data?.stream as Stream<Uint8List>?)?.asBroadcastStream();
|
||||
|
||||
audioStream!.listen(
|
||||
(event) {},
|
||||
cancelOnError: true,
|
||||
);
|
||||
|
||||
return Response(
|
||||
res.statusCode!,
|
||||
body: audioStream,
|
||||
context: {
|
||||
"shelf.io.buffer_output": false,
|
||||
},
|
||||
headers: res.headers.map,
|
||||
);
|
||||
} catch (e) {
|
||||
log('[PlaybackSever] Error: $e');
|
||||
return Response.internalServerError();
|
||||
}
|
||||
}
|
||||
}
|
48
lib/services/server/server.dart
Executable file
48
lib/services/server/server.dart
Executable file
@ -0,0 +1,48 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:math' hide log;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart' hide Response;
|
||||
import 'package:rhythm_box/services/rhythm_media.dart';
|
||||
import 'package:rhythm_box/services/server/routes/playback.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
class PlaybackServerProvider extends GetxController {
|
||||
HttpServer? _server;
|
||||
Router? _router;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
_initServer();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
Future<void> _initServer() async {
|
||||
const pipeline = Pipeline();
|
||||
if (kDebugMode) {
|
||||
pipeline.addMiddleware(logRequests());
|
||||
}
|
||||
|
||||
final port = Random().nextInt(17500) + 5000;
|
||||
|
||||
RhythmMedia.serverPort = port;
|
||||
|
||||
_router = Router();
|
||||
_router!.get("/ping", (Request request) => Response.ok("pong"));
|
||||
_router!.get(
|
||||
"/stream/<trackId>",
|
||||
Get.find<ServerPlaybackRoutesProvider>().getStreamTrackId,
|
||||
);
|
||||
|
||||
_server = await serve(
|
||||
pipeline.addHandler(_router!.call),
|
||||
InternetAddress.anyIPv4,
|
||||
port,
|
||||
);
|
||||
|
||||
log('[Playback] Playback server at http://${_server!.address.host}:${_server!.port}');
|
||||
}
|
||||
}
|
17
lib/services/server/sourced_track.dart
Executable file
17
lib/services/server/sourced_track.dart
Executable file
@ -0,0 +1,17 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
||||
import 'package:rhythm_box/services/local_track.dart';
|
||||
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
|
||||
|
||||
class SourcedTrackProvider extends GetxController {
|
||||
Future<SourcedTrack?> fetch(RhythmMedia? media) async {
|
||||
final track = media?.track;
|
||||
if (track == null || track is LocalTrack) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final sourcedTrack = await SourcedTrack.fetchFromTrack(track: track);
|
||||
|
||||
return sourcedTrack;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user