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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## 0.3.0 (unreleased)

### New Features

* Added `HttpCacheManager.init(startServer: false)`, which starts the manager without its local cache server, for apps that read streams directly through `createStream` and `HttpCacheStream.request` instead of over HTTP. No socket is bound, so it works where binding is refused, such as a macOS App Sandbox without the `com.apple.security.network.server` entitlement. Downloading and caching work as usual. The default is unchanged (`startServer: true`).
* Added `HttpCacheManager.hasServer` and `HttpCacheStream.hasCacheUrl`.
* Added `CacheServerUnavailableException` (a `StateError`), thrown by `HttpCacheManager.getCacheUrl` and `HttpCacheStream.cacheUrl` when the manager has no local cache server. Without a server, `ensureActive()` does nothing and `port` is ignored.

### Fixes

* Fixed `HttpCacheManager.init` returning the previous, disposed manager after `dispose()` when the init completed without an asynchronous step (no server, with `cacheDir` or `config` given).

## 0.2.1

### New Features
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ Reference the file in `android/app/src/main/res/AndroidManifest.xml` by adding t

See the example project for a full example.

### Without the local server

If your app reads streams directly (through `createStream` and `HttpCacheStream.request`) rather than handing a URL to a player, you can skip the local server entirely. No socket is bound, so no cleartext or network-server configuration is needed, and it works in sandboxes that refuse to bind one (e.g. a macOS App Sandbox without the `com.apple.security.network.server` entitlement):

```dart
await HttpCacheManager.init(startServer: false);

final stream = HttpCacheManager.instance.createStream(sourceUrl);
final response = await stream.request(start: 0); // downloads and caches as it streams
await for (final chunk in response.stream) {
// ...
}
```

Without a server there is no cache URL: `getCacheUrl` and `HttpCacheStream.cacheUrl` throw a `CacheServerUnavailableException` (check `HttpCacheManager.hasServer` or `HttpCacheStream.hasCacheUrl`), and `ensureActive()` does nothing.


# Core Components

Expand Down
73 changes: 52 additions & 21 deletions lib/src/cache_manager/http_cache_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,50 @@ import '../etc/helpers.dart';
///
/// Use [init] to initialize the manager before creating streams.
class HttpCacheManager {
final LocalCacheServer _server;
/// Null when [init] was called with `startServer: false`: streams are then read
/// through [HttpCacheStream.request] only, and there is no cache URL to play.
final LocalCacheServer? _server;

/// The global configuration used for all streams managed by this manager.
final GlobalCacheConfig config;

final Map<RequestKey, HttpCacheStream> _streams = {};
final Map<RequestKey, File> _customCacheFiles = {};
HttpCacheManager._(this._server, this.config) {
_server.start(createStream);
_server?.start(createStream);
}

/// Whether the local cache server was started, i.e. whether [getCacheUrl] and [HttpCacheStream.cacheUrl] are available.
///
/// False when [init] was called with `startServer: false`.
bool get hasServer => _server != null;

/// Gets the cache URL for the given source URL.
/// If a custom cache file is provided, it will be saved and used for the cache stream.
///
/// Throws a [CacheServerUnavailableException] if the manager has no local cache server (see [hasServer]).
Uri getCacheUrl(final Uri sourceUrl, {final File? file}) {
_checkDisposed();
final server = _server;
if (server == null) {
throw CacheServerUnavailableException(sourceUrl);
}
if (file != null) {
_customCacheFiles[sourceUrl.requestKey] = file;
}
return _server.encodeSourceUrl(sourceUrl);
return server.encodeSourceUrl(sourceUrl);
}

/// Ensures the local cache server is accepting connections, restarting it on the same port if it is not.
///
/// On iOS, the server's socket can be reclaimed while the app is suspended in the background. The server is checked periodically
/// and restarted automatically, but a player may connect before the next check. Call this before retrying a request to a cache URL
/// that failed to connect.
///
/// Does nothing if the manager has no local cache server (see [hasServer]): no server is started or bound.
Future<void> ensureActive() {
_checkDisposed();
return _server.ensureActive();
return _server?.ensureActive() ?? Future.value();
}

/// Create a [HttpCacheStream] instance for the given URL. If an instance already exists, the existing instance will be returned.
Expand Down Expand Up @@ -72,7 +87,7 @@ class HttpCacheManager {

final cacheStream = HttpCacheStream(
sourceUrl: sourceUrl,
cacheUrl: _server.encodeSourceUrl(sourceUrl),
cacheUrl: _server?.encodeSourceUrl(sourceUrl),
files: cacheFiles,
config: config ?? createStreamConfig(),
);
Expand Down Expand Up @@ -178,13 +193,13 @@ class HttpCacheManager {
/// The input [url] can either be [sourceUrl] or [cacheUrl].
HttpCacheStream? getExistingStream(Uri url) {
_checkDisposed();
url = _server.decodeSourceUrl(url) ?? url;
url = _server?.decodeSourceUrl(url) ?? url;
return _streams[url.requestKey];
}

CacheFiles _resolveCacheFiles(Uri sourceUrl, [File? cacheFile]) {
if (cacheFile == null) {
sourceUrl = _server.decodeSourceUrl(sourceUrl) ?? sourceUrl;
sourceUrl = _server?.decodeSourceUrl(sourceUrl) ?? sourceUrl;
cacheFile = _customCacheFiles[sourceUrl.requestKey] ??
config.cacheFileResolver(config.cacheDirectory, sourceUrl);
}
Expand All @@ -202,7 +217,7 @@ class HttpCacheManager {
_onStreamCreated = null;

try {
await _server.close(force: true);
await _server?.close(force: true);
} finally {
_customCacheFiles.clear();
for (final stream in _streams.values.toList()) {
Expand Down Expand Up @@ -240,31 +255,47 @@ class HttpCacheManager {
/// [customHttpClient] is the custom http client to use. If null, a default http client will be used.
/// You can also provide [GlobalCacheConfig] for the initial configuration.
/// Use `port` to specify the port for the local server. If not provided, a random available port will be used.
///
/// Set [startServer] to false to skip the local cache server, for consumers that read streams directly through
/// [createStream] and [HttpCacheStream.request] rather than over HTTP. No socket is bound, so this works where binding
/// is refused (e.g. a macOS App Sandbox without the `com.apple.security.network.server` entitlement). Downloading
/// and caching work as usual, but [getCacheUrl] and [HttpCacheStream.cacheUrl] throw a [CacheServerUnavailableException],
/// [ensureActive] does nothing, and [port] is ignored. See [hasServer].
///
/// If the manager is already initialized, the existing instance is returned and these arguments are ignored.
static Future<HttpCacheManager> init({
final Directory? cacheDir,
final http.Client? customHttpClient,
final GlobalCacheConfig? config,
final int? port,
final bool startServer = true,
}) {
assert(config == null || (cacheDir == null && customHttpClient == null),
'Cannot set cacheDir or httpClient when config is provided. Set them in the config instead.');
if (_instance != null) {
return Future.value(instance);
}
return _initFuture ??= () async {
try {
final cacheConfig = config ??
GlobalCacheConfig(
cacheDirectory:
cacheDir ?? await GlobalCacheConfig.defaultCacheDirectory(),
customHttpClient: customHttpClient,
);
final httpCacheServer = await LocalCacheServer.init(port: port);
return _instance = HttpCacheManager._(httpCacheServer, cacheConfig);
} finally {
_initFuture = null;
}
final pending = _initFuture;
if (pending != null) return pending;
final future = _initFuture = () async {
final cacheConfig = config ??
GlobalCacheConfig(
cacheDirectory:
cacheDir ?? await GlobalCacheConfig.defaultCacheDirectory(),
customHttpClient: customHttpClient,
);
final httpCacheServer =
startServer ? await LocalCacheServer.init(port: port) : null;
return _instance = HttpCacheManager._(httpCacheServer, cacheConfig);
}();
// Cleared once the future is stored, not from inside the closure: without a server
// and with cacheDir or config given, the closure has no await, so it would finish
// (and clear _initFuture) before the assignment above, leaving a stale future that
// hands out the old manager after it is disposed.
future.whenComplete(() {
if (identical(_initFuture, future)) _initFuture = null;
}).ignore();
return future;
}

/// The singleton instance of [HttpCacheManager].
Expand Down
17 changes: 13 additions & 4 deletions lib/src/cache_stream/http_cache_stream.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,16 @@ class HttpCacheStream {
final Uri sourceUrl;

/// The Url of the cached stream served by the local cache server (e.g., http://localhost:8080/http/example.com/file.mp3)
final Uri cacheUrl;
///
/// Throws a [CacheServerUnavailableException] when the stream has no cache URL, which is the case when
/// `HttpCacheManager.init` was called with `startServer: false`. Check [hasCacheUrl] first, or read the content
/// through [request] instead.
Uri get cacheUrl =>
_cacheUrl ?? (throw CacheServerUnavailableException(sourceUrl));
final Uri? _cacheUrl;

/// Whether this stream is served by the local cache server, i.e. whether [cacheUrl] is available.
bool get hasCacheUrl => _cacheUrl != null;

/// The complete, partial, and metadata files used for the cache.
final CacheFiles files;
Expand All @@ -61,10 +70,10 @@ class HttpCacheStream {

HttpCacheStream({
required this.sourceUrl,
required this.cacheUrl,
final Uri? cacheUrl,
required this.files,
required this.config,
}) {
}) : _cacheUrl = cacheUrl {
_initFuture.run(() async {
try {
_cachedResponseHeaders =
Expand Down Expand Up @@ -620,5 +629,5 @@ class HttpCacheStream {

@override
String toString() =>
'HttpCacheStream{sourceUrl: $sourceUrl, cacheUrl: $cacheUrl, cacheFile: $cacheFile}';
'HttpCacheStream{sourceUrl: $sourceUrl, cacheUrl: $_cacheUrl, cacheFile: $cacheFile}';
}
13 changes: 13 additions & 0 deletions lib/src/models/exceptions/state_errors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,16 @@ class CacheManagerDisposedException extends StateError {
CacheManagerDisposedException()
: super('Attempted to use a disposed HttpCacheManager');
}

/// Thrown when a cache URL is requested from an `HttpCacheManager` that was
/// initialized with `startServer: false`. Without the local cache server there
/// is no URL to serve the cache from: read the content through
/// `HttpCacheStream.request` (or `download`) instead.
class CacheServerUnavailableException extends StateError {
final Uri sourceUrl;
CacheServerUnavailableException(this.sourceUrl)
: super(
'HttpCacheManager was initialized without a local cache server (startServer: false), '
'so there is no cache URL for $sourceUrl. Read it through HttpCacheStream.request instead.',
);
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: http_cache_stream
description: "Simultaneously download, cache, and stream remote content. Perfect for media players and any plugin that streams web content."
version: 0.2.1
version: 0.3.0
homepage: https://github.com/Colton127/http_cache_stream
repository: https://github.com/Colton127/http_cache_stream
topics:
Expand Down
Loading
Loading