Skip to content
This repository has been archived by the owner on May 17, 2024. It is now read-only.

Commit

Permalink
feat: add renditions API and example
Browse files Browse the repository at this point in the history
  • Loading branch information
luwes committed Jul 21, 2023
1 parent 75bf2ee commit 733e361
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 56 deletions.
69 changes: 68 additions & 1 deletion hls-video-element.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { CustomVideoElement } from 'custom-media-element';
import { MediaTracksMixin } from 'media-tracks';
import Hls from 'hls.js/dist/hls.mjs';

class HLSVideoElement extends CustomVideoElement {
class HLSVideoElement extends MediaTracksMixin(CustomVideoElement) {

attributeChangedCallback(attrName, oldValue, newValue) {
if (attrName !== 'src') {
Expand Down Expand Up @@ -78,6 +79,72 @@ class HLSVideoElement extends CustomVideoElement {

this.api.attachMedia(this.nativeEl);

// Set up renditions

// Create a map to save the unique id's we create for each level and rendition.
// hls.js uses the levels array index primarily but we'll use the id to have a
// 1 to 1 relation from rendition to level.
const levelIdMap = new WeakMap();

this.api.on(Hls.Events.MANIFEST_PARSED, (event, data) => {
removeAllVideoTracks();

const videoTrack = this.addVideoTrack('main');
videoTrack.selected = true;

for (const [id, level] of data.levels.entries()) {
const videoRendition = videoTrack.addRendition(
level.url[0],
level.width,
level.height,
level.videoCodec,
level.bitrate
);

// The returned levels all have an id of `0`, save the id in a WeakMap.
levelIdMap.set(level, `${id}`);
videoRendition.id = `${id}`;
}
});

// Fired when a level is removed after calling `removeLevel()`
this.api.on(Hls.Events.LEVELS_UPDATED, (event, data) => {
const videoTrack = this.videoTracks[this.videoTracks.selectedIndex ?? 0];
if (!videoTrack) return;

const levelIds = data.levels.map((l) => levelIdMap.get(l));

for (const rendition of this.videoRenditions) {
if (rendition.id && !levelIds.includes(rendition.id)) {
videoTrack.removeRendition(rendition);
}
}
});

// hls.js doesn't support enabling multiple renditions.
//
// 1. if all renditions are enabled it's auto selection.
// 2. if 1 of the renditions is disabled we assume a selection was made
// and lock it to the first rendition that is enabled.
const switchRendition = ({ target: renditions }) => {
const level = renditions.selectedIndex;
if (level != this.api.nextLevel) {
this.api.nextLevel = level;
}
};

this.videoRenditions.addEventListener('change', switchRendition);

const removeAllVideoTracks = () => {
for (const videoTrack of this.videoTracks) {
this.removeVideoTrack(videoTrack);
}
};

this.api.once(Hls.Events.DESTROYING, () => {
removeAllVideoTracks();
});

} else if (this.nativeEl.canPlayType('application/vnd.apple.mpegurl')) {

this.nativeEl.src = this.src;
Expand Down
247 changes: 193 additions & 54 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,56 +1,195 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<title>HLS Video Element</title>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css" />
<style>
hls-video {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
background: #000;
}
</style>

<script type="importmap">
{
"imports": {
"custom-media-element": "https://cdn.jsdelivr.net/npm/[email protected]",
"hls.js/": "https://cdn.jsdelivr.net/npm/[email protected]/"
}
}
</script>
<script type="module" src="./hls-video-element.js"></script>
</head>
<body>

<h2>On-demand</h2>
<hls-video
id="video1"
preload="metadata"
controls
src="https://stream.mux.com/DS00Spx1CV902MCtPj5WknGlR102V5HFkDe.m3u8"
crossorigin
>
<track label="thumbnails" id="customTrack" default kind="metadata" src="https://image.mux.com/DS00Spx1CV902MCtPj5WknGlR102V5HFkDe/storyboard.vtt"></track>
</hls-video>
<br>
<!doctype html>

<title>&lt;hls-video&gt;</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css" />
<style>
body {
text-align: center;
}
media-controller,
hls-video {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
background: #000;
}
nav {
display: flex;
justify-content: space-between;
}
</style>

<script type="importmap">
{
"imports": {
"custom-media-element": "https://cdn.jsdelivr.net/npm/[email protected]",
"media-tracks": "https://cdn.jsdelivr.net/npm/[email protected]/+esm",
"hls.js/": "https://cdn.jsdelivr.net/npm/[email protected]/"
}
}
</script>
<script type="module" src="./hls-video-element.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/media-chrome/+esm"></script>

<h1>&lt;hls-video&gt;</h1>
<br>

<h2>On-demand</h2>
<hls-video
id="myVideo"
controls
src="https://stream.mux.com/DS00Spx1CV902MCtPj5WknGlR102V5HFkDe.m3u8"
crossorigin
>
<track label="thumbnails" id="customTrack" default kind="metadata" src="https://image.mux.com/DS00Spx1CV902MCtPj5WknGlR102V5HFkDe/storyboard.vtt"></track>
</hls-video>

<br>

<nav>
<nav>
<button id="loadbtn">Load new clip</button>
<script type="module">
loadbtn.onclick = () => {
video1.src = 'https://stream.mux.com/1EFcsL5JET00t00mBv01t00xt00T4QeNQtsXx2cKY6DLd7RM.m3u8';
};
</script>

<h2>Live</h2>
<hls-video
controls
src="https://stream.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM"
crossorigin
></hls-video>

<p><a href="https://github.com/muxinc/hls-video-element">github.com/muxinc/hls-video-element</a></p>
</body>
</html>
<button id="removebtn">Remove first rendition</button>
</nav>
<nav>
<select id="qualityselect">
<option value="auto">Auto</option>
</select>
<input id="qualityselected" value="N/A" readonly size="5">
</nav>
</nav>

<script type="module">

myVideo.videoTracks.addEventListener('removetrack', ({ track }) => {
let i = qualityselect.options.length;
while (--i) qualityselect.options.remove(i);
});

myVideo.videoRenditions.addEventListener('addrendition', ({ rendition }) => {
qualityselect.append(new Option(
`${Math.min(rendition.width, rendition.height)}p`,
rendition.id,
));
});

myVideo.videoRenditions.addEventListener('removerendition', ({ rendition }) => {
qualityselect.querySelector(`[value="${rendition.id}"]`).remove();
});

myVideo.addEventListener('resize', () => {
qualityselected.value = `${Math.min(myVideo.videoWidth, myVideo.videoHeight)}p`;
});

loadbtn.onclick = () => {
myVideo.src = 'https://stream.mux.com/1EFcsL5JET00t00mBv01t00xt00T4QeNQtsXx2cKY6DLd7RM.m3u8';
};

removebtn.onclick = () => {
myVideo.api.removeLevel(0);
};

qualityselect.addEventListener('change', () => {
myVideo.videoRenditions.selectedIndex = qualityselect.selectedIndex - 1;
});
</script>

<h2>Live</h2>
<hls-video
controls
src="https://stream.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM"
crossorigin
></hls-video>

<script>
const video = document.querySelector('hls-video');

video.addEventListener('emptied', (e) => {
console.log(e.type);
});

video.addEventListener('loadstart', (e) => {
console.log(e.type);
});

video.addEventListener('loadedmetadata', (e) => {
console.log(e.type);
});

video.addEventListener('loadeddata', (e) => {
console.log(e.type);
});

video.addEventListener('play', (e) => {
console.log(e.type);
});

video.addEventListener('waiting', (e) => {
console.log(e.type);
});

video.addEventListener('playing', (e) => {
console.log(e.type);
});

video.addEventListener('pause', (e) => {
console.log(e.type);
});

video.addEventListener('seeking', (e) => {
console.log(e.type);
});

video.addEventListener('seeked', (e) => {
console.log(e.type);
});

video.addEventListener('ended', (e) => {
console.log(e.type);
});

video.addEventListener('durationchange', (e) => {
console.log(e.type, video.duration);
});

video.addEventListener('volumechange', (e) => {
console.log(e.type, video.volume);
});

video.addEventListener('resize', (e) => {
console.log(e.type, video.videoWidth, video.videoHeight);
});
</script>

<br>

<h2>With <a href="https://github.com/muxinc/media-chrome" target="_blank">Media Chrome</a></h2>

<media-controller>
<hls-video
src="https://stream.mux.com/O6LdRc0112FEJXH00bGsN9Q31yu5EIVHTgjTKRkKtEq1k.m3u8"
poster="https://image.mux.com/O6LdRc0112FEJXH00bGsN9Q31yu5EIVHTgjTKRkKtEq1k/thumbnail.jpg?time=56"
crossorigin
slot="media"
muted
>
<track label="thumbnails" default kind="thumbnails" src="https://image.mux.com/O6LdRc0112FEJXH00bGsN9Q31yu5EIVHTgjTKRkKtEq1k/storyboard.vtt"></track>
</hls-video>
<media-loading-indicator slot="centered-chrome" no-auto-hide></media-loading-indicator>
<media-control-bar>
<media-play-button></media-play-button>
<media-seek-backward-button seek-offset="15"></media-seek-backward-button>
<media-seek-forward-button seek-offset="15"></media-seek-forward-button>
<media-mute-button></media-mute-button>
<media-volume-range></media-volume-range>
<media-time-range></media-time-range>
<media-time-display show-duration remaining></media-time-display>
<media-playback-rate-button></media-playback-rate-button>
<media-pip-button></media-pip-button>
<media-fullscreen-button></media-fullscreen-button>
</media-control-bar>
</media-controller>

<br>
<br>
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
},
"dependencies": {
"custom-media-element": "^0.2.0",
"hls.js": "^1.4.9"
"hls.js": "^1.4.9",
"media-tracks": "^0.2.1"
},
"keywords": [
"HLS",
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ hls.js@^1.4.9:
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.4.9.tgz#8b23b029cafda4b64ef1c5acd4f65c4f245103d7"
integrity sha512-i2BuNh7C7sd+wpW1V9p+P37KKCWNc6Ph/3BiPr+8nfJ7eZdtQQvSQUn2QwKU+7Fvc7b5BpS/lM6RJ3LUf+XbWg==

media-tracks@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/media-tracks/-/media-tracks-0.2.1.tgz#60bac586ca23f2d443da8e094e0ed13e943600e9"
integrity sha512-Q+NQmQ6CmUlEcmkomYvU1G8UfUPvZW59fsGOJFcQJ8AZTO2UkGCs2ycjPqZb1+K2PyJije5b/NKedVUVI6yPaA==

playwright-core@^1.31.1:
version "1.36.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.36.1.tgz#f5f275d70548768ca892583519c89b237a381c77"
Expand Down

0 comments on commit 733e361

Please sign in to comment.