spotube/lib/provider/server/server.dart
Kingkor Roy Tirtho 22a49e56a2
refactor: use tcp server based track matcher (#1386)
* refactor: remove SourcedTrack based audio player and utilize mediakit playback system

* feat: implement local (loopback) server to resolve stream source and leverage the media_kit playback API

* feat: add source change support and re-add prefetching tracks

* fix: assign lastId when track fetch completes regardless of error

* chore: remove print statements

* fix: remote queue not working

* fix: increase mpv network timeout to reduce auto-skipping

* fix: do not pre-fetch local tracks

* fix(proxy-playlist): reset collections on load

* chore: fix lint warnings

* fix(mobile): player overlay should not be visible when the player is not playing

* chore: fix typo in turkish translation

* cd: checkout PR branch

* cd: upgrade flutter version

* chore: fix lint errors
2024-04-11 17:56:41 +06:00

120 lines
3.7 KiB
Dart

import 'dart:io';
import 'dart:math';
import 'package:catcher_2/catcher_2.dart';
import 'package:dio/dio.dart' hide Response;
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logger/logger.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/server/active_sourced_track.dart';
import 'package:spotube/provider/server/sourced_track.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
class PlaybackServer {
final Ref ref;
UserPreferences get userPreferences => ref.read(userPreferencesProvider);
ProxyPlaylist get playlist => ref.read(ProxyPlaylistNotifier.provider);
final Logger logger;
final Dio dio;
final Router router;
static final port = Random().nextInt(17000) + 1500;
PlaybackServer(this.ref)
: logger = getLogger('PlaybackServer'),
dio = Dio(),
router = Router() {
router.get('/stream/<trackId>', getStreamTrackId);
const pipeline = Pipeline();
if (kDebugMode) {
pipeline.addMiddleware(logRequests());
}
serve(pipeline.addHandler(router.call), InternetAddress.loopbackIPv4, port)
.then((server) {
logger
.t('Playback server at http://${server.address.host}:${server.port}');
ref.onDispose(() {
dio.close(force: true);
server.close();
});
});
}
/// @get('/stream/<trackId>')
Future<Response> getStreamTrackId(Request request, String trackId) async {
try {
final track =
playlist.tracks.firstWhere((element) => element.id == trackId);
final activeSourcedTrack = ref.read(activeSourcedTrackProvider);
final sourcedTrack = activeSourcedTrack?.id == track.id
? activeSourcedTrack
: await ref.read(sourcedTrackProvider(track).future);
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
final res = await dio.get(
sourcedTrack!.url,
options: Options(
headers: {
...request.headers,
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"host": Uri.parse(sourcedTrack.url).host,
"Cache-Control": "max-age=0",
"Connection": "keep-alive",
},
responseType: ResponseType.stream,
validateStatus: (status) => status! < 500,
),
);
final audioStream =
(res.data?.stream as Stream<Uint8List>?)?.asBroadcastStream();
// if (res.statusCode! > 300) {
// debugPrint(
// "[[Request]]\n"
// "URI: ${res.requestOptions.uri}\n"
// "Status: ${res.statusCode}\n"
// "Request Headers: ${res.requestOptions.headers}\n"
// "Response Body: ${res.data}\n"
// "Response Headers: ${res.headers.map}",
// );
// }
audioStream!.listen(
(event) {},
cancelOnError: true,
);
return Response(
res.statusCode!,
body: audioStream,
context: {
"shelf.io.buffer_output": false,
},
headers: res.headers.map,
);
} catch (e, stack) {
Catcher2.reportCheckedError(e, stack);
return Response.internalServerError();
}
}
}
final playbackServerProvider = Provider<PlaybackServer>((ref) {
return PlaybackServer(ref);
});