1/* 2 * Copyright (C) 2022 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 17import {ArrayUtils} from 'common/array_utils'; 18import {Timestamp} from 'common/time'; 19import {AbstractParser} from 'parsers/legacy/abstract_parser'; 20import {CoarseVersion} from 'trace/coarse_version'; 21import {ScreenRecordingTraceEntry} from 'trace/screen_recording'; 22import {ScreenRecordingUtils} from 'trace/screen_recording_utils'; 23import {TraceType} from 'trace/trace_type'; 24 25class ParserScreenRecording extends AbstractParser { 26 private realToBootTimeOffsetNs: bigint | undefined; 27 28 override getTraceType(): TraceType { 29 return TraceType.SCREEN_RECORDING; 30 } 31 32 override getCoarseVersion(): CoarseVersion { 33 return CoarseVersion.LATEST; 34 } 35 36 override getMagicNumber(): number[] { 37 return ParserScreenRecording.MPEG4_MAGIC_NUMBER; 38 } 39 40 override getRealToMonotonicTimeOffsetNs(): bigint | undefined { 41 return undefined; 42 } 43 44 override getRealToBootTimeOffsetNs(): bigint | undefined { 45 return this.realToBootTimeOffsetNs; 46 } 47 48 override decodeTrace(videoData: Uint8Array): Array<bigint> { 49 const posVersion = this.searchMagicString(videoData); 50 const [posTimeOffset, metadataVersion] = this.parseMetadataVersion( 51 videoData, 52 posVersion, 53 ); 54 55 if (metadataVersion !== 1 && metadataVersion !== 2) { 56 throw TypeError(`Metadata version "${metadataVersion}" not supported`); 57 } 58 59 if (metadataVersion === 1) { 60 // UI traces contain "elapsed" timestamps (SYSTEM_TIME_BOOTTIME), whereas 61 // metadata Version 1 contains SYSTEM_TIME_MONOTONIC timestamps. 62 // 63 // Here we are pretending that metadata Version 1 contains "elapsed" 64 // timestamps as well, in order to synchronize with the other traces. 65 // 66 // If no device suspensions are involved, SYSTEM_TIME_MONOTONIC should 67 // indeed correspond to SYSTEM_TIME_BOOTTIME and things will work as 68 // expected. 69 console.warn(`Screen recording may not be synchronized with the 70 other traces. Metadata contains monotonic time instead of elapsed.`); 71 } 72 73 const [posCount, timeOffsetNs] = this.parserealToBootTimeOffsetNs( 74 videoData, 75 posTimeOffset, 76 ); 77 this.realToBootTimeOffsetNs = timeOffsetNs; 78 const [posTimestamps, count] = this.parseFramesCount(videoData, posCount); 79 const timestampsElapsedNs = this.parseTimestampsElapsedNs( 80 videoData, 81 posTimestamps, 82 count, 83 ); 84 85 return timestampsElapsedNs; 86 } 87 88 protected override getTimestamp(decodedEntry: bigint): Timestamp { 89 return this.timestampConverter.makeTimestampFromBootTimeNs(decodedEntry); 90 } 91 92 override processDecodedEntry( 93 index: number, 94 entry: bigint, 95 ): ScreenRecordingTraceEntry { 96 const videoTimeSeconds = ScreenRecordingUtils.timestampToVideoTimeSeconds( 97 this.decodedEntries[0], 98 entry, 99 ); 100 const videoData = this.traceFile.file; 101 return new ScreenRecordingTraceEntry(videoTimeSeconds, videoData); 102 } 103 104 private searchMagicString(videoData: Uint8Array): number { 105 let pos = ArrayUtils.searchSubarray( 106 videoData, 107 ParserScreenRecording.WINSCOPE_META_MAGIC_STRING, 108 ); 109 if (pos === undefined) { 110 throw new TypeError("video data doesn't contain winscope magic string"); 111 } 112 pos += ParserScreenRecording.WINSCOPE_META_MAGIC_STRING.length; 113 return pos; 114 } 115 116 private parseMetadataVersion( 117 videoData: Uint8Array, 118 pos: number, 119 ): [number, number] { 120 if (pos + 4 > videoData.length) { 121 throw new TypeError( 122 'Failed to parse metadata version. Video data is too short.', 123 ); 124 } 125 const version = Number( 126 ArrayUtils.toUintLittleEndian(videoData, pos, pos + 4), 127 ); 128 pos += 4; 129 return [pos, version]; 130 } 131 132 private parserealToBootTimeOffsetNs( 133 videoData: Uint8Array, 134 pos: number, 135 ): [number, bigint] { 136 if (pos + 8 > videoData.length) { 137 throw new TypeError( 138 'Failed to parse realtime-to-elapsed time offset. Video data is too short.', 139 ); 140 } 141 const offset = ArrayUtils.toIntLittleEndian(videoData, pos, pos + 8); 142 pos += 8; 143 return [pos, offset]; 144 } 145 146 private parseFramesCount( 147 videoData: Uint8Array, 148 pos: number, 149 ): [number, number] { 150 if (pos + 4 > videoData.length) { 151 throw new TypeError( 152 'Failed to parse frames count. Video data is too short.', 153 ); 154 } 155 const count = Number( 156 ArrayUtils.toUintLittleEndian(videoData, pos, pos + 4), 157 ); 158 pos += 4; 159 return [pos, count]; 160 } 161 162 private parseTimestampsElapsedNs( 163 videoData: Uint8Array, 164 pos: number, 165 count: number, 166 ): Array<bigint> { 167 if (pos + count * 8 > videoData.length) { 168 throw new TypeError( 169 'Failed to parse timestamps. Video data is too short.', 170 ); 171 } 172 const timestamps: Array<bigint> = []; 173 for (let i = 0; i < count; ++i) { 174 const timestamp = ArrayUtils.toUintLittleEndian(videoData, pos, pos + 8); 175 pos += 8; 176 timestamps.push(timestamp); 177 } 178 return timestamps; 179 } 180 181 private static readonly MPEG4_MAGIC_NUMBER = [ 182 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32, 183 ]; // ....ftypmp42 184 private static readonly WINSCOPE_META_MAGIC_STRING = [ 185 0x23, 0x56, 0x56, 0x31, 0x4e, 0x53, 0x43, 0x30, 0x50, 0x45, 0x54, 0x31, 186 0x4d, 0x45, 0x32, 0x23, 187 ]; // #VV1NSC0PET1ME2# 188} 189 190export {ParserScreenRecording}; 191