Files
rustdesk/flutter/lib/models/web_video_frame_queue.dart
4234b99029 WebClient: 3.44 webcodecs offline (#15722)
* feat(web): zero-readback WebCodecs video path

Decoded VideoFrames from js/src/webcodecs.js are handed to Flutter via
window.onVideoFrame and imported GPU-side with createImageFromTextureSource;
any failure unregisters the hook so the JS side falls back to RGBA readback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): load bundled terminal font when Google CDNs are unreachable

In air-gapped deployments GoogleFonts.robotoMono() cannot download the
terminal font; when index.html signals offline mode, load the copy bundled
with the web app under the family name google_fonts registers.

Part of the fix for rustdesk/rustdesk-server-pro#996.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: bump windows arm64 to Flutter 3.44.8, add web build patch script

apply_flutter_3.44_web_patches.sh prepares a 3.44.x web build on top of the
shared source patches: qr_code_scanner's web impl needs dart:ui_web for the
removed platformViewRegistry, and flutter/web/fonts is refreshed to the font
paths the 3.44 engine requests. The disabled build-rustdesk-web job runs it
automatically once FLUTTER_VERSION moves to 3.44.x, and version-guarded
'Patch flutter' steps no longer fail when the guard does not match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): prevent stale WebCodecs frames across sessions

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(web): harden WebCodecs reconnect and Flutter 3.44 patches

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ci): harden Flutter 3.44 patch input validation

Validate required files before checking patch state,
parameterize the theme-range validator, and prevent
missing inputs from satisfying NO_MATCHES checks.

Signed-off-by: fufesou <linlong1266@gmail.com>

* Remove unused code

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(web): retry font loading and dispose stale decoded images

Signed-off-by: fufesou <linlong1266@gmail.com>

* remove unused code

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(web): Bad state: RenderBox was not laid out

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-08-07 15:20:57 +08:00

134 lines
3.7 KiB
Dart

import 'dart:async';
typedef VideoFrameImporter<Frame, Image> = Future<Image> Function(Frame frame);
typedef VideoFrameCloser<Frame> = void Function(Frame frame);
typedef VideoImageDisposer<Image> = void Function(Image image);
typedef VideoSessionValidator = bool Function();
typedef VideoImageCallback<Image> = Future<void> Function(
int display, Image image, VideoSessionValidator isCurrentSession);
typedef VideoQueueErrorCallback = void Function(
Object error, StackTrace stackTrace);
class WebVideoFrameQueue<Frame, Image> {
WebVideoFrameQueue({
required VideoFrameImporter<Frame, Image> importFrame,
required VideoFrameCloser<Frame> closeFrame,
required VideoImageDisposer<Image> disposeImage,
required VideoQueueErrorCallback onImportError,
required VideoQueueErrorCallback onCallbackError,
}) : _importFrame = importFrame,
_closeFrame = closeFrame,
_disposeImage = disposeImage,
_onImportError = onImportError,
_onCallbackError = onCallbackError;
final VideoFrameImporter<Frame, Image> _importFrame;
final VideoFrameCloser<Frame> _closeFrame;
final VideoImageDisposer<Image> _disposeImage;
final VideoQueueErrorCallback _onImportError;
final VideoQueueErrorCallback _onCallbackError;
final Map<int, _QueuedFrame<Frame>> _pending = {};
VideoImageCallback<Image>? _callback;
int _generation = 0;
bool _processing = false;
bool _enabled = true;
bool get isEnabled => _enabled;
void beginSession(VideoImageCallback<Image> callback) {
_invalidateSession();
_enabled = true;
_callback = callback;
}
void endSession() {
_invalidateSession();
_callback = null;
}
void _invalidateSession() {
_generation++;
for (final queued in _pending.values) {
_closeFrame(queued.frame);
}
_pending.clear();
}
bool submit(int display, Frame frame) {
if (!_enabled || _callback == null) {
_closeFrame(frame);
return false;
}
final replaced = _pending.remove(display);
if (replaced != null) {
_closeFrame(replaced.frame);
}
_pending[display] = _QueuedFrame(display, frame, _generation);
_startProcessing();
return true;
}
void _startProcessing() {
if (_processing) return;
_processing = true;
unawaited(Future<void>(_process));
}
Future<void> _process() async {
while (_pending.isNotEmpty) {
final display = _pending.keys.first;
final queued = _pending.remove(display)!;
if (!_enabled || queued.generation != _generation) {
_closeFrame(queued.frame);
continue;
}
await _importAndDeliver(queued);
}
_processing = false;
}
Future<void> _importAndDeliver(_QueuedFrame<Frame> queued) async {
Image? image;
try {
image = await _importFrame(queued.frame);
} catch (error, stackTrace) {
if (queued.generation == _generation) {
_enabled = false;
_onImportError(error, stackTrace);
}
} finally {
_closeFrame(queued.frame);
}
if (image != null) {
await _deliver(queued, image);
}
}
Future<void> _deliver(_QueuedFrame<Frame> queued, Image image) async {
final callback = _callback;
bool isCurrentSession() =>
_enabled &&
queued.generation == _generation &&
identical(callback, _callback);
if (!isCurrentSession() || callback == null) {
_disposeImage(image);
return;
}
try {
await callback(queued.display, image, isCurrentSession);
} catch (error, stackTrace) {
_disposeImage(image);
_onCallbackError(error, stackTrace);
}
}
}
class _QueuedFrame<Frame> {
const _QueuedFrame(this.display, this.frame, this.generation);
final int display;
final Frame frame;
final int generation;
}