| /* |
| * Copyright (C) 2023 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.MediaDetailsSidebarPanel = class MediaDetailsSidebarPanel extends WI.DOMDetailsSidebarPanel |
| { |
| // Static |
| |
| static StyleClassName = "media"; |
| |
| // Public |
| |
| constructor(delegate) |
| { |
| super("media-details", WI.UIString("Media")); |
| |
| this.#ui.sourceRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Source", "Source @ Media Sidebar", "Title for Source row in Media Sidebar")); |
| this.#ui.viewportRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Viewport", "Viewport @ Media Sidebar", "Title for Viewport row in Media Sidebar")); |
| this.#ui.framesRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Frames", "Frames @ Media Sidebar Frame Count", "Title for Frames row in Media Sidebar")); |
| this.#ui.resolutionRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Resolution", "Resolution @ Media Sidebar", "Title for Resolution row in Media Sidebar")); |
| this.#ui.videoFormatRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Video Format", "Video Format @ Media Sidebar", "Title for Video Format row in Media Sidebar")); |
| this.#ui.audioFormatRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Audio Format", "Audio Format @ Media Sidebar", "Title for Audio Format row in Media Sidebar")); |
| |
| let generalGroup = new WI.DetailsSectionGroup([this.#ui.sourceRow, this.#ui.viewportRow, this.#ui.framesRow, this.#ui.resolutionRow, this.#ui.videoFormatRow, this.#ui.audioFormatRow]); |
| this.#ui.generalSection = new WI.DetailsSection("media-details-general", WI.UIString("General", "General @ Media Sidebar", "Title for General Section in Media Sidebar"), [generalGroup]); |
| |
| this.#ui.videoCodecRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Video Codec", "Video Codec @ Media Sidebar", "Title for Video Codec row in Media Sidebar")); |
| this.#ui.videoProtectedRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Protected", "Protected @ Media Sidebar", "Title for Protected row in Media Sidebar")); |
| this.#ui.transferRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Transfer Function", "Transfer Function @ Media Sidebar", "Title for Transfer Function row in Media Sidebar")); |
| this.#ui.primariesRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Color Primaries", "Color Primaries @ Media Sidebar", "Title for Color Primaries row in Media Sidebar")); |
| this.#ui.matrixRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Matrix Coefficients", "Matrix Coefficients @ Media Sidebar", "Title for Matrix Coefficients row in Media Sidebar")); |
| this.#ui.fullRangeRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Color Range", "Color Range @ Media Sidebar", "Title for Color Range row in Media Sidebar")); |
| this.#ui.projectionRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Projection", "Projections @ Media Sidebar", "Title for Projection row in Media Sidebar")); |
| |
| let videoGroup = new WI.DetailsSectionGroup([this.#ui.videoCodecRow, this.#ui.videoProtectedRow, this.#ui.primariesRow, this.#ui.transferRow, this.#ui.matrixRow, this.#ui.fullRangeRow, this.#ui.projectionRow]); |
| this.#ui.videoSection = new WI.DetailsSection("media-video-details", WI.UIString("Video Details", "Video Details @ Media Sidebar", "Title for Video Details section in Media Sidebar"), [videoGroup]); |
| this.#ui.audioCodecRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Audio Codec", "Audio Codec @ Media Sidebar", "Title for Audio Codec row in Media Sidebar")); |
| this.#ui.audioProtectedRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Protected", "Protected @ Media Sidebar", "Title for Protected row in Media Sidebar")); |
| this.#ui.sampleRateRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Sample Rate", "Sample Rate @ Media Sidebar", "Title for Sample Rate row in Media Sidebar")); |
| this.#ui.channelsRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Channels", "Channels @ Media Sidebar", "Title for Channels row in Media Sidebar")); |
| |
| let audioGroup = new WI.DetailsSectionGroup([this.#ui.audioCodecRow, this.#ui.audioProtectedRow, this.#ui.sampleRateRow, this.#ui.channelsRow]); |
| this.#ui.audioSection = new WI.DetailsSection("media-audio-details", WI.UIString("Audio Details", "Audio Details @ Media Sidebar", "Title for Audio Details section in Media Sidebar"), [audioGroup]); |
| |
| this.#ui.spatialSizeRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Size", "Size @ Spatial Section @ Media Sidebar", "Titel for Size row in Spatial Section of Media Sidebar")); |
| this.#ui.fovRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("FOV", "FOV @ Media Sidebar", "Title for FOV in Media Sidebar")); |
| this.#ui.baselineRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Baseline", "Baseline @ Media Sidebar", "Title for Baseline in Media Sidebar")); |
| this.#ui.disparityRow = new WI.MediaDetailsSidebarPanel.#Row(WI.UIString("Disparity", "Disparity @ Media Sidebar", "Title for Disparity in Media Sidebar")); |
| |
| let spatialGroup = new WI.DetailsSectionGroup([this.#ui.spatialSizeRow, this.#ui.fovRow, this.#ui.baselineRow, this.#ui.disparityRow]); |
| this.#ui.spatialSection = new WI.DetailsSection("media-spatial-details", WI.UIString("Spatial Details", "Spatial Details @ Media Sidebar", "Title for Media Details section in Media Sidebar"), [spatialGroup]); |
| } |
| |
| layout() |
| { |
| this.#ui.sourceRow.updateValue(); |
| this.#ui.viewportRow.updateValue(); |
| this.#ui.framesRow.updateValue(); |
| this.#ui.resolutionRow.updateValue(); |
| this.#ui.videoFormatRow.updateValue(); |
| this.#ui.audioFormatRow.updateValue(); |
| this.#ui.videoCodecRow.updateValue(); |
| this.#ui.videoProtectedRow.updateValue(); |
| this.#ui.transferRow.updateValue(); |
| this.#ui.primariesRow.updateValue(); |
| this.#ui.matrixRow.updateValue(); |
| this.#ui.fullRangeRow.updateValue(); |
| this.#ui.projectionRow.updateValue(); |
| this.#ui.audioCodecRow.updateValue(); |
| this.#ui.audioProtectedRow.updateValue(); |
| this.#ui.sampleRateRow.updateValue(); |
| this.#ui.channelsRow.updateValue(); |
| this.#ui.spatialSizeRow.updateValue(); |
| this.#ui.fovRow.updateValue(); |
| this.#ui.baselineRow.updateValue(); |
| this.#ui.disparityRow.updateValue(); |
| } |
| |
| // Protected |
| |
| // DOMDetailsSidebarPanel Overrides |
| |
| supportsDOMNode(nodeToInspect) |
| { |
| return nodeToInspect.isMediaElement(); |
| } |
| |
| attached() |
| { |
| super.attached(); |
| this.#startUpdateTimer(); |
| } |
| |
| detached() |
| { |
| super.detached(); |
| this.#cancelUpdateTimer(); |
| } |
| |
| // View Overrides |
| |
| initialLayout() |
| { |
| super.initialLayout(); |
| |
| this.contentView.element.appendChild(this.#ui.generalSection.element); |
| this.contentView.element.appendChild(this.#ui.videoSection.element); |
| this.contentView.element.appendChild(this.#ui.audioSection.element); |
| this.contentView.element.appendChild(this.#ui.spatialSection.element); |
| } |
| |
| // Private |
| |
| static #Row = class MediaDetailsSectionSimpleRow extends WI.DetailsSectionSimpleRow |
| { |
| // Public |
| |
| set pendingValue(value) |
| { |
| this.#pendingValue = value; |
| this.#dirty = true; |
| } |
| |
| updateValue() |
| { |
| if (!this.#dirty) |
| return; |
| this.value = this.#pendingValue; |
| this.#pendingValue = null; |
| this.#dirty = false; |
| } |
| |
| // Private |
| |
| #pendingValue = null; |
| #dirty = null; |
| }; |
| |
| #values = { |
| source: null, |
| viewport: null, |
| devicePixelRatio: null, |
| quality: null, |
| videoFormat: null, |
| audioFormat: null, |
| video: null, |
| audio: null, |
| }; |
| |
| #ui = { |
| sourceRow: null, |
| viewportRow: null, |
| framesRow: null, |
| resolutionRow: null, |
| videoFormatRow: null, |
| audioFormatRow: null, |
| generalSection: null, |
| videoCodecRow: null, |
| transferRow: null, |
| primariesRow: null, |
| matrixRow: null, |
| fullRangeRow: null, |
| projectionRow: null, |
| videoSection: null, |
| audioCodecRow: null, |
| sampleRateRow: null, |
| channelsRow: null, |
| audioSection: null, |
| spatialSection: null, |
| spatialSizeRow: null, |
| fovRow: null, |
| baselineRow: null, |
| disparityRow: null, |
| }; |
| |
| #updateTimer = null; |
| #updateTimerInterval = 250; |
| |
| #startUpdateTimer() |
| { |
| this.#cancelUpdateTimer(); |
| this.#updateTimerFired(); |
| } |
| |
| #cancelUpdateTimer() |
| { |
| if (this.#updateTimer) |
| clearTimeout(this.#updateTimer); |
| } |
| |
| async #updateTimerFired() |
| { |
| if (!this.domNode) |
| return; |
| |
| let stats = await this.domNode.getMediaStats(); |
| this.#setSource(stats.source); |
| this.#setViewport(stats.viewport); |
| this.#setDevicePixelRatio(stats.devicePixelRatio); |
| this.#setQuality(stats.quality); |
| this.#setVideoFormat(stats.video?.humanReadableCodecString); |
| this.#setAudioFormat(stats.audio?.humanReadableCodecString); |
| this.#setVideo(stats.video); |
| this.#setAudio(stats.audio); |
| |
| if (this.isAttached) |
| this.#updateTimer = setTimeout(() => this.#updateTimerFired(), this.#updateTimerInterval); |
| } |
| |
| #setSource(source) |
| { |
| if (this.#values.source === source) |
| return; |
| |
| this.#values.source = source; |
| this.#ui.sourceRow.pendingValue = this.#values.source; |
| this.needsLayout(); |
| } |
| |
| #setViewport(viewport) |
| { |
| if (Object.shallowEqual(this.#values.viewport, viewport)) |
| return; |
| |
| this.#values.viewport = viewport; |
| this.#updateViewportRow(); |
| } |
| |
| #setDevicePixelRatio(devicePixelRatio) |
| { |
| if (this.#values.devicePixelRatio === devicePixelRatio) |
| return; |
| |
| this.#values.devicePixelRatio = devicePixelRatio; |
| this.#updateViewportRow(); |
| } |
| |
| #updateViewportRow() |
| { |
| this.#ui.viewportRow.pendingValue = WI.UIString("%dx%d (%dx)").format(this.#values.viewport?.width ?? 0, this.#values.viewport?.height ?? 0, this.#values.devicePixelRatio ?? 1); |
| this.needsLayout(); |
| } |
| |
| #setQuality(quality) |
| { |
| if (Object.shallowEqual(this.#values.quality, quality)) |
| return; |
| |
| this.#values.quality = quality; |
| let formatString = WI.UIString("%d dropped of %d", "Dropped Frame Format @ Media Sidebar", "Format string for Dropped Frame count in Media Sidebar"); |
| this.#ui.framesRow.pendingValue = formatString.format(quality?.droppedVideoFrames ?? 0, quality?.totalVideoFrames ?? 0); |
| this.needsLayout(); |
| } |
| |
| #setVideoFormat(videoFormat) |
| { |
| if (this.#values.videoFormat === videoFormat) |
| return; |
| |
| this.#values.videoFormat = videoFormat; |
| this.#ui.videoFormatRow.pendingValue = videoFormat; |
| this.needsLayout(); |
| } |
| |
| #setAudioFormat(audioFormat) |
| { |
| if (this.#values.audioFormat === audioFormat) |
| return; |
| |
| this.#values.audioFormat = audioFormat; |
| this.#ui.audioFormatRow.pendingValue = audioFormat; |
| this.needsLayout(); |
| } |
| |
| #localizedVideoProjectionMetadataKindString(projectionKind) |
| { |
| if (!projectionKind) |
| return ""; |
| |
| switch (projectionKind) { |
| case "unknown": return WI.UIString("Unknown", "unknown @ Media Sidebar", "Value for 'unknown' in the Media Sidebar") |
| case "equirectangular": return WI.UIString("Equirectangular", "equirectangular @ Media Sidebar", "Value for 'equirectangular' in the Media Sidebar") |
| case "half-equirectangular": return WI.UIString("Half Equirectangular", "half-equirectangular @ Media Sidebar", "Value for 'half-equirectangular' in the Media Sidebar") |
| case "equi-angular-cubemap": return WI.UIString("Equi Angular Cubemap", "equi-angular-cubemap @ Media Sidebar", "Value for 'equi-angular-cubemap' in the Media Sidebar") |
| case "parametric": return WI.UIString("Parametric", "parametric @ Media Sidebar", "Value for 'parametric' in the Media Sidebar") |
| case "pyramid": return WI.UIString("Pyramid", "pyramid @ Media Sidebar", "Value for 'pyramid' in the Media Sidebar") |
| case "apple-immersive-video": return WI.UIString("Apple Immersive Video", "apple-immersive-video @ Media Sidebar", "Value for 'immersive' in the Media Sidebar") |
| } |
| } |
| |
| #setVideo(video) |
| { |
| const checkmark = "\u2713"; |
| |
| if (this.#values.video === video) |
| return; |
| |
| if (this.#values.video?.width === video?.width |
| && this.#values.video?.height === video?.height |
| && this.#values.video?.framerate === video?.framerate |
| && this.#values.video?.codec === video?.codec |
| && Object.shallowEqual(this.#values.video?.colorSpace, video?.colorSpace) |
| && this.#values.video?.colorSpace?.primaries === video?.colorSpace?.primaries |
| && this.#values.video?.colorSpace?.matrix === video?.colorSpace?.matrix |
| && this.#values.video?.fullRange === video?.fullRange |
| && this.#values.video?.isProtected === video?.isProtected) |
| return; |
| |
| this.#values.video = video; |
| this.#ui.videoSection.element.classList.toggle("hidden", !video); |
| this.#ui.resolutionRow.pendingValue = WI.UIString("%dx%d (%dfps)").format(video?.width ?? 0, video?.height ?? 0, video?.framerate ?? 0); |
| this.#ui.resolutionRow.element.classList.toggle("hidden", !video); |
| this.#ui.videoCodecRow.pendingValue = video?.codec; |
| this.#ui.videoProtectedRow.pendingValue = video?.isProtected ? checkmark : null; |
| this.#ui.transferRow.pendingValue = video?.colorSpace?.transfer; |
| this.#ui.primariesRow.pendingValue = video?.colorSpace?.primaries; |
| this.#ui.matrixRow.pendingValue = video?.colorSpace?.matrix; |
| if (video?.fullRange) |
| this.#ui.fullRangeRow.pendingValue = WI.UIString("Full range", "Full range @ Media Sidebar", "Value string for Full Range color in the Media Sidebar"); |
| else |
| this.#ui.fullRangeRow.pendingValue = WI.UIString("Video range", "Video range @ Media Sidebar", "Value string for Video Range color in the Media Sidebar"); |
| this.#ui.projectionRow.pendingValue = this.#localizedVideoProjectionMetadataKindString(video?.videoProjectionMetadata?.kind); |
| this.#ui.projectionRow.element.classList.toggle("hidden", !video?.videoProjectionMetadata); |
| this.#ui.spatialSizeRow.pendingValue = WI.UIString("%dx%d").format(video?.immersiveVideoMetadata?.width ?? 0, video?.immersiveVideoMetadata?.height ?? 0); |
| let horizontalFieldOfView = video?.immersiveVideoMetadata?.horizontalFieldOfView; |
| if (!isNaN(horizontalFieldOfView)) |
| horizontalFieldOfView /= 1000; |
| else { |
| // COMPATIBLITY (macOS 26.2, iOS 26.2): `horizontalFOVDegrees` was renamed to `horizontalFieldOfView`. |
| horizontalFieldOfView = video?.spatialVideoMetadata?.horizontalFOVDegrees ?? 0; |
| } |
| this.#ui.fovRow.pendingValue = WI.UIString("%dÂș").format(horizontalFieldOfView); |
| let stereoCameraBaseline = video?.immersiveVideoMetadata?.stereoCameraBaseline; |
| if (isNaN(stereoCameraBaseline)) { |
| // COMPATIBLITY (macOS 26.2, iOS 26.2): `baseline` was renamed to `stereoCameraBaseline`. |
| stereoCameraBaseline = (video?.spatialVideoMetadata?.baseline ?? 0) / 1000; |
| } |
| this.#ui.baselineRow.pendingValue = WI.UIString("%dmm").format(stereoCameraBaseline); |
| let horizontalDisparityAdjustment = video?.immersiveVideoMetadata?.horizontalDisparityAdjustment; |
| if (!isNaN(horizontalDisparityAdjustment)) |
| horizontalDisparityAdjustment /= 10; |
| else { |
| // COMPATIBLITY (macOS 26.2, iOS 26.2): `disparityAdjustment` was renamed to `horizontalDisparityAdjustment`. |
| horizontalDisparityAdjustment = (video?.spatialVideoMetadata?.disparityAdjustment ?? 0) * 100; |
| } |
| this.#ui.disparityRow.pendingValue = WI.UIString("%d%%").format(horizontalDisparityAdjustment); |
| this.#ui.spatialSection.element.classList.toggle("hidden", !video?.immersiveVideoMetadata && !video?.spatialVideoMetadata); |
| this.needsLayout(); |
| } |
| |
| #setAudio(audio) |
| { |
| if (Object.shallowEqual(this.#values.audio, audio)) |
| return; |
| |
| this.#values.audio = audio; |
| this.#ui.audioSection.element.classList.toggle("hidden", !audio); |
| this.#ui.audioCodecRow.pendingValue = audio?.codec; |
| this.#ui.audioProtectedRow.pendingValue = audio?.isProtected ? WI.UIString("True") : null; |
| this.#ui.sampleRateRow.pendingValue = WI.UIString("%d Hz").format(audio?.sampleRate); |
| switch (audio?.numberOfChannels) { |
| case 1: |
| this.#ui.channelsRow.pendingValue = WI.UIString("Mono"); |
| break; |
| case 2: |
| this.#ui.channelsRow.pendingValue = WI.UIString("Stereo"); |
| break; |
| default: |
| this.#ui.channelsRow.pendingValue = audio?.numberOfChannels; |
| break; |
| } |
| this.needsLayout(); |
| } |
| }; |