1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.cts.verifier.camera.video; 17 18 import android.app.AlertDialog; 19 import android.content.Context; 20 import android.content.DialogInterface; 21 import android.graphics.Matrix; 22 import android.graphics.SurfaceTexture; 23 import android.hardware.Camera; 24 import android.hardware.Camera.CameraInfo; 25 import android.hardware.Camera.Size; 26 import android.hardware.camera2.CameraAccessException; 27 import android.hardware.camera2.CameraCharacteristics; 28 import android.hardware.camera2.CameraManager; 29 import android.media.CamcorderProfile; 30 import android.media.MediaPlayer; 31 import android.media.MediaRecorder; 32 import android.os.Bundle; 33 import android.os.Environment; 34 import android.os.Handler; 35 import android.text.method.ScrollingMovementMethod; 36 import android.util.Log; 37 import android.view.Surface; 38 import android.view.TextureView; 39 import android.view.View; 40 import android.widget.AdapterView; 41 import android.widget.ArrayAdapter; 42 import android.widget.Button; 43 import android.widget.ImageButton; 44 import android.widget.Spinner; 45 import android.widget.TextView; 46 import android.widget.Toast; 47 import android.widget.VideoView; 48 49 import com.android.cts.verifier.PassFailButtons; 50 import com.android.cts.verifier.R; 51 52 import java.io.File; 53 import java.io.IOException; 54 import java.text.SimpleDateFormat; 55 import java.util.ArrayList; 56 import java.util.Comparator; 57 import java.util.Date; 58 import java.util.List; 59 import java.util.Optional; 60 import java.util.TreeSet; 61 62 63 /** 64 * Tests for manual verification of camera video capture 65 */ 66 public class CameraVideoActivity extends PassFailButtons.Activity 67 implements TextureView.SurfaceTextureListener { 68 69 private static final String TAG = "CtsCameraVideo"; 70 private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); 71 private static final int MEDIA_TYPE_IMAGE = 1; 72 private static final int MEDIA_TYPE_VIDEO = 2; 73 private static final int VIDEO_LENGTH = 3000; // in ms 74 75 private TextureView mPreviewView; 76 private SurfaceTexture mPreviewTexture; 77 private int mPreviewTexWidth; 78 private int mPreviewTexHeight; 79 private int mPreviewRotation; 80 private int mVideoRotation; 81 82 private VideoView mPlaybackView; 83 84 private Spinner mCameraSpinner; 85 private Spinner mResolutionSpinner; 86 87 private int mCurrentCameraId = -1; 88 private Camera mCamera; 89 private boolean mIsExternalCamera; 90 91 private MediaRecorder mMediaRecorder; 92 93 private List<Size> mPreviewSizes; 94 private Size mNextPreviewSize; 95 private Size mPreviewSize; 96 private List<Integer> mVideoSizeIds; 97 private List<String> mVideoSizeNames; 98 private int mCurrentVideoSizeId; 99 private String mCurrentVideoSizeName; 100 101 private boolean isRecording = false; 102 private boolean isPlayingBack = false; 103 private Button captureButton; 104 private ImageButton mPassButton; 105 private ImageButton mFailButton; 106 107 private TextView mStatusLabel; 108 109 private TreeSet<CameraCombination> mTestedCombinations = new TreeSet<>(COMPARATOR); 110 private TreeSet<CameraCombination> mUntestedCombinations = new TreeSet<>(COMPARATOR); 111 private TreeSet<String> mUntestedCameras = new TreeSet<>(); 112 113 private File outputVideoFile; 114 115 private class CameraCombination { 116 private final int mCameraIndex; 117 private final int mVideoSizeIdIndex; 118 private final String mVideoSizeName; 119 CameraCombination( int cameraIndex, int videoSizeIdIndex, String videoSizeName)120 private CameraCombination( 121 int cameraIndex, int videoSizeIdIndex, String videoSizeName) { 122 this.mCameraIndex = cameraIndex; 123 this.mVideoSizeIdIndex = videoSizeIdIndex; 124 this.mVideoSizeName = videoSizeName; 125 } 126 127 @Override toString()128 public String toString() { 129 return String.format("Camera %d, %s", mCameraIndex, mVideoSizeName); 130 } 131 } 132 133 private static final Comparator<CameraCombination> COMPARATOR = 134 Comparator.<CameraCombination, Integer>comparing(c -> c.mCameraIndex) 135 .thenComparing(c -> c.mVideoSizeIdIndex); 136 137 /** 138 * @see #MEDIA_TYPE_IMAGE 139 * @see #MEDIA_TYPE_VIDEO 140 */ getOutputMediaFile(int type)141 private static File getOutputMediaFile(int type) { 142 // Question: why do I need to comment this to get it working? 143 // Logcat says "external storage not ready" 144 // if (Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED) { 145 // Log.e(TAG, "external storage not ready"); 146 // return null; 147 // } 148 149 File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory( 150 Environment.DIRECTORY_MOVIES), TAG); 151 152 if (!mediaStorageDir.exists()) { 153 if (!mediaStorageDir.mkdirs()) { 154 Log.d(TAG, "failed to create directory"); 155 return null; 156 } 157 } 158 159 String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); 160 File mediaFile; 161 if (type == MEDIA_TYPE_IMAGE) { 162 mediaFile = new File(mediaStorageDir.getPath() + File.separator + 163 "IMG_" + timeStamp + ".jpg"); 164 } else if (type == MEDIA_TYPE_VIDEO) { 165 mediaFile = new File(mediaStorageDir.getPath() + File.separator + 166 "VID_" + timeStamp + ".mp4"); 167 if (VERBOSE) { 168 Log.v(TAG, "getOutputMediaFile: output file " + mediaFile.getPath()); 169 } 170 } else { 171 return null; 172 } 173 174 return mediaFile; 175 } 176 177 private static final int BIT_RATE_720P = 8000000; 178 private static final int BIT_RATE_MIN = 64000; 179 private static final int BIT_RATE_MAX = BIT_RATE_720P; 180 getVideoBitRate(Camera.Size sz)181 private int getVideoBitRate(Camera.Size sz) { 182 int rate = BIT_RATE_720P; 183 float scaleFactor = sz.height * sz.width / (float)(1280 * 720); 184 rate = (int)(rate * scaleFactor); 185 186 // Clamp to the MIN, MAX range. 187 return Math.max(BIT_RATE_MIN, Math.min(BIT_RATE_MAX, rate)); 188 } 189 prepareVideoRecorder()190 private boolean prepareVideoRecorder() { 191 192 mMediaRecorder = new MediaRecorder(); 193 194 // Step 1: unlock and set camera to MediaRecorder 195 mCamera.unlock(); 196 mMediaRecorder.setCamera(mCamera); 197 198 // Step 2: set sources 199 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); 200 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 201 202 // Step 3: set a CamcorderProfile 203 if (mIsExternalCamera) { 204 Camera.Size recordSize = null; 205 switch (mCurrentVideoSizeId) { 206 case CamcorderProfile.QUALITY_QCIF: 207 recordSize = mCamera.new Size(176, 144); 208 break; 209 case CamcorderProfile.QUALITY_QVGA: 210 recordSize = mCamera.new Size(320, 240); 211 break; 212 case CamcorderProfile.QUALITY_CIF: 213 recordSize = mCamera.new Size(352, 288); 214 break; 215 case CamcorderProfile.QUALITY_480P: 216 recordSize = mCamera.new Size(720, 480); 217 break; 218 case CamcorderProfile.QUALITY_720P: 219 recordSize = mCamera.new Size(1280, 720); 220 break; 221 default: 222 String msg = "Unknown CamcorderProfile: " + mCurrentVideoSizeId; 223 Log.e(TAG, msg); 224 releaseMediaRecorder(); 225 throw new AssertionError(msg); 226 } 227 228 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); 229 mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT); 230 mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); 231 mMediaRecorder.setVideoEncodingBitRate(getVideoBitRate(recordSize)); 232 mMediaRecorder.setVideoSize(recordSize.width, recordSize.height); 233 } else { 234 mMediaRecorder.setProfile(CamcorderProfile.get(mCurrentCameraId, mCurrentVideoSizeId)); 235 } 236 237 // Step 4: set output file 238 outputVideoFile = getOutputMediaFile(MEDIA_TYPE_VIDEO); 239 mMediaRecorder.setOutputFile(outputVideoFile.toString()); 240 241 // Step 5: set preview output 242 // This is not necessary since preview has been taken care of 243 244 // Step 6: set orientation hint 245 mMediaRecorder.setOrientationHint(mVideoRotation); 246 247 // Step 7: prepare configured MediaRecorder 248 try { 249 mMediaRecorder.prepare(); 250 } catch (IOException e) { 251 Log.e(TAG, "IOException preparing MediaRecorder: ", e); 252 releaseMediaRecorder(); 253 throw new AssertionError(e); 254 } 255 256 mMediaRecorder.setOnErrorListener( 257 new MediaRecorder.OnErrorListener() { 258 @Override 259 public void onError(MediaRecorder mr, int what, int extra) { 260 if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) { 261 Log.e(TAG, "unknown error in media recorder, error: " + extra); 262 } else { 263 Log.e(TAG, "media recorder server died, error: " + extra); 264 } 265 266 failTest("Media recorder error."); 267 } 268 }); 269 270 if (VERBOSE) { 271 Log.v(TAG, "prepareVideoRecorder: prepared configured MediaRecorder"); 272 } 273 274 return true; 275 } 276 277 @Override onCreate(Bundle savedInstanceState)278 public void onCreate(Bundle savedInstanceState) { 279 super.onCreate(savedInstanceState); 280 281 setContentView(R.layout.camera_video); 282 setPassFailButtonClickListeners(); 283 setInfoResources(R.string.camera_video, R.string.video_info, /*viewId*/-1); 284 285 mPreviewView = (TextureView) findViewById(R.id.video_capture); 286 mPlaybackView = (VideoView) findViewById(R.id.video_playback); 287 mPlaybackView.setOnCompletionListener(mPlaybackViewListener); 288 289 captureButton = (Button) findViewById(R.id.record_button); 290 mPassButton = (ImageButton) findViewById(R.id.pass_button); 291 mFailButton = (ImageButton) findViewById(R.id.fail_button); 292 mPassButton.setEnabled(false); 293 mFailButton.setEnabled(true); 294 295 mPreviewView.setSurfaceTextureListener(this); 296 297 int numCameras = Camera.getNumberOfCameras(); 298 String[] cameraNames = new String[numCameras]; 299 for (int i = 0; i < numCameras; i++) { 300 cameraNames[i] = "Camera " + i; 301 mUntestedCameras.add("All combinations for Camera " + i + "\n"); 302 } 303 if (VERBOSE) { 304 Log.v(TAG, "onCreate: number of cameras=" + numCameras); 305 } 306 mCameraSpinner = (Spinner) findViewById(R.id.cameras_selection); 307 mCameraSpinner.setAdapter( 308 new ArrayAdapter<String>( 309 this, R.layout.cf_format_list_item, cameraNames)); 310 mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener); 311 312 mResolutionSpinner = (Spinner) findViewById(R.id.resolution_selection); 313 mResolutionSpinner.setOnItemSelectedListener(mResolutionSelectedListener); 314 315 mStatusLabel = (TextView) findViewById(R.id.status_label); 316 317 Button mNextButton = (Button) findViewById(R.id.next_button); 318 mNextButton.setOnClickListener(v -> { 319 setUntestedCombination(); 320 if (VERBOSE) { 321 Log.v(TAG, "onClick: mCurrentVideoSizeId = " + 322 mCurrentVideoSizeId + " " + mCurrentVideoSizeName); 323 Log.v(TAG, "onClick: setting preview size " 324 + mNextPreviewSize.width + "x" + mNextPreviewSize.height); 325 } 326 327 startPreview(); 328 if (VERBOSE) { 329 Log.v(TAG, "onClick: started new preview"); 330 } 331 captureButton.performClick(); 332 }); 333 } 334 335 /** 336 * Set an untested combination of the current camera and video size. 337 * Triggered by next button click. 338 */ setUntestedCombination()339 private void setUntestedCombination() { 340 Optional<CameraCombination> combination = mUntestedCombinations.stream().filter( 341 c -> c.mCameraIndex == mCurrentCameraId).findFirst(); 342 if (!combination.isPresent()) { 343 Toast.makeText(this, "All Camera " + mCurrentCameraId + " tests are done.", 344 Toast.LENGTH_SHORT).show(); 345 return; 346 } 347 348 // There is untested combination for the current camera, set the next untested combination. 349 int mNextVideoSizeIdIndex = combination.get().mVideoSizeIdIndex; 350 351 mCurrentVideoSizeId = mVideoSizeIds.get(mNextVideoSizeIdIndex); 352 mCurrentVideoSizeName = mVideoSizeNames.get(mNextVideoSizeIdIndex); 353 mNextPreviewSize = matchPreviewRecordSize(); 354 mResolutionSpinner.setSelection(mNextVideoSizeIdIndex); 355 } 356 357 @Override onResume()358 public void onResume() { 359 super.onResume(); 360 361 setUpCamera(mCameraSpinner.getSelectedItemPosition()); 362 if (VERBOSE) { 363 Log.v(TAG, "onResume: camera has been setup"); 364 } 365 366 setUpCaptureButton(); 367 if (VERBOSE) { 368 Log.v(TAG, "onResume: captureButton has been setup"); 369 } 370 371 } 372 373 @Override onPause()374 public void onPause() { 375 super.onPause(); 376 377 releaseMediaRecorder(); 378 shutdownCamera(); 379 mPreviewTexture = null; 380 } 381 382 private MediaPlayer.OnCompletionListener mPlaybackViewListener = 383 new MediaPlayer.OnCompletionListener() { 384 385 @Override 386 public void onCompletion(MediaPlayer mp) { 387 isPlayingBack = false; 388 mPlaybackView.stopPlayback(); 389 captureButton.setEnabled(true); 390 391 mStatusLabel.setMovementMethod(new ScrollingMovementMethod()); 392 StringBuilder progress = new StringBuilder(); 393 progress.append(getResources().getString(R.string.status_ready)); 394 progress.append("\n---- Progress ----\n"); 395 progress.append(getTestDetails()); 396 mStatusLabel.setText(progress.toString()); 397 } 398 399 }; 400 releaseMediaRecorder()401 private void releaseMediaRecorder() { 402 if (mMediaRecorder != null) { 403 mMediaRecorder.reset(); 404 mMediaRecorder.release(); 405 mMediaRecorder = null; 406 mCamera.lock(); // check here, lock camera for later use 407 } 408 } 409 410 @Override getTestDetails()411 public String getTestDetails() { 412 StringBuilder reportBuilder = new StringBuilder(); 413 reportBuilder.append("Tested combinations:\n"); 414 for (CameraCombination combination: mTestedCombinations) { 415 reportBuilder.append(combination); 416 reportBuilder.append("\n"); 417 } 418 reportBuilder.append("Untested combinations:\n"); 419 for (String untestedCam : mUntestedCameras) { 420 reportBuilder.append(untestedCam); 421 } 422 for (CameraCombination combination: mUntestedCombinations) { 423 reportBuilder.append(combination); 424 reportBuilder.append("\n"); 425 } 426 return reportBuilder.toString(); 427 } 428 429 @Override onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height)430 public void onSurfaceTextureAvailable(SurfaceTexture surface, 431 int width, int height) { 432 mPreviewTexture = surface; 433 mPreviewTexWidth = width; 434 mPreviewTexHeight = height; 435 if (mCamera != null) { 436 startPreview(); 437 } 438 } 439 440 @Override onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height)441 public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { 442 // Ignored, Camera does all the work for us 443 } 444 445 @Override onSurfaceTextureDestroyed(SurfaceTexture surface)446 public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { 447 return true; 448 } 449 450 451 @Override onSurfaceTextureUpdated(SurfaceTexture surface)452 public void onSurfaceTextureUpdated(SurfaceTexture surface) { 453 // Invoked every time there's a new Camera preview frame 454 } 455 456 private AdapterView.OnItemSelectedListener mCameraSpinnerListener = 457 new AdapterView.OnItemSelectedListener() { 458 @Override 459 public void onItemSelected(AdapterView<?> parent, 460 View view, int pos, long id) { 461 if (mCurrentCameraId != pos) { 462 setUpCamera(pos); 463 } 464 } 465 466 @Override 467 public void onNothingSelected(AdapterView<?> parent) { 468 // Intentionally left blank 469 } 470 471 }; 472 473 private AdapterView.OnItemSelectedListener mResolutionSelectedListener = 474 new AdapterView.OnItemSelectedListener() { 475 @Override 476 public void onItemSelected(AdapterView<?> parent, 477 View view, int position, long id) { 478 if (mVideoSizeIds.get(position) != mCurrentVideoSizeId) { 479 mCurrentVideoSizeId = mVideoSizeIds.get(position); 480 mCurrentVideoSizeName = mVideoSizeNames.get(position); 481 if (VERBOSE) { 482 Log.v(TAG, "onItemSelected: mCurrentVideoSizeId = " + 483 mCurrentVideoSizeId + " " + mCurrentVideoSizeName); 484 } 485 mNextPreviewSize = matchPreviewRecordSize(); 486 if (VERBOSE) { 487 Log.v(TAG, "onItemSelected: setting preview size " 488 + mNextPreviewSize.width + "x" + mNextPreviewSize.height); 489 } 490 491 startPreview(); 492 if (VERBOSE) { 493 Log.v(TAG, "onItemSelected: started new preview"); 494 } 495 } 496 } 497 498 @Override 499 public void onNothingSelected(AdapterView<?> parent) { 500 // Intentionally left blank 501 } 502 503 }; 504 505 setUpCaptureButton()506 private void setUpCaptureButton() { 507 captureButton.setOnClickListener ( 508 new View.OnClickListener() { 509 @Override 510 public void onClick(View V) { 511 if ((!isRecording) && (!isPlayingBack)) { 512 if (prepareVideoRecorder()) { 513 mMediaRecorder.start(); 514 if (VERBOSE) { 515 Log.v(TAG, "onClick: started mMediaRecorder"); 516 } 517 isRecording = true; 518 captureButton.setEnabled(false); 519 mStatusLabel.setText(getResources() 520 .getString(R.string.status_recording)); 521 } else { 522 releaseMediaRecorder(); 523 Log.e(TAG, "media recorder cannot be set up"); 524 failTest("Unable to set up media recorder."); 525 } 526 Handler h = new Handler(); 527 Runnable mDelayedPreview = new Runnable() { 528 @Override 529 public void run() { 530 mMediaRecorder.stop(); 531 releaseMediaRecorder(); 532 533 mPlaybackView.setVideoPath(outputVideoFile.getPath()); 534 mPlaybackView.start(); 535 isRecording = false; 536 isPlayingBack = true; 537 mStatusLabel.setText(getResources() 538 .getString(R.string.status_playback)); 539 540 int resIdx = mResolutionSpinner.getSelectedItemPosition(); 541 CameraCombination combination = new CameraCombination( 542 mCurrentCameraId, resIdx, 543 mVideoSizeNames.get(resIdx)); 544 545 mUntestedCombinations.remove(combination); 546 mTestedCombinations.add(combination); 547 548 if (mUntestedCombinations.isEmpty()) { 549 mPassButton.setEnabled(true); 550 if (VERBOSE) { 551 Log.v(TAG, "run: test success"); 552 } 553 } 554 } 555 }; 556 h.postDelayed(mDelayedPreview, VIDEO_LENGTH); 557 } 558 559 } 560 } 561 ); 562 } 563 564 private class VideoSizeNamePair { 565 private int sizeId; 566 private String sizeName; 567 VideoSizeNamePair(int id, String name)568 public VideoSizeNamePair(int id, String name) { 569 sizeId = id; 570 sizeName = name; 571 } 572 getSizeId()573 public int getSizeId() { 574 return sizeId; 575 } 576 getSizeName()577 public String getSizeName() { 578 return sizeName; 579 } 580 } 581 getVideoSizeNamePairs(int cameraId)582 private ArrayList<VideoSizeNamePair> getVideoSizeNamePairs(int cameraId) { 583 int[] qualityArray = { 584 CamcorderProfile.QUALITY_LOW, 585 CamcorderProfile.QUALITY_HIGH, 586 CamcorderProfile.QUALITY_QCIF, // 176x144 587 CamcorderProfile.QUALITY_QVGA, // 320x240 588 CamcorderProfile.QUALITY_CIF, // 352x288 589 CamcorderProfile.QUALITY_480P, // 720x480 590 CamcorderProfile.QUALITY_720P, // 1280x720 591 CamcorderProfile.QUALITY_1080P, // 1920x1080 or 1920x1088 592 CamcorderProfile.QUALITY_2160P 593 }; 594 595 final Camera.Size skip = mCamera.new Size(-1, -1); 596 Camera.Size[] videoSizeArray = { 597 skip, 598 skip, 599 mCamera.new Size(176, 144), 600 mCamera.new Size(320, 240), 601 mCamera.new Size(352, 288), 602 mCamera.new Size(720, 480), 603 mCamera.new Size(1280, 720), 604 skip, 605 skip 606 }; 607 608 String[] nameArray = { 609 "LOW", 610 "HIGH", 611 "QCIF", 612 "QVGA", 613 "CIF", 614 "480P", 615 "720P", 616 "1080P", 617 "2160P" 618 }; 619 620 ArrayList<VideoSizeNamePair> availableSizes = 621 new ArrayList<VideoSizeNamePair> (); 622 623 Camera.Parameters p = mCamera.getParameters(); 624 List<Camera.Size> supportedVideoSizes = p.getSupportedVideoSizes(); 625 for (int i = 0; i < qualityArray.length; i++) { 626 if (mIsExternalCamera) { 627 Camera.Size videoSz = videoSizeArray[i]; 628 if (videoSz.equals(skip)) { 629 continue; 630 } 631 if (supportedVideoSizes.contains(videoSz)) { 632 VideoSizeNamePair pair = new VideoSizeNamePair(qualityArray[i], nameArray[i]); 633 availableSizes.add(pair); 634 } 635 } else { 636 if (CamcorderProfile.hasProfile(cameraId, qualityArray[i])) { 637 VideoSizeNamePair pair = new VideoSizeNamePair(qualityArray[i], nameArray[i]); 638 availableSizes.add(pair); 639 } 640 } 641 } 642 return availableSizes; 643 } 644 645 static class ResolutionQuality { 646 private int videoSizeId; 647 private int width; 648 private int height; 649 ResolutionQuality()650 public ResolutionQuality() { 651 // intentionally left blank 652 } ResolutionQuality(int newSizeId, int newWidth, int newHeight)653 public ResolutionQuality(int newSizeId, int newWidth, int newHeight) { 654 videoSizeId = newSizeId; 655 width = newWidth; 656 height = newHeight; 657 } 658 } 659 findRecordSize(int cameraId)660 private Size findRecordSize(int cameraId) { 661 int[] possibleQuality = { 662 CamcorderProfile.QUALITY_LOW, 663 CamcorderProfile.QUALITY_HIGH, 664 CamcorderProfile.QUALITY_QCIF, 665 CamcorderProfile.QUALITY_QVGA, 666 CamcorderProfile.QUALITY_CIF, 667 CamcorderProfile.QUALITY_480P, 668 CamcorderProfile.QUALITY_720P, 669 CamcorderProfile.QUALITY_1080P, 670 CamcorderProfile.QUALITY_2160P 671 }; 672 673 final Camera.Size skip = mCamera.new Size(-1, -1); 674 Camera.Size[] videoSizeArray = { 675 skip, 676 skip, 677 mCamera.new Size(176, 144), 678 mCamera.new Size(320, 240), 679 mCamera.new Size(352, 288), 680 mCamera.new Size(720, 480), 681 mCamera.new Size(1280, 720), 682 skip, 683 skip 684 }; 685 686 ArrayList<ResolutionQuality> qualityList = new ArrayList<ResolutionQuality>(); 687 Camera.Parameters p = mCamera.getParameters(); 688 List<Camera.Size> supportedVideoSizes = p.getSupportedVideoSizes(); 689 for (int i = 0; i < possibleQuality.length; i++) { 690 if (mIsExternalCamera) { 691 Camera.Size videoSz = videoSizeArray[i]; 692 if (videoSz.equals(skip)) { 693 continue; 694 } 695 if (supportedVideoSizes.contains(videoSz)) { 696 qualityList.add(new ResolutionQuality(possibleQuality[i], 697 videoSz.width, videoSz.height)); 698 } 699 } else { 700 if (CamcorderProfile.hasProfile(cameraId, possibleQuality[i])) { 701 CamcorderProfile profile = CamcorderProfile.get(cameraId, possibleQuality[i]); 702 qualityList.add(new ResolutionQuality(possibleQuality[i], 703 profile.videoFrameWidth, profile.videoFrameHeight)); 704 } 705 } 706 } 707 708 Size recordSize = null; 709 for (int i = 0; i < qualityList.size(); i++) { 710 if (mCurrentVideoSizeId == qualityList.get(i).videoSizeId) { 711 recordSize = mCamera.new Size(qualityList.get(i).width, 712 qualityList.get(i).height); 713 break; 714 } 715 } 716 717 if (recordSize == null) { 718 Log.e(TAG, "findRecordSize: did not find a match"); 719 failTest("Cannot find video size"); 720 } 721 return recordSize; 722 } 723 724 // Match preview size with current recording size mCurrentVideoSizeId matchPreviewRecordSize()725 private Size matchPreviewRecordSize() { 726 Size recordSize = findRecordSize(mCurrentCameraId); 727 728 Size matchedSize = null; 729 // First try to find exact match in size 730 for (int i = 0; i < mPreviewSizes.size(); i++) { 731 if (mPreviewSizes.get(i).equals(recordSize)) { 732 matchedSize = mCamera.new Size(recordSize.width, recordSize.height); 733 break; 734 } 735 } 736 // Second try to find same ratio in size 737 if (matchedSize == null) { 738 for (int i = mPreviewSizes.size() - 1; i >= 0; i--) { 739 if (mPreviewSizes.get(i).width * recordSize.height == 740 mPreviewSizes.get(i).height * recordSize.width) { 741 matchedSize = mCamera.new Size(mPreviewSizes.get(i).width, 742 mPreviewSizes.get(i).height); 743 break; 744 } 745 } 746 } 747 //Third try to find one with similar if not the same apect ratio 748 if (matchedSize == null) { 749 for (int i = mPreviewSizes.size() - 1; i >= 0; i--) { 750 if (Math.abs((float)mPreviewSizes.get(i).width * recordSize.height / 751 mPreviewSizes.get(i).height / recordSize.width - 1) < 0.12) { 752 matchedSize = mCamera.new Size(mPreviewSizes.get(i).width, 753 mPreviewSizes.get(i).height); 754 break; 755 } 756 } 757 } 758 // Last resort, just use the first preview size 759 if (matchedSize == null) { 760 matchedSize = mCamera.new Size(mPreviewSizes.get(0).width, 761 mPreviewSizes.get(0).height); 762 } 763 764 if (VERBOSE) { 765 Log.v(TAG, "matchPreviewRecordSize " + matchedSize.width + "x" + matchedSize.height); 766 } 767 768 return matchedSize; 769 } 770 setUpCamera(int id)771 private void setUpCamera(int id) { 772 shutdownCamera(); 773 774 mCurrentCameraId = id; 775 try { 776 mCamera = Camera.open(id); 777 } 778 catch (Exception e) { 779 Log.e(TAG, "camera is not available", e); 780 failTest("camera not available" + e.getMessage()); 781 return; 782 } 783 mIsExternalCamera = isExternalCamera(id); 784 785 Camera.Parameters p = mCamera.getParameters(); 786 if (VERBOSE) { 787 Log.v(TAG, "setUpCamera: setUpCamera got camera parameters"); 788 } 789 790 // Get preview resolutions 791 List<Size> unsortedSizes = p.getSupportedPreviewSizes(); 792 793 class SizeCompare implements Comparator<Size> { 794 @Override 795 public int compare(Size lhs, Size rhs) { 796 if (lhs.width < rhs.width) return -1; 797 if (lhs.width > rhs.width) return 1; 798 if (lhs.height < rhs.height) return -1; 799 if (lhs.height > rhs.height) return 1; 800 return 0; 801 } 802 }; 803 804 SizeCompare s = new SizeCompare(); 805 TreeSet<Size> sortedResolutions = new TreeSet<Size>(s); 806 sortedResolutions.addAll(unsortedSizes); 807 808 mPreviewSizes = new ArrayList<Size>(sortedResolutions); 809 810 ArrayList<VideoSizeNamePair> availableVideoSizes = getVideoSizeNamePairs(id); 811 String[] availableVideoSizeNames = new String[availableVideoSizes.size()]; 812 mVideoSizeIds = new ArrayList<Integer>(); 813 mVideoSizeNames = new ArrayList<String>(); 814 for (int i = 0; i < availableVideoSizes.size(); i++) { 815 availableVideoSizeNames[i] = availableVideoSizes.get(i).getSizeName(); 816 mVideoSizeIds.add(availableVideoSizes.get(i).getSizeId()); 817 mVideoSizeNames.add(availableVideoSizeNames[i]); 818 } 819 820 mResolutionSpinner.setAdapter( 821 new ArrayAdapter<String>( 822 this, R.layout.cf_format_list_item, availableVideoSizeNames)); 823 824 // Update untested 825 mUntestedCameras.remove("All combinations for Camera " + id + "\n"); 826 827 for (int videoSizeIdIndex = 0; 828 videoSizeIdIndex < mVideoSizeIds.size(); videoSizeIdIndex++) { 829 CameraCombination combination = new CameraCombination( 830 id, videoSizeIdIndex, mVideoSizeNames.get(videoSizeIdIndex)); 831 832 if (!mTestedCombinations.contains(combination)) { 833 mUntestedCombinations.add(combination); 834 } 835 } 836 837 // Set initial values 838 mCurrentVideoSizeId = mVideoSizeIds.get(0); 839 mCurrentVideoSizeName = mVideoSizeNames.get(0); 840 mNextPreviewSize = matchPreviewRecordSize(); 841 mResolutionSpinner.setSelection(0); 842 843 // Set up correct display orientation 844 CameraInfo info = new CameraInfo(); 845 Camera.getCameraInfo(id, info); 846 int rotation = getWindowManager().getDefaultDisplay().getRotation(); 847 int degrees = 0; 848 switch (rotation) { 849 case Surface.ROTATION_0: degrees = 0; break; 850 case Surface.ROTATION_90: degrees = 90; break; 851 case Surface.ROTATION_180: degrees = 180; break; 852 case Surface.ROTATION_270: degrees = 270; break; 853 } 854 855 if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { 856 mVideoRotation = (info.orientation + degrees) % 360; 857 mPreviewRotation = (360 - mVideoRotation) % 360; // compensate the mirror 858 } else { // back-facing 859 mVideoRotation = (info.orientation - degrees + 360) % 360; 860 mPreviewRotation = mVideoRotation; 861 } 862 if (mPreviewRotation != 0 && mPreviewRotation != 180) { 863 Log.w(TAG, 864 "Display orientation correction is not 0 or 180, as expected!"); 865 } 866 867 mCamera.setDisplayOrientation(mPreviewRotation); 868 869 // Start up preview if display is ready 870 if (mPreviewTexture != null) { 871 startPreview(); 872 } 873 } 874 shutdownCamera()875 private void shutdownCamera() { 876 if (mCamera != null) { 877 mCamera.setPreviewCallback(null); 878 mCamera.stopPreview(); 879 mCamera.release(); 880 mCamera = null; 881 } 882 } 883 884 /** 885 * starts capturing and drawing frames on screen 886 */ startPreview()887 private void startPreview() { 888 889 mCamera.stopPreview(); 890 891 Matrix transform = new Matrix(); 892 float widthRatio = mNextPreviewSize.width / (float)mPreviewTexWidth; 893 float heightRatio = mNextPreviewSize.height / (float)mPreviewTexHeight; 894 if (VERBOSE) { 895 Log.v(TAG, "startPreview: widthRatio=" + widthRatio + " " + "heightRatio=" + 896 heightRatio); 897 } 898 899 if (heightRatio < widthRatio) { 900 transform.setScale(1, heightRatio / widthRatio); 901 transform.postTranslate(0, 902 mPreviewTexHeight * (1 - heightRatio / widthRatio) / 2); 903 if (VERBOSE) { 904 Log.v(TAG, "startPreview: shrink vertical by " + heightRatio / widthRatio); 905 } 906 } else { 907 transform.setScale(widthRatio / heightRatio, 1); 908 transform.postTranslate(mPreviewTexWidth * (1 - widthRatio / heightRatio) / 2, 0); 909 if (VERBOSE) { 910 Log.v(TAG, "startPreview: shrink horizontal by " + widthRatio / heightRatio); 911 } 912 } 913 914 mPreviewView.setTransform(transform); 915 916 mPreviewSize = mNextPreviewSize; 917 918 Camera.Parameters p = mCamera.getParameters(); 919 p.setPreviewSize(mPreviewSize.width, mPreviewSize.height); 920 mCamera.setParameters(p); 921 922 try { 923 mCamera.setPreviewTexture(mPreviewTexture); 924 if (mPreviewTexture == null) { 925 Log.e(TAG, "preview texture is null."); 926 } 927 if (VERBOSE) { 928 Log.v(TAG, "startPreview: set preview texture in startPreview"); 929 } 930 mCamera.startPreview(); 931 if (VERBOSE) { 932 Log.v(TAG, "startPreview: started preview in startPreview"); 933 } 934 } catch (IOException ioe) { 935 Log.e(TAG, "Unable to start up preview", ioe); 936 // Show a dialog box to tell user test failed 937 failTest("Unable to start preview."); 938 } 939 } 940 failTest(String failMessage)941 private void failTest(String failMessage) { 942 DialogInterface.OnClickListener dialogClickListener = 943 new DialogInterface.OnClickListener() { 944 @Override 945 public void onClick(DialogInterface dialog, int which) { 946 switch (which) { 947 case DialogInterface.BUTTON_POSITIVE: 948 setTestResultAndFinish(/* passed */false); 949 break; 950 case DialogInterface.BUTTON_NEGATIVE: 951 break; 952 } 953 } 954 }; 955 956 AlertDialog.Builder builder = new AlertDialog.Builder(CameraVideoActivity.this); 957 builder.setMessage(getString(R.string.dialog_fail_test) + ". " + failMessage) 958 .setPositiveButton(R.string.fail_quit, dialogClickListener) 959 .setNegativeButton(R.string.cancel, dialogClickListener) 960 .show(); 961 } 962 isExternalCamera(int cameraId)963 private boolean isExternalCamera(int cameraId) { 964 CameraManager manager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE); 965 try { 966 String cameraIdStr = manager.getCameraIdList()[cameraId]; 967 CameraCharacteristics characteristics = 968 manager.getCameraCharacteristics(cameraIdStr); 969 970 if (characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) == 971 CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL) { 972 // External camera doesn't support FOV informations 973 return true; 974 } 975 } catch (CameraAccessException e) { 976 Toast.makeText(this, "Could not access camera " + cameraId + 977 ": " + e.getMessage(), Toast.LENGTH_LONG).show(); 978 } 979 return false; 980 } 981 982 } 983