Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/features/addons/dash_assembler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class DashAssembler {
/// Refuse absurd manifests instead of downloading the internet.
static const int maxSegments = 512;

/// Segment fetch parallelism.
static const int parallelDownloads = 12;
/// Segment fetch parallelism (bounded to 6 to prevent memory spikes).
static const int parallelDownloads = 6;

/// Assembled-file cache caps (hi-res tracks are ~50–100MB each).
static const int maxCachedFiles = 16;
Expand Down Expand Up @@ -235,4 +235,4 @@ class DashAssembler {
}
} catch (_) {}
}
}
}
29 changes: 27 additions & 2 deletions lib/features/addons/local_media_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,21 @@ class LocalMediaServer {

final Dio _dio;
HttpServer? _server;
String? _authToken;
final Map<String, _Session> _sessions = {};

static String _generateAuthToken() {
final rand = Random.secure();
final bytes = List<int>.generate(16, (_) => rand.nextInt(256));
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}

Future<int> ensureStarted() async {
final running = _server;
if (running != null) return running.port;
final bound =
await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
_authToken ??= _generateAuthToken();
bound.listen(_route, onError: (_) {});
_server = bound;
return bound.port;
Expand Down Expand Up @@ -75,7 +83,8 @@ class LocalMediaServer {
() => _Session(_dio, safe, manifestXml),
);
session.kickoff();
return Uri.parse('http://127.0.0.1:$port/a/$safe.m4a');
final token = _authToken;
return Uri.parse('http://127.0.0.1:$port/a/$safe.m4a?token=$token');
} catch (_) {
return null;
}
Expand All @@ -96,11 +105,22 @@ class LocalMediaServer {

Future<void> _route(HttpRequest req) async {
try {
// 1. Enforce GET method
if (req.method != 'GET') {
req.response.statusCode = HttpStatus.methodNotAllowed;
await req.response.close();
return;
}

// 2. Validate ephemeral authentication token
final token = req.uri.queryParameters['token'];
if (_authToken == null || token != _authToken) {
req.response.statusCode = HttpStatus.forbidden;
await req.response.close();
return;
}

// 3. Validate path structure
final segs = req.uri.pathSegments;
if (segs.length != 2 ||
segs[0] != 'a' ||
Expand Down Expand Up @@ -233,7 +253,12 @@ class _Session {
onSegment: (i, bytes) => slots[i] = bytes,
onBatch: (base, end) async {
for (var i = base; i < end; i++) {
sink.add(slots[i]!);
final chunk = slots[i];
if (chunk != null) {
sink.add(chunk);
// Null out immediately after writing so GC can reclaim the memory
slots[i] = null;
}
}
await sink.flush();
},
Expand Down
78 changes: 64 additions & 14 deletions lib/features/audio_output/wasapi_engine.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:developer' as developer;
import 'dart:io';

Expand All @@ -24,75 +25,124 @@ class WasapiEngine {
return event.map((k, v) => MapEntry(k.toString(), v));
}
return <String, dynamic>{};
}).handleError((error) {
wasapiLog('deviceEvents stream error: $error');
});
}

Future<List<DacDevice>> enumerateDevices() async {
if (!isSupported) return const [];
try {
final raw = await _channel.invokeMethod<List<dynamic>>('enumerateDevices');
final raw = await _channel
.invokeMethod<List<dynamic>>('enumerateDevices')
.timeout(const Duration(seconds: 4));
if (raw == null) return const [];
return raw
.whereType<Map>()
.map(DacDevice.fromJson)
.toList();
return raw.whereType<Map>().map(DacDevice.fromJson).toList();
} on TimeoutException {
wasapiLog('enumerateDevices timed out');
return const [];
} on PlatformException catch (e) {
wasapiLog('enumerateDevices failed: ${e.message}');
return const [];
} catch (e) {
wasapiLog('Unexpected error in enumerateDevices: $e');
return const [];
}
}

Future<DacDevice?> probeDevice(String id) async {
if (!isSupported) return null;
if (!isSupported || id.trim().isEmpty) return null;
try {
final raw = await _channel.invokeMethod<Map<dynamic, dynamic>>(
'probeDevice',
{'id': id},
);
).timeout(const Duration(seconds: 5));

if (raw == null) return null;
return DacDevice.fromJson(raw);
} on TimeoutException {
wasapiLog('probeDevice timed out for device $id');
return null;
} on PlatformException catch (e) {
wasapiLog('probeDevice failed: ${e.message}');
wasapiLog('probeDevice failed for $id:${e.message}');
return null;
} catch (e) {
wasapiLog('Unexpected error in probeDevice for $id:$e');
return null;
}
}

/// Verifies whether the specified DAC device supports a given PCM format
/// before requesting exclusive mode from libmpv.
Future<bool> isFormatSupported({
required String id,
required int sampleRate,
required int channels,
required int bitDepth,
}) async {
if (!isSupported || id.trim().isEmpty) return false;
try {
final supported = await _channel.invokeMethod<bool>('isFormatSupported', {
'id': id,
'sampleRate': sampleRate,
'channels': channels,
'bitDepth': bitDepth,
}).timeout(const Duration(seconds: 3));

return supported ?? false;
} catch (e) {
wasapiLog('Format check error ($sampleRate Hz, $bitDepth-bit):$e');
return false;
}
}

Future<bool> setHardwareVolume(String id, double scalar) async {
if (!isSupported) return false;
if (!isSupported || id.trim().isEmpty) return false;
try {
await _channel.invokeMethod<void>('setHardwareVolume', {
'id': id,
'scalar': scalar.clamp(0.0, 1.0),
});
}).timeout(const Duration(seconds: 2));
return true;
} on PlatformException catch (e) {
wasapiLog('setHardwareVolume failed: ${e.message}');
return false;
} catch (_) {
return false;
}
}

Future<double?> getHardwareVolume(String id) async {
if (!isSupported) return null;
if (!isSupported || id.trim().isEmpty) return null;
try {
final v = await _channel.invokeMethod<num>('getHardwareVolume', {'id': id});
final v = await _channel.invokeMethod<num>('getHardwareVolume', {
'id': id,
}).timeout(const Duration(seconds: 2));
return v?.toDouble();
} on PlatformException catch (e) {
wasapiLog('getHardwareVolume failed: ${e.message}');
return null;
} catch (_) {
return null;
}
}

Future<String> defaultDeviceId() async {
if (!isSupported) return '';
try {
return await _channel.invokeMethod<String>('defaultDeviceId') ?? '';
return await _channel
.invokeMethod<String>('defaultDeviceId')
.timeout(const Duration(seconds: 2)) ??
'';
} on PlatformException catch (e) {
wasapiLog('defaultDeviceId failed: ${e.message}');
return '';
} catch (_) {
return '';
}
}
}

void wasapiLog(String message) {
developer.log(message, name: 'WASAPI');
}
}
33 changes: 18 additions & 15 deletions lib/features/innertube/signature_decipher.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:isolate';

import 'package:dio/dio.dart';
import 'package:meta/meta.dart';
Expand Down Expand Up @@ -154,7 +155,9 @@ class SignatureDecipher {
).timeout(const Duration(seconds: 20));
final js = res.data ?? '';
if (js.isEmpty) return null;
final script = _parse(jsUrl, js);

// Offload regex parsing to an isolate worker thread to prevent main UI thread stutters
final script = await Isolate.run(() => _parse(jsUrl, js));
if (script == null) return null;
if (_byJsUrl.length > 8) {
_byJsUrl.clear();
Expand Down Expand Up @@ -260,7 +263,7 @@ class SignatureDecipher {
'\\.get\\("n"\\)\\)&&\\($_sc=($_mc)(?:\\[(\\d+)\\])?\\($_sc\\)'),
];

PlayerScript? _parse(String jsUrl, String js) {
static PlayerScript? _parse(String jsUrl, String js) {
// STS lives in the player script (NewPipe: signatureTimestamp[=:](\d+)).
final sts = RegExp(r'signatureTimestamp[=:](\d+)')
.firstMatch(js)
Expand Down Expand Up @@ -339,7 +342,7 @@ class SignatureDecipher {

/// Extract `NAME=function(...)` source with balanced braces
/// (lexer approach, mirroring NewPipe).
_FuncCode _extractFunction(String js, String name) {
static _FuncCode _extractFunction(String js, String name) {
final idx = js.indexOf('$name=function');
if (idx < 0) throw const FormatException('no func');
final openIdx = js.indexOf('{', idx);
Expand All @@ -362,7 +365,7 @@ class SignatureDecipher {

/// Strip NewPipe-style early-return guards, bind the array
/// variable + extra literals, and compile op statements.
_Prepared _prepare(String js, String body, List<String> params,
static _Prepared _prepare(String js, String body, List<String> params,
{String? extraLiteral}) {
final env = <String, int>{};
late final String arrayVar;
Expand Down Expand Up @@ -426,7 +429,7 @@ class SignatureDecipher {
return _Prepared(ops, env, arrayVar);
}

String? _findNFunc(String js) {
static String? _findNFunc(String js) {
for (final pattern in _nNamePatterns) {
final m = pattern.firstMatch(js);
if (m == null) continue;
Expand Down Expand Up @@ -458,7 +461,7 @@ class SignatureDecipher {

// -- helper objects + op classification --------------------------------------

DecipherOp _resolveOp(String js, String obj, String method,
static DecipherOp _resolveOp(String js, String obj, String method,
String? argSrc, Map<String, int> outerEnv) {
final helper = _helperBody(js, obj);
final member = _memberBody(helper, method);
Expand All @@ -473,7 +476,7 @@ class SignatureDecipher {
return _classify(member, env);
}

String _helperBody(String js, String obj) {
static String _helperBody(String js, String obj) {
// Helper objects are `var NAME={...};` (2+ char names).
final m = RegExp('(?:var\\s+)?${RegExp.escape(obj)}\\s*=\\s*\\{')
.firstMatch(js);
Expand All @@ -482,7 +485,7 @@ class SignatureDecipher {
return _balanced(js, start);
}

String _memberBody(String helper, String method) {
static String _memberBody(String helper, String method) {
var m = RegExp(RegExp.escape(method) +
r'\s*:\s*function\s*\(([^)]*)\)\s*\{')
.firstMatch(helper);
Expand All @@ -500,7 +503,7 @@ class SignatureDecipher {
throw FormatException('no member $method');
}

List<String> _memberParams(String helper, String method) {
static List<String> _memberParams(String helper, String method) {
final m = RegExp(RegExp.escape(method) +
r'\s*(?::\s*function)?\s*\(([^)]*)\)\s*\{')
.firstMatch(helper);
Expand All @@ -513,7 +516,7 @@ class SignatureDecipher {
.toList();
}

DecipherOp _classify(String body, Map<String, int> env) {
static DecipherOp _classify(String body, Map<String, int> env) {
final b = body.replaceAll(RegExp(r'\s+'), '');
if (b.contains('.reverse(')) return const DecipherOp(DecipherOpType.reverse);
if (b.contains('.splice(')) {
Expand Down Expand Up @@ -559,7 +562,7 @@ class SignatureDecipher {
throw const FormatException('unknown op');
}

DecipherOp _spliceOp(List<String> args, Map<String, int> env) {
static DecipherOp _spliceOp(List<String> args, Map<String, int> env) {
if (args.length >= 2) {
final start = _evalInt(args[0], env, 0) ?? 0;
final count = _evalInt(args[1], env, 0) ?? 0;
Expand All @@ -570,7 +573,7 @@ class SignatureDecipher {
return DecipherOp(DecipherOpType.splice, [start]);
}

List<String> _splitArgs(String src) {
static List<String> _splitArgs(String src) {
final out = <String>[];
var depth = 0;
final buf = StringBuffer();
Expand All @@ -589,7 +592,7 @@ class SignatureDecipher {
return out;
}

int? _evalInt(String expr, Map<String, int> env, int len) {
static int? _evalInt(String expr, Map<String, int> env, int len) {
final e = expr.trim();
final lit = int.tryParse(e);
if (lit != null) return lit;
Expand All @@ -605,7 +608,7 @@ class SignatureDecipher {
return null;
}

String _balanced(String src, int openIdx) {
static String _balanced(String src, int openIdx) {
var depth = 0;
var inStr = '';
var escaped = false;
Expand Down Expand Up @@ -712,4 +715,4 @@ class _Prepared {

extension _Let<T> on T {
R let<R>(R Function(T) fn) => fn(this);
}
}
Loading
Loading