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