Skip to main content
Version: 11.13.0

Playlist and caching

Build a playlist and preload the next item in the background with the caching API.

Tip: open your browser's developer console and use the player and THEOplayer variables to interact with this demo.

Playlist

    The playlist is just an array of source descriptions — each click assigns the next one to player.source. Once playback passes 75% of the current item, THEOplayer.cache.createTask() starts caching the first 25% of the next item, so it starts faster. You can also trigger it manually with "Cache next item now".

    player.addEventListener('ended', () => {
    player.autoplay = true;
    player.source = playlist[active + 1].source;
    });

    // Cache part of an upcoming item:
    const task = THEOplayer.cache.createTask(playlist[active + 1].source, { amount: '25%' });
    task.start();

    See the Cache API.

    Code
    demo.html
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <title>Playlist and caching</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
    id="player-ui"
    configuration='{"libraryLocation":"https://cdn.theoplayer.com/dash/theoplayer/","licenseUrl":"../../../../theoplayer-license.txt","mutedAutoplay":"all"}'
    ></theoplayer-default-ui>
    <script src="../utils.js"></script>
    <script>
    const playlist = [
    {
    title: 'Big Buck Bunny',
    poster: 'https://cdn.theoplayer.com/video/big_buck_bunny/poster.jpg',
    source: {
    sources: { src: 'https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny_metadata.m3u8', type: 'application/x-mpegurl' },
    poster: 'https://cdn.theoplayer.com/video/big_buck_bunny/poster.jpg',
    metadata: { title: 'Big Buck Bunny' },
    },
    },
    {
    title: 'Star Wars',
    poster: 'https://cdn.theoplayer.com/video/star_wars_episode_vii-the_force_awakens_official_comic-con_2015_reel_(2015)/poster.jpg',
    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' },
    },
    },
    {
    title: 'Tears of Steel',
    poster: 'https://cdn.theoplayer.com/video/tears_of_steel/poster.jpg',
    source: {
    sources: { src: 'https://cdn.theoplayer.com/video/tears_of_steel/index.m3u8', type: 'application/x-mpegurl' },
    poster: 'https://cdn.theoplayer.com/video/tears_of_steel/poster.jpg',
    metadata: { title: 'Tears of Steel' },
    },
    },
    ];

    onPlayerReady((player) => {
    let active = 0;
    // Per-item cache state reported to the demo page.
    const cacheStatus = playlist.map(() => 'none');
    const cacheProgress = playlist.map(() => 0);
    const cacheTasks = playlist.map(() => null);

    function canCacheNext() {
    const next = active + 1;
    return next < playlist.length && cacheStatus[next] === 'none';
    }

    function report() {
    sendToParent('playlist-state', {
    active,
    canCacheNext: canCacheNext(),
    items: playlist.map((item, index) => ({
    title: item.title,
    poster: item.poster,
    status: cacheStatus[index],
    progress: cacheProgress[index],
    })),
    });
    }

    function loadItem(index, autoplay) {
    if (index < 0 || index >= playlist.length) return;
    active = index;
    if (autoplay) player.autoplay = true;
    player.source = playlist[index].source;
    report();
    }

    function watchTask(task, index) {
    cacheTasks[index] = task;
    cacheStatus[index] = task.status === 'done' ? 'done' : task.status === 'error' ? 'error' : 'caching';
    cacheProgress[index] = Math.round(task.percentageCached * 100);
    report();
    task.addEventListener('progress', () => {
    cacheProgress[index] = Math.round(task.percentageCached * 100);
    report();
    });
    task.addEventListener('statechange', () => {
    if (task.status === 'done') cacheStatus[index] = 'done';
    else if (task.status === 'error') cacheStatus[index] = 'error';
    cacheProgress[index] = Math.round(task.percentageCached * 100);
    report();
    });
    }

    // Reuse an existing task for the same source instead of creating a duplicate.
    // Tasks persist in THEOplayer.cache.tasks across reloads of this page.
    function taskForSource(src) {
    for (const task of THEOplayer.cache.tasks) {
    const taskSrc = task.source && task.source.sources && task.source.sources.src;
    if (taskSrc === src && task.status !== 'error' && task.status !== 'evicted') return task;
    }
    return null;
    }

    function cacheNext() {
    const next = active + 1;
    if (!canCacheNext()) return;
    const src = playlist[next].source.sources.src;
    const task = taskForSource(src) || THEOplayer.cache.createTask(playlist[next].source, { amount: '25%' });
    if (cacheTasks[next] !== task) watchTask(task, next);
    if (task.status === 'idle') task.start();
    }

    // Cache the first 25% of the next item once playback passes 75%.
    function maybeCacheNext() {
    if (!canCacheNext()) return;
    if (!Number.isFinite(player.duration) || player.duration <= 0) return;
    if (player.currentTime / player.duration < 0.75) return;
    cacheNext();
    }

    onParentMessage('playlist-load', (message) => {
    loadItem(message.index, true);
    });
    onParentMessage('playlist-previous', () => {
    loadItem(active - 1, true);
    });
    onParentMessage('playlist-next', () => {
    loadItem(active + 1, true);
    });
    onParentMessage('playlist-cache-next', () => {
    cacheNext();
    });

    player.addEventListener('ended', () => loadItem(active + 1, true));
    player.addEventListener('timeupdate', maybeCacheNext);
    player.addEventListener('sourcechange', report);

    // Initial item, no autoplay.
    loadItem(0, false);
    });
    </script>
    </body>
    </html>