mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-05 23:19:42 +00:00
feat(playback): add uncompressed flac playback support
This commit is contained in:
parent
e8a54d3209
commit
cecb687592
@ -39,6 +39,11 @@ class InstallDependenciesCommand extends Command {
|
||||
|
||||
switch (argResults!.option("platform")) {
|
||||
case "windows":
|
||||
await shell.run(
|
||||
"""
|
||||
choco install innosetup -y
|
||||
""",
|
||||
);
|
||||
break;
|
||||
case "linux":
|
||||
await shell.run(
|
||||
|
||||
@ -462,5 +462,6 @@
|
||||
"configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider",
|
||||
"audio_scrobblers": "Audio Scrobblers",
|
||||
"scrobbling": "Scrobbling",
|
||||
"source": "Source: "
|
||||
"source": "Source: ",
|
||||
"uncompressed": "Uncompressed"
|
||||
}
|
||||
|
||||
@ -2936,6 +2936,12 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Source: '**
|
||||
String get source;
|
||||
|
||||
/// No description provided for @uncompressed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Uncompressed'**
|
||||
String get uncompressed;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@ -1540,4 +1540,7 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1541,4 +1541,7 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1551,4 +1551,7 @@ class AppLocalizationsCa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1541,4 +1541,7 @@ class AppLocalizationsCs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1553,4 +1553,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1539,4 +1539,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1554,4 +1554,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1551,4 +1551,7 @@ class AppLocalizationsEu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1539,4 +1539,7 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1539,4 +1539,7 @@ class AppLocalizationsFi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1559,4 +1559,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1545,4 +1545,7 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1547,4 +1547,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1546,4 +1546,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1510,4 +1510,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1548,4 +1548,7 @@ class AppLocalizationsKa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1514,4 +1514,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1551,4 +1551,7 @@ class AppLocalizationsNe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1545,4 +1545,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1547,4 +1547,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1544,4 +1544,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1547,4 +1547,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1553,4 +1553,7 @@ class AppLocalizationsTa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1536,4 +1536,7 @@ class AppLocalizationsTh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1554,4 +1554,7 @@ class AppLocalizationsTl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1547,4 +1547,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1543,4 +1543,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1549,4 +1549,7 @@ class AppLocalizationsVi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
@ -1503,6 +1503,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get source => 'Source: ';
|
||||
|
||||
@override
|
||||
String get uncompressed => 'Uncompressed';
|
||||
}
|
||||
|
||||
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
||||
|
||||
@ -41,14 +41,6 @@ enum YoutubeClientEngine {
|
||||
}
|
||||
}
|
||||
|
||||
enum MusicCodec {
|
||||
m4a._("M4a (Best for downloaded music)"),
|
||||
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata");
|
||||
|
||||
final String label;
|
||||
const MusicCodec._(this.label);
|
||||
}
|
||||
|
||||
enum SearchMode {
|
||||
youtube._("YouTube"),
|
||||
youtubeMusic._("YouTube Music");
|
||||
|
||||
@ -44,6 +44,11 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
title: Text(context.l10n.audio_quality),
|
||||
value: preferences.audioQuality,
|
||||
options: [
|
||||
if (preferences.audioSource == AudioSource.dabMusic)
|
||||
SelectItemButton(
|
||||
value: SourceQualities.uncompressed,
|
||||
child: Text(context.l10n.uncompressed),
|
||||
),
|
||||
SelectItemButton(
|
||||
value: SourceQualities.high,
|
||||
child: Text(context.l10n.high),
|
||||
|
||||
@ -101,10 +101,7 @@ class ServerPlaybackRoutes {
|
||||
);
|
||||
|
||||
final contentLengthRes = await Future<dio_lib.Response?>.value(
|
||||
dio.head(
|
||||
url,
|
||||
options: options,
|
||||
),
|
||||
dio.head(url, options: options),
|
||||
).catchError((e, stack) async {
|
||||
AppLogger.reportError(e, stack);
|
||||
|
||||
@ -135,6 +132,21 @@ class ServerPlaybackRoutes {
|
||||
);
|
||||
}
|
||||
|
||||
if (headers["range"] == "bytes=0-") {
|
||||
final bufferSize =
|
||||
userPreferences.audioQuality == SourceQualities.uncompressed
|
||||
? 6 * 1024 * 1024
|
||||
: 4 * 1024 * 1024;
|
||||
final endRange = min(bufferSize,
|
||||
int.parse(contentLengthRes?.headers.value("content-length") ?? "0"));
|
||||
options = options.copyWith(
|
||||
headers: {
|
||||
...options.headers ?? {},
|
||||
"range": "bytes=0-$endRange",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final res = await dio.get<Uint8List>(url, options: options);
|
||||
|
||||
final bytes = res.data;
|
||||
@ -166,7 +178,7 @@ class ServerPlaybackRoutes {
|
||||
await trackPartialCacheFile.rename(trackCacheFile.path);
|
||||
}
|
||||
|
||||
if (contentRange.total == fileLength && track.codec == SourceCodecs.m4a) {
|
||||
if (contentRange.total == fileLength && track.codec != SourceCodecs.weba) {
|
||||
final playlistTrack = playlist.tracks.firstWhereOrNull(
|
||||
(element) => element.id == track.query.id,
|
||||
);
|
||||
|
||||
@ -54,6 +54,7 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
|
||||
}
|
||||
|
||||
await audioPlayer.setAudioNormalization(state.normalizeAudio);
|
||||
await _updatePlayerBufferSize(event.audioQuality, state.audioQuality);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
@ -79,6 +80,24 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Sets audio player's buffer size based on the selected audio quality
|
||||
/// Uncompressed quality gets a larger buffer size for smoother playback
|
||||
/// while other qualities use a standard buffer size.
|
||||
Future<void> _updatePlayerBufferSize(
|
||||
SourceQualities newQuality,
|
||||
SourceQualities oldQuality,
|
||||
) async {
|
||||
if (newQuality == SourceQualities.uncompressed) {
|
||||
audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldQuality == SourceQualities.uncompressed &&
|
||||
newQuality != SourceQualities.uncompressed) {
|
||||
audioPlayer.setDemuxerBufferSize(4 * 1024 * 1024); // 4MB
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setData(PreferencesTableCompanion data) async {
|
||||
final db = ref.read(databaseProvider);
|
||||
|
||||
@ -155,6 +174,7 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
|
||||
|
||||
void setAudioQuality(SourceQualities quality) {
|
||||
setData(PreferencesTableCompanion(audioQuality: Value(quality)));
|
||||
_updatePlayerBufferSize(quality, state.audioQuality);
|
||||
}
|
||||
|
||||
void setDownloadLocation(String downloadDir) {
|
||||
@ -204,6 +224,11 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
|
||||
}
|
||||
|
||||
void setAudioSource(AudioSource type) {
|
||||
// Only allow uncompressed quality for DAB Music
|
||||
if (type != AudioSource.dabMusic &&
|
||||
state.audioQuality == SourceQualities.uncompressed) {
|
||||
setAudioQuality(SourceQualities.high);
|
||||
}
|
||||
setData(PreferencesTableCompanion(audioSource: Value(type)));
|
||||
}
|
||||
|
||||
|
||||
@ -131,4 +131,8 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
||||
Future<void> setAudioNormalization(bool normalize) async {
|
||||
await _mkPlayer.setAudioNormalization(normalize);
|
||||
}
|
||||
|
||||
Future<void> setDemuxerBufferSize(int sizeInBytes) async {
|
||||
await _mkPlayer.setDemuxerBufferSize(sizeInBytes);
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,4 +133,12 @@ class CustomPlayer extends Player {
|
||||
await nativePlayer.setProperty('af', '');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setDemuxerBufferSize(int sizeInBytes) async {
|
||||
await nativePlayer.setProperty('demuxer-max-bytes', sizeInBytes.toString());
|
||||
await nativePlayer.setProperty(
|
||||
'demuxer-max-back-bytes',
|
||||
sizeInBytes.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,13 +3,15 @@ import 'package:spotube/models/playback/track_sources.dart';
|
||||
enum SourceCodecs {
|
||||
m4a._("M4a (Best for downloaded music)"),
|
||||
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"),
|
||||
mp3._("MP3 (Widely supported audio format)");
|
||||
mp3._("MP3 (Widely supported audio format)"),
|
||||
flac._("FLAC (Lossless, best quality)\nLarge file size");
|
||||
|
||||
final String label;
|
||||
const SourceCodecs._(this.label);
|
||||
}
|
||||
|
||||
enum SourceQualities {
|
||||
uncompressed(3),
|
||||
high(2),
|
||||
medium(1),
|
||||
low(0);
|
||||
|
||||
@ -212,7 +212,10 @@ abstract class SourcedTrack extends BasicSourcedTrack {
|
||||
final preferences = ref.read(userPreferencesProvider);
|
||||
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.dabMusic => SourceCodecs.mp3,
|
||||
AudioSource.dabMusic =>
|
||||
preferences.audioQuality == SourceQualities.uncompressed
|
||||
? SourceCodecs.flac
|
||||
: SourceCodecs.mp3,
|
||||
AudioSource.jiosaavn => SourceCodecs.m4a,
|
||||
_ => preferences.streamMusicCodec
|
||||
};
|
||||
|
||||
@ -56,9 +56,10 @@ class DABMusicSourcedTrack extends SourcedTrack {
|
||||
SourceQualities quality,
|
||||
) async {
|
||||
try {
|
||||
final isUncompressed = quality == SourceQualities.uncompressed;
|
||||
final streamResponse = await dabMusicApiClient.music.getStream(
|
||||
trackId: id,
|
||||
quality: "5", // mp3 320kbps (best available)
|
||||
quality: isUncompressed ? "27" : "5",
|
||||
);
|
||||
if (streamResponse.url == null) {
|
||||
throw Exception("No stream URL found for track ID: $id");
|
||||
@ -66,9 +67,11 @@ class DABMusicSourcedTrack extends SourcedTrack {
|
||||
return [
|
||||
TrackSource(
|
||||
url: streamResponse.url!,
|
||||
quality: SourceQualities.high,
|
||||
bitrate: "320kbps",
|
||||
codec: SourceCodecs.mp3,
|
||||
quality: isUncompressed
|
||||
? SourceQualities.uncompressed
|
||||
: SourceQualities.high,
|
||||
bitrate: isUncompressed ? "2998kbps" : "320kbps",
|
||||
codec: isUncompressed ? SourceCodecs.flac : SourceCodecs.mp3,
|
||||
),
|
||||
];
|
||||
} catch (e, stackTrace) {
|
||||
@ -126,7 +129,7 @@ class DABMusicSourcedTrack extends SourcedTrack {
|
||||
if (results.isEmpty) {
|
||||
final res = await dabMusicApiClient.music.getSearch(
|
||||
q: SourcedTrack.getSearchTerm(query),
|
||||
limit: 20,
|
||||
limit: 5,
|
||||
);
|
||||
results = res.tracks ?? <Track>[];
|
||||
}
|
||||
|
||||
@ -1,118 +1,147 @@
|
||||
{
|
||||
"ar": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"bn": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"cs": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"de": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"eu": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"id": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"ka": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"ne": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
"audio_source",
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"ta": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"th": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"tl": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"vi": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
],
|
||||
|
||||
"zh_TW": [
|
||||
"source"
|
||||
"source",
|
||||
"uncompressed"
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user