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

[camera] Max video duration #3519

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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.7.1

* Implemented option to provide a maxVideoDuration to limit a video recording in time.

## 0.7.0+3

* Clockwise rotation of focus point in android
Expand Down
12 changes: 11 additions & 1 deletion 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,16 @@ As of version [0.5.0](https://github.com/flutter/plugins/blob/master/packages/ca
}
```

As of version [0.7.1](https://github.com/flutter/plugins/blob/master/packages/camera/camera/CHANGELOG.md#071) the startVideoRecording method can be used with the maxVideoDuration parameter. To do this the result of the recording needs to be retrieved by listening to controller.onVideoRecordedEvent which yields a VideoRecordedEvent when the recording is finished. Like so:
```dart
recordVideo() async {
controller.onVideoRecordedEvent().listen((VideoRecordedEvent event) {
// Handle VideoRecordedEvent
});
await controller.startVideoRecording(maxVideoDuration: const Duration(seconds: 5));
}
```

### Example

Here is a small example flutter app displaying a full screen camera preview.
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 io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize;

import android.annotation.SuppressLint;
Expand Down Expand Up @@ -105,6 +106,7 @@ public class Camera {
private boolean useAutoFocus = true;
private Range<Integer> fpsRange;
private PlatformChannel.DeviceOrientation lockedCaptureOrientation;
private Integer maxDurationLimit;

private static final HashMap<String, Integer> supportedImageFormats;
// Current supported outputs
Expand Down Expand Up @@ -178,19 +180,24 @@ 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(
lockedCaptureOrientation == null
? deviceOrientationListener.getMediaOrientation()
: deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation))
.build();
: deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation));

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

@SuppressLint("MissingPermission")
Expand Down Expand Up @@ -609,8 +616,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 @@ -619,10 +627,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 @@ -631,6 +656,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 @@ -639,19 +676,12 @@ 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();
resetCaptureSession();
dartMessenger.sendVideoRecordedEvent(videoRecordingFile.getAbsolutePath(), maxDurationLimit);
maxDurationLimit = null;
result.success(videoRecordingFile.getAbsolutePath());
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 @@ -32,7 +32,8 @@ enum DeviceEventType {
enum CameraEventType {
ERROR("error"),
CLOSING("camera_closing"),
INITIALIZED("initialized");
INITIALIZED("initialized"),
VIDEO_RECORDED("video_recorded");

private final String method;

Expand Down Expand Up @@ -98,6 +99,17 @@ void sendCameraErrorEvent(@Nullable String description) {
});
}

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

void send(CameraEventType eventType) {
send(eventType, new HashMap<>());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,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
3 changes: 2 additions & 1 deletion packages/camera/camera/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,8 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
}

try {
await controller.startVideoRecording();
await controller.startVideoRecording(
maxVideoDuration: null);
} on CameraException catch (e) {
_showCameraException(e);
return;
Expand Down
48 changes: 39 additions & 9 deletions packages/camera/camera/ios/Classes/CameraPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,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 @@ -797,6 +798,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 stopVideoRecordingWithResult:nil maxVideoDuration:maxVideoDuration];
}
});
}
_isRecording = YES;
_isRecordingPaused = NO;
_videoTimeOffset = CMTimeMake(0, 1);
Expand All @@ -809,18 +818,30 @@ - (void)startVideoRecordingWithResult:(FlutterResult)result {
}
}

- (void)stopVideoRecordingWithResult:(FlutterResult)result {
- (void)stopVideoRecordingWithResult:(FlutterResult)result
maxVideoDuration:(int64_t)maxVideoDuration {
if (_isRecording) {
_isRecording = NO;
if (_videoWriter.status != AVAssetWriterStatusUnknown) {
[_videoWriter finishWritingWithCompletionHandler:^{
if (self->_videoWriter.status == AVAssetWriterStatusCompleted) {
result(self->_videoRecordingPath);
if (result != nil) {
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]);
if (result != nil) {
result([FlutterError errorWithCode:@"IOError"
message:@"AVAssetWriter could not finish writing!"
details:nil]);
}
[self->_methodChannel invokeMethod:errorMethod
arguments:@"AVAssetWriter could not finish writing!"];
}
}];
}
Expand All @@ -829,7 +850,10 @@ - (void)stopVideoRecordingWithResult:(FlutterResult)result {
[NSError errorWithDomain:NSCocoaErrorDomain
code:NSURLErrorResourceUnavailable
userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}];
result(getFlutterError(error));
if (result != nil) {
result(getFlutterError(error));
}
[self->_methodChannel invokeMethod:errorMethod arguments:@"Video is not recording!"];
}
}

Expand Down Expand Up @@ -1385,9 +1409,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 stopVideoRecordingWithResult:result maxVideoDuration: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
24 changes: 22 additions & 2 deletions packages/camera/camera/lib/src/camera_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ class CameraController extends ValueNotifier<CameraValue> {
///
/// The video is returned as a [XFile] after calling [stopVideoRecording].
/// Throws a [CameraException] if the capture fails.
Future<void> startVideoRecording() async {
Future<void> startVideoRecording({Duration maxVideoDuration}) async {
_throwIfNotInitialized("startVideoRecording");
if (value.isRecordingVideo) {
throw CameraException(
Expand All @@ -446,12 +446,19 @@ class CameraController extends ValueNotifier<CameraValue> {
}

try {
await CameraPlatform.instance.startVideoRecording(_cameraId);
await CameraPlatform.instance.startVideoRecording(
_cameraId,
maxVideoDuration: maxVideoDuration,
);
value = value.copyWith(
isRecordingVideo: true,
isRecordingPaused: false,
recordingOrientation: Optional.fromNullable(
value.lockedCaptureOrientation ?? value.deviceOrientation));
// Listen for VideoRecordedEvent to reset isRecordingVideo
CameraPlatform.instance.onVideoRecordedEvent(_cameraId).listen((event) {
value = value.copyWith(isRecordingVideo: false);
});
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
Expand Down Expand Up @@ -518,6 +525,19 @@ class CameraController extends ValueNotifier<CameraValue> {
}
}

/// Stream yielding a [VideoRecordedEvent] every time a video stops recording
Stream<VideoRecordedEvent> onVideoRecordedEvent() {
if (!value.isInitialized || _isDisposed) {
throw CameraException(
'Uninitialized CameraController',
'cameraTimeLimitReachedEventStream was called on uninitialized CameraController',
);
}
return CameraPlatform.instance
.onVideoRecordedEvent(_cameraId)
.asBroadcastStream();
}

/// Returns a widget showing a live camera preview.
Widget buildPreview() {
_throwIfNotInitialized("buildPreview");
Expand Down
Loading