Skip to content

Commit

Permalink
feat(playback-core, mux-player): hande live edge for native playback.…
Browse files Browse the repository at this point in the history
… fix edge case when attempting to use seekable before ready. Fix tests.
  • Loading branch information
cjpillsbury committed Mar 1, 2023
1 parent 677d873 commit 06c38c1
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 22 deletions.
5 changes: 3 additions & 2 deletions packages/mux-player/src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -43,7 +44,7 @@ const getHotKeys = (props: MuxTemplateProps) => {
export const content = (props: MuxTemplateProps) => html`
<media-theme
template="${props.themeTemplate ?? muxTemplate.content.children[0]}"
default-stream-type="${props.defaultStreamType}"
default-stream-type="${props.defaultStreamType ?? 'on-demand'}"
class="${props.secondaryColor ? 'two-tone' : false}"
hotkeys="${getHotKeys(props) || false}"
nohotkeys="${props.noHotKeys || !props.hasSrc || props.isDialogOpen || false}"
Expand Down
20 changes: 5 additions & 15 deletions packages/mux-player/test/player.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,7 @@ describe('<mux-player>', () => {
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`);
Expand Down Expand Up @@ -878,7 +874,7 @@ describe('<mux-player> 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(`<mux-player
Expand All @@ -888,10 +884,7 @@ describe('<mux-player> seek to live behaviors', function () {
preload="auto"
></mux-player>`);

// 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();
Expand All @@ -901,7 +894,7 @@ describe('<mux-player> 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(`<mux-player
playback-id="v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM"
Expand All @@ -910,10 +903,7 @@ describe('<mux-player> seek to live behaviors', function () {
preload="auto"
></mux-player>`);

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();
Expand Down
8 changes: 4 additions & 4 deletions packages/mux-player/test/template.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('<mux-player> template render', () => {
assert.equal(
normalizeAttributes(minify(div.innerHTML)),
normalizeAttributes(
`<media-theme default-showing-captions="" disabled="" nohotkeys="" stream-type="on-demand" exportparts="${exportParts}"><mux-video slot="media" crossorigin="" playsinline="" exportparts="video"></mux-video><mxp-dialog no-auto-hide=""><p></p></mxp-dialog></media-theme>`
`<media-theme default-showing-captions="" default-stream-type="on-demand" disabled="" exportparts="${exportParts}" nohotkeys=""><mux-video crossorigin="" exportparts="video" playsinline="" slot="media"></mux-video><mxp-dialog no-auto-hide=""><p></p></mxp-dialog></media-theme>`
)
);
});
Expand All @@ -35,7 +35,7 @@ describe('<mux-player> template render', () => {
assert.equal(
normalizeAttributes(minify(div.innerHTML)),
normalizeAttributes(
`<media-theme hotkeys=" noarrowleft noarrowright" stream-type="live" default-showing-captions="" disabled="" nohotkeys="" exportparts="${exportParts}"><mux-video slot="media" crossorigin="" playsinline="" stream-type="live" cast-stream-type="live" exportparts="video"></mux-video><mxp-dialog no-auto-hide="" open=""><h3>Errr</h3><p></p></mxp-dialog></media-theme>`
`<media-theme default-showing-captions="" default-stream-type="on-demand" disabled="" exportparts="${exportParts}" hotkeys=" noarrowleft noarrowright" nohotkeys=""><mux-video cast-stream-type="live" crossorigin="" exportparts="video" playsinline="" slot="media" stream-type="live"></mux-video><mxp-dialog no-auto-hide="" open=""><h3>Errr</h3><p></p></mxp-dialog></media-theme>`
)
);
});
Expand All @@ -52,7 +52,7 @@ describe('<mux-player> template render', () => {
assert.equal(
normalizeAttributes(minify(div.innerHTML)),
normalizeAttributes(
`<media-theme default-showing-captions="" disabled="" nohotkeys="" stream-type="on-demand" exportparts="${exportParts}"><mux-video slot="media" crossorigin="" playsinline="" stream-type="on-demand" exportparts="video"></mux-video><mxp-dialog no-auto-hide=""><p></p></mxp-dialog></media-theme>`
`<media-theme default-showing-captions="" default-stream-type="on-demand" disabled="" exportparts="${exportParts}" nohotkeys=""><mux-video crossorigin="" exportparts="video" playsinline="" slot="media" stream-type="on-demand"></mux-video><mxp-dialog no-auto-hide=""><p></p></mxp-dialog></media-theme>`
)
);
});
Expand All @@ -73,7 +73,7 @@ describe('<mux-player> template render', () => {
assert.equal(
normalizeAttributes(minify(div.innerHTML)),
normalizeAttributes(
`<media-theme stream-type="on-demand" default-showing-captions="" disabled="" nohotkeys="" exportparts="${exportParts}"><mux-video slot="media" crossorigin="" playsinline="" stream-type="on-demand" exportparts="video"></mux-video><mxp-dialog no-auto-hide="" open=""><h3>Errr</h3><p></p></mxp-dialog></media-theme>`
`<media-theme default-showing-captions="" default-stream-type="on-demand" disabled="" exportparts="${exportParts}" nohotkeys=""><mux-video crossorigin="" exportparts="video" playsinline="" slot="media" stream-type="on-demand"></mux-video><mxp-dialog no-auto-hide="" open=""><h3>Errr</h3><p></p></mxp-dialog></media-theme>`
)
);
});
Expand Down
30 changes: 29 additions & 1 deletion packages/playback-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down Expand Up @@ -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;

Expand All @@ -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) {
Expand Down

0 comments on commit 06c38c1

Please sign in to comment.