Screenshots and downloads
Import the standalone helpers from videojs-vue/media/utils. This entry has no Vue runtime dependency, registers no custom elements, and does not load hls.js. Importing it is SSR-safe; call the helpers in the browser from a user interaction.
import { captureVideoFrame, downloadMedia } from 'videojs-vue/media/utils'
// player is a template ref holding a VideoPlayerHandle.
const frame = await captureVideoFrame(player.value?.media)
downloadMedia(frame, 'frame.png')Capture a frame
captureVideoFrame(media, options?) returns Promise<Blob> at the video's intrinsic resolution. It captures immediately without seeking or pausing. The result contains the video pixels, without player controls, overlays, or captions.
| Option | Default | Meaning |
|---|---|---|
type | 'image/png' | 'image/png', 'image/jpeg', or 'image/webp' |
quality | Browser default | Finite number from 0 to 1 for JPEG/WebP; ignored for PNG |
The browser may fall back to PNG if it cannot encode the requested type. Check blob.type when choosing a file extension. The helper creates no object URLs, so the Blob can also be uploaded or previewed by your application.
The media must be a native HTMLVideoElement, with readyState >= 2 and nonzero videoWidth / videoHeight. Wait for loadeddata or play/seek before capturing. Audio elements, missing refs, and adapter elements reject with a descriptive error. HlsVideo currently has no public native-video accessor; its protected internals and shadow DOM are not used by this helper.
Set crossorigin="anonymous" before loading cross-origin video and configure the server's CORS headers. Canvas export can still throw SecurityError; browser errors are preserved so applications can show the actual cause. Canvas context failures and a null encoding result also reject. See canvas encoding and cross-origin canvas restrictions.
Download a file
downloadMedia(source, filename) requests a browser download and returns void. source is a file URL string (relative, HTTP(S), or Blob URL) or a Blob. The filename must include the desired extension. The helper does not infer a downloadable source from the player.
// No fetch or buffering; suitable for same-origin files or attachment endpoints.
downloadMedia('/media/lesson.mp4', 'lesson.mp4')
// Caller-owned Blob URLs remain caller-owned and are never revoked here.
downloadMedia(myObjectUrl, 'recording.webm')Browsers decide whether to honor the filename and download request. A cross-origin URL may open in a new tab unless the server provides an attachment response. The helper cannot report whether a file was saved. See the HTML download attribute.
For authenticated files or predictable cross-origin download behavior, fetch the bytes and pass a Blob:
const controller = new AbortController()
const response = await fetch('/api/media/lesson/file', {
credentials: 'include',
// headers: { Authorization: `Bearer ${token}` },
signal: controller.signal,
})
if (!response.ok) {
throw new Error(`Download failed: ${response.status}`)
}
downloadMedia(await response.blob(), 'lesson.mp4')
// Call controller.abort() to cancel pending fetch/body reads.Fetching a Blob buffers the entire file in memory. For large files, prefer a server download endpoint with Content-Disposition: attachment. CORS and authentication remain server/application responsibilities. For Blob inputs, the helper removes its temporary anchor immediately and revokes its own object URL after 60 seconds to allow the browser to consume it.
HLS playlists and MSE blob: playback URLs are not standalone video files. These helpers do not merge segments, retrieve encryption keys, or create offline HLS archives. Provide a real downloadable file endpoint for streaming content.
Runnable example
The Playground's Screenshots & downloads example saves PNG frames and fetches MP4 files. Copy both files below into the same directory and render <MediaExportExample />. The local composable cancels fetches and ignores late encoding/body results on source changes, unmount, or KeepAlive deactivation. It reports HTTP, canvas, and network errors without changing playback state.
<script setup lang="ts">
import { VideoPlayer } from 'videojs-vue'
import type { VideoMediaProps, VideoPlayerHandle } from 'videojs-vue'
import { onDeactivated, reactive, useAttrs, useTemplateRef, watch } from 'vue'
import { useMediaExport } from './useMediaExport'
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
settings?: Partial<VideoMediaProps>
skin?: 'default' | 'neutral'
title?: string
labels?: {
capture: string
download: string
cancel: string
busy: string
}
}>(),
{
settings: () => ({
src: 'https://vjs.zencdn.net/v/oceans.mp4',
crossorigin: 'anonymous',
playsinline: true,
}),
skin: 'default',
title: 'Video export',
labels: () => ({
capture: 'Save current frame',
download: 'Download MP4',
cancel: 'Cancel',
busy: 'Preparing download…',
}),
},
)
const attrs = useAttrs()
const player = useTemplateRef<VideoPlayerHandle>('player')
const mediaExport = reactive(
useMediaExport({
getMedia: () => player.value?.media ?? null,
getSource: () => props.settings.src ?? '',
}),
)
// Invalidate late encodes/responses even if the browser cannot cancel them.
watch(() => props.settings.src, mediaExport.cancel, { flush: 'sync' })
onDeactivated(mediaExport.cancel)
</script>
<template>
<div class="media-export-example">
<VideoPlayer
ref="player"
v-bind="{ ...settings, ...attrs }"
:skin="skin"
:title="title"
/>
<div class="media-export-actions">
<button
type="button"
:disabled="mediaExport.isBusy"
@click="mediaExport.capture"
>
{{ labels.capture }}
</button>
<button
type="button"
:disabled="mediaExport.isBusy || !settings.src"
@click="mediaExport.download"
>
{{ labels.download }}
</button>
<button
v-if="mediaExport.isBusy"
type="button"
@click="mediaExport.cancel"
>
{{ labels.cancel }}
</button>
<p
v-if="mediaExport.isBusy"
role="status"
>
{{ labels.busy }}
</p>
<p
v-if="mediaExport.failure"
role="alert"
>
{{ mediaExport.failure }}
</p>
</div>
</div>
</template>
<style scoped>
.media-export-example {
color: white;
background: #18181b;
}
.media-export-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
padding: 1rem;
font-size: 0.875rem;
}
.media-export-actions button {
border: 1px solid #71717a;
border-radius: 0.25rem;
padding: 0.375rem 0.75rem;
color: inherit;
background: transparent;
cursor: pointer;
font: inherit;
}
.media-export-actions button:disabled {
opacity: 0.4;
cursor: default;
}
.media-export-actions button:focus-visible {
outline: 2px solid #a5b4fc;
outline-offset: 3px;
}
.media-export-actions [role='alert'] {
flex-basis: 100%;
color: #fda4af;
}
</style>import type { PlayerMedia } from 'videojs-vue'
import { captureVideoFrame, downloadMedia } from 'videojs-vue/media/utils'
import { onScopeDispose, shallowRef } from 'vue'
export function useMediaExport(options: {
getMedia: () => PlayerMedia | null
getSource: () => string
}) {
const isBusy = shallowRef(false)
const failure = shallowRef('')
let version = 0
let controller: AbortController | null = null
function cancel() {
version++
controller?.abort()
controller = null
isBusy.value = false
failure.value = ''
}
async function run(
createBlob: (signal: AbortSignal) => Promise<Blob>,
filename: string,
) {
cancel()
const token = version
const request = new AbortController()
controller = request
isBusy.value = true
try {
const blob = await createBlob(request.signal)
if (version === token) {
downloadMedia(blob, filename)
}
} catch (error) {
if (version === token) {
failure.value = error instanceof Error ? error.message : String(error)
}
} finally {
if (version === token) {
controller = null
isBusy.value = false
}
}
}
function capture() {
return run(() => captureVideoFrame(options.getMedia()), 'frame.png')
}
function download() {
return run(async signal => {
const source = options.getSource()
if (!source.trim()) {
throw new Error('A file URL is required.')
}
// Add credentials or Authorization headers here for your file endpoint.
const response = await fetch(source, { signal })
if (!response.ok) {
throw new Error(`Download request failed: ${response.status}`)
}
return response.blob()
}, 'video.mp4')
}
onScopeDispose(cancel)
return { isBusy, failure, capture, download, cancel }
}