Creating bookmarks
Select a bookmark to seek within the current stream or switch to another stream and start playback.
Tip: open your browser's developer console and use the player and THEOplayer variables to interact with this demo.
Each bookmark stores its title, source, poster, and playhead position. Selecting a bookmark saves the current position before seeking or loading the selected source. Use the button below the player to add a bookmark at the current playhead.
function selectBookmark(bookmark) {
player.autoplay = true;
player.addEventListener(
'loadedmetadata',
() => {
player.currentTime = bookmark.currentTime;
},
{ once: true }
);
player.source = {
sources: [{ src: bookmark.src, type: 'application/x-mpegurl' }],
poster: bookmark.poster,
};
}
Code
demo.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Creating bookmarks</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet" />
<style>
html,
body {
margin: 0;
padding: 0;
}
theoplayer-default-ui:not(:defined) {
display: inline-block;
box-sizing: border-box;
}
theoplayer-default-ui {
width: 100%;
aspect-ratio: 16 / 9;
font-family: 'Noto Sans', sans-serif;
background: #000;
}
</style>
<script src="https://cdn.theoplayer.com/dash/theoplayer/THEOplayer.js"></script>
<script nomodule src="https://unpkg.com/@theoplayer/web-ui@2/polyfills"></script>
<script async src="https://unpkg.com/@theoplayer/web-ui@2"></script>
</head>
<body>
<!--
libraryLocation: For demonstration purposes, we use the THEOplayer CDN.
For production use, host the library yourself and update this URL and the script above.
licenseUrl: Point this to your license file, or replace it with a license property.
-->
<theoplayer-default-ui
configuration='{"libraryLocation":"https://cdn.theoplayer.com/dash/theoplayer/","licenseUrl":"../../../../theoplayer-license.txt","mutedAutoplay":"all"}'
source='{"sources":{"src":"https://cdn.theoplayer.com/video/star_wars_episode_vii-the_force_awakens_official_comic-con_2015_reel_(2015)/index.m3u8","type":"application/x-mpegurl"},"poster":"https://cdn.theoplayer.com/video/star_wars_episode_vii-the_force_awakens_official_comic-con_2015_reel_(2015)/poster.jpg","metadata":{"title":"Star Wars Reel"}}'
></theoplayer-default-ui>
<script src="../utils.js"></script>
<script>
const starWars = {
src: 'https://cdn.theoplayer.com/video/star_wars_episode_vii-the_force_awakens_official_comic-con_2015_reel_(2015)/index.m3u8',
poster: 'https://cdn.theoplayer.com/video/star_wars_episode_vii-the_force_awakens_official_comic-con_2015_reel_(2015)/poster.jpg',
title: 'Star Wars Reel',
};
const tearsOfSteel = {
src: 'https://cdn.theoplayer.com/video/tears_of_steel/index.m3u8',
poster: 'https://cdn.theoplayer.com/video/tears_of_steel/poster.jpg',
title: 'Tears of Steel',
};
const bookmarks = [
{ ...starWars, currentTime: 150, duration: 0 },
{ ...starWars, currentTime: 20, duration: 0 },
{ ...tearsOfSteel, currentTime: 500, duration: 0 },
];
let activeIndex = -1;
let currentSource = starWars.src;
let switchToken = 0;
let lastReportAt = 0;
function getSource(bookmark) {
return {
sources: [{ src: bookmark.src, type: 'application/x-mpegurl' }],
poster: bookmark.poster,
metadata: { title: bookmark.title },
};
}
onPlayerReady((player) => {
function report() {
sendToParent('bookmarks', {
bookmarks: bookmarks.map((bookmark) => ({ ...bookmark })),
activeIndex,
});
lastReportAt = Date.now();
}
function saveActivePosition() {
if (activeIndex < 0 || !bookmarks[activeIndex] || player.readyState < 1) return;
bookmarks[activeIndex].currentTime = player.currentTime;
bookmarks[activeIndex].duration = Number.isFinite(player.duration) ? player.duration : 0;
}
function selectBookmark(index) {
if (!bookmarks[index]) return;
saveActivePosition();
const bookmark = bookmarks[index];
const token = ++switchToken;
currentSource = bookmark.src;
activeIndex = index;
report();
player.addEventListener(
'loadedmetadata',
() => {
if (token !== switchToken) return;
player.currentTime = bookmark.currentTime;
saveActivePosition();
report();
},
{ once: true }
);
player.source = getSource(bookmark);
}
player.addEventListener('timeupdate', () => {
if (Date.now() - lastReportAt < 500) return;
saveActivePosition();
report();
});
player.addEventListener('durationchange', () => {
if (activeIndex >= 0 && bookmarks[activeIndex].src === currentSource) saveActivePosition();
report();
});
onParentMessage('select-bookmark', (message) => {
if (typeof message.index === 'number') {
player.autoplay = true;
selectBookmark(message.index);
}
});
onParentMessage('add-bookmark', () => {
if (activeIndex < 0) return;
saveActivePosition();
const current = bookmarks[activeIndex];
bookmarks.push({
title: `Bookmark ${bookmarks.length + 1}`,
src: currentSource,
poster: current ? current.poster : '',
currentTime: player.currentTime,
duration: Number.isFinite(player.duration) ? player.duration : 0,
});
activeIndex = bookmarks.length - 1;
report();
});
report();
selectBookmark(1);
});
</script>
</body>
</html>