Skip to content
Deep Dive

Statamic's video field grew up

Since October, Statamic’s video field recognizes the provider and video ID of a link, and it supports Cloudflare Stream. The release notes cover this in a few short lines, but behind them is one large contribution, split into five pieces, three of which shipped.

· 9 min read · Thomas Sausen

Between the monthly posts

Statamic's October roundup covers 6.36 to 6.38 and the changes you can see in the control panel: dragging assets into folders, Live Preview before the first save, reordering sets with the keyboard. A monthly post can't show a change that started long before the month and only partly shipped.

The video fieldtype is that change. Between 6.32 on September 9 and 6.38 on October 23, it went from a plain-text URL input to a field that recognizes the provider and the video ID. It can also build the embed URL, with or without tracking. Cloudflare Stream joined YouTube and Vimeo. The release notes show six short entries across four releases. Behind them is a single pull request that grew too large to merge in one go.

Where it started

For most of its life, the video fieldtype stored a string and returned that string. Everything clever happened in modifiers. The embed_url modifier turned a YouTube or Vimeo page link into a player URL, and trackable_embed_url did the same without the privacy tweaks. A third one, is_embeddable, told you whether to reach for an iframe or a <video> tag. Statamic's own template scaffolding still renders the field that way:

{{ if video | is_embeddable }}
    <iframe src="{{ video | embed_url }}"></iframe>
{{ else }}
    <video src="{{ video | embed_url }}"></video>
{{ /if }}

That works, and it keeps working. But the field itself knew nothing about its content. If you wanted the YouTube ID for a thumbnail or a click-to-load facade, you parsed the URL yourself.

One pull request, five pieces

It began with statamic/ideas#1336, a request to support Cloudflare Stream in the video fieldtype. Erin Dalzell picked it up in #11871, which grew well beyond that: a provider dropdown, a cloudflare:<id> storage format, a lookup endpoint, and oEmbed support for a long list of providers through the Embera library. By the end, it touched 17 files and roughly 1,000 lines.

A change that size raises more questions, and Jason Varga's review worked through them. Some were about the control panel: how provider markup gets there, and what happens when lookups overlap while someone types. Others were about how Embera handles network requests and what templates should receive for each provider.

Erin addressed them and then split the pull request into five, so each part could be reviewed and shipped on its own:

  1. #15456: debounce field updates. Merged, shipped in 6.33.

  2. #15457: render direct video files in a <video> element. Closed.

  3. #15458: the Embed value object. Merged, shipped in 6.35.

  4. #15459: Cloudflare Stream. Merged, shipped in 6.38.

  5. #15460: oEmbed lookups through Embera. Closed.

The split had a practical rationale. Cloudflare Stream needed no new dependency; only the oEmbed piece did. So whether every Statamic install should pull in Embera no longer blocked everything else.

Four releases, six weeks

Release

Date

What changed

6.32.0

Sep 9

Assets treat .mpeg, .mpg and .mkv as video (#15378)

6.33.0

Sep 16

The field input is debounced (#15456)

6.35.0

Oct 1

Augmentation to an Embed value object (#15458), video ID and Vimeo privacy hash (#15553), a method-based API with trackable embed URLs (#15554)

6.38.0

Oct 23

Cloudflare Stream support (#15459)

The 6.35 entries need a word. Jason followed #15458 with #15554 about twelve hours later, before the release went out. The object had been built from five constructor arguments with public readonly properties ($video->provider). Now it's built from the value alone and exposes methods ($video->provider()). Only the provider is worked out up front, everything else on demand, which leaves room for providers that are expensive to resolve. The same pull request fixed trackable_embed_url, which still ran its own YouTube and Vimeo rewriting on the raw URL and would have produced broken output for any other provider.

The 6.32 change is a separate contribution and easy to overlook. Since 6.35, though, the field decides whether a URL points to a video file by checking it against that same extension list. The debounce in 6.33 means the field no longer pushes an update on every keystroke while you type or paste a URL.

One loose end: the control panel preview still only recognizes .mp4, .ogv, .mov and .webm. A .mkv or .m4v link is treated as a video file in your templates but gets no preview in the entry.

What your templates get now

Since 6.35, a video field augments to Statamic\Fieldtypes\Video\Embed. It extends ArrayableString and casts back to the URL the editor entered, so {{ video }}, {{ video ?? 'fallback' }}, and existing modifier chains behave as before.

Keeping that promise took a few rounds. The review compared the branch with 6.x and found cases where the object and the plain string behaved differently, for example in empty fields or with trackable URLs. Instead of fixing these cases one by one, the pull request added a ParityTest. It checks the augmented object against the raw string for every modifier and every kind of value. Cleared fields in localizations store an empty string, and Antlers fallbacks like ?? and or keep working there because Embed extends ArrayableString.

In Antlers, the object exposes five keys:

Key

Value

video:provider

youtube, vimeo, cloudflare, file, or unsupported

video:id

The provider's video ID

video:privacy_hash

The hash of an unlisted Vimeo video

video:embed_url

The privacy-friendly player URL

video:url

What the editor entered

In Blade or PHP you call methods: provider(), id(), privacyHash(), embedUrl(), url(), plus trackableEmbedUrl(), isEmbeddable() and isSupported(). Those last three aren't in the Antlers array. There you keep using trackable_embed_url and is_embeddable, which now hand off to the object.

The ID is the most useful addition for templates. A click-to-load YouTube facade, for example, no longer needs a helper of its own:

{{ if video:provider == 'youtube' }}
    <lite-youtube videoid="{{ video:id }}"></lite-youtube>
{{ /if }}

The ID parser is stricter than you might expect. The tests cover mobile links, youtu.be with timestamps, embed and nocookie URLs, links without a scheme, and they return null for malformed IDs such as watch?v=x"><script>. That's the kind of input you'd otherwise have to guard against in your own code.

The privacy split hasn't changed, but it now lives in one place. embed_url uses youtube-nocookie.com and adds dnt=1 for Vimeo. The trackable version leaves both out. One inconsistency is pinned in the tests: for a youtube.com/watch link with &t=2, embed_url converts the timestamp to start=2, while the trackable URL keeps t=2. If you rely on start times with trackable embeds, check the output.

Cloudflare Stream

Since 6.38, the field accepts Cloudflare Stream URLs on your customer subdomain, customer-<code>.cloudflarestream.com. A bare video URL, an /iframe link, and a /watch link all resolve to the iframe player. Player options in the query string, such as autoplay, muted, or poster, are kept and re-encoded. On a watch link they're dropped, since they only apply to the iframe.

The first version looked different: a dropdown to choose between "URL" and "Cloudflare Stream", and a separate ID input that stored cloudflare:<id>. Jason asked whether Cloudflare offered a URL to paste instead, since "URL" already covered YouTube, Vimeo, and video files. Erin, who uses Stream, came back with the URL formats, and the dropdown went. The final version detects the watch link behind the big blue button on a video page in the Cloudflare dashboard.

Manifest URLs were left out on purpose. They overlap with plain video file URLs, and treating them as Cloudflare URLs could have changed how existing sites render them.

The edge cases are handled deliberately:

  • A download link (/downloads/default.mp4) counts as a file.

  • HLS and DASH manifest URLs are rejected, and the control panel tells editors to use the URL from the Video Link button in Cloudflare instead.

  • Thumbnail URLs and links without a customer subdomain are unsupported.

  • trackableEmbedUrl() returns the same player URL, because Cloudflare has no tracking-free variant to switch between.

The test cases cover whatever editors are likely to paste from Cloudflare's dashboard, plus a few hostile inputs: HTML in the query string, a path traversal in the ID, a trailing newline. All of them end up either encoded or unsupported.

What didn't ship

Two of the five pieces didn't make it in: rendering direct video files in a <video> element (#15457) and oEmbed lookups for providers like Loom, Wistia, and TikTok (#15460). The oEmbed version was worked out in detail, but it would have added a dependency to every install for something nobody had specifically asked for, and Erin closed it.

Because the Embed class computes everything except the provider on demand, a provider that needs a lookup can still be added later, in core or as an addon.

Before you update

If your templates only output the field or run it through modifiers, there's nothing to do. Two things are worth a quick search through your own code:

  • PHP that checks the augmented value with is_string() or compares it strictly with ===. It's an object now. Cast it with (string) or call url().

  • Self-hosted .mkv or .m4v links. They work in your templates as file, but editors won't see a preview in the control panel.

The REST API is unaffected and still returns the plain URL string. An early version of #15458 serialized the whole object, which the review caught before it shipped.

Sources

Author

Thomas Sausen is a freelance web developer and the editor of Flatbard. He builds with Statamic and writes about the surrounding ecosystem.