[camera] Set audio encoding bitrate when recording video (#3124)
Fixes flutter/flutter#38787
diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md
index 8177467..fe5070d 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 3ff98a8..13495b2 100644
--- a/packages/camera/android/build.gradle
+++ b/packages/camera/android/build.gradle
@@ -51,4 +51,5 @@
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 0fcda27..63e4d03 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 @@
if (activity == null) {
throw new IllegalStateException("No activity available!");
}
-
this.cameraName = cameraName;
this.enableAudio = enableAudio;
this.flutterTexture = flutterTexture;
@@ -120,23 +120,12 @@
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 0000000..57dc6e6
--- /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 db89eb2..c5ea83a 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 @@
private Map<String, String> decodeSentMessage(ByteBuffer sentMessage) {
sentMessage.position(0);
+ //noinspection unchecked
return (Map<String, String>) 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 0000000..f60e85d
--- /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<CamcorderProfile> 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 cbe1b1b..64cae9b 100644
--- a/packages/camera/pubspec.yaml
+++ b/packages/camera/pubspec.yaml
@@ -2,7 +2,7 @@
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