Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add Airplay support to hls-video #40

Merged
merged 5 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions examples/nextjs/app/hls-video/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,36 @@ export const metadata: Metadata = {
title: 'HLS Video - Media Elements',
};

export default function Page() {
type PageProps = {
searchParams: {
autoplay: string;
muted: string;
preload: string;
};
};

export default function Page(props: PageProps) {
return (
<>
<section>
<Player
as={HlsVideo}
className="video"
src="https://stream.mux.com/jtWZbHQ013SLyISc9LbIGn8f4c3lWan00qOkoPMZEXmcU.m3u8"
poster="https://image.mux.com/jtWZbHQ013SLyISc9LbIGn8f4c3lWan00qOkoPMZEXmcU/thumbnail.webp?time=0"
src="https://stream.mux.com/Sc89iWAyNkhJ3P1rQ02nrEdCFTnfT01CZ2KmaEcxXfB008.m3u8"
poster="https://image.mux.com/Sc89iWAyNkhJ3P1rQ02nrEdCFTnfT01CZ2KmaEcxXfB008/thumbnail.webp?time=13"
controls
crossOrigin=""
playsInline
autoPlay={props.searchParams?.autoplay}
muted={props.searchParams?.muted}
preload={props.searchParams?.preload}
suppressHydrationWarning
>
<track
label="thumbnails"
default
kind="metadata"
src="https://image.mux.com/jtWZbHQ013SLyISc9LbIGn8f4c3lWan00qOkoPMZEXmcU/storyboard.vtt"
src="https://image.mux.com/Sc89iWAyNkhJ3P1rQ02nrEdCFTnfT01CZ2KmaEcxXfB008/storyboard.vtt"
/>
</Player>
</section>
Expand Down
17 changes: 14 additions & 3 deletions examples/nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { dirname } from 'node:path';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as fs from 'node:fs/promises';
import type React from 'react';
Expand All @@ -17,8 +17,19 @@ export const metadata: Metadata = {
description: 'A collection of custom media elements for the web.',
};

const fileDir = dirname(fileURLToPath(import.meta.url));
const themeScript = await fs.readFile(`${fileDir}/theme-toggle.js`, 'utf-8');
// https://francoisbest.com/posts/2023/reading-files-on-vercel-during-nextjs-isr
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const nextJsRootDir = path.resolve(__dirname, '../')

function resolve(...paths: string[]) {
const dirname = path.dirname(fileURLToPath(import.meta.url))
const absPath = path.resolve(dirname, ...paths)
// Required for ISR serverless functions to pick up the file path
// as a dependency to bundle.
return path.resolve(process.cwd(), absPath.replace(nextJsRootDir, '.'))
}

const themeScript = await fs.readFile(resolve('theme-toggle.js'), 'utf-8');

export default async function RootLayout({
children,
Expand Down
8 changes: 7 additions & 1 deletion examples/nextjs/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
experimental: {
outputFileTracingIncludes: {
'/hls-video': ['./app/theme-toggle.js'],
}
}
};

export default nextConfig;
60 changes: 45 additions & 15 deletions packages/hls-video-element/hls-video-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { MediaTracksMixin } from 'media-tracks';
import Hls from 'hls.js/dist/hls.mjs';

const HlsVideoMixin = (superclass) => {

return class HlsVideo extends superclass {
static shadowRootOptions = { ...superclass.shadowRootOptions };

Expand All @@ -12,6 +11,8 @@ const HlsVideoMixin = (superclass) => {
return superclass.getTemplateHTML(rest);
};

#airplaySourceEl = null;

attributeChangedCallback(attrName, oldValue, newValue) {
if (attrName !== 'src') {
super.attributeChangedCallback(attrName, oldValue, newValue);
Expand All @@ -23,6 +24,13 @@ const HlsVideoMixin = (superclass) => {
}

#destroy() {
this.#airplaySourceEl?.remove();

this.nativeEl?.removeEventListener(
'webkitcurrentplaybacktargetiswirelesschanged',
this.#toggleHlsLoad
);

if (this.api) {
this.api.detachMedia();
this.api.destroy();
Expand All @@ -39,20 +47,24 @@ const HlsVideoMixin = (superclass) => {

// Prefer using hls.js over native if it is supported.
if (Hls.isSupported()) {

this.api = new Hls({
// Mimic the media element with an Infinity duration for live streams.
liveDurationInfinity: true
liveDurationInfinity: true,
// Disable auto quality level/fragment loading.
autoStartLoad: false,
});

// Wait 1 tick to allow other attributes to be set.
await Promise.resolve();

this.api.loadSource(this.src);
this.api.attachMedia(this.nativeEl);

// Set up preload
switch (this.nativeEl.preload) {
case 'none': {
// when preload is none, load the source on first play
const loadSourceOnPlay = () => this.api.loadSource(this.src);
const loadSourceOnPlay = () => this.api.startLoad();
this.nativeEl.addEventListener('play', loadSourceOnPlay, {
once: true,
});
Expand All @@ -78,15 +90,30 @@ const HlsVideoMixin = (superclass) => {
this.api.on(Hls.Events.DESTROYING, () => {
this.nativeEl.removeEventListener('play', increaseBufferOnPlay);
});
this.api.loadSource(this.src);
this.api.startLoad();
break;
}
default:
// load source immediately for any other preload value
this.api.loadSource(this.src);
this.api.startLoad();
}

this.api.attachMedia(this.nativeEl);
// Stop loading the HLS stream when AirPlay is active.
// https://github.com/video-dev/hls.js/issues/6482#issuecomment-2159399478
if (this.nativeEl.webkitCurrentPlaybackTargetIsWireless) {
this.api.stopLoad();
}

this.nativeEl.addEventListener(
'webkitcurrentplaybacktargetiswirelesschanged',
this.#toggleHlsLoad
);

this.#airplaySourceEl = document.createElement('source');
this.#airplaySourceEl.setAttribute('type', 'application/x-mpegURL');
this.#airplaySourceEl.setAttribute('src', this.src);
this.nativeEl.disableRemotePlayback = false;
this.nativeEl.append(this.#airplaySourceEl);

// Set up tracks & renditions

Expand Down Expand Up @@ -134,8 +161,8 @@ const HlsVideoMixin = (superclass) => {

this.audioTracks.addEventListener('change', () => {
// Cast to number, hls.js uses numeric id's.
const audioTrackId = +[...this.audioTracks].find(t => t.enabled)?.id;
const availableIds = this.api.audioTracks.map(t => t.id);
const audioTrackId = +[...this.audioTracks].find((t) => t.enabled)?.id;
const availableIds = this.api.audioTracks.map((t) => t.id);
if (audioTrackId != this.api.audioTrack && availableIds.includes(audioTrackId)) {
this.api.audioTrack = audioTrackId;
}
Expand Down Expand Up @@ -211,10 +238,17 @@ const HlsVideoMixin = (superclass) => {

// Use native HLS. e.g. iOS Safari.
if (this.nativeEl.canPlayType('application/vnd.apple.mpegurl')) {

this.nativeEl.src = this.src;
}
}

#toggleHlsLoad = () => {
if (this.nativeEl?.webkitCurrentPlaybackTargetIsWireless) {
this.api?.stopLoad();
} else {
this.api?.startLoad();
}
};
};
};

Expand All @@ -226,8 +260,4 @@ if (globalThis.customElements && !globalThis.customElements.get('hls-video')) {

export default HlsVideoElement;

export {
Hls,
HlsVideoMixin,
HlsVideoElement,
};
export { Hls, HlsVideoMixin, HlsVideoElement };
13 changes: 10 additions & 3 deletions packages/hls-video-element/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,14 @@ <h2>Live</h2>

<h2>With <a href="https://github.com/muxinc/media-chrome" target="_blank">Media Chrome</a></h2>

<style>
media-airplay-button[mediaairplayunavailable],
media-fullscreen-button[mediafullscreenunavailable],
media-pip-button[mediapipunavailable] {
display: none;
}
</style>

<media-controller>
<hls-video
src="https://stream.mux.com/O6LdRc0112FEJXH00bGsN9Q31yu5EIVHTgjTKRkKtEq1k.m3u8"
Expand All @@ -204,20 +212,19 @@ <h2>With <a href="https://github.com/muxinc/media-chrome" target="_blank">Media
playsinline
slot="media"
muted
preload="none"
>
<track default kind="metadata" label="thumbnails" src="https://image.mux.com/O6LdRc0112FEJXH00bGsN9Q31yu5EIVHTgjTKRkKtEq1k/storyboard.vtt">
</hls-video>
<media-loading-indicator slot="centered-chrome" no-auto-hide></media-loading-indicator>
<media-control-bar>
<media-play-button></media-play-button>
<media-seek-backward-button seek-offset="15"></media-seek-backward-button>
<media-seek-forward-button seek-offset="15"></media-seek-forward-button>
<media-mute-button></media-mute-button>
<media-volume-range></media-volume-range>
<media-time-range></media-time-range>
<media-time-display show-duration remaining></media-time-display>
<media-playback-rate-button></media-playback-rate-button>
<media-pip-button></media-pip-button>
<media-airplay-button></media-airplay-button>
<media-fullscreen-button></media-fullscreen-button>
</media-control-bar>
</media-controller>
Expand Down