• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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