1 /* 2 * Copyright (C) 2023 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 17 package android.tools.monitors 18 19 import android.app.Instrumentation 20 import android.media.MediaCodec 21 import android.media.MediaFormat 22 import android.media.MediaParser 23 import android.os.SystemClock 24 import android.tools.io.TraceType 25 import android.tools.traces.TRACE_CONFIG_REQUIRE_CHANGES 26 import android.tools.traces.io.ResultReader 27 import android.tools.traces.monitors.ScreenRecorder 28 import android.tools.utils.CleanFlickerEnvironmentRule 29 import android.tools.utils.newTestResultWriter 30 import androidx.test.platform.app.InstrumentationRegistry 31 import androidx.test.uiautomator.UiDevice 32 import com.google.common.truth.Truth 33 import org.junit.After 34 import org.junit.ClassRule 35 import org.junit.FixMethodOrder 36 import org.junit.Test 37 import org.junit.runners.MethodSorters 38 39 /** Contains [ScreenRecorder] tests. To run this test: `atest FlickerLibTest:ScreenRecorderTest` */ 40 @FixMethodOrder(MethodSorters.NAME_ASCENDING) 41 class ScreenRecorderTest { 42 private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() 43 private val mScreenRecorder = ScreenRecorder(instrumentation.targetContext) 44 45 @After teardownnull46 fun teardown() { 47 if (mScreenRecorder.isEnabled) { 48 mScreenRecorder.stop(newTestResultWriter()) 49 } 50 } 51 52 @Test videoIsRecordednull53 fun videoIsRecorded() { 54 mScreenRecorder.start() 55 val device = UiDevice.getInstance(instrumentation) 56 device.wakeUp() 57 SystemClock.sleep(500) 58 device.pressHome() 59 var remainingTime = TIMEOUT 60 do { 61 remainingTime -= 100 62 SystemClock.sleep(STEP) 63 } while (!mScreenRecorder.isFrameRecorded && remainingTime > 0) 64 val writer = newTestResultWriter() 65 mScreenRecorder.stop(writer) 66 val result = writer.write() 67 68 val reader = ResultReader(result, TRACE_CONFIG_REQUIRE_CHANGES) 69 Truth.assertWithMessage("Screen recording file exists") 70 .that(reader.hasTraceFile(TraceType.SCREEN_RECORDING)) 71 .isTrue() 72 73 val outputData = 74 reader.readBytes(TraceType.SCREEN_RECORDING) ?: error("Screen recording not found") 75 val (metadataTrack, videoTrack) = parseScreenRecording(outputData) 76 77 Truth.assertThat(metadataTrack.isEmpty()).isFalse() 78 Truth.assertThat(videoTrack.isEmpty()).isFalse() 79 80 val actualMagicString = metadataTrack.copyOfRange(0, WINSCOPE_MAGIC_STRING.size) 81 Truth.assertThat(actualMagicString).isEqualTo(WINSCOPE_MAGIC_STRING) 82 } 83 parseScreenRecordingnull84 private fun parseScreenRecording(data: ByteArray): Pair<ByteArray, ByteArray> { 85 val inputReader = ScreenRecorderSeekableInputReader(data) 86 val outputConsumer = ScreenRecorderOutputConsumer() 87 val mediaParser = MediaParser.create(outputConsumer) 88 89 while (mediaParser.advance(inputReader)) { 90 // no op 91 } 92 mediaParser.release() 93 94 return Pair(outputConsumer.getMetadataTrack(), outputConsumer.getVideoTrack()) 95 } 96 97 companion object { 98 private const val TIMEOUT = 10000L 99 private const val STEP = 100L 100 private val WINSCOPE_MAGIC_STRING = 101 byteArrayOf( 102 0x23, 103 0x56, 104 0x56, 105 0x31, 106 0x4e, 107 0x53, 108 0x43, 109 0x30, 110 0x50, 111 0x45, 112 0x54, 113 0x31, 114 0x4d, 115 0x45, 116 0x32, 117 0x23 118 ) // "#VV1NSC0PET1ME2#" 119 120 @ClassRule @JvmField val ENV_CLEANUP = CleanFlickerEnvironmentRule() 121 } 122 123 internal class ScreenRecorderSeekableInputReader(private val bytes: ByteArray) : 124 MediaParser.SeekableInputReader { 125 private var position = 0L 126 getPositionnull127 override fun getPosition(): Long = position 128 129 override fun getLength(): Long = bytes.size.toLong() - position 130 131 override fun seekToPosition(position: Long) { 132 this.position = position 133 } 134 readnull135 override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int { 136 if (position >= bytes.size) { 137 return -1 138 } 139 140 val actualLength = kotlin.math.min(readLength.toLong(), bytes.size - position) 141 for (i in 0 until actualLength) { 142 buffer[(offset + i).toInt()] = bytes[(position + i).toInt()] 143 } 144 145 position += actualLength 146 147 return actualLength.toInt() 148 } 149 } 150 151 internal class ScreenRecorderOutputConsumer : MediaParser.OutputConsumer { 152 private var videoTrack = ArrayList<Byte>() 153 private var metadataTrack = ArrayList<Byte>() 154 private var videoTrackIndex = -1 155 private var metadataTrackIndex = -1 156 private val auxBuffer = ByteArray(4 * 1024) 157 getVideoTracknull158 fun getVideoTrack(): ByteArray { 159 return videoTrack.toByteArray() 160 } 161 getMetadataTracknull162 fun getMetadataTrack(): ByteArray { 163 return metadataTrack.toByteArray() 164 } 165 onSeekMapFoundnull166 override fun onSeekMapFound(seekMap: MediaParser.SeekMap) { 167 // do nothing 168 } 169 onTrackCountFoundnull170 override fun onTrackCountFound(numberOfTracks: Int) { 171 Truth.assertThat(numberOfTracks).isEqualTo(2) 172 } 173 onTrackDataFoundnull174 override fun onTrackDataFound(i: Int, trackData: MediaParser.TrackData) { 175 if ( 176 videoTrackIndex == -1 && 177 trackData.mediaFormat.getString(MediaFormat.KEY_MIME, "").startsWith("video/") 178 ) { 179 videoTrackIndex = i 180 } 181 182 if ( 183 metadataTrackIndex == -1 && 184 trackData.mediaFormat.getString(MediaFormat.KEY_MIME, "") == 185 "application/octet-stream" 186 ) { 187 metadataTrackIndex = i 188 } 189 } 190 onSampleDataFoundnull191 override fun onSampleDataFound(trackIndex: Int, inputReader: MediaParser.InputReader) { 192 when (trackIndex) { 193 videoTrackIndex -> processSampleData(inputReader, videoTrack) 194 metadataTrackIndex -> processSampleData(inputReader, metadataTrack) 195 else -> throw RuntimeException("unexpected track index: $trackIndex") 196 } 197 } 198 onSampleCompletednull199 override fun onSampleCompleted( 200 trackIndex: Int, 201 timeMicros: Long, 202 flags: Int, 203 size: Int, 204 offset: Int, 205 cryptoData: MediaCodec.CryptoInfo? 206 ) { 207 // do nothing 208 } 209 processSampleDatanull210 private fun processSampleData( 211 inputReader: MediaParser.InputReader, 212 buffer: ArrayList<Byte> 213 ) { 214 while (inputReader.length > 0) { 215 val requestLength = kotlin.math.min(inputReader.length, auxBuffer.size.toLong()) 216 val actualLength = inputReader.read(auxBuffer, 0, requestLength.toInt()) 217 if (actualLength == -1) { 218 break 219 } 220 221 for (i in 0 until actualLength) { 222 buffer.add(auxBuffer[i]) 223 } 224 } 225 } 226 } 227 } 228