Skip to content

Commit

Permalink
[camera] Set audio encoding bitrate when recording video (flutter#3124)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvanbeusekom committed Oct 8, 2020
1 parent 53e8f47 commit 66a4b8b
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 19 deletions.
4 changes: 4 additions & 0 deletions packages/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/camera/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ android {

dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:3.5.13'
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -82,7 +83,6 @@ public Camera(
if (activity == null) {
throw new IllegalStateException("No activity available!");
}

this.cameraName = cameraName;
this.enableAudio = enableAudio;
this.flutterTexture = flutterTexture;
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public void sendCameraClosingEvent() {

private Map<String, String> decodeSentMessage(ByteBuffer sentMessage) {
sentMessage.position(0);
//noinspection unchecked
return (Map<String, String>) StandardMethodCodec.INSTANCE.decodeEnvelope(sentMessage);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 1 addition & 1 deletion packages/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 66a4b8b

Please sign in to comment.