add plugin camera desktop

This commit is contained in:
kyo
2026-06-22 09:39:31 +07:00
parent 92daeef0c5
commit 5407afbd6e
144 changed files with 15900 additions and 1 deletions

View File

@@ -0,0 +1,101 @@
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:camera_desktop/camera_desktop.dart';
import 'package:camera_desktop/src/image_stream_ffi.dart';
/// End-to-end proof for the orphaned-poller fix, using the REAL camera + real
/// FFI poller.
///
/// Scenario: an app starts an image stream and then disposes the camera WITHOUT
/// cancelling the stream subscription (exactly what `CameraController.dispose()`
/// does — it never stops image streams). Before the fix, the 8ms FFI poll timer
/// kept firing forever after the camera was gone. After the fix, `dispose()`
/// stops it.
///
/// We observe the real `ImageStreamFfi.pollCount` (number of poll ticks) before
/// and after `dispose()`: it must stop advancing.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets(
'disposing a streaming camera stops the FFI poll timer (no leak)',
(WidgetTester tester) async {
final plugin = CameraPlatform.instance as CameraDesktopPlugin;
// Capture the real FFI poller created for this stream (the default
// factory is ImageStreamFfi.tryCreate, which succeeds in a real build).
ImageStreamFfi? realPoller;
final defaultFactory = plugin.imageStreamPollerFactory;
plugin.imageStreamPollerFactory = (handle) {
final poller = defaultFactory(handle);
realPoller = poller as ImageStreamFfi?;
return poller;
};
addTearDown(() => plugin.imageStreamPollerFactory = defaultFactory);
final cameras = await plugin.availableCameras();
// ignore: avoid_print
print('[poller-leak-test] cameras found: ${cameras.length}');
if (cameras.isEmpty) {
markTestSkipped('No camera available on this machine.');
return;
}
final cameraId = await plugin.createCameraWithSettings(
cameras.first,
const MediaSettings(resolutionPreset: ResolutionPreset.low),
);
try {
await plugin.initializeCamera(cameraId);
} on CameraException catch (e) {
await plugin.dispose(cameraId);
markTestSkipped('Camera initialize failed: ${e.code} ${e.description}');
return;
}
// Start streaming. Critically: DO NOT cancel this subscription.
final sub = plugin.onStreamedFrameAvailable(cameraId).listen((_) {});
// Allow onListen (startImageStream round-trip) + several poll ticks.
await tester.pump();
await Future<void>.delayed(const Duration(milliseconds: 500));
expect(
realPoller,
isNotNull,
reason: 'FFI fast path should be active in a real macOS build',
);
final pollsWhileStreaming = realPoller!.pollCount;
expect(
pollsWhileStreaming,
greaterThan(0),
reason: 'the poll timer should be running while streaming',
);
// Dispose the camera WITHOUT cancelling the subscription — the leak path.
await plugin.dispose(cameraId);
final pollsAtDispose = realPoller!.pollCount;
await Future<void>.delayed(const Duration(milliseconds: 600));
final pollsAfterWait = realPoller!.pollCount;
// ignore: avoid_print
print(
'[poller-leak-test] polls: whileStreaming=$pollsWhileStreaming '
'atDispose=$pollsAtDispose afterWait(+600ms)=$pollsAfterWait',
);
expect(
pollsAfterWait,
equals(pollsAtDispose),
reason: 'after dispose the poll timer MUST be stopped; before the fix '
'it keeps firing ~125x/sec forever',
);
await sub.cancel();
},
timeout: const Timeout(Duration(seconds: 90)),
);
}

View File

@@ -0,0 +1,21 @@
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:camera_desktop/camera_desktop.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('CameraDesktopPlugin is registered as CameraPlatform', (
WidgetTester tester,
) async {
// On Linux, CameraDesktopPlugin should be auto-registered via registerWith().
expect(CameraPlatform.instance, isA<CameraDesktopPlugin>());
});
testWidgets('availableCameras returns a list', (WidgetTester tester) async {
final cameras = await CameraPlatform.instance.availableCameras();
// On a machine with no cameras this may be empty, but it shouldn't throw.
expect(cameras, isA<List<CameraDescription>>());
});
}