diff --git a/.github/workflows/emulator_script.sh b/.github/workflows/emulator_script.sh index e2c7d1fb..e472c108 100755 --- a/.github/workflows/emulator_script.sh +++ b/.github/workflows/emulator_script.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash adb logcat -c -adb logcat CameraOrchestrator:I CameraEngine:I CameraView:I CameraCallbacks:I CameraIntegrationTest:I MediaEncoderEngine:I MediaEncoder:I AudioMediaEncoder:I VideoMediaEncoder:I TextureMediaEncoder:I VideoRecorder:I FullVideoRecorder:I SnapshotVideoRecorder:I MessageQueue:W WorkerHandler:I MPEG4Writer:I *:E -v color & +adb logcat CameraOrchestrator:I CameraEngine:I CameraView:I CameraCallbacks:I CameraIntegrationTest:I MediaEncoderEngine:I MediaEncoder:I AudioMediaEncoder:I VideoMediaEncoder:I TextureMediaEncoder:I VideoRecorder:I FullVideoRecorder:I SnapshotVideoRecorder:I MessageQueue:W WorkerHandler:I DeviceEncoders:I MPEG4Writer:I *:E -v color & ./gradlew cameraview:connectedCheck \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java index 33c3244a..3a31c576 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java @@ -1,8 +1,11 @@ package com.otaliastudios.cameraview.internal; import android.annotation.SuppressLint; +import android.media.AudioFormat; +import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecList; +import android.media.MediaFormat; import android.os.Build; import androidx.annotation.NonNull; @@ -49,8 +52,9 @@ import java.util.List; * - MediaCodecList (https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/MediaCodecList.cpp#322) * * To be fair, what {@link android.media.MediaRecorder} does is actually choose the first one - * that configures itself without errors. We currently do not offer this option here. - * TODO add a tryConfigure() step, that throws AudioException/VideoException ? + * that configures itself without errors. We offer this option through + * {@link #tryConfigureVideo(String, Size, int, int)} and + * {@link #tryConfigureAudio(String, int, int, int)}. * * 2. {@link #MODE_PREFER_HARDWARE} * @@ -350,4 +354,63 @@ public class DeviceEncoders { } } + @SuppressLint("NewApi") + public void tryConfigureVideo(@NonNull String mimeType, + @NonNull Size size, + int frameRate, + int bitRate) { + if (mVideoEncoder != null) { + MediaCodec codec = null; + try { + MediaFormat format = MediaFormat.createVideoFormat(mimeType, size.getWidth(), + size.getHeight()); + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, + MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); + format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); + codec = MediaCodec.createByCodecName(mVideoEncoder.getName()); + codec.configure(format, null, null, + MediaCodec.CONFIGURE_FLAG_ENCODE); + } catch (Exception e) { + throw new VideoException("Failed to configure video codec: " + e.getMessage()); + } finally { + if (codec != null) { + try { + codec.release(); + } catch (Exception ignore) {} + } + } + } + } + + @SuppressLint("NewApi") + public void tryConfigureAudio(@NonNull String mimeType, + int bitRate, + int sampleRate, + int channels) { + if (mAudioEncoder != null) { + MediaCodec codec = null; + try { + final MediaFormat format = MediaFormat.createAudioFormat(mimeType, sampleRate, + channels); + int channelMask = channels == 2 ? AudioFormat.CHANNEL_IN_STEREO + : AudioFormat.CHANNEL_IN_MONO; + format.setInteger(MediaFormat.KEY_CHANNEL_MASK, channelMask); + format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); + + codec = MediaCodec.createByCodecName(mAudioEncoder.getName()); + codec.configure(format, null, null, + MediaCodec.CONFIGURE_FLAG_ENCODE); + } catch (Exception e) { + throw new AudioException("Failed to configure video audio: " + e.getMessage()); + } finally { + if (codec != null) { + try { + codec.release(); + } catch (Exception ignore) {} + } + } + } + } + } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java index 51678513..d63dd95a 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java @@ -84,9 +84,15 @@ public abstract class FullVideoRecorder extends VideoRecorder { // 2. Set the video and audio sources. applyVideoSource(stub, mMediaRecorder); - boolean hasAudio = stub.audio == Audio.ON - || stub.audio == Audio.MONO - || stub.audio == Audio.STEREO; + int audioChannels = 0; + if (stub.audio == Audio.ON) { + audioChannels = mProfile.audioChannels; + } else if (stub.audio == Audio.MONO) { + audioChannels = 1; + } else if (stub.audio == Audio.STEREO) { + audioChannels = 2; + } + boolean hasAudio = audioChannels > 0; if (hasAudio) { mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); } @@ -156,13 +162,23 @@ public abstract class FullVideoRecorder extends VideoRecorder { try { newVideoSize = encoders.getSupportedVideoSize(stub.size); newVideoBitRate = encoders.getSupportedVideoBitRate(stub.videoBitRate); - newAudioBitRate = encoders.getSupportedAudioBitRate(stub.audioBitRate); newVideoFrameRate = encoders.getSupportedVideoFrameRate(newVideoSize, stub.videoFrameRate); + encoders.tryConfigureVideo(videoType, newVideoSize, newVideoFrameRate, + newVideoBitRate); + if (hasAudio) { + newAudioBitRate = encoders.getSupportedAudioBitRate(stub.audioBitRate); + encoders.tryConfigureAudio(audioType, newAudioBitRate, + mProfile.audioSampleRate, audioChannels); + } encodersFound = true; } catch (DeviceEncoders.VideoException videoException) { + LOG.i("prepareMediaRecorder:", "Got VideoException:", + videoException.getMessage()); videoEncoderOffset++; } catch (DeviceEncoders.AudioException audioException) { + LOG.i("prepareMediaRecorder:", "Got AudioException:", + audioException.getMessage()); audioEncoderOffset++; } } @@ -185,13 +201,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { // 6B. Configure MediaRecorder from stub and from profile (audio) if (hasAudio) { - if (stub.audio == Audio.ON) { - mMediaRecorder.setAudioChannels(mProfile.audioChannels); - } else if (stub.audio == Audio.MONO) { - mMediaRecorder.setAudioChannels(1); - } else if (stub.audio == Audio.STEREO) { - mMediaRecorder.setAudioChannels(2); - } + mMediaRecorder.setAudioChannels(audioChannels); mMediaRecorder.setAudioSamplingRate(mProfile.audioSampleRate); mMediaRecorder.setAudioEncoder(mProfile.audioCodec); mMediaRecorder.setAudioEncodingBitRate(stub.audioBitRate); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java index 7ef9054d..8fc7dc2f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java @@ -146,6 +146,19 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram case DEVICE_DEFAULT: videoType = "video/avc"; break; } String audioType = "audio/mp4a-latm"; + TextureConfig videoConfig = new TextureConfig(); + AudioConfig audioConfig = new AudioConfig(); + + // See if we have audio + int audioChannels = 0; + if (mResult.audio == Audio.ON) { + audioChannels = audioConfig.channels; + } else if (mResult.audio == Audio.MONO) { + audioChannels = 1; + } else if (mResult.audio == Audio.STEREO) { + audioChannels = 2; + } + boolean hasAudio = audioChannels > 0; // Check the availability of values Size newVideoSize = null; @@ -157,18 +170,30 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram boolean encodersFound = false; DeviceEncoders deviceEncoders = null; while (!encodersFound) { + LOG.i("Checking DeviceEncoders...", + "videoOffset:", videoEncoderOffset, + "audioOffset:", audioEncoderOffset); deviceEncoders = new DeviceEncoders(DeviceEncoders.MODE_PREFER_HARDWARE, videoType, audioType, videoEncoderOffset, audioEncoderOffset); try { newVideoSize = deviceEncoders.getSupportedVideoSize(mResult.size); newVideoBitRate = deviceEncoders.getSupportedVideoBitRate(mResult.videoBitRate); - newAudioBitRate = deviceEncoders.getSupportedAudioBitRate(mResult.audioBitRate); newVideoFrameRate = deviceEncoders.getSupportedVideoFrameRate(newVideoSize, mResult.videoFrameRate); + deviceEncoders.tryConfigureVideo(videoType, newVideoSize, newVideoFrameRate, + newVideoBitRate); + if (hasAudio) { + newAudioBitRate = deviceEncoders + .getSupportedAudioBitRate(mResult.audioBitRate); + deviceEncoders.tryConfigureAudio(audioType, newAudioBitRate, + audioConfig.samplingFrequency, audioChannels); + } encodersFound = true; } catch (DeviceEncoders.VideoException videoException) { + LOG.i("Got VideoException:", videoException.getMessage()); videoEncoderOffset++; } catch (DeviceEncoders.AudioException audioException) { + LOG.i("Got AudioException:", audioException.getMessage()); audioEncoderOffset++; } } @@ -178,7 +203,6 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram mResult.videoFrameRate = newVideoFrameRate; // Video - TextureConfig videoConfig = new TextureConfig(); videoConfig.width = mResult.size.getWidth(); videoConfig.height = mResult.size.getHeight(); videoConfig.bitRate = mResult.videoBitRate; @@ -206,12 +230,9 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram // Audio AudioMediaEncoder audioEncoder = null; - if (mResult.audio == Audio.ON || mResult.audio == Audio.MONO - || mResult.audio == Audio.STEREO) { - AudioConfig audioConfig = new AudioConfig(); + if (hasAudio) { audioConfig.bitRate = mResult.audioBitRate; - if (mResult.audio == Audio.MONO) audioConfig.channels = 1; - if (mResult.audio == Audio.STEREO) audioConfig.channels = 2; + audioConfig.channels = audioChannels; audioConfig.encoder = deviceEncoders.getAudioEncoder(); audioEncoder = new AudioMediaEncoder(audioConfig); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java index b74f74ba..e1fbd848 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java @@ -16,11 +16,11 @@ public class AudioConfig { public int channels = 1; public String encoder; public String mimeType = "audio/mp4a-latm"; + public int samplingFrequency = 44100; // samples/sec // Not configurable options (for now) final int encoding = AudioFormat.ENCODING_PCM_16BIT; // Determines the sampleSizePerChannel // The 44.1KHz frequency is the only setting guaranteed to be available on all devices. - final int samplingFrequency = 44100; // samples/sec // If sampleSizePerChannel changes, review noise introduction final int sampleSizePerChannel = 2; // byte/sample/channel [16bit]. final int byteRatePerChannel = samplingFrequency * sampleSizePerChannel; // byte/sec/channel @@ -28,10 +28,11 @@ public class AudioConfig { @NonNull AudioConfig copy() { AudioConfig config = new AudioConfig(); - config.bitRate = this.bitRate; - config.channels = this.channels; - config.encoder = this.encoder; + config.bitRate = bitRate; + config.channels = channels; + config.encoder = encoder; config.mimeType = mimeType; + config.samplingFrequency = samplingFrequency; return config; }