|
|
|
@ -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,99 +40,140 @@ 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); |
|
|
|
|
} |
|
|
|
|
mMediaRecorder.setOutputFormat(mProfile.fileFormat); |
|
|
|
|
|
|
|
|
|
// 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; |
|
|
|
|
switch (mProfile.audioCodec) { |
|
|
|
|
case MediaRecorder.AudioEncoder.AMR_NB: audioType = "audio/3gpp"; break; |
|
|
|
|
case MediaRecorder.AudioEncoder.AMR_WB: audioType = "audio/amr-wb"; break; |
|
|
|
|
case MediaRecorder.AudioEncoder.AAC: |
|
|
|
|
case MediaRecorder.AudioEncoder.HE_AAC: |
|
|
|
|
case MediaRecorder.AudioEncoder.AAC_ELD: audioType = "audio/mp4a-latm"; break; |
|
|
|
|
case MediaRecorder.AudioEncoder.VORBIS: audioType = "audio/vorbis"; break; |
|
|
|
|
case MediaRecorder.AudioEncoder.DEFAULT: |
|
|
|
|
default: audioType = "audio/3gpp"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 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; |
|
|
|
|
// 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; |
|
|
|
|
} |
|
|
|
|
if (stub.videoCodec == VideoCodec.H_263) { |
|
|
|
|
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
|
|
|
|
|
} |
|
|
|
|
switch (mProfile.videoCodec) { |
|
|
|
|
case MediaRecorder.VideoEncoder.H263: videoType = "video/3gpp"; break; |
|
|
|
|
case MediaRecorder.VideoEncoder.H264: videoType = "video/avc"; break; |
|
|
|
|
case MediaRecorder.VideoEncoder.MPEG_4_SP: videoType = "video/mp4v-es"; break; |
|
|
|
|
case MediaRecorder.VideoEncoder.VP8: videoType = "video/x-vnd.on2.vp8"; break; |
|
|
|
|
case MediaRecorder.VideoEncoder.HEVC: videoType = "video/hevc"; break; |
|
|
|
|
case MediaRecorder.VideoEncoder.DEFAULT: |
|
|
|
|
default: videoType = "video/avc"; |
|
|
|
|
} |
|
|
|
|
mMediaRecorder.setOutputFormat(mProfile.fileFormat); |
|
|
|
|
|
|
|
|
|
// 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; |
|
|
|
|
} |
|
|
|
|
// 4. Update the VideoResult stub with information from the profile, if the
|
|
|
|
|
// stub values are absent or incomplete
|
|
|
|
|
if (stub.videoFrameRate <= 0) stub.videoFrameRate = mProfile.videoFrameRate; |
|
|
|
|
if (stub.videoBitRate <= 0) stub.videoBitRate = mProfile.videoBitRate; |
|
|
|
|
if (stub.audioBitRate <= 0 && hasAudio) stub.audioBitRate = mProfile.audioBitRate; |
|
|
|
|
|
|
|
|
|
// Check DeviceEncoders support
|
|
|
|
|
boolean flip = stub.rotation % 180 != 0; |
|
|
|
|
if (flip) stub.size = stub.size.flip(); |
|
|
|
|
Size newVideoSize = null; |
|
|
|
|
int newVideoBitRate = 0; |
|
|
|
|
int newAudioBitRate = 0; |
|
|
|
|
int newVideoFrameRate = 0; |
|
|
|
|
int videoEncoderOffset = 0; |
|
|
|
|
int audioEncoderOffset = 0; |
|
|
|
|
boolean encodersFound = false; |
|
|
|
|
while (!encodersFound) { |
|
|
|
|
DeviceEncoders encoders = new DeviceEncoders(DeviceEncoders.MODE_RESPECT_ORDER, |
|
|
|
|
videoType, audioType, videoEncoderOffset, audioEncoderOffset); |
|
|
|
|
try { |
|
|
|
|
newVideoSize = encoders.getSupportedVideoSize(stub.size); |
|
|
|
|
newVideoBitRate = encoders.getSupportedVideoBitRate(stub.videoBitRate); |
|
|
|
|
newAudioBitRate = encoders.getSupportedAudioBitRate(stub.audioBitRate); |
|
|
|
|
newVideoFrameRate = encoders.getSupportedVideoFrameRate(newVideoSize, |
|
|
|
|
stub.videoFrameRate); |
|
|
|
|
encodersFound = true; |
|
|
|
|
} catch (DeviceEncoders.VideoException videoException) { |
|
|
|
|
videoEncoderOffset++; |
|
|
|
|
} catch (DeviceEncoders.AudioException audioException) { |
|
|
|
|
audioEncoderOffset++; |
|
|
|
|
// 5. 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; |
|
|
|
|
switch (mProfile.audioCodec) { |
|
|
|
|
case MediaRecorder.AudioEncoder.AMR_NB: audioType = "audio/3gpp"; break; |
|
|
|
|
case MediaRecorder.AudioEncoder.AMR_WB: audioType = "audio/amr-wb"; break; |
|
|
|
|
case MediaRecorder.AudioEncoder.AAC: |
|
|
|
|
case MediaRecorder.AudioEncoder.HE_AAC: |
|
|
|
|
case MediaRecorder.AudioEncoder.AAC_ELD: audioType = "audio/mp4a-latm"; break; |
|
|
|
|
case MediaRecorder.AudioEncoder.VORBIS: audioType = "audio/vorbis"; break; |
|
|
|
|
case MediaRecorder.AudioEncoder.DEFAULT: |
|
|
|
|
default: audioType = "audio/3gpp"; |
|
|
|
|
} |
|
|
|
|
// 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; |
|
|
|
|
switch (mProfile.videoCodec) { |
|
|
|
|
case MediaRecorder.VideoEncoder.H263: videoType = "video/3gpp"; break; |
|
|
|
|
case MediaRecorder.VideoEncoder.H264: videoType = "video/avc"; break; |
|
|
|
|
case MediaRecorder.VideoEncoder.MPEG_4_SP: videoType = "video/mp4v-es"; break; |
|
|
|
|
case MediaRecorder.VideoEncoder.VP8: videoType = "video/x-vnd.on2.vp8"; break; |
|
|
|
|
case MediaRecorder.VideoEncoder.HEVC: videoType = "video/hevc"; break; |
|
|
|
|
case MediaRecorder.VideoEncoder.DEFAULT: |
|
|
|
|
default: videoType = "video/avc"; |
|
|
|
|
} |
|
|
|
|
// C. Check DeviceEncoders support
|
|
|
|
|
boolean flip = stub.rotation % 180 != 0; |
|
|
|
|
if (flip) stub.size = stub.size.flip(); |
|
|
|
|
Size newVideoSize = null; |
|
|
|
|
int newVideoBitRate = 0; |
|
|
|
|
int newAudioBitRate = 0; |
|
|
|
|
int newVideoFrameRate = 0; |
|
|
|
|
int videoEncoderOffset = 0; |
|
|
|
|
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 { |
|
|
|
|
newVideoSize = encoders.getSupportedVideoSize(stub.size); |
|
|
|
|
newVideoBitRate = encoders.getSupportedVideoBitRate(stub.videoBitRate); |
|
|
|
|
newAudioBitRate = encoders.getSupportedAudioBitRate(stub.audioBitRate); |
|
|
|
|
newVideoFrameRate = encoders.getSupportedVideoFrameRate(newVideoSize, |
|
|
|
|
stub.videoFrameRate); |
|
|
|
|
encodersFound = true; |
|
|
|
|
} catch (DeviceEncoders.VideoException videoException) { |
|
|
|
|
videoEncoderOffset++; |
|
|
|
|
} catch (DeviceEncoders.AudioException audioException) { |
|
|
|
|
audioEncoderOffset++; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// D. Apply results
|
|
|
|
|
stub.size = newVideoSize; |
|
|
|
|
stub.videoBitRate = newVideoBitRate; |
|
|
|
|
stub.audioBitRate = newAudioBitRate; |
|
|
|
|
stub.videoFrameRate = newVideoFrameRate; |
|
|
|
|
if (flip) stub.size = stub.size.flip(); |
|
|
|
|
} |
|
|
|
|
stub.size = newVideoSize; |
|
|
|
|
stub.videoBitRate = newVideoBitRate; |
|
|
|
|
stub.audioBitRate = newAudioBitRate; |
|
|
|
|
stub.videoFrameRate = newVideoFrameRate; |
|
|
|
|
if (flip) stub.size = stub.size.flip(); |
|
|
|
|
|
|
|
|
|
// Set video params
|
|
|
|
|
// 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 +181,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 +195,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 +221,7 @@ public abstract class FullVideoRecorder extends VideoRecorder { |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Prepare the Recorder
|
|
|
|
|
// 8. Prepare the Recorder
|
|
|
|
|
try { |
|
|
|
|
mMediaRecorder.prepare(); |
|
|
|
|
mMediaRecorderPrepared = true; |
|
|
|
@ -220,13 +261,15 @@ public abstract class FullVideoRecorder extends VideoRecorder { |
|
|
|
|
try { |
|
|
|
|
mMediaRecorder.stop(); |
|
|
|
|
} catch (Exception e) { |
|
|
|
|
LOG.w("stop:", "Error while closing media recorder.", e); |
|
|
|
|
// This can happen if stopVideo() is called right after takeVideo()
|
|
|
|
|
// (in which case we don't care). Or when prepare()/start() have failed for
|
|
|
|
|
// some reason and we are not allowed to call stop.
|
|
|
|
|
// Make sure we don't override the error if one exists already.
|
|
|
|
|
mResult = null; |
|
|
|
|
if (mError == null) mError = e; |
|
|
|
|
if (mError == null) { |
|
|
|
|
LOG.w("stop:", "Error while closing media recorder.", e); |
|
|
|
|
mError = e; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
mMediaRecorder.release(); |
|
|
|
|
} |
|
|
|
|