1 /* 2 * Copyright (C) 2017 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.media.tests; 17 18 import com.android.ddmlib.CollectingOutputReceiver; 19 import com.android.tradefed.config.Option; 20 import com.android.tradefed.device.DeviceNotAvailableException; 21 import com.android.tradefed.device.IFileEntry; 22 import com.android.tradefed.device.ITestDevice; 23 import com.android.tradefed.log.LogUtil.CLog; 24 import com.android.tradefed.result.ITestInvocationListener; 25 import com.android.tradefed.result.TestDescription; 26 import com.android.tradefed.testtype.IDeviceTest; 27 import com.android.tradefed.testtype.IRemoteTest; 28 import com.android.tradefed.util.CommandResult; 29 import com.android.tradefed.util.CommandStatus; 30 import com.android.tradefed.util.FileUtil; 31 import com.android.tradefed.util.RunUtil; 32 33 import java.io.File; 34 import java.text.ParseException; 35 import java.text.SimpleDateFormat; 36 import java.util.Arrays; 37 import java.util.Date; 38 import java.util.HashMap; 39 import java.util.Map; 40 import java.util.TimeZone; 41 import java.util.concurrent.TimeUnit; 42 import java.util.regex.Matcher; 43 import java.util.regex.Pattern; 44 45 /** 46 * Tests adb command "screenrecord", i.e. "adb screenrecord [--size] [--bit-rate] [--time-limit]" 47 * 48 * <p>The test use the above command to record a video of DUT's screen. It then tries to verify that 49 * a video was actually recorded and that the video is a valid video file. It currently uses 50 * 'avprobe' to do the video analysis along with extracting parameters from the adb command's 51 * output. 52 */ 53 public class AdbScreenrecordTest implements IDeviceTest, IRemoteTest { 54 55 //=================================================================== 56 // TEST OPTIONS 57 //=================================================================== 58 @Option(name = "run-key", description = "Run key for the test") 59 private String mRunKey = "AdbScreenRecord"; 60 61 @Option(name = "time-limit", description = "Recording time in seconds", isTimeVal = true) 62 private long mRecordTimeInSeconds = -1; 63 64 @Option(name = "size", description = "Video Size: 'widthxheight', e.g. '1280x720'") 65 private String mVideoSize = null; 66 67 @Option(name = "bit-rate", description = "Video bit rate in megabits per second, e.g. 4000000") 68 private long mBitRate = -1; 69 70 //=================================================================== 71 // CLASS VARIABLES 72 //=================================================================== 73 private ITestDevice mDevice; 74 private TestRunHelper mTestRunHelper; 75 76 //=================================================================== 77 // CONSTANTS 78 //=================================================================== 79 private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000; // 5 min 80 private static final long DEVICE_SYNC_MS = 5 * 60 * 1000; // 5 min 81 private static final long POLLING_INTERVAL_MS = 5 * 1000; // 5 sec 82 private static final long CMD_TIMEOUT_MS = 5 * 1000; // 5 sec 83 private static final String ERR_OPTION_MALFORMED = "Test option %1$s is not correct [%2$s]"; 84 private static final String OPTION_TIME_LIMIT = "--time-limit"; 85 private static final String OPTION_SIZE = "--size"; 86 private static final String OPTION_BITRATE = "--bit-rate"; 87 private static final String RESULT_KEY_RECORDED_FRAMES = "recorded_frames"; 88 private static final String RESULT_KEY_RECORDED_LENGTH = "recorded_length"; 89 private static final String RESULT_KEY_VERIFIED_DURATION = "verified_duration"; 90 private static final String RESULT_KEY_VERIFIED_BITRATE = "verified_bitrate"; 91 private static final String TEST_FILE = "/sdcard/screenrecord_test.mp4"; 92 private static final String AVPROBE_NOT_INSTALLED = 93 "Program 'avprobe' is not installed on host '%1$s'"; 94 private static final String REGEX_IS_VIDEO_OK = 95 "Duration: (\\d\\d:\\d\\d:\\d\\d.\\d\\d).+bitrate: (\\d+ .b\\/s)"; 96 private static final String AVPROBE_STR = "avprobe"; 97 98 //=================================================================== 99 // ENUMS 100 //=================================================================== 101 enum HOST_SOFTWARE { 102 AVPROBE 103 } 104 105 @Override setDevice(ITestDevice device)106 public void setDevice(ITestDevice device) { 107 mDevice = device; 108 } 109 110 @Override getDevice()111 public ITestDevice getDevice() { 112 return mDevice; 113 } 114 115 /** Main test function invoked by test harness */ 116 @Override run(ITestInvocationListener listener)117 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 118 initializeTest(listener); 119 120 CLog.i("Verify required software is installed on host"); 121 verifyRequiredSoftwareIsInstalled(HOST_SOFTWARE.AVPROBE); 122 123 mTestRunHelper.startTest(1); 124 125 Map<String, String> resultsDictionary = new HashMap<String, String>(); 126 try { 127 CLog.i("Verify that test options are valid"); 128 if (!verifyTestParameters()) { 129 return; 130 } 131 132 // "resultDictionary" can be used to post results to dashboards like BlackBox 133 resultsDictionary = runTest(resultsDictionary, TEST_TIMEOUT_MS); 134 final String metricsStr = Arrays.toString(resultsDictionary.entrySet().toArray()); 135 CLog.i("Uploading metrics values:\n" + metricsStr); 136 mTestRunHelper.endTest(resultsDictionary); 137 } catch (TestFailureException e) { 138 CLog.i("TestRunHelper.reportFailure triggered"); 139 } finally { 140 deleteFileFromDevice(getAbsoluteFilename()); 141 } 142 } 143 144 /** 145 * Test code that calls "adb screenrecord" and checks for pass/fail criterias 146 * 147 * <p> 148 * 149 * <ul> 150 * <li>1. Run adb screenrecord command 151 * <li>2. Wait until there is a video file; fail if none appears 152 * <li>3. Analyze adb output and extract recorded number of frames and video length 153 * <li>4. Pull recorded video file off device 154 * <li>5. Using avprobe, analyze video file and extract duration and bitrate 155 * <li>6. Return extracted results 156 * </ul> 157 * 158 * @throws DeviceNotAvailableException 159 * @throws TestFailureException 160 */ runTest(Map<String, String> results, final long timeout)161 private Map<String, String> runTest(Map<String, String> results, final long timeout) 162 throws DeviceNotAvailableException, TestFailureException { 163 final CollectingOutputReceiver receiver = new CollectingOutputReceiver(); 164 final String cmd = generateAdbScreenRecordCommand(); 165 final String deviceFileName = getAbsoluteFilename(); 166 167 CLog.i("START Execute device shell command: '" + cmd + "'"); 168 getDevice().executeShellCommand(cmd, receiver, timeout, TimeUnit.MILLISECONDS, 3); 169 String adbOutput = receiver.getOutput(); 170 CLog.i(adbOutput); 171 CLog.i("END Execute device shell command"); 172 173 CLog.i("Wait for recorded file: " + deviceFileName); 174 if (!waitForFile(getDevice(), timeout, deviceFileName)) { 175 mTestRunHelper.reportFailure("Recorded test file not found"); 176 } 177 178 CLog.i("Get number of recorded frames and recorded length from adb output"); 179 extractVideoDataFromAdbOutput(adbOutput, results); 180 181 CLog.i("Get duration and bitrate info from video file using '" + AVPROBE_STR + "'"); 182 try { 183 extractDurationAndBitrateFromVideoFileUsingAvprobe(deviceFileName, results); 184 } catch (ParseException e) { 185 throw new RuntimeException(e); 186 } 187 deleteFileFromDevice(deviceFileName); 188 return results; 189 } 190 191 /** Convert a string on form HH:mm:ss.SS to nearest number of seconds */ convertBitrateToKilobits(String bitrate)192 private long convertBitrateToKilobits(String bitrate) { 193 Matcher m = Pattern.compile("(\\d+) (.)b\\/s").matcher(bitrate); 194 if (!m.matches()) { 195 return -1; 196 } 197 198 final String unit = m.group(2).toUpperCase(); 199 long factor = 1; 200 switch (unit) { 201 case "K": 202 factor = 1; 203 break; 204 case "M": 205 factor = 1000; 206 break; 207 case "G": 208 factor = 1000000; 209 break; 210 } 211 212 long rate = Long.parseLong(m.group(1)); 213 214 return rate * factor; 215 } 216 217 /** 218 * Convert a string on form HH:mm:ss.SS to nearest number of seconds 219 * 220 * @throws ParseException 221 */ convertDurationToMilliseconds(String duration)222 private long convertDurationToMilliseconds(String duration) throws ParseException { 223 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS"); 224 sdf.setTimeZone(TimeZone.getTimeZone("UTC")); 225 Date convertedDate = sdf.parse("1970-01-01 " + duration); 226 return convertedDate.getTime(); 227 } 228 229 /** 230 * Deletes a file off a device 231 * 232 * @param deviceFileName - path and filename to file to be deleted 233 * @throws DeviceNotAvailableException 234 */ deleteFileFromDevice(String deviceFileName)235 private void deleteFileFromDevice(String deviceFileName) throws DeviceNotAvailableException { 236 if (deviceFileName == null || deviceFileName.isEmpty()) { 237 return; 238 } 239 240 CLog.i("Delete file from device: " + deviceFileName); 241 getDevice().executeShellCommand("rm -f " + deviceFileName); 242 } 243 244 /** 245 * Extracts duration and bitrate data from a video file 246 * 247 * @throws DeviceNotAvailableException 248 * @throws ParseException 249 * @throws TestFailureException 250 */ extractDurationAndBitrateFromVideoFileUsingAvprobe( String deviceFileName, Map<String, String> results)251 private void extractDurationAndBitrateFromVideoFileUsingAvprobe( 252 String deviceFileName, Map<String, String> results) 253 throws DeviceNotAvailableException, ParseException, TestFailureException { 254 CLog.i("Check if the recorded file has some data in it: " + deviceFileName); 255 IFileEntry video = getDevice().getFileEntry(deviceFileName); 256 if (video == null || video.getFileEntry().getSizeValue() < 1) { 257 mTestRunHelper.reportFailure("Video Entry info failed"); 258 } 259 260 final File recordedVideo = getDevice().pullFile(deviceFileName); 261 CLog.i("Recorded video file: " + recordedVideo.getAbsolutePath()); 262 263 CommandResult result = 264 RunUtil.getDefault() 265 .runTimedCmd( 266 CMD_TIMEOUT_MS, 267 AVPROBE_STR, 268 "-loglevel", 269 "info", 270 recordedVideo.getAbsolutePath()); 271 272 // Remove file from host machine 273 FileUtil.deleteFile(recordedVideo); 274 275 if (result.getStatus() != CommandStatus.SUCCESS) { 276 mTestRunHelper.reportFailure(AVPROBE_STR + " command failed"); 277 } 278 279 String data = result.getStderr(); 280 CLog.i("data: " + data); 281 if (data == null || data.isEmpty()) { 282 mTestRunHelper.reportFailure(AVPROBE_STR + " output data is empty"); 283 } 284 285 Matcher m = Pattern.compile(REGEX_IS_VIDEO_OK).matcher(data); 286 if (!m.find()) { 287 final String errMsg = 288 "Video verification failed; no matching verification pattern found"; 289 mTestRunHelper.reportFailure(errMsg); 290 } 291 292 String duration = m.group(1); 293 long durationInMilliseconds = convertDurationToMilliseconds(duration); 294 String bitrate = m.group(2); 295 long bitrateInKilobits = convertBitrateToKilobits(bitrate); 296 297 results.put(RESULT_KEY_VERIFIED_DURATION, Long.toString(durationInMilliseconds / 1000)); 298 results.put(RESULT_KEY_VERIFIED_BITRATE, Long.toString(bitrateInKilobits)); 299 } 300 301 /** Extracts recorded number of frames and recorded video length from adb output 302 * @throws TestFailureException */ extractVideoDataFromAdbOutput(String adbOutput, Map<String, String> results)303 private boolean extractVideoDataFromAdbOutput(String adbOutput, Map<String, String> results) 304 throws TestFailureException { 305 final String regEx = "recorded (\\d+) frames in (\\d+) second"; 306 Matcher m = Pattern.compile(regEx).matcher(adbOutput); 307 if (!m.find()) { 308 mTestRunHelper.reportFailure("Regular Expression did not find recorded frames"); 309 return false; 310 } 311 312 int recordedFrames = Integer.parseInt(m.group(1)); 313 int recordedLength = Integer.parseInt(m.group(2)); 314 CLog.i("Recorded frames: " + recordedFrames); 315 CLog.i("Recorded length: " + recordedLength); 316 if (recordedFrames <= 0) { 317 mTestRunHelper.reportFailure("No recorded frames detected"); 318 return false; 319 } 320 321 results.put(RESULT_KEY_RECORDED_FRAMES, Integer.toString(recordedFrames)); 322 results.put(RESULT_KEY_RECORDED_LENGTH, Integer.toString(recordedLength)); 323 return true; 324 } 325 326 /** Generates an adb command from passed in test options */ generateAdbScreenRecordCommand()327 private String generateAdbScreenRecordCommand() { 328 final String SPACE = " "; 329 StringBuilder sb = new StringBuilder(128); 330 sb.append("screenrecord --verbose ").append(getAbsoluteFilename()); 331 332 // Add test options if they have been passed in to the test 333 if (mRecordTimeInSeconds != -1) { 334 final long timeLimit = TimeUnit.MILLISECONDS.toSeconds(mRecordTimeInSeconds); 335 sb.append(SPACE).append(OPTION_TIME_LIMIT).append(SPACE).append(timeLimit); 336 } 337 338 if (mVideoSize != null) { 339 sb.append(SPACE).append(OPTION_SIZE).append(SPACE).append(mVideoSize); 340 } 341 342 if (mBitRate != -1) { 343 sb.append(SPACE).append(OPTION_BITRATE).append(SPACE).append(mBitRate); 344 } 345 346 return sb.toString(); 347 } 348 349 /** Returns absolute path to device recorded video file */ getAbsoluteFilename()350 private String getAbsoluteFilename() { 351 return TEST_FILE; 352 } 353 354 /** Performs test initialization steps */ initializeTest(ITestInvocationListener listener)355 private void initializeTest(ITestInvocationListener listener) 356 throws UnsupportedOperationException, DeviceNotAvailableException { 357 TestDescription testId = new TestDescription(getClass().getCanonicalName(), mRunKey); 358 359 // Allocate helpers 360 mTestRunHelper = new TestRunHelper(listener, testId); 361 362 getDevice().disableKeyguard(); 363 getDevice().waitForDeviceAvailable(DEVICE_SYNC_MS); 364 365 CLog.i("Sync device time to host time"); 366 getDevice().setDate(new Date()); 367 } 368 369 /** Verifies that required software is installed on host machine */ verifyRequiredSoftwareIsInstalled(HOST_SOFTWARE software)370 private void verifyRequiredSoftwareIsInstalled(HOST_SOFTWARE software) { 371 String swName = ""; 372 switch (software) { 373 case AVPROBE: 374 swName = AVPROBE_STR; 375 CommandResult result = 376 RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT_MS, swName, "-version"); 377 String output = result.getStdout(); 378 if (result.getStatus() == CommandStatus.SUCCESS && output.startsWith(swName)) { 379 return; 380 } 381 break; 382 } 383 384 CLog.i("Program '" + swName + "' not found, report test failure"); 385 String hostname = RunUtil.getDefault().runTimedCmd(CMD_TIMEOUT_MS, "hostname").getStdout(); 386 387 String err = String.format(AVPROBE_NOT_INSTALLED, (hostname == null) ? "" : hostname); 388 throw new RuntimeException(err); 389 } 390 391 /** Verifies that passed in test parameters are legitimate 392 * @throws TestFailureException */ verifyTestParameters()393 private boolean verifyTestParameters() throws TestFailureException { 394 if (mRecordTimeInSeconds != -1 && mRecordTimeInSeconds < 1) { 395 final String error = 396 String.format(ERR_OPTION_MALFORMED, OPTION_TIME_LIMIT, mRecordTimeInSeconds); 397 mTestRunHelper.reportFailure(error); 398 return false; 399 } 400 401 if (mVideoSize != null) { 402 final String videoSizeRegEx = "\\d+x\\d+"; 403 Matcher m = Pattern.compile(videoSizeRegEx).matcher(mVideoSize); 404 if (!m.matches()) { 405 final String error = String.format(ERR_OPTION_MALFORMED, OPTION_SIZE, mVideoSize); 406 mTestRunHelper.reportFailure(error); 407 return false; 408 } 409 } 410 411 if (mBitRate != -1 && mBitRate < 1) { 412 final String error = String.format(ERR_OPTION_MALFORMED, OPTION_BITRATE, mBitRate); 413 mTestRunHelper.reportFailure(error); 414 return false; 415 } 416 417 return true; 418 } 419 420 /** Checks for existence of a file on the device */ waitForFile( ITestDevice device, final long timeout, final String absoluteFilename)421 private static boolean waitForFile( 422 ITestDevice device, final long timeout, final String absoluteFilename) 423 throws DeviceNotAvailableException { 424 final long checkFileStartTime = System.currentTimeMillis(); 425 426 do { 427 RunUtil.getDefault().sleep(POLLING_INTERVAL_MS); 428 if (device.doesFileExist(absoluteFilename)) { 429 return true; 430 } 431 } while (System.currentTimeMillis() - checkFileStartTime < timeout); 432 433 return false; 434 } 435 } 436