blob: 0a397e8cceb5f02522e986b4b8ecdd82be98c9dc [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.cts.verifier.camera.video;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.graphics.Matrix;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Size;
import android.media.CamcorderProfile;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.VideoView;
import com.android.cts.verifier.PassFailButtons;
import com.android.cts.verifier.R;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.TreeSet;
/**
* Tests for manual verification of camera video capture
*/
public class CameraVideoActivity extends PassFailButtons.Activity
implements TextureView.SurfaceTextureListener {
private static final String TAG = "CtsCameraVideo";
private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
private static final int MEDIA_TYPE_IMAGE = 1;
private static final int MEDIA_TYPE_VIDEO = 2;
private static final int VIDEO_LENGTH = 3000; // in ms
private TextureView mPreviewView;
private SurfaceTexture mPreviewTexture;
private int mPreviewTexWidth;
private int mPreviewTexHeight;
private int mPreviewRotation;
private int mVideoRotation;
private VideoView mPlaybackView;
private Spinner mCameraSpinner;
private Spinner mResolutionSpinner;
private int mCurrentCameraId = -1;
private Camera mCamera;
private MediaRecorder mMediaRecorder;
private List<Size> mPreviewSizes;
private Size mNextPreviewSize;
private Size mPreviewSize;
private List<Integer> mVideoSizeIds;
private int mCurrentVideoSizeId;
private boolean isRecording = false;
private boolean isPlayingBack = false;
private Button captureButton;
private ImageButton mPassButton;
private ImageButton mFailButton;
private TextView mStatusLabel;
private TreeSet<String> mTestedCombinations = new TreeSet<String>();
private TreeSet<String> mUntestedCombinations = new TreeSet<String>();
private File outputVideoFile;
/**
* @see #MEDIA_TYPE_IMAGE
* @see #MEDIA_TYPE_VIDEO
*/
private static File getOutputMediaFile(int type) {
// Question: why do I need to comment this to get it working?
// Logcat says "external storage not ready"
// if (Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED) {
// Log.e(TAG, "external storage not ready");
// return null;
// }
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_MOVIES), TAG);
if (!mediaStorageDir.exists()) {
if (!mediaStorageDir.mkdirs()) {
Log.d(TAG, "failed to create directory");
return null;
}
}
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
File mediaFile;
if (type == MEDIA_TYPE_IMAGE) {
mediaFile = new File(mediaStorageDir.getPath() + File.separator +
"IMG_" + timeStamp + ".jpg");
} else if (type == MEDIA_TYPE_VIDEO) {
mediaFile = new File(mediaStorageDir.getPath() + File.separator +
"VID_" + timeStamp + ".mp4");
if (VERBOSE) {
Log.v(TAG, "getOutputMediaFile: output file " + mediaFile.getPath());
}
} else {
return null;
}
return mediaFile;
}
private boolean prepareVideoRecorder() {
mMediaRecorder = new MediaRecorder();
// Step 1: unlock and set camera to MediaRecorder
mCamera.unlock();
mMediaRecorder.setCamera(mCamera);
// Step 2: set sources
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
// Step 3: set a CamcorderProfile
mMediaRecorder.setProfile(CamcorderProfile.get(mCurrentCameraId, mCurrentVideoSizeId));
// Step 4: set output file
outputVideoFile = getOutputMediaFile(MEDIA_TYPE_VIDEO);
mMediaRecorder.setOutputFile(outputVideoFile.toString());
// Step 5: set preview output
// This is not necessary since preview has been taken care of
// Step 6: set orientation hint
mMediaRecorder.setOrientationHint(mVideoRotation);
// Step 7: prepare configured MediaRecorder
try {
mMediaRecorder.prepare();
} catch (IOException e) {
Log.e(TAG, "IOException preparing MediaRecorder: ", e);
releaseMediaRecorder();
throw new AssertionError(e);
}
mMediaRecorder.setOnErrorListener(
new MediaRecorder.OnErrorListener() {
@Override
public void onError(MediaRecorder mr, int what, int extra) {
if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) {
Log.e(TAG, "unknown error in media recorder, error: " + extra);
} else {
Log.e(TAG, "media recorder server died, error: " + extra);
}
failTest("Media recorder error.");
}
});
if (VERBOSE) {
Log.v(TAG, "prepareVideoRecorder: prepared configured MediaRecorder");
}
return true;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.camera_video);
setPassFailButtonClickListeners();
setInfoResources(R.string.camera_video, R.string.video_info, /*viewId*/-1);
mPreviewView = (TextureView) findViewById(R.id.video_capture);
mPlaybackView = (VideoView) findViewById(R.id.video_playback);
mPlaybackView.setOnCompletionListener(mPlaybackViewListener);
captureButton = (Button) findViewById(R.id.record_button);
mPassButton = (ImageButton) findViewById(R.id.pass_button);
mFailButton = (ImageButton) findViewById(R.id.fail_button);
mPassButton.setEnabled(false);
mFailButton.setEnabled(true);
mPreviewView.setSurfaceTextureListener(this);
int numCameras = Camera.getNumberOfCameras();
String[] cameraNames = new String[numCameras];
for (int i = 0; i < numCameras; i++) {
cameraNames[i] = "Camera " + i;
mUntestedCombinations.add("All combinations for Camera " + i + "\n");
}
if (VERBOSE) {
Log.v(TAG, "onCreate: number of cameras=" + numCameras);
}
mCameraSpinner = (Spinner) findViewById(R.id.cameras_selection);
mCameraSpinner.setAdapter(
new ArrayAdapter<String>(
this, R.layout.cf_format_list_item, cameraNames));
mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener);
mResolutionSpinner = (Spinner) findViewById(R.id.resolution_selection);
mResolutionSpinner.setOnItemSelectedListener(mResolutionSelectedListener);
mStatusLabel = (TextView) findViewById(R.id.status_label);
}
@Override
public void onResume() {
super.onResume();
setUpCamera(mCameraSpinner.getSelectedItemPosition());
if (VERBOSE) {
Log.v(TAG, "onResume: camera has been setup");
}
setUpCaptureButton();
if (VERBOSE) {
Log.v(TAG, "onResume: captureButton has been setup");
}
}
@Override
public void onPause() {
super.onPause();
releaseMediaRecorder();
shutdownCamera();
mPreviewTexture = null;
}
private MediaPlayer.OnCompletionListener mPlaybackViewListener =
new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
isPlayingBack = false;
mPlaybackView.stopPlayback();
captureButton.setEnabled(true);
mStatusLabel.setText(getResources().getString(R.string.status_ready));
}
};
private void releaseMediaRecorder() {
if (mMediaRecorder != null) {
mMediaRecorder.reset();
mMediaRecorder.release();
mMediaRecorder = null;
mCamera.lock(); // check here, lock camera for later use
}
}
@Override
public String getTestDetails() {
StringBuilder reportBuilder = new StringBuilder();
reportBuilder.append("Tested combinations:\n");
for (String combination : mTestedCombinations) {
reportBuilder.append(combination);
}
reportBuilder.append("Untested combinations:\n");
for (String combination : mUntestedCombinations) {
reportBuilder.append(combination);
}
return reportBuilder.toString();
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface,
int width, int height) {
mPreviewTexture = surface;
mPreviewTexWidth = width;
mPreviewTexHeight = height;
if (mCamera != null) {
startPreview();
}
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
// Ignored, Camera does all the work for us
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
// Invoked every time there's a new Camera preview frame
}
private AdapterView.OnItemSelectedListener mCameraSpinnerListener =
new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent,
View view, int pos, long id) {
if (mCurrentCameraId != pos) {
setUpCamera(pos);
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
// Intentionally left blank
}
};
private AdapterView.OnItemSelectedListener mResolutionSelectedListener =
new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent,
View view, int position, long id) {
if (mVideoSizeIds.get(position) != mCurrentVideoSizeId) {
mCurrentVideoSizeId = mVideoSizeIds.get(position);
if (VERBOSE) {
Log.v(TAG, "onItemSelected: mCurrentVideoSizeId = " +
mCurrentVideoSizeId);
}
mNextPreviewSize = matchPreviewRecordSize();
if (VERBOSE) {
Log.v(TAG, "onItemSelected: setting preview size "
+ mNextPreviewSize.width + "x" + mNextPreviewSize.height);
}
startPreview();
if (VERBOSE) {
Log.v(TAG, "onItemSelected: started new preview");
}
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
// Intentionally left blank
}
};
private void setUpCaptureButton() {
captureButton.setOnClickListener (
new View.OnClickListener() {
@Override
public void onClick(View V) {
if ((!isRecording) && (!isPlayingBack)) {
if (prepareVideoRecorder()) {
mMediaRecorder.start();
if (VERBOSE) {
Log.v(TAG, "onClick: started mMediaRecorder");
}
isRecording = true;
captureButton.setEnabled(false);
mStatusLabel.setText(getResources()
.getString(R.string.status_recording));
} else {
releaseMediaRecorder();
Log.e(TAG, "media recorder cannot be set up");
failTest("Unable to set up media recorder.");
}
Handler h = new Handler();
Runnable mDelayedPreview = new Runnable() {
@Override
public void run() {
mMediaRecorder.stop();
releaseMediaRecorder();
mPlaybackView.setVideoPath(outputVideoFile.getPath());
mPlaybackView.start();
isRecording = false;
isPlayingBack = true;
mStatusLabel.setText(getResources()
.getString(R.string.status_playback));
String combination = "Camera " + mCurrentCameraId + ", " +
mCurrentVideoSizeId + "\n";
mUntestedCombinations.remove(combination);
mTestedCombinations.add(combination);
if (mUntestedCombinations.isEmpty()) {
mPassButton.setEnabled(true);
if (VERBOSE) {
Log.v(TAG, "run: test success");
}
}
}
};
h.postDelayed(mDelayedPreview, VIDEO_LENGTH);
}
}
}
);
}
private class VideoSizeNamePair {
private int sizeId;
private String sizeName;
public VideoSizeNamePair(int id, String name) {
sizeId = id;
sizeName = name;
}
public int getSizeId() {
return sizeId;
}
public String getSizeName() {
return sizeName;
}
}
private ArrayList<VideoSizeNamePair> getVideoSizeNamePairs(int cameraId) {
int[] qualityArray = {
CamcorderProfile.QUALITY_LOW,
CamcorderProfile.QUALITY_HIGH,
CamcorderProfile.QUALITY_QCIF,
CamcorderProfile.QUALITY_QVGA,
CamcorderProfile.QUALITY_CIF,
CamcorderProfile.QUALITY_480P,
CamcorderProfile.QUALITY_720P,
CamcorderProfile.QUALITY_1080P,
CamcorderProfile.QUALITY_2160P
};
String[] nameArray = {
"LOW",
"HIGH",
"QCIF",
"QVGA",
"CIF",
"480P",
"720P",
"1080P",
"2160P"
};
ArrayList<VideoSizeNamePair> availableSizes =
new ArrayList<VideoSizeNamePair> ();
for (int i = 0; i < qualityArray.length; i++) {
if (CamcorderProfile.hasProfile(cameraId, qualityArray[i])) {
VideoSizeNamePair pair = new VideoSizeNamePair(qualityArray[i], nameArray[i]);
availableSizes.add(pair);
}
}
return availableSizes;
}
static class ResolutionQuality {
private int videoSizeId;
private int width;
private int height;
public ResolutionQuality() {
// intentionally left blank
}
public ResolutionQuality(int newSizeId, int newWidth, int newHeight) {
videoSizeId = newSizeId;
width = newWidth;
height = newHeight;
}
}
private Size findRecordSize(int cameraId) {
int[] possibleQuality = {
CamcorderProfile.QUALITY_LOW,
CamcorderProfile.QUALITY_HIGH,
CamcorderProfile.QUALITY_QCIF,
CamcorderProfile.QUALITY_QVGA,
CamcorderProfile.QUALITY_CIF,
CamcorderProfile.QUALITY_480P,
CamcorderProfile.QUALITY_720P,
CamcorderProfile.QUALITY_1080P,
CamcorderProfile.QUALITY_2160P
};
ArrayList<ResolutionQuality> qualityList = new ArrayList<ResolutionQuality>();
for (int i = 0; i < possibleQuality.length; i++) {
if (CamcorderProfile.hasProfile(cameraId, possibleQuality[i])) {
CamcorderProfile profile = CamcorderProfile.get(cameraId, possibleQuality[i]);
qualityList.add(new ResolutionQuality(possibleQuality[i],
profile.videoFrameWidth, profile.videoFrameHeight));
}
}
Size recordSize = null;
for (int i = 0; i < qualityList.size(); i++) {
if (mCurrentVideoSizeId == qualityList.get(i).videoSizeId) {
recordSize = mCamera.new Size(qualityList.get(i).width,
qualityList.get(i).height);
break;
}
}
if (recordSize == null) {
Log.e(TAG, "findRecordSize: did not find a match");
failTest("Cannot find video size");
}
return recordSize;
}
// Match preview size with current recording size mCurrentVideoSizeId
private Size matchPreviewRecordSize() {
Size recordSize = findRecordSize(mCurrentCameraId);
Size matchedSize = null;
// First try to find exact match in size
for (int i = 0; i < mPreviewSizes.size(); i++) {
if (mPreviewSizes.get(i).equals(recordSize)) {
matchedSize = mCamera.new Size(recordSize.width, recordSize.height);
break;
}
}
// Second try to find same ratio in size
if (matchedSize == null) {
for (int i = mPreviewSizes.size() - 1; i >= 0; i--) {
if (mPreviewSizes.get(i).width * recordSize.height ==
mPreviewSizes.get(i).height * recordSize.width) {
matchedSize = mCamera.new Size(mPreviewSizes.get(i).width,
mPreviewSizes.get(i).height);
break;
}
}
}
//Third try to find one with similar if not the same apect ratio
if (matchedSize == null) {
for (int i = mPreviewSizes.size() - 1; i >= 0; i--) {
if (Math.abs((float)mPreviewSizes.get(i).width * recordSize.height /
mPreviewSizes.get(i).height / recordSize.width - 1) < 0.12) {
matchedSize = mCamera.new Size(mPreviewSizes.get(i).width,
mPreviewSizes.get(i).height);
break;
}
}
}
// Last resort, just use the first preview size
if (matchedSize == null) {
matchedSize = mCamera.new Size(mPreviewSizes.get(0).width,
mPreviewSizes.get(0).height);
}
if (VERBOSE) {
Log.v(TAG, "matchPreviewRecordSize " + matchedSize.width + "x" + matchedSize.height);
}
return matchedSize;
}
private void setUpCamera(int id) {
shutdownCamera();
mCurrentCameraId = id;
try {
mCamera = Camera.open(id);
}
catch (Exception e) {
Log.e(TAG, "camera is not available", e);
failTest("camera not available" + e.getMessage());
return;
}
Camera.Parameters p = mCamera.getParameters();
if (VERBOSE) {
Log.v(TAG, "setUpCamera: setUpCamera got camera parameters");
}
// Get preview resolutions
List<Size> unsortedSizes = p.getSupportedPreviewSizes();
class SizeCompare implements Comparator<Size> {
@Override
public int compare(Size lhs, Size rhs) {
if (lhs.width < rhs.width) return -1;
if (lhs.width > rhs.width) return 1;
if (lhs.height < rhs.height) return -1;
if (lhs.height > rhs.height) return 1;
return 0;
}
};
SizeCompare s = new SizeCompare();
TreeSet<Size> sortedResolutions = new TreeSet<Size>(s);
sortedResolutions.addAll(unsortedSizes);
mPreviewSizes = new ArrayList<Size>(sortedResolutions);
ArrayList<VideoSizeNamePair> availableVideoSizes = getVideoSizeNamePairs(id);
String[] availableVideoSizeNames = new String[availableVideoSizes.size()];
mVideoSizeIds = new ArrayList<Integer>();
for (int i = 0; i < availableVideoSizes.size(); i++) {
availableVideoSizeNames[i] = availableVideoSizes.get(i).getSizeName();
mVideoSizeIds.add(availableVideoSizes.get(i).getSizeId());
}
mResolutionSpinner.setAdapter(
new ArrayAdapter<String>(
this, R.layout.cf_format_list_item, availableVideoSizeNames));
// Update untested
mUntestedCombinations.remove("All combinations for Camera " + id + "\n");
for (int videoSizeId: mVideoSizeIds) {
String combination = "Camera " + id + ", " + videoSizeId + "\n";
if (!mTestedCombinations.contains(combination)) {
mUntestedCombinations.add(combination);
}
}
// Set initial values
mCurrentVideoSizeId = mVideoSizeIds.get(0);
mNextPreviewSize = matchPreviewRecordSize();
mResolutionSpinner.setSelection(0);
// Set up correct display orientation
CameraInfo info = new CameraInfo();
Camera.getCameraInfo(id, info);
int rotation = getWindowManager().getDefaultDisplay().getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0: degrees = 0; break;
case Surface.ROTATION_90: degrees = 90; break;
case Surface.ROTATION_180: degrees = 180; break;
case Surface.ROTATION_270: degrees = 270; break;
}
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
mVideoRotation = (info.orientation + degrees) % 360;
mPreviewRotation = (360 - mVideoRotation) % 360; // compensate the mirror
} else { // back-facing
mVideoRotation = (info.orientation - degrees + 360) % 360;
mPreviewRotation = mVideoRotation;
}
if (mPreviewRotation != 0 && mPreviewRotation != 180) {
Log.w(TAG,
"Display orientation correction is not 0 or 180, as expected!");
}
mCamera.setDisplayOrientation(mPreviewRotation);
// Start up preview if display is ready
if (mPreviewTexture != null) {
startPreview();
}
}
private void shutdownCamera() {
if (mCamera != null) {
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}
/**
* starts capturing and drawing frames on screen
*/
private void startPreview() {
mCamera.stopPreview();
Matrix transform = new Matrix();
float widthRatio = mNextPreviewSize.width / (float)mPreviewTexWidth;
float heightRatio = mNextPreviewSize.height / (float)mPreviewTexHeight;
if (VERBOSE) {
Log.v(TAG, "startPreview: widthRatio=" + widthRatio + " " + "heightRatio=" +
heightRatio);
}
if (heightRatio < widthRatio) {
transform.setScale(1, heightRatio / widthRatio);
transform.postTranslate(0,
mPreviewTexHeight * (1 - heightRatio / widthRatio) / 2);
if (VERBOSE) {
Log.v(TAG, "startPreview: shrink vertical by " + heightRatio / widthRatio);
}
} else {
transform.setScale(widthRatio / heightRatio, 1);
transform.postTranslate(mPreviewTexWidth * (1 - widthRatio / heightRatio) / 2, 0);
if (VERBOSE) {
Log.v(TAG, "startPreview: shrink horizontal by " + widthRatio / heightRatio);
}
}
mPreviewView.setTransform(transform);
mPreviewSize = mNextPreviewSize;
Camera.Parameters p = mCamera.getParameters();
p.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
mCamera.setParameters(p);
try {
mCamera.setPreviewTexture(mPreviewTexture);
if (mPreviewTexture == null) {
Log.e(TAG, "preview texture is null.");
}
if (VERBOSE) {
Log.v(TAG, "startPreview: set preview texture in startPreview");
}
mCamera.startPreview();
if (VERBOSE) {
Log.v(TAG, "startPreview: started preview in startPreview");
}
} catch (IOException ioe) {
Log.e(TAG, "Unable to start up preview", ioe);
// Show a dialog box to tell user test failed
failTest("Unable to start preview.");
}
}
private void failTest(String failMessage) {
DialogInterface.OnClickListener dialogClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
setTestResultAndFinish(/* passed */false);
break;
case DialogInterface.BUTTON_NEGATIVE:
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(CameraVideoActivity.this);
builder.setMessage(getString(R.string.dialog_fail_test) + ". " + failMessage)
.setPositiveButton(R.string.fail_quit, dialogClickListener)
.setNegativeButton(R.string.cancel, dialogClickListener)
.show();
}
}