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 Chrome

+ + With Media playsinline slot="media" muted + preload="none" > - - - +