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