Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[camera] Limit video length with maxVideoDuration on startVideoRecording #3403

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c7a749a
Added maxVideoDuration to startVideoRecording
danielroek Dec 23, 2020
131918d
updated documentation
danielroek Dec 23, 2020
9b3ae14
updated documentation
danielroek Dec 23, 2020
3ca25df
Fixed long line in docs
danielroek Dec 23, 2020
5e626b9
Formatting
danielroek Dec 23, 2020
02811b7
Started implementation for Android
danielroek Dec 24, 2020
b8b07e2
WIP: Started implementation of stream when time limit is reached
danielroek Dec 24, 2020
aff2938
Merge remote-tracking branch 'origin/master' into limit_video_length
danielroek Jan 8, 2021
532cbb0
Merge remote-tracking branch 'origin/master' into limit_video_length
danielroek Jan 11, 2021
50edc53
Initial working implementation
danielroek Jan 11, 2021
6e31d7c
Android implementation works
danielroek Jan 11, 2021
d3bab02
Improved implementation
danielroek Jan 11, 2021
ae7365c
Updated README order
danielroek Jan 11, 2021
ae1b47c
removed debugPrints
danielroek Jan 11, 2021
4037b5e
Fixed url in README.md
danielroek Jan 11, 2021
36acc32
Formatting
danielroek Jan 11, 2021
5a78233
Merge remote-tracking branch 'origin/master' into limit_video_length
danielroek Jan 13, 2021
09dad16
Implemented Java feedback
danielroek Jan 13, 2021
771f116
Implemented Event and Stream to notify about videoRecording
danielroek Jan 13, 2021
16e43fb
stopVideoRecording now listens to VideoRecordedEvent
danielroek Jan 13, 2021
7cb73ef
Fixed future returning xFile
danielroek Jan 13, 2021
759f163
finished iOS implementation
Feb 3, 2021
0b4270d
Fixed formatting
Feb 3, 2021
12231da
fixed formatting
Feb 3, 2021
3b8d6ed
Reverted platform_interface changes
danielroek Feb 3, 2021
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/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.7

* Added maxVideoDuration to startVideoRecording to limit the length of a recording.

## 0.6.6

* Adds auto focus support for Android and iOS implementations.
Expand Down
31 changes: 29 additions & 2 deletions packages/camera/camera/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ It's important to note that the `MediaRecorder` class is not working properly on

### Handling Lifecycle states

As of version [0.5.0](https://github.com/flutter/plugins/blob/master/packages/camera/CHANGELOG.md#050) of the camera plugin, lifecycle changes are no longer handled by the plugin. This means developers are now responsible to control camera resources when the lifecycle state is updated. Failure to do so might lead to unexpected behavior (for example as described in issue [#39109](https://github.com/flutter/flutter/issues/39109)). Handling lifecycle changes can be done by overriding the `didChangeAppLifecycleState` method like so:
As of version [0.5.0](https://github.com/flutter/plugins/blob/master/packages/camera/camera/CHANGELOG.md#050) of the camera plugin, lifecycle changes are no longer handled by the plugin. This means developers are now responsible to control camera resources when the lifecycle state is updated. Failure to do so might lead to unexpected behavior (for example as described in issue [#39109](https://github.com/flutter/flutter/issues/39109)). Handling lifecycle changes can be done by overriding the `didChangeAppLifecycleState` method like so:

```dart
@override
Expand All @@ -66,6 +66,33 @@ As of version [0.5.0](https://github.com/flutter/plugins/blob/master/packages/ca
}
```

As of version [0.6.5](https://github.com/flutter/plugins/blob/master/packages/camera/camera/CHANGELOG.md#065) the startVideoRecording method can be used with the maxVideoDuration. To do this the result of the recording needs to be retrieved by calling controller.onCameraTimeLimitReachedEvent which accepts a callback to retrieve the XFile result. Like so:

```dart
Future<void> startVideoRecording() async {
if (!controller.value.isInitialized) {
showInSnackBar('Error: select a camera first.');
return;
}

if (controller.value.isRecordingVideo) {
// A recording is already started, do nothing.
return;
}

try {
await controller.startVideoRecording(
maxVideoDuration: const Duration(milliseconds: 5000),
);
controller.onCameraTimeLimitReachedEvent(onCameraTimeLimitReached: (XFile file) {
//Handle the XFile
});
} on CameraException catch (e) {
_showCameraException(e);
return;
}
}
```
Comment on lines +69 to +95
Copy link
Contributor

Choose a reason for hiding this comment

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

Really like that you didn't forget to updated the documentation in the README.md. But this should probably also be updated according the the discussion @BeMacized started on using a stream instead of registering a callback method.

### Example

Here is a small example flutter app displaying a full screen camera preview.
Expand Down Expand Up @@ -122,7 +149,7 @@ class _CameraAppState extends State<CameraApp> {
}
```

For a more elaborate usage example see [here](https://github.com/flutter/plugins/tree/master/packages/camera/example).
For a more elaborate usage example see [here](https://github.com/flutter/plugins/tree/master/packages/camera/camera/example).

*Note*: This plugin is still under development, and some APIs might not be available yet.
[Feedback welcome](https://github.com/flutter/flutter/issues) and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package io.flutter.plugins.camera;

import static android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED;
import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN;
import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize;

Expand Down Expand Up @@ -106,6 +107,7 @@ public class Camera {
private int exposureOffset;
private boolean useAutoFocus = true;
private Range<Integer> fpsRange;
private Integer maxDurationLimit;

private static final HashMap<String, Integer> supportedImageFormats;
// Current supported outputs
Expand Down Expand Up @@ -187,16 +189,21 @@ private void initFps(CameraCharacteristics cameraCharacteristics) {
Log.i("Camera", "[FPS Range] is:" + fpsRange);
}

private void prepareMediaRecorder(String outputFilePath) throws IOException {
private void prepareMediaRecorder(String outputFilePath, Integer maxVideoDuration)
throws IOException {
if (mediaRecorder != null) {
mediaRecorder.release();
}

mediaRecorder =
MediaRecorderBuilder mediaRecorderBuilder =
new MediaRecorderBuilder(recordingProfile, outputFilePath)
.setEnableAudio(enableAudio)
.setMediaOrientation(getMediaOrientation())
.build();
.setMediaOrientation(getMediaOrientation());

if (maxVideoDuration != null) {
mediaRecorderBuilder.setMaxVideoDuration(maxVideoDuration);
}
mediaRecorder = mediaRecorderBuilder.build();
}

@SuppressLint("MissingPermission")
Expand Down Expand Up @@ -603,8 +610,9 @@ private void unlockAutoFocus() {
(errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null));
}

public void startVideoRecording(Result result) {
public void startVideoRecording(Result result, Integer maxVideoDuration) {
final File outputDir = applicationContext.getCacheDir();
maxDurationLimit = maxVideoDuration;
try {
videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir);
} catch (IOException | SecurityException e) {
Expand All @@ -613,10 +621,27 @@ public void startVideoRecording(Result result) {
}

try {
prepareMediaRecorder(videoRecordingFile.getAbsolutePath());
prepareMediaRecorder(videoRecordingFile.getAbsolutePath(), maxVideoDuration);
recordingVideo = true;
createCaptureSession(
CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface());
if (maxVideoDuration != null) {
mediaRecorder.setOnInfoListener(
(mr, what, extra) -> {
if (what == MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
try {
dartMessenger.sendVideoRecordedEvent(
videoRecordingFile.getAbsolutePath(), maxVideoDuration);
recordingVideo = false;
videoRecordingFile = null;
maxDurationLimit = null;
resetCaptureSession();
} catch (CameraAccessException e) {
result.error("videoRecordingFailed", e.getMessage(), null);
}
}
});
}
result.success(null);
} catch (CameraAccessException | IOException e) {
recordingVideo = false;
Expand All @@ -625,6 +650,18 @@ public void startVideoRecording(Result result) {
}
}

public void resetCaptureSession() throws CameraAccessException {
try {
cameraCaptureSession.abortCaptures();
mediaRecorder.stop();
} catch (IllegalStateException e) {
// Ignore exceptions and try to continue (chances are camera session already aborted capture)
}

mediaRecorder.reset();
startPreview();
}

public void stopVideoRecording(@NonNull final Result result) {
if (!recordingVideo) {
result.success(null);
Expand All @@ -634,18 +671,11 @@ public void stopVideoRecording(@NonNull final Result result) {
try {
recordingVideo = false;

try {
cameraCaptureSession.abortCaptures();
mediaRecorder.stop();
} catch (CameraAccessException | IllegalStateException e) {
// Ignore exceptions and try to continue (changes are camera session already aborted capture)
}

mediaRecorder.reset();
startPreview();
result.success(videoRecordingFile.getAbsolutePath());
resetCaptureSession();
dartMessenger.sendVideoRecordedEvent(videoRecordingFile.getAbsolutePath(), maxDurationLimit);
maxDurationLimit = null;
videoRecordingFile = null;
} catch (CameraAccessException | IllegalStateException e) {
} catch (CameraAccessException e) {
result.error("videoRecordingFailed", e.getMessage(), null);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum EventType {
ERROR,
CAMERA_CLOSING,
INITIALIZED,
VIDEO_RECORDED,
}

DartMessenger(BinaryMessenger messenger, long cameraId) {
Expand Down Expand Up @@ -53,6 +54,17 @@ void sendCameraInitializedEvent(
});
}

void sendVideoRecordedEvent(String path, Integer maxVideoDuration) {
this.send(
EventType.VIDEO_RECORDED,
new HashMap<String, Object>() {
{
if (path != null) put("path", path);
if (maxVideoDuration != null) put("maxVideoDuration", maxVideoDuration);
}
});
}

void sendCameraClosingEvent() {
send(EventType.CAMERA_CLOSING);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
}
case "startVideoRecording":
{
camera.startVideoRecording(result);
camera.startVideoRecording(result, call.argument("maxVideoDuration"));
break;
}
case "stopVideoRecording":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ MediaRecorder makeMediaRecorder() {

private boolean enableAudio;
private int mediaOrientation;
private int maxVideoDuration;

public MediaRecorderBuilder(
@NonNull CamcorderProfile recordingProfile, @NonNull String outputFilePath) {
Expand All @@ -47,6 +48,11 @@ public MediaRecorderBuilder setMediaOrientation(int orientation) {
return this;
}

public MediaRecorderBuilder setMaxVideoDuration(int maxVideoDuration) {
this.maxVideoDuration = maxVideoDuration;
return this;
}

public MediaRecorder build() throws IOException {
MediaRecorder mediaRecorder = recorderFactory.makeMediaRecorder();

Expand All @@ -67,6 +73,8 @@ public MediaRecorder build() throws IOException {
mediaRecorder.setOutputFile(outputFilePath);
mediaRecorder.setOrientationHint(this.mediaOrientation);

mediaRecorder.setMaxDuration(maxVideoDuration);

mediaRecorder.prepare();

return mediaRecorder;
Expand Down
7 changes: 6 additions & 1 deletion packages/camera/camera/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,10 @@ class _CameraExampleHomeState extends State<CameraExampleHome>

try {
await controller.initialize();
controller.onVideoRecordedEvent().listen((VideoRecordedEvent event) {
// Handle VideoRecordedEvent
debugPrint('event is stream ${event.file.path}');
});
_minAvailableExposureOffset = await controller.getMinExposureOffset();
_maxAvailableExposureOffset = await controller.getMaxExposureOffset();
_maxAvailableZoom = await controller.getMaxZoomLevel();
Expand Down Expand Up @@ -663,6 +667,7 @@ class _CameraExampleHomeState extends State<CameraExampleHome>

void onStopButtonPressed() {
stopVideoRecording().then((file) {
debugPrint('file after future ${file.path}');
if (mounted) setState(() {});
if (file != null) {
showInSnackBar('Video recorded to ${file.path}');
Expand Down Expand Up @@ -698,7 +703,7 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
}

try {
await controller.startVideoRecording();
await controller.startVideoRecording(maxVideoDuration: null);
} on CameraException catch (e) {
_showCameraException(e);
return;
Expand Down
41 changes: 28 additions & 13 deletions packages/camera/camera/ios/Classes/CameraPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,8 @@ - (CVPixelBufferRef)copyPixelBuffer {
return pixelBuffer;
}

- (void)startVideoRecordingWithResult:(FlutterResult)result {
- (void)startVideoRecordingWithResult:(FlutterResult)result
maxVideoDuration:(int64_t)maxVideoDuration {
if (!_isRecording) {
NSError *error;
_videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4"
Expand All @@ -766,6 +767,14 @@ - (void)startVideoRecordingWithResult:(FlutterResult)result {
result([FlutterError errorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]);
return;
}
if (maxVideoDuration != 0) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(maxVideoDuration * NSEC_PER_MSEC)),
dispatch_get_main_queue(), ^{
if (self->_isRecording) {
[self stopVideoRecording:maxVideoDuration];
}
});
}
_isRecording = YES;
_isRecordingPaused = NO;
_videoTimeOffset = CMTimeMake(0, 1);
Expand All @@ -778,27 +787,27 @@ - (void)startVideoRecordingWithResult:(FlutterResult)result {
}
}

- (void)stopVideoRecordingWithResult:(FlutterResult)result {
- (void)stopVideoRecording:(int64_t)maxVideoDuration {
if (_isRecording) {
_isRecording = NO;
if (_videoWriter.status != AVAssetWriterStatusUnknown) {
[_videoWriter finishWritingWithCompletionHandler:^{
if (self->_videoWriter.status == AVAssetWriterStatusCompleted) {
result(self->_videoRecordingPath);
[self->_methodChannel invokeMethod:@"video_recorded"
arguments:@{
@"path" : self->_videoRecordingPath,
@"maxVideoDuration" : @(maxVideoDuration),
}];

self->_videoRecordingPath = nil;
} else {
result([FlutterError errorWithCode:@"IOError"
message:@"AVAssetWriter could not finish writing!"
details:nil]);
[self->_methodChannel invokeMethod:errorMethod
arguments:@"AVAssetWriter could not finish writing!"];
}
}];
}
} else {
NSError *error =
[NSError errorWithDomain:NSCocoaErrorDomain
code:NSURLErrorResourceUnavailable
userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}];
result(getFlutterError(error));
[self->_methodChannel invokeMethod:errorMethod arguments:@"Video is not recording!"];
}
}

Expand Down Expand Up @@ -1282,9 +1291,15 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re
[_camera setUpCaptureSessionForAudio];
result(nil);
} else if ([@"startVideoRecording" isEqualToString:call.method]) {
[_camera startVideoRecordingWithResult:result];
if ([call.arguments[@"maxVideoDuration"] class] != [NSNull class]) {
[_camera startVideoRecordingWithResult:result
maxVideoDuration:((NSNumber *)call.arguments[@"maxVideoDuration"])
.intValue];
} else {
[_camera startVideoRecordingWithResult:result maxVideoDuration:0];
}
} else if ([@"stopVideoRecording" isEqualToString:call.method]) {
[_camera stopVideoRecordingWithResult:result];
[_camera stopVideoRecording:0];
} else if ([@"pauseVideoRecording" isEqualToString:call.method]) {
[_camera pauseVideoRecordingWithResult:result];
} else if ([@"resumeVideoRecording" isEqualToString:call.method]) {
Expand Down
1 change: 1 addition & 0 deletions packages/camera/camera/lib/camera.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export 'package:camera_platform_interface/camera_platform_interface.dart'
CameraDescription,
CameraException,
CameraLensDirection,
VideoRecordedEvent,
FlashMode,
ExposureMode,
FocusMode,
Expand Down
Loading