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