Skip to content

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.

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

OptionDefaultMeaning
type'image/png''image/png', 'image/jpeg', or 'image/webp'
qualityBrowser defaultFinite 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.

ts
// 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:

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

vue
<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>
ts
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 }
}