Skip to content

Commit

Permalink
chore: refactor internal playback
Browse files Browse the repository at this point in the history
to preprare for live videos (still not ready)
  • Loading branch information
MSOB7YY committed Aug 15, 2024
1 parent 640bb62 commit fcee0ee
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 82 deletions.
230 changes: 149 additions & 81 deletions lib/base/audio_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import 'package:namida/core/constants.dart';
import 'package:namida/core/enums.dart';
import 'package:namida/core/extensions.dart';
import 'package:namida/core/namida_converter_ext.dart';
import 'package:namida/core/translations/language.dart';
import 'package:namida/core/utils.dart';
import 'package:namida/main.dart';
import 'package:namida/ui/dialogs/common_dialogs.dart';
Expand Down Expand Up @@ -625,20 +626,21 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
if (audioStream != null) {
final url = audioStream.buildUrl();
if (url != null) {
activeAudioSource = LockCachingAudioSource(
activeAudioSource = _buildLockCachingAudioSource(
url,
cacheFile: File(audioStream.cachePath(videoId)),
tag: mediaItem,
onCacheDone: (cacheFile) async => await _onAudioCacheDone(videoId, cacheFile),
stream: audioStream,
videoId: videoId,
streamsResult: mainStreams ?? YoutubeInfoController.current.currentYTStreams.value,
);
}
}
}
final videoOptions = VideoSourceOptions(
source: LockCachingVideoSource(
source: _buildLockCachingVideoSource(
url,
cacheFile: File(stream.cachePath(videoId)),
onCacheDone: (cacheFile) async => await _onVideoCacheDone(videoId, cacheFile),
stream: stream,
videoId: videoId,
streamsResult: mainStreams ?? YoutubeInfoController.current.currentYTStreams.value,
),
loop: false,
videoOnly: false,
Expand Down Expand Up @@ -725,11 +727,11 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
final url = stream.buildUrl();
if (url == null) throw Exception('null url');
await setSource(
LockCachingAudioSource(
_buildLockCachingAudioSource(
url,
cacheFile: File(stream.cachePath(videoId)),
tag: mediaItem,
onCacheDone: (cacheFile) async => await _onAudioCacheDone(videoId, cacheFile),
stream: stream,
videoId: videoId,
streamsResult: mainStreams ?? YoutubeInfoController.current.currentYTStreams.value,
),
initialPosition: positionToRestore,
item: currentItem.value,
Expand Down Expand Up @@ -815,7 +817,7 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
sizeInBytes: prevVideoStream.sizeInBytes,
frameratePrecise: prevVideoStream.fps.toDouble(),
creationTimeMS: (prevVideoInfo?.publishedAt.date ?? prevVideoInfo?.publishDate.date)?.millisecondsSinceEpoch ?? 0,
durationMS: prevVideoStream.duration.inMilliseconds,
durationMS: prevVideoStream.duration?.inMilliseconds ?? 0,
bitrate: prevVideoStream.bitrate,
),
);
Expand Down Expand Up @@ -1115,7 +1117,8 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
snackyy(message: 'Error getting streams', top: false, isError: true);
return null;
});
if (streamsResult != null) {
if (streamsResult != null && (streamsResult.audioStreams.isNotEmpty || streamsResult.mixedStreams.isNotEmpty)) {
// only when video has actual streams. otherwise its deleted/privated/etc
if (markedAsWatched != null) {
// -- older request was initiated, wait to see the value.
markedAsWatched.future.then(
Expand Down Expand Up @@ -1144,106 +1147,147 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
fetchFullVideoPage();

if (streamsResult == null) {
if (!okaySetFromCache()) snackyy(message: 'Failed to fetch streams', top: false, isError: true);
if (!okaySetFromCache()) snackyy(title: lang.ERROR, message: 'Failed to fetch streams', top: false, isError: true);
return;
}

final audiostreams = streamsResult.audioStreams;
final videoStreams = streamsResult.videoStreams;
final mixedStreams = streamsResult.mixedStreams;

bool useMixedStream = false;

if (audiostreams.isEmpty) {
if (!okaySetFromCache()) snackyy(message: 'Empty audio streams', top: false, isError: true);
return;
if (mixedStreams.isEmpty) {
// -- live videos has only mixedStreams
if (!okaySetFromCache()) {
final playabilty = streamsResult.playability;
final extraReasons = [playabilty.reason, ...?playabilty.messages].whereType<String>();
final extraReasonsText = extraReasons.isEmpty ? '' : ' | ${extraReasons.join(' | ')}';
snackyy(title: lang.ERROR, message: 'Empty audio streams. playabilty: `${playabilty.status.name}`$extraReasonsText', top: false, isError: true);
if (willPlayWhenReady) skipItem();
}
return;
} else {
useMixedStream = true;
}
}

// -----------------------
AudioVideoSource? finalAudioSource;
AudioVideoSource? finalVideoSource;

final cachedAudioSet = playedFromCacheDetails.audio;
final cachedVideoSet = playedFromCacheDetails.video;

_isCurrentAudioFromCache = cachedAudioSet != null;

final prefferedAudioStream = YoutubeController.inst.getPreferredAudioStream(audiostreams);
bool isAudioStreamRequiredBetterThanCachedSet = cachedAudioSet == null
? true
: prefferedAudioStream == null
? false
: _allowSwitchingVideoStreamIfCachedPlaying
? prefferedAudioStream.bitrate > prefferedAudioStream.bitrate
: false;
if (isAudioStreamRequiredBetterThanCachedSet) {
currentAudioStream.value = prefferedAudioStream;
if (prefferedAudioStream != null) {
final audioUri = prefferedAudioStream.buildUrl();
if (audioUri != null) {
finalAudioSource = LockCachingAudioSource(
audioUri,
cacheFile: File(prefferedAudioStream.cachePath(item.id)),
onCacheDone: (cacheFile) async => await _onAudioCacheDone(item.id, cacheFile),
);
VideoSourceOptions? videoSourceOptions;

if (useMixedStream) {
AudioVideoSource? finalMixedSource;
final prefferedMixedStream = YoutubeController.inst.getPreferredStreamQuality(mixedStreams, preferIncludeWebm: false);

currentVideoStream.value = prefferedMixedStream;
if (prefferedMixedStream != null) {
final mixedUri = prefferedMixedStream.buildUrl();
if (mixedUri != null) {
finalMixedSource = HlsSource(mixedUri);
finalAudioSource = AudioVideoSource.file('');
}
}
}

if (!_isAudioOnlyPlayback && videoStreams.isNotEmpty) {
if (cachedVideoSet != null ? _allowSwitchingVideoStreamIfCachedPlaying : true) {
final prefferedVideoStream = YoutubeController.inst.getPreferredStreamQuality(videoStreams, preferIncludeWebm: false);
bool isVideoStreamRequiredBetterThanCachedSet = cachedVideoSet == null
? true
: prefferedVideoStream == null
? false
: _allowSwitchingVideoStreamIfCachedPlaying
? prefferedVideoStream.width > cachedVideoSet.width
: false;

if (isVideoStreamRequiredBetterThanCachedSet) {
currentVideoStream.value = prefferedVideoStream;
if (prefferedVideoStream != null) {
final videoUri = prefferedVideoStream.buildUrl();
if (videoUri != null) {
finalVideoSource = LockCachingVideoSource(
videoUri,
cacheFile: File(prefferedVideoStream.cachePath(item.id)),
onCacheDone: (cacheFile) async => await _onVideoCacheDone(item.id, cacheFile),
);
}
if (finalMixedSource != null) {
videoSourceOptions = VideoSourceOptions(
source: finalMixedSource,
loop: false,
videoOnly: true,
);
} else {
if (!okaySetFromCache()) snackyy(title: lang.ERROR, message: 'Failed to get mixed source', top: false, isError: true);
return;
}
} else {
AudioVideoSource? finalVideoSource;

final cachedAudioSet = playedFromCacheDetails.audio;
final cachedVideoSet = playedFromCacheDetails.video;

_isCurrentAudioFromCache = cachedAudioSet != null;

final prefferedAudioStream = YoutubeController.inst.getPreferredAudioStream(audiostreams);
bool isAudioStreamRequiredBetterThanCachedSet = cachedAudioSet == null
? true
: prefferedAudioStream == null
? false
: _allowSwitchingVideoStreamIfCachedPlaying
? prefferedAudioStream.bitrate > prefferedAudioStream.bitrate
: false;
if (isAudioStreamRequiredBetterThanCachedSet) {
currentAudioStream.value = prefferedAudioStream;
if (prefferedAudioStream != null) {
final audioUri = prefferedAudioStream.buildUrl();
if (audioUri != null) {
finalAudioSource = _buildLockCachingAudioSource(
audioUri,
stream: prefferedAudioStream,
videoId: item.id,
streamsResult: streamsResult,
);
}
}
}
}

await playerStoppingSeikoo?.future;
if (checkInterrupted()) return;
if (!YoutubeInfoController.video.jsPreparedIfRequired) await YoutubeInfoController.video.ensureJSPlayerInitialized();
if (checkInterrupted()) return;

if (finalAudioSource != null || finalVideoSource != null) {
heyIhandledPlaying = false;
if (!_isAudioOnlyPlayback && videoStreams.isNotEmpty) {
if (cachedVideoSet != null ? _allowSwitchingVideoStreamIfCachedPlaying : true) {
final prefferedVideoStream = YoutubeController.inst.getPreferredStreamQuality(videoStreams, preferIncludeWebm: false);
bool isVideoStreamRequiredBetterThanCachedSet = cachedVideoSet == null
? true
: prefferedVideoStream == null
? false
: _allowSwitchingVideoStreamIfCachedPlaying
? prefferedVideoStream.width > cachedVideoSet.width
: false;

if (isVideoStreamRequiredBetterThanCachedSet) {
currentVideoStream.value = prefferedVideoStream;
if (prefferedVideoStream != null) {
final videoUri = prefferedVideoStream.buildUrl();
if (videoUri != null) {
finalVideoSource = _buildLockCachingVideoSource(
videoUri,
stream: prefferedVideoStream,
videoId: item.id,
streamsResult: streamsResult,
);
}
}
}
}
if (finalAudioSource != null || finalVideoSource != null) {
heyIhandledPlaying = false;

finalAudioSource ??= cachedAudioSet != null ? AudioVideoSource.file(cachedAudioSet.file.path) : null;
finalVideoSource ??= cachedVideoSet != null && !_isAudioOnlyPlayback ? AudioVideoSource.file(cachedVideoSet.path) : null;
finalAudioSource ??= cachedAudioSet != null ? AudioVideoSource.file(cachedAudioSet.file.path) : null;
finalVideoSource ??= cachedVideoSet != null && !_isAudioOnlyPlayback ? AudioVideoSource.file(cachedVideoSet.path) : null;

final videoOptions = finalVideoSource == null
? null
: VideoSourceOptions(
if (finalVideoSource != null) {
videoSourceOptions = VideoSourceOptions(
source: finalVideoSource,
loop: false,
videoOnly: false,
);
}
}
}

if (finalAudioSource == null) {
if (!okaySetFromCache()) snackyy(message: 'Failed to get audio source', top: false, isError: true);
if (!okaySetFromCache()) snackyy(title: lang.ERROR, message: 'Failed to get audio source', top: false, isError: true);
return;
}

await playerStoppingSeikoo?.future;
if (checkInterrupted()) return;
if (!YoutubeInfoController.video.jsPreparedIfRequired) await YoutubeInfoController.video.ensureJSPlayerInitialized();
if (checkInterrupted()) return;

duration = await setSource(
finalAudioSource,
item: pi,
initialPosition: positionToRestore,
startPlaying: startPlaying,
videoOptions: videoOptions,
videoOptions: videoSourceOptions,
keepOldVideoSource: false,
isVideoFile: false,
);
Expand All @@ -1254,7 +1298,7 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
if (checkInterrupted()) return;
void showSnackError(String nextAction) {
if (item == currentItem.value) {
snackyy(message: 'Error playing video, $nextAction: $e', top: false, isError: true);
snackyy(title: lang.ERROR, message: 'Error playing video, $nextAction: $e', top: false, isError: true);
}
}

Expand Down Expand Up @@ -1797,6 +1841,30 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
fastForward: MediaControl.fastForward,
rewind: MediaControl.rewind,
);

// -- builders

AudioVideoSource _buildLockCachingAudioSource(Uri uri, {required AudioStream stream, required String videoId, required VideoStreamsResult? streamsResult}) {
final isLive = streamsResult != null && streamsResult.audioStreams.isEmpty && streamsResult.mixedStreams.isNotEmpty;
return isLive
? HlsSource(uri)
: LockCachingAudioSource(
uri,
cacheFile: File(stream.cachePath(videoId)),
onCacheDone: (cacheFile) async => await _onAudioCacheDone(videoId, cacheFile),
);
}

AudioVideoSource _buildLockCachingVideoSource(Uri uri, {required VideoStream stream, required String videoId, required VideoStreamsResult? streamsResult}) {
final isLive = streamsResult != null && streamsResult.audioStreams.isEmpty && streamsResult.mixedStreams.isNotEmpty;
return isLive
? HlsSource(uri)
: LockCachingVideoSource(
uri,
cacheFile: File(stream.cachePath(videoId)),
onCacheDone: (cacheFile) async => await _onVideoCacheDone(videoId, cacheFile),
);
}
}

// ----------------------- Extensions --------------------------
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: namida
description: A Beautiful and Feature-rich Music Player, With YouTube & Video Support Built in Flutter
publish_to: "none"
version: 3.9.0-beta+240815227
version: 3.9.1-beta+240815231

environment:
sdk: ">=3.4.0 <4.0.0"
Expand Down

0 comments on commit fcee0ee

Please sign in to comment.