Skip to content

Commit fb157ec

Browse files
mvanbeusekomEgor
authored andcommitted
[camera] Set audio encoding bitrate when recording video (flutter#3124)
Fixes flutter/flutter#38787
1 parent 71d966a commit fb157ec

File tree

7 files changed

+189
-19
lines changed

7 files changed

+189
-19
lines changed

packages/camera/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.5.8+8
2+
3+
* Fixed garbled audio (in video) by setting audio encoding bitrate.
4+
15
## 0.5.8+7
26

37
* Keep handling deprecated Android v1 classes for backward compatibility.

packages/camera/android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,5 @@ android {
5151

5252
dependencies {
5353
testImplementation 'junit:junit:4.12'
54+
testImplementation 'org.mockito:mockito-core:3.5.13'
5455
}

packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import androidx.annotation.NonNull;
2929
import io.flutter.plugin.common.EventChannel;
3030
import io.flutter.plugin.common.MethodChannel.Result;
31+
import io.flutter.plugins.camera.media.MediaRecorderBuilder;
3132
import io.flutter.view.TextureRegistry.SurfaceTextureEntry;
3233
import java.io.File;
3334
import java.io.FileOutputStream;
@@ -82,7 +83,6 @@ public Camera(
8283
if (activity == null) {
8384
throw new IllegalStateException("No activity available!");
8485
}
85-
8686
this.cameraName = cameraName;
8787
this.enableAudio = enableAudio;
8888
this.flutterTexture = flutterTexture;
@@ -120,23 +120,12 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException {
120120
if (mediaRecorder != null) {
121121
mediaRecorder.release();
122122
}
123-
mediaRecorder = new MediaRecorder();
124-
125-
// There's a specific order that mediaRecorder expects. Do not change the order
126-
// of these function calls.
127-
if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
128-
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
129-
mediaRecorder.setOutputFormat(recordingProfile.fileFormat);
130-
if (enableAudio) mediaRecorder.setAudioEncoder(recordingProfile.audioCodec);
131-
mediaRecorder.setVideoEncoder(recordingProfile.videoCodec);
132-
mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate);
133-
if (enableAudio) mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate);
134-
mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate);
135-
mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
136-
mediaRecorder.setOutputFile(outputFilePath);
137-
mediaRecorder.setOrientationHint(getMediaOrientation());
138-
139-
mediaRecorder.prepare();
123+
124+
mediaRecorder =
125+
new MediaRecorderBuilder(recordingProfile, outputFilePath)
126+
.setEnableAudio(enableAudio)
127+
.setMediaOrientation(getMediaOrientation())
128+
.build();
140129
}
141130

142131
@SuppressLint("MissingPermission")
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2019 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
package io.flutter.plugins.camera.media;
5+
6+
import android.media.CamcorderProfile;
7+
import android.media.MediaRecorder;
8+
import androidx.annotation.NonNull;
9+
import java.io.IOException;
10+
11+
public class MediaRecorderBuilder {
12+
static class MediaRecorderFactory {
13+
MediaRecorder makeMediaRecorder() {
14+
return new MediaRecorder();
15+
}
16+
}
17+
18+
private final String outputFilePath;
19+
private final CamcorderProfile recordingProfile;
20+
private final MediaRecorderFactory recorderFactory;
21+
22+
private boolean enableAudio;
23+
private int mediaOrientation;
24+
25+
public MediaRecorderBuilder(
26+
@NonNull CamcorderProfile recordingProfile, @NonNull String outputFilePath) {
27+
this(recordingProfile, outputFilePath, new MediaRecorderFactory());
28+
}
29+
30+
MediaRecorderBuilder(
31+
@NonNull CamcorderProfile recordingProfile,
32+
@NonNull String outputFilePath,
33+
MediaRecorderFactory helper) {
34+
this.outputFilePath = outputFilePath;
35+
this.recordingProfile = recordingProfile;
36+
this.recorderFactory = helper;
37+
}
38+
39+
public MediaRecorderBuilder setEnableAudio(boolean enableAudio) {
40+
this.enableAudio = enableAudio;
41+
return this;
42+
}
43+
44+
public MediaRecorderBuilder setMediaOrientation(int orientation) {
45+
this.mediaOrientation = orientation;
46+
return this;
47+
}
48+
49+
public MediaRecorder build() throws IOException {
50+
MediaRecorder mediaRecorder = recorderFactory.makeMediaRecorder();
51+
52+
// There's a specific order that mediaRecorder expects. Do not change the order
53+
// of these function calls.
54+
if (enableAudio) {
55+
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
56+
mediaRecorder.setAudioEncodingBitRate(recordingProfile.audioBitRate);
57+
}
58+
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
59+
mediaRecorder.setOutputFormat(recordingProfile.fileFormat);
60+
if (enableAudio) mediaRecorder.setAudioEncoder(recordingProfile.audioCodec);
61+
mediaRecorder.setVideoEncoder(recordingProfile.videoCodec);
62+
mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate);
63+
if (enableAudio) mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate);
64+
mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate);
65+
mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
66+
mediaRecorder.setOutputFile(outputFilePath);
67+
mediaRecorder.setOrientationHint(this.mediaOrientation);
68+
69+
mediaRecorder.prepare();
70+
71+
return mediaRecorder;
72+
}
73+
}

packages/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public void sendCameraClosingEvent() {
9595

9696
private Map<String, String> decodeSentMessage(ByteBuffer sentMessage) {
9797
sentMessage.position(0);
98+
//noinspection unchecked
9899
return (Map<String, String>) StandardMethodCodec.INSTANCE.decodeEnvelope(sentMessage);
99100
}
100101

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package io.flutter.plugins.camera.media;
2+
3+
import static org.junit.Assert.assertNotNull;
4+
import static org.mockito.Mockito.*;
5+
6+
import android.media.CamcorderProfile;
7+
import android.media.MediaRecorder;
8+
import java.io.IOException;
9+
import java.lang.reflect.Constructor;
10+
import org.junit.Test;
11+
import org.mockito.InOrder;
12+
13+
public class MediaRecorderBuilderTest {
14+
@Test
15+
public void ctor_test() {
16+
MediaRecorderBuilder builder =
17+
new MediaRecorderBuilder(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P), "");
18+
19+
assertNotNull(builder);
20+
}
21+
22+
@Test
23+
public void build_Should_set_values_in_correct_order_When_audio_is_disabled() throws IOException {
24+
CamcorderProfile recorderProfile = getEmptyCamcorderProfile();
25+
MediaRecorderBuilder.MediaRecorderFactory mockFactory =
26+
mock(MediaRecorderBuilder.MediaRecorderFactory.class);
27+
MediaRecorder mockMediaRecorder = mock(MediaRecorder.class);
28+
String outputFilePath = "mock_video_file_path";
29+
int mediaOrientation = 1;
30+
MediaRecorderBuilder builder =
31+
new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory)
32+
.setEnableAudio(false)
33+
.setMediaOrientation(mediaOrientation);
34+
35+
when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder);
36+
37+
MediaRecorder recorder = builder.build();
38+
39+
InOrder inOrder = inOrder(recorder);
40+
inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE);
41+
inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat);
42+
inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec);
43+
inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate);
44+
inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate);
45+
inOrder
46+
.verify(recorder)
47+
.setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight);
48+
inOrder.verify(recorder).setOutputFile(outputFilePath);
49+
inOrder.verify(recorder).setOrientationHint(mediaOrientation);
50+
inOrder.verify(recorder).prepare();
51+
}
52+
53+
@Test
54+
public void build_Should_set_values_in_correct_order_When_audio_is_enabled() throws IOException {
55+
CamcorderProfile recorderProfile = getEmptyCamcorderProfile();
56+
MediaRecorderBuilder.MediaRecorderFactory mockFactory =
57+
mock(MediaRecorderBuilder.MediaRecorderFactory.class);
58+
MediaRecorder mockMediaRecorder = mock(MediaRecorder.class);
59+
String outputFilePath = "mock_video_file_path";
60+
int mediaOrientation = 1;
61+
MediaRecorderBuilder builder =
62+
new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory)
63+
.setEnableAudio(true)
64+
.setMediaOrientation(mediaOrientation);
65+
66+
when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder);
67+
68+
MediaRecorder recorder = builder.build();
69+
70+
InOrder inOrder = inOrder(recorder);
71+
inOrder.verify(recorder).setAudioSource(MediaRecorder.AudioSource.MIC);
72+
inOrder.verify(recorder).setAudioEncodingBitRate(recorderProfile.audioBitRate);
73+
inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE);
74+
inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat);
75+
inOrder.verify(recorder).setAudioEncoder(recorderProfile.audioCodec);
76+
inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec);
77+
inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate);
78+
inOrder.verify(recorder).setAudioSamplingRate(recorderProfile.audioSampleRate);
79+
inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate);
80+
inOrder
81+
.verify(recorder)
82+
.setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight);
83+
inOrder.verify(recorder).setOutputFile(outputFilePath);
84+
inOrder.verify(recorder).setOrientationHint(mediaOrientation);
85+
inOrder.verify(recorder).prepare();
86+
}
87+
88+
private CamcorderProfile getEmptyCamcorderProfile() {
89+
try {
90+
Constructor<CamcorderProfile> constructor =
91+
CamcorderProfile.class.getDeclaredConstructor(
92+
int.class, int.class, int.class, int.class, int.class, int.class, int.class,
93+
int.class, int.class, int.class, int.class, int.class);
94+
95+
constructor.setAccessible(true);
96+
return constructor.newInstance(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
97+
} catch (Exception ignored) {
98+
}
99+
100+
return null;
101+
}
102+
}

packages/camera/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: camera
22
description: A Flutter plugin for getting information about and controlling the
33
camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video,
44
and streaming image buffers to dart.
5-
version: 0.5.8+7
5+
version: 0.5.8+8
66

77
homepage: https://github.com/flutter/plugins/tree/master/packages/camera
88

0 commit comments

Comments
 (0)