eminos/statamic-video-tools
Statamic Video Tools is an add-on for embedding and managing videos in your Statamic site. It provides video fieldtypes, responsive embed output, and handy template helpers for YouTube, Vimeo, and other sources, making video content easy to add and display.
Confirm the stack first. This is a Statamic addon, so you need Statamic 4/5 on Laravel 10/11, PHP 8.2+, and — critically — ffmpeg/ffprobe binaries on the server. whisper.cpp plus a model file is optional but required for transcription. Without ffmpeg the addon will appear installed and then fail at conversion time.
A real queue is mandatory. Encoding, poster extraction, and transcription are queued jobs. QUEUE_CONNECTION=sync will time out on any non-trivial video. Set up Redis/database queues and run a worker (ideally a dedicated one, see Gotchas).
Install order:
composer require the package.vendor:publish — check DOCUMENTATION.md for the exact tag) and read config/video-tools.php end to end. This file is the package: presets, conditions, binaries, paths, and transcription settings all live here.assets/videos.yaml → taxonomies/fields section). The CP status field with polling and re-run buttons only appears once the fieldtype is on the container.Where to look first: DOCUMENTATION.md for the tag/component API, then the presets array in the config, then the published fieldtype in your blueprint. Don't start with the source — the preset config tells you what actually happens to a file.
First use case to validate the whole pipeline: upload a short H.264 MP4 into the configured container, watch the CP status field go through its states, confirm the poster JPEG appears, then render it in a template with the self-closing tag:
{{ video :src="video_file" preset="h264" }}
If that renders a <video> element with a working poster and source, the install is sound. Everything else (HLS, AV1, transcription, alpha) is incremental.
Self-closing vs pair mode. Self-closing renders the full <video> element for you — use it in Bard sets, listing pages, and anywhere you don't need control. Pair mode gives you the markup so you can add class, data-*, playsinline, poster, captions, and lazy-loading attributes yourself:
{{ video :src="video_file" preset="hls" }}
<video controls playsinline preload="metadata" poster="{{ poster }}">
{{ sources }}
{{ tracks }}
</video>
{{ /video }}
Remember that in pair mode the alpha dual-source output and the VTT track are your responsibility to loop over — the tag won't inject them for you.
Prefer <x-video> in Blade-heavy projects. Mixing Antlers tags into Blade views works via the Statamic runtime, but the Blade component keeps prop typing, IDE completion, and @aware/slot semantics intact. Standardise on one of the two per project rather than mixing.
Treat presets as named contracts. Reference presets by name (preset="h264") and keep the name stable across environments even if the underlying ffmpeg args differ. Configure a ladder: h264 as the universal fallback, av1/webm for modern browsers, hevc where you control the client, and hls for anything over a couple of minutes. Order sources so the broadest-compatibility format sits last as the fallback.
Preset conditions are the main cost-control lever. Skipping AV1 for sources under 720p, or skipping HLS for clips under 60 seconds, saves enormous CPU. Conditions can key off resolution, duration, codec, pixel format, and alpha — put the cheap checks (duration, resolution) first and the probe-heavy ones later. Conditions are evaluated per asset per preset, so a misconfigured condition silently doubles your queue volume.
Alpha needs a strategy, not just a flag. Transparent sources generate dual output (HEVC/MP4 for Safari, AV1/WebM for Chrome/Firefox). In self-closing mode this is handled. In pair mode you must render both <source> elements in the right order, and you should document internally why there are two files per asset so nobody "cleans up" the duplicate.
Wire the events into your existing cache strategy. VideoAssetProcessed fires when the asset is ready; VideoConversionCompleted fires per conversion. Register listeners for:
StaticCache::flush() or a targeted clear) — otherwise your rendered tag points at a URL that changed after re-encode.Implement ShouldQueue on these listeners so a slow webhook doesn't stall the conversion worker.
Transcription as a content source, not just a caption file. Whisper output gives you VTT, SRT, TXT, formatted paragraphs, and word-level JSON. The practical pattern is: VTT into {{ tracks }} for captions, TXT/paragraphs into a hidden or collapsed transcript section for SEO and accessibility, and the word-level JSON into search indexing or chapter generation. Grab the transcript in the same event listener rather than re-reading files at render time.
Re-run buttons are an editorial workflow. The CP fieldtype's per-preset re-run is what you'll use after changing a preset's args — but it re-encodes in place. Pair it with your event listeners so caches clear automatically, and communicate to editors that re-running is safe but slow.
Environment-specific config. Publish the config and override per environment: local can use a single fast h264 preset with transcription disabled; production gets the full ladder. Never let local dev spin up AV1 encodes of a 4K upload.
PATH differs from your shell's. Use absolute paths in the config and verify with the exact user the queue worker runs as.libsvtav1 or libaom-av1; HEVC may be absent or licence-restricted depending on your distro's build. Test each preset once manually with the same arguments before blaming the package.--timeout above your slowest job and retry_after above timeout, or jobs get double-processed. Consider a dedicated queue connection for video with a single worker so encodes don't starvation-block mail and webhook jobs..m3u8 and .ts/.m4s segments must be served with correct MIME types and, for segments, byte-range support. If the player stalls at "loading" while the files clearly exist, it's almost always the server config, not the package.whisper.cpp binary or model file typically means no transcript rather than a hard error. Check storage/logs and the failed-jobs table before assuming the feature is broken.tiny is fine for search indexing, medium/large for published captions). Running it on the same worker as ffmpeg will thrash — separate queues, or accept serialised processing.ffprobe misses. Spot-check the dual-source output on a known transparent file rather than trusting the condition.StaticCache::flush() in VideoConversionCompleted on a batch import will flush dozens of times in a minute. Debounce, or flush once on VideoAssetProcessed instead.resources/views/vendor/ let you restyle the CP fieldtype and the component's markup without forking. Publish config and views rather than editing vendor code — the v1.0.0 surface is broad enough that you shouldn't need to patch the package itself.How can I help you explore Laravel packages today?