-
Notifications
You must be signed in to change notification settings - Fork 9.8k
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @ABausG for the contribution, this is great!
I've left some questions about the current approach (mostly about cross-browser compat).
BTW, @bselwe is currently OOTO (until the end of the week), but he was working in normalizing the exceptions thrown from the code.
In this PR, for example, we're manually throwing DomExceptions from the low-level Camera object, but I think @bselwe was working on getting some standardization in what types of exceptions to throw from where. The guiding lines here would be the core camera
plugin, seeing what types of Exceptions are expected there, so everything works transparently.
I'll defer to @bselwe for more comments in that regard!
Other than that, this is looking amazing! Thanks again for the contribution!
/// /// Throws a [html.DomException.INVALID_STATE] if there already is an active Recording | ||
Future<void> startVideoRecording({Duration? maxVideoDuration}) async { | ||
if (_mediaRecorder != null && _mediaRecorder!.state != 'inactive') { | ||
throw html.DomException.INVALID_STATE; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(See my general comment about the exception style within the plugin. @bselwe was overhauling this, and I don't think we'd want to throw a raw DomException
, even from the low-level Camera object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, AFAIK, it is not possible to create an instance of html.DomException
(the class has only a private factory). It also seems that html.DomException.INVALID_STATE
is a constant string rather than an exception so we would not be able to provide both code and description for the error.
I think the general rule would be to follow MethodChannelCamera
(an implementation of the camera platform used for iOS and Android platforms) and CameraController
:
- Video recording methods in
MethodChannelCamera
invoke methods on the method channel - this may throw aPlatformException
. For consistency, any error that is related to an invalid state/execution of the camera during video recording should throw aPlatformException
as well. - We should rather avoid throwing a
CameraException
in internal classes (not exposed to the end user). If we throw platform exceptions, they are usually caught and mapped to camera exceptions in theCameraController
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throwing a PlatformException
now and not checking fo the state
of the mediaRecorder`
if (maxVideoDuration != null) { | ||
_mediaRecorder!.addEventListener('dataavailable', (event) { | ||
final blob = (event as html.BlobEvent).data; | ||
final file = XFile(html.Url.createObjectUrl(blob)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file
needs some extra data, like name and mimeType to be initialized at this point. This will help later when the user wants to save it to disk.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added mimeType and Filename. For now using the hashcode
of the blob as filename
/// Pauses the current video Recording | ||
/// Throws a [html.DomException.INVALID_STATE] if there is no active Recording | ||
Future<void> pauseVideoRecording() async { | ||
if (_mediaRecorder == null || _mediaRecorder!.state == 'inactive') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the docs, the _mediaRecorder state check is performed by the browser?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be probably enough to throw a PlatformException
with an appropriate code and description if _mediaRecorder
is null. Any exception thrown by MediaRecorder.pause
would then be caught in the camera platform here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above. Now throwing PlatformExceptions
final availableData = Completer<XFile>(); | ||
_mediaRecorder!.addEventListener('dataavailable', (event) { | ||
final blob = (event as html.BlobEvent).data; | ||
availableData.complete(XFile(html.Url.createObjectUrl(blob))); | ||
}); | ||
_mediaRecorder?.stop(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There seems to be some code duplication here vs startVideoRecording (with a max length).
Could you unify these two codepaths so the implementation of thedataavailable
event on startVideoRecording
uses the same logic as "stop"?
I also wonder why the start with max length emits a VideoRecordedEvent
, but stopVideoRecording
doesn't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unified it by registering the listener on dataavailable
only in the startVideoRecording.
If the listener is triggered it will do the following things:
- emit a
VideoRecordedEvent
- Complete the Completer used to obtain the
XFile
instopVideoRecording
- remove the listener
- stop the MediaRecorder
- This stop is necessary to only get data once if a
maxVideoDuration
is provided asMediaRecorder
only takes in splices instead of a maxDuration
- This stop is necessary to only get data once if a
.add(VideoRecordedEvent(this.textureId, file, maxVideoDuration)); | ||
_mediaRecorder!.stop(); | ||
}); | ||
_mediaRecorder!.start(maxVideoDuration.inMilliseconds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if maxVideoDuration
is 0 milliseconds
? Should that be disallowed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could throw a DomException
with NotSupportedError
that would be caught in the camera platform here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throwing a PlatformException
if (_mediaRecorder == null || _mediaRecorder!.state == 'inactive') { | ||
throw html.DomException.INVALID_STATE; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(These exceptions are already thrown by the browser, no need to assert for _mediaRecorder.state)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, same as above. It would be probably enough to throw a PlatformException
with an appropriate code and description if _mediaRecorder
is null. Any exception thrown by MediaRecorder.resume
would then be caught in the camera platform here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thanks for the contribution @ABausG and your comments @ditman! 🙌
I've added some clarification regarding which exceptions to throw and when. The general rule would be to try to match the exceptions thrown in MethodChannelCamera
(an implementation of the camera platform used for iOS and Android platforms) and handled in CameraController
. Let me know if there's anything unclear about this approach.
I think it would also be nice to include Camera
tests. That could include:
- For
startVideoRecording
:- Throws an exception when the media recorder is in an invalid state.
- Initializes a media recorder instance from the video element stream.
- Uses an appropriate mime type for the video recording based on
MediaRecorder.isTypeSupported()
. - Uses
maxVideoDuration
to record a video. - Emits a
VideoRecordedEvent
when the video recording has been stopped.
- For
pauseVideoRecording
:- Throws an exception when the media recorder is null.
- Calls pause on the media recorder.
- And similar tests for other methods.
Thanks again!
final StreamController<VideoRecordedEvent> _videoRecorderController = | ||
StreamController(); | ||
|
||
/// Returns a Stream that emits when a video Recodring with a defined maxVideoDuration was created |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Returns a Stream that emits when a video Recodring with a defined maxVideoDuration was created | |
/// Returns a Stream that emits when a video recording with a defined maxVideoDuration was created. |
/// /// Throws a [html.DomException.INVALID_STATE] if there already is an active Recording | ||
Future<void> startVideoRecording({Duration? maxVideoDuration}) async { | ||
if (_mediaRecorder != null && _mediaRecorder!.state != 'inactive') { | ||
throw html.DomException.INVALID_STATE; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, AFAIK, it is not possible to create an instance of html.DomException
(the class has only a private factory). It also seems that html.DomException.INVALID_STATE
is a constant string rather than an exception so we would not be able to provide both code and description for the error.
I think the general rule would be to follow MethodChannelCamera
(an implementation of the camera platform used for iOS and Android platforms) and CameraController
:
- Video recording methods in
MethodChannelCamera
invoke methods on the method channel - this may throw aPlatformException
. For consistency, any error that is related to an invalid state/execution of the camera during video recording should throw aPlatformException
as well. - We should rather avoid throwing a
CameraException
in internal classes (not exposed to the end user). If we throw platform exceptions, they are usually caught and mapped to camera exceptions in theCameraController
.
if (_mediaRecorder == null || _mediaRecorder!.state == 'inactive') { | ||
throw html.DomException.INVALID_STATE; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, same as above. It would be probably enough to throw a PlatformException
with an appropriate code and description if _mediaRecorder
is null. Any exception thrown by MediaRecorder.resume
would then be caught in the camera platform here.
/// Pauses the current video Recording | ||
/// Throws a [html.DomException.INVALID_STATE] if there is no active Recording | ||
Future<void> pauseVideoRecording() async { | ||
if (_mediaRecorder == null || _mediaRecorder!.state == 'inactive') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be probably enough to throw a PlatformException
with an appropriate code and description if _mediaRecorder
is null. Any exception thrown by MediaRecorder.pause
would then be caught in the camera platform here.
.add(VideoRecordedEvent(this.textureId, file, maxVideoDuration)); | ||
_mediaRecorder!.stop(); | ||
}); | ||
_mediaRecorder!.start(maxVideoDuration.inMilliseconds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could throw a DomException
with NotSupportedError
that would be caught in the camera platform here.
videoElement.captureStream(), {'mimeType': 'video/webm'}); | ||
|
||
if (maxVideoDuration != null) { | ||
_mediaRecorder!.addEventListener('dataavailable', (event) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to remove the event listener (removeEventListener
) when the video recording is stopped?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Removing it now
StreamController(); | ||
|
||
/// Returns a Stream that emits when a video Recodring with a defined maxVideoDuration was created | ||
Stream<VideoRecordedEvent> get onVideoRecordedEventStream => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WDYT about renaming it to onVideoRecorded
for consistency with other stream names in the camera plugin (onCameraInitialized
, onCameraResolutionChanged
, etc.)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed it to onVideoRecordedEvent
to match the naming of the getter in the camera_plugin_interface
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we maybe remove throwing an unimplemented error in prepareForVideoRecording
?
Match naming from camera_plugin_interface
I was thinking about that method as well. I'm seeing two options:
|
Some issues with removing listener and stopping the mediaRecorder
packages/camera/camera_web/example/integration_test/camera_test.dart
Outdated
Show resolved
Hide resolved
MediaRecorder.start with a Timeslice does not seem to fire in the test case
testWidgets( | ||
'starts a video recording with a given maxDuration ' | ||
'emits a VideoRecordedEvent', (tester) async { | ||
// TODO: MediaRecorder with a Timeslice does not seem to call the dataavailable Listener in a Test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could not get this test to work. As it does not seem to trigger the datavailable event of the media recorder.
I was able to manually test the behaviour with setting a max duration (I tested with 4 seconds) however this test seems to timeout waiting for the first element of the event stream.
Could you take a look at what's might be going on?
Also is there a (nice) way to debug the integration tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will have a look at this test.
There is a way to debug integration tests in the browser. You'll need to execute this in camera_web/example
:
flutter run -d web-server --target integration_test/camera_test.dart --debug
It will produce a link that you should open in the browser, open inspection tools (CMD+Option+J in Chrome), find a dart file you want to debug in the "Source" tab and mark a breakpoint. Refresh the web page and it should stop at the breakpoint.
|
||
await camera.startVideoRecording(); | ||
|
||
expect('recording', camera.mediaRecorder!.state); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expect('recording', camera.mediaRecorder!.state); | |
expect(camera.mediaRecorder!.state, equals('recording`)); |
throwsA(predicate<PlatformException>( | ||
(ex) => ex.code == CameraErrorCode.notSupported.toString()))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit
throwsA(predicate<PlatformException>( | |
(ex) => ex.code == CameraErrorCode.notSupported.toString()))); | |
throwsA(predicate<PlatformException>( | |
(ex) => ex.code == CameraErrorCode.notSupported.toString(),),),); |
|
||
await camera.pauseVideoRecording(); | ||
|
||
expect('paused', camera.mediaRecorder!.state); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expect('paused', camera.mediaRecorder!.state); | |
expect(camera.mediaRecorder!.state, equals('paused')); |
|
||
await camera.resumeVideoRecording(); | ||
|
||
expect('recording', camera.mediaRecorder!.state); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expect('recording', camera.mediaRecorder!.state); | |
expect(camera.mediaRecorder!.state, equals('recording')); |
Ahhh, I didn't notice that, very good catch there @bselwe! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After reading the PR again, I don't understand very well how these features are currently implemented:
- How does this respect the max length of a video recording passed by a user? I don't think we ever check or use the value of max length (only null checks).
- How does this combine several
ondataavailable
events with video "chunks" to produce one single, large video file that can be used later? It seems this code emits the File as soon as the firstondataavailable
event fires.- See the how they use
ondataavailable
andstop
events here (it's for audio, but the API is the same). - Is this even supported by XFile, do we need to change anything there? Do we need to be able to create a XFile from an html.File, or a bunch of Blobs or something other than "bytes"?
- See the how they use
- @bselwe noticed that video on Chrome/Edge is missing some metadata, and the duration is listed as
Infinity
; have you noticed this? It seems to be this bug. SO1, SO2. (Is this something that can be fixed when putting together the XFile from all the "chunks"? Does the mp4 codec have this problem?)
For now, I'm going to publish the camera_web plugin with what we currently have, and when this PR matures a little bit more, we can release it in a new version. This is a great start, but I'm a little concerned about the comments above :S
We discussed this, and it seems that we're leveraging the
This is already achieved by the behavior of the |
All (the pull request submitter and all commit authors) CLAs are signed, but one or more commits were authored or co-authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that by leaving a comment that contains only Note to project maintainer: There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent. In those cases, you can manually confirm consent of the commit author(s), and set the ℹ️ Googlers: Go here for more info. |
@googlebot I consent. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! All tests pass locally, let's go with this one!
* feat: Add Support for Video Recording in Camera Web * docs: add video recording documentation Co-authored-by: Bartosz Selwesiuk <[email protected]>
* feat: Add Support for Video Recording in Camera Web * docs: add video recording documentation Co-authored-by: Bartosz Selwesiuk <[email protected]>
Adds ability to record
webm
of the camera platform interface.Implements calls
startVideoRecording
,pauseVideoRecording
,resumeVideoRecording
andstopVideoRecording
Part of flutter/flutter#45297.
Pre-launch Checklist
dart format
.)[shared_preferences]
///
).