dereuromark/media-embed
A PHP library for embedding media from YouTube, Vimeo, Spotify, and many other providers. It converts URLs into responsive, privacy-friendly embed code with lazy loading and no JavaScript dependency, making it easy to add rich media to any Laravel or PHP application.
dereuromark/media-embed is a framework-agnostic PHP library, not a Laravel package. There is no service provider, no auto-discovery, no publishable config — you composer require dereuromark/media-embed and instantiate MediaEmbed\MediaEmbed yourself. That is the whole install.
First place to look is src/MediaEmbed.php (the provider registry and parseUrl() / parseId() / parseText() entry points) and the provider definitions it ships with — that array is your source of truth for which services are supported and what config keys a provider actually accepts in your installed version.
A first working call in php artisan tinker:
$engine = new MediaEmbed\MediaEmbed();
$media = $engine->parseUrl('https://www.youtube.com/watch?v=9bZkp7q19f0');
$media?->name(); // "Youtube"
$media?->id(); // "9bZkp7q19f0"
$media?->slug(); // "youtube"
$media?->getEmbedUrl(); // protocol-relative embed URL
$media?->getEmbedCode();// ready-made <iframe> HTML
$media?->getImage(); // thumbnail URL, if the provider defines one
The first real use case is almost always the same: a video_url column on a Post/Product/Page model, rendered as an embed on the public show page. Bind the engine as a singleton, wrap it in one thin service, and never touch the library directly from controllers or Blade.
One service, one binding. Wrap the engine so custom providers and defaults live in one place:
// app/Services/MediaEmbedService.php
class MediaEmbedService
{
protected MediaEmbed $engine;
public function __construct(array $customProviders = [])
{
$this->engine = new MediaEmbed();
foreach ($customProviders as $name => $config) {
$this->engine->addProvider($name, $config);
}
}
public function parse(?string $url): ?MediaObject
{
return $url ? ($this->engine->parseUrl($url) ?: null) : null;
}
}
// AppServiceProvider::register()
$this->app->singleton(MediaEmbedService::class, fn () => new MediaEmbedService(
config('media.providers', [])
));
Blade component with a graceful fallback. Never assume parseUrl() succeeds:
@if ($media)
<div class="aspect-video [&>iframe]:h-full [&>iframe]:w-full">
{!! $media->getEmbedCode() !!}
</div>
@else
<a href="{{ $url }}" rel="noopener nofollow">{{ $url }}</a>
@endif
Pass a width/height into the component and call $media->setWidth() / setHeight() before rendering, so the same engine serves a 640px article body and a 1280px hero.
Persist the parse, don't repeat it. Parsing runs regexes across every provider on every call. On save, store provider, media_id, and thumbnail on the row (or a media_meta JSON cast) and read those back on render. Keep a re-parse path (php artisan media:refresh) for when the library adds providers.
A validation rule for the form request. This is the highest-leverage five lines in the integration — it stops editors pasting a YouTube channel URL and getting an empty box:
class EmbeddableUrl implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! app(MediaEmbedService::class)->parse($value)) {
$fail('The :attribute must be a supported video URL.');
}
}
}
Custom provider for your own CDN or a privacy-friendly variant. The regex needs a named id group or nothing resolves:
// config/media.php
'providers' => [
'youtube_nocookie' => [
'name' => 'YoutubeNoCookie',
'website' => 'https://www.youtube-nocookie.com',
'url' => 'https://www.youtube.com/watch?v={id}',
'match' => '#https?://(?:www\.)?youtube\.com/watch\?v=(?P<id>[\w-]+)#i',
'embed' => 'https://www.youtube-nocookie.com/embed/{id}',
'image' => 'https://i.ytimg.com/vi/{id}/hqdefault.jpg',
'width' => 640,
'height' => 360,
],
],
Bulk rendering of user text (comments, markdown bodies): parseText() exists in recent releases for exactly this. If your pinned version lacks it, loop preg_match_all over URLs and replace each hit with getEmbedCode(). Either way, run the result through your HTML sanitizer afterwards — see below.
parseUrl() returns null, not an exception. Every call site needs ?-> or an explicit check. Silent nulls are the #1 source of "the video just doesn't show up" bug reports.//www.youtube.com/embed/...). Fine over HTTPS, broken in file:// previews, mail clients, and a few CSP/proxy setups. Normalize to https: in your wrapper if you render outside a browser context.getEmbedCode() returns raw HTML and does not escape interpolated IDs. Provider configs with a permissive match regex plus a user-supplied URL are an injection surface. Output with {!! !!} only for engine-generated markup from providers you control, and run user-submitted embeds through a whitelist sanitizer as defense in depth.aspect-ratio/aspect-video container and make the iframe w-full h-full.youtu.be/xyz, /shorts/, m. subdomains, and ?t=90 timestamps are handled inconsistently across providers and versions. Test your actual corpus, and expect to append the t= start time manually to the embed URL after parsing.addProvider() mutates the engine. Register custom providers once in the container binding (as above); calling addProvider() inside a request handler on a singleton will leak or duplicate entries across worker requests.config/media.php and cache it — don't inline arrays in controllers, or you'll have three slightly different YouTube configs within a quarter."dereuromark/media-embed": "^2.0" or whatever your installed major is), keep the MediaEmbedService adapter thin, and expose only name(), id(), getEmbedUrl(), getEmbedCode(), getImage() from it. If the package stalls, swapping to hand-rolled oEmbed or a hosted embed service becomes a one-file change.youtube-nocookie provider above or render a thumbnail-plus-click-to-load facade using getImage() — the library gives you the thumbnail, which is all a facade needs.preg_match directly against the failing URL before blaming the library — the regex is the fragile part, not the parser.How can I help you explore Laravel packages today?