According to the docs, you can use CamcorderProfile
to get the devices default video codec format, then set it to MediaRecorder
, like this:
CamcorderProfile mProfile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH);
//
mMediaRecorder.setVideoEncoder(mProfile.videoCodec);
But for some reason it is returning the wrong format.
I'm using the CameraView library and in the FullVideoRecorder class the following is defined:
switch (mResult.getVideoCodec()) {
case H_263: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H263); break;
case H_264: mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); break;
case DEVICE_DEFAULT: mMediaRecorder.setVideoEncoder(mProfile.videoCodec); break;
}
The device I'm experiencing the issue with works perfectly fine when I set the video encoder to H_263
, but for some reason, when I set it to default it crashes - In this case default means that CamcorderProfile
should select the devices default video codec format.
My question:
Is there any reason why CamcorderProfile.videoCodec
would return the wrong value and how can this be resolved?
Edit - adding more information
I implemented the following to make sure if CamcoderProfile
is returning the wrong value:
//In onCreate
CamcorderProfile camcorderProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
//getVideoCodec method below
String profileCodec = getVideoCodec(camcorderProfile.videoCodec);
//Log the result I get
Log.e("Video Codec =", profileCodec);
private String getVideoCodec(int videoCodec){
switch(videoCodec){
case MediaRecorder.VideoEncoder.H263:
return "H263";
case MediaRecorder.VideoEncoder.H264:
return "H264";
case MediaRecorder.VideoEncoder.MPEG_4_SP:
return "MPEG_4_SP";
case MediaRecorder.VideoEncoder.DEFAULT:
return "DEFAULT";
default:
return "unknown";
}
}
In my log I get Video Codec = H264
, but this is incorrect, it should return Video Codec = H263
.
If I pass the following to MediaRecorder
, it works perfectly:
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H263);
but not when I set any of the following:
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
mMediaRecorder.setVideoEncoder(mProfile.videoCodec);
It looks related to an issue found on CameraView library
https://github.com/natario1/CameraView/issues/467
According with Android documentation if the old android.hardware.camera is used then you cannot trust the value returned by the video profile API.
The same issue is present if you are using the new android.hardware.camera2 with INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY mode.
When using the Camera 2 API in LEGACY mode (i.e. when CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL is set to CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY), hasProfile(int) may return true for unsupported resolutions. To ensure a a given resolution is supported in LEGACY mode, the configuration given in CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP must contain the the resolution in the supported output sizes.
camcorder.hasProfile is the method for testing if camcorder profile exists for the given camera at the given quality level.
so before using frame rate and resolutions, they have to be checked.
Supported values can be retrieved using getSupportedVideoSizes, getSupportedPreviewSizes, getSupportedPreviewFpsRange methods
getSupportedVideoSizes gets the supported video frame sizes that can be used by MediaRecorder.
If the returned list is not null, the returned list will contain at least one Size and one of the sizes in the returned list must be passed to MediaRecorder.setVideoSize() for camcorder application if camera is used as the video source. In this case, the size of the preview can be different from the resolution of the recorded video during video recording.
So, maybe we should do a checking for the video sizes and if it is empty then lock the preview size to be equal to recording size.
It seems that the issue is with the library. Let me explain..
After having a look at how OpenCamera implemented their camera, I noticed that they first check if camCoderProfile
has CamcorderProfile.QUALITY...
then setting the profile and passing along the size of the profile, as shown below:
private void initialiseVideoQuality() {
int cameraId = camera_controller.getCameraId();
List<Integer> profiles = new ArrayList<>();
List<VideoQualityHandler.Dimension2D> dimensions = new ArrayList<>();
if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH) ) {
CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH);
profiles.add(CamcorderProfile.QUALITY_HIGH);
dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight));
}
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) {
if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P) ) {
CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P);
profiles.add(CamcorderProfile.QUALITY_2160P);
dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight));
}
}
if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P) ) {
CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P);
profiles.add(CamcorderProfile.QUALITY_1080P);
dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight));
}
if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P) ) {
CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P);
profiles.add(CamcorderProfile.QUALITY_720P);
dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight));
}
if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P) ) {
CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P);
profiles.add(CamcorderProfile.QUALITY_480P);
dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight));
}
if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_CIF) ) {
CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_CIF);
profiles.add(CamcorderProfile.QUALITY_CIF);
dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight));
}
if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA) ) {
CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA);
profiles.add(CamcorderProfile.QUALITY_QVGA);
dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight));
}
if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QCIF) ) {
CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QCIF);
profiles.add(CamcorderProfile.QUALITY_QCIF);
dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight));
}
if( CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW) ) {
CamcorderProfile profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW);
profiles.add(CamcorderProfile.QUALITY_LOW);
dimensions.add(new VideoQualityHandler.Dimension2D(profile.videoFrameWidth, profile.videoFrameHeight));
}
this.video_quality_handler.initialiseVideoQualityFromProfiles(profiles, dimensions);
}
It looks like OpenCamera only changes the video quality from default
/0, if the supported profile width is 1920 and hight is 1080 - I think it is because the camera activity is always in landscape:
if( video_quality_handler.getCurrentVideoQualityIndex() == -1 && video_quality_handler.getSupportedVideoQuality().size() > 0 ) {
video_quality_handler.setCurrentVideoQualityIndex(0); // start with highest quality
//If I log video_quality_handler.getSupportedVideoQuality() here, I get:
//[1, 5_r1440x1080, 5, 4_r960x720, 4_r800x450, 4, 7_r640x480, 7_r480x320, 7_r352x288, 7, 2]
//With 1 being QUALITY_HIGH
//https://developer.android.com/reference/android/media/CamcorderProfile.html#constants_2
for(int i=0;i<video_quality_handler.getSupportedVideoQuality().size();i++) {
CamcorderProfile profile = getCamcorderProfile(video_quality_handler.getSupportedVideoQuality().get(i));
if( profile.videoFrameWidth == 1920 && profile.videoFrameHeight == 1080 ) {
video_quality_handler.setCurrentVideoQualityIndex(i);
break;
}
}
}
private CamcorderProfile getCamcorderProfile(String quality) {
if( camera_controller == null ) {
//Camera is not opened
return CamcorderProfile.get(0, CamcorderProfile.QUALITY_HIGH);
}
int cameraId = camera_controller.getCameraId();
CamcorderProfile camcorder_profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); // default
try {
String profile_string = quality;
int index = profile_string.indexOf('_');
if( index != -1 ) {
profile_string = quality.substring(0, index);
}
int profile = Integer.parseInt(profile_string);
camcorder_profile = CamcorderProfile.get(cameraId, profile);
if( index != -1 && index+1 < quality.length() ) {
String override_string = quality.substring(index+1);
if( override_string.charAt(0) == 'r' && override_string.length() >= 4 ) {
index = override_string.indexOf('x');
if( index == -1 ) {
Log.d(TAG, "override_string invalid format, can't find x");
}
else {
String resolution_w_s = override_string.substring(1, index); // skip first 'r'
String resolution_h_s = override_string.substring(index+1);
// copy to local variable first, so that if we fail to parse height, we don't set the width either
int resolution_w = Integer.parseInt(resolution_w_s);
int resolution_h = Integer.parseInt(resolution_h_s);
camcorder_profile.videoFrameWidth = resolution_w;
camcorder_profile.videoFrameHeight = resolution_h;
}
}
else {
Log.d(TAG, "unknown override_string initial code, or otherwise invalid format");
}
}
}
catch(NumberFormatException e) {
e.printStackTrace();
}
return camcorder_profile;
}
}
For now, I will be using the same implementation OpenCamera used. Since it is licensed under GPLv3, I have change the project to only implement video recording and made the source code available here.