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
1 change: 0 additions & 1 deletion .flutter-plugins-dependencies

This file was deleted.

31 changes: 31 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: tests

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: subosito/flutter-action@v2
with:
channel: stable

- name: Print versions
run: flutter --version

- name: Install dependencies
run: flutter pub get

- name: Run tests
run: flutter test

# Static analysis is informative but kept non-blocking so style lints in
# tests don't mask the test result.
- name: Analyze
if: always()
continue-on-error: true
run: flutter analyze
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
## 0.1.0

This release significantly simplifies cache management. A new `getCacheUrl` API automates the full lifecycle of cache streams, eliminating the need to create or manage `HttpCacheStream` instances for most integrations.

### Breaking Changes

- **`HttpCacheServer` has been removed.** Use `HttpCacheManager.getCacheUrl` instead. It covers all previous use cases, including HLS/DASH and other dynamic URL patterns, with full lifecycle automation.
- **`InvalidCacheLengthException` has been renamed to `InvalidCacheSizeException`**, which now exposes the expected and actual cache size.

### New Features

**`HttpCacheManager`**

- Added `getCacheUrl(Uri sourceUrl)`. Returns a local cache URL that can be passed directly to any media player. Cache streams are created lazily on first request and their lifecycle is managed automatically by the cache manager.
- Added an optional `port` parameter to `HttpCacheManager.init`. Specifying a custom port allows cache urls to work across app restarts.

**`HttpCacheStream`**

- Added `release()`. Hands the stream back to the cache manager for lifecycle-managed teardown, governed by `StreamLifecycleConfig`. Use `release()` in place of `dispose()` when the stream may be reused. `dispose()` continues to work as before and bypasses the lifecycle configuration.
- Added `CacheState`, a richer alternative to the plain progress value. It tracks both the current cache position (bytes on disk) and the total source length. Access the latest snapshot via `cacheStream.cacheState`, or subscribe to changes via `cacheStream.cacheStateStream`.
- Added `StreamLifecycleConfig` to `CacheConfiguration`. Controls the inactive-stream behavior applied after `release()` is called. By default: downloads are paused after 10 seconds of inactivity, cancelled after the configured `readTimeout`, and the stream is fully disposed after 5 minutes.

### Improvements

- Cache headers are now read using async I/O.

## 0.0.6
* Add `onStreamCreated` callback to `HttpCacheManager`.

Expand Down
78 changes: 40 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,22 @@ Using http_cache_stream to simultaneously cache and stream a video file:
```dart
import 'package:http_cache_stream/http_cache_stream.dart';

// Initialize the cache manager
// Initialize the cache manager once, at app startup
await HttpCacheManager.init();

// Create a stream for a specific URL
// Get a local cache URL for any remote resource
final sourceUrl = Uri.parse('https://example.com/video.mp4');
final cacheStream = HttpCacheManager.instance.createStream(sourceUrl);

// Get the local cache URL to pass to your media player
final cacheUrl = cacheStream.cacheUrl;
final cacheUrl = HttpCacheManager.instance.getCacheUrl(sourceUrl);
// Example output: http://127.0.0.1:4612/example.com/video.mp4

// Use with any player
final videoPlayerController = VideoPlayerController.network(cacheUrl);
// Pass it directly to your media player
final videoPlayerController = VideoPlayerController.networkUrl(cacheUrl);
await videoPlayerController.initialize();
videoPlayerController.play();
```

The cache stream is created and managed automatically — no lifecycle management required. For direct access to the `HttpCacheStream` instance (e.g., to monitor download progress or configure per-stream settings), use `createStream` instead. See [HttpCacheStream](#httpcachestream) below.

See the example project to learn how to use http_cache_stream with [video_player](https://pub.dev/packages/video_player), [audioplayers](https://pub.dev/packages/audioplayers), and [just_audio](https://pub.dev/packages/just_audio)

## Platform configuration
Expand Down Expand Up @@ -85,54 +84,42 @@ See the example project for a full example.
The central manager for all cache streams:

```dart
// Initialize once
// Initialize once, at app startup
await HttpCacheManager.init();
final cacheManager = HttpCacheManager.instance;

// Get a cache URL — the recommended way to integrate with media players
final cacheUrl = cacheManager.getCacheUrl(sourceUrl);

// Pre-cache files:
await cacheManager.preCacheUrl(Uri.parse('https://example.com/file.mp3'));

// Configure global settings
cacheManager.config.requestHeaders[HttpHeaders.userAgentHeader] = 'MyApp/1.0';

// Create streams
final cacheStream = cacheManager.createStream(url);

// Manage cache
await cacheManager.deleteCache(); // Clear inactive cache
// Delete inactive cache files
await cacheManager.deleteCache();
```

## HttpCacheStream

Manages downloading, caching, and streaming for a specific URL:
Provides direct access to the download, cache, and streaming state for a specific URL. Use `createStream` when you need to monitor progress or configure per-stream settings.

```dart
final cacheStream = cacheManager.createStream(Uri.parse('https://example.com/file.mp3'));

// Start download explicitly (optional)
// Start download explicitly (optional — begins automatically on first request)
cacheStream.download();

// Get download progress
cacheStream.progressStream.listen((progress) {
print('Download progress: ${(progress * 100).toStringAsFixed(1)}%');
// Monitor CacheState for position and source length
cacheStream.cacheStateStream.listen((state) {
print('${state.position} / ${state.sourceLength} bytes');
});

cacheStream.dispose(); // Release when done
```

## HttpCacheServer

Creates a local proxy server that dynamically handles URLs from the same source. Ideal for HLS/DASH streaming where a master playlist references multiple segment files. Cache streams are automatically created and disposed.

```dart
// Create a server for a base URL
final sourceUri = Uri.parse('https://example.com/');
final cacheServer = await cacheManager.createServer(sourceUri);

// The URI of the cache server. Requests to this URI will be fulfilled from the source URI.
final serverUri = cacheServer.uri;

// Manually obtain a cache url
final hlsCacheUrl = cacheServer.getCacheUrl(Uri.parse('https://example.com/video.m3u8'));

cacheServer.dispose(); // Release when done
// Release when done — the stream will be paused and eventually disposed
// automatically according to StreamLifecycleConfig.
// Call dispose() instead to tear down immediately.
cacheStream.release();
```


Expand Down Expand Up @@ -176,6 +163,21 @@ cacheStream.config.copyCachedResponseHeaders = true;
```


### Stream Lifecycle

When `createStream` is used, call `release()` instead of `dispose()` to hand the stream back to the cache manager. The behavior after release is controlled by `StreamLifecycleConfig`:

```dart
// Configure globally (applies to all streams)
HttpCacheManager.instance.config.lifecycleConfig = StreamLifecycleConfig(
pauseAfter: Duration(seconds: 10), // Pause the download after 10s of inactivity
disposeAfter: Duration(minutes: 5), // Fully dispose the stream after 5 minutes
);
```

Call `dispose()` to tear down a stream immediately, bypassing the lifecycle configuration.


### Range Request Controls
When a client requests a byte range that's significantly ahead of what's currently cached, the library can initiate a separate direct connection to the source, rather than waiting for the sequential cache download to reach that position. The `rangeRequestSplitThreshold` setting controls this behavior by defining how many bytes ahead a request must be to trigger a separate connection.

Expand Down
2 changes: 0 additions & 2 deletions example/ios/Flutter/AppFrameworkInfo.plist
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# platform :ios, '13.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down
37 changes: 2 additions & 35 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,49 +1,16 @@
PODS:
- audio_session (0.0.1):
- Flutter
- audioplayers_darwin (0.0.1):
- Flutter
- Flutter (1.0.0)
- just_audio (0.0.1):
- Flutter
- FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS

DEPENDENCIES:
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`)
- Flutter (from `Flutter`)
- just_audio (from `.symlinks/plugins/just_audio/darwin`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)

EXTERNAL SOURCES:
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
audioplayers_darwin:
:path: ".symlinks/plugins/audioplayers_darwin/ios"
Flutter:
:path: Flutter
just_audio:
:path: ".symlinks/plugins/just_audio/darwin"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"

SPEC CHECKSUMS:
audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe
audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467

PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

COCOAPODS: 1.16.2
48 changes: 27 additions & 21 deletions example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
Expand Down Expand Up @@ -52,6 +53,7 @@
511BAEB2CDB143F2F0CA690D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
921BCB6ED5B245FDA091615F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
Expand Down Expand Up @@ -80,6 +82,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
10FECA1D154572E08E5E73F9 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -112,6 +115,7 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
Expand Down Expand Up @@ -198,13 +202,15 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
D5F582AABA8F541DF0C0D951 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
Expand Down Expand Up @@ -238,6 +244,9 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
Expand Down Expand Up @@ -323,23 +332,6 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
D5F582AABA8F541DF0C0D951 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
E526F1AAFB0242452D860C25 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
Expand Down Expand Up @@ -455,7 +447,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down Expand Up @@ -585,7 +577,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -636,7 +628,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down Expand Up @@ -726,6 +718,20 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */

/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
Loading
Loading