Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 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
94954d1
Merge branch 'main' into camera_web_stream
TecHaxter Sep 21, 2025
006de9c
[camera_web] chore: addressed review comments
TecHaxter Sep 27, 2025
f5b3793
Merge branch 'main' into camera_web_stream
TecHaxter Sep 27, 2025
f0a1d27
[camera_web] fix: reformatting
TecHaxter Sep 27, 2025
def248d
[camera_web] fix: todo comment
TecHaxter Sep 27, 2025
1b5e9a1
[camera_web] fix: todo comment
TecHaxter Sep 27, 2025
33174ca
[camera_web] fix: reformatting
TecHaxter Sep 28, 2025
18d2a1a
Merge branch 'main' into camera_web_stream
TecHaxter Apr 3, 2026
08d33a8
[camera_web] chore: streaming frame time tolerance
TecHaxter Apr 3, 2026
cc1b895
[camera_web] docs: updated comment indentation
TecHaxter Apr 3, 2026
37a029e
[camera_web] chore: imageData late to final
TecHaxter Apr 3, 2026
0a3390a
[camera_web] chore: optimized offscreen canvas resize
TecHaxter Apr 3, 2026
19ec1c3
[camera_web] chore: optimized canvas element resize
TecHaxter Apr 3, 2026
526df97
[camera_web] perf: Reuse offscreen canvas context
TecHaxter Apr 3, 2026
e0ec25e
[camera_web] bump: camera_platform_interface
TecHaxter Apr 3, 2026
0c0d694
[camera_web] feat: enabled supportsImageStreaming to return true
TecHaxter Apr 3, 2026
f38e0b7
[camera_web] test: added test for supportsImageStreaming
TecHaxter Apr 3, 2026
6ac4c87
[camera_web] fix: changelog *
TecHaxter Apr 3, 2026
06ce8de
[camera_web]: fix lint issues
TecHaxter Apr 8, 2026
15f28b5
[camera_web] fix mockito mock code gen by removing const record
TecHaxter Apr 8, 2026
4c031b4
[camera_web] dart format
TecHaxter Apr 11, 2026
8869a5c
[camera_web]: Addressed comments
TecHaxter May 17, 2026
d202223
[camera_web]: Fix CONTEXT_LOST_WEBGL issue
TecHaxter Jun 7, 2026
a98095c
[camera_web]: Added MediaStreamTrackProcessor camera streaming support
TecHaxter Jun 14, 2026
da1ea0d
Merge branch 'main' into camera_web_stream
TecHaxter Jun 14, 2026
7accd27
[camera_web]: Added MediaStreamTrackProcessor happy path in camera_te…
TecHaxter Jun 21, 2026
880fbef
[camera_web]: refactored and added error handling in legacy cameraFra…
TecHaxter Jun 28, 2026
62e6a0a
[camera_web]: Added camera tests
TecHaxter Jun 28, 2026
44d9a16
[camera_web]: Added new camera error codes
TecHaxter Jul 2, 2026
3268971
[camera_web]: Added getMediaStreamVideoTrack camera service method an…
TecHaxter Jul 2, 2026
23ffc95
[camera_web]: Added new camera error code test and refactored existing
TecHaxter Jul 2, 2026
2522a35
[camera_web]: Added camera service tests
TecHaxter Jul 2, 2026
b37cc71
[camera_web]: Refactor to remove unnecessary test
TecHaxter Jul 2, 2026
3e4bc76
[camera_web]: Added new error code for non-fatel continue streaming
TecHaxter Jul 5, 2026
86b2f76
[camera_web]: Handled DOMException in camera_service, refactored to f…
TecHaxter Jul 5, 2026
5de3844
[camera_web]: Moved wasm type checker in visible testing and updated …
TecHaxter Jul 5, 2026
2527cd0
[camera_web]: Move asDOMException ext and added copyVideoFrameToBuffe…
TecHaxter Jul 5, 2026
e95607a
[camera_web]: Refactored camera_test.dart
TecHaxter Jul 6, 2026
1fb7fc3
[camera_web]: Added camera_web tests
TecHaxter Jul 6, 2026
2e2690b
Merge branch 'main' into camera_web_stream
mdebbar Aug 11, 2026
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
4 changes: 4 additions & 0 deletions packages/camera/camera_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.3.6

* Adds support for camera image stream on web.

## 0.3.5+5

* Removes invalid @JS annotation from extension type constructors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,43 @@ void main() {
expect(CameraErrorCode.notStarted.toString(), equals('cameraNotStarted'));
});

testWidgets('noVideoTrack', (WidgetTester tester) async {
expect(CameraErrorCode.noVideoTrack.toString(), equals('noVideoTrack'));
});

testWidgets('cameraFrameDimensionsZero', (WidgetTester tester) async {
expect(
CameraErrorCode.cameraFrameDimensionsZero.toString(),
equals('cameraFrameDimensionsZero'),
);
});

testWidgets('videoTrackReaderClosed', (WidgetTester tester) async {
expect(CameraErrorCode.videoTrackReaderClosed.toString(), equals('videoTrackReaderClosed'));
});

testWidgets('videoTrackReaderNotInitialized', (WidgetTester tester) async {
expect(
CameraErrorCode.videoTrackReaderNotInitialized.toString(),
equals('videoTrackReaderNotInitialized'),
);
});

testWidgets('videoRecordingNotStarted', (WidgetTester tester) async {
expect(
CameraErrorCode.videoRecordingNotStarted.toString(),
equals('videoRecordingNotStarted'),
);
});

testWidgets('missingPlaneLayout', (WidgetTester tester) async {
expect(CameraErrorCode.missingPlaneLayout.toString(), equals('missingPlaneLayout'));
});

testWidgets('unableToCloneFrame', (WidgetTester tester) async {
expect(CameraErrorCode.unableToCloneFrame.toString(), equals('unableToCloneFrame'));
});

testWidgets('unknown', (WidgetTester tester) async {
expect(CameraErrorCode.unknown.toString(), equals('cameraUnknown'));
});
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 @@ -793,5 +794,292 @@ void main() {
);
});
});

group('hasMediaStreamTrackProcessor', () {
setUp(() {
cameraService.jsUtil = jsUtil;
});

testWidgets('returns true if broswer has MediaStreamTrackProcessor '
'otherwise false', (WidgetTester widgetTester) async {
for (final supportsMediaStreamTrackProcessor in <bool>[true, false]) {
when(
jsUtil.hasProperty(window, 'MediaStreamTrackProcessor'.toJS),
).thenReturn(supportsMediaStreamTrackProcessor);

final bool hasMediaStreamTrackProcessor = cameraService.hasMediaStreamTrackProcessor();

expect(
hasMediaStreamTrackProcessor,
supportsMediaStreamTrackProcessor ? isTrue : isFalse,
);
}
});
});

group('getMediaStreamVideoTrack', () {
testWidgets('returns the first video track when tracks exist in the stream', (
WidgetTester widgetTester,
) async {
final web.MediaStream mockStream = getVideoElementWithBlankStream(
const Size(10, 10),
).captureStream();

final web.MediaStreamTrack track = cameraService.getMediaStreamVideoTrack(mockStream);

expect(track, isNotNull);
});

testWidgets('throw CameraWebException when no video tracks are found', (
WidgetTester widgetTester,
) async {
final mockStream = createJSInteropWrapper(FakeMediaStream([])) as web.MediaStream;

expect(
() => cameraService.getMediaStreamVideoTrack(mockStream, cameraId: 42),
throwsA(
isA<CameraWebException>().having((e) => e.code, 'code', CameraErrorCode.noVideoTrack),
),
);
});
});

group('getMediaStreamTrackReader', () {
testWidgets('returns ReadableStreamDefaultReader of MediaStreamTrack', (
WidgetTester tester,
) async {
final web.MediaStreamTrack mediaStreamTrack = getVideoElementWithBlankStream(
const Size(10, 10),
).captureStream().getTracks().toDart.first;

final web.ReadableStreamDefaultReader readableStreamDefaultReader = cameraService
.getMediaStreamTrackReader(mediaStreamTrack);

expect(
readableStreamDefaultReader.read().toDart,
isA<Future<web.ReadableStreamReadResult>>(),
);
mediaStreamTrack.stop();
});
});

group('readVideoTrack', () {
setUp(() {
cameraService.isVideoFrameType = (_) => true;
});

web.ReadableStreamDefaultReader mockMediaTrackReader({
MockVideoFrame? mockFrame,
bool done = false,
}) {
final mockReader = MockReadableStreamDefaultReader();
mockReader.read = (() => Future.value(
web.ReadableStreamReadResult(
done: done,
value: mockFrame == null ? null : createJSInteropWrapper(mockFrame) as JSAny,
),
).toJS).toJS;
return createJSInteropWrapper(mockReader) as web.ReadableStreamDefaultReader;
}

testWidgets('returns VideoFrame from ReadableStreamDefaultReader', (
WidgetTester widgetTester,
) async {
final mockFrame = MockVideoFrame(width: 10, height: 10, bufferSize: 32);
final web.ReadableStreamDefaultReader mockReader = mockMediaTrackReader(
mockFrame: mockFrame,
);

final web.VideoFrame videoFrame = await cameraService.readVideoTrack(mockReader);

expect(videoFrame.visibleRect, equals(mockFrame.visibleRect));
expect(
videoFrame.allocationSize(web.VideoFrameCopyToOptions()),
equals(mockFrame.bufferSize),
);
expect(videoFrame.format, equals(mockFrame.format));
});

void expectReadVideoFrameException({
required web.ReadableStreamDefaultReader reader,
required CameraErrorCode errorCode,
}) {
expect(
() => cameraService.readVideoTrack(reader),
throwsA(
isA<CameraWebException>().having((CameraWebException e) => e.code, 'code', errorCode),
),
);
}

testWidgets('throws CameraWebException if the video track reader is closed', (
WidgetTester tester,
) async {
final web.ReadableStreamDefaultReader mockReader = mockMediaTrackReader(done: true);

expectReadVideoFrameException(
reader: mockReader,
errorCode: CameraErrorCode.videoTrackReaderClosed,
);
});

testWidgets('throws CameraWebException if the video track reader is not initialized', (
WidgetTester tester,
) async {
final mockFrame = MockVideoFrame(width: 0, height: 0, bufferSize: 0)..visibleRect = null;
final web.ReadableStreamDefaultReader mockReader = mockMediaTrackReader(
mockFrame: mockFrame,
);

expectReadVideoFrameException(
reader: mockReader,
errorCode: CameraErrorCode.videoTrackReaderNotInitialized,
);
});
});

group('copyVideoFrameToBufferAndGetStride', () {
web.VideoFrame stubVideoFrameCopyTo(JSPromise<JSArray<web.PlaneLayout>> Function() result) {
final mockFrame = MockVideoFrame(width: 10, height: 10, bufferSize: 32);
mockFrame.copyTo = (JSAny? destination, web.VideoFrameCopyToOptions options) => result();
return createJSInteropWrapper(mockFrame) as web.VideoFrame;
}

testWidgets('returns first plane stride', (WidgetTester tester) async {
const expectedStride = 640;
final web.VideoFrame videoFrame = stubVideoFrameCopyTo(
() => Future<JSArray<web.PlaneLayout>>.value(
JSArray<web.PlaneLayout>()..add(web.PlaneLayout(offset: 0, stride: expectedStride)),
).toJS,
);

final int stride = await cameraService.copyVideoFrameToBufferAndGetStride(
videoFrame,
destination: JSUint8Array.withLength(100),
copyOptions: web.VideoFrameCopyToOptions(),
);

expect(stride, expectedStride);
});

void expectCameraWebException(web.VideoFrame videoFrame, CameraErrorCode code) {
expect(
() => cameraService.copyVideoFrameToBufferAndGetStride(
videoFrame,
destination: JSUint8Array.withLength(100),
copyOptions: web.VideoFrameCopyToOptions(),
),
throwsA(isA<CameraWebException>().having((CameraWebException e) => e.code, 'code', code)),
);
}

testWidgets('throws missingPlaneLayout when browser returns no planes', (
WidgetTester tester,
) async {
final web.VideoFrame videoFrame = stubVideoFrameCopyTo(
() => Future<JSArray<web.PlaneLayout>>.value(JSArray<web.PlaneLayout>()).toJS,
);

expectCameraWebException(videoFrame, CameraErrorCode.missingPlaneLayout);
});

testWidgets('throws unableToCloneFrame when DOMException occurs', (
WidgetTester tester,
) async {
final web.VideoFrame videoFrame = stubVideoFrameCopyTo(
() => throw web.DOMException('Frame is corrupted', 'InvalidStateError'),
);

expectCameraWebException(videoFrame, CameraErrorCode.unableToCloneFrame);
});
});

group('getCameraImageData', () {
testWidgets('returns CameraImageData with valid planes', (WidgetTester tester) async {
const mockWidth = 2;
const mockHeight = 2;
final mockBytes = Uint8List(mockWidth * mockHeight * 4);

final CameraImageData cameraImageData = cameraService.getCameraImageData(
width: mockWidth,
height: mockHeight,
bytes: mockBytes,
bytesPerRow: mockWidth * 4,
);

expect(cameraImageData.width, equals(mockWidth));
expect(cameraImageData.height, equals(mockHeight));

expect(cameraImageData.format.group, equals(ImageFormatGroup.unknown));
expect(cameraImageData.format.raw, equals('rgba8888'));

expect(cameraImageData.planes.length, equals(1));
final CameraImagePlane plane = cameraImageData.planes.first;

expect(plane.width, equals(mockWidth));
expect(plane.height, equals(mockHeight));
expect(plane.bytesPerPixel, equals(4));
expect(plane.bytesPerRow, equals(mockWidth * 4));
expect(plane.bytes, equals(mockBytes));
});
});

group('hasPropertyOffScreenCanvas', () {
setUp(() {
cameraService.jsUtil = jsUtil;
});
testWidgets('hasPropertyOffScreenCanvas returns true if broswer has OffscreenCanvas '
'otherwise false', (WidgetTester widgetTester) async {
for (final supportsOffscreenCanvas in <bool>[true, false]) {
when(
jsUtil.hasProperty(window, 'OffscreenCanvas'.toJS),
).thenReturn(supportsOffscreenCanvas);
final bool hasOffScreenCanvas = cameraService.hasPropertyOffScreenCanvas();
expect(hasOffScreenCanvas, supportsOffscreenCanvas ? isTrue : isFalse);
}
});
});

group('takeFrame', () {
testWidgets('returns Camera Image of Size '
'when videoElement is of Size '
'regardless of OffscreenCanvas support', (WidgetTester widgetTester) async {
const size = Size(10, 10);
final completer = Completer<void>();
final web.VideoElement videoElement = getVideoElementWithBlankStream(size)
..onLoadedMetadata.listen((_) {
completer.complete();
})
..load();
await completer.future;

for (final supportsOffscreenCanvas in <bool>[true, false]) {
when(
jsUtil.hasProperty(window, 'OffscreenCanvas'.toJS),
).thenReturn(supportsOffscreenCanvas);

final CameraImageData cameraImageData = cameraService.takeFrame(videoElement);

expect(size, Size(cameraImageData.width.toDouble(), cameraImageData.height.toDouble()));
}
});

testWidgets('throws CameraWebException when video element not loaded', (
WidgetTester widgetTester,
) async {
final web.VideoElement videoElement = getVideoElementWithBlankStream(const Size(10, 10));

expect(
() => cameraService.takeFrame(videoElement),
throwsA(
isA<CameraWebException>().having(
(CameraWebException e) => e.code,
'code',
CameraErrorCode.cameraFrameDimensionsZero,
),
),
);
});
});
});
}
Loading