feat: youtube on a separate dart isolate

This commit is contained in:
Kingkor Roy Tirtho 2025-01-28 20:14:03 +06:00
parent 4c3718467d
commit 8e34430d9f
4 changed files with 147 additions and 37 deletions

View File

@ -18,6 +18,7 @@ import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/server/active_sourced_track.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/isolates/yt_explode.dart';
import 'package:spotube/services/sourced_track/models/source_info.dart';
import 'package:spotube/services/sourced_track/models/video_info.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
@ -114,7 +115,8 @@ class SiblingTracksSheet extends HookConsumerWidget {
activeSourceInfo,
);
} else {
final resultsYt = await youtubeClient.search.search(searchTerm.trim());
final resultsYt =
await IsolatedYoutubeExplode.instance.search(searchTerm.trim());
final searchResults = await Future.wait(
resultsYt

View File

@ -11,6 +11,7 @@ import 'package:spotube/provider/audio_player/audio_player_streams.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/isolates/yt_explode.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/utils/platform.dart';
@ -40,6 +41,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
..where((tbl) => tbl.id.equals(0)))
.getSingle();
if (state.audioSource == AudioSource.youtube) {
await IsolatedYoutubeExplode.initialize();
}
final subscription = (db.select(db.preferencesTable)
..where((tbl) => tbl.id.equals(0)))
.watchSingle()
@ -207,6 +212,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
void setAudioSource(AudioSource type) {
setData(PreferencesTableCompanion(audioSource: Value(type)));
if (type != AudioSource.youtube && IsolatedYoutubeExplode.isInitialized) {
IsolatedYoutubeExplode.instance.dispose();
}
}
void setSystemTitleBar(bool isSystemTitleBar) {

View File

@ -0,0 +1,121 @@
import 'dart:async';
import 'dart:isolate';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
/// A Isolate wrapper for the YoutubeExplode class
/// It contains methods that are computationally expensive
class IsolatedYoutubeExplode {
final Isolate _isolate;
final SendPort _sendPort;
final ReceivePort _receivePort;
IsolatedYoutubeExplode._(
Isolate isolate,
ReceivePort receivePort,
SendPort sendPort,
) : _isolate = isolate,
_receivePort = receivePort,
_sendPort = sendPort;
static IsolatedYoutubeExplode? _instance;
static IsolatedYoutubeExplode get instance => _instance!;
static bool get isInitialized => _instance != null;
static Future<void> initialize() async {
if (_instance != null) {
return;
}
final completer = Completer<SendPort>();
final receivePort = ReceivePort();
/// Listen for the main isolate to set the main port
final subscription = receivePort.listen((message) {
if (message is SendPort) {
completer.complete(message);
}
});
final isolate = await Isolate.spawn(_isolateEntry, receivePort.sendPort);
_instance = IsolatedYoutubeExplode._(
isolate,
receivePort,
await completer.future,
);
if (completer.isCompleted) {
subscription.cancel();
}
}
static void _isolateEntry(SendPort mainSendPort) {
final receivePort = ReceivePort();
final youtubeExplode = YoutubeExplode();
/// Send the main port to the main isolate
mainSendPort.send(receivePort.sendPort);
receivePort.listen((message) async {
final SendPort replyPort = message[0];
final String methodName = message[1];
final List<dynamic> arguments = message[2];
// Run the requested method on YoutubeExplode
var result = switch (methodName) {
"search" => youtubeExplode.search
.search(arguments[0] as String, filter: TypeFilters.video)
.then((s) => s.toList()),
"video" => youtubeExplode.videos.get(arguments[0] as String),
"manifest" => youtubeExplode.videos.streamsClient.getManifest(
arguments[0] as String,
requireWatchPage: false,
ytClients: [
YoutubeApiClient.mediaConnect,
YoutubeApiClient.ios,
YoutubeApiClient.android,
YoutubeApiClient.mweb,
YoutubeApiClient.tv,
],
),
_ => throw ArgumentError('Invalid method name: $methodName'),
};
replyPort.send(await result);
});
}
Future<T> _runMethod<T>(String methodName, List<dynamic> args) {
final completer = Completer<T>();
final responsePort = ReceivePort();
responsePort.listen((message) {
completer.complete(message as T);
responsePort.close();
});
_sendPort.send([responsePort.sendPort, methodName, args]);
return completer.future;
}
Future<List<Video>> search(String query) async {
return _runMethod<List<Video>>("search", [query]);
}
Future<Video> video(String videoId) async {
return _runMethod<Video>("video", [videoId]);
}
Future<StreamManifest> manifest(String videoId) async {
return _runMethod<StreamManifest>("manifest", [videoId]);
}
void dispose() {
_receivePort.close();
_isolate.kill(priority: Isolate.immediate);
}
}

View File

@ -1,10 +1,10 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/services/isolates/yt_explode.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/song_link/song_link.dart';
import 'package:spotube/services/sourced_track/enums.dart';
@ -16,7 +16,6 @@ import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
final youtubeClient = YoutubeExplode();
final officialMusicRegex = RegExp(
r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)",
caseSensitive: false,
@ -48,6 +47,9 @@ class YoutubeSourcedTrack extends SourcedTrack {
required Track track,
required Ref ref,
}) async {
if (!IsolatedYoutubeExplode.isInitialized) {
await IsolatedYoutubeExplode.initialize();
}
final database = ref.read(databaseProvider);
final cachedSource = await (database.select(database.sourceMatchTable)
..where((s) => s.trackId.equals(track.id!))
@ -81,18 +83,10 @@ class YoutubeSourcedTrack extends SourcedTrack {
track: track,
);
}
final item = await youtubeClient.videos.get(cachedSource.sourceId);
final manifest = await youtubeClient.videos.streamsClient.getManifest(
cachedSource.sourceId,
requireWatchPage: false,
ytClients: [
YoutubeApiClient.mediaConnect,
YoutubeApiClient.ios,
YoutubeApiClient.android,
YoutubeApiClient.mweb,
YoutubeApiClient.tv,
],
);
final item =
await IsolatedYoutubeExplode.instance.video(cachedSource.sourceId);
final manifest =
await IsolatedYoutubeExplode.instance.manifest(cachedSource.sourceId);
return YoutubeSourcedTrack(
ref: ref,
siblings: [],
@ -144,17 +138,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
) async {
SourceMap? sourceMap;
if (index == 0) {
final manifest = await youtubeClient.videos.streamsClient.getManifest(
item.id,
requireWatchPage: false,
ytClients: [
YoutubeApiClient.mediaConnect,
YoutubeApiClient.ios,
YoutubeApiClient.android,
YoutubeApiClient.mweb,
YoutubeApiClient.tv,
],
);
final manifest = await IsolatedYoutubeExplode.instance.manifest(item.id);
sourceMap = toSourceMap(manifest);
}
@ -248,7 +232,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
await toSiblingType(
0,
YoutubeVideoInfo.fromVideo(
await youtubeClient.videos.get(ytLink!.url!),
await IsolatedYoutubeExplode.instance.video(ytLink!.url!),
),
)
];
@ -260,10 +244,8 @@ class YoutubeSourcedTrack extends SourcedTrack {
final query = SourcedTrack.getSearchTerm(track);
final searchResults = await youtubeClient.search.search(
"$query - Topic",
filter: TypeFilters.video,
);
final searchResults =
await IsolatedYoutubeExplode.instance.search("$query - Topic");
if (ServiceUtils.onlyContainsEnglish(query)) {
return await Future.wait(searchResults
@ -294,12 +276,8 @@ class YoutubeSourcedTrack extends SourcedTrack {
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
..insert(0, sourceInfo);
final manifest = await youtubeClient.videos.streamsClient
.getManifest(newSourceInfo.id)
.timeout(
const Duration(seconds: 5),
onTimeout: () => throw ClientException("Timeout"),
);
final manifest =
await IsolatedYoutubeExplode.instance.manifest(newSourceInfo.id);
final database = ref.read(databaseProvider);