Skip to content

[camera_web] Re: Support for camera stream on web #7950

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 53 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
2552bdc
[camera/camera_web] Supporting camera stream on web
TecHaxter Mar 31, 2024
72a7127
[camera_web] Version and Changelog updated
TecHaxter Apr 22, 2024
d366566
[camera_web] - integration test updated: mock videoElement initialize…
TecHaxter Apr 23, 2024
4a18e22
[camera_web] - integration test updated: mock videoElement initialize…
TecHaxter Apr 23, 2024
b12f7e7
[camera_web] camera service takeFrame with better exception handelling
TecHaxter May 8, 2024
8697046
[camera_web] - integration test updated: mock videoElement initialize…
TecHaxter Apr 23, 2024
b42fbb9
[camera_web] camera service takeFrame with better exception handelling
TecHaxter May 8, 2024
b7b9342
[camera_web] tested takeFrame camera service function
TecHaxter May 8, 2024
8ec3a04
[camera_web] - integration test updated: mock videoElement initialize…
TecHaxter Apr 23, 2024
622841c
[camera_web] camera service takeFrame with better exception handelling
TecHaxter May 8, 2024
096ac4d
[camera_web] tested takeFrame camera service function
TecHaxter May 8, 2024
ed6f086
[camera_web] tested cameraFrameStream
TecHaxter May 8, 2024
d1244a0
[camera_web] - integration test updated: mock videoElement initialize…
TecHaxter Apr 23, 2024
d891a36
[camera_web] camera service takeFrame with better exception handelling
TecHaxter May 8, 2024
7dfb194
[camera_web] tested takeFrame camera service function
TecHaxter May 8, 2024
b9111cb
[camera_web] tested cameraFrameStream
TecHaxter May 8, 2024
f80cb03
[camera_web] platform version updated according to guidelines
TecHaxter May 8, 2024
41ac242
[camera_web] - integration test updated: mock videoElement initialize…
TecHaxter Apr 23, 2024
3edea8c
[camera_web] camera service takeFrame with better exception handelling
TecHaxter May 8, 2024
d1e4d95
[camera_web] tested takeFrame camera service function
TecHaxter May 8, 2024
86da719
[camera_web] tested cameraFrameStream
TecHaxter May 8, 2024
dcf48f9
[camera_web] platform version updated according to guidelines
TecHaxter May 8, 2024
7e01244
style: removed late from late final_cameraFrameStreamController
TecHaxter May 23, 2024
6d20e38
refactor: cameraFrameStream handles looping of animation frames
TecHaxter May 23, 2024
f56879e
refactor: saperated _triggerAnimationFramesLoop from cameraFrameStrea…
TecHaxter May 23, 2024
1e7c869
chore: can use off screen canvas constant
TecHaxter May 26, 2024
5b10bce
chore: removed unnecessary string interpolation from videoElement sty…
TecHaxter May 26, 2024
90bb72e
chore: reusing the OffscreenCanvas or CanvasElement depending on canU…
TecHaxter May 26, 2024
b196180
refactor: removed getCameraImageDataFromBytes to be used in-line
TecHaxter May 26, 2024
5c1c662
fix: setup hasPropertyOffScreenCanvas in tests
TecHaxter May 26, 2024
f484f02
chore: version and changelog updated
TecHaxter Jul 14, 2024
98bceb7
docs: removed "Streaming of frames" from Missing implementation
TecHaxter Jul 14, 2024
bf9c3fe
Revert "docs: removed "Streaming of frames" from Missing implementation"
TecHaxter Jul 14, 2024
f183b04
merge: branch 'main' flutter/packages + replaced dart:html import wit…
TecHaxter Oct 28, 2024
22cad65
fix: version bump
TecHaxter Oct 28, 2024
380c35b
fix: import organized in camera_test
TecHaxter Oct 28, 2024
e31fbf2
Merge branch 'main' of https://github.com/TecHaxter/flutter_packages …
TecHaxter Nov 1, 2024
cc818ba
[camera_web] fix: Changelog.md
TecHaxter Nov 1, 2024
a04a472
[camera_web] chore: imports organized for camera.dart
TecHaxter Nov 1, 2024
aa9a819
[camera_web] fix: CameraImageData constructed with unkown ImageFormat…
TecHaxter Nov 4, 2024
5941119
[camera_web]: chore: preserving animationFrameId from requestAnimatio…
TecHaxter Nov 4, 2024
3756949
[camera_web] chore: removed completer from _triggerAnimationFramesLoo…
TecHaxter Nov 4, 2024
74bb74e
[camera_web] fix: jsified willReadFrequently in getContext of Offscre…
TecHaxter Nov 4, 2024
54aa92e
[camera_web] chore: optimized frame rate by using simple arithmatic c…
TecHaxter Nov 4, 2024
e46ef6f
[camera_web] chore: removed canUseOffScreenCanvas named paramater fro…
TecHaxter Nov 4, 2024
5e7bb5e
[camera_web] chore: initializing canUseOffscreenCanvas from construct…
TecHaxter Nov 4, 2024
c48418b
[camera_web]: replaced dynamic with Object? in map value for willRead…
TecHaxter Nov 6, 2024
a0cd373
[camera_web] test: camera image stream test cases for with and withou…
TecHaxter Nov 6, 2024
d7b7d5c
[camera_web] test: camera frame stream test cases for with and withou…
TecHaxter Nov 6, 2024
6cd955a
Merge branch 'main' into camera_web_stream
TecHaxter Nov 6, 2024
a959df5
chore: updated animate function in _triggerAnimationFramesLoop to get…
TecHaxter Nov 12, 2024
526160b
Merge branch 'main' into camera_web_stream
TecHaxter Nov 12, 2024
98af1e3
Merge branch 'camera_web_stream' of https://github.com/TecHaxter/flut…
TecHaxter Nov 12, 2024
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
3 changes: 2 additions & 1 deletion packages/camera/camera_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 0.3.6

* Supporting camera image stream on web.
* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.

## 0.3.5
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ void main() {
).thenAnswer(
(_) => Future<MediaStream>.value(canvasElement.captureStream()));

when(
() => cameraService.hasPropertyOffScreenCanvas(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to mock this? The code should be running on Chrome, which AFAIK has an offscreen canvas. What happens to this test if we revert this change?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cameraService is a mock class object itself.
If we don't stub hasPropertyOffScreenCanvas, it throws an error - "TypeError: null: type 'Null' is not a subtype of type 'bool'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we mark this resolved?

).thenAnswer((_) => true);

final Camera camera = Camera(
textureId: cameraId,
cameraService: cameraService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

// ignore_for_file: only_throw_errors

import 'dart:async';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';

Expand Down Expand Up @@ -903,5 +904,103 @@ void main() {
);
});
});

group('camera image stream', () {
setUp(
() {
cameraService = cameraService..jsUtil = jsUtil;
},
);
testWidgets(
'returns true if broswer has OffscreenCanvas '
'otherwise false',
(WidgetTester widgetTester) async {
when(
() => jsUtil.hasProperty(
window,
'OffscreenCanvas'.toJS,
),
).thenReturn(true);
final bool hasOffScreenCanvas =
cameraService.hasPropertyOffScreenCanvas();
expect(
hasOffScreenCanvas,
true,
);
when(
() => jsUtil.hasProperty(
window,
'OffscreenCanvas'.toJS,
),
).thenReturn(false);
final bool hasNotOffScreenCanvas =
cameraService.hasPropertyOffScreenCanvas();
expect(
hasNotOffScreenCanvas,
false,
);
},
);
testWidgets(
'returns Camera Image of Size '
'when videoElement is of Size '
'when browser supports OffscreenCanvas',
(WidgetTester widgetTester) async {
const Size size = Size(10, 10);
final Completer<void> completer = Completer<void>();
final web.VideoElement videoElement =
getVideoElementWithBlankStream(size)
..onLoadedMetadata.listen((_) {
completer.complete();
})
..load();
await completer.future;
when(() => cameraService.hasPropertyOffScreenCanvas()).thenReturn(
true,
);
final CameraImageData cameraImageData = cameraService.takeFrame(
videoElement,
);
expect(
size,
Size(
cameraImageData.width.toDouble(),
cameraImageData.height.toDouble(),
),
);
verify(cameraService.hasPropertyOffScreenCanvas).called(1);
},
);
testWidgets(
'returns Camera Image of Size '
'when videoElement is of Size '
'when browser does not supports OffscreenCanvas',
(WidgetTester widgetTester) async {
const Size size = Size(10, 10);
final Completer<void> loadVideo = Completer<void>();
final web.VideoElement videoElement =
getVideoElementWithBlankStream(size)
..onLoadedMetadata.listen((_) {
loadVideo.complete();
})
..load();
await loadVideo.future;
when(() => cameraService.hasPropertyOffScreenCanvas()).thenReturn(
false,
);
final CameraImageData cameraImageData = cameraService.takeFrame(
videoElement,
);
expect(
size,
Size(
cameraImageData.width.toDouble(),
cameraImageData.height.toDouble(),
),
);
verify(cameraService.hasPropertyOffScreenCanvas).called(1);
},
);
});
});
}
107 changes: 107 additions & 0 deletions packages/camera/camera_web/example/integration_test/camera_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'dart:typed_data';
import 'dart:ui';

import 'package:async/async.dart';
Expand Down Expand Up @@ -61,6 +62,10 @@ void main() {
cameraId: any(named: 'cameraId'),
),
).thenAnswer((_) => Future<MediaStream>.value(mediaStream));

when(
() => cameraService.hasPropertyOffScreenCanvas(),
).thenAnswer((_) => true);
Comment on lines +66 to +68
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above, we need a test where offscreen canvas is available, and one where it isn't, so we test both code paths.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fixed, can be marked as resolved?

});

setUpAll(() {
Expand Down Expand Up @@ -1704,5 +1709,107 @@ void main() {
});
});
});
group('cameraFrameStream', () {
testWidgets(
'CameraImageData bytes is a multiple of 4 '
'when browser supports OffscreenCanvas',
(WidgetTester tester) async {
final VideoElement videoElement = getVideoElementWithBlankStream(
const Size(10, 10),
);

final Camera camera = Camera(
textureId: textureId,
cameraService: cameraService,
)..videoElement = videoElement;

when(() => cameraService.hasPropertyOffScreenCanvas()).thenReturn(
true,
);

when(
() => cameraService.takeFrame(videoElement),
).thenAnswer(
(_) => CameraImageData(
format: const CameraImageFormat(
ImageFormatGroup.unknown,
raw: 0,
),
planes: <CameraImagePlane>[
CameraImagePlane(
bytes: Uint8List(32),
bytesPerRow: videoElement.width * 4,
),
],
height: 10,
width: 10,
),
);

final CameraImageData cameraImageData =
await camera.cameraFrameStream().first;
expect(
cameraImageData,
equals(
isA<CameraImageData>().having(
(CameraImageData e) => e.planes.first.bytes.length % 4,
'bytes',
equals(0),
),
),
);
},
);
testWidgets(
'CameraImageData bytes is a multiple of 4 '
'when browser does not supports OffscreenCanvas',
(WidgetTester tester) async {
final VideoElement videoElement = getVideoElementWithBlankStream(
const Size(10, 10),
);

final Camera camera = Camera(
textureId: textureId,
cameraService: cameraService,
)..videoElement = videoElement;

when(() => cameraService.hasPropertyOffScreenCanvas()).thenReturn(
false,
);

when(
() => cameraService.takeFrame(videoElement),
).thenAnswer(
(_) => CameraImageData(
format: const CameraImageFormat(
ImageFormatGroup.unknown,
raw: 0,
),
planes: <CameraImagePlane>[
CameraImagePlane(
bytes: Uint8List(32),
bytesPerRow: videoElement.width * 4,
),
],
height: 10,
width: 10,
),
);

final CameraImageData cameraImageData =
await camera.cameraFrameStream().first;
expect(
cameraImageData,
equals(
isA<CameraImageData>().having(
(CameraImageData e) => e.planes.first.bytes.length % 4,
'bytes',
equals(0),
),
),
);
},
);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ void main() {
(_) async => videoElement.captureStream(),
);

when(
() => cameraService.hasPropertyOffScreenCanvas(),
).thenAnswer((_) => true);
Comment on lines +103 to +105
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be needed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cameraService is a mock class object itself.
If we don't stub hasPropertyOffScreenCanvas, it throws an error - "TypeError: null: type 'Null' is not a subtype of type 'bool'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we mark this resolved?


CameraPlatform.instance = CameraPlugin(
cameraService: cameraService,
)..window = window;
Expand Down
73 changes: 72 additions & 1 deletion packages/camera/camera_web/lib/src/camera.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ class Camera {
required CameraService cameraService,
this.options = const CameraOptions(),
this.recorderOptions = const (audioBitrate: null, videoBitrate: null),
}) : _cameraService = cameraService;
}) : _cameraService = cameraService,
canUseOffscreenCanvas = cameraService.hasPropertyOffScreenCanvas();

/// The texture id used to register the camera view.
final int textureId;
Expand Down Expand Up @@ -159,6 +160,10 @@ class Camera {
final StreamController<VideoRecordedEvent> videoRecorderController =
StreamController<VideoRecordedEvent>.broadcast();

/// Used to check if allowed to paint canvas off screen
@visibleForTesting
bool canUseOffscreenCanvas = false;

/// Initializes the camera stream displayed in the [videoElement].
/// Registers the camera view with [textureId] under [_getViewType] type.
/// Emits the camera default video track on the [onEnded] stream when it ends.
Expand Down Expand Up @@ -638,4 +643,70 @@ class Camera {
..height = '100%'
..objectFit = 'cover';
}

final StreamController<CameraImageData> _cameraFrameStreamController =
StreamController<CameraImageData>.broadcast();

// TODO(replace): introduced fps in
/// [CameraImageStreamOptions]
final int cameraStreamFPS = 60;

/// Returns a stream of camera frames.
///
/// To stop listening to new animation frames close all listening streams.
Stream<CameraImageData> cameraFrameStream({
CameraImageStreamOptions? options,
}) {
_cameraFrameStreamController.onListen = () {
_triggerAnimationFramesLoop(
_addCameraImageDataEvent,
fps: cameraStreamFPS,
);
};

return _cameraFrameStreamController.stream;
}

/// Triggers animation frames in a loop at a specified FPS
/// as long as [animationFrameId] is not cancelled
void _triggerAnimationFramesLoop(
VoidCallback action, {
required int fps,
}) {
int? animationFrameId;
num then = 0, fpsInterval = 1000 / fps;

int? animate(num timestamp) {
// Schedule the next frame
animationFrameId = window.requestAnimationFrame(animate.toJS);
// Calculate the elapsed time since the last frame
final num elapsed = timestamp - then;

// if we're close to the next frame (by ~8ms), do it.
if (fpsInterval - elapsed <= 8) {
// Get ready for next frame
then = timestamp;
// Perform the action task
action();
}
return animationFrameId;
}

// Initialize the animation loop
animationFrameId = animate(window.performance.now());

// Listen for the stream controller cancellation to stop the animation
_cameraFrameStreamController.onCancel = () {
if (animationFrameId != null) {
window.cancelAnimationFrame(animationFrameId!);
animationFrameId = null;
}
};
}

/// Used to trigger add event of camera image data in camera frame stream
void _addCameraImageDataEvent() {
final CameraImageData image = _cameraService.takeFrame(videoElement);
_cameraFrameStreamController.add(image);
}
}
Loading