blob: 1dfd860bd8bbaeaa7d7381d2d444c2b3b65280c0 [file]
/*
* 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();
}
};