Skip to main content
Version: 11.13.0

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>