Skip to main content

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.html out of array_filter(), or set it to real markup on
    upload, or
  • let isVideoMedia() accept an oembed media that carries
    player: "fluent_player" (or provider: "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.

J V

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);

J V

Peter Claus LamprechtΒ Thank you..