Skip to content

Commit

Permalink
feat(android-playback): option to download track bytes and play inste…
Browse files Browse the repository at this point in the history
…ad of Streaming
  • Loading branch information
KRTirtho committed Oct 16, 2022
1 parent 3b4306b commit dcc8ba5
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 23 deletions.
18 changes: 18 additions & 0 deletions lib/components/Settings/Settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down
9 changes: 5 additions & 4 deletions lib/provider/Downloader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
baseTrack,
noSponsorBlock: true,
);
final track = (await ref.read(playbackProvider).toSpotubeTrack(
baseTrack,
noSponsorBlock: true,
))
.item1;
_queue.add(() async {
final cleanTitle = track.ytTrack.title.replaceAll(
RegExp(r'[/\\?%*:|"<>]'),
Expand Down
91 changes: 72 additions & 19 deletions lib/provider/Playback.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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!,
Expand Down Expand Up @@ -487,11 +491,11 @@ class Playback extends PersistedChangeNotifier {
}
});

final ytUri = (audioQuality == AudioQuality.high
? audioManifest.withHighestBitrate()
: audioManifest.sortByBitrate().last)
.url
.toString();
final chosenStreamInfo = audioQuality == AudioQuality.high
? audioManifest.withHighestBitrate()
: audioManifest.sortByBitrate().last;

final ytUri = chosenStreamInfo.url.toString();

final skipSegments = cachedTrack?.skipSegments != null &&
cachedTrack!.skipSegments!.isNotEmpty
Expand All @@ -517,21 +521,70 @@ class Playback extends PersistedChangeNotifier {
);
}

return SpotubeTrack.fromTrack(
track: track,
ytTrack: ytVideo,
// Since Mac OS's & IOS's CodeAudio doesn't support WebMedia
// ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg'
// codec/mimetype for those Platforms
ytUri: ytUri,
skipSegments: skipSegments,
return Tuple2(
SpotubeTrack.fromTrack(
track: track,
ytTrack: ytVideo,
// Since Mac OS's & IOS's CodeAudio doesn't support WebMedia
// ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg'
// codec/mimetype for those Platforms
ytUri: ytUri,
skipSegments: skipSegments,
),
chosenStreamInfo,
);
} catch (e, stack) {
_logger.e("topSpotubeTrack", e, stack);
rethrow;
}
}

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);
Expand Down
10 changes: 10 additions & 0 deletions lib/provider/UserPreferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -63,6 +66,11 @@ class UserPreferences extends PersistedChangeNotifier {
}
}

void setAndroidBytesPlay(bool value) {
androidBytesPlay = value;
notifyListeners();
}

void setThemeMode(ThemeMode mode) {
themeMode = mode;
notifyListeners();
Expand Down Expand Up @@ -191,6 +199,7 @@ class UserPreferences extends PersistedChangeNotifier {
orElse: () => kIsDesktop ? LayoutMode.extended : LayoutMode.compact,
);
rotatingAlbumArt = map["rotatingAlbumArt"] ?? rotatingAlbumArt;
androidBytesPlay = map["androidBytesPlay"] ?? androidBytesPlay;
}

@override
Expand All @@ -210,6 +219,7 @@ class UserPreferences extends PersistedChangeNotifier {
"downloadLocation": downloadLocation,
"layoutMode": layoutMode.name,
"rotatingAlbumArt": rotatingAlbumArt,
"androidBytesPlay": androidBytesPlay,
};
}
}
Expand Down
7 changes: 7 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit dcc8ba5

Please sign in to comment.