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.
| 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.
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
xhrSetupproperty innew Hls()to add a callback to the loader’sonreadystatechangeevent wherein we could read the header from the manifest request.- This didn’t work because
onreadystatechangewas only ever called forREADY_STATEof 0, and not again. - Similarly, the
loadendandabortevents were never fired either.
- This didn’t work because
// 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 usingresponse.blob()andURL.createObjectURLto 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.
- This didn’t work because the manifest contains relaive links to ABR playlists
and media segments. The
// 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;
}