From 2971c85de588cc7ce75e6ae87f4ffb617c0f58f4 Mon Sep 17 00:00:00 2001 From: huulbaek Date: Thu, 21 Mar 2019 10:09:59 +0100 Subject: [PATCH 01/14] Pause/resume video recording for Android --- .../flutter/plugins/camera/CameraPlugin.java | 38 +++++++++++++ packages/camera/lib/camera.dart | 56 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index a204c1749264..a6209db2d2c1 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -222,6 +222,16 @@ public void onMethodCall(MethodCall call, final Result result) { camera.stopVideoRecording(result); break; } + case "pauseVideoRecording": + { + camera.pauseVideoRecording(result); + break; + } + case "resumeVideoRecording": + { + camera.resumeVideoRecording(result); + break; + } case "startImageStream": { try { @@ -722,6 +732,34 @@ private void stopVideoRecording(@NonNull final Result result) { } } + private void pauseVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + mediaRecorder.pause(); + result.success(null); + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + } + } + + private void resumeVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + mediaRecorder.resume(); + result.success(null); + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + } + } + private void startPreview() throws CameraAccessException { closeCaptureSession(); diff --git a/packages/camera/lib/camera.dart b/packages/camera/lib/camera.dart index 8edbb8c59658..e3733fb5eabd 100644 --- a/packages/camera/lib/camera.dart +++ b/packages/camera/lib/camera.dart @@ -493,6 +493,62 @@ class CameraController extends ValueNotifier { } } + /// Pause recording. + Future pauseVideoRecording() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'pauseVideoRecording was called on uninitialized CameraController', + ); + } + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'pauseVideoRecording was called when no video is recording.', + ); + } + try { + // value = value.copyWith(isRecordingVideo: false); + // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + await _channel.invokeMethod( + 'pauseVideoRecording', + {'textureId': _textureId}, + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resume recording. + Future resumeVideoRecording() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'resumeVideoRecording was called on uninitialized CameraController', + ); + } + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'resumeVideoRecording was called when no video is recording.', + ); + } + try { + // value = value.copyWith(isRecordingVideo: false); + // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + await _channel.invokeMethod( + 'resumeVideoRecording', + {'textureId': _textureId}, + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Releases the resources of this camera. @override Future dispose() async { From 3e87cac7a0421446f2ec4a341a61f243f48a3460 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Thu, 1 Aug 2019 09:22:52 -0700 Subject: [PATCH 02/14] Specify type --- packages/camera/lib/camera.dart | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/camera/lib/camera.dart b/packages/camera/lib/camera.dart index e3733fb5eabd..ee8e2823c794 100644 --- a/packages/camera/lib/camera.dart +++ b/packages/camera/lib/camera.dart @@ -508,11 +508,7 @@ class CameraController extends ValueNotifier { ); } try { - // value = value.copyWith(isRecordingVideo: false); - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - await _channel.invokeMethod( + await _channel.invokeMethod( 'pauseVideoRecording', {'textureId': _textureId}, ); @@ -536,11 +532,7 @@ class CameraController extends ValueNotifier { ); } try { - // value = value.copyWith(isRecordingVideo: false); - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - await _channel.invokeMethod( + await _channel.invokeMethod( 'resumeVideoRecording', {'textureId': _textureId}, ); From 423adcd7f3999c075866cefed4c15e8ac54f83fc Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Tue, 6 Aug 2019 14:22:38 -0700 Subject: [PATCH 03/14] Add pausing and resuming to example app --- packages/camera/example/lib/main.dart | 53 +++++++++++++++++++++++++++ packages/camera/lib/camera.dart | 36 ++++++++++-------- 2 files changed, 73 insertions(+), 16 deletions(-) diff --git a/packages/camera/example/lib/main.dart b/packages/camera/example/lib/main.dart index 70b7ef285796..13d0e4a22dff 100644 --- a/packages/camera/example/lib/main.dart +++ b/packages/camera/example/lib/main.dart @@ -206,6 +206,19 @@ class _CameraExampleHomeState extends State ? onVideoRecordButtonPressed : null, ), + IconButton( + icon: controller != null && controller.value.isRecordingPaused + ? Icon(Icons.play_arrow) + : Icon(Icons.pause), + color: Colors.blue, + onPressed: controller != null && + controller.value.isInitialized && + controller.value.isRecordingVideo + ? (controller != null && controller.value.isRecordingPaused + ? onResumeButtonPressed + : onPauseButtonPressed) + : null, + ), IconButton( icon: const Icon(Icons.stop), color: Colors.red, @@ -308,6 +321,20 @@ class _CameraExampleHomeState extends State }); } + void onPauseButtonPressed() { + pauseVideoRecording().then((_) { + if (mounted) setState(() {}); + showInSnackBar('Video recording paused'); + }); + } + + void onResumeButtonPressed() { + resumeVideoRecording().then((_) { + if (mounted) setState(() {}); + showInSnackBar('Video recording resumed'); + }); + } + Future startVideoRecording() async { if (!controller.value.isInitialized) { showInSnackBar('Error: select a camera first.'); @@ -349,6 +376,32 @@ class _CameraExampleHomeState extends State await _startVideoPlayer(); } + Future pauseVideoRecording() async { + if (!controller.value.isRecordingVideo) { + return null; + } + + try { + await controller.pauseVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + Future resumeVideoRecording() async { + if (!controller.value.isRecordingVideo) { + return null; + } + + try { + await controller.resumeVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + Future _startVideoPlayer() async { final VideoPlayerController vcontroller = VideoPlayerController.file(File(videoPath)); diff --git a/packages/camera/lib/camera.dart b/packages/camera/lib/camera.dart index 2eec2d677b75..e458f582184e 100644 --- a/packages/camera/lib/camera.dart +++ b/packages/camera/lib/camera.dart @@ -130,14 +130,17 @@ class CameraValue { this.isRecordingVideo, this.isTakingPicture, this.isStreamingImages, - }); + bool isRecordingPaused, + }) : _isRecordingPaused = isRecordingPaused; const CameraValue.uninitialized() : this( - isInitialized: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false); + isInitialized: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + isRecordingPaused: false, + ); /// True after [CameraController.initialize] has completed successfully. final bool isInitialized; @@ -151,6 +154,11 @@ class CameraValue { /// True when images from the camera are being streamed. final bool isStreamingImages; + final bool _isRecordingPaused; + + /// True when camera [isRecordingVideo] and recording is paused. + bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused; + final String errorDescription; /// The size of the preview in pixels. @@ -172,6 +180,7 @@ class CameraValue { bool isStreamingImages, String errorDescription, Size previewSize, + bool isRecordingPaused, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -180,6 +189,7 @@ class CameraValue { isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, isTakingPicture: isTakingPicture ?? this.isTakingPicture, isStreamingImages: isStreamingImages ?? this.isStreamingImages, + isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, ); } @@ -446,7 +456,7 @@ class CameraController extends ValueNotifier { 'startVideoRecording', {'textureId': _textureId, 'filePath': filePath}, ); - value = value.copyWith(isRecordingVideo: true); + value = value.copyWith(isRecordingVideo: true, isRecordingPaused: false); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -492,11 +502,8 @@ class CameraController extends ValueNotifier { ); } try { - // value = value.copyWith(isRecordingVideo: false); - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - await _channel.invokeMethod( + value = value.copyWith(isRecordingPaused: true); + await _channel.invokeMethod( 'pauseVideoRecording', {'textureId': _textureId}, ); @@ -520,11 +527,8 @@ class CameraController extends ValueNotifier { ); } try { - // value = value.copyWith(isRecordingVideo: false); - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - await _channel.invokeMethod( + value = value.copyWith(isRecordingPaused: false); + await _channel.invokeMethod( 'resumeVideoRecording', {'textureId': _textureId}, ); From 0214093e26b0cb841d16566495531bb246bca522 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Tue, 6 Aug 2019 14:44:52 -0700 Subject: [PATCH 04/14] iOS side of pausing/resuming --- packages/camera/ios/Classes/CameraPlugin.m | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m index 4c001926ae25..8b8981feabf6 100644 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/ios/Classes/CameraPlugin.m @@ -139,6 +139,7 @@ @interface FLTCam : NSObject *)messenger { if (!_isStreamingImages) { FlutterEventChannel *eventChannel = @@ -725,6 +735,10 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re } else if ([@"stopImageStream" isEqualToString:call.method]) { [_camera stopImageStream]; result(nil); + } else if ([@"pauseVideoRecording" isEqualToString:call.method]) { + [_camera pauseVideoRecording]; + } else if ([@"resumeVideoRecording" isEqualToString:call.method]) { + [_camera resumeVideoRecording]; } else { NSDictionary *argsMap = call.arguments; NSUInteger textureId = ((NSNumber *)argsMap[@"textureId"]).unsignedIntegerValue; From 113cd55eb4ad4865d75a6e0bab292a4b6ee9ab18 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Tue, 6 Aug 2019 17:26:57 -0700 Subject: [PATCH 05/14] More documentation --- packages/camera/lib/camera.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/camera/lib/camera.dart b/packages/camera/lib/camera.dart index e458f582184e..ad565b1f8df8 100644 --- a/packages/camera/lib/camera.dart +++ b/packages/camera/lib/camera.dart @@ -487,7 +487,9 @@ class CameraController extends ValueNotifier { } } - /// Pause recording. + /// Pause video recording. + /// + /// This feature is only available on iOS and Android sdk 24+. Future pauseVideoRecording() async { if (!value.isInitialized || _isDisposed) { throw CameraException( @@ -512,7 +514,9 @@ class CameraController extends ValueNotifier { } } - /// Resume recording. + /// Resume video recording after pausing. + /// + /// This feature is only available on iOS and Android sdk 24+. Future resumeVideoRecording() async { if (!value.isInitialized || _isDisposed) { throw CameraException( From 566454765185a3e50d1d75afc23e7ef66668eb14 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Wed, 7 Aug 2019 12:16:08 -0700 Subject: [PATCH 06/14] Version bump --- packages/camera/CHANGELOG.md | 4 ++++ packages/camera/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md index 2d09af5b8953..e9209dc90b96 100644 --- a/packages/camera/CHANGELOG.md +++ b/packages/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.3 + +* Add feature to pause and resume video recording. + ## 0.5.2+2 * Fix memory leak related to not unregistering stream handler in FlutterEventChannel when disposing camera. diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml index a7e65c706ef1..cd608c9987be 100644 --- a/packages/camera/pubspec.yaml +++ b/packages/camera/pubspec.yaml @@ -2,7 +2,7 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. -version: 0.5.2+2 +version: 0.5.3 authors: - Flutter Team From 3f47c60e587910018562224708fad2cb241e1e50 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Wed, 14 Aug 2019 13:34:19 -0700 Subject: [PATCH 07/14] Add video pausing and resuming --- packages/camera/ios/Classes/CameraPlugin.m | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m index 4ae0c7fa41b8..be6141701ed0 100644 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/ios/Classes/CameraPlugin.m @@ -177,9 +177,13 @@ @interface FLTCam : NSObject 0) { + CFRelease(sampleBuffer); + sampleBuffer = [self adjustTime:sampleBuffer by:_timeOffset]; + } + CMTime lastSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); + CMTime dur = CMSampleBufferGetDuration(sampleBuffer); + + if (output == _captureVideoOutput) { + _lastVideoSampleTime = CMTimeAdd(lastSampleTime, dur); + } else { + _lastAudioSampleTime = CMTimeAdd(lastSampleTime, dur); + } + if (_videoWriter.status != AVAssetWriterStatusWriting) { [_videoWriter startWriting]; [_videoWriter startSessionAtSourceTime:lastSampleTime]; @@ -432,7 +473,24 @@ - (void)captureOutput:(AVCaptureOutput *)output } else if (output == _audioOutput) { [self newAudioSample:sampleBuffer]; } + CFRelease(sampleBuffer); + } +} + +- (CMSampleBufferRef) adjustTime:(CMSampleBufferRef) sample by:(CMTime) offset +{ + CMItemCount count; + CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count); + CMSampleTimingInfo* pInfo = malloc(sizeof(CMSampleTimingInfo) * count); + CMSampleBufferGetSampleTimingInfoArray(sample, count, pInfo, &count); + for (CMItemCount i = 0; i < count; i++) { + pInfo[i].decodeTimeStamp = CMTimeSubtract(pInfo[i].decodeTimeStamp, offset); + pInfo[i].presentationTimeStamp = CMTimeSubtract(pInfo[i].presentationTimeStamp, offset); } + CMSampleBufferRef sout; + CMSampleBufferCreateCopyWithNewTiming(nil, sample, count, pInfo, &sout); + free(pInfo); + return sout; } - (void)newVideoSample:(CMSampleBufferRef)sampleBuffer { @@ -524,6 +582,8 @@ - (void)startVideoRecordingAtPath:(NSString *)path result:(FlutterResult)result } _isRecording = YES; _isRecordingPaused = NO; + _timeOffset = CMTimeMake(0, 0); + _recordingIsDiconnected = NO; result(nil); } else { _eventSink(@{@"event" : @"error", @"errorDescription" : @"Video is already recording!"}); @@ -556,6 +616,7 @@ - (void)stopVideoRecordingWithResult:(FlutterResult)result { - (void)pauseVideoRecording { _isRecordingPaused = YES; + _recordingIsDiconnected = YES; } - (void)resumeVideoRecording { From 5baee78a4da7b77b6a2774e2c625d01a5617dac3 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Thu, 15 Aug 2019 16:37:03 -0700 Subject: [PATCH 08/14] get pausing and recording to work for no audio --- packages/camera/ios/Classes/CameraPlugin.m | 108 +++++++++++++-------- 1 file changed, 65 insertions(+), 43 deletions(-) diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m index be6141701ed0..65ef303ad33e 100644 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/ios/Classes/CameraPlugin.m @@ -177,14 +177,17 @@ @interface FLTCam : NSObject 0) { + _lastVideoSampleTime = CMTimeAdd(currentSampleTime, dur); + } else { + _lastVideoSampleTime = currentSampleTime; + } - if (_timeOffset.value > 0) { - CFRelease(sampleBuffer); - sampleBuffer = [self adjustTime:sampleBuffer by:_timeOffset]; - } + CVPixelBufferRef nextBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CMTime nextSampleTime = CMTimeSubtract(_lastVideoSampleTime, _videoTimeOffset); + [_videoAdaptor appendPixelBuffer:nextBuffer withPresentationTime:nextSampleTime]; + } else { + if (_audioIsDiconnected) { + _audioIsDiconnected = NO; - CMTime lastSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); - CMTime dur = CMSampleBufferGetDuration(sampleBuffer); + if (_audioTimeOffset.value == 0) { + _audioTimeOffset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime); + } else { + CMTime adjustedSampleTime = CMTimeSubtract(currentSampleTime, _audioTimeOffset); + CMTime offset = CMTimeSubtract(adjustedSampleTime, _lastAudioSampleTime); + _audioTimeOffset = CMTimeAdd(_audioTimeOffset, offset); + } + } - if (output == _captureVideoOutput) { - _lastVideoSampleTime = CMTimeAdd(lastSampleTime, dur); - } else { - _lastAudioSampleTime = CMTimeAdd(lastSampleTime, dur); - } + if (dur.value > 0) { + _lastAudioSampleTime = CMTimeAdd(currentSampleTime, dur); + } else { + _lastAudioSampleTime = currentSampleTime; + } + + if (_audioTimeOffset.value != 0) { + CFRelease(sampleBuffer); + sampleBuffer = [self adjustTime:sampleBuffer by:_audioTimeOffset]; + } - if (_videoWriter.status != AVAssetWriterStatusWriting) { - [_videoWriter startWriting]; - [_videoWriter startSessionAtSourceTime:lastSampleTime]; - } - if (output == _captureVideoOutput) { - [self newVideoSample:sampleBuffer]; - } else if (output == _audioOutput) { [self newAudioSample:sampleBuffer]; } + CFRelease(sampleBuffer); } } -- (CMSampleBufferRef) adjustTime:(CMSampleBufferRef) sample by:(CMTime) offset -{ +- (CMSampleBufferRef) adjustTime:(CMSampleBufferRef) sample by:(CMTime) offset { CMItemCount count; CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count); CMSampleTimingInfo* pInfo = malloc(sizeof(CMSampleTimingInfo) * count); @@ -582,8 +596,10 @@ - (void)startVideoRecordingAtPath:(NSString *)path result:(FlutterResult)result } _isRecording = YES; _isRecordingPaused = NO; - _timeOffset = CMTimeMake(0, 0); - _recordingIsDiconnected = NO; + _videoTimeOffset = CMTimeMake(0, 1); + _audioTimeOffset = CMTimeMake(0, 1); + _videoIsDiconnected = NO; + _audioIsDiconnected = NO; result(nil); } else { _eventSink(@{@"event" : @"error", @"errorDescription" : @"Video is already recording!"}); @@ -616,7 +632,8 @@ - (void)stopVideoRecordingWithResult:(FlutterResult)result { - (void)pauseVideoRecording { _isRecordingPaused = YES; - _recordingIsDiconnected = YES; + _videoIsDiconnected = YES; + _audioIsDiconnected = YES; } - (void)resumeVideoRecording { @@ -675,6 +692,11 @@ - (BOOL)setupWriterForPath:(NSString *)path { nil]; _videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings]; + + _videoAdaptor = [AVAssetWriterInputPixelBufferAdaptor + assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_videoWriterInput + sourcePixelBufferAttributes:@{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat)}]; + NSParameterAssert(_videoWriterInput); _videoWriterInput.expectsMediaDataInRealTime = YES; From da26df13f2adbecf29c868f57be6d2fdbd5d3d66 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Thu, 15 Aug 2019 18:51:58 -0700 Subject: [PATCH 09/14] It works --- packages/camera/ios/Classes/CameraPlugin.m | 29 +++++++++++----------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m index 65ef303ad33e..806e21fd8bdc 100644 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/ios/Classes/CameraPlugin.m @@ -432,7 +432,6 @@ - (void)captureOutput:(AVCaptureOutput *)output CFRetain(sampleBuffer); CMTime currentSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); - CMTime dur = CMSampleBufferGetDuration(sampleBuffer); if (_videoWriter.status != AVAssetWriterStatusWriting) { [_videoWriter startWriting]; @@ -446,40 +445,40 @@ - (void)captureOutput:(AVCaptureOutput *)output if (_videoTimeOffset.value == 0) { _videoTimeOffset = CMTimeSubtract(currentSampleTime, _lastVideoSampleTime); } else { - CMTime adjustedSampleTime = CMTimeSubtract(currentSampleTime, _videoTimeOffset); - CMTime offset = CMTimeSubtract(adjustedSampleTime, _lastVideoSampleTime); + CMTime offset = CMTimeSubtract(currentSampleTime, _lastVideoSampleTime); _videoTimeOffset = CMTimeAdd(_videoTimeOffset, offset); } - } - if (dur.value > 0) { - _lastVideoSampleTime = CMTimeAdd(currentSampleTime, dur); - } else { - _lastVideoSampleTime = currentSampleTime; + return; } + _lastVideoSampleTime = currentSampleTime; + CVPixelBufferRef nextBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); CMTime nextSampleTime = CMTimeSubtract(_lastVideoSampleTime, _videoTimeOffset); [_videoAdaptor appendPixelBuffer:nextBuffer withPresentationTime:nextSampleTime]; } else { + CMTime dur = CMSampleBufferGetDuration(sampleBuffer); + + if (dur.value > 0) { + currentSampleTime = CMTimeAdd(currentSampleTime, dur); + } + if (_audioIsDiconnected) { _audioIsDiconnected = NO; if (_audioTimeOffset.value == 0) { _audioTimeOffset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime); } else { - CMTime adjustedSampleTime = CMTimeSubtract(currentSampleTime, _audioTimeOffset); - CMTime offset = CMTimeSubtract(adjustedSampleTime, _lastAudioSampleTime); + CMTime offset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime); _audioTimeOffset = CMTimeAdd(_audioTimeOffset, offset); } - } - if (dur.value > 0) { - _lastAudioSampleTime = CMTimeAdd(currentSampleTime, dur); - } else { - _lastAudioSampleTime = currentSampleTime; + return; } + _lastAudioSampleTime = currentSampleTime; + if (_audioTimeOffset.value != 0) { CFRelease(sampleBuffer); sampleBuffer = [self adjustTime:sampleBuffer by:_audioTimeOffset]; From 542ee3da52255c738b98a501dbf5250745c938f7 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Fri, 16 Aug 2019 11:40:26 -0700 Subject: [PATCH 10/14] Formatting --- packages/camera/ios/Classes/CameraPlugin.m | 30 ++++++++++++---------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m index 806e21fd8bdc..47b669e987f1 100644 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/ios/Classes/CameraPlugin.m @@ -177,8 +177,8 @@ @interface FLTCam : NSObject Date: Mon, 19 Aug 2019 12:37:21 -0700 Subject: [PATCH 11/14] Add test for pausing and resuming --- .../camera/example/test_driver/camera.dart | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/camera/example/test_driver/camera.dart b/packages/camera/example/test_driver/camera.dart index 7d59016ff0b1..e1c0a673b967 100644 --- a/packages/camera/example/test_driver/camera.dart +++ b/packages/camera/example/test_driver/camera.dart @@ -143,4 +143,59 @@ void main() { } } }); + + test('Pause and resume video recording', () async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + ); + + await controller.initialize(); + await controller.prepareForVideoRecording(); + + final String filePath = + '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.mp4'; + + int startPause; + int timePaused = 0; + + await controller.startVideoRecording(filePath); + final int recordingStart = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + + await controller.pauseVideoRecording(); + startPause = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + await controller.resumeVideoRecording(); + timePaused += DateTime.now().millisecondsSinceEpoch - startPause; + + sleep(const Duration(milliseconds: 500)); + + await controller.pauseVideoRecording(); + startPause = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + await controller.resumeVideoRecording(); + timePaused += DateTime.now().millisecondsSinceEpoch - startPause; + + sleep(const Duration(milliseconds: 500)); + + await controller.stopVideoRecording(); + final int recordingTime = + DateTime.now().millisecondsSinceEpoch - recordingStart; + + final File videoFile = File(filePath); + final VideoPlayerController videoController = VideoPlayerController.file( + videoFile, + ); + await videoController.initialize(); + final int duration = videoController.value.duration.inMilliseconds; + await videoController.dispose(); + + expect(duration, lessThan(recordingTime - timePaused)); + }); } From ca47f3856a1fac9a779df7e911e277283e73004d Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Mon, 19 Aug 2019 12:47:51 -0700 Subject: [PATCH 12/14] Call success outside try catch block --- .../src/main/java/io/flutter/plugins/camera/Camera.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index e8760ae76f5b..110c5b690b09 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -396,10 +396,12 @@ public void pauseVideoRecording(@NonNull final Result result) { try { mediaRecorder.pause(); - result.success(null); } catch (IllegalStateException e) { result.error("videoRecordingFailed", e.getMessage(), null); + return; } + + result.success(null); } public void resumeVideoRecording(@NonNull final Result result) { @@ -410,10 +412,12 @@ public void resumeVideoRecording(@NonNull final Result result) { try { mediaRecorder.resume(); - result.success(null); } catch (IllegalStateException e) { result.error("videoRecordingFailed", e.getMessage(), null); + return; } + + result.success(null); } public void startPreview() throws CameraAccessException { From c758b268ee1f05ac779a45705662d23af5f4d152 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Mon, 19 Aug 2019 12:49:19 -0700 Subject: [PATCH 13/14] formatting --- packages/camera/ios/Classes/CameraPlugin.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m index 47b669e987f1..f79766850165 100644 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/ios/Classes/CameraPlugin.m @@ -491,7 +491,7 @@ - (void)captureOutput:(AVCaptureOutput *)output } } -- (CMSampleBufferRef)adjustTime:(CMSampleBufferRef)sample by:(CMTime) offset { +- (CMSampleBufferRef)adjustTime:(CMSampleBufferRef)sample by:(CMTime)offset { CMItemCount count; CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count); CMSampleTimingInfo *pInfo = malloc(sizeof(CMSampleTimingInfo) * count); From 8ff306f13f014a40090ae0f97fd440aa49320023 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Mon, 19 Aug 2019 14:31:10 -0700 Subject: [PATCH 14/14] Disable audio in test and call result on iOS --- packages/camera/example/test_driver/camera.dart | 1 + packages/camera/ios/Classes/CameraPlugin.m | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/camera/example/test_driver/camera.dart b/packages/camera/example/test_driver/camera.dart index e1c0a673b967..d68b8c5ba1fc 100644 --- a/packages/camera/example/test_driver/camera.dart +++ b/packages/camera/example/test_driver/camera.dart @@ -153,6 +153,7 @@ void main() { final CameraController controller = CameraController( cameras[0], ResolutionPreset.low, + enableAudio: false, ); await controller.initialize(); diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m index df65e57647d0..42cdb6d5fdf9 100644 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/ios/Classes/CameraPlugin.m @@ -873,8 +873,10 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re result(nil); } else if ([@"pauseVideoRecording" isEqualToString:call.method]) { [_camera pauseVideoRecording]; + result(nil); } else if ([@"resumeVideoRecording" isEqualToString:call.method]) { [_camera resumeVideoRecording]; + result(nil); } else { NSDictionary *argsMap = call.arguments; NSUInteger textureId = ((NSNumber *)argsMap[@"textureId"]).unsignedIntegerValue;