|
|
|
@ -8,6 +8,7 @@ import com.otaliastudios.cameraview.VideoResult; |
|
|
|
|
import com.otaliastudios.cameraview.controls.Audio; |
|
|
|
|
import com.otaliastudios.cameraview.controls.VideoCodec; |
|
|
|
|
import com.otaliastudios.cameraview.internal.DeviceEncoders; |
|
|
|
|
import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles; |
|
|
|
|
import com.otaliastudios.cameraview.size.Size; |
|
|
|
|
|
|
|
|
|
import androidx.annotation.NonNull; |
|
|
|
@ -17,10 +18,9 @@ import androidx.annotation.Nullable; |
|
|
|
|
* A {@link VideoRecorder} that uses {@link android.media.MediaRecorder} APIs. |
|
|
|
|
* |
|
|
|
|
* When started, the media recorder will be prepared in |
|
|
|
|
* {@link #onPrepareMediaRecorder(VideoResult.Stub, MediaRecorder)}. |
|
|
|
|
* Subclasses should override this method and, before calling super(), do two things: |
|
|
|
|
* - set the media recorder VideoSource |
|
|
|
|
* - define {@link #mProfile} |
|
|
|
|
* {@link #prepareMediaRecorder(VideoResult.Stub)}. This will call two abstract methods: |
|
|
|
|
* - {@link #getCamcorderProfile(VideoResult.Stub)} |
|
|
|
|
* - {@link #applyVideoSource(VideoResult.Stub, MediaRecorder)} |
|
|
|
|
* |
|
|
|
|
* Subclasses can also call {@link #prepareMediaRecorder(VideoResult.Stub)} before start happens, |
|
|
|
|
* in which case it will not be prepared twice. This can be used for example to test some |
|
|
|
@ -32,7 +32,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { |
|
|
|
|
private static final CameraLogger LOG = CameraLogger.create(TAG); |
|
|
|
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess") protected MediaRecorder mMediaRecorder; |
|
|
|
|
@SuppressWarnings("WeakerAccess") protected CamcorderProfile mProfile; |
|
|
|
|
private CamcorderProfile mProfile; |
|
|
|
|
private boolean mMediaRecorderPrepared; |
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -40,24 +40,69 @@ public abstract class FullVideoRecorder extends VideoRecorder { |
|
|
|
|
super(listener); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"}) |
|
|
|
|
protected boolean prepareMediaRecorder(@NonNull VideoResult.Stub stub) { |
|
|
|
|
/** |
|
|
|
|
* Subclasses should return an appropriate CamcorderProfile. |
|
|
|
|
* This could be taken from the {@link CamcorderProfiles} utility class based on the |
|
|
|
|
* stub declared size, for instance. |
|
|
|
|
* |
|
|
|
|
* @param stub the stub |
|
|
|
|
* @return the profile |
|
|
|
|
*/ |
|
|
|
|
@NonNull |
|
|
|
|
protected abstract CamcorderProfile getCamcorderProfile(@NonNull VideoResult.Stub stub); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Subclasses should apply a video source to the given recorder. |
|
|
|
|
* |
|
|
|
|
* @param stub the stub |
|
|
|
|
* @param mediaRecorder the recorder |
|
|
|
|
*/ |
|
|
|
|
protected abstract void applyVideoSource(@NonNull VideoResult.Stub stub, |
|
|
|
|
@NonNull MediaRecorder mediaRecorder); |
|
|
|
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess") |
|
|
|
|
protected final boolean prepareMediaRecorder(@NonNull VideoResult.Stub stub) { |
|
|
|
|
if (mMediaRecorderPrepared) return true; |
|
|
|
|
return onPrepareMediaRecorder(stub, new MediaRecorder()); |
|
|
|
|
// We kind of trust the stub size at this point. It's coming from CameraOptions sizes
|
|
|
|
|
// and it's clipped to be less than CamcorderProfile's highest available profile.
|
|
|
|
|
// However, we still can't trust the developer parameters (e.g. bit rates), and even
|
|
|
|
|
// without them, the camera declared sizes can cause crashes in MediaRecorder (#467, #602).
|
|
|
|
|
// A possible solution was to prepare without checking DeviceEncoders first, and should it
|
|
|
|
|
// fail, prepare again checking them. However, when parameters are wrong, MediaRecorder
|
|
|
|
|
// fails on start() instead of prepare() (start failed -19), so this wouldn't be effective.
|
|
|
|
|
return prepareMediaRecorder(stub, true); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
protected boolean onPrepareMediaRecorder(@NonNull VideoResult.Stub stub, |
|
|
|
|
@NonNull MediaRecorder mediaRecorder) { |
|
|
|
|
mMediaRecorder = mediaRecorder; |
|
|
|
|
@SuppressWarnings("SameParameterValue") |
|
|
|
|
private boolean prepareMediaRecorder(@NonNull VideoResult.Stub stub, |
|
|
|
|
boolean applyEncodersConstraints) { |
|
|
|
|
// 1. Create reference and ask for the CamcorderProfile
|
|
|
|
|
mMediaRecorder = new MediaRecorder(); |
|
|
|
|
mProfile = getCamcorderProfile(stub); |
|
|
|
|
|
|
|
|
|
// 2. Set the video and audio sources.
|
|
|
|
|
applyVideoSource(stub, mMediaRecorder); |
|
|
|
|
boolean hasAudio = stub.audio == Audio.ON |
|
|
|
|
|| stub.audio == Audio.MONO |
|
|
|
|
|| stub.audio == Audio.STEREO; |
|
|
|
|
if (hasAudio) { |
|
|
|
|
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 3. Set the output format. Before, change the profile data if the user
|
|
|
|
|
// has specified a specific codec.
|
|
|
|
|
if (stub.videoCodec == VideoCodec.H_264) { |
|
|
|
|
mProfile.videoCodec = MediaRecorder.VideoEncoder.H264; |
|
|
|
|
mProfile.fileFormat = MediaRecorder.OutputFormat.MPEG_4; |
|
|
|
|
} else if (stub.videoCodec == VideoCodec.H_263) { |
|
|
|
|
mProfile.videoCodec = MediaRecorder.VideoEncoder.H263; |
|
|
|
|
mProfile.fileFormat = MediaRecorder.OutputFormat.MPEG_4; // should work
|
|
|
|
|
} |
|
|
|
|
mMediaRecorder.setOutputFormat(mProfile.fileFormat); |
|
|
|
|
|
|
|
|
|
// Get the audio mime type
|
|
|
|
|
// 4. Update the VideoResult stub with DeviceEncoders constraints
|
|
|
|
|
if (applyEncodersConstraints) { |
|
|
|
|
// A. Get the audio mime type
|
|
|
|
|
// https://android.googlesource.com/platform/frameworks/av/+/master/media/libmediaplayerservice/StagefrightRecorder.cpp#1096
|
|
|
|
|
// https://github.com/MrAlex94/Waterfox-Old/blob/master/media/libstagefright/frameworks/av/media/libstagefright/MediaDefs.cpp
|
|
|
|
|
String audioType; |
|
|
|
@ -71,17 +116,10 @@ public abstract class FullVideoRecorder extends VideoRecorder { |
|
|
|
|
case MediaRecorder.AudioEncoder.DEFAULT: |
|
|
|
|
default: audioType = "audio/3gpp"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Get the video mime type
|
|
|
|
|
// B. Get the video mime type
|
|
|
|
|
// https://android.googlesource.com/platform/frameworks/av/+/master/media/libmediaplayerservice/StagefrightRecorder.cpp#1650
|
|
|
|
|
// https://github.com/MrAlex94/Waterfox-Old/blob/master/media/libstagefright/frameworks/av/media/libstagefright/MediaDefs.cpp
|
|
|
|
|
String videoType; |
|
|
|
|
if (stub.videoCodec == VideoCodec.H_264) { |
|
|
|
|
mProfile.videoCodec = MediaRecorder.VideoEncoder.H264; |
|
|
|
|
} |
|
|
|
|
if (stub.videoCodec == VideoCodec.H_263) { |
|
|
|
|
mProfile.videoCodec = MediaRecorder.VideoEncoder.H263; |
|
|
|
|
} |
|
|
|
|
switch (mProfile.videoCodec) { |
|
|
|
|
case MediaRecorder.VideoEncoder.H263: videoType = "video/3gpp"; break; |
|
|
|
|
case MediaRecorder.VideoEncoder.H264: videoType = "video/avc"; break; |
|
|
|
@ -91,16 +129,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { |
|
|
|
|
case MediaRecorder.VideoEncoder.DEFAULT: |
|
|
|
|
default: videoType = "video/avc"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Merge stub and profile
|
|
|
|
|
stub.videoFrameRate = stub.videoFrameRate > 0 ? stub.videoFrameRate |
|
|
|
|
: mProfile.videoFrameRate; |
|
|
|
|
stub.videoBitRate = stub.videoBitRate > 0 ? stub.videoBitRate : mProfile.videoBitRate; |
|
|
|
|
if (hasAudio) { |
|
|
|
|
stub.audioBitRate = stub.audioBitRate > 0 ? stub.audioBitRate : mProfile.audioBitRate; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Check DeviceEncoders support
|
|
|
|
|
// C. Check DeviceEncoders support
|
|
|
|
|
boolean flip = stub.rotation % 180 != 0; |
|
|
|
|
if (flip) stub.size = stub.size.flip(); |
|
|
|
|
Size newVideoSize = null; |
|
|
|
@ -111,6 +140,9 @@ public abstract class FullVideoRecorder extends VideoRecorder { |
|
|
|
|
int audioEncoderOffset = 0; |
|
|
|
|
boolean encodersFound = false; |
|
|
|
|
while (!encodersFound) { |
|
|
|
|
LOG.i("prepareMediaRecorder:", "Checking DeviceEncoders...", |
|
|
|
|
"videoOffset:", videoEncoderOffset, |
|
|
|
|
"audioOffset:", audioEncoderOffset); |
|
|
|
|
DeviceEncoders encoders = new DeviceEncoders(DeviceEncoders.MODE_RESPECT_ORDER, |
|
|
|
|
videoType, audioType, videoEncoderOffset, audioEncoderOffset); |
|
|
|
|
try { |
|
|
|
@ -126,13 +158,25 @@ public abstract class FullVideoRecorder extends VideoRecorder { |
|
|
|
|
audioEncoderOffset++; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// D. Apply results
|
|
|
|
|
stub.size = newVideoSize; |
|
|
|
|
stub.videoBitRate = newVideoBitRate; |
|
|
|
|
stub.audioBitRate = newAudioBitRate; |
|
|
|
|
stub.videoFrameRate = newVideoFrameRate; |
|
|
|
|
if (flip) stub.size = stub.size.flip(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Set video params
|
|
|
|
|
// 5. Update the VideoResult stub with information from the profile, if the
|
|
|
|
|
// stub values are absent or incomplete
|
|
|
|
|
stub.videoFrameRate = stub.videoFrameRate > 0 ? stub.videoFrameRate |
|
|
|
|
: mProfile.videoFrameRate; |
|
|
|
|
stub.videoBitRate = stub.videoBitRate > 0 ? stub.videoBitRate : mProfile.videoBitRate; |
|
|
|
|
if (hasAudio) { |
|
|
|
|
stub.audioBitRate = stub.audioBitRate > 0 ? stub.audioBitRate : mProfile.audioBitRate; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 6A. Configure MediaRecorder from stub and from profile (video)
|
|
|
|
|
boolean flip = stub.rotation % 180 != 0; |
|
|
|
|
mMediaRecorder.setVideoSize( |
|
|
|
|
flip ? stub.size.getHeight() : stub.size.getWidth(), |
|
|
|
|
flip ? stub.size.getWidth() : stub.size.getHeight()); |
|
|
|
@ -140,7 +184,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { |
|
|
|
|
mMediaRecorder.setVideoEncoder(mProfile.videoCodec); |
|
|
|
|
mMediaRecorder.setVideoEncodingBitRate(stub.videoBitRate); |
|
|
|
|
|
|
|
|
|
// Set audio params
|
|
|
|
|
// 6B. Configure MediaRecorder from stub and from profile (audio)
|
|
|
|
|
if (hasAudio) { |
|
|
|
|
if (stub.audio == Audio.ON) { |
|
|
|
|
mMediaRecorder.setAudioChannels(mProfile.audioChannels); |
|
|
|
@ -154,7 +198,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { |
|
|
|
|
mMediaRecorder.setAudioEncodingBitRate(stub.audioBitRate); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Set other params
|
|
|
|
|
// 7. Set other params
|
|
|
|
|
if (stub.location != null) { |
|
|
|
|
mMediaRecorder.setLocation( |
|
|
|
|
(float) stub.location.getLatitude(), |
|
|
|
@ -180,7 +224,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Prepare the Recorder
|
|
|
|
|
// 8. Prepare the Recorder
|
|
|
|
|
try { |
|
|
|
|
mMediaRecorder.prepare(); |
|
|
|
|
mMediaRecorderPrepared = true; |
|
|
|
|