Index: src/services/AudioEmbedService.js
===================================================================
--- src/services/AudioEmbedService.js	(revision da71f9d9973e0edeed10e4bade48fe63b1e6d58c)
+++ src/services/AudioEmbedService.js	(revision a85f53969a50098465bd62c4577ec9921551b4b4)
@@ -82,4 +82,36 @@
 
     return null;
+  }
+
+  // Direct media files (video/audio) hosted anywhere → a native <video>/<audio>
+  // player. Kept OUT of detectProvider() on purpose: the timeline/cover callers
+  // switch on provider slugs (youtube/spotify/…) and a bare file has none, so
+  // overloading detectProvider would suppress e.g. a PeerTube fallback. Only
+  // autoembed() and [[embed:…]] use this.
+  static MEDIA_FILE_EXT = {
+    video: ['mp4', 'webm', 'm4v', 'mov', 'ogv'],
+    audio: ['mp3', 'ogg', 'oga', 'wav', 'm4a', 'flac', 'opus', 'aac'],
+  };
+
+  static detectMediaFile(url) {
+    if (!url || typeof url !== 'string') return null;
+    if (!/^https?:\/\//i.test(url)) return null;
+    let pathname;
+    try { pathname = new URL(url).pathname.toLowerCase(); } catch { return null; }
+    const ext = (pathname.match(/\.([a-z0-9]+)$/) || [])[1];
+    if (!ext) return null;
+    if (this.MEDIA_FILE_EXT.video.includes(ext)) return { kind: 'video', url };
+    if (this.MEDIA_FILE_EXT.audio.includes(ext)) return { kind: 'audio', url };
+    return null;
+  }
+
+  static mediaFileEmbed(url) {
+    const m = this.detectMediaFile(url);
+    if (!m) return null;
+    const src = this.escape(m.url);
+    if (m.kind === 'video') {
+      return `<figure class="folio-embed folio-embed--video"><video src="${src}" controls preload="metadata" playsinline></video></figure>`;
+    }
+    return `<figure class="folio-embed folio-embed--audio"><audio src="${src}" controls preload="metadata"></audio></figure>`;
   }
 
@@ -238,4 +270,7 @@
           return iframe || match;
         }
+        // Bare media file (…/clip.webm, …/song.mp3) → native player.
+        const media = this.mediaFileEmbed(url);
+        if (media) return media;
         return match;
       }
@@ -255,4 +290,7 @@
       const detected = this.detectProvider(url);
       if (!detected) {
+        // Bare media file (…/clip.webm, …/song.mp3) → native player.
+        const media = this.mediaFileEmbed(url);
+        if (media) return media;
         return `<div class="post-embed-missing"><em>Embed: niet-ondersteunde of ongeldige URL.</em></div>`;
       }
Index: src/services/HtmlSanitizerService.js
===================================================================
--- src/services/HtmlSanitizerService.js	(revision da71f9d9973e0edeed10e4bade48fe63b1e6d58c)
+++ src/services/HtmlSanitizerService.js	(revision a85f53969a50098465bd62c4577ec9921551b4b4)
@@ -25,17 +25,25 @@
   'strong', 'em', 'b', 'i', 'u', 's', 'mark', 'small', 'sub', 'sup',
   'code', 'a', 'span', 'img',
+  // Native media (bare .webm/.mp4/.mp3 embeds + federated-in players)
+  'video', 'audio', 'source',
 ];
 
 // Per-tag attribute allowlist. '*' applies to every tag.
 const ALLOWED_ATTRS = {
-  '*': ['class', 'id', 'dir', 'lang', 'data-sc'],
-  a:   ['href', 'title', 'target', 'rel'],
-  img: ['src', 'alt', 'title', 'width', 'height', 'loading'],
+  '*':   ['class', 'id', 'dir', 'lang', 'data-sc'],
+  a:     ['href', 'title', 'target', 'rel'],
+  img:   ['src', 'alt', 'title', 'width', 'height', 'loading'],
+  video: ['src', 'controls', 'preload', 'poster', 'width', 'height', 'loop', 'muted', 'autoplay', 'playsinline'],
+  audio: ['src', 'controls', 'preload', 'loop', 'muted', 'autoplay'],
+  source: ['src', 'type'],
 };
 
 const ALLOWED_SCHEMES = ['http', 'https', 'mailto', 'tel'];
 const ALLOWED_SCHEMES_BY_TAG = {
-  img: ['http', 'https', 'data'],
-  a:   ['http', 'https', 'mailto', 'tel'],
+  img:    ['http', 'https', 'data'],
+  a:      ['http', 'https', 'mailto', 'tel'],
+  video:  ['http', 'https'],
+  audio:  ['http', 'https'],
+  source: ['http', 'https'],
 };
 
