diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md index 81774679389d..fe5070d605e5 100644 --- a/packages/camera/CHANGELOG.md +++ b/packages/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.8+8 + +* Fixed garbled audio (in video) by setting audio encoding bitrate. + ## 0.5.8+7 * Keep handling deprecated Android v1 classes for backward compatibility. diff --git a/packages/camera/android/build.gradle b/packages/camera/android/build.gradle index 3ff98a8d7f47..13495b2bd29d 100644 --- a/packages/camera/android/build.gradle +++ b/packages/camera/android/build.gradle @@ -51,4 +51,5 @@ android { dependencies { testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:3.5.13' } 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 0fcda278d836..63e4d03e982a 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 @@ -28,6 +28,7 @@ import androidx.annotation.NonNull; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.media.MediaRecorderBuilder; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; import java.io.FileOutputStream; @@ -82,7 +83,6 @@ public Camera( if (activity == null) { throw new IllegalStateException("No activity available!"); } - this.cameraName = cameraName; this.enableAudio = enableAudio; this.flutterTexture = flutterTexture; @@ -120,23 +120,12 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { if (mediaRecorder != null) { mediaRecorder.release(); } - mediaRecorder = new MediaRecorder(); - - // There's a specific order that mediaRecorder expects. Do not change the order - // of these function calls. - if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); - mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); - mediaRecorder.setOutputFormat(recordingProfile.fileFormat); - if (enableAudio) mediaRecorder.setAudioEncoder(recordingProfile.audioCodec); - mediaRecorder.setVideoEncoder(recordingProfile.videoCodec); - mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate); - if (enableAudio) mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate); - mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate); - mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - mediaRecorder.setOutputFile(outputFilePath); - mediaRecorder.setOrientationHint(getMediaOrientation()); - - mediaRecorder.prepare(); + + mediaRecorder = + new MediaRecorderBuilder(recordingProfile, outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation(getMediaOrientation()) + .build(); } @SuppressLint("MissingPermission") diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java new file mode 100644 index 000000000000..57dc6e633408 --- /dev/null +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -0,0 +1,73 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +package io.flutter.plugins.camera.media; + +import android.media.CamcorderProfile; +import android.media.MediaRecorder; +import androidx.annotation.NonNull; +import java.io.IOException; + +public class MediaRecorderBuilder { + static class MediaRecorderFactory { + MediaRecorder makeMediaRecorder() { + return new MediaRecorder(); + } + } + + private final String outputFilePath; + private final CamcorderProfile recordingProfile; + private final MediaRecorderFactory recorderFactory; + + private boolean enableAudio; + private int mediaOrientation; + + public MediaRecorderBuilder( + @NonNull CamcorderProfile recordingProfile, @NonNull String outputFilePath) { + this(recordingProfile, outputFilePath, new MediaRecorderFactory()); + } + + MediaRecorderBuilder( + @NonNull CamcorderProfile recordingProfile, + @NonNull String outputFilePath, + MediaRecorderFactory helper) { + this.outputFilePath = outputFilePath; + this.recordingProfile = recordingProfile; + this.recorderFactory = helper; + } + + public MediaRecorderBuilder setEnableAudio(boolean enableAudio) { + this.enableAudio = enableAudio; + return this; + } + + public MediaRecorderBuilder setMediaOrientation(int orientation) { + this.mediaOrientation = orientation; + return this; + } + + public MediaRecorder build() throws IOException { + MediaRecorder mediaRecorder = recorderFactory.makeMediaRecorder(); + + // There's a specific order that mediaRecorder expects. Do not change the order + // of these function calls. + if (enableAudio) { + mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + mediaRecorder.setAudioEncodingBitRate(recordingProfile.audioBitRate); + } + mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); + mediaRecorder.setOutputFormat(recordingProfile.fileFormat); + if (enableAudio) mediaRecorder.setAudioEncoder(recordingProfile.audioCodec); + mediaRecorder.setVideoEncoder(recordingProfile.videoCodec); + mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate); + if (enableAudio) mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate); + mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate); + mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + mediaRecorder.setOutputFile(outputFilePath); + mediaRecorder.setOrientationHint(this.mediaOrientation); + + mediaRecorder.prepare(); + + return mediaRecorder; + } +} diff --git a/packages/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index db89eb279f41..c5ea83aa058a 100644 --- a/packages/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -95,6 +95,7 @@ public void sendCameraClosingEvent() { private Map decodeSentMessage(ByteBuffer sentMessage) { sentMessage.position(0); + //noinspection unchecked return (Map) StandardMethodCodec.INSTANCE.decodeEnvelope(sentMessage); } diff --git a/packages/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java new file mode 100644 index 000000000000..f60e85de3903 --- /dev/null +++ b/packages/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -0,0 +1,102 @@ +package io.flutter.plugins.camera.media; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.*; + +import android.media.CamcorderProfile; +import android.media.MediaRecorder; +import java.io.IOException; +import java.lang.reflect.Constructor; +import org.junit.Test; +import org.mockito.InOrder; + +public class MediaRecorderBuilderTest { + @Test + public void ctor_test() { + MediaRecorderBuilder builder = + new MediaRecorderBuilder(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P), ""); + + assertNotNull(builder); + } + + @Test + public void build_Should_set_values_in_correct_order_When_audio_is_disabled() throws IOException { + CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(false) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + + MediaRecorder recorder = builder.build(); + + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat); + inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec); + inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate); + inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate); + inOrder + .verify(recorder) + .setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); + } + + @Test + public void build_Should_set_values_in_correct_order_When_audio_is_enabled() throws IOException { + CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(true) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + + MediaRecorder recorder = builder.build(); + + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setAudioSource(MediaRecorder.AudioSource.MIC); + inOrder.verify(recorder).setAudioEncodingBitRate(recorderProfile.audioBitRate); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat); + inOrder.verify(recorder).setAudioEncoder(recorderProfile.audioCodec); + inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec); + inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate); + inOrder.verify(recorder).setAudioSamplingRate(recorderProfile.audioSampleRate); + inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate); + inOrder + .verify(recorder) + .setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); + } + + private CamcorderProfile getEmptyCamcorderProfile() { + try { + Constructor constructor = + CamcorderProfile.class.getDeclaredConstructor( + int.class, int.class, int.class, int.class, int.class, int.class, int.class, + int.class, int.class, int.class, int.class, int.class); + + constructor.setAccessible(true); + return constructor.newInstance(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } catch (Exception ignored) { + } + + return null; + } +} diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml index cbe1b1b6d9dd..64cae9be5ae3 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.8+7 +version: 0.5.8+8 homepage: https://github.com/flutter/plugins/tree/master/packages/camera