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