MediaRecorderTest.java revision 699780f54b7ddd7b00f891218140df663b3ab84e
1/* 2 * Copyright (C) 2008 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 17package com.android.mediaframeworktest.functional.mediarecorder; 18 19import com.android.mediaframeworktest.MediaFrameworkTest; 20import com.android.mediaframeworktest.MediaNames; 21 22import java.io.*; 23 24import android.content.Context; 25import android.graphics.Canvas; 26import android.graphics.Color; 27import android.graphics.Paint; 28import android.graphics.Typeface; 29import android.hardware.Camera; 30import android.media.MediaMetadataRetriever; 31import android.media.MediaPlayer; 32import android.media.MediaRecorder; 33import android.media.EncoderCapabilities; 34import android.media.EncoderCapabilities.VideoEncoderCap; 35import android.media.EncoderCapabilities.AudioEncoderCap; 36import android.test.ActivityInstrumentationTestCase2; 37import android.util.Log; 38import android.view.Surface; 39import android.view.SurfaceHolder; 40import android.view.SurfaceView; 41import com.android.mediaframeworktest.MediaProfileReader; 42import com.android.mediaframeworktest.MediaFrameworkTestRunner; 43 44import android.test.suitebuilder.annotation.LargeTest; 45import android.test.suitebuilder.annotation.Suppress; 46import java.util.List; 47 48 49/** 50 * Junit / Instrumentation test case for the media recorder api 51 */ 52public class MediaRecorderTest extends ActivityInstrumentationTestCase2<MediaFrameworkTest> { 53 private String TAG = "MediaRecorderTest"; 54 private int mOutputDuration =0; 55 private int mOutputVideoWidth = 0; 56 private int mOutputVideoHeight= 0 ; 57 58 private SurfaceHolder mSurfaceHolder = null; 59 private MediaRecorder mRecorder; 60 61 private int MIN_VIDEO_FPS = 5; 62 private int HIGH_SPEED_FPS = 120; 63 64 private static final int CAMERA_ID = 0; 65 66 Context mContext; 67 Camera mCamera; 68 69 public MediaRecorderTest() { 70 super(MediaFrameworkTest.class); 71 72 } 73 74 protected void setUp() throws Exception { 75 getActivity(); 76 mRecorder = new MediaRecorder(); 77 super.setUp(); 78 } 79 80 private void recordVideo(int frameRate, int width, int height, 81 int videoFormat, int outFormat, String outFile, boolean videoOnly) { 82 Log.v(TAG,"startPreviewAndPrepareRecording"); 83 try { 84 if (!videoOnly) { 85 Log.v(TAG, "setAudioSource"); 86 mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 87 } 88 mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 89 mRecorder.setOutputFormat(outFormat); 90 Log.v(TAG, "output format " + outFormat); 91 mRecorder.setOutputFile(outFile); 92 mRecorder.setVideoFrameRate(frameRate); 93 mRecorder.setVideoSize(width, height); 94 Log.v(TAG, "setEncoder"); 95 mRecorder.setVideoEncoder(videoFormat); 96 if (!videoOnly) { 97 mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 98 } 99 mSurfaceHolder = MediaFrameworkTest.mSurfaceView.getHolder(); 100 Log.v(TAG, "setPreview"); 101 mRecorder.setPreviewDisplay(mSurfaceHolder.getSurface()); 102 Log.v(TAG, "prepare"); 103 mRecorder.prepare(); 104 Log.v(TAG, "start"); 105 mRecorder.start(); 106 Thread.sleep(MediaNames.RECORDED_TIME); 107 Log.v(TAG, "stop"); 108 mRecorder.stop(); 109 mRecorder.release(); 110 } catch (Exception e) { 111 Log.v("record video failed ", e.toString()); 112 mRecorder.release(); 113 } 114 } 115 116 private boolean validateGetSurface(boolean useSurface) { 117 Log.v(TAG,"validateGetSurface, useSurface=" + useSurface); 118 MediaRecorder recorder = new MediaRecorder(); 119 Surface surface; 120 boolean success = true; 121 try { 122 /* initialization */ 123 if (!useSurface) { 124 mCamera = Camera.open(CAMERA_ID); 125 Camera.Parameters parameters = mCamera.getParameters(); 126 parameters.setPreviewSize(352, 288); 127 parameters.set("orientation", "portrait"); 128 mCamera.setParameters(parameters); 129 mCamera.unlock(); 130 recorder.setCamera(mCamera); 131 mSurfaceHolder = MediaFrameworkTest.mSurfaceView.getHolder(); 132 recorder.setPreviewDisplay(mSurfaceHolder.getSurface()); 133 } 134 135 recorder.setAudioSource(MediaRecorder.AudioSource.MIC); 136 int videoSource = useSurface ? 137 MediaRecorder.VideoSource.SURFACE : 138 MediaRecorder.VideoSource.CAMERA; 139 recorder.setVideoSource(videoSource); 140 recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 141 recorder.setOutputFile(MediaNames.RECORDED_SURFACE_3GP); 142 recorder.setVideoFrameRate(30); 143 recorder.setVideoSize(352, 288); 144 recorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); 145 recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 146 147 /* Test: getSurface() before prepare() 148 * should throw IllegalStateException 149 */ 150 try { 151 surface = recorder.getSurface(); 152 throw new Exception("getSurface failed to throw IllegalStateException"); 153 } catch (IllegalStateException e) { 154 // OK 155 } 156 157 recorder.prepare(); 158 159 /* Test: getSurface() after prepare() 160 * should succeed for surface source 161 * should fail for camera source 162 */ 163 try { 164 surface = recorder.getSurface(); 165 if (!useSurface) { 166 throw new Exception("getSurface failed to throw IllegalStateException"); 167 } 168 } catch (IllegalStateException e) { 169 if (useSurface) { 170 throw new Exception("getSurface failed to throw IllegalStateException"); 171 } 172 } 173 174 recorder.start(); 175 176 /* Test: getSurface() after start() 177 * should succeed for surface source 178 * should fail for camera source 179 */ 180 try { 181 surface = recorder.getSurface(); 182 if (!useSurface) { 183 throw new Exception("getSurface failed to throw IllegalStateException"); 184 } 185 } catch (IllegalStateException e) { 186 if (useSurface) { 187 throw new Exception("getSurface failed to throw IllegalStateException"); 188 } 189 } 190 191 try { 192 recorder.stop(); 193 } catch (Exception e) { 194 // stop() could fail if the recording is empty, as we didn't render anything. 195 // ignore any failure in stop, we just want it stopped. 196 } 197 198 /* Test: getSurface() after stop() 199 * should throw IllegalStateException 200 */ 201 try { 202 surface = recorder.getSurface(); 203 throw new Exception("getSurface failed to throw IllegalStateException"); 204 } catch (IllegalStateException e) { 205 // OK 206 } 207 } catch (Exception e) { 208 // fail 209 success = false; 210 } 211 212 try { 213 if (mCamera != null) { 214 mCamera.lock(); 215 mCamera.release(); 216 mCamera = null; 217 } 218 recorder.release(); 219 } catch (Exception e) { 220 success = false; 221 } 222 223 return success; 224 } 225 226 private boolean recordVideoFromSurface( 227 int frameRate, int captureRate, int width, int height, 228 int videoFormat, int outFormat, String outFile, boolean videoOnly) { 229 Log.v(TAG,"recordVideoFromSurface"); 230 MediaRecorder recorder = new MediaRecorder(); 231 int sleepTime = 33; // normal capture at 33ms / frame 232 try { 233 if (!videoOnly) { 234 recorder.setAudioSource(MediaRecorder.AudioSource.MIC); 235 } 236 recorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); 237 recorder.setOutputFormat(outFormat); 238 recorder.setOutputFile(outFile); 239 recorder.setVideoFrameRate(frameRate); 240 if (captureRate > 0) { 241 recorder.setCaptureRate(captureRate); 242 sleepTime = 1000 / captureRate; 243 } 244 recorder.setVideoSize(width, height); 245 recorder.setVideoEncoder(videoFormat); 246 if (!videoOnly) { 247 recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 248 } 249 recorder.prepare(); 250 Surface surface = recorder.getSurface(); 251 252 Paint paint = new Paint(); 253 paint.setTextSize(16); 254 paint.setColor(Color.RED); 255 int i; 256 257 /* Test: draw 10 frames at 30fps before start 258 * these should be dropped and not causing malformed stream. 259 */ 260 for(i = 0; i < 10; i++) { 261 Canvas canvas = surface.lockCanvas(null); 262 int background = (i * 255 / 99); 263 canvas.drawARGB(255, background, background, background); 264 String text = "Frame #" + i; 265 canvas.drawText(text, 100, 100, paint); 266 surface.unlockCanvasAndPost(canvas); 267 Thread.sleep(sleepTime); 268 } 269 270 Log.v(TAG, "start"); 271 recorder.start(); 272 273 /* Test: draw another 90 frames at 30fps after start */ 274 for(i = 10; i < 100; i++) { 275 Canvas canvas = surface.lockCanvas(null); 276 int background = (i * 255 / 99); 277 canvas.drawARGB(255, background, background, background); 278 String text = "Frame #" + i; 279 canvas.drawText(text, 100, 100, paint); 280 surface.unlockCanvasAndPost(canvas); 281 Thread.sleep(sleepTime); 282 } 283 284 Log.v(TAG, "stop"); 285 recorder.stop(); 286 recorder.release(); 287 } catch (Exception e) { 288 Log.v("record video failed ", e.toString()); 289 recorder.release(); 290 return false; 291 } 292 return true; 293 } 294 295 private boolean recordVideoWithPara(VideoEncoderCap videoCap, AudioEncoderCap audioCap, boolean highQuality){ 296 boolean recordSuccess = false; 297 int videoEncoder = videoCap.mCodec; 298 int audioEncoder = audioCap.mCodec; 299 int videoWidth = highQuality? videoCap.mMaxFrameWidth: videoCap.mMinFrameWidth; 300 int videoHeight = highQuality? videoCap.mMaxFrameHeight: videoCap.mMinFrameHeight; 301 int videoFps = highQuality? videoCap.mMaxFrameRate: videoCap.mMinFrameRate; 302 int videoBitrate = highQuality? videoCap.mMaxBitRate: videoCap.mMinBitRate; 303 int audioBitrate = highQuality? audioCap.mMaxBitRate: audioCap.mMinBitRate; 304 int audioChannels = highQuality? audioCap.mMaxChannels: audioCap.mMinChannels ; 305 int audioSamplingRate = highQuality? audioCap.mMaxSampleRate: audioCap.mMinSampleRate; 306 307 //Overide the fps if the min_camera_fps is set 308 if (MediaFrameworkTestRunner.mMinCameraFps != 0 && 309 MediaFrameworkTestRunner.mMinCameraFps > videoFps){ 310 videoFps = MediaFrameworkTestRunner.mMinCameraFps; 311 } 312 313 if (videoFps < MIN_VIDEO_FPS) { 314 videoFps = MIN_VIDEO_FPS; 315 } 316 317 mSurfaceHolder = MediaFrameworkTest.mSurfaceView.getHolder(); 318 String filename = ("/sdcard/" + videoEncoder + "_" + audioEncoder + "_" + highQuality + ".3gp"); 319 try { 320 Log.v(TAG, "video encoder : " + videoEncoder); 321 Log.v(TAG, "audio encoder : " + audioEncoder); 322 Log.v(TAG, "quality : " + (highQuality?"high": "low")); 323 Log.v(TAG, "encoder : " + MediaProfileReader.getVideoCodecName(videoEncoder)); 324 Log.v(TAG, "audio : " + MediaProfileReader.getAudioCodecName(audioEncoder)); 325 Log.v(TAG, "videoWidth : " + videoWidth); 326 Log.v(TAG, "videoHeight : " + videoHeight); 327 Log.v(TAG, "videoFPS : " + videoFps); 328 Log.v(TAG, "videobitrate : " + videoBitrate); 329 Log.v(TAG, "audioBitrate : " + audioBitrate); 330 Log.v(TAG, "audioChannel : " + audioChannels); 331 Log.v(TAG, "AudioSampleRate : " + audioSamplingRate); 332 333 MediaRecorder mMediaRecorder = new MediaRecorder(); 334 mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 335 mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 336 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 337 mMediaRecorder.setOutputFile(filename); 338 mMediaRecorder.setVideoFrameRate(videoFps); 339 mMediaRecorder.setVideoSize(videoWidth, videoHeight); 340 mMediaRecorder.setVideoEncodingBitRate(videoBitrate); 341 mMediaRecorder.setAudioEncodingBitRate(audioBitrate); 342 mMediaRecorder.setAudioChannels(audioChannels); 343 mMediaRecorder.setAudioSamplingRate(audioSamplingRate); 344 mMediaRecorder.setVideoEncoder(videoEncoder); 345 mMediaRecorder.setAudioEncoder(audioEncoder); 346 mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface()); 347 mMediaRecorder.prepare(); 348 mMediaRecorder.start(); 349 Thread.sleep(MediaNames.RECORDED_TIME); 350 mMediaRecorder.stop(); 351 mMediaRecorder.release(); 352 recordSuccess = validateVideo(filename, videoWidth, videoHeight); 353 } catch (Exception e) { 354 Log.v(TAG, e.toString()); 355 return false; 356 } 357 return recordSuccess; 358 } 359 360 private boolean invalidRecordSetting(int frameRate, int width, int height, 361 int videoFormat, int outFormat, String outFile, boolean videoOnly) { 362 try { 363 if (!videoOnly) { 364 Log.v(TAG, "setAudioSource"); 365 mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 366 } 367 mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); 368 mRecorder.setOutputFormat(outFormat); 369 Log.v(TAG, "output format " + outFormat); 370 mRecorder.setOutputFile(outFile); 371 mRecorder.setVideoFrameRate(frameRate); 372 mRecorder.setVideoSize(width, height); 373 Log.v(TAG, "setEncoder"); 374 mRecorder.setVideoEncoder(videoFormat); 375 if (!videoOnly) { 376 mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); 377 } 378 mSurfaceHolder = MediaFrameworkTest.mSurfaceView.getHolder(); 379 Log.v(TAG, "setPreview"); 380 mRecorder.setPreviewDisplay(mSurfaceHolder.getSurface()); 381 Log.v(TAG, "prepare"); 382 mRecorder.prepare(); 383 Log.v(TAG, "start"); 384 mRecorder.start(); 385 Thread.sleep(MediaNames.RECORDED_TIME); 386 Log.v(TAG, "stop"); 387 mRecorder.stop(); 388 mRecorder.release(); 389 } catch (Exception e) { 390 Log.v("record video failed ", e.toString()); 391 mRecorder.release(); 392 Log.v(TAG, "reset and release"); 393 return true; 394 } 395 return false; 396 } 397 398 private void getOutputVideoProperty(String outputFilePath) { 399 MediaPlayer mediaPlayer = new MediaPlayer(); 400 try { 401 mediaPlayer.setDataSource(outputFilePath); 402 Log.v(TAG, "file Path = " + outputFilePath); 403 mediaPlayer.setDisplay(MediaFrameworkTest.mSurfaceView.getHolder()); 404 Log.v(TAG, "before player prepare"); 405 mediaPlayer.prepare(); 406 Log.v(TAG, "before getduration"); 407 mOutputDuration = mediaPlayer.getDuration(); 408 Log.v(TAG, "get video dimension"); 409 Thread.sleep(1000); 410 mOutputVideoHeight = mediaPlayer.getVideoHeight(); 411 mOutputVideoWidth = mediaPlayer.getVideoWidth(); 412 mediaPlayer.release(); 413 } catch (Exception e) { 414 Log.v(TAG, e.toString()); 415 mediaPlayer.release(); 416 } 417 } 418 419 private boolean validateVideo(String filePath, int width, int height) { 420 boolean validVideo = false; 421 getOutputVideoProperty(filePath); 422 if (mOutputVideoWidth == width && mOutputVideoHeight == height && 423 mOutputDuration > MediaNames.VALID_VIDEO_DURATION ) { 424 validVideo = true; 425 } 426 Log.v(TAG, "width = " + mOutputVideoWidth + " height = " + mOutputVideoHeight + " Duration = " + mOutputDuration); 427 return validVideo; 428 } 429 430 private boolean validateMetadata(String filePath, int captureRate) { 431 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 432 433 retriever.setDataSource(filePath); 434 435 // verify capture rate meta key is present and correct 436 String captureFps = retriever.extractMetadata( 437 MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE); 438 439 if (captureFps == null) { 440 Log.d(TAG, "METADATA_KEY_CAPTURE_FRAMERATE is missing"); 441 return false; 442 } 443 444 if (Math.abs(Float.parseFloat(captureFps) - captureRate) > 0.001) { 445 Log.d(TAG, "METADATA_KEY_CAPTURE_FRAMERATE is incorrect: " 446 + captureFps + "vs. " + captureRate); 447 return false; 448 } 449 450 // verify other meta keys here if necessary 451 return true; 452 } 453 @LargeTest 454 /* 455 * This test case set the camera in portrait mode. 456 * Verification: validate the video dimension and the duration. 457 */ 458 public void testPortraitH263() throws Exception { 459 boolean videoRecordedResult = false; 460 try { 461 mCamera = Camera.open(CAMERA_ID); 462 Camera.Parameters parameters = mCamera.getParameters(); 463 parameters.setPreviewSize(352, 288); 464 parameters.set("orientation", "portrait"); 465 mCamera.setParameters(parameters); 466 mCamera.unlock(); 467 mRecorder.setCamera(mCamera); 468 Thread.sleep(1000); 469 int codec = MediaRecorder.VideoEncoder.H263; 470 int frameRate = MediaProfileReader.getMaxFrameRateForCodec(codec); 471 recordVideo(frameRate, 352, 288, codec, 472 MediaRecorder.OutputFormat.THREE_GPP, 473 MediaNames.RECORDED_PORTRAIT_H263, true); 474 mCamera.lock(); 475 mCamera.release(); 476 videoRecordedResult = 477 validateVideo(MediaNames.RECORDED_PORTRAIT_H263, 352, 288); 478 } catch (Exception e) { 479 Log.v(TAG, e.toString()); 480 } 481 assertTrue("PortraitH263", videoRecordedResult); 482 } 483 484 @LargeTest 485 public void testInvalidVideoPath() throws Exception { 486 boolean isTestInvalidVideoPathSuccessful = false; 487 isTestInvalidVideoPathSuccessful = invalidRecordSetting(15, 176, 144, MediaRecorder.VideoEncoder.H263, 488 MediaRecorder.OutputFormat.THREE_GPP, MediaNames.INVALD_VIDEO_PATH, false); 489 assertTrue("Invalid outputFile Path", isTestInvalidVideoPathSuccessful); 490 } 491 492 @LargeTest 493 //test cases for the new codec 494 public void testDeviceSpecificCodec() throws Exception { 495 int noOfFailure = 0; 496 boolean recordSuccess = false; 497 String deviceType = MediaProfileReader.getDeviceType(); 498 Log.v(TAG, "deviceType = " + deviceType); 499 List<VideoEncoderCap> videoEncoders = MediaProfileReader.getVideoEncoders(); 500 List<AudioEncoderCap> audioEncoders = MediaProfileReader.getAudioEncoders(); 501 for (int k = 0; k < 2; k++) { 502 for (VideoEncoderCap videoEncoder: videoEncoders) { 503 for (AudioEncoderCap audioEncoder: audioEncoders) { 504 if (k == 0) { 505 recordSuccess = recordVideoWithPara(videoEncoder, audioEncoder, true); 506 } else { 507 recordSuccess = recordVideoWithPara(videoEncoder, audioEncoder, false); 508 } 509 if (!recordSuccess) { 510 Log.v(TAG, "testDeviceSpecificCodec failed"); 511 Log.v(TAG, "Encoder = " + videoEncoder.mCodec + "Audio Encoder = " + audioEncoder.mCodec); 512 noOfFailure++; 513 } 514 } 515 } 516 } 517 if (noOfFailure != 0) { 518 assertTrue("testDeviceSpecificCodec", false); 519 } 520 } 521 522 // Test MediaRecorder.getSurface() api with surface or camera source 523 public void testGetSurfaceApi() { 524 boolean success = false; 525 int noOfFailure = 0; 526 try { 527 for (int k = 0; k < 2; k++) { 528 success = validateGetSurface( 529 k == 0 ? true : false /* useSurface */); 530 if (!success) { 531 noOfFailure++; 532 } 533 } 534 } catch (Exception e) { 535 Log.v(TAG, e.toString()); 536 } 537 assertTrue("testGetSurfaceApi", noOfFailure == 0); 538 } 539 540 // Test recording from surface source with/without audio 541 public void testSurfaceRecording() { 542 boolean success = false; 543 int noOfFailure = 0; 544 try { 545 int codec = MediaRecorder.VideoEncoder.H264; 546 int frameRate = MediaProfileReader.getMaxFrameRateForCodec(codec); 547 for (int k = 0; k < 2; k++) { 548 String filename = "/sdcard/surface_" + 549 (k==0?"video_only":"with_audio") + ".3gp"; 550 551 success = recordVideoFromSurface(frameRate, 0, 352, 288, codec, 552 MediaRecorder.OutputFormat.THREE_GPP, filename, 553 k == 0 ? true : false /* videoOnly */); 554 if (success) { 555 success = validateVideo(filename, 352, 288); 556 } 557 if (!success) { 558 noOfFailure++; 559 } 560 } 561 } catch (Exception e) { 562 Log.v(TAG, e.toString()); 563 } 564 assertTrue("testSurfaceRecording", noOfFailure == 0); 565 } 566 567 // Test recording from surface source with/without audio 568 public void testSurfaceRecordingTimeLapse() { 569 boolean success = false; 570 int noOfFailure = 0; 571 try { 572 int codec = MediaRecorder.VideoEncoder.H264; 573 int frameRate = MediaProfileReader.getMaxFrameRateForCodec(codec); 574 for (int k = 0; k < 2; k++) { 575 // k==0: time lapse test, set capture rate to MIN_VIDEO_FPS 576 // k==1: slow motion test, set capture rate to HIGH_SPEED_FPS 577 String filename = "/sdcard/surface_" + 578 (k==0 ? "time_lapse" : "slow_motion") + ".3gp"; 579 580 // always set videoOnly=false, MediaRecorder should disable 581 // audio automatically with time lapse/slow motion 582 int captureRate = k==0 ? MIN_VIDEO_FPS : HIGH_SPEED_FPS; 583 success = recordVideoFromSurface( 584 frameRate, captureRate, 352, 288, codec, 585 MediaRecorder.OutputFormat.THREE_GPP, 586 filename, false /* videoOnly */); 587 if (success) { 588 success = validateVideo(filename, 352, 288); 589 if (success) { 590 success = validateMetadata(filename, captureRate); 591 } 592 } 593 if (!success) { 594 noOfFailure++; 595 } 596 } 597 } catch (Exception e) { 598 Log.v(TAG, e.toString()); 599 noOfFailure++; 600 } 601 assertTrue("testSurfaceRecordingTimeLapse", noOfFailure == 0); 602 } 603 604} 605