Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 97bcf7e177ca103298874640116738590712ff8c)
+++ src/services/ActivityPubService.js	(revision 7d01696196f37e165850a2e39428e7ea68dba231)
@@ -17,4 +17,6 @@
  */
 import crypto from 'crypto';
+import fs from 'fs';
+import path from 'path';
 import dns from 'dns';
 import net from 'net';
@@ -380,5 +382,5 @@
   try {
     for (const a of JSON.parse(post.c2s_attachments || '[]')) {
-      if (a && a.url) urls.push({ url: abs(a.url), name: a.name || '', mt: a.mediaType });
+      if (a && a.url) urls.push({ url: abs(a.url), name: a.name || '', mt: a.mediaType, poster: a.poster ? abs(a.poster) : null });
     }
   } catch { /* malformed never blocks the Note */ }
@@ -480,4 +482,5 @@
       const a = { type: ty, mediaType: mt, url: x.url };
       if (x.name) a.name = String(x.name).slice(0, 1500); // alt text / description (AS2 `name`)
+      if (x.poster) a.icon = { type: 'Image', url: x.poster }; // the video's still (shaer-zowq)
       return a; });
   for (const a of openAudio) attachment.push(a); // fedi_open tracks → native Audio players
@@ -2379,5 +2382,18 @@
       && /^(image|audio|video)\//.test(String(a.mediaType || '')))
     .slice(0, 4)
-    .map((a) => ({ url: a.url, mediaType: String(a.mediaType), name: String(a.name || '').slice(0, 120) }));
+    .map((a) => {
+      const entry = { url: a.url, mediaType: String(a.mediaType), name: String(a.name || '').slice(0, 120) };
+      // A video's poster frame, when the upload leg made one (shaer-zowq):
+      // rides along so the tag, the federated attachment and the apps all
+      // show a still instead of a black box.
+      if (entry.mediaType.startsWith('video/')) {
+        try {
+          const mediaRoot = path.resolve(process.env.MEDIA_PATH || './storage/media');
+          const rel = entry.url.replace(/^\/media\//, '');
+          if (fs.existsSync(path.join(mediaRoot, rel + '.poster.jpg'))) entry.poster = entry.url + '.poster.jpg';
+        } catch { /* no poster is fine */ }
+      }
+      return entry;
+    });
   if (!html.trim() && !media.length) return { status: 400, error: 'empty_note' };
   // The web reads the post's content, so the media goes IN it (we build these
@@ -2389,5 +2405,6 @@
     if (a.mediaType.startsWith('image/')) return `<p><img src="${a.url}" alt="${esc(a.name)}"></p>`;
     if (a.mediaType.startsWith('audio/')) return `<p><audio controls preload="metadata" src="${a.url}"></audio></p>`;
-    return `<p><video controls playsinline preload="metadata" src="${a.url}"></video></p>`;
+    const poster = a.poster ? ` poster="${a.poster}"` : '';
+    return `<p><video controls playsinline preload="metadata"${poster} src="${a.url}"></video></p>`;
   }).join('');
   const postId = crypto.randomUUID();
@@ -2818,5 +2835,9 @@
     const rows = (Array.isArray(list) ? list : [])
       .filter((m) => m && m.url)
-      .map((m) => ({ type: 'Document', mediaType: m.type || undefined, url: m.url }));
+      .map((m) => {
+        const a = { type: 'Document', mediaType: m.type || undefined, url: m.url };
+        if (m.poster) a.icon = { type: 'Image', url: m.poster }; // the video's still (shaer-zowq)
+        return a;
+      });
     return rows.length ? rows : undefined;
   } catch { return undefined; }
@@ -3027,5 +3048,11 @@
 }
 function mediaFromNote(note) {
-  const atts = (Array.isArray(note.attachment) ? note.attachment : []).map((a) => ({ url: safeUrl(a && a.url), type: (a && a.mediaType) || '' })).filter((m) => m.url);
+  const atts = (Array.isArray(note.attachment) ? note.attachment : []).map((a) => {
+    const m = { url: safeUrl(a && a.url), type: (a && a.mediaType) || '' };
+    // A federated video may carry its poster as an AS2 icon (shaer-zowq).
+    const iconUrl = a && a.icon && safeUrl(typeof a.icon === 'string' ? a.icon : a.icon.url);
+    if (iconUrl && /^video\//i.test(m.type)) m.poster = iconUrl;
+    return m;
+  }).filter((m) => m.url);
   if (!atts.some((m) => !m.type || /image/i.test(m.type)) && note.image) {
     const im = Array.isArray(note.image) ? note.image[0] : note.image;
