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 mPlaybackView.start(); 537 isRecording = false; 538 isPlayingBack = true; 539 mStatusLabel.setText(getResources() 540 .getString(R.string.status_playback)); 541 542 int resIdx = mResolutionSpinner.getSelectedItemPosition(); 543 CameraCombination combination = new CameraCombination( 544 mCurrentCameraId, resIdx, 545 mVideoSizeNames.get(resIdx)); 546 547 mUntestedCombinations.remove(combination); 548 mTestedCombinations.add(combination); 549 550 if (mUntestedCombinations.isEmpty() && 551 mUntestedCameras.isEmpty()) { 552 mPassButton.setEnabled(true); 553 if (VERBOSE) { 554 Log.v(TAG, "run: test success"); 555 } 556 } 557 } 558 }; 559 h.postDelayed(mDelayedPreview, VIDEO_LENGTH); 560 } 561 562 } 563 } 564 ); 565 } 566 567 private class VideoSizeNamePair { 568 private int sizeId; 569 private String sizeName; 570 VideoSizeNamePair(int id, String name)571 public VideoSizeNamePair(int id, String name) { 572 sizeId = id; 573 sizeName = name; 574 } 575 getSizeId()576 public int getSizeId() { 577 return sizeId; 578 } 579 getSizeName()580 public String getSizeName() { 581 return sizeName; 582 } 583 } 584 getVideoSizeNamePairs(int cameraId)585 private ArrayList<VideoSizeNamePair> getVideoSizeNamePairs(int cameraId) { 586 int[] qualityArray = { 587 CamcorderProfile.QUALITY_LOW, 588 CamcorderProfile.QUALITY_HIGH, 589 CamcorderProfile.QUALITY_QCIF, // 176x144 590 CamcorderProfile.QUALITY_QVGA, // 320x240 591 CamcorderProfile.QUALITY_CIF, // 352x288 592 CamcorderProfile.QUALITY_480P, // 720x480 593 CamcorderProfile.QUALITY_720P, // 1280x720 594 CamcorderProfile.QUALITY_1080P, // 1920x1080 or 1920x1088 595 CamcorderProfile.QUALITY_2160P 596 }; 597 598 final Camera.Size skip = mCamera.new Size(-1, -1); 599 Camera.Size[] videoSizeArray = { 600 skip, 601 skip, 602 mCamera.new Size(176, 144), 603 mCamera.new Size(320, 240), 604 mCamera.new Size(352, 288), 605 mCamera.new Size(720, 480), 606 mCamera.new Size(1280, 720), 607 skip, 608 skip 609 }; 610 611 String[] nameArray = { 612 "LOW", 613 "HIGH", 614 "QCIF", 615 "QVGA", 616 "CIF", 617 "480P", 618 "720P", 619 "1080P", 620 "2160P" 621 }; 622 623 ArrayList<VideoSizeNamePair> availableSizes = 624 new ArrayList<VideoSizeNamePair> (); 625 626 Camera.Parameters p = mCamera.getParameters(); 627 List<Camera.Size> supportedVideoSizes = p.getSupportedVideoSizes(); 628 for (int i = 0; i < qualityArray.length; i++) { 629 if (mIsExternalCamera) { 630 Camera.Size videoSz = videoSizeArray[i]; 631 if (videoSz.equals(skip)) { 632 continue; 633 } 634 if (supportedVideoSizes.contains(videoSz)) { 635 VideoSizeNamePair pair = new VideoSizeNamePair(qualityArray[i], nameArray[i]); 636 availableSizes.add(pair); 637 } 638 } else { 639 if (CamcorderProfile.hasProfile(cameraId, qualityArray[i])) { 640 VideoSizeNamePair pair = new VideoSizeNamePair(qualityArray[i], nameArray[i]); 641 availableSizes.add(pair); 642 } 643 } 644 } 645 return availableSizes; 646 } 647 648 static class ResolutionQuality { 649 private int videoSizeId; 650 private int width; 651 private int height; 652 ResolutionQuality()653 public ResolutionQuality() { 654 // intentionally left blank 655 } ResolutionQuality(int newSizeId, int newWidth, int newHeight)656 public ResolutionQuality(int newSizeId, int newWidth, int newHeight) { 657 videoSizeId = newSizeId; 658 width = newWidth; 659 height = newHeight; 660 } 661 } 662 findRecordSize(int cameraId)663 private Size findRecordSize(int cameraId) { 664 int[] possibleQuality = { 665 CamcorderProfile.QUALITY_LOW, 666 CamcorderProfile.QUALITY_HIGH, 667 CamcorderProfile.QUALITY_QCIF, 668 CamcorderProfile.QUALITY_QVGA, 669 CamcorderProfile.QUALITY_CIF, 670 CamcorderProfile.QUALITY_480P, 671 CamcorderProfile.QUALITY_720P, 672 CamcorderProfile.QUALITY_1080P, 673 CamcorderProfile.QUALITY_2160P 674 }; 675 676 final Camera.Size skip = mCamera.new Size(-1, -1); 677 Camera.Size[] videoSizeArray = { 678 skip, 679 skip, 680 mCamera.new Size(176, 144), 681 mCamera.new Size(320, 240), 682 mCamera.new Size(352, 288), 683 mCamera.new Size(720, 480), 684 mCamera.new Size(1280, 720), 685 skip, 686 skip 687 }; 688 689 ArrayList<ResolutionQuality> qualityList = new ArrayList<ResolutionQuality>(); 690 Camera.Parameters p = mCamera.getParameters(); 691 List<Camera.Size> supportedVideoSizes = p.getSupportedVideoSizes(); 692 for (int i = 0; i < possibleQuality.length; i++) { 693 if (mIsExternalCamera) { 694 Camera.Size videoSz = videoSizeArray[i]; 695 if (videoSz.equals(skip)) { 696 continue; 697 } 698 if (supportedVideoSizes.contains(videoSz)) { 699 qualityList.add(new ResolutionQuality(possibleQuality[i], 700 videoSz.width, videoSz.height)); 701 } 702 } else { 703 if (CamcorderProfile.hasProfile(cameraId, possibleQuality[i])) { 704 CamcorderProfile profile = CamcorderProfile.get(cameraId, possibleQuality[i]); 705 qualityList.add(new ResolutionQuality(possibleQuality[i], 706 profile.videoFrameWidth, profile.videoFrameHeight)); 707 } 708 } 709 } 710 711 Size recordSize = null; 712 for (int i = 0; i < qualityList.size(); i++) { 713 if (mCurrentVideoSizeId == qualityList.get(i).videoSizeId) { 714 recordSize = mCamera.new Size(qualityList.get(i).width, 715 qualityList.get(i).height); 716 break; 717 } 718 } 719 720 if (recordSize == null) { 721 Log.e(TAG, "findRecordSize: did not find a match"); 722 failTest("Cannot find video size"); 723 } 724 return recordSize; 725 } 726 727 // Match preview size with current recording size mCurrentVideoSizeId matchPreviewRecordSize()728 private Size matchPreviewRecordSize() { 729 Size recordSize = findRecordSize(mCurrentCameraId); 730 731 Size matchedSize = null; 732 // First try to find exact match in size 733 for (int i = 0; i < mPreviewSizes.size(); i++) { 734 if (mPreviewSizes.get(i).equals(recordSize)) { 735 matchedSize = mCamera.new Size(recordSize.width, recordSize.height); 736 break; 737 } 738 } 739 // Second try to find same ratio in size 740 if (matchedSize == null) { 741 for (int i = mPreviewSizes.size() - 1; i >= 0; i--) { 742 if (mPreviewSizes.get(i).width * recordSize.height == 743 mPreviewSizes.get(i).height * recordSize.width) { 744 matchedSize = mCamera.new Size(mPreviewSizes.get(i).width, 745 mPreviewSizes.get(i).height); 746 break; 747 } 748 } 749 } 750 //Third try to find one with similar if not the same apect ratio 751 if (matchedSize == null) { 752 for (int i = mPreviewSizes.size() - 1; i >= 0; i--) { 753 if (Math.abs((float)mPreviewSizes.get(i).width * recordSize.height / 754 mPreviewSizes.get(i).height / recordSize.width - 1) < 0.12) { 755 matchedSize = mCamera.new Size(mPreviewSizes.get(i).width, 756 mPreviewSizes.get(i).height); 757 break; 758 } 759 } 760 } 761 // Last resort, just use the first preview size 762 if (matchedSize == null) { 763 matchedSize = mCamera.new Size(mPreviewSizes.get(0).width, 764 mPreviewSizes.get(0).height); 765 } 766 767 if (VERBOSE) { 768 Log.v(TAG, "matchPreviewRecordSize " + matchedSize.width + "x" + matchedSize.height); 769 } 770 771 return matchedSize; 772 } 773 setUpCamera(int id)774 private void setUpCamera(int id) { 775 shutdownCamera(); 776 777 mCurrentCameraId = id; 778 mIsExternalCamera = isExternalCamera(id); 779 try { 780 mCamera = Camera.open(id); 781 } 782 catch (Exception e) { 783 Log.e(TAG, "camera is not available", e); 784 failTest("camera not available" + e.getMessage()); 785 return; 786 } 787 788 Camera.Parameters p = mCamera.getParameters(); 789 if (VERBOSE) { 790 Log.v(TAG, "setUpCamera: setUpCamera got camera parameters"); 791 } 792 793 // Get preview resolutions 794 List<Size> unsortedSizes = p.getSupportedPreviewSizes(); 795 796 class SizeCompare implements Comparator<Size> { 797 @Override 798 public int compare(Size lhs, Size rhs) { 799 if (lhs.width < rhs.width) return -1; 800 if (lhs.width > rhs.width) return 1; 801 if (lhs.height < rhs.height) return -1; 802 if (lhs.height > rhs.height) return 1; 803 return 0; 804 } 805 }; 806 807 if (mIsExternalCamera) { 808 setVideoFrameRate(p.getPreviewFrameRate()); 809 } 810 811 SizeCompare s = new SizeCompare(); 812 TreeSet<Size> sortedResolutions = new TreeSet<Size>(s); 813 sortedResolutions.addAll(unsortedSizes); 814 815 mPreviewSizes = new ArrayList<Size>(sortedResolutions); 816 817 ArrayList<VideoSizeNamePair> availableVideoSizes = getVideoSizeNamePairs(id); 818 String[] availableVideoSizeNames = new String[availableVideoSizes.size()]; 819 mVideoSizeIds = new ArrayList<Integer>(); 820 mVideoSizeNames = new ArrayList<String>(); 821 for (int i = 0; i < availableVideoSizes.size(); i++) { 822 availableVideoSizeNames[i] = availableVideoSizes.get(i).getSizeName(); 823 mVideoSizeIds.add(availableVideoSizes.get(i).getSizeId()); 824 mVideoSizeNames.add(availableVideoSizeNames[i]); 825 } 826 827 mResolutionSpinner.setAdapter( 828 new ArrayAdapter<String>( 829 this, R.layout.camera_list_item, availableVideoSizeNames)); 830 831 // Update untested 832 mUntestedCameras.remove("All combinations for Camera " + id + "\n"); 833 834 for (int videoSizeIdIndex = 0; 835 videoSizeIdIndex < mVideoSizeIds.size(); videoSizeIdIndex++) { 836 CameraCombination combination = new CameraCombination( 837 id, videoSizeIdIndex, mVideoSizeNames.get(videoSizeIdIndex)); 838 839 if (!mTestedCombinations.contains(combination)) { 840 mUntestedCombinations.add(combination); 841 } 842 } 843 844 // Set initial values 845 mCurrentVideoSizeId = mVideoSizeIds.get(0); 846 mCurrentVideoSizeName = mVideoSizeNames.get(0); 847 mNextPreviewSize = matchPreviewRecordSize(); 848 mResolutionSpinner.setSelection(0); 849 850 // Set up correct display orientation 851 CameraInfo info = new CameraInfo(); 852 Camera.getCameraInfo(id, info); 853 int rotation = getWindowManager().getDefaultDisplay().getRotation(); 854 int degrees = 0; 855 switch (rotation) { 856 case Surface.ROTATION_0: degrees = 0; break; 857 case Surface.ROTATION_90: degrees = 90; break; 858 case Surface.ROTATION_180: degrees = 180; break; 859 case Surface.ROTATION_270: degrees = 270; break; 860 } 861 862 if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { 863 mVideoRotation = (info.orientation + degrees) % 360; 864 mPreviewRotation = (360 - mVideoRotation) % 360; // compensate the mirror 865 } else { // back-facing 866 mVideoRotation = (info.orientation - degrees + 360) % 360; 867 mPreviewRotation = mVideoRotation; 868 } 869 870 mCamera.setDisplayOrientation(mPreviewRotation); 871 872 // Start up preview if display is ready 873 if (mPreviewTexture != null) { 874 startPreview(); 875 } 876 } 877 shutdownCamera()878 private void shutdownCamera() { 879 if (mCamera != null) { 880 mCamera.setPreviewCallback(null); 881 mCamera.stopPreview(); 882 mCamera.release(); 883 mCamera = null; 884 } 885 } 886 887 /** 888 * starts capturing and drawing frames on screen 889 */ startPreview()890 private void startPreview() { 891 892 mCamera.stopPreview(); 893 894 Matrix transform = new Matrix(); 895 float widthRatio = mNextPreviewSize.width / (float)mPreviewTexWidth; 896 float heightRatio = mNextPreviewSize.height / (float)mPreviewTexHeight; 897 float scaledWidth = (float) mPreviewTexWidth; 898 float scaledHeight = (float) mPreviewTexHeight; 899 if (VERBOSE) { 900 Log.v(TAG, "startPreview: widthRatio=" + widthRatio + " " + "heightRatio=" + 901 heightRatio); 902 } 903 904 if (heightRatio < widthRatio) { 905 scaledHeight = mPreviewTexHeight * (heightRatio / widthRatio); 906 transform.setScale(1, heightRatio / widthRatio); 907 transform.postTranslate(0, (mPreviewTexHeight - scaledHeight) / 2); 908 if (VERBOSE) { 909 Log.v(TAG, "startPreview: shrink vertical by " + heightRatio / widthRatio); 910 } 911 } else { 912 scaledWidth = mPreviewTexWidth * (widthRatio / heightRatio); 913 transform.setScale(widthRatio / heightRatio, 1); 914 transform.postTranslate((mPreviewTexWidth - scaledWidth) / 2, 0); 915 if (VERBOSE) { 916 Log.v(TAG, "startPreview: shrink horizontal by " + widthRatio / heightRatio); 917 } 918 } 919 920 if (mPreviewRotation == 90 || mPreviewRotation == 270) { 921 float scaledAspect = scaledWidth / scaledHeight; 922 float previewAspect = (float) mNextPreviewSize.width / (float) mNextPreviewSize.height; 923 transform.postScale(1.0f, scaledAspect * previewAspect, 924 (float) mPreviewTexWidth / 2, (float) mPreviewTexHeight / 2); 925 } 926 927 mPreviewView.setTransform(transform); 928 929 mPreviewSize = mNextPreviewSize; 930 931 Camera.Parameters p = mCamera.getParameters(); 932 p.setPreviewSize(mPreviewSize.width, mPreviewSize.height); 933 mCamera.setParameters(p); 934 935 try { 936 mCamera.setPreviewTexture(mPreviewTexture); 937 if (mPreviewTexture == null) { 938 Log.e(TAG, "preview texture is null."); 939 } 940 if (VERBOSE) { 941 Log.v(TAG, "startPreview: set preview texture in startPreview"); 942 } 943 mCamera.startPreview(); 944 if (VERBOSE) { 945 Log.v(TAG, "startPreview: started preview in startPreview"); 946 } 947 } catch (IOException ioe) { 948 Log.e(TAG, "Unable to start up preview", ioe); 949 // Show a dialog box to tell user test failed 950 failTest("Unable to start preview."); 951 } 952 } 953 failTest(String failMessage)954 private void failTest(String failMessage) { 955 DialogInterface.OnClickListener dialogClickListener = 956 new DialogInterface.OnClickListener() { 957 @Override 958 public void onClick(DialogInterface dialog, int which) { 959 switch (which) { 960 case DialogInterface.BUTTON_POSITIVE: 961 setTestResultAndFinish(/* passed */false); 962 break; 963 case DialogInterface.BUTTON_NEGATIVE: 964 break; 965 } 966 } 967 }; 968 969 AlertDialog.Builder builder = new AlertDialog.Builder(CameraVideoActivity.this); 970 builder.setMessage(getString(R.string.dialog_fail_test) + ". " + failMessage) 971 .setPositiveButton(R.string.fail_quit, dialogClickListener) 972 .setNegativeButton(R.string.cancel, dialogClickListener) 973 .show(); 974 } 975 isExternalCamera(int cameraId)976 private boolean isExternalCamera(int cameraId) { 977 try { 978 return CameraUtils.isExternal(this, cameraId); 979 } catch (Exception e) { 980 Toast.makeText(this, "Could not access camera " + cameraId + 981 ": " + e.getMessage(), Toast.LENGTH_LONG).show(); 982 } 983 return false; 984 } 985 986 } 987