From 0d602e73988f63eb39e3699ba9d11adca3fcfa6b Mon Sep 17 00:00:00 2001 From: Sven Gehring Date: Fri, 1 Mar 2024 13:31:41 +0100 Subject: [PATCH] feat: add support for frameTimeout option This adds a frameTimeout option that can be passed a Duration. The option is expected to be used with streams where isLive=true, so that there is a timeout between receiving frames. If no new frame is received for the given timeout duration, the stream is automatically reloaded. --- README.md | 1 + lib/src/mjpeg.dart | 43 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d8fe6d4..9734024 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Parameter | Description --- | --- `isLive` | Whether or not the stream should be loaded continuously `timeout` | HTTP Timeout when fetching the MJPEG stream +`frameTimeout` | If no frame is received for this duration, the stream will be reloaded. This should only be used together with `isLive` as there's no point in waiting for new frames in a stream that's not live. `width` | Force width `height` | Force height `error` | Error builder used when an error occurred diff --git a/lib/src/mjpeg.dart b/lib/src/mjpeg.dart index a0c6c22..594ace4 100644 --- a/lib/src/mjpeg.dart +++ b/lib/src/mjpeg.dart @@ -43,6 +43,7 @@ class Mjpeg extends HookWidget { final double? height; final bool isLive; final Duration timeout; + final Duration? frameTimeout; final WidgetBuilder? loading; final Client? httpClient; final Widget Function(BuildContext contet, dynamic error, dynamic stack)? @@ -55,6 +56,7 @@ class Mjpeg extends HookWidget { this.isLive = false, this.width, this.timeout = const Duration(seconds: 5), + this.frameTimeout, this.height, this.fit, required this.stream, @@ -78,6 +80,7 @@ class Mjpeg extends HookWidget { isLive && visible.visible, headers, timeout, + frameTimeout, httpClient ?? Client(), preprocessor ?? MjpegPreprocessor(), isMounted, @@ -87,9 +90,10 @@ class Mjpeg extends HookWidget { isLive, visible.visible, timeout, + frameTimeout, httpClient, preprocessor, - isMounted + isMounted, ]); final key = useMemoized(() => UniqueKey(), [manager]); @@ -153,26 +157,55 @@ class _StreamManager { final String stream; final bool isLive; final Duration _timeout; + final Duration? _frameTimeout; final Map headers; final Client _httpClient; final MjpegPreprocessor _preprocessor; final bool Function() _mounted; // ignore: cancel_subscriptions StreamSubscription? _subscription; + Timer? _frameTimeoutTimer; - _StreamManager(this.stream, this.isLive, this.headers, this._timeout, - this._httpClient, this._preprocessor, this._mounted); + _StreamManager( + this.stream, + this.isLive, + this.headers, + this._timeout, + this._frameTimeout, + this._httpClient, + this._preprocessor, + this._mounted, + ); Future dispose() async { if (_subscription != null) { await _subscription!.cancel(); _subscription = null; } + _frameTimeoutTimer?.cancel(); _httpClient.close(); } + void _resetFrameTimeoutTimer( + BuildContext context, + ValueNotifier image, + ValueNotifier?> errorState, + ) { + if (_frameTimeout == null) return; + + _frameTimeoutTimer?.cancel(); + _frameTimeoutTimer = Timer(_frameTimeout!, () { + errorState.value = null; + updateStream(context, image, errorState); + }); + } + void _sendImage(BuildContext context, ValueNotifier image, - ValueNotifier errorState, List chunks) async { + ValueNotifier?> errorState, List chunks) async { + // If frame timeout timer is active and we are expecting new frames, + // reset the timer on each frame received. + _resetFrameTimeoutTimer(context, image, errorState); + // pass image through preprocessor sending to [Image] for rendering final List? imageData = _preprocessor.process(chunks); if (imageData == null) return; @@ -258,6 +291,8 @@ class _StreamManager { image.value = null; } } + } finally { + _resetFrameTimeoutTimer(context, image, errorState); } } }