Uploaded videos render as a plain link card instead of the player (2.9 ff)
Since the update to 2.9, every video uploaded through the post editor shows up
as a generic link card β domain and file name, no player. YouTube embeds in the
same spaces are fine. This is not limited to old posts: a video uploaded today,
on 2.9.1, reproduces it immediately.
The cause is one clause added to isVideoMedia() in app.js; the section
When it started below has the version series it was measured against.
Environment
Three installations, all WordPress 7.1 / PHP 8.3:
| FluentCommunity | Pro | FluentPlayer | FluentPlayer Pro | |
|---|---|---|---|---|
| test system | 2.9.1 | 2.9.1 | 1.4.0 | 1.4.0 |
| production A | 2.9.1 | 2.9.1 | 1.4.0 | not installed |
| production B | 2.9.1 | 2.9.1 | 1.4.0 | not installed |
FluentMessaging 2.9.0 everywhere. The behaviour is identical on all three, with
and without FluentPlayer Pro.
All three store their media on Bunny.net, through FluentCommunity Pro's own
cloud storage module β fluent-community-pro/app/Modules/CloudStorage/BunnyCDN/BunnyCdnDriver.php.
A video uploaded with the editor's video button therefore does not land in
wp-content/uploads; it goes to Bunny Storage and is served from the pull zone.
The row in <prefix>fcom_media_archive looks like this:
media_type : fluent_player
driver : s3
media_path : bunny://storage.bunnycdn.com/<zone>/fluentcom-β¦-live-studio-ferienhaus.mp4
media_url : https://cdn.example.com/fluentcom-β¦-live-studio-ferienhaus.mp4
getRemotePath() in that driver builds the bunny:// path (line 79). The
consequence for this report: media_preview.url and media_preview.settings.src
point at the CDN host, not at the site host.
We have no install that keeps its media locally, so we cannot say whether the
link card also appears with plain local uploads. If it does not, the CDN host is
the difference worth looking at first.
What you see
The media wrapper comes out as feed_media_meta_data:
<div class="feed_media feed_media_meta_data" is_preview="false">
<a href="https://cdn.example.com/fluentcom-β¦-live-studio-ferienhaus.mp4"
target="_blank" rel="noopener" class="feed_media_link">
<div class="feed_media_content">
<div class="media_domain">cdn.example.com</div>
<div class="feed_media_title">live-studio-ferienhaus.mp4</div>
No <video>, no <media-player>, no FluentPlayer markup at all. Logged in and
logged out behave the same.
What is stored
The post meta written by the editor's own upload, taken from the database and
confirmed byte for byte against the REST response the Vue app receives:
'media_preview' => [
'type' => 'oembed',
'url' => 'https://cdn.example.com/fluentcom-β¦-live-studio-ferienhaus.mp4',
'content_type' => 'video',
'player' => 'fluent_player',
'provider' => 'upload',
'title' => 'live-studio-ferienhaus.mp4',
'media_id' => 119,
'settings' => [
'src' => 'https://cdn.example.com/fluentcom-β¦-live-studio-ferienhaus.mp4',
'title' => 'live-studio-ferienhaus.mp4',
'viewType' => 'video',
'crossorigin' => false,
],
]
There is no html key.
Why the player is skipped
From the shipped fluent-community/assets/app.js (2.9.1), the media component:
mediaWrapperType() {
return this.media.type === "oembed" && !this.isVideoMedia ? "meta_data" : this.media.type
}
isVideoMedia() {
return this.isYouTubePlaylistUrl(this.mediaSrcUrl) && !this.media.html
|| this.media.type === "oembed" && !/<(iframe|video)\b/i.test(this.media.html || "")
? false
: ["oembed", "iframe_html", "video", "fluent_player"].includes(this.media.type)
}
For an uploaded video type is oembed and html is absent, so the second
clause makes isVideoMedia false, mediaWrapperType falls back to meta_data,
and the template renders the link branch. The player branch is never reached.
When it started
The clause is new in 2.9. We keep nightly file backups of one production
install, so this is measured rather than inferred β for each backup, the plugin
version and whether its app.js carries the iframe|video test:
| backup | FluentCommunity | clause in isVideoMedia() |
|---|---|---|
| 2026-07-26 β¦ 2026-08-09 | 2.7.5 | absent |
| 2026-08-16 | 2.7.7 | absent |
| 2026-08-23 β¦ 2026-09-03 | 2.8.1 | absent |
| 2026-09-04 | 2.9.1 | present |
In 2.8.1 the function read:
isVideoMedia() {
return this.isYouTubePlaylistUrl(this.mediaSrcUrl) && !this.media.html
? false
: ["oembed", "iframe_html", "video", "fluent_player"].includes(this.media.type)
}
An uploaded video passed, because its type is oembed and the YouTube
playlist clause did not apply. mediaWrapperType() is unchanged between the two
versions, so the whole difference is that one added clause.
We cannot separate 2.9.0 from 2.9.1: both were installed on the same evening,
between two nightly backups.
Why the stored shape has no html
The uploader emits html: "", and the save path drops it:
fluent-community/Modules/Integrations/FluentPlayer/Bootstrap.php:383 (and the
same call at :401)
$data['meta']['media_preview'] = array_filter(self::sanitizeMediaHtml($media));
array_filter() without a callback removes every falsy value, so the empty
string never reaches the database. The plugin therefore writes precisely the
shape its own renderer rejects.
Scope
On one production install 55 posts are affected, on a second 2 β every post
whose video came from the editor upload, going back to April 2026. YouTube
embeds are unaffected because their html carries an <iframe>, which
satisfies the regex.
Reproduction on a clean 2.9.1: create a post, upload an MP4 with the editor's
video button, save, open the post.
Suggested fix
Either would do it:
- keep
media_preview.htmlout ofarray_filter(), or set it to real markup on
upload, or - let
isVideoMedia()accept anoembedmedia that carries
player: "fluent_player"(orprovider: "upload") without requiring embed
markup.
The second looks safer for existing content, since it also repairs the posts
already stored.
Happy to send a PR for either.
What we run in the meantime
A read-time filter, on all three installs. It supplies the missing markup and
writes nothing to the database; as soon as html carries a <video> tag,
isVideoMedia() is true again and the component takes its first branch, so the
FluentPlayer preview renders as it should.
add_filter('fluent_community/rendering_feed_model', function ($feed, $config = []) {
$meta = $feed->meta;
if (!is_array($meta) || empty($meta['media_preview']) || !is_array($meta['media_preview'])) {
return $feed;
}
$media = $meta['media_preview'];
if (
!isset($media['player'], $media['type'], $media['provider'], $media['content_type'])
|| $media['player'] !== 'fluent_player'
|| $media['type'] !== 'oembed'
|| $media['provider'] !== 'upload'
|| $media['content_type'] !== 'video'
|| !empty($media['html'])
) {
return $feed;
}
$src = !empty($media['settings']['src']) ? $media['settings']['src'] : (!empty($media['url']) ? $media['url'] : '');
if (!$src || stripos($src, 'https://') !== 0) {
return $feed;
}
$media['html'] = '<video src="' . esc_url($src) . '" controls playsinline preload="metadata"></video>';
$meta['media_preview'] = $media;
$feed->meta = $meta;
return $feed;
}, 20, 2);
Two things we checked before settling on it: a <video src="β¦"> survives your
own oembed allowlist in RemoteUrlParser::sanitizeOembedHtml() unchanged
(wp_kses_allowed_html('post') plus iframe β note that <source> does not,
which is why the source sits on the video tag), and YouTube embeds are untouched
because they already carry an <iframe> in html and have provider: "youtube".
Posting it in case it is useful to other users while this is open. We would
rather drop it again than maintain it.
Side observation
With the branch repaired locally, the player renders and plays, but the browser
console reports:
FluentPlayer: Failed to fetch media data: Medium nicht gefunden
fluent-player.js posts action=fluent_player_get_media_data&media_id=<id> to
admin-ajax.php. The id comes from media_preview.media_id, which is a row in
<prefix>fcom_media_archive, while the handler resolves it against WordPress
posts:
fluent-player/app/Blocks/FluentCommunityMediaBlock.php:838
$media = Media::findVisible($mediaId);
with post_password_required($mediaId) and current_user_can('edit_post', $mediaId)
in the same function. On our installs the ids do not exist as posts at all
(fcom_media_archive 101, 102, 119 versus FluentPlayer media posts 40, 281,
292), so the lookup always fails.
It has no visible effect β the player already has its source from the
server-rendered markup and plays fine β so this is a note rather than a second
report.
I've verified the bug, and already fixed in dev.
Tawsif Ahmed Riyad, thank you!
i also brought this up last week in support ticket...
J V, until the update with the fix is available, use this snippet:
add_filter('fluent_community/rendering_feed_model', function ($feed, $config = []) {
$meta = $feed->meta;
if (!is_array($meta) || empty($meta['media_preview']) || !is_array($meta['media_preview'])) {
return $feed;
}
$media = $meta['media_preview'];
if (
!isset($media['player'], $media['type'], $media['provider'], $media['content_type'])
|| $media['player'] !== 'fluent_player'
|| $media['type'] !== 'oembed'
|| $media['provider'] !== 'upload'
|| $media['content_type'] !== 'video'
|| !empty($media['html'])
) {
return $feed;
}
$src = !empty($media['settings']['src']) ? $media['settings']['src'] : (!empty($media['url']) ? $media['url'] : '');
if (!$src || stripos($src, 'https://') !== 0) {
return $feed;
}
$media['html'] = '<video src="' . esc_url($src) . '" controls playsinline preload="metadata"></video>';
$meta['media_preview'] = $media;
$feed->meta = $meta;
return $feed;
}, 20, 2);Peter Claus LamprechtΒ Thank you..