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