Revert to file-based asset caching, avoiding duplicate cache entries.

This commit is contained in:
Ryan Heise 2021-01-10 11:58:45 +11:00
parent 2779ae71b4
commit a74834af37
1 changed files with 49 additions and 60 deletions

View File

@ -7,6 +7,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:just_audio_platform_interface/just_audio_platform_interface.dart'; import 'package:just_audio_platform_interface/just_audio_platform_interface.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -196,6 +198,24 @@ class AudioPlayer {
}); });
}); });
} }
_removeOldAssetCacheDir();
}
/// Old versions of just_audio used an asset caching system that created a
/// separate cache file per asset per player instance, and was highly
/// dependent on the app calling [dispose] to clean up afterwards. If the app
/// is upgrading from an old version of just_audio, this will delete the old
/// cache directory.
Future<void> _removeOldAssetCacheDir() async {
final oldAssetCacheDir = Directory(
p.join((await getTemporaryDirectory()).path, 'just_audio_asset_cache'));
if (oldAssetCacheDir.existsSync()) {
try {
oldAssetCacheDir.deleteSync(recursive: true);
} catch (e) {
print("Failed to delete old asset cache dir: $e");
}
}
} }
/// The latest [PlaybackEvent]. /// The latest [PlaybackEvent].
@ -1284,8 +1304,6 @@ class SequenceState {
} }
/// A local proxy HTTP server for making remote GET requests with headers. /// A local proxy HTTP server for making remote GET requests with headers.
///
/// TODO: Recursively attach headers to items in playlists like m3u8.
class _ProxyHttpServer { class _ProxyHttpServer {
HttpServer _server; HttpServer _server;
@ -1302,11 +1320,7 @@ class _ProxyHttpServer {
final uri = source.uri; final uri = source.uri;
final headers = source.headers?.cast<String, String>(); final headers = source.headers?.cast<String, String>();
final path = _requestKey(uri); final path = _requestKey(uri);
if (uri.scheme == 'asset') {
_handlerMap[path] = _proxyHandlerForAsset(uri);
} else {
_handlerMap[path] = _proxyHandlerForUri(uri, headers); _handlerMap[path] = _proxyHandlerForUri(uri, headers);
}
return uri.replace( return uri.replace(
scheme: 'http', scheme: 'http',
host: InternetAddress.loopbackIPv4.address, host: InternetAddress.loopbackIPv4.address,
@ -1478,7 +1492,10 @@ abstract class UriAudioSource extends IndexedAudioSource {
@override @override
Future<void> _setup(AudioPlayer player) async { Future<void> _setup(AudioPlayer player) async {
await super._setup(player); await super._setup(player);
if (uri.scheme == 'asset' || headers != null) { if (uri.scheme == 'asset') {
_overrideUri = Uri.file(
(await _loadAsset(uri.path.replaceFirst(RegExp(r'^/'), ''))).path);
} else if (headers != null) {
_overrideUri = player._proxy.addUriAudioSource(this); _overrideUri = player._proxy.addUriAudioSource(this);
} }
} }
@ -1491,8 +1508,29 @@ abstract class UriAudioSource extends IndexedAudioSource {
super._dispose(); super._dispose();
} }
Future<File> _loadAsset(String assetPath) async {
final file = await _getCacheFile(assetPath);
this._cacheFile = file;
// Not technically inter-isolate-safe, although low risk. Could consider
// locking the file or creating a separate lock file.
if (!file.existsSync()) {
file.createSync(recursive: true);
await file.writeAsBytes(
(await rootBundle.load(assetPath)).buffer.asUint8List());
}
return file;
}
/// Get file for caching asset media with proper extension
Future<File> _getCacheFile(final String assetPath) async => File(p.joinAll([
(await getTemporaryDirectory()).path,
'just_audio_cache',
'assets',
...Uri.parse(assetPath).pathSegments,
]));
@override @override
bool get _requiresProxy => headers != null || uri.scheme == 'asset'; bool get _requiresProxy => headers != null;
} }
/// An [AudioSource] representing a regular media file such as an MP3 or M4A /// An [AudioSource] representing a regular media file such as an MP3 or M4A
@ -1869,60 +1907,9 @@ abstract class StreamAudioSource extends IndexedAudioSource {
id: _id, uri: _uri.toString(), headers: null); id: _id, uri: _uri.toString(), headers: null);
} }
/// An asset cache that holds loaded assets in memory for 10 seconds.
class _AssetCache {
static final _assets = <String, Future<ByteData>>{};
static final _timers = <String, Timer>{};
static Future<ByteData> load(String path) async {
var asset = _assets[path];
if (asset == null) {
_assets[path] = asset = rootBundle.load(path);
} else {
_timers[path].cancel();
}
_timers[path] = Timer(Duration(seconds: 10), () {
_assets.remove(path);
_timers.remove(path);
});
return asset;
}
}
/// The type of functions that can handle HTTP requests sent to the proxy. /// The type of functions that can handle HTTP requests sent to the proxy.
typedef void _ProxyHandler(HttpRequest request); typedef void _ProxyHandler(HttpRequest request);
/// A proxy handler for serving assets.
_ProxyHandler _proxyHandlerForAsset(Uri assetUri) {
Future<void> handler(HttpRequest request) async {
final assetPath = assetUri.path.replaceFirst(RegExp(r'^/'), '');
// This would be better if Flutter provided a stream-based API to load
// assets.
final byteData = await _AssetCache.load(assetPath);
final range = _HttpRange.parse(
request.headers[HttpHeaders.rangeHeader], byteData.lengthInBytes);
request.response.headers.clear();
request.response.headers.set(HttpHeaders.acceptRangesHeader, 'bytes');
request.response.statusCode = range == null ? 200 : 206;
final length = range?.length ?? byteData.lengthInBytes;
request.response.contentLength = length;
if (range != null) {
request.response.headers
.set(HttpHeaders.contentRangeHeader, range.contentRangeHeader);
}
// Write response
final bytes = byteData.buffer.asUint8List(range?.start ?? 0, length);
request.response.add(bytes);
await request.response.flush();
await request.response.close();
}
return handler;
}
/// A proxy handler for serving audio from a [StreamAudioSource]. /// A proxy handler for serving audio from a [StreamAudioSource].
_ProxyHandler _proxyHandlerForSource(StreamAudioSource source) { _ProxyHandler _proxyHandlerForSource(StreamAudioSource source) {
Future<void> handler(HttpRequest request) async { Future<void> handler(HttpRequest request) async {
@ -1948,6 +1935,8 @@ _ProxyHandler _proxyHandlerForSource(StreamAudioSource source) {
} }
/// A proxy handler for serving audio from a URI with optional headers. /// A proxy handler for serving audio from a URI with optional headers.
///
/// TODO: Recursively attach headers to items in playlists like m3u8.
_ProxyHandler _proxyHandlerForUri(Uri uri, Map headers) { _ProxyHandler _proxyHandlerForUri(Uri uri, Map headers) {
Future<void> handler(HttpRequest request) async { Future<void> handler(HttpRequest request) async {
final originRequest = await HttpClient().getUrl(uri); final originRequest = await HttpClient().getUrl(uri);