Add screenrecording viewer
This change also needed and includes the refactorings listed below. Extend viewer's interface to support overlay views: viewers return a list of views that could have either "tab" or "overlay" type. Extend TraceViewComponent to render overlay views as well. Simplify TraceViewComponent's interface for simpler event handling: receive list of viewers + callback as input instead of entire TraceCoordinator Test: npm run build:all && npm run test:all Fix: b/238090772 Change-Id: Iac4c7e66ebe662a76166318d045c2c35e689ef15
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { BrowserModule } from "@angular/platform-browser";
|
||||
import { DragDropModule } from "@angular/cdk/drag-drop";
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { MatCardModule } from "@angular/material/card";
|
||||
@@ -46,6 +47,7 @@ import { TreeNodeDataViewComponent } from "viewers/components/tree_node_data_vie
|
||||
import { TreeNodePropertiesDataViewComponent } from "viewers/components/tree_node_properties_data_view.component";
|
||||
import { ViewerInputMethodComponent } from "viewers/components/viewer_input_method.component";
|
||||
import { ViewerProtologComponent} from "viewers/viewer_protolog/viewer_protolog.component";
|
||||
import { ViewerScreenRecordingComponent } from "viewers/viewer_screen_recording/viewer_screen_recording.component";
|
||||
import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component";
|
||||
import { ViewerTransactionsComponent } from "viewers/viewer_transactions/viewer_transactions.component";
|
||||
import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component";
|
||||
@@ -58,6 +60,7 @@ import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/view
|
||||
ViewerInputMethodComponent,
|
||||
ViewerProtologComponent,
|
||||
ViewerTransactionsComponent,
|
||||
ViewerScreenRecordingComponent,
|
||||
CollectTracesComponent,
|
||||
UploadTracesComponent,
|
||||
AdbProxyComponent,
|
||||
@@ -103,6 +106,7 @@ import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/view
|
||||
MatTabsModule,
|
||||
MatSnackBarModule,
|
||||
ScrollingModule,
|
||||
DragDropModule,
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
||||
@@ -19,12 +19,15 @@ import { MatSliderChange } from "@angular/material/slider";
|
||||
import { TraceCoordinator } from "app/trace_coordinator";
|
||||
import { PersistentStore } from "common/persistent_store";
|
||||
import { Timestamp } from "common/trace/timestamp";
|
||||
import { FileUtils } from "common/utils/file_utils";
|
||||
import { proxyClient, ProxyState } from "trace_collection/proxy_client";
|
||||
import { ViewerInputMethodComponent } from "viewers/components/viewer_input_method.component";
|
||||
import { Viewer } from "viewers/viewer";
|
||||
import { ViewerProtologComponent} from "viewers/viewer_protolog/viewer_protolog.component";
|
||||
import { ViewerSurfaceFlingerComponent } from "viewers/viewer_surface_flinger/viewer_surface_flinger.component";
|
||||
import { ViewerWindowManagerComponent } from "viewers/viewer_window_manager/viewer_window_manager.component";
|
||||
import { ViewerTransactionsComponent } from "viewers/viewer_transactions/viewer_transactions.component";
|
||||
import { ViewerScreenRecordingComponent } from "viewers/viewer_screen_recording/viewer_screen_recording.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-root",
|
||||
@@ -51,8 +54,9 @@ import { ViewerTransactionsComponent } from "viewers/viewer_transactions/viewer_
|
||||
<trace-view
|
||||
*ngIf="dataLoaded"
|
||||
id="viewers"
|
||||
[viewers]="allViewers"
|
||||
[store]="store"
|
||||
[traceCoordinator]="traceCoordinator"
|
||||
(downloadTracesButtonClick)="onDownloadTracesButtonClick()"
|
||||
></trace-view>
|
||||
|
||||
<div *ngIf="dataLoaded" id="timescrub">
|
||||
@@ -129,6 +133,7 @@ export class AppComponent {
|
||||
currentTimestamp?: Timestamp;
|
||||
currentTimestampIndex = 0;
|
||||
allTimestamps: Timestamp[] = [];
|
||||
allViewers: Viewer[] = [];
|
||||
@Input() dataLoaded = false;
|
||||
|
||||
constructor(
|
||||
@@ -144,6 +149,10 @@ export class AppComponent {
|
||||
customElements.define("viewer-protolog",
|
||||
createCustomElement(ViewerProtologComponent, {injector}));
|
||||
}
|
||||
if (!customElements.get("viewer-screen-recording")) {
|
||||
customElements.define("viewer-screen-recording",
|
||||
createCustomElement(ViewerScreenRecordingComponent, {injector}));
|
||||
}
|
||||
if (!customElements.get("viewer-surface-flinger")) {
|
||||
customElements.define("viewer-surface-flinger",
|
||||
createCustomElement(ViewerSurfaceFlingerComponent, {injector}));
|
||||
@@ -182,8 +191,9 @@ export class AppComponent {
|
||||
|
||||
public onDataLoadedChange(dataLoaded: boolean) {
|
||||
if (dataLoaded && !(this.traceCoordinator.getViewers().length > 0)) {
|
||||
this.allTimestamps = this.traceCoordinator.getTimestamps();
|
||||
this.traceCoordinator.createViewers();
|
||||
this.allViewers = this.traceCoordinator.getViewers();
|
||||
this.allTimestamps = this.traceCoordinator.getTimestamps();
|
||||
this.currentTimestampIndex = 0;
|
||||
this.notifyCurrentTimestamp();
|
||||
this.dataLoaded = dataLoaded;
|
||||
@@ -194,4 +204,18 @@ export class AppComponent {
|
||||
this.currentTimestamp = this.allTimestamps[this.currentTimestampIndex];
|
||||
this.traceCoordinator.notifyCurrentTimestamp(this.currentTimestamp);
|
||||
}
|
||||
|
||||
private async onDownloadTracesButtonClick() {
|
||||
const traces = await this.traceCoordinator.getAllTracesForDownload();
|
||||
const zipFileBlob = await FileUtils.createZipArchive(traces);
|
||||
const zipFileName = "winscope.zip";
|
||||
const a = document.createElement("a");
|
||||
document.body.appendChild(a);
|
||||
const url = window.URL.createObjectURL(zipFileBlob);
|
||||
a.href = url;
|
||||
a.download = zipFileName;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,35 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { TraceViewComponent } from "./trace_view.component";
|
||||
import { MatCardModule } from "@angular/material/card";
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import { TraceCoordinator } from "app/trace_coordinator";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA} from "@angular/core";
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {TraceViewComponent} from "./trace_view.component";
|
||||
import {View, Viewer, ViewType} from "viewers/viewer";
|
||||
|
||||
class FakeViewer implements Viewer {
|
||||
constructor(title: string, content: string) {
|
||||
this.title = title;
|
||||
this.htmlElement = document.createElement("div");
|
||||
this.htmlElement.innerText = content;
|
||||
}
|
||||
|
||||
notifyCurrentTraceEntries(entries: any) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
getViews(): View[] {
|
||||
return [new View(ViewType.TAB, this.htmlElement, this.title)];
|
||||
}
|
||||
|
||||
getDependencies(): any[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
private htmlElement: HTMLElement;
|
||||
private title: string;
|
||||
}
|
||||
|
||||
describe("TraceViewComponent", () => {
|
||||
let fixture: ComponentFixture<TraceViewComponent>;
|
||||
@@ -27,27 +50,22 @@ describe("TraceViewComponent", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [TraceViewComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatCardModule
|
||||
],
|
||||
declarations: [TraceViewComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(TraceViewComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.traceCoordinator = new TraceCoordinator();
|
||||
component.viewerTabs = [
|
||||
{
|
||||
label: "Surface Flinger",
|
||||
cardId: 0,
|
||||
},
|
||||
{
|
||||
label: "Window Manager",
|
||||
cardId: 1,
|
||||
}
|
||||
];
|
||||
htmlElement = fixture.nativeElement;
|
||||
component = fixture.componentInstance;
|
||||
component.viewers = [
|
||||
new FakeViewer("Title0", "Content0"),
|
||||
new FakeViewer("Title1", "Content1")
|
||||
];
|
||||
component.ngOnChanges();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("can be created", () => {
|
||||
@@ -56,35 +74,57 @@ describe("TraceViewComponent", () => {
|
||||
});
|
||||
|
||||
it("creates viewer tabs", () => {
|
||||
fixture.detectChanges();
|
||||
const tabs = htmlElement.querySelectorAll(".viewer-tab");
|
||||
const tabs: NodeList = htmlElement.querySelectorAll(".viewer-tab");
|
||||
expect(tabs.length).toEqual(2);
|
||||
expect(component.activeViewerCardId).toEqual(0);
|
||||
expect(tabs.item(0)!.textContent).toEqual("Title0");
|
||||
expect(tabs.item(1)!.textContent).toEqual("Title1");
|
||||
});
|
||||
|
||||
it("changes active viewer on click", async () => {
|
||||
fixture.detectChanges();
|
||||
expect(component.activeViewerCardId).toEqual(0);
|
||||
it("changes active viewer on click", () => {
|
||||
const tabs = htmlElement.querySelectorAll(".viewer-tab");
|
||||
tabs[0].dispatchEvent(new Event("click"));
|
||||
const tabsContent =
|
||||
htmlElement.querySelectorAll(".trace-view-content div");
|
||||
|
||||
// Initially tab 0
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const firstId = component.activeViewerCardId;
|
||||
expect(tabsContent.length).toEqual(2);
|
||||
expect(tabsContent[0].innerHTML).toEqual("Content0");
|
||||
expect(tabsContent[1].innerHTML).toEqual("Content1");
|
||||
expect((<any>tabsContent[0]).style?.display).toEqual("");
|
||||
expect((<any>tabsContent[1]).style?.display).toEqual("none");
|
||||
|
||||
// Switch to tab 1
|
||||
tabs[1].dispatchEvent(new Event("click"));
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const secondId = component.activeViewerCardId;
|
||||
expect(firstId !== secondId).toBeTrue;
|
||||
expect(tabsContent.length).toEqual(2);
|
||||
expect(tabsContent[0].innerHTML).toEqual("Content0");
|
||||
expect(tabsContent[1].innerHTML).toEqual("Content1");
|
||||
expect((<any>tabsContent[0]).style?.display).toEqual("none");
|
||||
expect((<any>tabsContent[1]).style?.display).toEqual("");
|
||||
|
||||
// Switch to tab 0
|
||||
tabs[0].dispatchEvent(new Event("click"));
|
||||
fixture.detectChanges();
|
||||
expect(tabsContent.length).toEqual(2);
|
||||
expect(tabsContent[0].innerHTML).toEqual("Content0");
|
||||
expect(tabsContent[1].innerHTML).toEqual("Content1");
|
||||
expect((<any>tabsContent[0]).style?.display).toEqual("");
|
||||
expect((<any>tabsContent[1]).style?.display).toEqual("none");
|
||||
});
|
||||
|
||||
it("downloads all traces", async () => {
|
||||
spyOn(component, "downloadAllTraces").and.callThrough();
|
||||
fixture.detectChanges();
|
||||
const downloadButton: HTMLButtonElement | null = htmlElement.querySelector(".save-btn");
|
||||
it("emits event on download button click", () => {
|
||||
const spy = spyOn(component.downloadTracesButtonClick, "emit");
|
||||
|
||||
const downloadButton: null|HTMLButtonElement =
|
||||
htmlElement.querySelector(".save-btn");
|
||||
expect(downloadButton).toBeInstanceOf(HTMLButtonElement);
|
||||
|
||||
downloadButton?.dispatchEvent(new Event("click"));
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(component.downloadAllTraces).toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
downloadButton?.dispatchEvent(new Event("click"));
|
||||
fixture.detectChanges();
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,20 +13,15 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
Inject,
|
||||
ElementRef,
|
||||
} from "@angular/core";
|
||||
import { TraceCoordinator } from "app/trace_coordinator";
|
||||
import { PersistentStore } from "common/persistent_store";
|
||||
import { FileUtils } from "common/utils/file_utils";
|
||||
import { Viewer } from "viewers/viewer";
|
||||
import {Component, ElementRef, EventEmitter, Inject, Input, Output} from "@angular/core";
|
||||
import {PersistentStore} from "common/persistent_store";
|
||||
import {Viewer, View, ViewType} from "viewers/viewer";
|
||||
|
||||
@Component({
|
||||
selector: "trace-view",
|
||||
template: `
|
||||
<div class="container-overlay">
|
||||
</div>
|
||||
<div class="header-items-wrapper">
|
||||
<nav mat-tab-nav-bar class="viewer-nav-bar">
|
||||
<a
|
||||
@@ -41,7 +36,7 @@ import { Viewer } from "viewers/viewer";
|
||||
color="primary"
|
||||
mat-button
|
||||
class="save-btn"
|
||||
(click)="downloadAllTraces()"
|
||||
(click)="downloadTracesButtonClick.emit()"
|
||||
>Download all traces</button>
|
||||
</div>
|
||||
<div class="trace-view-content">
|
||||
@@ -49,6 +44,16 @@ import { Viewer } from "viewers/viewer";
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.container-overlay {
|
||||
z-index: 10;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.header-items-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -73,45 +78,21 @@ import { Viewer } from "viewers/viewer";
|
||||
]
|
||||
})
|
||||
export class TraceViewComponent {
|
||||
@Input() viewers!: Viewer[];
|
||||
@Input() store!: PersistentStore;
|
||||
@Input() traceCoordinator!: TraceCoordinator;
|
||||
viewerTabs: ViewerTab[] = [];
|
||||
activeViewerCardId = 0;
|
||||
views: HTMLElement[] = [];
|
||||
@Output() downloadTracesButtonClick = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
@Inject(ElementRef) private elementRef: ElementRef,
|
||||
) {}
|
||||
private elementRef: ElementRef;
|
||||
private viewerTabs: ViewerTab[] = [];
|
||||
private activeViewerCardId = 0;
|
||||
|
||||
ngDoCheck() {
|
||||
if (this.traceCoordinator.getViewers().length > 0 && !this.viewersAdded()) {
|
||||
let cardCounter = 0;
|
||||
this.activeViewerCardId = 0;
|
||||
this.viewerTabs = [];
|
||||
this.traceCoordinator.getViewers().forEach((viewer: Viewer) => {
|
||||
// create tab for viewer nav bar
|
||||
const tab = {
|
||||
label: viewer.getTitle(),
|
||||
cardId: cardCounter,
|
||||
};
|
||||
this.viewerTabs.push(tab);
|
||||
constructor(@Inject(ElementRef) elementRef: ElementRef) {
|
||||
this.elementRef = elementRef;
|
||||
}
|
||||
|
||||
// add properties to view and add view to trace view card
|
||||
const view = viewer.getView();
|
||||
(view as any).store = this.store;
|
||||
view.id = `card-${cardCounter}`;
|
||||
view.style.display = this.isActiveViewerCard(cardCounter) ? "" : "none";
|
||||
|
||||
const traceViewContent = this.elementRef.nativeElement.querySelector(".trace-view-content")!;
|
||||
traceViewContent.appendChild(view);
|
||||
this.views.push(view);
|
||||
cardCounter++;
|
||||
});
|
||||
} else if (this.traceCoordinator.getViewers().length === 0 && this.viewersAdded()) {
|
||||
this.activeViewerCardId = 0;
|
||||
this.views.forEach(view => view.remove());
|
||||
this.views = [];
|
||||
}
|
||||
ngOnChanges() {
|
||||
this.renderViewsTab();
|
||||
this.renderViewsOverlay();
|
||||
}
|
||||
|
||||
public showViewer(cardId: number) {
|
||||
@@ -124,22 +105,56 @@ export class TraceViewComponent {
|
||||
return this.activeViewerCardId === cardId;
|
||||
}
|
||||
|
||||
public async downloadAllTraces() {
|
||||
const traces = await this.traceCoordinator.getAllTracesForDownload();
|
||||
const zipFileBlob = await FileUtils.createZipArchive(traces);
|
||||
const zipFileName = "winscope.zip";
|
||||
const a = document.createElement("a");
|
||||
document.body.appendChild(a);
|
||||
const url = window.URL.createObjectURL(zipFileBlob);
|
||||
a.href = url;
|
||||
a.download = zipFileName;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
private renderViewsTab() {
|
||||
this.activeViewerCardId = 0;
|
||||
this.viewerTabs = [];
|
||||
|
||||
const views: View[] = this.viewers
|
||||
.map(viewer => viewer.getViews())
|
||||
.flat()
|
||||
.filter(view => (view.type === ViewType.TAB));
|
||||
|
||||
for (const [cardCounter, view] of views.entries()) {
|
||||
if (!view) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// create tab for viewer nav bar
|
||||
const tab = {
|
||||
label: view.title,
|
||||
cardId: cardCounter,
|
||||
};
|
||||
this.viewerTabs.push(tab);
|
||||
|
||||
// add properties to view and add view to trace view card
|
||||
(view as any).store = this.store;
|
||||
view.htmlElement.id = `card-${cardCounter}`;
|
||||
view.htmlElement.style.display = this.isActiveViewerCard(cardCounter) ? "" : "none";
|
||||
|
||||
const traceViewContent = this.elementRef.nativeElement.querySelector(".trace-view-content")!;
|
||||
traceViewContent.appendChild(view.htmlElement);
|
||||
}
|
||||
}
|
||||
|
||||
private viewersAdded() {
|
||||
return this.views.length > 0;
|
||||
private renderViewsOverlay() {
|
||||
const views: View[] = this.viewers
|
||||
.map(viewer => viewer.getViews())
|
||||
.flat()
|
||||
.filter(view => (view.type === ViewType.OVERLAY));
|
||||
|
||||
views.forEach(view => {
|
||||
view.htmlElement.style.pointerEvents = "all";
|
||||
view.htmlElement.style.position = "absolute";
|
||||
view.htmlElement.style.bottom = "10%";
|
||||
view.htmlElement.style.right = "0px";
|
||||
|
||||
const containerOverlay = this.elementRef.nativeElement.querySelector(".container-overlay");
|
||||
if (!containerOverlay) {
|
||||
throw new Error("Failed to find overlay container sub-element");
|
||||
}
|
||||
|
||||
containerOverlay!.appendChild(view.htmlElement);
|
||||
});
|
||||
}
|
||||
|
||||
private isActiveViewerCard(cardId: number) {
|
||||
|
||||
@@ -38,7 +38,6 @@ class TraceCoordinator {
|
||||
traces = this.parsers.map(parser => parser.getTrace()).concat(traces);
|
||||
let parserErrors: ParserError[];
|
||||
[this.parsers, parserErrors] = await new ParserFactory().createParsers(traces);
|
||||
console.log("created parsers: ", this.parsers);
|
||||
return parserErrors;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("ParserScreenRecording", () => {
|
||||
let parser: Parser;
|
||||
|
||||
beforeAll(async () => {
|
||||
parser = await UnitTestUtils.getParser("traces/elapsed_and_real_timestamp/screen_recording.mp4");
|
||||
parser = await UnitTestUtils.getParser("traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4");
|
||||
});
|
||||
|
||||
it("has expected trace type", () => {
|
||||
@@ -32,14 +32,15 @@ describe("ParserScreenRecording", () => {
|
||||
|
||||
it ("provides elapsed timestamps", () => {
|
||||
const timestamps = parser.getTimestamps(TimestampType.ELAPSED)!;
|
||||
console.log(timestamps[timestamps.length-1]);
|
||||
|
||||
expect(timestamps.length)
|
||||
.toEqual(15);
|
||||
.toEqual(123);
|
||||
|
||||
const expected = [
|
||||
new Timestamp(TimestampType.ELAPSED, 144857685000n),
|
||||
new Timestamp(TimestampType.ELAPSED, 144866679000n),
|
||||
new Timestamp(TimestampType.ELAPSED, 144875772000n),
|
||||
new Timestamp(TimestampType.ELAPSED, 211827840430n),
|
||||
new Timestamp(TimestampType.ELAPSED, 211842401430n),
|
||||
new Timestamp(TimestampType.ELAPSED, 211862172430n),
|
||||
];
|
||||
expect(timestamps.slice(0, 3))
|
||||
.toEqual(expected);
|
||||
@@ -49,12 +50,12 @@ describe("ParserScreenRecording", () => {
|
||||
const timestamps = parser.getTimestamps(TimestampType.REAL)!;
|
||||
|
||||
expect(timestamps.length)
|
||||
.toEqual(15);
|
||||
.toEqual(123);
|
||||
|
||||
const expected = [
|
||||
new Timestamp(TimestampType.REAL, 1659687791485257266n),
|
||||
new Timestamp(TimestampType.REAL, 1659687791494251266n),
|
||||
new Timestamp(TimestampType.REAL, 1659687791503344266n),
|
||||
new Timestamp(TimestampType.REAL, 1666361048792787045n),
|
||||
new Timestamp(TimestampType.REAL, 1666361048807348045n),
|
||||
new Timestamp(TimestampType.REAL, 1666361048827119045n),
|
||||
];
|
||||
expect(timestamps.slice(0, 3))
|
||||
.toEqual(expected);
|
||||
@@ -62,33 +63,33 @@ describe("ParserScreenRecording", () => {
|
||||
|
||||
it("retrieves trace entry from elapsed timestamp", () => {
|
||||
{
|
||||
const timestamp = new Timestamp(TimestampType.ELAPSED, 144857685000n);
|
||||
const timestamp = new Timestamp(TimestampType.ELAPSED, 211827840430n);
|
||||
const entry = parser.getTraceEntry(timestamp)!;
|
||||
expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
|
||||
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0);
|
||||
}
|
||||
|
||||
{
|
||||
const timestamp = new Timestamp(TimestampType.ELAPSED, 145300550000n);
|
||||
const timestamp = new Timestamp(TimestampType.ELAPSED, 213198917430n);
|
||||
const entry = parser.getTraceEntry(timestamp)!;
|
||||
expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
|
||||
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0.442, 0.001);
|
||||
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(1.371077000, 0.001);
|
||||
}
|
||||
});
|
||||
|
||||
it("retrieves trace entry from real timestamp", () => {
|
||||
{
|
||||
const timestamp = new Timestamp(TimestampType.REAL, 1659687791485257266n);
|
||||
const timestamp = new Timestamp(TimestampType.REAL, 1666361048792787045n);
|
||||
const entry = parser.getTraceEntry(timestamp)!;
|
||||
expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
|
||||
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0);
|
||||
}
|
||||
|
||||
{
|
||||
const timestamp = new Timestamp(TimestampType.REAL, 1659687791928122266n);
|
||||
const timestamp = new Timestamp(TimestampType.REAL, 1666361050163864045n);
|
||||
const entry = parser.getTraceEntry(timestamp)!;
|
||||
expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
|
||||
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0.322, 0.001);
|
||||
expect(Number(entry.videoTimeSeconds)).toBeCloseTo(1.371077000, 0.001);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ import {Parser} from "./parser";
|
||||
import {ScreenRecordingTraceEntry} from "common/trace/screen_recording";
|
||||
|
||||
class ScreenRecordingMetadataEntry {
|
||||
constructor(public timestampMonotonicNs: bigint, public timestampRealtimeNs: bigint) {
|
||||
constructor(public timestampElapsedNs: bigint, public timestampRealtimeNs: bigint) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,15 +40,31 @@ class ParserScreenRecording extends Parser {
|
||||
override decodeTrace(videoData: Uint8Array): ScreenRecordingMetadataEntry[] {
|
||||
const posVersion = this.searchMagicString(videoData);
|
||||
const [posTimeOffset, metadataVersion] = this.parseMetadataVersion(videoData, posVersion);
|
||||
if (metadataVersion !== 1) {
|
||||
|
||||
if (metadataVersion !== 1 && metadataVersion !== 2) {
|
||||
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);
|
||||
if (metadataVersion === 1) {
|
||||
// UI traces contain "elapsed" timestamps (SYSTEM_TIME_BOOTTIME), whereas
|
||||
// metadata Version 1 contains SYSTEM_TIME_MONOTONIC timestamps.
|
||||
//
|
||||
// Here we are pretending that metadata Version 1 contains "elapsed"
|
||||
// timestamps as well, in order to synchronize with the other traces.
|
||||
//
|
||||
// If no device suspensions are involved, SYSTEM_TIME_MONOTONIC should
|
||||
// indeed correspond to SYSTEM_TIME_BOOTTIME and things will work as
|
||||
// expected.
|
||||
console.warn(`Screen recording may not be synchronized with the
|
||||
other traces. Metadata contains monotonic time instead of elapsed.`);
|
||||
}
|
||||
|
||||
const [posCount, timeOffsetNs] = this.parseRealToElapsedTimeOffsetNs(videoData, posTimeOffset);
|
||||
const [posTimestamps, count] = this.parseFramesCount(videoData, posCount);
|
||||
const timestampsElapsedNs = this.parseTimestampsElapsedNs(videoData, posTimestamps, count);
|
||||
|
||||
return timestampsElapsedNs.map((timestampElapsedNs: bigint) => {
|
||||
return new ScreenRecordingMetadataEntry(timestampElapsedNs, timestampElapsedNs + timeOffsetNs);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,15 +73,7 @@ class ParserScreenRecording extends Parser {
|
||||
return undefined;
|
||||
}
|
||||
if (type === TimestampType.ELAPSED) {
|
||||
// Traces typically contain "elapsed" timestamps (SYSTEM_TIME_BOOTTIME),
|
||||
// whereas screen recordings contain SYSTEM_TIME_MONOTONIC timestamps.
|
||||
//
|
||||
// Here we are pretending that screen recordings contain "elapsed" timestamps
|
||||
// as well, in order to synchronize with the other traces.
|
||||
//
|
||||
// If no device suspensions are involved, SYSTEM_TIME_MONOTONIC should indeed
|
||||
// correspond to SYSTEM_TIME_BOOTTIME and things will work as expected.
|
||||
return new Timestamp(type, decodedEntry.timestampMonotonicNs);
|
||||
return new Timestamp(type, decodedEntry.timestampElapsedNs);
|
||||
}
|
||||
else if (type === TimestampType.REAL) {
|
||||
return new Timestamp(type, decodedEntry.timestampRealtimeNs);
|
||||
@@ -75,7 +83,7 @@ class ParserScreenRecording extends Parser {
|
||||
|
||||
override processDecodedEntry(index: number, entry: ScreenRecordingMetadataEntry): ScreenRecordingTraceEntry {
|
||||
const initialTimestampNs = this.getTimestamps(TimestampType.ELAPSED)![0].getValueNs();
|
||||
const currentTimestampNs = entry.timestampMonotonicNs;
|
||||
const currentTimestampNs = entry.timestampElapsedNs;
|
||||
const videoTimeSeconds = Number(currentTimestampNs - initialTimestampNs) / 1000000000;
|
||||
const videoData = this.trace;
|
||||
return new ScreenRecordingTraceEntry(videoTimeSeconds, videoData);
|
||||
@@ -99,9 +107,9 @@ class ParserScreenRecording extends Parser {
|
||||
return [pos, version];
|
||||
}
|
||||
|
||||
private parseRealToMonotonicTimeOffsetNs(videoData: Uint8Array, pos: number) : [number, bigint] {
|
||||
private parseRealToElapsedTimeOffsetNs(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.");
|
||||
throw new TypeError("Failed to parse realtime-to-elapsed time offset. Video data is too short.");
|
||||
}
|
||||
const offset = ArrayUtils.toIntLittleEndian(videoData, pos, pos+8);
|
||||
pos += 8;
|
||||
@@ -117,9 +125,9 @@ class ParserScreenRecording extends Parser {
|
||||
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.");
|
||||
private parseTimestampsElapsedNs(videoData: Uint8Array, pos: number, count: number) : bigint[] {
|
||||
if (pos + count * 8 > videoData.length) {
|
||||
throw new TypeError("Failed to parse timestamps. Video data is too short.");
|
||||
}
|
||||
const timestamps: bigint[] = [];
|
||||
for (let i = 0; i < count; ++i) {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 {browser, element, by} from "protractor";
|
||||
import {E2eTestUtils} from "./utils";
|
||||
|
||||
describe("Viewer ScreenRecording", () => {
|
||||
beforeAll(async () => {
|
||||
browser.manage().timeouts().implicitlyWait(1000);
|
||||
browser.get("file://" + E2eTestUtils.getProductionIndexHtmlPath());
|
||||
}),
|
||||
|
||||
it("processes trace and renders view", async () => {
|
||||
const inputFile = element(by.css("input[type=\"file\"]"));
|
||||
await inputFile.sendKeys(E2eTestUtils.getFixturePath(
|
||||
"traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4"));
|
||||
|
||||
const loadData = element(by.css(".load-btn"));
|
||||
await loadData.click();
|
||||
|
||||
const viewer = element(by.css("viewer-screen-recording"));
|
||||
expect(await viewer.isPresent())
|
||||
.toBeTruthy();
|
||||
|
||||
const video = element(by.css("viewer-screen-recording video"));
|
||||
expect(await video.isPresent())
|
||||
.toBeTruthy();
|
||||
expect(await video.getAttribute("src"))
|
||||
.toContain("blob:");
|
||||
expect(await video.getAttribute("currentTime"))
|
||||
.toEqual("0");
|
||||
});
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
@@ -14,14 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import {Viewer} from "viewers/viewer";
|
||||
import {View, Viewer, ViewType} from "viewers/viewer";
|
||||
import {ViewerEvents} from "viewers/common/viewer_events";
|
||||
import { PresenterInputMethod } from "viewers/common/presenter_input_method";
|
||||
import { ImeUiData } from "viewers/common/ime_ui_data";
|
||||
import {PresenterInputMethod} from "viewers/common/presenter_input_method";
|
||||
import {ImeUiData} from "viewers/common/ime_ui_data";
|
||||
|
||||
abstract class ViewerInputMethod implements Viewer {
|
||||
constructor() {
|
||||
this.view = document.createElement("viewer-input-method");
|
||||
this.htmlElement = document.createElement("viewer-input-method");
|
||||
this.presenter = this.initialisePresenter();
|
||||
this.addViewerEventListeners();
|
||||
}
|
||||
@@ -30,35 +30,31 @@ abstract class ViewerInputMethod implements Viewer {
|
||||
this.presenter.notifyCurrentTraceEntries(entries);
|
||||
}
|
||||
|
||||
public getView(): HTMLElement {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
public abstract getViews(): View[];
|
||||
public abstract getDependencies(): TraceType[];
|
||||
|
||||
protected imeUiCallback = (uiData: ImeUiData) => {
|
||||
// Angular does not deep watch @Input properties. Clearing inputData to null before repopulating
|
||||
// automatically ensures that the UI will change via the Angular change detection cycle. Without
|
||||
// resetting, Angular does not auto-detect that inputData has changed.
|
||||
(this.view as any).inputData = null;
|
||||
(this.view as any).inputData = uiData;
|
||||
(this.htmlElement as any).inputData = null;
|
||||
(this.htmlElement as any).inputData = uiData;
|
||||
};
|
||||
|
||||
protected addViewerEventListeners() {
|
||||
this.view.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
|
||||
this.view.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
|
||||
this.view.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
|
||||
this.view.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
|
||||
this.view.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
|
||||
this.view.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
|
||||
this.view.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
|
||||
this.view.addEventListener(ViewerEvents.AdditionalPropertySelected, (event) => this.presenter.newAdditionalPropertiesTree((event as CustomEvent).detail.selectedItem));
|
||||
this.htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
|
||||
this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
|
||||
this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
|
||||
this.htmlElement.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
|
||||
this.htmlElement.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
|
||||
this.htmlElement.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
|
||||
this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
|
||||
this.htmlElement.addEventListener(ViewerEvents.AdditionalPropertySelected, (event) => this.presenter.newAdditionalPropertiesTree((event as CustomEvent).detail.selectedItem));
|
||||
}
|
||||
|
||||
abstract getDependencies(): TraceType[];
|
||||
abstract getTitle(): string;
|
||||
protected abstract initialisePresenter(): PresenterInputMethod;
|
||||
|
||||
protected view: HTMLElement;
|
||||
protected htmlElement: HTMLElement;
|
||||
protected presenter: PresenterInputMethod;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,11 +15,24 @@
|
||||
*/
|
||||
import { TraceType } from "common/trace/trace_type";
|
||||
|
||||
enum ViewType {
|
||||
TAB,
|
||||
OVERLAY
|
||||
}
|
||||
|
||||
class View {
|
||||
constructor(
|
||||
public type: ViewType,
|
||||
public htmlElement: HTMLElement,
|
||||
public title: string
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
interface Viewer {
|
||||
notifyCurrentTraceEntries(entries: Map<TraceType, any>): void;
|
||||
getView(): HTMLElement;
|
||||
getTitle(): string;
|
||||
getViews(): View[];
|
||||
getDependencies(): TraceType[];
|
||||
}
|
||||
|
||||
export { Viewer };
|
||||
export {Viewer, View, ViewType};
|
||||
|
||||
@@ -22,6 +22,7 @@ import {ViewerProtoLog} from "./viewer_protolog/viewer_protolog";
|
||||
import {ViewerSurfaceFlinger} from "./viewer_surface_flinger/viewer_surface_flinger";
|
||||
import {ViewerWindowManager} from "./viewer_window_manager/viewer_window_manager";
|
||||
import {ViewerTransactions} from "./viewer_transactions/viewer_transactions";
|
||||
import {ViewerScreenRecording} from "./viewer_screen_recording/viewer_screen_recording";
|
||||
|
||||
class ViewerFactory {
|
||||
static readonly VIEWERS = [
|
||||
@@ -29,6 +30,7 @@ class ViewerFactory {
|
||||
ViewerInputMethodManagerService,
|
||||
ViewerInputMethodService,
|
||||
ViewerProtoLog,
|
||||
ViewerScreenRecording,
|
||||
ViewerSurfaceFlinger,
|
||||
ViewerTransactions,
|
||||
ViewerWindowManager,
|
||||
|
||||
@@ -14,19 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import { PresenterInputMethodClients } from "./presenter_input_method_clients";
|
||||
import { ViewerInputMethod } from "viewers/common/viewer_input_method";
|
||||
import {PresenterInputMethodClients} from "./presenter_input_method_clients";
|
||||
import {ViewerInputMethod} from "viewers/common/viewer_input_method";
|
||||
import {View, ViewType} from "viewers/viewer";
|
||||
|
||||
class ViewerInputMethodClients extends ViewerInputMethod {
|
||||
public getTitle(): string {
|
||||
return "Input Method Clients";
|
||||
override getViews(): View[] {
|
||||
return [new View(ViewType.TAB, this.htmlElement, "Input Method Clients")];
|
||||
}
|
||||
|
||||
public getDependencies(): TraceType[] {
|
||||
override getDependencies(): TraceType[] {
|
||||
return ViewerInputMethodClients.DEPENDENCIES;
|
||||
}
|
||||
|
||||
protected initialisePresenter() {
|
||||
override initialisePresenter() {
|
||||
return new PresenterInputMethodClients(this.imeUiCallback, this.getDependencies());
|
||||
}
|
||||
|
||||
|
||||
@@ -14,19 +14,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import { PresenterInputMethodManagerService } from "./presenter_input_method_manager_service";
|
||||
import { ViewerInputMethod } from "viewers/common/viewer_input_method";
|
||||
import {PresenterInputMethodManagerService} from "./presenter_input_method_manager_service";
|
||||
import {ViewerInputMethod} from "viewers/common/viewer_input_method";
|
||||
import {View, ViewType} from "viewers/viewer";
|
||||
|
||||
class ViewerInputMethodManagerService extends ViewerInputMethod {
|
||||
public getTitle(): string {
|
||||
return "Input Method Manager Service";
|
||||
override getViews(): View[] {
|
||||
return [
|
||||
new View(ViewType.TAB,
|
||||
this.htmlElement,
|
||||
"Input Method Manager Service")
|
||||
];
|
||||
}
|
||||
|
||||
public getDependencies(): TraceType[] {
|
||||
override getDependencies(): TraceType[] {
|
||||
return ViewerInputMethodManagerService.DEPENDENCIES;
|
||||
}
|
||||
|
||||
protected initialisePresenter() {
|
||||
override initialisePresenter() {
|
||||
return new PresenterInputMethodManagerService(this.imeUiCallback, this.getDependencies());
|
||||
}
|
||||
|
||||
|
||||
@@ -14,19 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import { PresenterInputMethodService } from "./presenter_input_method_service";
|
||||
import { ViewerInputMethod } from "viewers/common/viewer_input_method";
|
||||
import {PresenterInputMethodService} from "./presenter_input_method_service";
|
||||
import {ViewerInputMethod} from "viewers/common/viewer_input_method";
|
||||
import {View, ViewType} from "viewers/viewer";
|
||||
|
||||
class ViewerInputMethodService extends ViewerInputMethod {
|
||||
public getTitle(): string {
|
||||
return "Input Method Service";
|
||||
override getViews(): View[] {
|
||||
return [
|
||||
new View(
|
||||
ViewType.TAB,
|
||||
this.htmlElement,
|
||||
"Input Method Service"
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
public getDependencies(): TraceType[] {
|
||||
override getDependencies(): TraceType[] {
|
||||
return ViewerInputMethodService.DEPENDENCIES;
|
||||
}
|
||||
|
||||
protected initialisePresenter() {
|
||||
override initialisePresenter() {
|
||||
return new PresenterInputMethodService(this.imeUiCallback, this.getDependencies());
|
||||
}
|
||||
|
||||
|
||||
@@ -14,29 +14,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import {Viewer} from "viewers/viewer";
|
||||
import {View, Viewer, ViewType} from "viewers/viewer";
|
||||
import {Presenter} from "./presenter";
|
||||
import {Events} from "./events";
|
||||
import {UiData} from "./ui_data";
|
||||
|
||||
class ViewerProtoLog implements Viewer {
|
||||
constructor() {
|
||||
this.view = document.createElement("viewer-protolog");
|
||||
this.htmlElement = document.createElement("viewer-protolog");
|
||||
|
||||
this.presenter = new Presenter((data: UiData) => {
|
||||
(this.view as any).inputData = data;
|
||||
(this.htmlElement as any).inputData = data;
|
||||
});
|
||||
|
||||
this.view.addEventListener(Events.LogLevelsFilterChanged, (event) => {
|
||||
this.htmlElement.addEventListener(Events.LogLevelsFilterChanged, (event) => {
|
||||
return this.presenter.onLogLevelsFilterChanged((event as CustomEvent).detail);
|
||||
});
|
||||
this.view.addEventListener(Events.TagsFilterChanged, (event) => {
|
||||
this.htmlElement.addEventListener(Events.TagsFilterChanged, (event) => {
|
||||
return this.presenter.onTagsFilterChanged((event as CustomEvent).detail);
|
||||
});
|
||||
this.view.addEventListener(Events.SourceFilesFilterChanged, (event) => {
|
||||
this.htmlElement.addEventListener(Events.SourceFilesFilterChanged, (event) => {
|
||||
return this.presenter.onSourceFilesFilterChanged((event as CustomEvent).detail);
|
||||
});
|
||||
this.view.addEventListener(Events.SearchStringFilterChanged, (event) => {
|
||||
this.htmlElement.addEventListener(Events.SearchStringFilterChanged, (event) => {
|
||||
return this.presenter.onSearchStringFilterChanged((event as CustomEvent).detail);
|
||||
});
|
||||
}
|
||||
@@ -45,12 +45,8 @@ class ViewerProtoLog implements Viewer {
|
||||
this.presenter.notifyCurrentTraceEntries(entries);
|
||||
}
|
||||
|
||||
public getView(): HTMLElement {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
public getTitle() {
|
||||
return "ProtoLog";
|
||||
public getViews(): View[] {
|
||||
return [new View(ViewType.TAB, this.htmlElement, "ProtoLog")];
|
||||
}
|
||||
|
||||
public getDependencies(): TraceType[] {
|
||||
@@ -58,7 +54,7 @@ class ViewerProtoLog implements Viewer {
|
||||
}
|
||||
|
||||
public static readonly DEPENDENCIES: TraceType[] = [TraceType.PROTO_LOG];
|
||||
private view: HTMLElement;
|
||||
private htmlElement: HTMLElement;
|
||||
private presenter: Presenter;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from "@angular/core/testing";
|
||||
import {ViewerScreenRecordingComponent} from "./viewer_screen_recording.component";
|
||||
|
||||
describe("ViewerScreenRecordingComponent", () => {
|
||||
let fixture: ComponentFixture<ViewerScreenRecordingComponent>;
|
||||
let component: ViewerScreenRecordingComponent;
|
||||
let htmlElement: HTMLElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ComponentFixtureAutoDetect, useValue: true }
|
||||
],
|
||||
declarations: [
|
||||
ViewerScreenRecordingComponent,
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ViewerScreenRecordingComponent);
|
||||
component = fixture.componentInstance;
|
||||
htmlElement = fixture.nativeElement;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("can be created", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("can be minimized and maximized", () => {
|
||||
const buttonMinimize = htmlElement.querySelector(".button-minimize");
|
||||
const video = htmlElement.querySelector("video");
|
||||
expect(buttonMinimize).toBeTruthy();
|
||||
expect(video).toBeTruthy();
|
||||
expect(video!.style.visibility).toEqual("visible");
|
||||
|
||||
buttonMinimize!.dispatchEvent(new Event("click"));
|
||||
fixture.detectChanges();
|
||||
expect(video!.style.visibility).toEqual("hidden");
|
||||
|
||||
buttonMinimize!.dispatchEvent(new Event("click"));
|
||||
fixture.detectChanges();
|
||||
expect(video!.style.visibility).toEqual("visible");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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 {Component, ElementRef, Inject, Input} from "@angular/core";
|
||||
import {DomSanitizer, SafeUrl} from "@angular/platform-browser";
|
||||
import {ScreenRecordingTraceEntry} from "common/trace/screen_recording";
|
||||
|
||||
@Component({
|
||||
selector: "viewer-screen-recording",
|
||||
template: `
|
||||
<div class="container" cdkDrag cdkDragBoundary=".container-overlay">
|
||||
<div class="header">
|
||||
<button mat-button class="button-drag" cdkDragHandle>
|
||||
<mat-icon class="drag-icon">drag_indicator</mat-icon>
|
||||
<span class="mat-body-2">Screen recording</span>
|
||||
</button>
|
||||
|
||||
<button mat-button class="button-minimize"
|
||||
(click)="onMinimizeButtonClick()">
|
||||
<mat-icon>
|
||||
{{isMinimized ? "maximize" : "minimize"}}
|
||||
</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<video
|
||||
[currentTime]="videoCurrentTime"
|
||||
[src]=videoUrl
|
||||
[style.visibility]="isMinimized ? 'hidden' : 'visible'"
|
||||
cdkDragHandle>
|
||||
</video>
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.container {
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: white;
|
||||
border: 1px solid var(--default-border);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.button-drag {
|
||||
flex-grow: 1;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.button-minimize {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
video {
|
||||
height: 50vh;
|
||||
cursor: grab;
|
||||
}
|
||||
`,
|
||||
]
|
||||
})
|
||||
class ViewerScreenRecordingComponent {
|
||||
constructor(
|
||||
@Inject(ElementRef) elementRef: ElementRef,
|
||||
@Inject(DomSanitizer) sanitizer: DomSanitizer) {
|
||||
this.elementRef = elementRef;
|
||||
this.sanitizer = sanitizer;
|
||||
}
|
||||
|
||||
@Input()
|
||||
public set currentTraceEntry(entry: undefined|ScreenRecordingTraceEntry) {
|
||||
if (entry === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.videoUrl === undefined) {
|
||||
this.videoUrl = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(entry.videoData));
|
||||
}
|
||||
|
||||
this.videoCurrentTime = entry.videoTimeSeconds;
|
||||
}
|
||||
|
||||
public onMinimizeButtonClick() {
|
||||
this.isMinimized = !this.isMinimized;
|
||||
}
|
||||
|
||||
public videoUrl: undefined|SafeUrl = undefined;
|
||||
public videoCurrentTime = 0;
|
||||
public isMinimized = false;
|
||||
|
||||
private elementRef: ElementRef;
|
||||
private sanitizer: DomSanitizer;
|
||||
}
|
||||
|
||||
export {ViewerScreenRecordingComponent};
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 {TraceType} from "common/trace/trace_type";
|
||||
import {View, Viewer, ViewType} from "viewers/viewer";
|
||||
import {ScreenRecordingTraceEntry} from "common/trace/screen_recording";
|
||||
|
||||
class ViewerScreenRecording implements Viewer {
|
||||
constructor() {
|
||||
this.htmlElement = document.createElement("viewer-screen-recording");
|
||||
}
|
||||
|
||||
public notifyCurrentTraceEntries(entries: Map<TraceType, any>): void {
|
||||
const entry: undefined | ScreenRecordingTraceEntry = entries.get(TraceType.SCREEN_RECORDING)
|
||||
? entries.get(TraceType.SCREEN_RECORDING)[0]
|
||||
: undefined;
|
||||
|
||||
(<any>this.htmlElement).currentTraceEntry = entry;
|
||||
}
|
||||
|
||||
public getViews(): View[] {
|
||||
return [new View(ViewType.OVERLAY, this.htmlElement, "ScreenRecording")];
|
||||
}
|
||||
|
||||
public getDependencies(): TraceType[] {
|
||||
return ViewerScreenRecording.DEPENDENCIES;
|
||||
}
|
||||
|
||||
public static readonly DEPENDENCIES: TraceType[] = [TraceType.SCREEN_RECORDING];
|
||||
private htmlElement: HTMLElement;
|
||||
}
|
||||
|
||||
export {ViewerScreenRecording};
|
||||
@@ -14,40 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import {Viewer} from "viewers/viewer";
|
||||
import {View, Viewer, ViewType} from "viewers/viewer";
|
||||
import {Presenter} from "./presenter";
|
||||
import {UiData} from "./ui_data";
|
||||
import {ViewerEvents} from "viewers/common/viewer_events";
|
||||
|
||||
class ViewerSurfaceFlinger implements Viewer {
|
||||
constructor() {
|
||||
this.view = document.createElement("viewer-surface-flinger");
|
||||
this.htmlElement = document.createElement("viewer-surface-flinger");
|
||||
this.presenter = new Presenter((uiData: UiData) => {
|
||||
// Angular does not deep watch @Input properties. Clearing inputData to null before repopulating
|
||||
// automatically ensures that the UI will change via the Angular change detection cycle. Without
|
||||
// resetting, Angular does not auto-detect that inputData has changed.
|
||||
(this.view as any).inputData = null;
|
||||
(this.view as any).inputData = uiData;
|
||||
(this.htmlElement as any).inputData = null;
|
||||
(this.htmlElement as any).inputData = uiData;
|
||||
});
|
||||
this.view.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
|
||||
this.view.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
|
||||
this.view.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
|
||||
this.view.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
|
||||
this.view.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
|
||||
this.view.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
|
||||
this.view.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
|
||||
this.htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
|
||||
this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
|
||||
this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
|
||||
this.htmlElement.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
|
||||
this.htmlElement.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
|
||||
this.htmlElement.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
|
||||
this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
|
||||
}
|
||||
|
||||
public notifyCurrentTraceEntries(entries: Map<TraceType, any>): void {
|
||||
this.presenter.notifyCurrentTraceEntries(entries);
|
||||
}
|
||||
|
||||
public getView(): HTMLElement {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
public getTitle(): string {
|
||||
return "Surface Flinger";
|
||||
public getViews(): View[] {
|
||||
return [new View(ViewType.TAB, this.htmlElement, "Surface Flinger")];
|
||||
}
|
||||
|
||||
public getDependencies(): TraceType[] {
|
||||
@@ -55,7 +51,7 @@ class ViewerSurfaceFlinger implements Viewer {
|
||||
}
|
||||
|
||||
public static readonly DEPENDENCIES: TraceType[] = [TraceType.SURFACE_FLINGER];
|
||||
private view: HTMLElement;
|
||||
private htmlElement: HTMLElement;
|
||||
private presenter: Presenter;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,36 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import {Viewer} from "viewers/viewer";
|
||||
import {View, Viewer, ViewType} from "viewers/viewer";
|
||||
import {Presenter} from "./presenter";
|
||||
import {Events} from "./events";
|
||||
import {UiData} from "./ui_data";
|
||||
|
||||
class ViewerTransactions implements Viewer {
|
||||
constructor() {
|
||||
this.view = document.createElement("viewer-transactions");
|
||||
this.htmlElement = document.createElement("viewer-transactions");
|
||||
|
||||
this.presenter = new Presenter((data: UiData) => {
|
||||
(this.view as any).inputData = data;
|
||||
(this.htmlElement as any).inputData = data;
|
||||
});
|
||||
|
||||
this.view.addEventListener(Events.PidFilterChanged, (event) => {
|
||||
this.htmlElement.addEventListener(Events.PidFilterChanged, (event) => {
|
||||
this.presenter.onPidFilterChanged((event as CustomEvent).detail);
|
||||
});
|
||||
|
||||
this.view.addEventListener(Events.UidFilterChanged, (event) => {
|
||||
this.htmlElement.addEventListener(Events.UidFilterChanged, (event) => {
|
||||
this.presenter.onUidFilterChanged((event as CustomEvent).detail);
|
||||
});
|
||||
|
||||
this.view.addEventListener(Events.TypeFilterChanged, (event) => {
|
||||
this.htmlElement.addEventListener(Events.TypeFilterChanged, (event) => {
|
||||
this.presenter.onTypeFilterChanged((event as CustomEvent).detail);
|
||||
});
|
||||
|
||||
this.view.addEventListener(Events.IdFilterChanged, (event) => {
|
||||
this.htmlElement.addEventListener(Events.IdFilterChanged, (event) => {
|
||||
this.presenter.onIdFilterChanged((event as CustomEvent).detail);
|
||||
});
|
||||
|
||||
this.view.addEventListener(Events.EntryClicked, (event) => {
|
||||
this.htmlElement.addEventListener(Events.EntryClicked, (event) => {
|
||||
this.presenter.onEntryClicked((event as CustomEvent).detail);
|
||||
});
|
||||
}
|
||||
@@ -52,12 +52,8 @@ class ViewerTransactions implements Viewer {
|
||||
this.presenter.notifyCurrentTraceEntries(entries);
|
||||
}
|
||||
|
||||
public getView(): HTMLElement {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
public getTitle() {
|
||||
return "Transactions";
|
||||
public getViews(): View[] {
|
||||
return [new View(ViewType.TAB, this.htmlElement, "Transactions")];
|
||||
}
|
||||
|
||||
public getDependencies(): TraceType[] {
|
||||
@@ -65,7 +61,7 @@ class ViewerTransactions implements Viewer {
|
||||
}
|
||||
|
||||
public static readonly DEPENDENCIES: TraceType[] = [TraceType.TRANSACTIONS];
|
||||
private view: HTMLElement;
|
||||
private htmlElement: HTMLElement;
|
||||
private presenter: Presenter;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,40 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {TraceType} from "common/trace/trace_type";
|
||||
import {Viewer} from "viewers/viewer";
|
||||
import {Viewer, View, ViewType} from "viewers/viewer";
|
||||
import {Presenter} from "./presenter";
|
||||
import {UiData} from "./ui_data";
|
||||
import { ViewerEvents } from "viewers/common/viewer_events";
|
||||
|
||||
class ViewerWindowManager implements Viewer {
|
||||
constructor() {
|
||||
this.view = document.createElement("viewer-window-manager");
|
||||
this.htmlElement = document.createElement("viewer-window-manager");
|
||||
this.presenter = new Presenter((uiData: UiData) => {
|
||||
// Angular does not deep watch @Input properties. Clearing inputData to null before repopulating
|
||||
// automatically ensures that the UI will change via the Angular change detection cycle. Without
|
||||
// resetting, Angular does not auto-detect that inputData has changed.
|
||||
(this.view as any).inputData = null;
|
||||
(this.view as any).inputData = uiData;
|
||||
(this.htmlElement as any).inputData = null;
|
||||
(this.htmlElement as any).inputData = uiData;
|
||||
});
|
||||
this.view.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
|
||||
this.view.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
|
||||
this.view.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
|
||||
this.view.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
|
||||
this.view.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
|
||||
this.view.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
|
||||
this.view.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
|
||||
this.htmlElement.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => this.presenter.updatePinnedItems(((event as CustomEvent).detail.pinnedItem)));
|
||||
this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) => this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`));
|
||||
this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) => this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions));
|
||||
this.htmlElement.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => this.presenter.filterHierarchyTree((event as CustomEvent).detail.filterString));
|
||||
this.htmlElement.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) => this.presenter.updatePropertiesTree((event as CustomEvent).detail.userOptions));
|
||||
this.htmlElement.addEventListener(ViewerEvents.PropertiesFilterChange, (event) => this.presenter.filterPropertiesTree((event as CustomEvent).detail.filterString));
|
||||
this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) => this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem));
|
||||
}
|
||||
|
||||
public notifyCurrentTraceEntries(entries: Map<TraceType, any>): void {
|
||||
this.presenter.notifyCurrentTraceEntries(entries);
|
||||
}
|
||||
|
||||
public getView(): HTMLElement {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
public getTitle() {
|
||||
return "Window Manager";
|
||||
public getViews(): View[] {
|
||||
return [new View(ViewType.TAB, this.htmlElement, "Window Manager")];
|
||||
}
|
||||
|
||||
public getDependencies(): TraceType[] {
|
||||
@@ -55,7 +51,7 @@ class ViewerWindowManager implements Viewer {
|
||||
}
|
||||
|
||||
public static readonly DEPENDENCIES: TraceType[] = [TraceType.WINDOW_MANAGER];
|
||||
private view: HTMLElement;
|
||||
private htmlElement: HTMLElement;
|
||||
private presenter: Presenter;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user