From 560d4ef8d94c2179d46948b60da6c310809ca6c5 Mon Sep 17 00:00:00 2001 From: Alan Allard Date: Fri, 13 Mar 2026 14:39:36 +0100 Subject: [PATCH] feat: add SGAI checkbox for HLS interstitial support on Safari MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Safari, the web player defaults to native HLS playback (BaseTech), bypassing hls.js entirely. Since HLS interstitials are not supported natively in any browser, the SGAI interstitial feature (PR #126) only worked on Chrome/Firefox where hls.js is always used. This adds an `enableSgai` option that forces hls.js with `preferManagedMediaSource: true` even on Safari, enabling HLS interstitial playback on all browsers. Changes: - Add `enableSgai` option to IWebPlayerOptions (core) - When enabled, bypass Safari native fallback and use HlsJsTech - Add `preferManagedMediaSource: true` to HlsJsTech default config - Add SGAI checkbox to demo UI (auto-checked for SGAI streams) - Recreate player when SGAI toggle changes (tech switch requires it) - Fix tracking bug: `return` → `continue` in fireTrackingUrls Co-Authored-By: Claude Opus 4.6 --- packages/core/src/WebPlayer.ts | 10 ++++++++- packages/core/src/tech/HlsJsTech.ts | 3 ++- packages/demo/src/index.js | 35 +++++++++++++++++++++++------ packages/demo/src/style.css | 12 +++++++++- packages/demo/src/template.html | 4 ++++ 5 files changed, 54 insertions(+), 10 deletions(-) diff --git a/packages/core/src/WebPlayer.ts b/packages/core/src/WebPlayer.ts index 152ec55..435b3d1 100644 --- a/packages/core/src/WebPlayer.ts +++ b/packages/core/src/WebPlayer.ts @@ -12,6 +12,7 @@ export interface IWebPlayerOptions { disablePlayerSizeLevelCap?: boolean; iceServers?: RTCIceServer[]; enableCloudflareWhepBeta?: boolean; + enableSgai?: boolean; } export enum ReadyState { @@ -26,7 +27,7 @@ export default class WebPlayer extends EventEmitter { private _readyState: ReadyState = ReadyState.UNREADY; private tech: BaseTech; - private opts: IWebPlayerOptions; + public opts: IWebPlayerOptions; public video: HTMLVideoElement; public currentSrc?: string; @@ -83,6 +84,13 @@ export default class WebPlayer extends EventEmitter { private async loadTech(): Promise { if (this.manifestType === ManifestType.HLS) { + if (this.opts.enableSgai) { + // SGAI mode: always use hls.js for HLS interstitial support (including Safari) + const HlsJsTech = (await import('./tech/HlsJsTech')).default; + if (HlsJsTech.isSupported()) { + return HlsJsTech; + } + } if (canPlayManifestType(ManifestType.HLS) && isSafari()) { return BaseTech; } diff --git a/packages/core/src/tech/HlsJsTech.ts b/packages/core/src/tech/HlsJsTech.ts index 35f9053..c540fa6 100644 --- a/packages/core/src/tech/HlsJsTech.ts +++ b/packages/core/src/tech/HlsJsTech.ts @@ -7,6 +7,7 @@ const DEFAULT_CONFIG = { capLevelOnFPSDrop: true, capLevelToPlayerSize: true, enableInterstitialPlayback: true, + preferManagedMediaSource: true, }; const LIVE_EDGE = 5; // seconds from liveEdge @@ -595,7 +596,7 @@ export default class HlsJsTech extends BaseTech { for (const url of urls) { const trackingKey = `${url}-${eventType}`; if (this.interstitialTrackingFired.has(trackingKey)) { - return; // if already fired, duplicate + continue; // skip this URL, check remaining } this.interstitialTrackingFired.add(trackingKey); diff --git a/packages/demo/src/index.js b/packages/demo/src/index.js index f98f897..dfd1a3f 100644 --- a/packages/demo/src/index.js +++ b/packages/demo/src/index.js @@ -83,6 +83,7 @@ async function main() { const sgaiControls = document.querySelector('#sgai-controls'); const sgaiAdBreakButton = document.querySelector('#sgai-adbreak-button'); const sgaiDurationInput = document.querySelector('#sgai-duration'); + const sgaiCheckbox = document.querySelector('#sgai-checkbox'); renderExampleButtons(); if (!manifestInput.value) { @@ -97,7 +98,7 @@ async function main() { const searchParams = new URL(window.location.href).searchParams; const root = document.querySelector('#player'); - const video = document.createElement('video'); + let video = document.createElement('video'); root.appendChild(video); const snackbar = document.querySelector('#snackbar'); @@ -127,12 +128,13 @@ async function main() { } // Comment out this if you want to demo the player package - const player = new WebPlayer({ - video: video, + let player = new WebPlayer({ + video: video, iceServers: iceServers, enableCloudflareWhepBeta: process.env.CLOUDFLARE_BETA === "true", + enableSgai: sgaiCheckbox.checked, }); - renderEyevinnSkin({ + let skin = renderEyevinnSkin({ root, player, }); @@ -147,6 +149,7 @@ async function main() { function updateSgaiControls(manifestUrl) { if (manifestUrl === SGAI_STREAM_URL) { sgaiControls.style.display = 'block'; + sgaiCheckbox.checked = true; } else { sgaiControls.style.display = 'none'; } @@ -175,15 +178,33 @@ async function main() { } } - async function load() { + async function load() { try { if (epasUrlInput.value) { playerAnalytics = new PlayerAnalyticsConnector( epasUrlInput.value ); } - - player.reset(); + + // Recreate player if SGAI option changed + const wantSgai = sgaiCheckbox.checked; + if (wantSgai !== player.opts?.enableSgai) { + player.destroy(); + // Remove old video element and create fresh one + const newVideo = document.createElement('video'); + video.parentNode.replaceChild(newVideo, video); + video = newVideo; + player = new WebPlayer({ + video: video, + iceServers: iceServers, + enableCloudflareWhepBeta: process.env.CLOUDFLARE_BETA === "true", + enableSgai: wantSgai, + }); + player.on('*', () => {}); // re-init event forwarding + skin = renderEyevinnSkin({ root, player }); + } else { + player.reset(); + } try { playerAnalytics && await playerAnalytics.init({ diff --git a/packages/demo/src/style.css b/packages/demo/src/style.css index ddc2283..d9f7b87 100644 --- a/packages/demo/src/style.css +++ b/packages/demo/src/style.css @@ -36,15 +36,25 @@ video { .input-section .input-buttons .input-button, .input-section .input-buttons .autoplay-wrapper { font-size: 1em; - width: 10.5em; height: 2.15em; margin: 0.5em 0.3em; display: flex; align-items: center; justify-content: center; +} + +.input-section .input-buttons .input-button { + width: 10.5em; min-width: 10.3em; } +.input-section .input-buttons .autoplay-wrapper { + width: auto; + min-width: auto; + white-space: nowrap; + padding: 0 0.3em; +} + @media (max-width: 790px) { .input-section .input-buttons .input-button, .input-section .input-buttons .autoplay-wrapper { width: 47%; diff --git a/packages/demo/src/template.html b/packages/demo/src/template.html index 14388d4..0cf5243 100644 --- a/packages/demo/src/template.html +++ b/packages/demo/src/template.html @@ -44,6 +44,10 @@

WebPlayer@<%= htmlWebpackPlugin.options.title %>

+
+ + +