feat(packages): airplay button (#1531)

This commit is contained in:
Santiago Puppo
2026-05-27 10:22:31 -07:00
committed by GitHub
parent 760870fdf2
commit 338020e1d5
50 changed files with 869 additions and 2 deletions
@@ -90,6 +90,11 @@ function getTemplateHTML() {
${renderIcon('cast-exit', { class: cn(icon, iconState.cast.exit) })}
</media-cast-button>
<media-tooltip id="cast-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
<media-airplay-button commandfor="airplay-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.airplay.button)}">
${renderIcon('airplay-enter', { class: cn(icon, iconState.airplay.enter) })}
${renderIcon('airplay-exit', { class: cn(icon, iconState.airplay.exit) })}
</media-airplay-button>
<media-tooltip id="airplay-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
<media-pip-button commandfor="pip-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.pip.button)}">
${renderIcon('pip-enter', { class: cn(icon, iconState.pip.off) })}
${renderIcon('pip-exit', { class: cn(icon, iconState.pip.on) })}
@@ -77,6 +77,12 @@ function getTemplateHTML() {
</media-cast-button>
<media-tooltip id="cast-tooltip" side="top" class="media-tooltip"></media-tooltip>
<media-airplay-button commandfor="airplay-tooltip" class="media-button media-button--subtle media-button--icon media-button--airplay">
${renderIcon('airplay-enter', { class: 'media-icon media-icon--airplay-enter' })}
${renderIcon('airplay-exit', { class: 'media-icon media-icon--airplay-exit' })}
</media-airplay-button>
<media-tooltip id="airplay-tooltip" side="top" class="media-tooltip"></media-tooltip>
<media-pip-button commandfor="pip-tooltip" class="media-button media-button--subtle media-button--icon media-button--pip">
${renderIcon('pip-enter', { class: 'media-icon media-icon--pip-enter' })}
${renderIcon('pip-exit', { class: 'media-icon media-icon--pip-exit' })}
@@ -2,6 +2,7 @@
// elements used by the minimal skin without creating a skin element. Use
// this entry when building an ejected (light DOM) player layout for live
// HLS / DASH streams.
import { AirplayButtonElement } from '@/ui/airplay-button/airplay-button-element';
import { MediaContainerElement } from '../../media/container-element';
import { BufferingIndicatorElement } from '../../ui/buffering-indicator/buffering-indicator-element';
import { CaptionsButtonElement } from '../../ui/captions-button/captions-button-element';
@@ -44,6 +45,7 @@ defineVolumeSlider();
defineTime();
// Standalone elements.
safeDefine(AirplayButtonElement);
safeDefine(BufferingIndicatorElement);
safeDefine(CaptionsButtonElement);
safeDefine(CastButtonElement);
@@ -92,6 +92,11 @@ function getTemplateHTML() {
${renderIcon('cast-exit', { class: cn(icon, iconState.cast.exit) })}
</media-cast-button>
<media-tooltip id="cast-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
<media-airplay-button commandfor="airplay-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.airplay.button)}">
${renderIcon('airplay-enter', { class: cn(icon, iconState.airplay.enter) })}
${renderIcon('airplay-exit', { class: cn(icon, iconState.airplay.exit) })}
</media-airplay-button>
<media-tooltip id="airplay-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
<media-pip-button commandfor="pip-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.pip.button)}">
${renderIcon('pip-enter', { class: cn(icon, iconState.pip.off) })}
${renderIcon('pip-exit', { class: cn(icon, iconState.pip.on) })}
@@ -79,6 +79,12 @@ function getTemplateHTML() {
</media-cast-button>
<media-tooltip id="cast-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
<media-airplay-button commandfor="airplay-tooltip" class="media-button media-button--subtle media-button--icon media-button--airplay">
${renderIcon('airplay-enter', { class: 'media-icon media-icon--airplay-enter' })}
${renderIcon('airplay-exit', { class: 'media-icon media-icon--airplay-exit' })}
</media-airplay-button>
<media-tooltip id="airplay-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
<media-pip-button commandfor="pip-tooltip" class="media-button media-button--subtle media-button--icon media-button--pip">
${renderIcon('pip-enter', { class: 'media-icon media-icon--pip-enter' })}
${renderIcon('pip-exit', { class: 'media-icon media-icon--pip-exit' })}
@@ -1,6 +1,7 @@
// Registers the live video player, container, and all video UI custom
// elements without creating a skin element. Use this entry when building an
// ejected (light DOM) player layout for live HLS / DASH streams.
import { AirplayButtonElement } from '@/ui/airplay-button/airplay-button-element';
import { MediaContainerElement } from '../../media/container-element';
import { BufferingIndicatorElement } from '../../ui/buffering-indicator/buffering-indicator-element';
import { CaptionsButtonElement } from '../../ui/captions-button/captions-button-element';
@@ -43,6 +44,7 @@ defineVolumeSlider();
defineTime();
// Standalone elements.
safeDefine(AirplayButtonElement);
safeDefine(BufferingIndicatorElement);
safeDefine(CaptionsButtonElement);
safeDefine(CastButtonElement);
@@ -0,0 +1,10 @@
import { AirplayButtonElement } from '../../ui/airplay-button/airplay-button-element';
import { safeDefine } from '../safe-define';
safeDefine(AirplayButtonElement);
declare global {
interface HTMLElementTagNameMap {
[AirplayButtonElement.tagName]: AirplayButtonElement;
}
}
@@ -150,6 +150,11 @@ function getTemplateHTML() {
${renderIcon('cast-exit', { class: cn(icon, iconState.cast.exit) })}
</media-cast-button>
<media-tooltip id="cast-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
<media-airplay-button commandfor="airplay-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.airplay.button)}">
${renderIcon('airplay-enter', { class: cn(icon, iconState.airplay.enter) })}
${renderIcon('airplay-exit', { class: cn(icon, iconState.airplay.exit) })}
</media-airplay-button>
<media-tooltip id="airplay-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
<media-pip-button commandfor="pip-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.pip.button)}">
${renderIcon('pip-enter', { class: cn(icon, iconState.pip.off) })}
${renderIcon('pip-exit', { class: cn(icon, iconState.pip.on) })}
@@ -128,6 +128,12 @@ function getTemplateHTML() {
${renderIcon('cast-exit', { class: 'media-icon media-icon--cast-exit' })}
</media-cast-button>
<media-tooltip id="cast-tooltip" side="top" class="media-tooltip"></media-tooltip>
<media-airplay-button commandfor="airplay-tooltip" class="media-button media-button--subtle media-button--icon media-button--airplay">
${renderIcon('airplay-enter', { class: 'media-icon media-icon--airplay-enter' })}
${renderIcon('airplay-exit', { class: 'media-icon media-icon--airplay-exit' })}
</media-airplay-button>
<media-tooltip id="airplay-tooltip" side="top" class="media-tooltip"></media-tooltip>
<media-pip-button commandfor="pip-tooltip" class="media-button media-button--subtle media-button--icon media-button--pip">
${renderIcon('pip-enter', { class: 'media-icon media-icon--pip-enter' })}
@@ -1,7 +1,9 @@
// Registers the video player, container, and all video UI custom elements
// used by the minimal skin without creating a skin element. Use this entry
// when building an ejected (light DOM) player layout.
import { MediaContainerElement } from '../../media/container-element';
import { AirplayButtonElement } from '../../ui/airplay-button/airplay-button-element';
import { BufferingIndicatorElement } from '../../ui/buffering-indicator/buffering-indicator-element';
import { CaptionsButtonElement } from '../../ui/captions-button/captions-button-element';
import { CastButtonElement } from '../../ui/cast-button/cast-button-element';
@@ -48,6 +50,7 @@ defineTime();
defineMenu();
// Standalone elements.
safeDefine(AirplayButtonElement);
safeDefine(BufferingIndicatorElement);
safeDefine(CaptionsButtonElement);
safeDefine(CastButtonElement);
@@ -145,6 +145,11 @@ function getTemplateHTML() {
${renderIcon('cast-exit', { class: cn(icon, iconState.cast.exit) })}
</media-cast-button>
<media-tooltip id="cast-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
<media-airplay-button commandfor="airplay-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.airplay.button)}">
${renderIcon('airplay-enter', { class: cn(icon, iconState.airplay.enter) })}
${renderIcon('airplay-exit', { class: cn(icon, iconState.airplay.exit) })}
</media-airplay-button>
<media-tooltip id="airplay-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
<media-pip-button commandfor="pip-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.pip.button)}">
${renderIcon('pip-enter', { class: cn(icon, iconState.pip.off) })}
${renderIcon('pip-exit', { class: cn(icon, iconState.pip.on) })}
+6
View File
@@ -124,6 +124,12 @@ function getTemplateHTML() {
${renderIcon('cast-exit', { class: 'media-icon media-icon--cast-exit' })}
</media-cast-button>
<media-tooltip id="cast-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
<media-airplay-button commandfor="airplay-tooltip" class="media-button media-button--subtle media-button--icon media-button--airplay">
${renderIcon('airplay-enter', { class: 'media-icon media-icon--airplay-enter' })}
${renderIcon('airplay-exit', { class: 'media-icon media-icon--airplay-exit' })}
</media-airplay-button>
<media-tooltip id="airplay-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
<media-pip-button commandfor="pip-tooltip" class="media-button media-button--subtle media-button--icon media-button--pip">
${renderIcon('pip-enter', { class: 'media-icon media-icon--pip-enter' })}
+2
View File
@@ -2,6 +2,7 @@
// without creating a skin element. Use this entry when building an ejected
// (light DOM) player layout.
import { MediaContainerElement } from '../../media/container-element';
import { AirplayButtonElement } from '../../ui/airplay-button/airplay-button-element';
import { BufferingIndicatorElement } from '../../ui/buffering-indicator/buffering-indicator-element';
import { CaptionsButtonElement } from '../../ui/captions-button/captions-button-element';
import { CastButtonElement } from '../../ui/cast-button/cast-button-element';
@@ -50,6 +51,7 @@ defineTime();
defineMenu();
// Standalone elements.
safeDefine(AirplayButtonElement);
safeDefine(BufferingIndicatorElement);
safeDefine(CaptionsButtonElement);
safeDefine(CastButtonElement);
+1
View File
@@ -25,6 +25,7 @@ export * from './store/media-attach-mixin';
export * from './store/provider-mixin';
export * from './store/types';
// UI Components
export { AirplayButtonElement } from './ui/airplay-button/airplay-button-element';
export { AlertDialogCloseElement } from './ui/alert-dialog/alert-dialog-close-element';
export { AlertDialogDescriptionElement } from './ui/alert-dialog/alert-dialog-description-element';
export { AlertDialogElement } from './ui/alert-dialog/alert-dialog-element';
@@ -0,0 +1,18 @@
import { AirplayButtonCore, AirplayButtonDataAttrs, type MediaRemotePlaybackState } from '@videojs/core';
import { selectRemotePlayback } from '@videojs/core/dom';
import { playerContext } from '../../player/context';
import { PlayerController } from '../../player/player-controller';
import { MediaButtonElement } from '../media-button-element';
export class AirplayButtonElement extends MediaButtonElement<AirplayButtonCore> {
static readonly tagName = 'media-airplay-button';
protected readonly core = new AirplayButtonCore();
protected readonly stateAttrMap = AirplayButtonDataAttrs;
protected readonly mediaState = new PlayerController(this, playerContext, selectRemotePlayback);
protected activate(state: MediaRemotePlaybackState): void {
this.core.toggle(state);
}
}