UNPKG

chrome-devtools-frontend

Version:
638 lines (558 loc) • 23.6 kB
// 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. /* eslint-disable rulesdir/no-imperative-dom-api */ import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import type * as Protocol from '../../generated/protocol.js'; import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import playerPropertiesViewStyles from './playerPropertiesView.css.js'; const UIStrings = { /** *@description The type of media, for example - video, audio, or text. Capitalized. */ video: 'Video', /** *@description The type of media, for example - video, audio, or text. Capitalized. */ audio: 'Audio', /** *@description A video or audio stream - but capitalized. */ track: 'Track', /** *@description A device that converts media files into playable streams of audio or video. */ decoder: 'Decoder', /** *@description Title of the 'Properties' tool in the sidebar of the elements tool */ properties: 'Properties', /** *@description Menu label for text tracks, it is followed by a number, like 'Text Track #1' */ textTrack: 'Text track', /** * @description Placeholder text stating that there are no text tracks on this player. A text track * is all of the text that accompanies a particular video. */ noTextTracks: 'No text tracks', /** *@description Media property giving the width x height of the video */ resolution: 'Resolution', /** *@description Media property giving the file size of the media */ fileSize: 'File size', /** *@description Media property giving the media file bitrate */ bitrate: 'Bitrate', /** *@description Text for the duration of something */ duration: 'Duration', /** *@description The label for a timestamp when a video was started. */ startTime: 'Start time', /** *@description Media property signaling whether the media is streaming */ streaming: 'Streaming', /** *@description Media property describing where the media is playing from. */ playbackFrameUrl: 'Playback frame URL', /** *@description Media property giving the title of the frame where the media is embedded */ playbackFrameTitle: 'Playback frame title', /** *@description Media property describing whether the file is single or cross origin in nature */ singleoriginPlayback: 'Single-origin playback', /** *@description Media property describing support for range http headers */ rangeHeaderSupport: '`Range` header support', /** *@description Media property giving the media file frame rate */ frameRate: 'Frame rate', /** * @description Media property giving the distance of the playback quality from the ideal playback. * Roughness is the opposite to smoothness, i.e. whether each frame of the video was played at the * right time so that the video looks smooth when it plays. */ videoPlaybackRoughness: 'Video playback roughness', /** *@description A score describing how choppy the video playback is. */ videoFreezingScore: 'Video freezing score', /** *@description Media property giving the name of the renderer being used */ rendererName: 'Renderer name', /** *@description Media property giving the name of the decoder being used */ decoderName: 'Decoder name', /** *@description There is no decoder */ noDecoder: 'No decoder', /** *@description Media property signaling whether a hardware decoder is being used */ hardwareDecoder: 'Hardware decoder', /** *@description Media property signaling whether the content is encrypted. This is a noun phrase for *a demultiplexer that does decryption. */ decryptingDemuxer: 'Decrypting demuxer', /** *@description Media property giving the name of the video encoder being used. */ encoderName: 'Encoder name', /** *@description There is no encoder. */ noEncoder: 'No encoder', /** *@description Media property signaling whether the encoder is hardware accelerated. */ hardwareEncoder: 'Hardware encoder', } as const; const str_ = i18n.i18n.registerUIStrings('panels/media/PlayerPropertiesView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_); type TabData = Record<string, string|object>; // Keep this enum in sync with panels/media/base/media_log_properties.h export const enum PlayerPropertyKeys { RESOLUTION = 'kResolution', TOTAL_BYTES = 'kTotalBytes', BITRATE = 'kBitrate', MAX_DURATION = 'kMaxDuration', START_TIME = 'kStartTime', IS_CDM_ATTACHED = 'kIsCdmAttached', IS_STREAMING = 'kIsStreaming', FRAME_URL = 'kFrameUrl', FRAME_TITLE = 'kFrameTitle', IS_SINGLE_ORIGIN = 'kIsSingleOrigin', IS_RANGE_HEADER_SUPPORTED = 'kIsRangeHeaderSupported', RENDERER_NAME = 'kRendererName', VIDEO_DECODER_NAME = 'kVideoDecoderName', AUDIO_DECODER_NAME = 'kAudioDecoderName', IS_PLATFORM_VIDEO_DECODER = 'kIsPlatformVideoDecoder', IS_PLATFORM_AUDIO_DECODER = 'kIsPlatformAudioDecoder', VIDEO_ENCODER_NAME = 'kVideoEncoderName', IS_PLATFORM_VIDEO_ENCODER = 'kIsPlatformVideoEncoder', IS_VIDEO_DECRYPTION_DEMUXER_STREAM = 'kIsVideoDecryptingDemuxerStream', IS_AUDIO_DECRYPTING_DEMUXER_STREAM = 'kIsAudioDecryptingDemuxerStream', AUDIO_TRACKS = 'kAudioTracks', TEXT_TRACKS = 'kTextTracks', VIDEO_TRACKS = 'kVideoTracks', FRAMERATE = 'kFramerate', VIDEO_PLAYBACK_ROUGHNESS = 'kVideoPlaybackRoughness', VIDEO_PLAYBACK_FREEZING = 'kVideoPlaybackFreezing', } export class PropertyRenderer extends UI.Widget.VBox { private readonly contents: HTMLElement; private value: string|null; private pseudoColorProtectionElement: HTMLDivElement|null; constructor(title: Platform.UIString.LocalizedString) { super(); this.contentElement.classList.add('media-property-renderer'); const titleElement = this.contentElement.createChild('span', 'media-property-renderer-title'); this.contents = this.contentElement.createChild('div', 'media-property-renderer-contents'); UI.UIUtils.createTextChild(titleElement, title); this.value = null; this.pseudoColorProtectionElement = null; this.contentElement.classList.add('media-property-renderer-hidden'); } updateData(propvalue: string): void { // convert all empty possibilities into nulls for easier handling. if (propvalue === '' || propvalue === null) { return this.updateDataInternal(null); } try { propvalue = JSON.parse(propvalue) as string; } catch { // TODO(tmathmeyer) typecheck the type of propvalue against // something defined or sourced from the c++ definitions. // Do nothing, some strings just stay strings! } return this.updateDataInternal(propvalue); } protected updateDataInternal(propvalue: string|null): void { if (propvalue === null) { this.changeContents(null); } else if (this.value === propvalue) { return; // Don't rebuild element! } else { this.value = propvalue; this.changeContents(propvalue); } } protected unsetNestedContents(): void { this.contentElement.classList.add('media-property-renderer-hidden'); if (this.pseudoColorProtectionElement === null) { this.pseudoColorProtectionElement = document.createElement('div'); this.pseudoColorProtectionElement.classList.add('media-property-renderer'); this.pseudoColorProtectionElement.classList.add('media-property-renderer-hidden'); (this.contentElement.parentNode as HTMLElement) .insertBefore(this.pseudoColorProtectionElement, this.contentElement); } } changeNestedContents(value: object): void { if (value === null || Object.keys(value).length === 0) { this.unsetNestedContents(); } else { if (this.pseudoColorProtectionElement !== null) { this.pseudoColorProtectionElement.remove(); this.pseudoColorProtectionElement = null; } this.contentElement.classList.remove('media-property-renderer-hidden'); this.contents.removeChildren(); const jsonWrapperElement = new SourceFrame.JSONView.JSONView(new SourceFrame.JSONView.ParsedJSON(value, '', ''), true); jsonWrapperElement.show(this.contents); } } changeContents(value: string|null): void { if (value === null) { this.unsetNestedContents(); } else { if (this.pseudoColorProtectionElement !== null) { this.pseudoColorProtectionElement.remove(); this.pseudoColorProtectionElement = null; } this.contentElement.classList.remove('media-property-renderer-hidden'); this.contents.removeChildren(); const spanElement = document.createElement('span'); spanElement.textContent = value; this.contents.appendChild(spanElement); } } } export class FormattedPropertyRenderer extends PropertyRenderer { private readonly formatfunction: (arg0: string) => string; constructor(title: Platform.UIString.LocalizedString, formatfunction: (arg0: string) => string) { super(title); this.formatfunction = formatfunction; } override updateDataInternal(propvalue: string|null): void { if (propvalue === null) { this.changeContents(null); } else { this.changeContents(this.formatfunction(propvalue)); } } } export class DefaultPropertyRenderer extends PropertyRenderer { constructor(title: Platform.UIString.LocalizedString, defaultText: string) { super(title); this.changeContents(defaultText); } } export class NestedPropertyRenderer extends PropertyRenderer { constructor(title: Platform.UIString.LocalizedString, content: object) { super(title); this.changeNestedContents(content); } } export class AttributesView extends UI.Widget.VBox { private readonly contentHash: number; constructor(elements: UI.Widget.Widget[]) { super(); this.contentHash = 0; this.contentElement.classList.add('media-attributes-view'); for (const element of elements) { element.show(this.contentElement); // We just need a really simple way to compare the topical equality // of the attributes views in order to avoid deleting and recreating // a node containing exactly the same data. const content = this.contentElement.textContent; if (content !== null) { this.contentHash += Platform.StringUtilities.hashCode(content); } } } getContentHash(): number { return this.contentHash; } } export class TrackManager { private readonly type: string; private readonly view: PlayerPropertiesView; constructor(propertiesView: PlayerPropertiesView, type: string) { this.type = type; this.view = propertiesView; } updateData(value: string): void { const tabs = this.view.getTabs(this.type); const newTabs = JSON.parse(value) as TabData[]; let enumerate = 1; for (const tabData of newTabs) { this.addNewTab(tabs, tabData, enumerate); enumerate++; } } addNewTab(tabs: GenericTrackMenu|NoTracksPlaceholderMenu, tabData: TabData, tabNumber: number): void { const tabElements = []; for (const [name, data] of Object.entries(tabData)) { if (typeof data === 'object') { tabElements.push(new NestedPropertyRenderer(i18n.i18n.lockedString(name), data)); } else { tabElements.push(new DefaultPropertyRenderer(i18n.i18n.lockedString(name), data)); } } const newTab = new AttributesView(tabElements); tabs.addNewTab(tabNumber, newTab); } } export class VideoTrackManager extends TrackManager { constructor(propertiesView: PlayerPropertiesView) { super(propertiesView, 'video'); } } export class TextTrackManager extends TrackManager { constructor(propertiesView: PlayerPropertiesView) { super(propertiesView, 'text'); } } export class AudioTrackManager extends TrackManager { constructor(propertiesView: PlayerPropertiesView) { super(propertiesView, 'audio'); } } const TrackTypeLocalized = { Video: i18nLazyString(UIStrings.video), Audio: i18nLazyString(UIStrings.audio), }; class GenericTrackMenu extends UI.TabbedPane.TabbedPane { private readonly decoderName: string; private readonly trackName: string; constructor(decoderName: string, trackName: string = i18nString(UIStrings.track)) { super(); this.decoderName = decoderName; this.trackName = trackName; } addNewTab(trackNumber: number, element: AttributesView): void { const localizedTrackLower = i18nString(UIStrings.track); const tabId = `track-${trackNumber}` as Lowercase<string>; if (this.hasTab(tabId)) { const tabElement = this.tabView(tabId); if (tabElement === null) { return; } if ((tabElement as AttributesView).getContentHash() === element.getContentHash()) { return; } this.closeTab(tabId, /* userGesture=*/ false); } this.appendTab( tabId, // No need for localizing, internal ID. `${this.trackName} #${trackNumber}`, element, `${this.decoderName} ${localizedTrackLower} #${trackNumber}`); } } class DecoderTrackMenu extends GenericTrackMenu { constructor(decoderName: string, informationalElement: UI.Widget.Widget) { super(decoderName); const decoderLocalized = i18nString(UIStrings.decoder); const title = `${decoderName} ${decoderLocalized}`; const propertiesLocalized = i18nString(UIStrings.properties); const hoverText = `${title} ${propertiesLocalized}`; this.appendTab('decoder-properties', title, informationalElement, hoverText); } } class NoTracksPlaceholderMenu extends UI.Widget.VBox { private isPlaceholder: boolean; private readonly wrapping: GenericTrackMenu; constructor(wrapping: GenericTrackMenu, placeholderText: string) { super(); this.isPlaceholder = true; this.wrapping = wrapping; this.wrapping.appendTab('_placeholder', placeholderText, new UI.Widget.VBox(), placeholderText); this.wrapping.show(this.contentElement); } addNewTab(trackNumber: number, element: AttributesView): void { if (this.isPlaceholder) { this.wrapping.closeTab('_placeholder'); this.isPlaceholder = false; } this.wrapping.addNewTab(trackNumber, element); } } export class PlayerPropertiesView extends UI.Widget.VBox { private readonly mediaElements: PropertyRenderer[]; private readonly videoDecoderElements: PropertyRenderer[]; private readonly audioDecoderElements: PropertyRenderer[]; private readonly attributeMap: Map<string, PropertyRenderer|TrackManager>; private readonly videoProperties: AttributesView; private readonly videoDecoderProperties: AttributesView; private readonly audioDecoderProperties: AttributesView; private readonly videoDecoderTabs: DecoderTrackMenu; private readonly audioDecoderTabs: DecoderTrackMenu; private textTracksTabs: GenericTrackMenu|NoTracksPlaceholderMenu|null; constructor() { super(); this.registerRequiredCSS(playerPropertiesViewStyles); this.element.setAttribute('jslog', `${VisualLogging.pane('properties')}`); this.contentElement.classList.add('media-properties-frame'); this.mediaElements = []; this.videoDecoderElements = []; this.audioDecoderElements = []; this.attributeMap = new Map(); this.populateAttributesAndElements(); this.videoProperties = new AttributesView(this.mediaElements); this.videoDecoderProperties = new AttributesView(this.videoDecoderElements); this.audioDecoderProperties = new AttributesView(this.audioDecoderElements); this.videoProperties.show(this.contentElement); this.videoDecoderTabs = new DecoderTrackMenu(TrackTypeLocalized.Video(), this.videoDecoderProperties); this.videoDecoderTabs.show(this.contentElement); this.audioDecoderTabs = new DecoderTrackMenu(TrackTypeLocalized.Audio(), this.audioDecoderProperties); this.audioDecoderTabs.show(this.contentElement); this.textTracksTabs = null; } private lazyCreateTrackTabs(): GenericTrackMenu|NoTracksPlaceholderMenu { let textTracksTabs = this.textTracksTabs; if (textTracksTabs === null) { const textTracks = new GenericTrackMenu(i18nString(UIStrings.textTrack)); textTracksTabs = new NoTracksPlaceholderMenu(textTracks, i18nString(UIStrings.noTextTracks)); textTracksTabs.show(this.contentElement); this.textTracksTabs = textTracksTabs; } return textTracksTabs; } getTabs(type: string): GenericTrackMenu|NoTracksPlaceholderMenu { if (type === 'audio') { return this.audioDecoderTabs; } if (type === 'video') { return this.videoDecoderTabs; } if (type === 'text') { return this.lazyCreateTrackTabs(); } // There should be no other type allowed. throw new Error('Unreachable'); } onProperty(property: Protocol.Media.PlayerProperty): void { const renderer = this.attributeMap.get(property.name); if (!renderer) { throw new Error(`Player property "${property.name}" not supported.`); } renderer.updateData(property.value); } formatKbps(bitsPerSecond: string|number): string { if (bitsPerSecond === '') { return '0 kbps'; } const kbps = Math.floor(Number(bitsPerSecond) / 1000); return `${kbps} kbps`; } formatTime(seconds: string|number): string { if (seconds === '') { return '0:00'; } const date = new Date(); date.setSeconds(Number(seconds)); return date.toISOString().substr(11, 8); } formatFileSize(bytes: string): string { if (bytes === '') { return '0 bytes'; } const actualBytes = Number(bytes); if (actualBytes < 1000) { return `${bytes} bytes`; } const power = Math.floor(Math.log10(actualBytes) / 3); const suffix = ['bytes', 'kB', 'MB', 'GB', 'TB'][power]; const bytesDecimal = (actualBytes / Math.pow(1000, power)).toFixed(2); return `${bytesDecimal} ${suffix}`; } populateAttributesAndElements(): void { /* Media properties */ const resolution = new PropertyRenderer(i18nString(UIStrings.resolution)); this.mediaElements.push(resolution); this.attributeMap.set(PlayerPropertyKeys.RESOLUTION, resolution); const fileSize = new FormattedPropertyRenderer(i18nString(UIStrings.fileSize), this.formatFileSize); this.mediaElements.push(fileSize); this.attributeMap.set(PlayerPropertyKeys.TOTAL_BYTES, fileSize); const bitrate = new FormattedPropertyRenderer(i18nString(UIStrings.bitrate), this.formatKbps); this.mediaElements.push(bitrate); this.attributeMap.set(PlayerPropertyKeys.BITRATE, bitrate); const duration = new FormattedPropertyRenderer(i18nString(UIStrings.duration), this.formatTime); this.mediaElements.push(duration); this.attributeMap.set(PlayerPropertyKeys.MAX_DURATION, duration); const startTime = new PropertyRenderer(i18nString(UIStrings.startTime)); this.mediaElements.push(startTime); this.attributeMap.set(PlayerPropertyKeys.START_TIME, startTime); const streaming = new PropertyRenderer(i18nString(UIStrings.streaming)); this.mediaElements.push(streaming); this.attributeMap.set(PlayerPropertyKeys.IS_STREAMING, streaming); const frameUrl = new PropertyRenderer(i18nString(UIStrings.playbackFrameUrl)); this.mediaElements.push(frameUrl); this.attributeMap.set(PlayerPropertyKeys.FRAME_URL, frameUrl); const frameTitle = new PropertyRenderer(i18nString(UIStrings.playbackFrameTitle)); this.mediaElements.push(frameTitle); this.attributeMap.set(PlayerPropertyKeys.FRAME_TITLE, frameTitle); const singleOrigin = new PropertyRenderer(i18nString(UIStrings.singleoriginPlayback)); this.mediaElements.push(singleOrigin); this.attributeMap.set(PlayerPropertyKeys.IS_SINGLE_ORIGIN, singleOrigin); const rangeHeaders = new PropertyRenderer(i18nString(UIStrings.rangeHeaderSupport)); this.mediaElements.push(rangeHeaders); this.attributeMap.set(PlayerPropertyKeys.IS_RANGE_HEADER_SUPPORTED, rangeHeaders); const frameRate = new PropertyRenderer(i18nString(UIStrings.frameRate)); this.mediaElements.push(frameRate); this.attributeMap.set(PlayerPropertyKeys.FRAMERATE, frameRate); const roughness = new PropertyRenderer(i18nString(UIStrings.videoPlaybackRoughness)); this.mediaElements.push(roughness); this.attributeMap.set(PlayerPropertyKeys.VIDEO_PLAYBACK_ROUGHNESS, roughness); const freezingScore = new PropertyRenderer(i18nString(UIStrings.videoFreezingScore)); this.mediaElements.push(freezingScore); this.attributeMap.set(PlayerPropertyKeys.VIDEO_PLAYBACK_FREEZING, freezingScore); const rendererName = new PropertyRenderer(i18nString(UIStrings.rendererName)); this.mediaElements.push(rendererName); this.attributeMap.set(PlayerPropertyKeys.RENDERER_NAME, rendererName); /* Video Decoder Properties */ const decoderName = new DefaultPropertyRenderer(i18nString(UIStrings.decoderName), i18nString(UIStrings.noDecoder)); this.videoDecoderElements.push(decoderName); this.attributeMap.set(PlayerPropertyKeys.VIDEO_DECODER_NAME, decoderName); const videoPlatformDecoder = new PropertyRenderer(i18nString(UIStrings.hardwareDecoder)); this.videoDecoderElements.push(videoPlatformDecoder); this.attributeMap.set(PlayerPropertyKeys.IS_PLATFORM_VIDEO_DECODER, videoPlatformDecoder); const encoderName = new DefaultPropertyRenderer(i18nString(UIStrings.encoderName), i18nString(UIStrings.noEncoder)); this.videoDecoderElements.push(encoderName); this.attributeMap.set(PlayerPropertyKeys.VIDEO_ENCODER_NAME, encoderName); const videoPlatformEncoder = new PropertyRenderer(i18nString(UIStrings.hardwareEncoder)); this.videoDecoderElements.push(videoPlatformEncoder); this.attributeMap.set(PlayerPropertyKeys.IS_PLATFORM_VIDEO_ENCODER, videoPlatformEncoder); const videoDDS = new PropertyRenderer(i18nString(UIStrings.decryptingDemuxer)); this.videoDecoderElements.push(videoDDS); this.attributeMap.set(PlayerPropertyKeys.IS_VIDEO_DECRYPTION_DEMUXER_STREAM, videoDDS); const videoTrackManager = new VideoTrackManager(this); this.attributeMap.set(PlayerPropertyKeys.VIDEO_TRACKS, videoTrackManager); /* Audio Decoder Properties */ const audioDecoder = new DefaultPropertyRenderer(i18nString(UIStrings.decoderName), i18nString(UIStrings.noDecoder)); this.audioDecoderElements.push(audioDecoder); this.attributeMap.set(PlayerPropertyKeys.AUDIO_DECODER_NAME, audioDecoder); const audioPlatformDecoder = new PropertyRenderer(i18nString(UIStrings.hardwareDecoder)); this.audioDecoderElements.push(audioPlatformDecoder); this.attributeMap.set(PlayerPropertyKeys.IS_PLATFORM_AUDIO_DECODER, audioPlatformDecoder); const audioDDS = new PropertyRenderer(i18nString(UIStrings.decryptingDemuxer)); this.audioDecoderElements.push(audioDDS); this.attributeMap.set(PlayerPropertyKeys.IS_AUDIO_DECRYPTING_DEMUXER_STREAM, audioDDS); const audioTrackManager = new AudioTrackManager(this); this.attributeMap.set(PlayerPropertyKeys.AUDIO_TRACKS, audioTrackManager); const textTrackManager = new TextTrackManager(this); this.attributeMap.set(PlayerPropertyKeys.TEXT_TRACKS, textTrackManager); } }