feat(android-playback): option to download track bytes and play instead of Streaming

This commit is contained in:
Kingkor Roy Tirtho 2022-10-16 11:21:21 +06:00
parent 3b4306b7ab
commit dcc8ba5a54
6 changed files with 113 additions and 23 deletions

View File

@ -14,6 +14,7 @@ import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/platform.dart';
import 'package:url_launcher/url_launcher_string.dart';
class Settings extends HookConsumerWidget {
@ -272,6 +273,23 @@ class Settings extends HookConsumerWidget {
},
),
),
if (kIsMobile)
ListTile(
leading: const Icon(Icons.download_for_offline_rounded),
title: const Text(
"Pre download and play",
),
subtitle: const Text(
"Instead of streaming audio, download bytes and play instead (Recommended for higher bandwidth users)",
),
trailing: Switch.adaptive(
activeColor: Theme.of(context).primaryColor,
value: preferences.androidBytesPlay,
onChanged: (state) {
preferences.setAndroidBytesPlay(state);
},
),
),
ListTile(
leading: const Icon(Icons.fast_forward_rounded),
title: const Text(

View File

@ -52,10 +52,11 @@ class Downloader with ChangeNotifier {
// Using android Audio Focus to keep the app run in background
_playback.mobileAudioService?.session?.setActive(true);
grabberQueue.add(() async {
final track = await ref.read(playbackProvider).toSpotubeTrack(
final track = (await ref.read(playbackProvider).toSpotubeTrack(
baseTrack,
noSponsorBlock: true,
);
))
.item1;
_queue.add(() async {
final cleanTitle = track.ytTrack.title.replaceAll(
RegExp(r'[/\\?%*:|"<>]'),

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:audio_service/audio_service.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart';
import 'package:spotify/spotify.dart';
@ -21,6 +22,7 @@ import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Playlist;
import 'package:collection/collection.dart';
import 'package:spotube/extensions/list-sort-multiple.dart';
@ -154,7 +156,7 @@ class Playback extends PersistedChangeNotifier {
playlist!.tracks[currentTrackIndex + 1],
).then((v) {
_isPreSearching = false;
return v;
return v.item1;
});
}
if (track != null && preferences.skipSponsorSegments) {
@ -211,10 +213,12 @@ class Playback extends PersistedChangeNotifier {
status = PlaybackStatus.loading;
notifyListeners();
}
AudioOnlyStreamInfo? manifest;
// the track is not a SpotubeTrack so turning it to one
if (track is! SpotubeTrack) {
track = await toSpotubeTrack(track);
final s = await toSpotubeTrack(track);
track = s.item1;
manifest = s.item2;
}
final tag = MediaItem(
@ -238,7 +242,7 @@ class Playback extends PersistedChangeNotifier {
updatePersistence();
await player.play(
track.ytUri.startsWith("http")
? UrlSource(track.ytUri)
? await getAppropriateSource(track, manifest)
: DeviceFileSource(track.ytUri),
);
status = PlaybackStatus.playing;
@ -372,7 +376,7 @@ class Playback extends PersistedChangeNotifier {
}
// playlist & track list methods
Future<SpotubeTrack> toSpotubeTrack(
Future<Tuple2<SpotubeTrack, AudioOnlyStreamInfo>> toSpotubeTrack(
Track track, {
bool noSponsorBlock = false,
}) async {
@ -389,7 +393,7 @@ class Playback extends PersistedChangeNotifier {
_logger.v("[Track Search Artists] $artistsName");
final mainArtist = artistsName.first;
final featuredArtists = artistsName.length > 1
? "feat. " + artistsName.sublist(1).join(" ")
? "feat. ${artistsName.sublist(1).join(" ")}"
: "";
final title = ServiceUtils.getTitle(
track.name!,
@ -487,11 +491,11 @@ class Playback extends PersistedChangeNotifier {
}
});
final ytUri = (audioQuality == AudioQuality.high
final chosenStreamInfo = audioQuality == AudioQuality.high
? audioManifest.withHighestBitrate()
: audioManifest.sortByBitrate().last)
.url
.toString();
: audioManifest.sortByBitrate().last;
final ytUri = chosenStreamInfo.url.toString();
final skipSegments = cachedTrack?.skipSegments != null &&
cachedTrack!.skipSegments!.isNotEmpty
@ -517,7 +521,8 @@ class Playback extends PersistedChangeNotifier {
);
}
return SpotubeTrack.fromTrack(
return Tuple2(
SpotubeTrack.fromTrack(
track: track,
ytTrack: ytVideo,
// Since Mac OS's & IOS's CodeAudio doesn't support WebMedia
@ -525,6 +530,8 @@ class Playback extends PersistedChangeNotifier {
// codec/mimetype for those Platforms
ytUri: ytUri,
skipSegments: skipSegments,
),
chosenStreamInfo,
);
} catch (e, stack) {
_logger.e("topSpotubeTrack", e, stack);
@ -532,6 +539,52 @@ class Playback extends PersistedChangeNotifier {
}
}
Future<Source> getAppropriateSource(
SpotubeTrack track, [
AudioOnlyStreamInfo? manifest,
]) async {
if (!kIsMobile || !preferences.androidBytesPlay) {
return UrlSource(track.ytUri);
}
final List<int> bytesStore = [];
final bytesFuture = Completer<Uint8List>();
if (manifest == null) {
StreamManifest trackManifest = await raceMultiple(
() => youtube.videos.streams.getManifest(track.ytTrack.id),
);
final audioManifest = trackManifest.audioOnly.where((info) {
final isMp4a = info.codec.mimeType == "audio/mp4";
if (kIsLinux) {
return !isMp4a;
} else if (kIsMacOS || kIsIOS) {
return isMp4a;
} else {
return true;
}
});
manifest ??= audioManifest.sortByBitrate().last;
}
youtube.videos.streamsClient.get(manifest).listen(
(data) {
bytesStore.addAll(data);
},
onDone: () {
bytesFuture.complete(Uint8List.fromList(bytesStore));
},
onError: (e) {
_logger.e("toByteTrack", e);
bytesFuture.completeError(e);
},
);
final bytes = await bytesFuture.future;
return bytes.isNotEmpty ? BytesSource(bytes) : UrlSource(track.ytUri);
}
Future<void> setPlaylistPosition(int position) async {
if (playlist == null) return;
await playPlaylist(playlist!, position);

View File

@ -38,12 +38,15 @@ class UserPreferences extends PersistedChangeNotifier {
LayoutMode layoutMode;
bool rotatingAlbumArt;
bool androidBytesPlay;
UserPreferences({
required this.geniusAccessToken,
required this.recommendationMarket,
required this.themeMode,
required this.ytSearchFormat,
required this.layoutMode,
this.androidBytesPlay = true,
this.saveTrackLyrics = false,
this.accentColorScheme = Colors.green,
this.backgroundColorScheme = Colors.grey,
@ -63,6 +66,11 @@ class UserPreferences extends PersistedChangeNotifier {
}
}
void setAndroidBytesPlay(bool value) {
androidBytesPlay = value;
notifyListeners();
}
void setThemeMode(ThemeMode mode) {
themeMode = mode;
notifyListeners();
@ -191,6 +199,7 @@ class UserPreferences extends PersistedChangeNotifier {
orElse: () => kIsDesktop ? LayoutMode.extended : LayoutMode.compact,
);
rotatingAlbumArt = map["rotatingAlbumArt"] ?? rotatingAlbumArt;
androidBytesPlay = map["androidBytesPlay"] ?? androidBytesPlay;
}
@override
@ -210,6 +219,7 @@ class UserPreferences extends PersistedChangeNotifier {
"downloadLocation": downloadLocation,
"layoutMode": layoutMode.name,
"rotatingAlbumArt": rotatingAlbumArt,
"androidBytesPlay": androidBytesPlay,
};
}
}

View File

@ -1229,6 +1229,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
tuple:
dependency: "direct main"
description:
name: tuple
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
typed_data:
dependency: transitive
description:

View File

@ -60,6 +60,7 @@ dependencies:
fl_query: ^0.3.0
fl_query_hooks: ^0.3.0
flutter_inappwebview: ^5.4.3+7
tuple: ^2.0.1
dev_dependencies:
flutter_test: