Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Media Embed Laravel Package

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.

Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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.

Gotchas and Tips

  • 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.
  • Embed URLs are protocol-relative (//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.
  • Width/height are HTML attributes, not layout. The defaults (~640×360) will blow out a mobile column. Always wrap in an aspect-ratio/aspect-video container and make the iframe w-full h-full.
  • URL variants are lossy. 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.
  • Singleton state under Octane. 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.
  • No config file means your config is your config. Keep custom providers in config/media.php and cache it — don't inline arrays in controllers, or you'll have three slightly different YouTube configs within a quarter.
  • Maintenance risk is real. 176 stars, a single maintainer, and sporadic releases. Pin the version ("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.
  • GDPR/privacy. Stock YouTube embeds set cookies on page load. If you have EU traffic, either register the 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.
  • Debugging: dump the registered provider names to confirm your custom entry loaded, and test preg_match directly against the failing URL before blaming the library — the regex is the fragile part, not the parser.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console