| // 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_underscored_properties */ |
| |
| import * as i18n from '../i18n/i18n.js'; |
| import * as UI from '../ui/ui.js'; |
| |
| export 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. |
| */ |
| 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 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', |
| }; |
| |
| const str_ = i18n.i18n.registerUIStrings('media/PlayerPropertiesView.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| type TabData = { |
| [x: string]: string, |
| }; |
| |
| // Keep this enum in sync with media/base/media_log_properties.h |
| export const enum PlayerPropertyKeys { |
| Resolution = 'kResolution', |
| TotalBytes = 'kTotalBytes', |
| Bitrate = 'kBitrate', |
| MaxDuration = 'kMaxDuration', |
| StartTime = 'kStartTime', |
| IsVideoEncrypted = 'kIsVideoEncrypted', |
| IsStreaming = 'kIsStreaming', |
| FrameUrl = 'kFrameUrl', |
| FrameTitle = 'kFrameTitle', |
| IsSingleOrigin = 'kIsSingleOrigin', |
| IsRangeHeaderSupported = 'kIsRangeHeaderSupported', |
| VideoDecoderName = 'kVideoDecoderName', |
| AudioDecoderName = 'kAudioDecoderName', |
| IsPlatformVideoDecoder = 'kIsPlatformVideoDecoder', |
| IsPlatformAudioDecoder = 'kIsPlatformAudioDecoder', |
| VideoEncoderName = 'kVideoEncoderName', |
| IsPlatformVideoEncoder = 'kIsPlatformVideoEncoder', |
| IsVideoDecryptingDemuxerStream = 'kIsVideoDecryptingDemuxerStream', |
| IsAudioDecryptingDemuxerStream = 'kIsAudioDecryptingDemuxerStream', |
| AudioTracks = 'kAudioTracks', |
| TextTracks = 'kTextTracks', |
| VideoTracks = 'kVideoTracks', |
| Framerate = 'kFramerate', |
| VideoPlaybackRoughness = 'kVideoPlaybackRoughness', |
| VideoPlaybackFreezing = 'kVideoPlaybackFreezing', |
| } |
| |
| export class PropertyRenderer extends UI.Widget.VBox { |
| _title: string; |
| _contents: HTMLElement; |
| _value: string|null; |
| _pseudoColorProtectionElement: HTMLDivElement|null; |
| |
| constructor(title: string) { |
| super(); |
| this.contentElement.classList.add('media-property-renderer'); |
| const titleElement = this.contentElement.createChild('span', 'media-property-renderer-title'); |
| this._contents = this.contentElement.createChild('span', 'media-property-renderer-contents'); |
| UI.UIUtils.createTextChild(titleElement, title); |
| this._title = title; |
| this._value = null; |
| this._pseudoColorProtectionElement = null; |
| this.contentElement.classList.add('media-property-renderer-hidden'); |
| } |
| |
| updateData(propname: string, propvalue: string): void { |
| // convert all empty possibilities into nulls for easier handling. |
| if (propvalue === '' || propvalue === null) { |
| return this._updateData(propname, null); |
| } |
| try { |
| propvalue = JSON.parse(propvalue) as string; |
| } catch (err) { |
| // 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._updateData(propname, propvalue); |
| } |
| |
| _updateData(propname: string, 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); |
| } |
| } |
| |
| changeContents(value: string|null): void { |
| if (value === null) { |
| 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); |
| } |
| } 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 { |
| _formatfunction: (arg0: string) => string; |
| constructor(title: string, formatfunction: (arg0: string) => string) { |
| super(i18nString(title)); |
| this._formatfunction = formatfunction; |
| } |
| |
| _updateData(propname: string, propvalue: string|null): void { |
| if (propvalue === null) { |
| this.changeContents(null); |
| } else { |
| this.changeContents(this._formatfunction(propvalue)); |
| } |
| } |
| } |
| |
| export class DefaultPropertyRenderer extends PropertyRenderer { |
| constructor(title: string, defaultText: string) { |
| super(i18nString(title)); |
| this.changeContents(defaultText); |
| } |
| } |
| |
| export class DimensionPropertyRenderer extends PropertyRenderer { |
| _width: number; |
| _height: number; |
| |
| constructor(title: string) { |
| super(i18nString(title)); |
| this._width = 0; |
| this._height = 0; |
| } |
| |
| _updateData(propname: string, propvalue: string|null): void { |
| let needsUpdate = false; |
| if (propname === 'width' && Number(propvalue) !== this._width) { |
| this._width = Number(propvalue); |
| needsUpdate = true; |
| } |
| if (propname === 'height' && Number(propvalue) !== this._height) { |
| this._height = Number(propvalue); |
| needsUpdate = true; |
| } |
| // If both properties arent set, don't bother updating, since |
| // temporarily showing ie: 1920x0 is meaningless. |
| if (this._width === 0 || this._height === 0) { |
| this.changeContents(null); |
| } else if (needsUpdate) { |
| this.changeContents(`${this._width}×${this._height}`); |
| } |
| } |
| } |
| |
| export class AttributesView extends UI.Widget.VBox { |
| constructor(elements: UI.Widget.Widget[]) { |
| super(); |
| this.contentElement.classList.add('media-attributes-view'); |
| for (const element of elements) { |
| element.show(this.contentElement); |
| } |
| } |
| } |
| |
| export class TrackManager { |
| _type: string; |
| _view: PlayerPropertiesView; |
| |
| constructor(propertiesView: PlayerPropertiesView, type: string) { |
| this._type = type; |
| this._view = propertiesView; |
| } |
| |
| updateData(_name: string, 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)) { |
| tabElements.push(new DefaultPropertyRenderer(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: i18nString(UIStrings.video), |
| Audio: i18nString(UIStrings.audio), |
| }; |
| |
| class GenericTrackMenu extends UI.TabbedPane.TabbedPane { |
| _decoderName: string; |
| _trackName: string; |
| |
| constructor(decoderName: string, trackName: string = i18nString(UIStrings.track)) { |
| super(); |
| this._decoderName = decoderName; |
| this._trackName = trackName; |
| } |
| |
| addNewTab(trackNumber: number, element: UI.Widget.Widget): void { |
| const localizedTrackLower = i18nString(UIStrings.track); |
| this.appendTab( |
| `Track${trackNumber}`, // 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('DecoderProperties', title, informationalElement, hoverText); |
| } |
| } |
| |
| class NoTracksPlaceholderMenu extends UI.Widget.VBox { |
| _isPlaceholder: boolean; |
| _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: UI.Widget.Widget): void { |
| if (this._isPlaceholder) { |
| this._wrapping.closeTab('_placeholder'); |
| this._isPlaceholder = false; |
| } |
| this._wrapping.addNewTab(trackNumber, element); |
| } |
| } |
| |
| export class PlayerPropertiesView extends UI.Widget.VBox { |
| _mediaElements: PropertyRenderer[]; |
| _videoDecoderElements: PropertyRenderer[]; |
| _audioDecoderElements: PropertyRenderer[]; |
| _textTrackElements: PropertyRenderer[]; |
| _attributeMap: Map<string, PropertyRenderer|TrackManager>; |
| _videoProperties: AttributesView; |
| _videoDecoderProperties: AttributesView; |
| _audioDecoderProperties: AttributesView; |
| _videoDecoderTabs: DecoderTrackMenu; |
| _audioDecoderTabs: DecoderTrackMenu; |
| _textTrackTabs: GenericTrackMenu|NoTracksPlaceholderMenu|null; |
| _textTracksTabs?: NoTracksPlaceholderMenu; |
| |
| constructor() { |
| super(); |
| this.contentElement.classList.add('media-properties-frame'); |
| this.registerRequiredCSS('media/playerPropertiesView.css', {enableLegacyPatching: true}); |
| this._mediaElements = []; |
| this._videoDecoderElements = []; |
| this._audioDecoderElements = []; |
| this._textTrackElements = []; |
| 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._textTrackTabs = null; |
| } |
| |
| _lazyCreateTrackTabs(): GenericTrackMenu|NoTracksPlaceholderMenu { |
| let textTracksTabs = this._textTrackTabs; |
| 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.name, 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.TotalBytes, 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.MaxDuration, duration); |
| |
| const startTime = new PropertyRenderer(i18nString(UIStrings.startTime)); |
| this._mediaElements.push(startTime); |
| this._attributeMap.set(PlayerPropertyKeys.StartTime, startTime); |
| |
| const streaming = new PropertyRenderer(i18nString(UIStrings.streaming)); |
| this._mediaElements.push(streaming); |
| this._attributeMap.set(PlayerPropertyKeys.IsStreaming, streaming); |
| |
| const frameUrl = new PropertyRenderer(i18nString(UIStrings.playbackFrameUrl)); |
| this._mediaElements.push(frameUrl); |
| this._attributeMap.set(PlayerPropertyKeys.FrameUrl, frameUrl); |
| |
| const frameTitle = new PropertyRenderer(i18nString(UIStrings.playbackFrameTitle)); |
| this._mediaElements.push(frameTitle); |
| this._attributeMap.set(PlayerPropertyKeys.FrameTitle, frameTitle); |
| |
| const singleOrigin = new PropertyRenderer(i18nString(UIStrings.singleoriginPlayback)); |
| this._mediaElements.push(singleOrigin); |
| this._attributeMap.set(PlayerPropertyKeys.IsSingleOrigin, singleOrigin); |
| |
| const rangeHeaders = new PropertyRenderer(i18nString(UIStrings.rangeHeaderSupport)); |
| this._mediaElements.push(rangeHeaders); |
| this._attributeMap.set(PlayerPropertyKeys.IsRangeHeaderSupported, 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.VideoPlaybackRoughness, roughness); |
| |
| const freezingScore = new PropertyRenderer(i18nString(UIStrings.videoFreezingScore)); |
| this._mediaElements.push(freezingScore); |
| this._attributeMap.set(PlayerPropertyKeys.VideoPlaybackFreezing, freezingScore); |
| |
| /* Video Decoder Properties */ |
| const decoderName = new DefaultPropertyRenderer(i18nString(UIStrings.decoderName), i18nString(UIStrings.noDecoder)); |
| this._videoDecoderElements.push(decoderName); |
| this._attributeMap.set(PlayerPropertyKeys.VideoDecoderName, decoderName); |
| |
| const videoPlatformDecoder = new PropertyRenderer(i18nString(UIStrings.hardwareDecoder)); |
| this._videoDecoderElements.push(videoPlatformDecoder); |
| this._attributeMap.set(PlayerPropertyKeys.IsPlatformVideoDecoder, videoPlatformDecoder); |
| |
| const encoderName = new DefaultPropertyRenderer(i18nString(UIStrings.encoderName), i18nString(UIStrings.noEncoder)); |
| this._videoDecoderElements.push(encoderName); |
| this._attributeMap.set(PlayerPropertyKeys.VideoEncoderName, encoderName); |
| |
| const videoPlatformEncoder = new PropertyRenderer(i18nString(UIStrings.hardwareEncoder)); |
| this._videoDecoderElements.push(videoPlatformEncoder); |
| this._attributeMap.set(PlayerPropertyKeys.IsPlatformVideoEncoder, videoPlatformEncoder); |
| |
| const videoDDS = new PropertyRenderer(i18nString(UIStrings.decryptingDemuxer)); |
| this._videoDecoderElements.push(videoDDS); |
| this._attributeMap.set(PlayerPropertyKeys.IsVideoDecryptingDemuxerStream, videoDDS); |
| |
| const videoTrackManager = new VideoTrackManager(this); |
| this._attributeMap.set(PlayerPropertyKeys.VideoTracks, videoTrackManager); |
| |
| /* Audio Decoder Properties */ |
| const audioDecoder = |
| new DefaultPropertyRenderer(i18nString(UIStrings.decoderName), i18nString(UIStrings.noDecoder)); |
| this._audioDecoderElements.push(audioDecoder); |
| this._attributeMap.set(PlayerPropertyKeys.AudioDecoderName, audioDecoder); |
| |
| const audioPlatformDecoder = new PropertyRenderer(i18nString(UIStrings.hardwareDecoder)); |
| this._audioDecoderElements.push(audioPlatformDecoder); |
| this._attributeMap.set(PlayerPropertyKeys.IsPlatformAudioDecoder, audioPlatformDecoder); |
| |
| const audioDDS = new PropertyRenderer(i18nString(UIStrings.decryptingDemuxer)); |
| this._audioDecoderElements.push(audioDDS); |
| this._attributeMap.set(PlayerPropertyKeys.IsAudioDecryptingDemuxerStream, audioDDS); |
| |
| const audioTrackManager = new AudioTrackManager(this); |
| this._attributeMap.set(PlayerPropertyKeys.AudioTracks, audioTrackManager); |
| |
| const textTrackManager = new TextTrackManager(this); |
| this._attributeMap.set(PlayerPropertyKeys.TextTracks, textTrackManager); |
| } |
| } |