diff --git a/packages/mux-player/src/template.ts b/packages/mux-player/src/template.ts index 1c07ae2a9..5f51eb09a 100644 --- a/packages/mux-player/src/template.ts +++ b/packages/mux-player/src/template.ts @@ -34,7 +34,8 @@ const isLiveOrDVR = (props: MuxTemplateProps) => isLive(props) || isDVR(props); const getHotKeys = (props: MuxTemplateProps) => { let hotKeys = props.hotKeys ? `${props.hotKeys}` : ''; - if (isLiveOrDVR(props)) { + // Applies to any live content, including "dvr". We may want to only apply for non-DVR live. + if (getStreamTypeFromAttr(props.streamType) === 'live') { hotKeys += ' noarrowleft noarrowright'; } return hotKeys; @@ -43,7 +44,7 @@ const getHotKeys = (props: MuxTemplateProps) => { export const content = (props: MuxTemplateProps) => html` ', () => { player.muted = true; assert(player.muted, 'is muted'); - try { - await player.play(); - } catch (error) { - console.warn(error); - } + await player.play(); assert(!player.paused, 'is playing after player.play()'); assert.equal(Math.round(player.duration), 134, `is 134s long`); @@ -878,7 +874,7 @@ describe(' seek to live behaviors', function () { assert.exists(seekToLiveEl); }); - (isSafari ? it.skip : it)('should seek to live when seek to live button pressed', async function () { + it('should seek to live when seek to live button pressed', async function () { this.timeout(15000); const playerEl = await fixture(` seek to live behaviors', function () { preload="auto" >`); - // NOTE: Need try catch due to bug in play+autoplay behavior (CJP) - try { - await playerEl.play(); - } catch (_e) {} + await playerEl.play(); await waitUntil(() => !playerEl.paused, 'play() failed'); await waitUntil(() => playerEl.inLiveWindow, 'playback did not start inLiveWindow', { timeout: 11000 }); playerEl.pause(); @@ -901,7 +894,7 @@ describe(' seek to live behaviors', function () { await waitUntil(() => playerEl.inLiveWindow, 'clicking seek to live did not seek to live window'); }); - (isSafari ? it.skip : it)('should seek to live when play button is pressed', async function () { + it('should seek to live when play button is pressed', async function () { this.timeout(15000); const playerEl = await fixture(` seek to live behaviors', function () { preload="auto" >`); - try { - await playerEl.play(); - } catch (_e) {} - + await playerEl.play(); await waitUntil(() => !playerEl.paused, 'play() failed'); await waitUntil(() => playerEl.inLiveWindow, 'playback did not start inLiveWindow', { timeout: 11000 }); playerEl.pause(); diff --git a/packages/mux-player/test/template.test.js b/packages/mux-player/test/template.test.js index 36de86195..1a75e1663 100644 --- a/packages/mux-player/test/template.test.js +++ b/packages/mux-player/test/template.test.js @@ -14,7 +14,7 @@ describe(' template render', () => { assert.equal( normalizeAttributes(minify(div.innerHTML)), normalizeAttributes( - `

` + `

` ) ); }); @@ -35,7 +35,7 @@ describe(' template render', () => { assert.equal( normalizeAttributes(minify(div.innerHTML)), normalizeAttributes( - `

Errr

` + `

Errr

` ) ); }); @@ -52,7 +52,7 @@ describe(' template render', () => { assert.equal( normalizeAttributes(minify(div.innerHTML)), normalizeAttributes( - `

` + `

` ) ); }); @@ -73,7 +73,7 @@ describe(' template render', () => { assert.equal( normalizeAttributes(minify(div.innerHTML)), normalizeAttributes( - `

Errr

` + `

Errr

` ) ); }); diff --git a/packages/playback-core/src/index.ts b/packages/playback-core/src/index.ts index 54184ccd4..541978e57 100644 --- a/packages/playback-core/src/index.ts +++ b/packages/playback-core/src/index.ts @@ -117,6 +117,8 @@ export const getLiveEdgeStart = (mediaEl: HTMLMediaElement) => { const liveEdgeStartOffset = muxMediaState.get(mediaEl)?.liveEdgeStartOffset; if (typeof liveEdgeStartOffset !== 'number') return Number.NaN; const seekable = getSeekable(mediaEl); + // We aren't guaranteed that seekable is ready before invoking this, so handle that case. + if (!seekable.length) return Number.NaN; return seekable.end(seekable.length - 1) - liveEdgeStartOffset; }; @@ -371,7 +373,8 @@ export const loadMedia = ( return fetch(mediaPlaylistUrl).then((resp) => resp.text()); }) .then((mediaPlaylistStr) => { - const typeLine = mediaPlaylistStr.split('\n').find((line) => line.startsWith('#EXT-X-PLAYLIST-TYPE')) ?? ''; + const playlistLines = mediaPlaylistStr.split('\n'); + const typeLine = playlistLines.find((line) => line.startsWith('#EXT-X-PLAYLIST-TYPE')) ?? ''; const playlistType = typeLine.split(':')[1]?.trim() as HlsPlaylistTypes; @@ -386,6 +389,31 @@ export const loadMedia = ( mediaEl.dispatchEvent( new CustomEvent('targetlivewindowchange', { composed: true, bubbles: true, detail: targetLiveWindow }) ); + + if (streamType === StreamTypes.LIVE) { + // Required if playlist contains one or more EXT-X-PART tags. See: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-12#section-4.4.3.7 (CJP) + const partInfLine = playlistLines.find((line) => line.startsWith('#EXT-X-PART-INF')); + const lowLatency = !!partInfLine; + + // Computation of the live edge start offset per media-ui-extensions proposal. See: https://github.com/video-dev/media-ui-extensions/blob/main/proposals/0007-live-edge.md#recommended-computation-for-rfc8216bis12-aka-hls (CJP) + let liveEdgeStartOffset; + if (lowLatency) { + // The EXT-X-PART-INF only has one in-spec named attribute, PART-TARGET, which is required, + // so parsing & casting presumptuously here. See spec link above for more info. (CJP) + const partTarget = +partInfLine.split(':')[1].split('=')[1]; + liveEdgeStartOffset = partTarget * 2; + } else { + // This is required for all media playlists. See: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-12#section-4.4.3.1 (CJP) + const targetDurationLine = playlistLines.find((line) => + line.startsWith('#EXT-X-TARGETDURATION') + ) as string; + // EXT-X-TARGETDURATION has exactly one unnamed attribute that represents the target duration value, which is required, + // so parsing and casting presumptuously here. See spec link above for more info. (CJP) + const targetDuration = +targetDurationLine.split(':')[1]; + liveEdgeStartOffset = targetDuration * 3; + } + (muxMediaState.get(mediaEl) ?? {}).liveEdgeStartOffset = liveEdgeStartOffset; + } }); mediaEl.setAttribute('src', src); if (props.startTime) {