Skip to content

Instantly share code, notes, and snippets.

@do-adams
Last active April 14, 2025 22:37
Show Gist options
  • Select an option

  • Save do-adams/1f24d84c7e84a7ad54b8ba08441ebed6 to your computer and use it in GitHub Desktop.

Select an option

Save do-adams/1f24d84c7e84a7ad54b8ba08441ebed6 to your computer and use it in GitHub Desktop.
Convert .mp4 to. webm with ffmpeg using Vuetify 2.x breakpoints
#!/usr/bin/env tsx
import { execSync } from 'child_process'
import { existsSync } from 'fs'
import { extname } from 'path'
interface VideoConfig {
name: string
resolution: string
bitrate: string
frameRate: number
tileColumns: number
mediaQuery: string
}
const configs: VideoConfig[] = [
{
name: 'xs',
resolution: '640:384',
bitrate: '400k',
frameRate: 20,
tileColumns: 1,
mediaQuery: '(max-width: 959px)',
},
{
name: 'md',
resolution: '960:576',
bitrate: '800k',
frameRate: 24,
tileColumns: 2,
mediaQuery: '(min-width: 960px) and (max-width: 1263px)',
},
{
name: 'lg',
resolution: '1200:720',
bitrate: '1.5M',
frameRate: 24,
tileColumns: 2,
mediaQuery: '(min-width: 1264px)',
},
]
const commonWebMOptions = {
cpuUsed: 2,
noAudio: '-an',
noMetadata: '-map_metadata -1',
}
function runFFmpeg(command: string): void {
try {
execSync(command, { stdio: 'inherit' })
} catch (error) {
console.error(`FFmpeg error: ${(error as Error).message}`)
process.exit(1)
}
}
function generateWebMFFmpegCommands(
input: string,
config: VideoConfig
): string[] {
const baseCommand = `ffmpeg -i "${input}" -c:v libvpx-vp9 -b:v ${config.bitrate} -vf scale=${config.resolution} -r ${config.frameRate} -cpu-used ${commonWebMOptions.cpuUsed} -tile-columns ${config.tileColumns} ${commonWebMOptions.noAudio} ${commonWebMOptions.noMetadata}`
return [
`${baseCommand} -pass 1 -f webm /dev/null`,
`${baseCommand} -pass 2 ${config.name}.webm`,
]
}
function generateMP4FFmpegCommands(input: string): string[] {
const baseCommand = `ffmpeg -i "${input}" -c:v libx264 -b:v 800k -maxrate 1000k -bufsize 2000k -crf 25 -preset veryslow -an -movflags +faststart -vf "scale=1200:720:force_original_aspect_ratio=decrease" -color_primaries bt709 -color_trc bt709 -colorspace bt709`
return [
`${baseCommand} -pass 1 -f mp4 /dev/null`,
`${baseCommand} -pass 2 fallback.mp4`,
]
}
function generateHTMLBoilerplate(): string {
const sources = configs
.map(
(config) =>
` <source src="${config.name}.webm" type="video/webm" media="${config.mediaQuery}">`
)
.join('\n')
return `
<video autoplay loop muted playsinline disablepictureinpicture>
${sources}
<source src="fallback.mp4" type="video/mp4">
</video>
`
}
function main() {
const inputFile = process.argv[2]
// Validate input
if (!inputFile) {
console.error('Usage: tsx convert-m2w.ts <input.mp4>')
process.exit(1)
}
if (!existsSync(inputFile)) {
console.error(`Input file "${inputFile}" does not exist.`)
process.exit(1)
}
if (extname(inputFile).toLowerCase() !== '.mp4') {
console.error('Input file must be an .mp4.')
process.exit(1)
}
console.log(
`Converting "${inputFile}" to xs.webm, md.webm, lg.webm, and fallback.mp4...`
)
// Convert WebM files for each breakpoint
for (const config of configs) {
console.log(
`Creating ${config.name}.webm (${config.resolution} large resolution, ${config.bitrate}, ${config.frameRate}fps)...`
)
const [pass1, pass2] = generateWebMFFmpegCommands(inputFile, config)
runFFmpeg(pass1)
runFFmpeg(pass2)
}
// Convert fallback MP4
console.log(`Creating fallback.mp4 (1200x720, 800k, H.264)...`)
const [mp4Pass1, mp4Pass2] = generateMP4FFmpegCommands(inputFile)
runFFmpeg(mp4Pass1)
runFFmpeg(mp4Pass2)
// Generate and display completion message
const htmlBoilerplate = generateHTMLBoilerplate()
console.log(`
Conversion complete! Created:
- xs.webm (for xs: 0-959px)
- md.webm (for md: 960-1263px)
- lg.webm (for lg: 1264px+)
- fallback.mp4 (H.264 fallback)
Use this HTML in your Vuetify 2.x app:
${htmlBoilerplate}
`)
}
main()
@do-adams
Copy link
Copy Markdown
Author

do-adams commented Apr 11, 2025

Convert .mp4 to .webm with ffmpeg using Vuetify 2.x breakpoints

Overview

This TypeScript script, convert-m2w.ts, converts a high-resolution, silent .mp4 video into three optimized .webm files (xs.webm, md.webm, lg.webm) and one H.264 .mp4 fallback (fallback.mp4) tailored for Vuetify 2.x breakpoints:

  • xs (0-959px): For mobile devices.
  • md (960-1263px): For tablets and small desktops.
  • lg (1264px+): For large desktops.
  • fallback.mp4: For browsers like Safari that don’t fully support <source> media attributes or have .webm playback issues.

The .webm files are encoded with VP9 using two-pass FFmpeg for minimal size, optimized for a looping, audio-free animation (e.g., a homepage background). The .mp4 fallback uses H.264 for broad compatibility. The script outputs HTML boilerplate for a <video> element with responsive <source> tags.

Note: As of April 2025, Safari appears to ignore <source> elements with media attributes, falling back to the last <source> without a media query. Additionally, .webm files in Safari may freeze after a few loops during playback. The fallback.mp4 ensures reliable playback in Safari and other browsers with limited .webm or media support.

Features

  • Input: Accepts a silent .mp4 (e.g., 1200x720, H.264).
  • Outputs:
    • xs.webm: 640x384, 400k bitrate, 20fps (~300-600 KB).
    • md.webm: 960x576, 800k bitrate, 24fps (~600 KB-1.2 MB).
    • lg.webm: 1200x720, 1.5M bitrate, 24fps (~1-2 MB).
    • fallback.mp4: 1200x720, 800k bitrate, H.264 (~1-2 MB).
  • Optimization:
    • .webm: Two-pass VP9, scaled resolutions, reduced frame rates, no audio, stripped metadata.
    • .mp4: Two-pass H.264, CRF 25, veryslow preset, faststart for web, BT.709 color.
  • HTML Boilerplate: Generates <video autoplay loop muted playsinline disablepictureinpicture> with <source> tags:
    • xs.webm: (max-width: 959px)
    • md.webm: (min-width: 960px) and (max-width: 1263px)
    • lg.webm: (min-width: 1264px)
    • fallback.mp4: No media query for universal compatibility.
  • Error Handling: Validates input and catches FFmpeg errors.

Prerequisites

  • Node.js: v16+ recommended.

  • FFmpeg: Installed with H.264 and VP9 support.

    • macOS: brew install ffmpeg
    • Ubuntu: sudo apt-get install ffmpeg
    • Windows: Download from FFmpeg and add to PATH.

Usage

Note: If the script prompts you with "File '/dev/null' already exists. Overwrite? [y/N]", please enter 'y' to proceed.

  1. Save the Script:

    • Place convert-m2w.ts in your project directory.
  2. Make Executable (optional, Unix-like systems):

    chmod +x convert-m2w.ts
  3. Run the Script:

    npx tsx convert-m2w.ts input.mp4
    • Replace input.mp4 with your video file (must be .mp4).

    • Example:

      npx tsx convert-m2w.ts animation.mp4
  4. Output:

    • Creates xs.webm, md.webm, lg.webm, and fallback.mp4 in the current directory.

    • Prints a completion message with HTML boilerplate, e.g.:

      Conversion complete! Created:
      - xs.webm (for xs: 0-959px)
      - md.webm (for md: 960-1263px)
      - lg.webm (for lg: 1264px+)
      - fallback.mp4 (H.264 fallback)
      
      Use this HTML in your Vuetify 2.x app:
      
      <video autoplay loop muted playsinline disablepictureinpicture>
        <source src="xs.webm" type="video/webm" media="(max-width: 959px)">
        <source src="md.webm" type="video/webm" media="(min-width: 960px) and (max-width: 1263px)">
        <source src="lg.webm" type="video/webm" media="(min-width: 1264px)">
        <source src="fallback.mp4" type="video/mp4">
      </video>
      

Integration in Vuetify 2.x

  1. Add to Component:

    • Use the generated HTML in a Vuetify component, updating paths to match your project:

      <template>
        <v-container>
          <v-row>
            <v-col cols="12">
              <video autoplay loop muted playsinline disablepictureinpicture>
                <source src="/path/to/xs.webm" type="video/webm" media="(max-width: 959px)">
                <source src="/path/to/md.webm" type="video/webm" media="(min-width: 960px) and (max-width: 1263px)">
                <source src="/path/to/lg.webm" type="video/webm" media="(min-width: 1264px)">
                <source src="/path/to/fallback.mp4" type="video/mp4">
              </video>
            </v-col>
          </v-row>
        </v-container>
      </template>
      
      <style scoped>
      video {
        width: 100%;
        height: auto;
      }
      </style>

Testing

  1. Quality:

    • Test in Chrome, Firefox, Safari at:
      • xs: 320px, 600px, 959px
      • md: 960px, 1200px
      • lg: 1280px, 1920px
    • Check fallback.mp4 in Safari to ensure no freezing (unlike .webm).
  2. Responsive Loading:

    • Use DevTools’ Network tab to confirm:
      • Chrome/Firefox:
        • <960px: xs.webm
        • 960-1263px: md.webm
        • ≥1264px: lg.webm
      • Safari: Always fallback.mp4 (ignores media attributes).
    • Resize viewport to verify switches in non-Safari browsers.

Customization

  • Adjust Settings:
    • Edit configs in convert-m2w.ts to change:
      • resolution (e.g., 720:432 for xs).
      • bitrate (e.g., 300k for smaller xs).
      • frameRate (e.g., 15 for xs).
    • Modify generateMP4FFmpegCommands for different .mp4 settings (e.g., -crf 23).

Notes

  • Safari Behavior:
    • Safari (as of April 2025) ignores <source> media attributes, using the last non-media <source> (fallback.mp4).
    • .webm files may freeze after a few loops in Safari, making fallback.mp4 essential for reliability.
  • Assumptions:
    • Input .mp4 is silent and high-quality (e.g., 1200x720).
    • Video scales to viewport width with 5:3 aspect ratio.
    • FFmpeg and npm are installed.
  • Limitations:
    • Hardcoded .mp4 fallback name (fallback.mp4); adjust if dynamic naming is needed.
    • Assumes modern browser support for VP9 except in fallback cases.
  • Maintenance:
    • Monitor Safari updates for media attribute support.
    • Update mediaQuery in configs if Vuetify breakpoints change.

License

MIT License. Feel free to use and modify.


Created for a Pull Request to optimize a homepage animation in a Vuetify 2.x app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment