//# sourceURL=__InjectedScript_ModernMediaControls.js const MinimumSizeToShowAnyControl = 47; const MaximumSizeToShowSmallProminentControl = 88; if (!window.utils) { window.utils = { formattedStringForDuration: function (duration) { return ""; }, }; } function createControls(shadowRoot, media, host) { if (host) { for (let styleSheet of host.shadowRootStyleSheets) shadowRoot.appendChild(document.createElement("style")).textContent = styleSheet; } controller = new MediaController(shadowRoot, media, host); if (host) host.controller = controller; return controller; } function UIString(stringToLocalize, ...replacementStrings) { let localizedString = window.UIStrings?.[stringToLocalize] ?? stringToLocalize; for (let replacementString of replacementStrings) localizedString = localizedString.replace("%s", replacementString); return localizedString; } function formatTimeByUnit(value) { const time = value || 0; const absTime = Math.abs(time); const sign = Math.sign(time); return { seconds: sign * Math.floor(absTime % 60).toFixed(0), minutes: sign * Math.floor((absTime / 60) % 60).toFixed(0), hours: sign * Math.floor(absTime / (60 * 60)).toFixed(0), }; } function unitizeTime(value, unit) { let returnedUnit = UIString(unit); if (value > 1) returnedUnit = UIString(`${unit}s`); return `${value} ${returnedUnit}`; } class GestureRecognizer { constructor(target = null, delegate = null) { this._targetPointers = new Map(); this.modifierKeys = { alt: false, ctrl: false, meta: false, shift: false }; this._state = GestureRecognizer.States.Possible; this._enabled = true; this.target = target; this.delegate = delegate; } get state() { return this._state; } set state(state) { if (this._state === state && state !== GestureRecognizer.States.Changed) return; this._state = state; if ( this.delegate && typeof this.delegate.gestureRecognizerStateDidChange === "function" ) this.delegate.gestureRecognizerStateDidChange(this); } get target() { return this._target; } set target(target) { if (!target || this._target === target) return; this._target = target; this._initRecognizer(); } get numberOfTouches() { return this._targetPointers.size; } get enabled() { return this._enabled; } set enabled(enabled) { if (this._enabled === enabled) return; this._enabled = enabled; if (!enabled) { if (this.numberOfTouches === 0) { this._removeTrackingListeners(); this.reset(); } else this.enterCancelledState(); } this._updateBaseListeners(); } reset() {} locationInElement(element) { const p = new DOMPoint(); const count = this._targetPointers.size; if (!count) return p; this._targetPointers.forEach(function (pointer) { p.x += pointer.pageX; p.y += pointer.pageY; }); p.x /= count; p.y /= count; if (!element) return p; const wkPoint = window.webkitConvertPointFromPageToNode( element, new WebKitPoint(p.x, p.y), ); return new DOMPoint(wkPoint.x, wkPoint.y); } locationInClient() { const p = new DOMPoint(); const count = this._targetPointers.size; if (!count) return p; this._targetPointers.forEach(function (pointer) { p.x += pointer.clientX; p.y += pointer.clientY; }); p.x /= count; p.y /= count; return p; } touchesBegan(event) { if (event.currentTarget !== this._target) return; this._captureTarget.addEventListener( GestureRecognizer.Events.PointerMove, this, true, ); this._captureTarget.addEventListener( GestureRecognizer.Events.PointerUp, this, true, ); this._captureTarget.addEventListener( GestureRecognizer.Events.PointerCancel, this, true, ); this.enterPossibleState(); } touchesMoved(event) {} touchesEnded(event) {} touchesCancelled(event) {} gestureBegan(event) { if (event.currentTarget !== this._target) return; this._target.addEventListener( GestureRecognizer.Events.GestureChange, this, true, ); this._target.addEventListener( GestureRecognizer.Events.GestureEnd, this, true, ); this.enterPossibleState(); } gestureChanged(event) {} gestureEnded(event) {} enterPossibleState() { this.state = GestureRecognizer.States.Possible; } enterBeganState() { if ( this.delegate && typeof this.delegate.gestureRecognizerShouldBegin === "function" && !this.delegate.gestureRecognizerShouldBegin(this) ) { this.enterFailedState(); return; } this.state = GestureRecognizer.States.Began; } enterEndedState() { this.state = GestureRecognizer.States.Ended; this._removeTrackingListeners(); this.reset(); } enterCancelledState() { this.state = GestureRecognizer.States.Cancelled; this._removeTrackingListeners(); this.reset(); } enterFailedState() { this.state = GestureRecognizer.States.Failed; this._removeTrackingListeners(); this.reset(); } enterChangedState() { this.state = GestureRecognizer.States.Changed; } enterRecognizedState() { this.state = GestureRecognizer.States.Recognized; } handleEvent(event) { this._updateTargetTouches(event); this._updateKeyboardModifiers(event); switch (event.type) { case GestureRecognizer.Events.PointerDown: this.touchesBegan(event); break; case GestureRecognizer.Events.PointerMove: this.touchesMoved(event); break; case GestureRecognizer.Events.PointerUp: this.touchesEnded(event); break; case GestureRecognizer.Events.PointerCancel: this.touchesCancelled(event); break; case GestureRecognizer.Events.GestureStart: this.gestureBegan(event); break; case GestureRecognizer.Events.GestureChange: this.gestureChanged(event); break; case GestureRecognizer.Events.GestureEnd: this.gestureEnded(event); break; } } _initRecognizer() { this.reset(); this.state = GestureRecognizer.States.Possible; this._updateBaseListeners(); } _updateBaseListeners() { if (!this._target) return; if (this._enabled) { this._target.addEventListener(GestureRecognizer.Events.PointerDown, this); if (GestureRecognizer.SupportsGestures) this._target.addEventListener( GestureRecognizer.Events.GestureStart, this, ); } else { this._target.removeEventListener( GestureRecognizer.Events.PointerDown, this, ); if (GestureRecognizer.SupportsGestures) this._target.removeEventListener( GestureRecognizer.Events.GestureStart, this, ); } } get _captureTarget() { if (GestureRecognizer.SupportsTouches) return this._target; return window; } _removeTrackingListeners() { this._captureTarget.removeEventListener( GestureRecognizer.Events.PointerMove, this, true, ); this._captureTarget.removeEventListener( GestureRecognizer.Events.PointerUp, this, true, ); this._captureTarget.removeEventListener( GestureRecognizer.Events.PointerCancel, this, true, ); this._target.removeEventListener( GestureRecognizer.Events.GestureChange, this, true, ); this._target.removeEventListener( GestureRecognizer.Events.GestureEnd, this, true, ); this._targetPointers = new Map(); } _updateTargetTouches(event) { if (!(event instanceof PointerEvent)) return; if (event.type === GestureRecognizer.Events.PointerDown) { this._targetPointers.set(event.pointerId, event); return; } if (event.type === GestureRecognizer.Events.PointerMove) { this._targetPointers.set(event.pointerId, event); return; } this._targetPointers.delete(event.pointerId); } _updateKeyboardModifiers(event) { this.modifierKeys.alt = event.altKey; this.modifierKeys.ctrl = event.ctrlKey; this.modifierKeys.meta = event.metaKey; this.modifierKeys.shift = event.shiftKey; } } GestureRecognizer.SupportsTouches = "createTouch" in document; GestureRecognizer.SupportsGestures = !!window.GestureEvent; GestureRecognizer.States = { Possible: "possible", Began: "began", Changed: "changed", Ended: "ended", Cancelled: "cancelled", Failed: "failed", Recognized: "ended", }; GestureRecognizer.Events = { PointerDown: "pointerdown", PointerMove: "pointermove", PointerUp: "pointerup", PointerCancel: "pointercancel", GestureStart: "gesturestart", GestureChange: "gesturechange", GestureEnd: "gestureend", }; const MOVE_TOLERANCE = GestureRecognizer.SupportsTouches ? 40 : 0; const WAITING_FOR_NEXT_TAP_TO_START_TIMEOUT = 350; const WAITING_FOR_TAP_COMPLETION_TIMEOUT = 750; class TapGestureRecognizer extends GestureRecognizer { constructor(target, delegate) { super(target, delegate); this.numberOfTapsRequired = 1; this.numberOfTouchesRequired = 1; this.allowsRightMouseButton = false; } touchesBegan(event) { if (event.currentTarget !== this.target) return; if (event.button === 2 && !this.allowsRightMouseButton) return; super.touchesBegan(event); if (this.numberOfTouches !== this.numberOfTouchesRequired) { this.enterFailedState(); return; } this._startPoint = super.locationInElement(); this._startClientPoint = super.locationInClient(); this._rewindTimer(WAITING_FOR_TAP_COMPLETION_TIMEOUT); } touchesMoved(event) { const touchLocation = super.locationInElement(); const distance = Math.sqrt( Math.pow(this._startPoint.x - touchLocation.x, 2) + Math.pow(this._startPoint.y - touchLocation.y, 2), ); if (distance > MOVE_TOLERANCE) this.enterFailedState(); } touchesEnded(event) { this._taps++; if (this._taps === this.numberOfTapsRequired) { event.preventDefault(); this.enterRecognizedState(); this.reset(); } this._rewindTimer(WAITING_FOR_NEXT_TAP_TO_START_TIMEOUT); } reset() { this._taps = 0; this._clearTimer(); } locationInElement(element) { const p = this._startPoint || new DOMPoint(); if (!element) return p; const wkPoint = window.webkitConvertPointFromPageToNode( element, new WebKitPoint(p.x, p.y), ); return new DOMPoint(wkPoint.x, wkPoint.y); } locationInClient() { return this._startClientPoint || new DOMPoint(); } _clearTimer() { window.clearTimeout(this._timerId); delete this._timerId; } _rewindTimer(timeout) { this._clearTimer(); this._timerId = window.setTimeout(this._timerFired.bind(this), timeout); } _timerFired() { this.enterFailedState(); } } const MAXIMUM_TIME_FOR_RECORDING_GESTURES = 100; const MAXIMUM_DECELERATION_TIME = 500; class PinchGestureRecognizer extends GestureRecognizer { constructor(target, delegate) { super(target, delegate); this.scaleThreshold = 0; this._scaledMinimumAmount = false; } get velocity() { const lastGesture = this._gestures[this._gestures.length - 1]; if (!lastGesture) return this._velocity; const elapsedTime = Date.now() - (lastGesture.timeStamp + MAXIMUM_TIME_FOR_RECORDING_GESTURES); if (elapsedTime <= 0) return this._velocity; const f = Math.max( (MAXIMUM_DECELERATION_TIME - elapsedTime) / MAXIMUM_DECELERATION_TIME, 0, ); return this._velocity * f; } touchesBegan(event) { if (event.currentTarget !== this.target) return; if (!GestureRecognizer.SupportsGestures) { if (this.numberOfTouches > 2) { this.enterFailedState(); return; } if (this.numberOfTouches !== 2) return; this._startDistance = this._distance(); this._recordGesture(1); this._scaledMinimumAmount = false; this._updateStateWithEvent(event); } else if (this.numberOfTouches !== 2) { return; } super.touchesBegan(event); } touchesMoved(event) { if (GestureRecognizer.SupportsGestures) return; if (this.numberOfTouches !== 2) return; this._updateStateWithEvent(event); } touchesEnded(event) { if (GestureRecognizer.SupportsGestures) return; if (this.numberOfTouches >= 2 || !this._startDistance) return; if (this._scaledMinimumAmount) this.enterEndedState(); else this.enterFailedState(); } gestureBegan(event) { super.gestureBegan(event); this._recordGesture(event.scale); this._scaledMinimumAmount = false; this._updateStateWithEvent(event); event.preventDefault(); } gestureChanged(event) { event.preventDefault(); this._updateStateWithEvent(event); } gestureEnded(event) { if (this._scaledMinimumAmount) this.enterEndedState(); else this.enterFailedState(); } reset() { this.scale = 1; this._velocity = 0; this._gestures = []; delete this._startDistance; } _recordGesture(scale) { const currentTime = Date.now(); const count = this._gestures.push({ scale: scale, timeStamp: currentTime }); if (count <= 2) return; const scaleDirection = this._gestures[count - 1].scale >= this._gestures[count - 2].scale; let i = count - 3; for (; i >= 0; --i) { let gesture = this._gestures[i]; if ( currentTime - gesture.timeStamp > MAXIMUM_TIME_FOR_RECORDING_GESTURES || this._gestures[i + 1].scale >= gesture.scale !== scaleDirection ) break; } if (i > 0) this._gestures = this._gestures.slice(i + 1); } _updateStateWithEvent(event) { const scaleSinceStart = GestureRecognizer.SupportsGestures ? event.scale : this._distance() / this._startDistance; if (!this._scaledMinimumAmount) { if (Math.abs(1 - scaleSinceStart) >= this.scaleThreshold) { this._scaledMinimumAmount = true; this.scale = 1; this.enterBeganState(); } return; } this._recordGesture(scaleSinceStart); const oldestGesture = this._gestures[0]; const ds = scaleSinceStart - oldestGesture.scale; const dt = Date.now() - oldestGesture.timeStamp; this._velocity = dt === 0 ? 0 : (ds / dt) * 1000; this.scale *= scaleSinceStart / this._gestures[this._gestures.length - 2].scale; this.enterChangedState(); } _distance() { console.assert(this.numberOfTouches === 2); const firstTouch = this._targetTouches[0]; const firstTouchPoint = new DOMPoint(firstTouch.pageX, firstTouch.pageY); const secondTouch = this._targetTouches[1]; const secondTouchPoint = new DOMPoint(secondTouch.pageX, secondTouch.pageY); return Math.sqrt( Math.pow(firstTouchPoint.x - secondTouchPoint.x, 2) + Math.pow(firstTouchPoint.y - secondTouchPoint.y, 2), ); } } const scheduler = new (class { constructor() { this._frameID = -1; this._layoutCallbacks = new Set(); } get hasScheduledLayoutCallbacks() { return this._frameID !== -1 || this._layoutCallbacks.size > 0; } scheduleLayout(callback) { if (typeof callback !== "function") return; this._layoutCallbacks.add(callback); this._requestFrameIfNeeded(); } unscheduleLayout(callback) { if (typeof callback !== "function") return; this._layoutCallbacks.delete(callback); } flushScheduledLayoutCallbacks() { this._frameDidFire(); } _requestFrameIfNeeded() { if (this._frameID === -1 && this._layoutCallbacks.size > 0) this._frameID = window.requestAnimationFrame( this._frameDidFire.bind(this), ); } _frameDidFire() { if (typeof scheduler.frameWillFire === "function") scheduler.frameWillFire(); this._layout(); this._frameID = -1; this._requestFrameIfNeeded(); if (typeof scheduler.frameDidFire === "function") scheduler.frameDidFire(); } _layout() { const layoutCallbacks = this._layoutCallbacks; this._layoutCallbacks = new Set(); for (let callback of layoutCallbacks) callback(); } })(); class LayoutTraits { constructor(mode) { this._mode = mode; } get mode() { return this._mode; } get isFullscreen() { return ( this._mode == LayoutTraits.Mode.Fullscreen || this._mode == LayoutTraits.Mode.NarrowViewer ); } mediaControlsClass() { throw "Derived class must implement this function."; } supportingObjectClasses() { return [ AirplaySupport, AudioSupport, CloseSupport, ControlsVisibilitySupport, FullscreenSupport, MuteSupport, OverflowSupport, PiPSupport, PlacardSupport, PlaybackSupport, ScrubbingSupport, SeekBackwardSupport, SeekForwardSupport, SkipBackSupport, SkipForwardSupport, StartSupport, StatusSupport, TimeControlSupport, TracksSupport, VolumeSupport, ]; } resourceDirectory() { throw "Derived class must implement this function."; } controlsNeverAvailable() { throw "Derived class must implement this function."; } supportsIconWithFullscreenVariant() { throw "Derived class must implement this function."; } supportsDurationTimeLabel() { throw "Derived class must implement this function."; } skipDuration() { throw "Derived class must implement this function."; } controlsDependOnPageScaleFactor() { throw "Derived class must implement this function."; } promoteSubMenusWhenShowingMediaControlsContextMenu() { throw "Derived class must implement this function."; } supportsTouches() { return GestureRecognizer.SupportsTouches; } supportsAirPlay() { throw "Derived class must implement this function."; } supportsPiP() { throw "Derived class must implement this function."; } inheritsBorderRadius() { throw "Derived class must implement this function."; } } LayoutTraits.Mode = { Inline: 0, Fullscreen: 1, NarrowViewer: 2 }; window.layoutTraitsClasses = {}; const dirtyNodes = new Set(); const nodesRequiringChildrenUpdate = new Set(); class LayoutNode { constructor(stringOrElement) { if (!stringOrElement) this.element = document.createElement("div"); else if (stringOrElement instanceof Element) this.element = stringOrElement; else if ( typeof stringOrElement === "string" || stringOrElement instanceof String ) this.element = elementFromString(stringOrElement); this._parent = null; this._children = []; this._x = 0; this._y = 0; this._width = 0; this._height = 0; this._visible = true; this._needsLayout = false; this._dirtyProperties = new Set(); this._pendingDOMManipulation = LayoutNode.DOMManipulation.None; } get x() { return this._x; } set x(x) { if (x === this._x) return; this._x = x; this.markDirtyProperty("x"); } get y() { return this._y; } set y(y) { if (y === this._y) return; this._y = y; this.markDirtyProperty("y"); } get width() { return this._width; } set width(width) { if (width === this._width) return; this._width = width; this.markDirtyProperty("width"); this.layout(); } get height() { return this._height; } set height(height) { if (height === this._height) return; this._height = height; this.markDirtyProperty("height"); this.layout(); } get visible() { return this._visible; } set visible(flag) { if (flag === this._visible) return; this._visible = flag; this.markDirtyProperty("visible"); } get needsLayout() { return ( this._needsLayout || this._pendingDOMManipulation !== LayoutNode.DOMManipulation.None || this._dirtyProperties.size > 0 ); } set needsLayout(flag) { if (this.needsLayout === flag) return; this._needsLayout = flag; this._updateDirtyState(); } get parent() { return this._parent; } get children() { return this._children; } set children(children) { if (children.length === this._children.length) { let arraysDiffer = false; for (let i = children.length - 1; i >= 0; --i) { if (children[i] !== this._children[i]) { arraysDiffer = true; break; } } if (!arraysDiffer) return; } this._updatingChildren = true; while (this._children.length) this.removeChild(this._children[0]); for (let child of children) this.addChild(child); delete this._updatingChildren; this.didChangeChildren(); } parentOfType(type) { let node = this; while ((node = node._parent)) { if (node instanceof type) return node; } return null; } addChild(child, index) { child.remove(); if (index === undefined || index < 0 || index > this._children.length) index = this._children.length; this._children.splice(index, 0, child); child._parent = this; if (!this._updatingChildren) this.didChangeChildren(); child._markNodeManipulation(LayoutNode.DOMManipulation.Addition); return child; } insertBefore(newSibling, referenceSibling) { return this.addChild(newSibling, this._children.indexOf(referenceSibling)); } insertAfter(newSibling, referenceSibling) { const index = this._children.indexOf(referenceSibling); return this.addChild(newSibling, index + 1); } removeChild(child) { if (child._parent !== this) return; const index = this._children.indexOf(child); if (index === -1) return; this.willRemoveChild(child); this._children.splice(index, 1); child._parent = null; if (!this._updatingChildren) this.didChangeChildren(); child._markNodeManipulation(LayoutNode.DOMManipulation.Removal); return child; } remove() { if (this._parent instanceof LayoutNode) return this._parent.removeChild(this); } markDirtyProperty(propertyName) { const hadProperty = this._dirtyProperties.has(propertyName); this._dirtyProperties.add(propertyName); if (!hadProperty) this._updateDirtyState(); } computedValueForStyleProperty(propertyName) { return window.getComputedStyle(this.element).getPropertyValue(propertyName); } computedValueForStylePropertyInPx(propertyName) { const value = this.computedValueForStyleProperty(propertyName); if (!value) return 0; if (!value.endsWith("px")) return 0; return parseFloat(value); } layout() {} commit() { if (this._pendingDOMManipulation === LayoutNode.DOMManipulation.Removal) { const parent = this.element.parentNode; if (parent) parent.removeChild(this.element); } for (let propertyName of this._dirtyProperties) this.commitProperty(propertyName); this._dirtyProperties.clear(); if (this._pendingDOMManipulation === LayoutNode.DOMManipulation.Addition) nodesRequiringChildrenUpdate.add(this.parent); } commitProperty(propertyName) { const style = this.element.style; switch (propertyName) { case "x": style.left = `${this._x}px`; break; case "y": style.top = `${this._y}px`; break; case "width": style.width = `${this._width}px`; break; case "height": style.height = `${this._height}px`; break; case "visible": if (this._visible) style.removeProperty("display"); else style.display = "none"; break; } } willRemoveChild(child) {} didChangeChildren() {} _markNodeManipulation(manipulation) { this._pendingDOMManipulation = manipulation; this._updateDirtyState(); } _updateDirtyState() { if (this.needsLayout) { dirtyNodes.add(this); scheduler.scheduleLayout(performScheduledLayout); } else { dirtyNodes.delete(this); if (dirtyNodes.size === 0) scheduler.unscheduleLayout(performScheduledLayout); } } _updateChildren() { let nextChildElement = null; const element = this.element; for (let i = this.children.length - 1; i >= 0; --i) { let child = this.children[i]; let childElement = child.element; if ( child._pendingDOMManipulation === LayoutNode.DOMManipulation.Addition ) { element.insertBefore(childElement, nextChildElement); child._pendingDOMManipulation = LayoutNode.DOMManipulation.None; } nextChildElement = childElement; } } } LayoutNode.DOMManipulation = { None: 0, Removal: 1, Addition: 2 }; function performScheduledLayout() { const previousDirtyNodes = Array.from(dirtyNodes); dirtyNodes.clear(); previousDirtyNodes.forEach((node) => { node._needsLayout = false; node.layout(); node.commit(); }); nodesRequiringChildrenUpdate.forEach((node) => node._updateChildren()); nodesRequiringChildrenUpdate.clear(); } function elementFromString(elementString) { const element = document.createElement("div"); element.innerHTML = elementString; return element.firstElementChild; } class LayoutItem extends LayoutNode { constructor({ element = null, layoutDelegate = null } = {}) { super(element); this.layoutDelegate = layoutDelegate; } get layoutTraits() { if (!this.layoutDelegate?.layoutTraits) throw "No layout traits specified via layoutDelegate or overriden layouTraits() getter"; return this.layoutDelegate.layoutTraits; } } const Icons = { Airplay: { name: "Airplay", type: "svg", label: UIString("AirPlay") }, AirplayPlacard: { name: "airplay-placard", type: "png", label: UIString("AirPlay"), }, Close: { name: "X", type: "svg", label: UIString("Close") }, Ellipsis: { name: "Ellipsis", type: "svg", label: UIString("More\u2026") }, EnterFullscreen: { name: "EnterFullscreen", type: "svg", label: UIString("Enter Full Screen"), }, EnterPiP: { name: "PipIn", type: "svg", label: UIString("Enter Picture in Picture"), }, ExitFullscreen: { name: "ExitFullscreen", type: "svg", label: UIString("Exit Full Screen"), }, Forward: { name: "Forward", type: "svg", label: UIString("Forward") }, InvalidCircle: { name: "InvalidCircle", type: "pdf", label: UIString("Invalid"), }, InvalidPlacard: { name: "invalid-placard", type: "png", label: UIString("Invalid"), }, Overflow: { name: "Overflow", type: "svg", label: UIString("More\u2026") }, Pause: { name: "Pause", type: "svg", label: UIString("Pause") }, PiPPlacard: { name: "pip-placard", type: "png", label: UIString("Picture in Picture"), }, Play: { name: "Play", type: "svg", label: UIString("Play") }, PlayCircle: { name: "PlayCircle", type: "pdf", label: UIString("Play") }, Rewind: { name: "Rewind", type: "svg", label: UIString("Rewind") }, SkipBack10: { name: "SkipBack10", type: "svg", label: UIString("Skip Back 10 Seconds"), }, SkipBack15: { name: "SkipBack15", type: "svg", label: UIString("Skip Back 15 Seconds"), }, SkipForward10: { name: "SkipForward10", type: "svg", label: UIString("Skip Forward 10 Seconds"), }, SkipForward15: { name: "SkipForward15", type: "svg", label: UIString("Skip Forward 15 Seconds"), }, SpinnerSprite: { name: "SpinnerSprite", type: "png", label: UIString("Loading\u2026"), }, Tracks: { name: "MediaSelector", type: "svg", label: UIString("Media Selection"), }, Volume0: { name: "Volume0", type: "svg", label: UIString("Mute") }, Volume0RTL: { name: "Volume0-RTL", type: "svg", label: UIString("Mute") }, Volume1: { name: "Volume1", type: "svg", label: UIString("Mute") }, Volume1RTL: { name: "Volume1-RTL", type: "svg", label: UIString("Mute") }, Volume2: { name: "Volume2", type: "svg", label: UIString("Mute") }, Volume2RTL: { name: "Volume2-RTL", type: "svg", label: UIString("Mute") }, Volume3: { name: "Volume3", type: "svg", label: UIString("Mute") }, Volume3RTL: { name: "Volume3-RTL", type: "svg", label: UIString("Mute") }, VolumeMuted: { name: "VolumeMuted", type: "svg", label: UIString("Unmute") }, VolumeMutedRTL: { name: "VolumeMuted-RTL", type: "svg", label: UIString("Unmute"), }, }; const MimeTypes = { pdf: "application/pdf", png: "image/png", svg: "image/svg+xml", }; const IconsWithFullscreenVariants = [ Icons.Airplay, Icons.Tracks, Icons.EnterPiP, ]; const iconService = new (class IconService { constructor() { this.images = {}; } imageForIconAndLayoutTraits(icon, layoutTraits) { const [fileName, resourceDirectory] = this._fileNameAndResourceDirectoryForIconAndLayoutTraits( icon, layoutTraits, ); const path = `${resourceDirectory}/${fileName}.${icon.type}`; let image = this.images[path]; if (image) return image; if (utils?.createImageForIconNameAndType) image = this.images[path] = utils.createImageForIconNameAndType( fileName, icon.type, ); else { image = this.images[path] = new Image(); image.src = `${this.directoryPath}/${path}`; } return image; } _fileNameAndResourceDirectoryForIconAndLayoutTraits(icon, layoutTraits) { let resourceDirectory = layoutTraits.resourceDirectory(); let iconName = icon.name; if ( layoutTraits.supportsIconWithFullscreenVariant() && IconsWithFullscreenVariants.includes(icon) ) iconName += "-fullscreen"; let fileName = iconName; if (icon.type === "png") fileName = `${iconName}@${window.devicePixelRatio}x`; return [fileName, resourceDirectory]; } })(); class BackgroundTint extends LayoutNode { constructor() { super( `
`, ); } } const TenMinutes = 10 * 60; const OneHour = 6 * TenMinutes; const TenHours = 10 * OneHour; const MinimumTimeControlWidth = 160; const IdealMinimumTimeControlWidth = 200; class TimeControl extends LayoutItem { constructor(layoutDelegate) { super({ element: `
`, layoutDelegate }); this.style = TimeControl.Style.Side; this._shouldShowDurationTimeLabel = this.layoutTraits.supportsDurationTimeLabel(); this.elapsedTimeLabel = new TimeLabel(TimeLabel.Type.Elapsed); this.scrubber = new Slider(this.layoutDelegate, "scrubber"); this.durationTimeLabel = new TimeLabel(TimeLabel.Type.Duration); this.remainingTimeLabel = new TimeLabel(TimeLabel.Type.Remaining); this.activityIndicator = new LayoutNode( `
`, ); for (let segmentClassName of ["n", "ne", "e", "se", "s", "sw", "w", "nw"]) this.activityIndicator.element.appendChild( document.createElement("div"), ).className = segmentClassName; this._duration = 0; this._currentTime = 0; this._loading = false; this._supportsSeeking = true; if (this._shouldShowDurationTimeLabel) { this.element.classList.add("duration"); this.durationTimeLabel.element.addEventListener("click", this); this.remainingTimeLabel.element.addEventListener("click", this); } this.needsLayout = true; } set duration(duration) { if (this._duration === duration) return; this._duration = duration; this.needsLayout = true; } set currentTime(currentTime) { if (this._currentTime === currentTime) return; this._currentTime = currentTime; this.needsLayout = true; } get loading() { return this._loading; } set loading(flag) { if (this._loading === flag) return; this._loading = flag; this.scrubber.disabled = this._loading || !this._supportsSeeking; this.needsLayout = true; } get supportsSeeking() { return this._supportsSeeking; } set supportsSeeking(flag) { if (this._supportsSeeking === flag) return; this._supportsSeeking = flag; this.scrubber.disabled = this._loading || !this._supportsSeeking; this.needsLayout = true; } get minimumWidth() { return MinimumTimeControlWidth; } get idealMinimumWidth() { return IdealMinimumTimeControlWidth; } layout() { super.layout(); this._performIdealLayout(); } handleEvent(event) { switch (event.type) { case "click": switch (event.target) { case this.durationTimeLabel.element: case this.remainingTimeLabel.element: this._toggleDurationRemainingLabel(); break; } } } _performIdealLayout() { let children = [ this._loading ? this.activityIndicator : this.elapsedTimeLabel, this.scrubber, this.remainingTimeLabel, ]; if (this._shouldShowDurationTimeLabel) children.push(this.durationTimeLabel); this.children = children; if (this._loading) { this.durationTimeLabel.value = NaN; this.remainingTimeLabel.value = NaN; } else { const shouldShowZeroDurations = isNaN(this._duration) || this._duration > maxNonLiveDuration; if (shouldShowZeroDurations) { this.elapsedTimeLabel.value = 0; this.durationTimeLabel.value = 0; this.remainingTimeLabel.value = 0; this.elapsedTimeLabel.numberOfDigits = 4; this.durationTimeLabel.numberOfDigits = 4; this.remainingTimeLabel.numberOfDigits = 4; } else { let numberOfDigitsForTimeLabels; if (this._duration < OneHour) numberOfDigitsForTimeLabels = 4; else if (this._duration < TenHours) numberOfDigitsForTimeLabels = 5; else numberOfDigitsForTimeLabels = 6; this.elapsedTimeLabel.value = this._currentTime; this.durationTimeLabel.value = this._duration; this.remainingTimeLabel.value = this._currentTime - this._duration; this.elapsedTimeLabel.numberOfDigits = numberOfDigitsForTimeLabels; this.durationTimeLabel.numberOfDigits = numberOfDigitsForTimeLabels; this.remainingTimeLabel.numberOfDigits = numberOfDigitsForTimeLabels; } } if (this._duration) this.scrubber.value = this._currentTime / this._duration; } updateScrubberLabel() { this.scrubber.inputAccessibleLabel = this.elapsedTimeLabel.value; } _toggleDurationRemainingLabel() { if (!this._shouldShowDurationTimeLabel) return; this.element.classList.toggle("duration"); this.element.classList.toggle("remaining"); } } TimeControl.Style = { Above: "above", Side: "side", Below: "below" }; const MinusSignWidthsForDigits = { 3: 6, 4: 5, 5: 6, 6: 5 }; const WidthsForDigits = { 3: 27, 4: 35, 5: 46, 6: 54 }; class TimeLabel extends LayoutNode { constructor(type) { super(`
`); this._type = type; this.value = 0; this.numberOfDigits = 4; switch (this._type) { case TimeLabel.Type.Elapsed: this.element.setAttribute("id", "time-label-elapsed"); break; case TimeLabel.Type.Remaining: this.element.setAttribute("id", "time-label-remaining"); break; case TimeLabel.Type.Duration: this.element.setAttribute("id", "time-label-duration"); break; } } get value() { return this._value; } set value(value) { if (value === this._value || (isNaN(value) && isNaN(this._value))) return; this._value = value; this.markDirtyProperty("value"); } get numberOfDigits() { return this._numberOfDigits; } set numberOfDigits(numberOfDigits) { if (this._numberOfDigits === numberOfDigits) return; if (numberOfDigits < 4 || numberOfDigits > 6) throw new RangeError("TimeLabel numerOfDigits must be between 4 and 6."); this._numberOfDigits = numberOfDigits; let formatterOptions = { style: "digital", fractionalDigits: 0, hoursDisplay: "auto", minutesDisplay: "always", secondsDisplay: "always", }; if (numberOfDigits >= 5) formatterOptions.hoursDisplay = "always"; if (numberOfDigits == 6) formatterOptions.hours = "2-digit"; this._formatter = new Intl.DurationFormat(undefined, formatterOptions); this.markDirtyProperty("value"); } setValueWithNumberOfDigits(value, numberOfDigits) { this.value = value; this.numberOfDigits = numberOfDigits; } commitProperty(propertyName) { if (propertyName === "value") { this.element.textContent = this._formattedTime(); const timeAsString = utils.formattedStringForDuration(this.value); switch (this._type) { case TimeLabel.Type.Elapsed: this.element.setAttribute( "aria-label", UIString("Elapsed: %s", timeAsString), ); break; case TimeLabel.Type.Remaining: this.element.setAttribute( "aria-label", UIString("Remaining: %s", timeAsString), ); break; case TimeLabel.Type.Duration: this.element.setAttribute( "aria-label", UIString("Duration: %s", timeAsString), ); break; } if (this.parent instanceof TimeControl) this.parent.updateScrubberLabel(); } else super.commitProperty(propertyName); } _formattedTime() { if (isNaN(this._value)) return "--:--"; return this._formatter.format(formatTimeByUnit(this._value)); } } function doubleDigits(x) { if (x < 10) return `0${x}`; return `${x}`; } TimeLabel.Type = { Elapsed: 0, Remaining: 1, Duration: 2 }; class SliderBase extends LayoutNode { constructor(layoutDelegate, cssClassName = "") { super(`
`); console.assert( this.constructor !== SliderBase && this instanceof SliderBase, this, ); this._layoutDelegate = layoutDelegate; this.appearanceContainer = new LayoutNode(`
`); this._input = new LayoutNode( ``, ); this._input.element.addEventListener("pointerdown", this); this._input.element.addEventListener("input", this); this._input.element.addEventListener("change", this); this.value = 0; this.enabled = true; this.isActive = false; this._disabled = false; this._secondaryValue = 0; this._allowsRelativeScrubbing = false; this._startValue = NaN; this._startPosition = NaN; this.children = [this.appearanceContainer, this._input]; } set inputAccessibleLabel(timeValue) { this._input.element.setAttribute( "aria-valuetext", utils.formattedStringForDuration(timeValue), ); } get disabled() { return this._disabled; } set disabled(flag) { if (this._disabled === flag) return; this._disabled = flag; this.markDirtyProperty("disabled"); } get value() { if (this._value !== undefined) return this._value; return parseFloat(this._input.element.value); } set value(value) { if (this._animatedValue) delete this._animatedValue; this._setValueInternal(value); } setValueAnimated(value) { if (this.isActive || this._value == value) return; this._valueAnimation = { start: this.value, end: value, startTime: window.performance.now(), }; const duration = 150; const animate = (currentTime) => { if (!this._valueAnimation) return; const elapsedTime = currentTime - this._valueAnimation.startTime; const progress = Math.min(elapsedTime / duration, 1); const animatedValue = this._valueAnimation.start + (this._valueAnimation.end - this._valueAnimation.start) * progress; this._setValueInternal(animatedValue); if (progress == 1) delete this._valueAnimation; else requestAnimationFrame(animate); }; requestAnimationFrame(animate); } _setValueInternal(value) { if (this.isActive || this._value == value) return; this._value = value; this.markDirtyProperty("value"); this.needsLayout = true; } get secondaryValue() { return this._secondaryValue; } set secondaryValue(secondaryValue) { if (this._secondaryValue === secondaryValue) return; this._secondaryValue = secondaryValue; this.needsLayout = true; } get allowsRelativeScrubbing() { return this._allowsRelativeScrubbing; } set allowsRelativeScrubbing(allowsRelativeScrubbing) { this._allowsRelativeScrubbing = !!allowsRelativeScrubbing; this.element.classList.toggle( "allows-relative-scrubbing", this._allowsRelativeScrubbing, ); } handleEvent(event) { switch (event.type) { case "pointerdown": this._handlePointerdownEvent(event); return; case "pointermove": this._handlePointermoveEvent(event); return; case "pointerup": this._handlePointerupEvent(event); return; case "change": case "input": this._valueDidChange(); return; } } commitProperty(propertyName) { switch (propertyName) { case "value": this._input.element.value = this._value; delete this._value; break; case "disabled": this.element.classList.toggle("disabled", this._disabled); break; default: super.commitProperty(propertyName); break; } } _handlePointerdownEvent(event) { delete this._valueAnimation; this._pointerupTarget = this._interactionEndTarget(); this._pointerupTarget.addEventListener("pointerup", this, { capture: true, }); if (this._allowsRelativeScrubbing) this._pointerupTarget.addEventListener("pointermove", this, { capture: true, }); delete this._value; if (this._allowsRelativeScrubbing) { this._startValue = parseFloat(this._input.element.value); this._startPosition = this._playbackProgress(event.pageX); } if ( this.uiDelegate && typeof this.uiDelegate.controlValueWillStartChanging === "function" ) this.uiDelegate.controlValueWillStartChanging(this); this.isActive = true; this.appearanceContainer.element.classList.add("changing"); this.needsLayout = true; } _interactionEndTarget() { const mediaControls = this.parentOfType(MediaControls); if (mediaControls?.layoutTraits.supportsTouches()) return mediaControls.element; return !mediaControls || !mediaControls.layoutTraits.isFullscreen ? window : mediaControls.element; } _valueDidChange() { if ( this.uiDelegate && typeof this.uiDelegate.controlValueDidChange === "function" ) this.uiDelegate.controlValueDidChange(this); this.needsLayout = true; } _handlePointermoveEvent(event) { if ( !this._allowsRelativeScrubbing || isNaN(this._startValue) || isNaN(this._startPosition) ) return; let value = this._startValue + this._playbackProgress(event.pageX) - this._startPosition; this._input.element.value = Math.min(Math.max(0, value), 1); this._valueDidChange(); } _handlePointerupEvent(event) { this._pointerupTarget.removeEventListener("pointerup", this, { capture: true, }); this._pointerupTarget.removeEventListener("pointermove", this, { capture: true, }); delete this._pointerupTarget; this._startValue = NaN; this._startPosition = NaN; this.isActive = false; this.appearanceContainer.element.classList.remove("changing"); if ( this.uiDelegate && typeof this.uiDelegate.controlValueDidStopChanging === "function" ) this.uiDelegate.controlValueDidStopChanging(this); this.needsLayout = true; } _playbackProgress(pageX) { let x = window.webkitConvertPointFromPageToNode( this.element, new WebKitPoint(pageX, 0), ).x; if (this._layoutDelegate?.scaleFactor) x *= this._layoutDelegate.scaleFactor; return x / this.element.clientWidth; } } class Slider extends SliderBase { constructor( layoutDelegate, cssClassName = "", knobStyle = Slider.KnobStyle.Circle, ) { super(layoutDelegate, `default ${cssClassName}`); this._primaryFill = new LayoutNode(`
`); this._trackFill = new LayoutNode(`
`); this._secondaryFill = new LayoutNode(`
`); this._trackFill.children = [this._secondaryFill]; let fillContainer = new LayoutNode(`
`); this._knob = new LayoutNode(`
`); fillContainer.children = [this._primaryFill, this._knob, this._trackFill]; this.appearanceContainer.children = [fillContainer]; this.height = 16; this._knobStyle = knobStyle; } get knobStyle() { return this._knobStyle; } set knobStyle(knobStyle) { if (this._knobStyle === knobStyle) return; this._knob.element.classList.remove(this._knobStyle); this._knobStyle = knobStyle; this._knob.element.classList.add(this._knobStyle); this.needsLayout = true; } commit() { super.commit(); this._primaryFill.element.style.flexGrow = 100 * this.value; this._trackFill.element.style.flexGrow = 100 * (1 - this.value); } } Slider.KnobStyle = { Circle: "circle", Bar: "bar", None: "none", Pill: "pill" }; class Button extends LayoutItem { constructor({ layoutDelegate = null, cssClassName = "", iconName = "", } = {}) { super({ element: "