Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
A utility library that generates HTML embed tags for audio or video located on a given URL.
It also parses and validates given media URLs.

It currently works with [~42 services](docs/supported.md), including the most important ones like
It currently works with [40+ services](docs/supported.md), including the most important ones like
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

- YouTube
- Vimeo
Expand All @@ -20,7 +20,7 @@ It currently works with [~42 services](docs/supported.md), including the most im

etc. With community driven updates this aims to be a complete and up-to-date service wrapper lib.

All providers use modern iframe embeds (Flash support was removed as Flash was discontinued in 2020).
Providers use modern iframe embeds or provider-generated oEmbed HTML (Flash support was removed as Flash was discontinued in 2020).

## Demo
https://sandbox.dereuromark.de/sandbox/media-embed
Expand Down
57 changes: 56 additions & 1 deletion data/stubs.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
* - "embed-width" (string) Width of the embed (in pixels)
* - "embed-height" (string) Height of the embed (in pixels)
* - "image-src" (string) For preview thumbnail image
* - "iframe-player" (string) If an <iframe> player is available
* - "iframe-player" (string) If an <iframe> player is available (or use "oembed")
* - "oembed" (string) oEmbed endpoint for provider-generated HTML (or use "iframe-player")
* - "id" (string) The ID of the video, returned as $id
*/
[
Expand Down Expand Up @@ -61,6 +62,60 @@
'embed-height' => '295',
'iframe-player' => '//www.facebook.com/plugins/video.php?href=$1&show_text=0',
],
[
'name' => 'Tumblr',
'website' => 'https://www.tumblr.com',
'status' => 'active',
'category' => 'social',
'example-url' => 'https://staff.tumblr.com/post/822057428507049984/in-case-youre-looking-for-a-blog-thats-gone',
'url-match' => [
'https?://[a-z0-9-]+\\.tumblr\\.com/post/([0-9]+)(?:/[^?#]+)?',
'https?://www\\.tumblr\\.com/[a-z0-9-]+/([0-9]+)(?:/[^?#]+)?',
],
'embed-width' => 540,
'embed-height' => 600,
'oembed' => 'https://www.tumblr.com/oembed/1.0',
'notes' => 'Uses oEmbed HTML because Tumblr post embeds require provider-generated markup.',
],
[
'name' => 'Bluesky',
'website' => 'https://bsky.app',
'status' => 'active',
'category' => 'social',
'example-url' => 'https://bsky.app/profile/bsky.app/post/3mbhel6ij7s2y',
'url-match' => 'https://bsky\\.app/profile/([^/?#]+)/post/([^/?#]+)',
'embed-width' => 600,
'embed-height' => 600,
'oembed' => 'https://embed.bsky.app/oembed',
'id' => '$2/$3',
'notes' => 'Uses provider-generated oEmbed HTML for social posts.',
],
[
'name' => 'Flickr',
'website' => 'https://www.flickr.com',
'status' => 'active',
'category' => 'social',
'example-url' => 'https://www.flickr.com/photos/bees/2341623661/',
'url-match' => 'https?://(?:www\\.)?flickr\\.com/photos/([^/?#]+)/([0-9]+)(?:/[^?#]*)?',
'embed-width' => 1024,
'embed-height' => 768,
'oembed' => 'https://www.flickr.com/services/oembed/',
'id' => '$2/$3',
'notes' => 'Supports provider-generated oEmbed markup for photos and videos.',
],
[
'name' => 'Speaker Deck',
'website' => 'https://speakerdeck.com',
'status' => 'active',
'category' => 'social',
'example-url' => 'https://speakerdeck.com/speakerdeck/introduction-to-speakerdeck',
'url-match' => 'https?://speakerdeck\\.com/([^/?#]+)/([^/?#]+)',
'embed-width' => 710,
'embed-height' => 399,
'oembed' => 'https://speakerdeck.com/oembed.json',
'id' => '$2/$3',
'notes' => 'Uses provider-generated oEmbed HTML for presentations.',
],
[
'name' => 'Dailymotion',
'website' => 'https://www.dailymotion.com',
Expand Down
37 changes: 35 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,8 @@ $config = new ProviderConfig(
urlMatch: ['regex...'], // Required: URL patterns (array or string)
embedWidth: 640, // Required: Default width
embedHeight: 360, // Required: Default height
iframePlayer: '//.../$2', // Required: Iframe URL template
iframePlayer: '//.../$2', // Iframe URL template (or use oEmbed)
oEmbed: 'https://.../oembed', // oEmbed endpoint (or use iframePlayer)
status: 'active', // Optional: active, legacy, deprecated
category: 'video', // Optional: video, audio, social, streaming, 3d
exampleUrl: 'https://...', // Optional: fixture/example URL
Expand All @@ -357,7 +358,8 @@ For array-based configs (legacy format):
- **url-match** (required): Array of regex patterns to match URLs
- **embed-width** (required): Default width in pixels or as percentage
- **embed-height** (required): Default height in pixels or as percentage
- **iframe-player** (required): URL template for iframe embedding
- **iframe-player**: URL template for iframe embedding; either this or `oembed` is required
- **oembed**: Provider oEmbed endpoint; either this or `iframe-player` is required
- **status**: Optional provider lifecycle status (`active`, `legacy`, `deprecated`)
- **category**: Optional content category (`video`, `audio`, `social`, `streaming`, `3d`)
- **example-url**: Optional example URL covered by release fixture tests
Expand Down Expand Up @@ -536,6 +538,7 @@ $subset = $MediaEmbed->getProviders(['youtube', 'vimeo', 'dailymotion']);

// Filter by capabilities
$withIframe = $providers->withIframeSupport();
$withOEmbed = $providers->withOEmbedSupport();
$withThumbnails = $providers->withThumbnailSupport();
$active = $providers->withStatus('active');
$video = $providers->withCategory('video');
Expand Down Expand Up @@ -596,6 +599,36 @@ $thumb = $mediaEmbed->thumbnail($mediaObject); // string|null

`oEmbed()` returns null when the provider has no registered endpoint or the object was created via `parseId()` (no source URL). These calls perform an HTTP request, so a PSR-16 cache (and the injected HTTP client) apply.

For providers whose embed can only be generated dynamically, `iframe-player` may be omitted when an `oembed` endpoint is configured. Use `oEmbedHtml()` to fetch the provider-generated markup:

```php
$mediaObject = $mediaEmbed->parseUrl('https://social.example.com/posts/123');
$html = $mediaEmbed->oEmbedHtml($mediaObject);

// The HTML comes from the remote provider. Only render trusted provider output,
// or sanitize it according to your application's policy.
echo $html;
```

Calling the iframe-specific `MediaObject::getEmbedCode()` or `getEmbedSrc()` methods for an oEmbed-only provider throws `EmbedCodeUnavailableException` with this alternative. This avoids silently returning unusable markup.

Custom oEmbed-only providers can be registered inline without modifying the bundled provider file:

```php
$mediaEmbed = new MediaEmbed([
'custom_providers' => [[
'name' => 'Example Social',
'website' => 'https://social.example.com',
'url-match' => 'https://social\\.example\\.com/posts/([0-9]+)',
'embed-width' => 540,
'embed-height' => 600,
'oembed' => 'https://social.example.com/oembed',
]],
]);
```

The same provider array can be loaded from PHP or JSON through `providers_config`, `ArrayLoader`, `PhpFileLoader`, or `JsonFileLoader`. This keeps application-specific providers outside the package so they can be updated independently.

### oEmbed Discovery

For URLs not covered by built-in providers, use oEmbed auto-discovery:
Expand Down
26 changes: 15 additions & 11 deletions docs/supported.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Supported Media Services

42 services (41 active, 1 legacy)
46 services (45 active, 1 legacy)

Provider example URLs are covered by the release fixture matrix in `tests/Fixture/provider_urls.php`.

Expand All @@ -9,23 +9,25 @@ Provider example URLs are covered by the release fixture matrix in `tests/Fixtur
| [Aparat](https://www.aparat.com) | active | video | iframe | |
| [Apple Music](https://music.apple.com) | active | audio | iframe | |
| [Apple Podcasts](https://podcasts.apple.com) | active | audio | iframe | |
| [Audiomack](https://audiomack.com) | active | audio | iframe | |
| [Audiomack](https://audiomack.com) | active | audio | iframe, oEmbed | |
| [Bandcamp](https://bandcamp.com) | active | audio | iframe | |
| [Bilibili](https://www.bilibili.com) | active | video | iframe | |
| [Bilibili (Legacy)](https://www.bilibili.com) | legacy | video | iframe | Legacy URL format kept for existing links. |
| [BitChute](https://www.bitchute.com) | active | video | iframe | |
| [Bluesky](https://bsky.app) | active | social | oEmbed | Uses provider-generated oEmbed HTML for social posts. |
| [Coub](https://coub.com) | active | video | iframe | |
| [Dailymotion](https://www.dailymotion.com) | active | video | iframe, thumbnail | |
| [Deezer](https://www.deezer.com) | active | audio | iframe | Music content only (track, album, playlist, artist); podcasts are not supported. |
| [Facebook](https://www.facebook.com) | active | social | iframe | |
| [Flickr](https://www.flickr.com) | active | social | oEmbed | Supports provider-generated oEmbed markup for photos and videos. |
| [Giphy](https://giphy.com) | active | video | iframe, thumbnail | |
| [Instagram](https://www.instagram.com) | active | social | iframe | |
| [Kick](https://kick.com) | active | streaming | iframe | |
| [Loom](https://loom.com) | active | video | iframe | |
| [Mastodon](https://joinmastodon.org) | active | social | iframe | Federated: matches any instance host serving the /@user/id status format. |
| [Matterport](https://matterport.com) | active | 3d | iframe | |
| [Metatube](https://www.metatube.com) | active | video | iframe | |
| [Mixcloud](https://www.mixcloud.com) | active | audio | iframe | |
| [Mixcloud](https://www.mixcloud.com) | active | audio | iframe, oEmbed | |
| [Niconico](https://www.nicovideo.jp) | active | video | iframe | |
| [Odysee](https://odysee.com) | active | video | iframe | |
| [PeerTube](https://joinpeertube.org) | active | video | iframe | |
Expand All @@ -34,17 +36,19 @@ Provider example URLs are covered by the release fixture matrix in `tests/Fixtur
| [Rumble](https://rumble.com) | active | video | iframe | |
| [RuTube](https://www.rutube.ru) | active | video | iframe | |
| [Screencast](https://www.screencast.com) | active | video | iframe, fetch | |
| [Sketchfab](https://sketchfab.com) | active | 3d | iframe | |
| [SoundCloud](https://soundcloud.com) | active | audio | iframe | |
| [Spotify](https://open.spotify.com) | active | audio | iframe | |
| [Spreaker](https://www.spreaker.com) | active | audio | iframe | Episode URLs only; show pages expose no numeric ID in the URL. |
| [Sketchfab](https://sketchfab.com) | active | 3d | iframe, oEmbed | |
| [SoundCloud](https://soundcloud.com) | active | audio | iframe, oEmbed | |
| [Speaker Deck](https://speakerdeck.com) | active | social | oEmbed | Uses provider-generated oEmbed HTML for presentations. |
| [Spotify](https://open.spotify.com) | active | audio | iframe, oEmbed | |
| [Spreaker](https://www.spreaker.com) | active | audio | iframe, oEmbed | Episode URLs only; show pages expose no numeric ID in the URL. |
| [Streamable](https://streamable.com) | active | video | iframe | |
| [TED](https://www.ted.com) | active | video | iframe | |
| [TikTok](https://www.tiktok.com) | active | social | iframe | |
| [TED](https://www.ted.com) | active | video | iframe, oEmbed | |
| [TikTok](https://www.tiktok.com) | active | social | iframe, oEmbed | |
| [Tumblr](https://www.tumblr.com) | active | social | oEmbed | Uses oEmbed HTML because Tumblr post embeds require provider-generated markup. |
| [Twitch Clip](https://clips.twitch.tv) | active | streaming | iframe | |
| [Twitch Video](https://www.twitch.tv) | active | streaming | iframe | |
| [Twitter](https://twitter.com) | active | social | iframe | |
| [Vimeo](https://www.vimeo.com) | active | video | iframe | |
| [Vimeo](https://www.vimeo.com) | active | video | iframe, oEmbed | |
| [Vooplayer](https://vooplayer.com/) | active | video | iframe | |
| [Wistia](https://www.wistia.com) | active | video | iframe | |
| [YouTube](https://www.youtube.com) | active | video | iframe, thumbnail, timestamp | |
| [YouTube](https://www.youtube.com) | active | video | iframe, oEmbed, thumbnail, timestamp | |
8 changes: 7 additions & 1 deletion src/Docs/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,13 @@ protected function name(array $array): string {
* @return string
*/
protected function capabilities(array $array): string {
$capabilities = ['iframe'];
$capabilities = [];
if (!empty($array['iframe-player'])) {
$capabilities[] = 'iframe';
}
if (!empty($array['oembed'])) {
$capabilities[] = 'oEmbed';
}

if (!empty($array['image-src'])) {
$capabilities[] = 'thumbnail';
Expand Down
22 changes: 22 additions & 0 deletions src/Exception/EmbedCodeUnavailableException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace MediaEmbed\Exception;

/**
* Exception thrown when a provider has no static iframe renderer.
*/
class EmbedCodeUnavailableException extends MediaEmbedException {

/**
* @param string $providerSlug Provider slug.
*/
public function __construct(string $providerSlug) {
parent::__construct(sprintf(
'Provider "%s" has no iframe embed. Use MediaEmbed::oEmbedHtml() for oEmbed-only providers.',
$providerSlug,
));
}

}
24 changes: 22 additions & 2 deletions src/MediaEmbed.php
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,26 @@ public function oEmbed(MediaObject $object, ?int $maxWidth = null, ?int $maxHeig
return $this->oEmbedDiscovery()->fetch($endpoint, $maxWidth, $maxHeight);
}

/**
* Fetch raw embed HTML from a provider's oEmbed response.
*
* The returned HTML is controlled by the remote provider. Consumers must only
* render it for trusted providers, or sanitize it according to their policy.
*
* @param \MediaEmbed\Object\MediaObject $object Parsed media object.
* @param int|null $maxWidth Maximum embed width.
* @param int|null $maxHeight Maximum embed height.
* @return string|null
*/
public function oEmbedHtml(MediaObject $object, ?int $maxWidth = null, ?int $maxHeight = null): ?string {
$response = $this->oEmbed($object, $maxWidth, $maxHeight);
if ($response === null || !$response->hasHtml()) {
return null;
}

return $response->html;
}

/**
* Resolve a thumbnail URL for a parsed media object.
*
Expand Down Expand Up @@ -572,8 +592,8 @@ public function getProviders(array $whitelist = []): ProviderCollection {
*/
public function addProviderConfig(ProviderConfig $config, bool $override = false) {
$slug = $config->slug ?? $this->slug($config->name);
if ($config->iframePlayer === null || $config->iframePlayer === '') {
throw ProviderConfigException::missingField('iframe-player', $config->toArray());
if (!$config->hasIframeSupport() && !$config->hasOEmbedSupport()) {
throw ProviderConfigException::missingField('iframe-player or oembed', $config->toArray());
}
Comment thread
dereuromark marked this conversation as resolved.
Resolved

if (!$override && isset($this->providers[$slug])) {
Expand Down
4 changes: 4 additions & 0 deletions src/Object/MediaObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace MediaEmbed\Object;

use InvalidArgumentException;
use MediaEmbed\Exception\EmbedCodeUnavailableException;
use MediaEmbed\Template\TemplateResolver;

/**
Expand Down Expand Up @@ -549,6 +550,9 @@ public function isSourceResolved(): bool {
* @return string The unescaped src URL
*/
public function getEmbedSrc(): string {
if (empty($this->stub['iframe-player'])) {
throw new EmbedCodeUnavailableException($this->slug());
}
Comment on lines 552 to +555
$source = $this->templateResolver->resolve($this->stub['iframe-player'], $this->match);

return $this->appendQueryParams($source);
Expand Down
7 changes: 7 additions & 0 deletions src/Provider/ProviderCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ public function withIframeSupport(): self {
return $this->filter(fn (ProviderConfig $config) => $config->hasIframeSupport());
}

/**
* Get providers with oEmbed support.
*/
public function withOEmbedSupport(): self {
return $this->filter(fn (ProviderConfig $config) => $config->hasOEmbedSupport());
}

/**
* Get providers with thumbnail support.
*/
Expand Down
Loading
Loading