chrome-devtools-frontend
Version:
Chrome DevTools UI
337 lines (303 loc) • 13.7 kB
text/typescript
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../ui/legacy/legacy.js';
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as UI from '../../ui/legacy/legacy.js';
import {html, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import webAudioStyles from './webAudio.css.js';
import {Events as ModelEvents, WebAudioModel} from './WebAudioModel.js';
const {widgetConfig} = UI.Widget;
const {bindToAction} = UI.UIUtils;
const UIStrings = {
/**
* @description Text in Web Audio View if there is nothing to show.
* Web Audio API is an API for controlling audio on the web.
*/
noWebAudio: 'No Web Audio API usage detected',
/**
* @description Text in Web Audio View
*/
openAPageThatUsesWebAudioApiTo: 'Open a page that uses Web Audio API to start monitoring.',
/**
* @description Text that shows there is no recording
*/
noRecordings: '(no recordings)',
/**
* @description Label prefix for an audio context selection
* @example {realtime (1e03ec)} PH1
*/
audioContextS: 'Audio context: {PH1}',
/**
* @description The current state of an item
*/
state: 'State',
/**
* @description Text in Web Audio View
*/
sampleRate: 'Sample Rate',
/**
* @description Text in Web Audio View
*/
callbackBufferSize: 'Callback Buffer Size',
/**
* @description Label in the Web Audio View for the maximum number of output channels
* that this Audio Context has.
*/
maxOutputChannels: 'Max Output Channels',
/**
* @description Text in Web Audio View
*/
currentTime: 'Current Time',
/**
* @description Text in Web Audio View
*/
callbackInterval: 'Callback Interval',
/**
* @description Text in Web Audio View
*/
renderCapacity: 'Render Capacity',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/web_audio/WebAudioView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const WEBAUDIO_EXPLANATION_URL =
'https://developer.chrome.com/docs/devtools/webaudio' as Platform.DevToolsPath.UrlString;
interface ViewInput {
contexts: Protocol.WebAudio.BaseAudioContext[];
selectedContextIndex: number;
onContextSelectorSelectionChanged: (contextId: string) => void;
contextRealtimeData: Protocol.WebAudio.ContextRealtimeData|null;
}
type View = (input: ViewInput, output: object, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, _output, target) => {
const {
contexts,
selectedContextIndex,
onContextSelectorSelectionChanged,
contextRealtimeData,
} = input;
const selectedContext = selectedContextIndex > -1 ? contexts[selectedContextIndex] : null;
const titleForContext = (context: Protocol.WebAudio.BaseAudioContext): string =>
context.contextType + ' (' + context.contextId.substr(-6) + ')';
const selectorTitle = i18nString(
UIStrings.audioContextS,
{PH1: selectedContext ? titleForContext(selectedContext) : i18nString(UIStrings.noRecordings)});
// clang-format off
render(html`
<style>${webAudioStyles}</style>
<div class="web-audio-toolbar-container vbox" role="toolbar">
<devtools-toolbar class="web-audio-toolbar" role="presentation"
jslog=${VisualLogging.toolbar()}>
<devtools-button ${bindToAction('components.collect-garbage')}></devtools-button>
<div class="toolbar-divider"></div>
<select
title=${selectorTitle}
aria-label=${selectorTitle}
?disabled=${contexts.length === 0}
=${(e: Event) => onContextSelectorSelectionChanged((e.target as HTMLSelectElement).value)}
.value=${selectedContext ? selectedContext.contextId : ''}>
${contexts.length === 0
? html`<option value="" hidden>${i18nString(UIStrings.noRecordings)}</option>`
: contexts.map(context => html`
<option value=${context.contextId}>${titleForContext(context)}</option>
`)}
</select>
</devtools-toolbar>
</div>
<div class="web-audio-content-container vbox flex-auto">
${!selectedContext ? html`
<div class="web-audio-details-container vbox flex-auto">
<devtools-widget .widgetConfig=${widgetConfig(UI.EmptyWidget.EmptyWidget,
{header: i18nString(UIStrings.noWebAudio),
text: i18nString(UIStrings.openAPageThatUsesWebAudioApiTo),
link: WEBAUDIO_EXPLANATION_URL,
})}>
</devtools-widget>
</div>` : html`<div class="web-audio-details-container vbox flex-auto">
<div class="context-detail-container">
<div class="context-detail-header">
<div class="context-detail-title">
${selectedContext.contextType === 'realtime' ? i18n.i18n.lockedString('AudioContext')
: i18n.i18n.lockedString('OfflineAudioContext')}
</div>
<div class="context-detail-subtitle">${selectedContext.contextId}</div>
</div>
<div class="context-detail-row">
<div class="context-detail-row-entry">${i18nString(UIStrings.state)}</div>
<div class="context-detail-row-value">${selectedContext.contextState}</div>
</div>
<div class="context-detail-row">
<div class="context-detail-row-entry">${i18nString(UIStrings.sampleRate)}</div>
<div class="context-detail-row-value">${selectedContext.sampleRate} Hz</div>
</div>
${selectedContext.contextType === 'realtime' ? html`
<div class="context-detail-row">
<div class="context-detail-row-entry">${i18nString(UIStrings.callbackBufferSize)}</div>
<div class="context-detail-row-value">${selectedContext.callbackBufferSize} frames</div>
</div>` : ''}
<div class="context-detail-row">
<div class="context-detail-row-entry">${i18nString(UIStrings.maxOutputChannels)}</div>
<div class="context-detail-row-value">${selectedContext.maxOutputChannelCount} ch</div>
</div>
</div>
</div>`}
<div class="web-audio-summary-container">
${contextRealtimeData ?
html`<div class="context-summary-container">
<span>${i18nString(UIStrings.currentTime)}: ${contextRealtimeData.currentTime.toFixed(3)} s</span>
<span>\u2758</span>
<span>${i18nString(UIStrings.callbackInterval)}: μ = ${
(contextRealtimeData.callbackIntervalMean * 1000).toFixed(3)} ms, σ = ${
(Math.sqrt(contextRealtimeData.callbackIntervalVariance) * 1000).toFixed(3)} ms</span>
<span>\u2758</span>
<span>${i18nString(UIStrings.renderCapacity)}: ${
(contextRealtimeData.renderCapacity * 100).toFixed(3)} %</span>
</div>` : ''}
</div>
</div>`, target);
// clang-format on
};
export class WebAudioView extends UI.Widget.VBox implements SDK.TargetManager.SDKModelObserver<WebAudioModel> {
private readonly knownContexts = new Set<string>();
private readonly contextSelectorItems: UI.ListModel.ListModel<Protocol.WebAudio.BaseAudioContext>;
private contextRealtimeData: Protocol.WebAudio.ContextRealtimeData|null = null;
private readonly view: View;
private selectedContextIndex = -1;
private readonly pollRealtimeDataThrottler: Common.Throttler.Throttler;
constructor(element?: HTMLElement, view = DEFAULT_VIEW) {
super({jslog: `${VisualLogging.panel('web-audio').track({resize: true})}`, useShadowDom: true});
this.view = view;
this.contextSelectorItems = new UI.ListModel.ListModel();
this.contextSelectorItems.addEventListener(UI.ListModel.Events.ITEMS_REPLACED, this.requestUpdate, this);
SDK.TargetManager.TargetManager.instance().observeModels(WebAudioModel, this);
this.pollRealtimeDataThrottler = new Common.Throttler.Throttler(1000);
this.performUpdate();
}
override wasShown(): void {
super.wasShown();
for (const model of SDK.TargetManager.TargetManager.instance().models(WebAudioModel)) {
this.addEventListeners(model);
}
}
override willHide(): void {
super.willHide();
for (const model of SDK.TargetManager.TargetManager.instance().models(WebAudioModel)) {
this.removeEventListeners(model);
}
}
modelAdded(webAudioModel: WebAudioModel): void {
if (this.isShowing()) {
this.addEventListeners(webAudioModel);
}
}
modelRemoved(webAudioModel: WebAudioModel): void {
this.removeEventListeners(webAudioModel);
}
override performUpdate(): void {
const input = {
contexts: [...this.contextSelectorItems],
selectedContextIndex: this.selectedContextIndex,
onContextSelectorSelectionChanged: this.onContextSelectorSelectionChanged.bind(this),
contextRealtimeData: this.contextRealtimeData,
};
this.view(input, {}, this.contentElement);
}
private addEventListeners(webAudioModel: WebAudioModel): void {
webAudioModel.ensureEnabled();
webAudioModel.addEventListener(ModelEvents.CONTEXT_CREATED, this.contextCreated, this);
webAudioModel.addEventListener(ModelEvents.CONTEXT_DESTROYED, this.contextDestroyed, this);
webAudioModel.addEventListener(ModelEvents.CONTEXT_CHANGED, this.contextChanged, this);
webAudioModel.addEventListener(ModelEvents.MODEL_RESET, this.reset, this);
}
private removeEventListeners(webAudioModel: WebAudioModel): void {
webAudioModel.removeEventListener(ModelEvents.CONTEXT_CREATED, this.contextCreated, this);
webAudioModel.removeEventListener(ModelEvents.CONTEXT_DESTROYED, this.contextDestroyed, this);
webAudioModel.removeEventListener(ModelEvents.CONTEXT_CHANGED, this.contextChanged, this);
webAudioModel.removeEventListener(ModelEvents.MODEL_RESET, this.reset, this);
}
private onContextSelectorSelectionChanged(contextId: string): void {
this.selectedContextIndex = this.contextSelectorItems.findIndex(context => context.contextId === contextId);
void this.pollRealtimeDataThrottler.schedule(this.pollRealtimeData.bind(this));
this.requestUpdate();
}
private contextCreated(event: Common.EventTarget.EventTargetEvent<Protocol.WebAudio.BaseAudioContext>): void {
const context = event.data;
this.knownContexts.add(context.contextId);
this.contextSelectorItems.insert(this.contextSelectorItems.length, context);
if (this.selectedContextIndex === -1) {
this.selectedContextIndex = this.contextSelectorItems.length - 1;
void this.pollRealtimeDataThrottler.schedule(this.pollRealtimeData.bind(this));
}
this.requestUpdate();
}
private contextDestroyed(event: Common.EventTarget.EventTargetEvent<Protocol.WebAudio.GraphObjectId>): void {
const contextId = event.data;
this.knownContexts.delete(contextId);
const index = this.contextSelectorItems.findIndex(context => context.contextId === contextId);
if (index > -1) {
const selectedContext =
this.selectedContextIndex > -1 ? this.contextSelectorItems.at(this.selectedContextIndex) : null;
this.contextSelectorItems.remove(index);
const newSelectedIndex = selectedContext ? this.contextSelectorItems.indexOf(selectedContext) : -1;
if (newSelectedIndex > -1) {
this.selectedContextIndex = newSelectedIndex;
} else {
this.selectedContextIndex = Math.min(index, this.contextSelectorItems.length - 1);
}
}
this.requestUpdate();
}
private contextChanged(event: Common.EventTarget.EventTargetEvent<Protocol.WebAudio.BaseAudioContext>): void {
const context = event.data;
if (!this.knownContexts.has(context.contextId)) {
return;
}
const changedContext = event.data;
const index = this.contextSelectorItems.findIndex(context => context.contextId === changedContext.contextId);
if (index > -1) {
this.contextSelectorItems.replace(index, changedContext);
}
this.requestUpdate();
}
private reset(): void {
this.contextSelectorItems.replaceAll([]);
this.selectedContextIndex = -1;
this.knownContexts.clear();
this.requestUpdate();
}
private setContextRealtimeData(contextRealtimeData: Protocol.WebAudio.ContextRealtimeData|null): void {
this.contextRealtimeData = contextRealtimeData;
this.requestUpdate();
}
private async pollRealtimeData(): Promise<void> {
if (this.selectedContextIndex < 0) {
this.setContextRealtimeData(null);
return;
}
const context = this.contextSelectorItems.at(this.selectedContextIndex);
if (!context) {
this.setContextRealtimeData(null);
return;
}
for (const model of SDK.TargetManager.TargetManager.instance().models(WebAudioModel)) {
// Display summary only for real-time context.
if (context.contextType === 'realtime') {
if (!this.knownContexts.has(context.contextId)) {
continue;
}
const realtimeData = await model.requestRealtimeData(context.contextId);
if (realtimeData) {
this.setContextRealtimeData(realtimeData);
}
void this.pollRealtimeDataThrottler.schedule(this.pollRealtimeData.bind(this));
} else {
this.setContextRealtimeData(null);
}
}
}
}