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/audio_player/querying_track_info.dart';
import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/server/active_sourced_track.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.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/source_info.dart';
import 'package:spotube/services/sourced_track/models/video_info.dart'; import 'package:spotube/services/sourced_track/models/video_info.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
@ -114,7 +115,8 @@ class SiblingTracksSheet extends HookConsumerWidget {
activeSourceInfo, activeSourceInfo,
); );
} else { } else {
final resultsYt = await youtubeClient.search.search(searchTerm.trim()); final resultsYt =
await IsolatedYoutubeExplode.instance.search(searchTerm.trim());
final searchResults = await Future.wait( final searchResults = await Future.wait(
resultsYt 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/database/database.dart';
import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/services/audio_player/audio_player.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/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
@ -40,6 +41,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
..where((tbl) => tbl.id.equals(0))) ..where((tbl) => tbl.id.equals(0)))
.getSingle(); .getSingle();
if (state.audioSource == AudioSource.youtube) {
await IsolatedYoutubeExplode.initialize();
}
final subscription = (db.select(db.preferencesTable) final subscription = (db.select(db.preferencesTable)
..where((tbl) => tbl.id.equals(0))) ..where((tbl) => tbl.id.equals(0)))
.watchSingle() .watchSingle()
@ -207,6 +212,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
void setAudioSource(AudioSource type) { void setAudioSource(AudioSource type) {
setData(PreferencesTableCompanion(audioSource: Value(type))); setData(PreferencesTableCompanion(audioSource: Value(type)));
if (type != AudioSource.youtube && IsolatedYoutubeExplode.isInitialized) {
IsolatedYoutubeExplode.instance.dispose();
}
} }
void setSystemTitleBar(bool isSystemTitleBar) { 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:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/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/logger/logger.dart';
import 'package:spotube/services/song_link/song_link.dart'; import 'package:spotube/services/song_link/song_link.dart';
import 'package:spotube/services/sourced_track/enums.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:spotube/utils/service_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
final youtubeClient = YoutubeExplode();
final officialMusicRegex = RegExp( final officialMusicRegex = RegExp(
r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)", r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)",
caseSensitive: false, caseSensitive: false,
@ -48,6 +47,9 @@ class YoutubeSourcedTrack extends SourcedTrack {
required Track track, required Track track,
required Ref ref, required Ref ref,
}) async { }) async {
if (!IsolatedYoutubeExplode.isInitialized) {
await IsolatedYoutubeExplode.initialize();
}
final database = ref.read(databaseProvider); final database = ref.read(databaseProvider);
final cachedSource = await (database.select(database.sourceMatchTable) final cachedSource = await (database.select(database.sourceMatchTable)
..where((s) => s.trackId.equals(track.id!)) ..where((s) => s.trackId.equals(track.id!))
@ -81,18 +83,10 @@ class YoutubeSourcedTrack extends SourcedTrack {
track: track, track: track,
); );
} }
final item = await youtubeClient.videos.get(cachedSource.sourceId); final item =
final manifest = await youtubeClient.videos.streamsClient.getManifest( await IsolatedYoutubeExplode.instance.video(cachedSource.sourceId);
cachedSource.sourceId, final manifest =
requireWatchPage: false, await IsolatedYoutubeExplode.instance.manifest(cachedSource.sourceId);
ytClients: [
YoutubeApiClient.mediaConnect,
YoutubeApiClient.ios,
YoutubeApiClient.android,
YoutubeApiClient.mweb,
YoutubeApiClient.tv,
],
);
return YoutubeSourcedTrack( return YoutubeSourcedTrack(
ref: ref, ref: ref,
siblings: [], siblings: [],
@ -144,17 +138,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
) async { ) async {
SourceMap? sourceMap; SourceMap? sourceMap;
if (index == 0) { if (index == 0) {
final manifest = await youtubeClient.videos.streamsClient.getManifest( final manifest = await IsolatedYoutubeExplode.instance.manifest(item.id);
item.id,
requireWatchPage: false,
ytClients: [
YoutubeApiClient.mediaConnect,
YoutubeApiClient.ios,
YoutubeApiClient.android,
YoutubeApiClient.mweb,
YoutubeApiClient.tv,
],
);
sourceMap = toSourceMap(manifest); sourceMap = toSourceMap(manifest);
} }
@ -248,7 +232,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
await toSiblingType( await toSiblingType(
0, 0,
YoutubeVideoInfo.fromVideo( 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 query = SourcedTrack.getSearchTerm(track);
final searchResults = await youtubeClient.search.search( final searchResults =
"$query - Topic", await IsolatedYoutubeExplode.instance.search("$query - Topic");
filter: TypeFilters.video,
);
if (ServiceUtils.onlyContainsEnglish(query)) { if (ServiceUtils.onlyContainsEnglish(query)) {
return await Future.wait(searchResults return await Future.wait(searchResults
@ -294,12 +276,8 @@ class YoutubeSourcedTrack extends SourcedTrack {
final newSiblings = siblings.where((s) => s.id != sibling.id).toList() final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
..insert(0, sourceInfo); ..insert(0, sourceInfo);
final manifest = await youtubeClient.videos.streamsClient final manifest =
.getManifest(newSourceInfo.id) await IsolatedYoutubeExplode.instance.manifest(newSourceInfo.id);
.timeout(
const Duration(seconds: 5),
onTimeout: () => throw ClientException("Timeout"),
);
final database = ref.read(databaseProvider); final database = ref.read(databaseProvider);