UNPKG

chrome-devtools-frontend

Version:
395 lines (336 loc) • 14 kB
// 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. /* eslint-disable rulesdir/no-imperative-dom-api */ import type * 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 {Events, MediaModel, type PlayerEvent} from './MediaModel.js'; import {PlayerDetailView} from './PlayerDetailView.js'; import {PlayerListView} from './PlayerListView.js'; const UIStrings = { /** * @description Text to show if no media player has been selected * A media player can be an audio and video source of a page. */ noPlayerDetailsSelected: 'No media player selected', /** * @description Text to instruct the user on how to view media player details * A media player can be an audio and video source of a page. */ selectToViewDetails: 'Select a media player to inspect its details.', /** * @description Text to show if no player can be shown * A media player can be an audio and video source of a page. */ noMediaPlayer: 'No media player', /** * @description Text to explain this panel * A media player can be an audio and video source of a page. */ mediaPlayerDescription: 'On this page you can view and export media player details.', } as const; const str_ = i18n.i18n.registerUIStrings('panels/media/MainView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const MEDIA_PLAYER_EXPLANATION_URL = 'https://developer.chrome.com/docs/devtools/media-panel#hide-show' as Platform.DevToolsPath.UrlString; export interface TriggerHandler { onProperty(property: Protocol.Media.PlayerProperty): void; onError(error: Protocol.Media.PlayerError): void; onMessage(message: Protocol.Media.PlayerMessage): void; onEvent(event: PlayerEvent): void; } export interface TriggerDispatcher { onProperty(playerID: string, property: Protocol.Media.PlayerProperty): void; onError(playerID: string, error: Protocol.Media.PlayerError): void; onMessage(playerID: string, message: Protocol.Media.PlayerMessage): void; onEvent(playerID: string, event: PlayerEvent): void; } class PlayerDataCollection implements TriggerHandler { private readonly properties: Map<string, string>; private readonly messages: Protocol.Media.PlayerMessage[]; private readonly events: PlayerEvent[]; private readonly errors: Protocol.Media.PlayerError[]; constructor() { this.properties = new Map(); this.messages = []; this.events = []; this.errors = []; } onProperty(property: Protocol.Media.PlayerProperty): void { this.properties.set(property.name, property.value); } onError(error: Protocol.Media.PlayerError): void { this.errors.push(error); } onMessage(message: Protocol.Media.PlayerMessage): void { this.messages.push(message); } onEvent(event: PlayerEvent): void { this.events.push(event); } export(): { properties: Map<string, string>, messages: Protocol.Media.PlayerMessage[], events: PlayerEvent[], errors: Protocol.Media.PlayerError[], } { return {properties: this.properties, messages: this.messages, events: this.events, errors: this.errors}; } } export class PlayerDataDownloadManager implements TriggerDispatcher { private readonly playerDataCollection: Map<string, PlayerDataCollection>; constructor() { this.playerDataCollection = new Map(); } addPlayer(playerID: string): void { this.playerDataCollection.set(playerID, new PlayerDataCollection()); } onProperty(playerID: string, property: Protocol.Media.PlayerProperty): void { const playerProperty = this.playerDataCollection.get(playerID); if (!playerProperty) { return; } playerProperty.onProperty(property); } onError(playerID: string, error: Protocol.Media.PlayerError): void { const playerProperty = this.playerDataCollection.get(playerID); if (!playerProperty) { return; } playerProperty.onError(error); } onMessage(playerID: string, message: Protocol.Media.PlayerMessage): void { const playerProperty = this.playerDataCollection.get(playerID); if (!playerProperty) { return; } playerProperty.onMessage(message); } onEvent(playerID: string, event: PlayerEvent): void { const playerProperty = this.playerDataCollection.get(playerID); if (!playerProperty) { return; } playerProperty.onEvent(event); } exportPlayerData(playerID: string): { properties: Map<string, string>, messages: Protocol.Media.PlayerMessage[], events: PlayerEvent[], errors: Protocol.Media.PlayerError[], } { const playerProperty = this.playerDataCollection.get(playerID); if (!playerProperty) { throw new Error('Unable to find player'); } return playerProperty.export(); } deletePlayer(playerID: string): void { this.playerDataCollection.delete(playerID); } } export class MainView extends UI.Panel.PanelWithSidebar implements SDK.TargetManager.SDKModelObserver<MediaModel> { private detailPanels: Map<string, PlayerDetailView>; private deletedPlayers: Set<string>; private readonly downloadStore: PlayerDataDownloadManager; private readonly sidebar: PlayerListView; #playerIdsToPlayers: Map<string, Protocol.Media.Player>; #domNodeIdsToPlayerIds: Map<Protocol.DOM.BackendNodeId, string>; #placeholder: UI.EmptyWidget.EmptyWidget; readonly #initialPlayersLoadedPromise: Promise<void>; #initialPlayersLoadedPromiseResolve: () => void = () => {}; constructor(downloadStore: PlayerDataDownloadManager = new PlayerDataDownloadManager()) { super('media'); this.detailPanels = new Map(); this.#playerIdsToPlayers = new Map(); this.#domNodeIdsToPlayerIds = new Map(); this.#initialPlayersLoadedPromise = new Promise(resolve => { this.#initialPlayersLoadedPromiseResolve = resolve; }); this.deletedPlayers = new Set(); this.downloadStore = downloadStore; this.sidebar = new PlayerListView(this); this.sidebar.show(this.panelSidebarElement()); this.splitWidget().hideSidebar(); this.#placeholder = new UI.EmptyWidget.EmptyWidget(i18nString(UIStrings.noMediaPlayer), UIStrings.mediaPlayerDescription); this.#placeholder.show(this.mainElement()); this.#placeholder.link = MEDIA_PLAYER_EXPLANATION_URL; SDK.TargetManager.TargetManager.instance().observeModels(MediaModel, this, {scoped: true}); } renderMainPanel(playerID: string): void { if (!this.detailPanels.has(playerID)) { return; } const mainWidget = this.splitWidget().mainWidget(); if (mainWidget) { mainWidget.detachChildWidgets(); } this.detailPanels.get(playerID)?.show(this.mainElement()); } override wasShown(): void { super.wasShown(); for (const model of SDK.TargetManager.TargetManager.instance().models(MediaModel, {scoped: true})) { this.addEventListeners(model); } } override willHide(): void { for (const model of SDK.TargetManager.TargetManager.instance().models(MediaModel, {scoped: true})) { this.removeEventListeners(model); } } modelAdded(model: MediaModel): void { if (this.isShowing()) { this.addEventListeners(model); } } modelRemoved(model: MediaModel): void { this.removeEventListeners(model); } private addEventListeners(mediaModel: MediaModel): void { mediaModel.ensureEnabled(); mediaModel.addEventListener(Events.PLAYER_PROPERTIES_CHANGED, this.propertiesChanged, this); mediaModel.addEventListener(Events.PLAYER_EVENTS_ADDED, this.eventsAdded, this); mediaModel.addEventListener(Events.PLAYER_MESSAGES_LOGGED, this.messagesLogged, this); mediaModel.addEventListener(Events.PLAYER_ERRORS_RAISED, this.errorsRaised, this); mediaModel.addEventListener(Events.PLAYER_CREATED, this.playerCreated, this); } private removeEventListeners(mediaModel: MediaModel): void { mediaModel.removeEventListener(Events.PLAYER_PROPERTIES_CHANGED, this.propertiesChanged, this); mediaModel.removeEventListener(Events.PLAYER_EVENTS_ADDED, this.eventsAdded, this); mediaModel.removeEventListener(Events.PLAYER_MESSAGES_LOGGED, this.messagesLogged, this); mediaModel.removeEventListener(Events.PLAYER_ERRORS_RAISED, this.errorsRaised, this); mediaModel.removeEventListener(Events.PLAYER_CREATED, this.playerCreated, this); } private propertiesChanged(event: Common.EventTarget.EventTargetEvent<Protocol.Media.PlayerPropertiesChangedEvent>): void { for (const property of event.data.properties) { this.onProperty(event.data.playerId, property); } } private eventsAdded(event: Common.EventTarget.EventTargetEvent<Protocol.Media.PlayerEventsAddedEvent>): void { for (const ev of event.data.events) { // TODO(crbug.com/1228674): The conversion from Protocol.Media.PlayerEvent to PlayerEvent happens implicitly // by augmenting the protocol type with some additional property in various places. This needs to be cleaned up // in a conversion function that takes the protocol type and produces the PlayerEvent type. this.onEvent(event.data.playerId, ev as PlayerEvent); } } private messagesLogged(event: Common.EventTarget.EventTargetEvent<Protocol.Media.PlayerMessagesLoggedEvent>): void { for (const message of event.data.messages) { this.onMessage(event.data.playerId, message); } } private errorsRaised(event: Common.EventTarget.EventTargetEvent<Protocol.Media.PlayerErrorsRaisedEvent>): void { for (const error of event.data.errors) { this.onError(event.data.playerId, error); } } private shouldPropagate(playerID: string): boolean { return !this.deletedPlayers.has(playerID) && this.detailPanels.has(playerID); } onProperty(playerID: string, property: Protocol.Media.PlayerProperty): void { if (!this.shouldPropagate(playerID)) { return; } this.sidebar.onProperty(playerID, property); this.downloadStore.onProperty(playerID, property); this.detailPanels.get(playerID)?.onProperty(property); } onError(playerID: string, error: Protocol.Media.PlayerError): void { if (!this.shouldPropagate(playerID)) { return; } this.sidebar.onError(playerID, error); this.downloadStore.onError(playerID, error); this.detailPanels.get(playerID)?.onError(error); } onMessage(playerID: string, message: Protocol.Media.PlayerMessage): void { if (!this.shouldPropagate(playerID)) { return; } this.sidebar.onMessage(playerID, message); this.downloadStore.onMessage(playerID, message); this.detailPanels.get(playerID)?.onMessage(message); } onEvent(playerID: string, event: PlayerEvent): void { if (!this.shouldPropagate(playerID)) { return; } this.sidebar.onEvent(playerID, event); this.downloadStore.onEvent(playerID, event); this.detailPanels.get(playerID)?.onEvent(event); } selectPlayerByDOMNodeId(domNodeId: Protocol.DOM.BackendNodeId): void { const playerId = this.#domNodeIdsToPlayerIds.get(domNodeId); if (!playerId) { return; } const player = this.#playerIdsToPlayers.get(playerId); if (player) { this.sidebar.selectPlayerById(player.playerId); } } waitForInitialPlayers(): Promise<void> { return this.#initialPlayersLoadedPromise; } private playerCreated(event: Common.EventTarget.EventTargetEvent<Protocol.Media.Player>): void { const player = event.data; this.#playerIdsToPlayers.set(player.playerId, player); if (player.domNodeId) { this.#domNodeIdsToPlayerIds.set(player.domNodeId, player.playerId); } if (this.splitWidget().showMode() !== UI.SplitWidget.ShowMode.BOTH) { this.splitWidget().showBoth(); } this.sidebar.addMediaElementItem(player.playerId); this.detailPanels.set(player.playerId, new PlayerDetailView()); this.downloadStore.addPlayer(player.playerId); if (this.detailPanels.size === 1) { this.#placeholder.header = i18nString(UIStrings.noPlayerDetailsSelected); this.#placeholder.text = i18nString(UIStrings.selectToViewDetails); } this.#initialPlayersLoadedPromiseResolve(); } markPlayerForDeletion(playerID: string): void { // TODO(tmathmeyer): send this to chromium to save the storage space there too. this.deletedPlayers.add(playerID); this.detailPanels.delete(playerID); const player = this.#playerIdsToPlayers.get(playerID); if (player?.domNodeId) { this.#domNodeIdsToPlayerIds.delete(player.domNodeId); } this.#playerIdsToPlayers.delete(playerID); this.sidebar.deletePlayer(playerID); this.downloadStore.deletePlayer(playerID); if (this.detailPanels.size === 0) { this.#placeholder.header = i18nString(UIStrings.noMediaPlayer); this.#placeholder.text = i18nString(UIStrings.mediaPlayerDescription); this.splitWidget().hideSidebar(); const mainWidget = this.splitWidget().mainWidget(); if (mainWidget) { mainWidget.detachChildWidgets(); } this.#placeholder.show(this.mainElement()); } } markOtherPlayersForDeletion(playerID: string): void { for (const keyID of this.detailPanels.keys()) { if (keyID !== playerID) { this.markPlayerForDeletion(keyID); } } } exportPlayerData(playerID: string): void { const dump = this.downloadStore.exportPlayerData(playerID); const uriContent = 'data:application/octet-stream,' + encodeURIComponent(JSON.stringify(dump, null, 2)); const anchor = document.createElement('a'); anchor.href = uriContent; anchor.download = playerID + '.json'; anchor.click(); } }