# 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

PreviousNextCache next item now



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".

```js
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](https://optiview.dolby.com/docs/theoplayer/v11/api-reference/web/interfaces/Cache).

Code

demo.html

```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>
```
