Skip to content

Commit

Permalink
feat: play youtube videos in local miniplayer option
Browse files Browse the repository at this point in the history
closes #19
  • Loading branch information
MSOB7YY committed Feb 27, 2024
1 parent 168d653 commit 15ec081
Show file tree
Hide file tree
Showing 18 changed files with 2,819 additions and 2,210 deletions.
104 changes: 80 additions & 24 deletions lib/base/audio_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
bool get isCurrentAudioFromCache => _isCurrentAudioFromCache;
bool _isCurrentAudioFromCache = false;

VideoOptions? _latestVideoOptions;
Future<void> setAudioOnlyPlayback(bool audioOnly) async {
settings.save(ytIsAudioOnlyMode: audioOnly);
if (audioOnly) {
currentVideoStream.value = null;
currentAudioStream.value = null;
currentCachedVideo.value = null;
await super.setVideo(null);
} else {
if (_latestVideoOptions != null) await super.setVideo(_latestVideoOptions);
}
}

Expand Down Expand Up @@ -476,7 +476,12 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
Future<void> onItemPlaySelectable(Q pi, Selectable item, int index, bool startPlaying) async {
final tr = item.track;
videoPlayerInfo.value = null;
WaveformController.inst.generateWaveform(tr);
WaveformController.inst.resetWaveform();
WaveformController.inst.generateWaveform(
path: tr.path,
duration: Duration(seconds: tr.duration),
stillPlaying: (path) => currentItem is Track && path == (currentItem as Track).path,
);
final initialVideo = await VideoController.inst.updateCurrentVideo(tr, returnEarly: true);

// -- generating artwork in case it wasnt, to be displayed in notification
Expand Down Expand Up @@ -678,6 +683,22 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {

File? _nextSeekSetAudioCache;

Future<void> tryGenerateWaveform(YoutubeID? video) async {
if (video != null && WaveformController.inst.isDummy && !settings.youtubeStyleMiniplayer.value) {
final audioPath = currentCachedAudio.value?.file.path ?? _nextSeekSetAudioCache?.path;
final dur = currentItemDuration;
if (audioPath != null && dur != null) {
return WaveformController.inst.generateWaveform(
path: audioPath,
duration: dur,
stillPlaying: (path) =>
currentItem is YoutubeID && currentItem == video && (_nextSeekSetAudioCache != null && path == _nextSeekSetAudioCache?.path) ||
(currentCachedAudio.value != null && path == currentCachedAudio.value?.file.path),
);
}
}
}

/// Adds Cached File to [audioCacheMap] & writes metadata.
Future<void> _onAudioCacheDone(String videoId, File? audioCacheFile) async {
_nextSeekSetAudioCache = audioCacheFile;
Expand All @@ -689,6 +710,18 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
final videoInfo = currentVideoInfo.value;
if (videoInfo?.id == videoId) {
if (audioCacheFile != null) {
// -- generating waveform if needed
if (WaveformController.inst.isDummy && !settings.youtubeStyleMiniplayer.value) {
final dur = currentItemDuration;
if (dur != null) {
WaveformController.inst.generateWaveform(
path: audioCacheFile.path,
duration: dur,
stillPlaying: (path) => currentItem is YoutubeID && _nextSeekSetAudioCache != null && path == _nextSeekSetAudioCache?.path,
);
}
}

// -- Adding recently cached audio to cache map, for being displayed on cards.
audioCacheMap.addNoDuplicatesForce(
videoId,
Expand Down Expand Up @@ -721,6 +754,8 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
}) async {
canPlayAudioOnlyFromCache ??= (isAudioOnlyPlayback || !ConnectivityController.inst.hasConnection);

WaveformController.inst.resetWaveform();

YoutubeController.inst.currentYTQualities.clear();
YoutubeController.inst.currentYTAudioStreams.clear();
YoutubeController.inst.currentCachedQualities.clear();
Expand Down Expand Up @@ -777,9 +812,25 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {

videoPlayerInfo.value = null;

({AudioCacheDetails? audio, NamidaVideo? video}) playedFromCacheDetails = (audio: null, video: null);
({AudioCacheDetails? audio, NamidaVideo? video, Duration? duration}) playedFromCacheDetails = (audio: null, video: null, duration: null);
bool okaySetFromCache() => playedFromCacheDetails.audio != null && (canPlayAudioOnlyFromCache! || playedFromCacheDetails.video != null);

bool generatedWaveform = false;
void generateWaveform() {
if (!generatedWaveform && !settings.youtubeStyleMiniplayer.value) {
final audioDetails = playedFromCacheDetails.audio;
final dur = playedFromCacheDetails.duration;
if (audioDetails != null && dur != null) {
generatedWaveform = true;
WaveformController.inst.generateWaveform(
path: audioDetails.file.path,
duration: dur,
stillPlaying: (path) => currentItem is YoutubeID && path == currentCachedAudio.value?.file.path,
);
}
}
}

/// try playing cache always for faster playback initialization, if the quality should be
/// different then it will be set later after fetching.
playedFromCacheDetails = await _trySetYTVideoWithoutConnection(
Expand All @@ -797,6 +848,8 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
currentCachedAudio.value = playedFromCacheDetails.audio;
currentCachedVideo.value = playedFromCacheDetails.video;

generateWaveform();

bool heyIhandledAudioPlaying = false;
if (okaySetFromCache()) {
heyIhandledAudioPlaying = true;
Expand Down Expand Up @@ -848,7 +901,6 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
final cachedAudio = prefferedAudioStream?.getCachedFile(item.id);
final mediaItem = item.toMediaItem(currentVideoInfo.value, currentVideoThumbnail.value, index, currentQueue.length);
_isCurrentAudioFromCache = cachedAudio != null;
await playerStoppingSeikoo.future;
if (item != currentVideo) return; // race avoidance when playing multiple videos
final isVideoCacheSameAsPrevSet = cachedVideo != null &&
playedFromCacheDetails.video != null &&
Expand All @@ -863,8 +915,6 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
heyIhandledAudioPlaying = !((shouldResetVideoSource && isStreamRequiredBetterThanCachedSet) || shouldResetAudioSource);
}

await playerStoppingSeikoo.future;

VideoOptions? videoOptions;
if (shouldResetVideoSource && isStreamRequiredBetterThanCachedSet) {
videoOptions = VideoOptions(
Expand All @@ -875,6 +925,9 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
maxTotalCacheSize: _defaultMaxCache,
);
}
await playerStoppingSeikoo.future;
if (item != currentVideo) return;

if (cachedVideo?.path != null) {
File(cachedVideo!.path).setLastAccessedTry(DateTime.now());
}
Expand All @@ -901,6 +954,7 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
isVideoFile: cachedVideo?.path != null,
);
} else if (videoOptions != null) {
_latestVideoOptions = videoOptions;
await setVideo(videoOptions);
}

Expand Down Expand Up @@ -929,6 +983,7 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
possibleAudioFiles: audioCacheMap[item.id] ?? [],
possibleLocalFiles: Indexer.inst.allTracksMappedByYTID[item.id] ?? [],
);
if (item == currentVideo) generateWaveform();
if (!okaySetFromCache()) {
showSnackError('skipping');
skipToNext();
Expand Down Expand Up @@ -960,7 +1015,7 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
}

/// Returns Audio File and Video File.
Future<({AudioCacheDetails? audio, NamidaVideo? video})> _trySetYTVideoWithoutConnection({
Future<({AudioCacheDetails? audio, NamidaVideo? video, Duration? duration})> _trySetYTVideoWithoutConnection({
required YoutubeID item,
required int index,
required bool canPlayAudioOnly,
Expand Down Expand Up @@ -1014,7 +1069,7 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
// -- play audio & video
await whatToAwait();
try {
await setSource(
final dur = await setSource(
AudioSource.file(cachedAudio.file.path, tag: mediaItem),
startPlaying: startPlaying,
videoOptions: VideoOptions(
Expand All @@ -1035,16 +1090,16 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
file: cachedAudio.file,
);
refreshNotification();
return (audio: audioDetails, video: cachedVideo);
return (audio: audioDetails, video: cachedVideo, duration: dur);
} catch (_) {
// error in video is handled internally
// while error in audio means the cached file is probably faulty.
return (audio: null, video: cachedVideo);
return (audio: null, video: cachedVideo, duration: null);
}
} else if (cachedAudio != null && canPlayAudioOnly) {
// -- play audio only
await whatToAwait();
await setSource(
final dur = await setSource(
AudioSource.file(cachedAudio.file.path, tag: mediaItem),
startPlaying: startPlaying,
cachedAudioPath: cachedAudio.file.path,
Expand All @@ -1057,9 +1112,9 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
file: cachedAudio.file,
);
refreshNotification();
return (audio: audioDetails, video: null);
return (audio: audioDetails, video: null, duration: dur);
}
return (audio: null, video: null);
return (audio: null, video: null, duration: null);
}

static List<AudioCacheDetails> _getCachedAudiosForID(Map map) {
Expand Down Expand Up @@ -1363,6 +1418,7 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
if (cachedAudioPath != null) {
File(cachedAudioPath).setLastAccessedTry(DateTime.now());
}
if (!(videoOptions == null && keepOldVideoSource)) _latestVideoOptions = videoOptions;
return setAudioSource(
source,
preload: preload,
Expand All @@ -1381,16 +1437,16 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {

Future<void> setVideoSource({required String source, String cacheKey = '', bool loopingAnimation = false, bool isFile = false}) async {
if (isFile) File(source).setLastAccessedTry(DateTime.now());
await super.setVideo(
VideoOptions(
source: source,
loopingAnimation: loopingAnimation,
enableCaching: true,
cacheKey: cacheKey,
cacheDirectory: _defaultCacheDirectory,
maxTotalCacheSize: _defaultMaxCache,
),
final videoOptions = VideoOptions(
source: source,
loopingAnimation: loopingAnimation,
enableCaching: true,
cacheKey: cacheKey,
cacheDirectory: _defaultCacheDirectory,
maxTotalCacheSize: _defaultMaxCache,
);
_latestVideoOptions = videoOptions;
await super.setVideo(videoOptions);
}

@override
Expand Down
2 changes: 1 addition & 1 deletion lib/controller/current_color.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class CurrentColor {
void switchColorPalettes(bool isPlaying) {
_colorsSwitchTimer?.cancel();
_colorsSwitchTimer = null;
if (Player.inst.nowPlayingTrack == kDummyTrack) return;
if (Player.inst.currentQueue.isEmpty && Player.inst.currentQueueYoutube.isEmpty) return;
final durms = isPlaying ? 500 : 2000;
_colorsSwitchTimer = Timer.periodic(Duration(milliseconds: durms), (timer) {
if (settings.enablePartyModeColorSwap.value) {
Expand Down
6 changes: 4 additions & 2 deletions lib/controller/miniplayer_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,11 @@ class MiniPlayerController {
bool bounceUp = false;
bool bounceDown = false;

double get _currentItemExtent => Player.inst.currentQueueYoutube.isNotEmpty ? Dimensions.youtubeCardItemExtent : Dimensions.inst.trackTileItemExtent;

void animateQueueToCurrentTrack({bool jump = false, bool minZero = false}) {
if (queueScrollController.hasClients) {
final trackTileItemScrollOffsetInQueue = Dimensions.inst.trackTileItemExtent * Player.inst.currentIndex - screenSize.height * 0.3;
final trackTileItemScrollOffsetInQueue = _currentItemExtent * Player.inst.currentIndex - screenSize.height * 0.3;
if (queueScrollController.positions.lastOrNull?.pixels == trackTileItemScrollOffsetInQueue) {
return;
}
Expand Down Expand Up @@ -337,7 +339,7 @@ class MiniPlayerController {
void _updateScrollPositionInQueue() {
void updateIcon() {
final pixels = queueScrollController.position.pixels;
final sizeInSettings = Dimensions.inst.trackTileItemExtent * Player.inst.currentIndex - Get.height * 0.3;
final sizeInSettings = _currentItemExtent * Player.inst.currentIndex - Get.height * 0.3;
if (pixels > sizeInSettings) {
arrowIcon.value = Broken.arrow_up_1;
}
Expand Down
4 changes: 4 additions & 0 deletions lib/controller/player_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ class Player {

// ------- video -------

Future<void> tryGenerateWaveform(YoutubeID? video) async {
return _audioHandler.tryGenerateWaveform(video);
}

Future<void> setVideo({required String source, String cacheKey = '', bool loopingAnimation = false, required bool isFile}) async {
await _audioHandler.setVideoSource(source: source, cacheKey: cacheKey, loopingAnimation: loopingAnimation, isFile: isFile);
}
Expand Down
5 changes: 5 additions & 0 deletions lib/controller/settings_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ class SettingsController with SettingsFileWriter {
final RxInt imagesMaxCacheInMB = (8 * 32).obs; // 256 MB
final RxInt ytMiniplayerDimAfterSeconds = 15.obs;
final RxDouble ytMiniplayerDimOpacity = 0.5.obs;
final RxBool youtubeStyleMiniplayer = true.obs;
final RxBool hideStatusBarInExpandedMiniplayer = false.obs;
final RxBool displayFavouriteButtonInNotification = false.obs;
final RxBool enableSearchCleanup = true.obs;
Expand Down Expand Up @@ -397,6 +398,7 @@ class SettingsController with SettingsFileWriter {
imagesMaxCacheInMB.value = json['imagesMaxCacheInMB'] ?? imagesMaxCacheInMB.value;
ytMiniplayerDimAfterSeconds.value = json['ytMiniplayerDimAfterSeconds'] ?? ytMiniplayerDimAfterSeconds.value;
ytMiniplayerDimOpacity.value = json['ytMiniplayerDimOpacity'] ?? ytMiniplayerDimOpacity.value;
youtubeStyleMiniplayer.value = json['youtubeStyleMiniplayer'] ?? youtubeStyleMiniplayer.value;
hideStatusBarInExpandedMiniplayer.value = json['hideStatusBarInExpandedMiniplayer'] ?? hideStatusBarInExpandedMiniplayer.value;
displayFavouriteButtonInNotification.value = json['displayFavouriteButtonInNotification'] ?? displayFavouriteButtonInNotification.value;
enableSearchCleanup.value = json['enableSearchCleanup'] ?? enableSearchCleanup.value;
Expand Down Expand Up @@ -580,6 +582,7 @@ class SettingsController with SettingsFileWriter {
'imagesMaxCacheInMB': imagesMaxCacheInMB.value,
'ytMiniplayerDimAfterSeconds': ytMiniplayerDimAfterSeconds.value,
'ytMiniplayerDimOpacity': ytMiniplayerDimOpacity.value,
'youtubeStyleMiniplayer': youtubeStyleMiniplayer.value,
'hideStatusBarInExpandedMiniplayer': hideStatusBarInExpandedMiniplayer.value,
'displayFavouriteButtonInNotification': displayFavouriteButtonInNotification.value,
'enableSearchCleanup': enableSearchCleanup.value,
Expand Down Expand Up @@ -747,6 +750,7 @@ class SettingsController with SettingsFileWriter {
int? imagesMaxCacheInMB,
int? ytMiniplayerDimAfterSeconds,
double? ytMiniplayerDimOpacity,
bool? youtubeStyleMiniplayer,
bool? hideStatusBarInExpandedMiniplayer,
bool? displayFavouriteButtonInNotification,
bool? enableSearchCleanup,
Expand Down Expand Up @@ -961,6 +965,7 @@ class SettingsController with SettingsFileWriter {
if (imagesMaxCacheInMB != null) this.imagesMaxCacheInMB.value = imagesMaxCacheInMB;
if (ytMiniplayerDimAfterSeconds != null) this.ytMiniplayerDimAfterSeconds.value = ytMiniplayerDimAfterSeconds;
if (ytMiniplayerDimOpacity != null) this.ytMiniplayerDimOpacity.value = ytMiniplayerDimOpacity;
if (youtubeStyleMiniplayer != null) this.youtubeStyleMiniplayer.value = youtubeStyleMiniplayer;

if (hideStatusBarInExpandedMiniplayer != null) this.hideStatusBarInExpandedMiniplayer.value = hideStatusBarInExpandedMiniplayer;

Expand Down
18 changes: 10 additions & 8 deletions lib/controller/waveform_controller.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import 'package:get/get.dart';
import 'package:waveform_extractor/waveform_extractor.dart';

import 'package:namida/class/track.dart';
import 'package:namida/controller/player_controller.dart';
import 'package:namida/controller/settings_controller.dart';
import 'package:namida/core/extensions.dart';

Expand All @@ -17,29 +15,33 @@ class WaveformController {

final RxMap<int, double> _currentScaleMap = <int, double>{}.obs;

/// Extracts waveform data from a given track, or immediately read from .wave file if exists, then assigns wavedata to [_currentWaveform].
Future<void> generateWaveform(Track track) async {
bool get isDummy => _currentWaveform.isEmpty;

void resetWaveform() {
_currentWaveform = [];
currentWaveformUI.value = List.filled(settings.waveformTotalBars.value, 2.0);
}

/// Extracts waveform data from a given track, or immediately read from .wave file if exists, then assigns wavedata to [_currentWaveform].
Future<void> generateWaveform({required String path, required Duration duration, required bool Function(String path) stillPlaying}) async {
final samplePerSecond = _waveformExtractor.getSampleRateFromDuration(
audioDuration: Duration(seconds: track.duration),
audioDuration: duration,
maxSampleRate: 400,
scaleFactor: 0.4,
);

List<int> waveformData = [];
await Future.wait([
_waveformExtractor.extractWaveformDataOnly(track.path, samplePerSecond: samplePerSecond).then((value) {
_waveformExtractor.extractWaveformDataOnly(path, samplePerSecond: samplePerSecond).then((value) {
waveformData = value;
}),
Future.delayed(const Duration(milliseconds: 800)),
]);

if (track == Player.inst.nowPlayingTrack) {
if (stillPlaying(path)) {
// ----- Updating [_currentWaveform]
const maxWaveformCount = 2000;
final numberOfScales = (track.duration * 1000) ~/ 50;
final numberOfScales = duration.inMilliseconds ~/ 50;
final downscaledLists = await _downscaledWaveformLists.thready((
targetSizes: [maxWaveformCount, numberOfScales],
original: waveformData,
Expand Down
4 changes: 3 additions & 1 deletion lib/core/dimensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ class Dimensions {
/// + active miniplayer padding
double get globalBottomPaddingEffective {
return (Player.inst.currentQueueYoutube.isNotEmpty
? kYoutubeMiniplayerHeight
? settings.youtubeStyleMiniplayer.value
? kYoutubeMiniplayerHeight
: _kMiniplayerBottomPadding
: Player.inst.currentQueue.isNotEmpty
? _kMiniplayerBottomPadding
: 0.0) +
Expand Down
Loading

0 comments on commit 15ec081

Please sign in to comment.