diff --git a/examples/nextjs/app/hls-video/page.tsx b/examples/nextjs/app/hls-video/page.tsx
index 4fe7f10..8c1ba13 100644
--- a/examples/nextjs/app/hls-video/page.tsx
+++ b/examples/nextjs/app/hls-video/page.tsx
@@ -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 (
<>
diff --git a/examples/nextjs/app/layout.tsx b/examples/nextjs/app/layout.tsx
index 73b9db1..12a0941 100644
--- a/examples/nextjs/app/layout.tsx
+++ b/examples/nextjs/app/layout.tsx
@@ -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';
@@ -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,
diff --git a/examples/nextjs/next.config.mjs b/examples/nextjs/next.config.mjs
index 4678774..c4f4def 100644
--- a/examples/nextjs/next.config.mjs
+++ b/examples/nextjs/next.config.mjs
@@ -1,4 +1,10 @@
/** @type {import('next').NextConfig} */
-const nextConfig = {};
+const nextConfig = {
+ experimental: {
+ outputFileTracingIncludes: {
+ '/hls-video': ['./app/theme-toggle.js'],
+ }
+ }
+};
export default nextConfig;
diff --git a/packages/hls-video-element/hls-video-element.js b/packages/hls-video-element/hls-video-element.js
index f27be20..4f2fc13 100644
--- a/packages/hls-video-element/hls-video-element.js
+++ b/packages/hls-video-element/hls-video-element.js
@@ -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 };
@@ -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);
@@ -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();
@@ -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,
});
@@ -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
@@ -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;
}
@@ -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();
+ }
+ };
};
};
@@ -226,8 +260,4 @@ if (globalThis.customElements && !globalThis.customElements.get('hls-video')) {
export default HlsVideoElement;
-export {
- Hls,
- HlsVideoMixin,
- HlsVideoElement,
-};
+export { Hls, HlsVideoMixin, HlsVideoElement };
diff --git a/packages/hls-video-element/index.html b/packages/hls-video-element/index.html
index 481bf2c..9d6bd3a 100644
--- a/packages/hls-video-element/index.html
+++ b/packages/hls-video-element/index.html
@@ -196,6 +196,14 @@
Live
+
+
With Media
playsinline
slot="media"
muted
+ preload="none"
>
-
-
-
+