Skip to content

Commit

Permalink
fix: add Airplay support to hls-video (#40)
Browse files Browse the repository at this point in the history
* fix: add Airplay support to hls-video
related video-dev/hls.js#6482

* chore: use better supported video format (no HDR)

* fix: use autoStartLoad: false, manual start/stop

* site: add hls-video example url params
related vercel/next.js#55523

* site: add outputFileTracingIncludes for hls-video
  • Loading branch information
luwes authored Sep 25, 2024
1 parent fbbcd1b commit e4ce158
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 26 deletions.
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

0 comments on commit e4ce158

Please sign in to comment.