parser for new screen recording metadata

Bug: 235196806
Test: cd development/tools/winscope-ng && npm run build:unit && npm run test:unit
Change-Id: Ie964b30f2f88a35ce428fab9fe1da192599da6e5
This commit is contained in:
Kean Mariotti
2022-07-11 13:59:38 +00:00
parent 99a2ff31bc
commit 75862f22be
9 changed files with 299 additions and 44 deletions

View File

@@ -81,15 +81,74 @@ describe("ArrayUtils", () => {
it("toUintLittleEndian", () => {
const buffer = new Uint8Array([0, 0, 1, 1]);
expect(ArrayUtils.toUintLittleEndian(buffer, 0, -1)).toEqual(0);
expect(ArrayUtils.toUintLittleEndian(buffer, 0, 0)).toEqual(0);
expect(ArrayUtils.toUintLittleEndian(new Uint8Array([0xff, 0xff]), 0, -1)).toEqual(0n);
expect(ArrayUtils.toUintLittleEndian(new Uint8Array([0xff, 0xff]), 0, 0)).toEqual(0n);
expect(ArrayUtils.toUintLittleEndian(new Uint8Array([0xff, 0xff]), 1, 1)).toEqual(0n);
expect(ArrayUtils.toUintLittleEndian(buffer, 0, 1)).toEqual(0);
expect(ArrayUtils.toUintLittleEndian(buffer, 0, 2)).toEqual(0);
expect(ArrayUtils.toUintLittleEndian(new Uint8Array([0x00, 0x01, 0xff]), 0, 1)).toEqual(0n);
expect(ArrayUtils.toUintLittleEndian(new Uint8Array([0x00, 0x01, 0xff]), 1, 2)).toEqual(1n);
expect(ArrayUtils.toUintLittleEndian(new Uint8Array([0x00, 0x01, 0xff]), 2, 3)).toEqual(255n);
expect(ArrayUtils.toUintLittleEndian(buffer, 3, 4)).toEqual(1);
expect(ArrayUtils.toUintLittleEndian(buffer, 2, 4)).toEqual(1 + 256);
expect(ArrayUtils.toUintLittleEndian(buffer, 1, 4)).toEqual(256 + 256*256);
expect(ArrayUtils.toUintLittleEndian(buffer, 0, 3)).toEqual(256*256);
expect(ArrayUtils.toUintLittleEndian(new Uint8Array([0x00, 0x00]), 0, 2)).toEqual(0n);
expect(ArrayUtils.toUintLittleEndian(new Uint8Array([0x01, 0x00]), 0, 2)).toEqual(1n);
expect(ArrayUtils.toUintLittleEndian(new Uint8Array([0x00, 0x01]), 0, 2)).toEqual(256n);
expect(ArrayUtils.toUintLittleEndian(new Uint8Array([0xff, 0xff]), 0, 2)).toEqual(0xffffn);
expect(ArrayUtils.toUintLittleEndian(new Uint8Array([0xff, 0xff, 0xff, 0xff]), 0, 4)).toEqual(0xffffffffn);
expect(
ArrayUtils.toUintLittleEndian(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), 0, 8))
.toEqual(0xffffffffffffffffn);
expect(
ArrayUtils.toUintLittleEndian(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), 0, 9))
.toEqual(0xffffffffffffffffffn);
});
it("toIntLittleEndian", () => {
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0xff]), 0, -1)).toEqual(0n);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0xff]), 0, 0)).toEqual(0n);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0x00]), 0, 1)).toEqual(0n);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0x01]), 0, 1)).toEqual(1n);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0x7f]), 0, 1)).toEqual(127n);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0x80]), 0, 1)).toEqual(-128n);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0xff]), 0, 1)).toEqual(-1n);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0xff, 0x7f]), 0, 2)).toEqual(32767n);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0x00, 0x80]), 0, 2)).toEqual(-32768n);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0x01, 0x80]), 0, 2)).toEqual(-32767n);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0xff, 0xff]), 0, 2)).toEqual(-1n);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0xff, 0xff, 0xff, 0x7f]), 0, 4)).toEqual(0x7fffffffn);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0x00, 0x00, 0x00, 0x80]), 0, 4)).toEqual(-0x80000000n);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0x01, 0x00, 0x00, 0x80]), 0, 4)).toEqual(-0x7fffffffn);
expect(ArrayUtils.toIntLittleEndian(new Uint8Array([0xff, 0xff, 0xff, 0xff]), 0, 4)).toEqual(-1n);
expect(
ArrayUtils.toIntLittleEndian(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f]), 0, 8)
).toEqual(0x7fffffffffffffffn);
expect(
ArrayUtils.toIntLittleEndian(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80]), 0, 8)
).toEqual(-0x8000000000000000n);
expect(
ArrayUtils.toIntLittleEndian(new Uint8Array([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80]), 0, 8)
).toEqual(-0x7fffffffffffffffn);
expect(
ArrayUtils.toIntLittleEndian(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), 0, 8)
).toEqual(-1n);
expect(
ArrayUtils.toIntLittleEndian(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f]), 0, 9)
).toEqual(0x7fffffffffffffffffn);
expect(
ArrayUtils.toIntLittleEndian(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80]), 0, 9)
).toEqual(-0x800000000000000000n);
expect(
ArrayUtils.toIntLittleEndian(new Uint8Array([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80]), 0, 9)
).toEqual(-0x7fffffffffffffffffn);
expect(
ArrayUtils.toIntLittleEndian(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), 0, 9)
).toEqual(-1n);
});
});

View File

@@ -86,14 +86,30 @@ class ArrayUtils {
return result;
}
static toUintLittleEndian(buffer: Uint8Array, start: number, end: number) {
let result = 0;
for (let i = end-1; i>=start; --i) {
result *= 256;
result += buffer[i];
static toUintLittleEndian(buffer: Uint8Array, start: number, end: number): bigint {
let result = 0n;
for (let i = end-1; i >= start; --i) {
result *= 256n;
result += BigInt(buffer[i]);
}
return result;
}
static toIntLittleEndian(buffer: Uint8Array, start: number, end: number): bigint {
const numOfBits = BigInt(Math.max(0, 8 * (end-start)));
if (numOfBits <= 0n) {
return 0n;
}
let result = ArrayUtils.toUintLittleEndian(buffer, start, end);
const maxSignedValue = 2n ** (numOfBits - 1n) - 1n;
if (result > maxSignedValue) {
const valuesRange = 2n ** numOfBits;
result -= valuesRange;
}
return result;
}
}
export {ArrayUtils};

View File

@@ -20,6 +20,7 @@ import {ParserInputMethodManagerService} from "./parser_input_method_manager_ser
import {ParserInputMethodService} from "./parser_input_method_service";
import {ParserProtoLog} from "./parser_protolog";
import {ParserScreenRecording} from "./parser_screen_recording";
import {ParserScreenRecordingLegacy} from "./parser_screen_recording_legacy";
import {ParserSurfaceFlinger} from "./parser_surface_flinger";
import {ParserTransactions} from "./parser_transactions";
import {ParserWindowManager} from "./parser_window_manager";
@@ -33,6 +34,7 @@ class ParserFactory {
ParserInputMethodService,
ParserProtoLog,
ParserScreenRecording,
ParserScreenRecordingLegacy,
ParserSurfaceFlinger,
ParserTransactions,
ParserWindowManager,

View File

@@ -23,8 +23,8 @@ describe("ParserScreenRecording", () => {
let parser: Parser;
beforeAll(async () => {
const buffer = TestUtils.getFixtureBlob("screen_recording.mp4");
const parsers = await new ParserFactory().createParsers([buffer]);
const trace = TestUtils.getFixtureBlob("screen_recording.mp4");
const parsers = await new ParserFactory().createParsers([trace]);
expect(parsers.length).toEqual(1);
parser = parsers[0];
});
@@ -37,26 +37,23 @@ describe("ParserScreenRecording", () => {
const timestamps = parser.getTimestamps();
expect(timestamps.length)
.toEqual(85);
.toEqual(88);
expect(timestamps.slice(0, 3))
.toEqual([19446131807000, 19446158500000, 19446167117000]);
expect(timestamps.slice(timestamps.length-3, timestamps.length))
.toEqual([19448470076000, 19448487525000, 19448501007000]);
.toEqual([1658843852566916400, 1658843852889741300, 1658843852901528300]);
});
it("retrieves trace entry", () => {
{
const entry = parser.getTraceEntry(19446131807000)!;
const entry = parser.getTraceEntry(1658843852566916400)!;
expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0);
}
{
const entry = parser.getTraceEntry(19448501007000)!;
const entry = parser.getTraceEntry(1658843852889741300)!;
expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(2.37, 0.001);
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0.322, 0.001);
}
});
});

View File

@@ -18,6 +18,11 @@ import {ArrayUtils} from "common/utils/array_utils";
import {Parser} from "./parser";
import {ScreenRecordingTraceEntry} from "common/trace/screen_recording";
class ScreenRecordingMetadataEntry {
constructor(public timestampMonotonicNs: bigint, public timestampRealtimeNs: bigint) {
}
}
class ParserScreenRecording extends Parser {
constructor(trace: Blob) {
super(trace);
@@ -31,20 +36,29 @@ class ParserScreenRecording extends Parser {
return ParserScreenRecording.MPEG4_MAGIC_NMBER;
}
override decodeTrace(videoData: Uint8Array): number[] {
const posCount = this.searchMagicString(videoData);
const [posTimestamps, count] = this.parseTimestampsCount(videoData, posCount);
return this.parseTimestamps(videoData, posTimestamps, count);
override decodeTrace(videoData: Uint8Array): ScreenRecordingMetadataEntry[] {
const posVersion = this.searchMagicString(videoData);
const [posTimeOffset, metadataVersion] = this.parseMetadataVersion(videoData, posVersion);
if (metadataVersion !== 1) {
throw TypeError(`Metadata version "${metadataVersion}" not supported`);
}
const [posCount, timeOffsetNs] = this.parseRealToMonotonicTimeOffsetNs(videoData, posTimeOffset);
const [posTimestamps, count] = this.parseFramesCount(videoData, posCount);
const timestampsMonotonicNs = this.parseTimestampsMonotonicNs(videoData, posTimestamps, count);
return timestampsMonotonicNs.map((timestampMonotonicNs: bigint) => {
return new ScreenRecordingMetadataEntry(timestampMonotonicNs, timestampMonotonicNs + timeOffsetNs);
});
}
override getTimestamp(decodedEntry: number): number {
return decodedEntry;
override getTimestamp(decodedEntry: ScreenRecordingMetadataEntry): number {
return Number(decodedEntry.timestampRealtimeNs);
}
override processDecodedEntry(timestamp: number): ScreenRecordingTraceEntry {
const videoTimeSeconds = (timestamp - this.timestamps[0]) / 1000000000 + ParserScreenRecording.EPSILON;
override processDecodedEntry(entry: ScreenRecordingMetadataEntry): ScreenRecordingTraceEntry {
const videoTimeSeconds = (Number(entry.timestampRealtimeNs) - this.timestamps[0]) / 1000000000;
const videoData = this.trace;
return new ScreenRecordingTraceEntry(timestamp, videoTimeSeconds, videoData);
return new ScreenRecordingTraceEntry(Number(entry.timestampRealtimeNs), videoTimeSeconds, videoData);
}
private searchMagicString(videoData: Uint8Array): number {
@@ -56,22 +70,42 @@ class ParserScreenRecording extends Parser {
return pos;
}
private parseTimestampsCount(videoData: Uint8Array, pos: number) : [number, number] {
if (pos + 4 >= videoData.length) {
throw new TypeError("video data is too short. Expected timestamps count doesn't fit");
private parseMetadataVersion(videoData: Uint8Array, pos: number) : [number, number] {
if (pos + 4 > videoData.length) {
throw new TypeError("Failed to parse metadata version. Video data is too short.");
}
const timestampsCount = ArrayUtils.toUintLittleEndian(videoData, pos, pos+4);
const version = Number(ArrayUtils.toUintLittleEndian(videoData, pos, pos+4));
pos += 4;
return [pos, timestampsCount];
return [pos, version];
}
private parseTimestamps(videoData: Uint8Array, pos: number, count: number): number[] {
if (pos + count * 8 >= videoData.length) {
throw new TypeError("video data is too short. Expected timestamps do not fit");
private parseRealToMonotonicTimeOffsetNs(videoData: Uint8Array, pos: number) : [number, bigint] {
if (pos + 8 > videoData.length) {
throw new TypeError("Failed to parse realtime-to-monotonic time offset. Video data is too short.");
}
const timestamps: number[] = [];
const offset = ArrayUtils.toIntLittleEndian(videoData, pos, pos+8);
pos += 8;
return [pos, offset];
}
private parseFramesCount(videoData: Uint8Array, pos: number) : [number, number] {
if (pos + 4 > videoData.length) {
throw new TypeError("Failed to parse frames count. Video data is too short.");
}
const count = Number(ArrayUtils.toUintLittleEndian(videoData, pos, pos+4));
pos += 4;
return [pos, count];
}
private parseTimestampsMonotonicNs(videoData: Uint8Array, pos: number, count: number) : bigint[] {
if (pos + count * 16 > videoData.length) {
throw new TypeError("Failed to parse monotonic timestamps. Video data is too short.");
}
const timestamps: bigint[] = [];
for (let i = 0; i < count; ++i) {
const timestamp = ArrayUtils.toUintLittleEndian(videoData, pos, pos+8) * 1000;
const timestamp = ArrayUtils.toUintLittleEndian(videoData, pos, pos+8);
pos += 8;
//parse VSYNC ID here when available
pos += 8;
timestamps.push(timestamp);
}
@@ -79,8 +113,7 @@ class ParserScreenRecording extends Parser {
}
private static readonly MPEG4_MAGIC_NMBER = [0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32]; // ....ftypmp42
private static readonly WINSCOPE_META_MAGIC_STRING = [0x23, 0x56, 0x56, 0x31, 0x4e, 0x53, 0x43, 0x30, 0x50, 0x45, 0x54, 0x31, 0x4d, 0x45, 0x21, 0x23]; // #VV1NSC0PET1ME!#
private static readonly EPSILON = 0.00001;
private static readonly WINSCOPE_META_MAGIC_STRING = [0x23, 0x56, 0x56, 0x31, 0x4e, 0x53, 0x43, 0x30, 0x50, 0x45, 0x54, 0x31, 0x4d, 0x45, 0x32, 0x23]; // #VV1NSC0PET1ME2#
}
export {ParserScreenRecording};

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ScreenRecordingTraceEntry} from "common/trace/screen_recording";
import {TraceTypeId} from "common/trace/type_id";
import {TestUtils} from "test/test_utils";
import {Parser} from "./parser";
import {ParserFactory} from "./parser_factory";
describe("ParserScreenRecordingLegacy", () => {
let parser: Parser;
beforeAll(async () => {
const trace = TestUtils.getFixtureBlob("screen_recording_legacy.mp4");
const parsers = await new ParserFactory().createParsers([trace]);
expect(parsers.length).toEqual(1);
parser = parsers[0];
});
it("has expected trace type", () => {
expect(parser.getTraceTypeId()).toEqual(TraceTypeId.SCREEN_RECORDING);
});
it("provides timestamps", () => {
const timestamps = parser.getTimestamps();
expect(timestamps.length)
.toEqual(85);
expect(timestamps.slice(0, 3))
.toEqual([19446131807000, 19446158500000, 19446167117000]);
expect(timestamps.slice(timestamps.length-3, timestamps.length))
.toEqual([19448470076000, 19448487525000, 19448501007000]);
});
it("retrieves trace entry", () => {
{
const entry = parser.getTraceEntry(19446131807000)!;
expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0);
}
{
const entry = parser.getTraceEntry(19448501007000)!;
expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(2.37, 0.001);
}
});
});

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TraceTypeId} from "common/trace/type_id";
import {ArrayUtils} from "common/utils/array_utils";
import {Parser} from "./parser";
import {ScreenRecordingTraceEntry} from "common/trace/screen_recording";
class ParserScreenRecordingLegacy extends Parser {
constructor(trace: Blob) {
super(trace);
}
override getTraceTypeId(): TraceTypeId {
return TraceTypeId.SCREEN_RECORDING;
}
override getMagicNumber(): number[] {
return ParserScreenRecordingLegacy.MPEG4_MAGIC_NMBER;
}
override decodeTrace(videoData: Uint8Array): number[] {
const posCount = this.searchMagicString(videoData);
const [posTimestamps, count] = this.parseFramesCount(videoData, posCount);
return this.parseTimestamps(videoData, posTimestamps, count);
}
override getTimestamp(decodedEntry: number): number {
return decodedEntry;
}
override processDecodedEntry(timestamp: number): ScreenRecordingTraceEntry {
const videoTimeSeconds = (timestamp - this.timestamps[0]) / 1000000000 + ParserScreenRecordingLegacy.EPSILON;
const videoData = this.trace;
return new ScreenRecordingTraceEntry(timestamp, videoTimeSeconds, videoData);
}
private searchMagicString(videoData: Uint8Array): number {
let pos = ArrayUtils.searchSubarray(videoData, ParserScreenRecordingLegacy.WINSCOPE_META_MAGIC_STRING);
if (pos === undefined) {
throw new TypeError("video data doesn't contain winscope magic string");
}
pos += ParserScreenRecordingLegacy.WINSCOPE_META_MAGIC_STRING.length;
return pos;
}
private parseFramesCount(videoData: Uint8Array, pos: number) : [number, number] {
if (pos + 4 > videoData.length) {
throw new TypeError("Failed to parse frames count. Video data is too short.");
}
const framesCount = Number(ArrayUtils.toUintLittleEndian(videoData, pos, pos+4));
pos += 4;
return [pos, framesCount];
}
private parseTimestamps(videoData: Uint8Array, pos: number, count: number): number[] {
if (pos + count * 8 > videoData.length) {
throw new TypeError("Failed to parse timestamps. Video data is too short.");
}
const timestamps: number[] = [];
for (let i = 0; i < count; ++i) {
const timestamp = Number(ArrayUtils.toUintLittleEndian(videoData, pos, pos+8) * 1000n);
pos += 8;
timestamps.push(timestamp);
}
return timestamps;
}
private static readonly MPEG4_MAGIC_NMBER = [0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32]; // ....ftypmp42
private static readonly WINSCOPE_META_MAGIC_STRING = [0x23, 0x56, 0x56, 0x31, 0x4e, 0x53, 0x43, 0x30, 0x50, 0x45, 0x54, 0x31, 0x4d, 0x45, 0x21, 0x23]; // #VV1NSC0PET1ME!#
private static readonly EPSILON = 0.00001;
}
export {ParserScreenRecordingLegacy};

Binary file not shown.