chrome-devtools-frontend
Version:
Chrome DevTools UI
256 lines (223 loc) • 9.58 kB
text/typescript
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as i18n from '../../core/i18n/i18n.js';
import * as UI from '../../ui/legacy/legacy.js';
import playerListViewStyles from './playerListView.css.js';
import type * as Protocol from '../../generated/protocol.js';
import {type MainView, type TriggerDispatcher} from './MainView.js';
import {type PlayerEvent} from './MediaModel.js';
import {PlayerPropertyKeys} from './PlayerPropertiesView.js';
const UIStrings = {
/**
*@description A right-click context menu entry which when clicked causes the menu entry for that player to be removed.
*/
hidePlayer: 'Hide player',
/**
*@description A right-click context menu entry which should keep the element selected, while hiding all other entries.
*/
hideAllOthers: 'Hide all others',
/**
*@description Context menu entry which downloads the json dump when clicked
*/
savePlayerInfo: 'Save player info',
/**
*@description Side-panel entry title text for the players section.
*/
players: 'Players',
};
const str_ = i18n.i18n.registerUIStrings('panels/media/PlayerListView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export interface PlayerStatus {
playerTitle: string;
playerID: string;
exists: boolean;
playing: boolean;
titleEdited: boolean;
}
export interface PlayerStatusMapElement {
playerStatus: PlayerStatus;
playerTitleElement: HTMLElement|null;
}
export class PlayerListView extends UI.Widget.VBox implements TriggerDispatcher {
private readonly playerEntryFragments: Map<string, UI.Fragment.Fragment>;
private readonly playerEntriesWithHostnameFrameTitle: Set<string>;
private readonly mainContainer: MainView;
private currentlySelectedEntry: Element|null;
constructor(mainContainer: MainView) {
super(true);
this.playerEntryFragments = new Map();
this.playerEntriesWithHostnameFrameTitle = new Set();
// Container where new panels can be added based on clicks.
this.mainContainer = mainContainer;
this.currentlySelectedEntry = null;
this.contentElement.createChild('div', 'player-entry-header').textContent = i18nString(UIStrings.players);
}
private createPlayerListEntry(playerID: string): UI.Fragment.Fragment {
const entry = UI.Fragment.Fragment.build`
<div class="player-entry-row hbox">
<div class="player-entry-status-icon vbox">
<div $="icon" class="player-entry-status-icon-centering"></div>
</div>
<div $="frame-title" class="player-entry-frame-title">FrameTitle</div>
<div $="player-title" class="player-entry-player-title">PlayerTitle</div>
</div>
`;
const element = entry.element();
element.addEventListener('click', this.selectPlayer.bind(this, playerID, element));
element.addEventListener('contextmenu', this.rightClickPlayer.bind(this, playerID));
entry.$('icon').appendChild(UI.Icon.Icon.create('pause', 'media-player'));
return entry;
}
private selectPlayer(playerID: string, element: Element): void {
this.mainContainer.renderMainPanel(playerID);
if (this.currentlySelectedEntry !== null) {
this.currentlySelectedEntry.classList.remove('selected');
this.currentlySelectedEntry.classList.remove('force-white-icons');
}
element.classList.add('selected');
element.classList.add('force-white-icons');
this.currentlySelectedEntry = element;
}
private rightClickPlayer(playerID: string, event: Event): boolean {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.headerSection().appendItem(
i18nString(UIStrings.hidePlayer), this.mainContainer.markPlayerForDeletion.bind(this.mainContainer, playerID));
contextMenu.headerSection().appendItem(
i18nString(UIStrings.hideAllOthers),
this.mainContainer.markOtherPlayersForDeletion.bind(this.mainContainer, playerID));
contextMenu.headerSection().appendItem(
i18nString(UIStrings.savePlayerInfo), this.mainContainer.exportPlayerData.bind(this.mainContainer, playerID));
void contextMenu.show();
return true;
}
private setMediaElementFrameTitle(playerID: string, frameTitle: string, isHostname: boolean): void {
// Only remove the title from the set if we arent setting a hostname title.
// Otherwise, if it has a non-hostname title, and the requested new title is
// a hostname, just drop it.
if (this.playerEntriesWithHostnameFrameTitle.has(playerID)) {
if (!isHostname) {
this.playerEntriesWithHostnameFrameTitle.delete(playerID);
}
} else if (isHostname) {
return;
}
if (!this.playerEntryFragments.has(playerID)) {
return;
}
const fragment = this.playerEntryFragments.get(playerID);
if (fragment === undefined || fragment.element() === undefined) {
return;
}
fragment.$('frame-title').textContent = frameTitle;
}
private setMediaElementPlayerTitle(playerID: string, playerTitle: string): void {
if (!this.playerEntryFragments.has(playerID)) {
return;
}
const fragment = this.playerEntryFragments.get(playerID);
if (fragment === undefined) {
return;
}
fragment.$('player-title').textContent = playerTitle;
}
private setMediaElementPlayerIcon(playerID: string, iconName: string): void {
if (!this.playerEntryFragments.has(playerID)) {
return;
}
const fragment = this.playerEntryFragments.get(playerID);
if (fragment === undefined) {
return;
}
const icon = fragment.$('icon');
if (icon === undefined) {
return;
}
icon.textContent = '';
icon.appendChild(UI.Icon.Icon.create(iconName, 'media-player'));
}
private formatAndEvaluate(playerID: string, func: Function, candidate: string, min: number, max: number): void {
if (candidate.length <= min) {
return;
}
if (candidate.length >= max) {
candidate = candidate.substring(0, max - 3) + '...';
}
func.bind(this)(playerID, candidate);
}
addMediaElementItem(playerID: string): void {
const sidebarEntry = this.createPlayerListEntry(playerID);
this.contentElement.appendChild(sidebarEntry.element());
this.playerEntryFragments.set(playerID, sidebarEntry);
this.playerEntriesWithHostnameFrameTitle.add(playerID);
}
deletePlayer(playerID: string): void {
if (!this.playerEntryFragments.has(playerID)) {
return;
}
const fragment = this.playerEntryFragments.get(playerID);
if (fragment === undefined || fragment.element() === undefined) {
return;
}
this.contentElement.removeChild(fragment.element());
this.playerEntryFragments.delete(playerID);
}
onEvent(playerID: string, event: PlayerEvent): void {
const parsed = JSON.parse(event.value);
const eventType = parsed.event;
// Load events provide the actual underlying URL for the video, which makes
// a great way to identify a specific video within a page that potentially
// may have many videos. MSE videos have a special blob:http(s) protocol
// that we'd like to keep mind of, so we do prepend blob:
if (eventType === 'kLoad') {
const url = parsed.url as string;
const videoName = url.substring(url.lastIndexOf('/') + 1);
this.formatAndEvaluate(playerID, this.setMediaElementPlayerTitle, videoName, 1, 20);
return;
}
if (eventType === 'kPlay') {
this.setMediaElementPlayerIcon(playerID, 'play');
return;
}
if (eventType === 'kPause' || eventType === 'kEnded') {
this.setMediaElementPlayerIcon(playerID, 'pause');
return;
}
if (eventType === 'kWebMediaPlayerDestroyed') {
this.setMediaElementPlayerIcon(playerID, 'cross');
return;
}
}
onProperty(playerID: string, property: Protocol.Media.PlayerProperty): void {
// FrameUrl is always present, and we can generate a basic frame title from
// it by grabbing the hostname. It's not possible to generate a "good" player
// title from the FrameUrl though, since the page location itself might not
// have any relevance to the video being played, and would be shared by all
// videos on the page.
if (property.name === PlayerPropertyKeys.FrameUrl) {
const frameTitle = new URL(property.value).hostname;
this.formatAndEvaluate(playerID, this.setMediaElementFrameTitle, frameTitle, 1, 20);
return;
}
// On the other hand, the page may set a title, which usually makes for a
// better frame title than a hostname. Unfortunately, its only "usually",
// since the site is free to set the title to _anything_, it might just be
// junk, or it might be super long. If it's empty, or 1 character, It's
// preferable to just drop it. Titles longer than 20 will have the first
// 17 characters kept and an elipsis appended.
if (property.name === PlayerPropertyKeys.FrameTitle && property.value) {
this.formatAndEvaluate(playerID, this.setMediaElementFrameTitle, property.value, 1, 20);
return;
}
}
onError(_playerID: string, _error: Protocol.Media.PlayerError): void {
// TODO(tmathmeyer) show an error icon next to the player name
}
onMessage(_playerID: string, _message: Protocol.Media.PlayerMessage): void {
// TODO(tmathmeyer) show a message count number next to the player name.
}
override wasShown(): void {
super.wasShown();
this.registerCSSFiles([playerListViewStyles]);
}
}