Clipping

Live Instant Clipping #

Start a Live Broadcast #

To use this demo, click the “Start” button if there isn’t a broadcast in progress, then step through each section below.

Sample live broadcast: checking...
Stream Subdomain
Customer subdomain for playback
Input or Video ID
Live Input ID

Playback can take up to a minute to start once requested, and this feature is best demonstrated at least a few minutes into the broadcast.

Get the Preview Manifest #

Optionally, create a preview manifest to allow a user to replay recent content and select a start-time for a clip. Append a duration argument to the manifest URL to make a preview manifest:

Preview Manifest URL Pattern: https://customer-<CODE>.cloudflarestream.com/<VIDEO_ID||INPUT_ID>/manifest/video.m3u8?duration=<NNm|s>

Use Preview to Select Start and Duration #

Lots of implementation options for this. Here’s one way to do it: use HLS.js to play the preview manifest and let a user pick a start time and duration to make a clip.

Play the preview manifest generated above.
Preview Offset
Seconds into the broadcast when the preview starts
Clip Start Time
Seconds into the preview to start the clip
Clip Duration
Up to 60 seconds

Clip Manifest URL Pattern: https://customer-<CODE>.cloudflarestream.com/<VIDEO_ID>/manifest/clip.m3u8?time=<NNs>&duration=<NNs>

The code below includes how to override HLS.js’s loader to get the Video ID and preview start time from the response headers in the manifest loader.


<script>
  const video = document.getElementById('preview-video');

  // Override the pLoader (playlist loader) to modify the handler for
  // receiving the manifest and grab the header there.
  class pLoader extends Hls.DefaultConfig.loader {
    constructor(config) {
      super(config);
      var load = this.load.bind(this);
      this.load = function (context, config, callbacks) {
        if (context.type == 'manifest') {
          var onSuccess = callbacks.onSuccess;
          // copy the existing onSuccess handler so we can fire it later.

          callbacks.onSuccess = function (response, stats, context, networkDetails) {
            console.log(networkDetails);
            // The fourth argument here is undocumented in HLS.js but contains
            // the response object for the manifest fetch, which gives us headers:

            window.currentPreviewStart =
              parseInt(networkDetails.getResponseHeader('preview-start-seconds'));
            // Save the start time of the preview manifest

            previewOffset.value = window.currentPreviewStart;
            // Put the value in our text field example

            window.currentVideoId =
              networkDetails.getResponseHeader('stream-media-id');

            window.currentVideoUrl = `${window.streamSubdomain}/${window.currentVideoId}`;

            onSuccess(response, stats, context);
            // And fire the exisint success handler.
          };
        }
        load(context, config, callbacks);
      };
    }
  }

  const hls = new Hls({
    pLoader: pLoader,
  });

  // Start playback of the preview manifest:
  previewButton.addEventListener('click', (e) => {
    e.preventDefault();
    if (window?.currentLiveUrl) {
      const videoSrc = window.currentLiveUrl + '/manifest/video.m3u8?duration=3m';
      if (Hls.isSupported()) {
        hls.loadSource(videoSrc);
        hls.attachMedia(video);
      } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
        video.src = videoSrc;
      }
    } else {
      alert('Fetch the preview manifest first');
    }
  });

  // Grab the time into the preview where the user is, fill in the form
  previewTimeCapture.addEventListener('click', (e) => {
    e.preventDefault();
    previewStart.value = Math.floor(video.currentTime);
  });

  // Generate the clip URL. We'll need the video ID, the offset of the preview
  // and the time into the preview where the user marked.
  previewGenerateUrl.addEventListener('click', (e) => {
    if (!previewStart.value) {
      alert('Need a start time');
      return;
    } else if (!previewDuration.value || parseInt(previewDuration.value) > 60) {
      alert('Need a preview duration set and no more than 60 seconds');
      return;
    } else if (!window.currentVideoId) {
      alert('Fetch preview manifest.');
      return;
    }

    window.clipUrl =
      `${window.currentVideoUrl}/manifest/clip.m3u8` +
      `?time=${parseInt(previewStart.value) + window.currentPreviewStart}s` +
      `&duration=${previewDuration.value}s`;

    clipBaseUrl.innerText = window.clipUrl;
  });
</script>

Watch the Clip #



<video controls id="clip-video"></video>
<br /><button id="clip-start">Play Clip</button>

<script>
  const videoClip = document.getElementById('clip-video');

  const hlsClip = new Hls();

  document.getElementById('clip-start').addEventListener('click', (e) => {
    e.preventDefault();
    if (!window?.clipUrl) {
      alert('Generate a clip above first');
      return;
    }

    if (Hls.isSupported()) {
      hlsClip.loadSource(window.clipUrl);
      hlsClip.attachMedia(videoClip);
    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
      videoClip.src = window.clipUrl;
    }
  });
</script>

Download the MP4 #

You can also download an MP4 of the clip.

Download MP4

MP4 Download URL Pattern: https://customer-<CODE>.cloudflarestream.com/<VIDEO_ID>/clip.mp4?time=<NNs>&duration=<NNs>&filename=<FILENAME>.mp4


<script>
  document.getElementById('clip-download-url-generate').addEventListener('click', (e) => {
    e.preventDefault();
    if (!window?.clipUrl) {
      alert('Generate a clip above first');
      return;
    }

    const downloadUrl =
      `${window.currentVideoUrl}/clip.mp4` +
      `?time=${parseInt(previewStart.value) + window.currentPreviewStart}s` +
      `&duration=${previewDuration.value}s` +
      `&filename=clip-test-${previewDuration.value}s.mp4`;

    document.getElementById('clip-download-url').innerText = downloadUrl;
    document.getElementById('clip-download-link').href = downloadUrl;
  });
</script>

Scratchwork #

Trouble Reading the Header #

Things that did not work:
  • Using the xhrSetup property in new Hls() to add a callback to the loader’s onreadystatechange event wherein we could read the header from the manifest request.
    • This didn’t work because onreadystatechange was only ever called for READY_STATE of 0, and not again.
    • Similarly, the loadend and abort events were never fired either.
// DO NOT COPY. This was attempted but did not work.
const xhrModify = (xhr, url) => {
  // THIS NEVER EXECUTES
  xhr.loadend = function () {
    console.log(xhr);
  }
  // THIS NEVER EXECUTES
  xhr.abort = function () {
    console.log(xhr);
  }
  // THIS FIRES FOR ALL MANIFEST/SEG REQS BUT READYSTATE IS ALWAYS 1
  // AND NEVER ADVANCES...
  xhr.onreadystatechange = function () {
    if (xhr.readyState === xhr.HEADERS_RECEIVED) {
      console.log(url);
      console.log(xhr.status);
      const clipStart = xhr.getResponseHeader('clip-start-seconds');
      console.log(clipStart);
    }
  };
};

const hls = new Hls({
  xhrSetup: xhrModify,
});
  • Using fetch() to load the manifest manually, which makes getting the header easy. Then using response.blob() and URL.createObjectURL to pass the fetched content directly to HLS.js
    • This didn’t work because the manifest contains relaive links to ABR playlists and media segments. The blob: URL has a different host/path structure so all those subsequent requests were 404.
// DO NOT COPY. This was attempted but did not work.

const videoSrc = window.currentVideoUrl + '/manifest/video.m3u8?duration=3m';
const response = await fetch(videoSrc);

window.currentPreviewStart =
  parseInt(response.headers.get('clip-start-seconds'));
// Save the start time of the preview manifest

const manifestBlob = await response.blob();
const manifestBlobUrl = URL.createObjectURL(manifestBlob);
// THIS WORKS TO LOAD THE PRIMARY MANIFEST BUT BECAUSE THE ABR PLAYLISTS AND
// MEDIA SEGMENTS THEREIN ARE RELATIVE LINKS, NONE OF THEM LOAD.

if (Hls.isSupported()) {
  hls.loadSource(manifestBlobUrl);
  hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
  video.src = manifestBlobUrl;
}