Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactorying #2011

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 106 additions & 111 deletions lib/services/audio_player/audio_player.dart
Original file line number Diff line number Diff line change
@@ -1,56 +1,53 @@
import 'dart:io';

import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/services/logger/logger.dart';
import 'package:flutter/foundation.dart';
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/local_track.dart';
import 'package:spotube/services/audio_player/custom_player.dart';
import 'dart:async';

import 'package:media_kit/media_kit.dart' as mk;

import 'package:spotube/services/audio_player/playback_state.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/platform.dart';

part 'audio_players_streams_mixin.dart';
part 'audio_player_impl.dart';

// Constants class for shared constants like port and addresses
class Constants {
static const defaultServerPort = 8080;
static const defaultLocalHost = "localhost";
}

// Helper to get network address based on the platform
String getNetworkAddress() {
return kIsWindows ? Constants.defaultLocalHost : InternetAddress.anyIPv4.address;
}

// Helper to get URI for a given track
String getUriForTrack(Track track, int serverPort) {
return track is LocalTrack
? track.path
: "http://${getNetworkAddress()}:$serverPort/stream/${track.id}";
}

// SpotubeMedia class handling media creation logic
class SpotubeMedia extends mk.Media {
final Track track;
static int serverPort = Constants.defaultServerPort;

static int serverPort = 0;

SpotubeMedia(
this.track, {
Map<String, dynamic>? extras,
super.httpHeaders,
}) : super(
track is LocalTrack
? track.path
: "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}",
SpotubeMedia(this.track, {Map<String, dynamic>? extras, super.httpHeaders})
: super(
getUriForTrack(track, serverPort),
extras: {
...?extras,
"track": switch (track) {
LocalTrack() => track.toJson(),
SourcedTrack() => track.toJson(),
_ => track.toJson(),
},
"track": track.toJson(),
},
);

@override
String get uri {
return switch (track) {
/// [super.uri] must be used instead of [track.path] to prevent wrong
/// path format exceptions in Windows causing [extras] to be null
LocalTrack() => super.uri,
_ =>
"http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:"
"$serverPort/stream/${track.id}",
};
}
String get uri => getUriForTrack(track, serverPort);

factory SpotubeMedia.fromMedia(mk.Media media) {
final track = media.uri.startsWith("http")
Expand All @@ -62,102 +59,100 @@ class SpotubeMedia extends mk.Media {
httpHeaders: media.httpHeaders,
);
}

// @override
// operator ==(Object other) {
// if (other is! SpotubeMedia) return false;

// final isLocal = track is LocalTrack && other.track is LocalTrack;
// return isLocal
// ? (other.track as LocalTrack).path == (track as LocalTrack).path
// : other.track.id == track.id;
// }

// @override
// int get hashCode => track is LocalTrack
// ? (track as LocalTrack).path.hashCode
// : track.id.hashCode;
}

abstract class AudioPlayerInterface {
final CustomPlayer _mkPlayer;

AudioPlayerInterface()
: _mkPlayer = CustomPlayer(
configuration: const mk.PlayerConfiguration(
title: "Spotube",
logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error,
),
) {
_mkPlayer.stream.error.listen((event) {
AppLogger.reportError(event, StackTrace.current);
});
}

/// Whether the current platform supports the audioplayers plugin
static const bool _mkSupportedPlatform = true;

bool get mkSupportedPlatform => _mkSupportedPlatform;

Duration get duration {
return _mkPlayer.state.duration;
}

Playlist get playlist {
return _mkPlayer.state.playlist;
// Factory class to create SpotubeMedia instances
class SpotubeMediaFactory {
static SpotubeMedia create(Track track, {Map<String, dynamic>? extras, Map<String, String>? headers}) {
return SpotubeMedia(track, extras: extras, httpHeaders: headers);
}
}

Duration get position {
return _mkPlayer.state.position;
}
// Playback state management class
class PlaybackStateManager {
final CustomPlayer player;

Duration get bufferedPosition {
return _mkPlayer.state.buffer;
}
PlaybackStateManager(this.player);

Future<mk.AudioDevice> get selectedDevice async {
return _mkPlayer.state.audioDevice;
}
bool get isPlaying => player.state.playing;
bool get isPaused => !player.state.playing;
bool get isStopped => player.state.playlist.medias.isEmpty;

Future<List<mk.AudioDevice>> get devices async {
return _mkPlayer.state.audioDevices;
}
Duration get duration => player.state.duration;
Duration get position => player.state.position;
Duration get bufferedPosition => player.state.buffer;
bool get isShuffled => player.shuffled;
double get volume => player.state.volume / 100;

bool get hasSource {
return _mkPlayer.state.playlist.medias.isNotEmpty;
}

// states
bool get isPlaying {
return _mkPlayer.state.playing;
}

bool get isPaused {
return !_mkPlayer.state.playing;
}
Future<List<mk.AudioDevice>> get devices async => player.state.audioDevices;
Future<mk.AudioDevice> get selectedDevice async => player.state.audioDevice;

bool get isStopped {
return !hasSource;
}
PlaylistMode get loopMode => player.state.playlistMode;
}

Future<bool> get isCompleted async {
return _mkPlayer.state.completed;
}
// Main AudioPlayerInterface class with DI and error handling
abstract class AudioPlayerInterface {
final CustomPlayer player;
final PlaybackStateManager stateManager;

bool get isShuffled {
return _mkPlayer.shuffled;
AudioPlayerInterface(this.player)
: stateManager = PlaybackStateManager(player) {
player.stream.error.listen((event) {
AppLogger.reportError(event, StackTrace.current);
// Retry or fallback mechanism can be added here
});
}

PlaylistMode get loopMode {
return _mkPlayer.state.playlistMode;
}
// High-level control methods for playback
Future<void> play() async {
try {
await player.play();
} catch (e) {
AppLogger.reportError(e, StackTrace.current);
}
}

Future<void> pause() async {
try {
await player.pause();
} catch (e) {
AppLogger.reportError(e, StackTrace.current);
}
}

Future<void> stop() async {
try {
await player.stop();
} catch (e) {
AppLogger.reportError(e, StackTrace.current);
}
}

Future<void> seek(Duration position) async {
try {
await player.seek(position);
} catch (e) {
AppLogger.reportError(e, StackTrace.current);
}
}

// Access state information through the state manager
bool get isPlaying => stateManager.isPlaying;
bool get isPaused => stateManager.isPaused;
bool get isStopped => stateManager.isStopped;
Duration get duration => stateManager.duration;
Duration get position => stateManager.position;
Duration get bufferedPosition => stateManager.bufferedPosition;
bool get isShuffled => stateManager.isShuffled;
double get volume => stateManager.volume;
Future<List<mk.AudioDevice>> get devices => stateManager.devices;
Future<mk.AudioDevice> get selectedDevice => stateManager.selectedDevice;
PlaylistMode get loopMode => stateManager.loopMode;
}

/// Returns the current volume of the player, between 0 and 1
double get volume {
return _mkPlayer.state.volume / 100;
}
// Example implementation for a specific platform/player
class MyAudioPlayer extends AudioPlayerInterface {
MyAudioPlayer(CustomPlayer player) : super(player);

bool get isBuffering {
return _mkPlayer.state.buffering;
}
// Additional functionality can be added here if necessary
}
Loading