Skip to content

fix: admin provider video preview - #2463

Merged
ascorbic merged 6 commits into
emdash-cms:mainfrom
helio-cf:fix/admin-provider-video-preview
Aug 24, 2026
Merged

fix: admin provider video preview#2463
ascorbic merged 6 commits into
emdash-cms:mainfrom
helio-cf:fix/admin-provider-video-preview

Conversation

@helio-cf

@helio-cf helio-cf commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fix media previews for streaming providers such as Cloudflare Stream, whose items have no directly playable file URL

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 5

@changeset-bot

changeset-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8979400

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
@emdash-cms/admin Patch
@emdash-cms/cloudflare Patch
emdash Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread packages/core/src/ui.ts Outdated
Comment on lines +48 to +52
// Main Media component for EmDash media. Unlike `Image`, this resolves the
// media provider via `getEmbed()`, so it can render video/audio whose URLs
// are not a flat `src` — e.g. Cloudflare Stream, where playback lives in
// `meta.playback` as HLS/DASH and `Image` would emit a broken <img>.
EmDashMedia as Media,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me know if this is not intended to be exported, need it for dynamic cloudflare stream playback in app client

@helio-cf
helio-cf force-pushed the fix/admin-provider-video-preview branch from 90e402a to 47cbb6a Compare August 13, 2026 17:02
@helio-cf
helio-cf marked this pull request as ready for review August 13, 2026 17:13
@helio-cf helio-cf changed the title Fix/admin provider video preview fix: admin provider video preview Aug 13, 2026
@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Aug 13, 2026
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is the right fix for a real, narrow problem: Cloudflare Stream items carry a poster in previewUrl and HLS/DASH playback in meta.playback, while the admin UI assumed every video had a single fetchable src. The approach—introducing a small metaPlayback helper, using previewUrl for thumbnails/posters, and re-exporting Media from emdash/ui so frontend consumers can resolve provider embeds—is well-scoped and additive.

I checked the changed files against AGENTS.md conventions and the existing component patterns. The implementation looks sound, but there are a few AGENTS.md hygiene issues and one inconsistency that should be addressed before merge:

  • The changeset reads like a PR description: it includes internal mechanics (meta.thumbnail, getEmbed, browser version notes) and file-level narrative. A changeset is user-facing release notes, not a summary of the diff.
  • Several new inline comments are change summaries aimed at the reviewer rather than at future readers of the code. AGENTS.md asks us to avoid PR narrative and justification in comments.
  • The meta.size fallback added in providerItemToMediaItem does not reach the provider list-row size display, so the list view still shows "—" for items that only report size in meta.
  • The library grid/list thumbnail fix is not covered by a test; the new tests are all for the detail panel and the Cloudflare embed, with nothing guarding the core "non-image provider items should render a poster" behavior.

No security, data-integrity, or query-count issues were introduced. I’m not blocking merge, but the changeset and comments need cleanup.


Findings

  • [needs fixing] .changeset/stream-provider-video-preview.md:7-16

    The changeset is written as a PR description for reviewers, not as user-facing release notes. It lists internal mechanics (getEmbed, meta.thumbnail, EmDashMedia/barrel re-export plumbing, browser version notes) and justifies implementation choices. AGENTS.md says a changeset is “release notes a user reads while upgrading,” should lead with a present-tense verb describing the observable effect, and should leave out internal mechanics, file names, and implementation narrative.

    ---
    "@emdash-cms/admin": patch
    "@emdash-cms/cloudflare": patch
    "emdash": patch
    ---
    
    Fix media previews for streaming providers such as Cloudflare Stream.
    
    Streaming items expose playback URLs via `meta.playback` and a poster via `previewUrl`. The media library now shows posters for these items, and the detail panel plays the HLS/DASH sources instead of treating the poster as the video file. `Media` is now exported from `emdash/ui` for frontend rendering.
    
  • [suggestion] packages/admin/src/components/MediaLibrary.tsx:770-771

    This JSX comment is a change summary aimed at the reviewer, not at a future reader of the code. The condition {item.previewUrl ? <img … /> : <icon />} already makes the behavior clear. Per AGENTS.md, comments should not narrate the PR or explain what was changed.

    			<div className="aspect-square">
    				{item.previewUrl ? (
    					<img
    						src={item.previewUrl}
    						alt={item.alt || item.filename}
    						className="h-full w-full object-cover"
    						onLoad={handleImageLoad}
    					/>
    				) : (
    
  • [suggestion] packages/admin/src/components/MediaLibrary.tsx:870

    Same as the grid-item comment: this is a pointer to the PR change rather than everlasting context for code readers. The guard has been removed, so the comment can go.

    				{item.previewUrl ? (
    					<img
    						src={item.previewUrl}
    						alt={item.alt || item.filename}
    						className="h-full w-full object-cover"
    						onLoad={handleImageLoad}
    					/>
    
  • [suggestion] packages/admin/src/components/MediaDetailPanel.tsx:255-257

    This comment restates the branch condition and the source list that follows, and it includes implementation justification (“Browsers demux HLS natively; DASH is a fallback”) that belongs in the changeset/PR description. The code already expresses that streaming items use sources. Remove the comment and rely on the metaPlayback name and the <source> elements.

    								) : isVideo && playback ? (
    									<video
    										poster={item.url || undefined}
    										controls
    										preload="metadata"
    										className="max-h-full max-w-full"
    									>
    
  • [suggestion] packages/cloudflare/src/media/stream-runtime.ts:289-292

    This block comment explains why the implementation changed (list()/get() don’t set meta.thumbnail) rather than giving a non-obvious invariant a future reader needs. The expression value.previewUrl ?? toString(value.meta?.thumbnail) is self-explanatory about precedence. Drop the narrative comment.

    					poster: value.previewUrl ?? toString(value.meta?.thumbnail),
    
  • [suggestion] packages/core/src/ui.ts:48-51

    The comment at this public re-export is mostly a comparison to Image and a warning about what Image does wrong. That is PR justification, not a docstring. Keep a short, forward-looking description of what Media does.

    	// Unified media component that delegates rendering to the provider's getEmbed().
    	EmDashMedia as Media,
    
  • [needs fixing] packages/admin/src/components/MediaLibrary.tsx:888

    The changeset says provider items may report size in meta, and providerItemToMediaItem now falls back to meta.size for the detail panel. However, the provider list row still only reads the top-level item.size and shows "—" when it is missing, even if meta.size exists. This makes the list and detail inconsistent for the same item. Use the same metaNumber fallback here (and import it from ../lib/media-utils).

    import {
    	providerItemToMediaItem,
    	getFileIcon,
    	formatFileSize,
    	getMediaThumbnailUrl,
    	fallbackToOriginalThumbnail,
    	MEDIA_THUMBNAIL_WIDTH,
    	metaNumber,
    } from "../lib/media-utils";
    
    function ProviderListItem({ item, selected, onClick, onDimensionsLoaded }: ProviderListItemProps) {
    	const { t } = useLingui();
    	const size = item.size ?? metaNumber(item.meta, "size");
    
    	const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
    		const img = e.currentTarget;
    		if (onDimensionsLoaded && (!item.width || !item.height)) {
    			onDimensionsLoaded(img.naturalWidth, img.naturalHeight);
    		}
    	};
    
    	return (
    		<tr
    			className={cn(
    				"cursor-pointer transition-colors",
    				selected ? "bg-kumo-brand/10" : "hover:bg-kumo-tint/25",
    			)}
    			onClick={onClick}
    		>
    			<td className="px-4 py-3">
    				<div className="h-10 w-10 overflow-hidden rounded">
    					{item.previewUrl ? (
    						<img
    							src={item.previewUrl}
    							alt={item.alt || item.filename}
    							className="h-full w-full object-cover"
    							onLoad={handleImageLoad}
    						/>
    					) : (
    						<div className="flex h-full w-full items-center justify-center bg-kumo-tint text-xl">
    							{getFileIcon(item.mimeType)}
    						</div>
    					)}
    				</div>
    			</td>
    			<td className="px-4 py-3 text-base font-medium leading-5">{item.filename}</td>
    			<td className="px-4 py-3 text-sm text-kumo-subtle">{item.mimeType}</td>
    			<td className="px-4 py-3 text-sm text-kumo-subtle tabular-nums">
    				{size ? formatFileSize(size) : "—"}
    			</td>
    
  • [suggestion] packages/admin/src/components/MediaLibrary.tsx:770

    The grid/list thumbnail fix is not covered by tests. This is the core behavior being changed—provider-backed video/audio items with a previewUrl should render a poster in the library—and it is currently unguarded against a future regression that re-adds an image/* gate or otherwise drops the poster. Add a focused test for ProviderGridItem/ProviderListItem (or for MediaLibrary with mocked provider items) asserting that a non-image item with previewUrl renders the expected <img>.

Comment on lines +21 to +23
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me know if we have something for this already ie zod

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small enough tho, im fine with this approach

@helio-cf

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@helio-cf

Copy link
Copy Markdown
Contributor Author

recheck

github-actions Bot added a commit that referenced this pull request Aug 13, 2026
@github-actions github-actions Bot added cla: signed review/needs-rereview Author pushed changes since the last review and removed cla: needed review/needs-review No maintainer or bot review yet labels Aug 13, 2026
@MA2153 MA2153 added the bot:review Trigger an emdashbot code review on this PR label Aug 13, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of PR #2463: fix/admin-provider-video-preview.

All of my prior findings have been addressed:

  • Changeset — rewritten as concise, user-facing release notes that describe the observable fix rather than the implementation mechanics.
  • Narrative comments — the PR-summary-style JSX comments in MediaLibrary are gone; the remaining comments in MediaDetailPanel and stream-runtime.ts now state non-obvious invariants (item.url is the poster for streaming items, previewUrl is preferred over the legacy meta.thumbnail fallback) and are acceptable under AGENTS.md.
  • Provider list sizeProviderListItem now reads item.size ?? metaNumber(item.meta, "size"), so the list and detail panel are consistent.
  • Grid/list poster testsMediaLibrary.test.tsx now asserts that a non-image provider item with previewUrl renders the poster and that meta.size surfaces in the list view.

I re-read the changed files against AGENTS.md. The implementation remains the right approach: metaPlayback cleanly separates streaming sources from direct file URLs, the detail panel branches correctly between streaming and local video, the Cloudflare Stream getEmbed poster falls back correctly, and Media is re-exported from emdash/ui without any barrel-file side effects. No new security, data-integrity, logged-out query-count, i18n, RTL, or SQL-safety issues were introduced.

The diff is clean and the PR is ready to merge.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 13, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Aug 14, 2026
Streaming providers such as Cloudflare Stream have no directly playable
file URL: the poster is reported as `previewUrl` and playback lives in
`meta.playback` as HLS/DASH. Consumers assumed a flat, fetchable `src`,
so provider-backed video rendered no thumbnail in the library and a
permanently-0:00 player in the detail panel.

- Stop gating library thumbnails on an `image/*` mime type; `previewUrl`
  is a poster that video items have too.
- Play a streaming item's HLS/DASH sources in the detail panel, using
  its thumbnail as the poster. Locally stored video is unchanged.
- Read `size` from `meta` when it is not a first-class field, so
  streaming assets report a real file size.
- Prefer `previewUrl` for the poster in the Stream provider's
  `getEmbed()`. `list()`/`get()` never set `meta.thumbnail`, so posters
  were dropped for every value the media picker produced.
- Export `EmDashMedia` from `emdash/ui` as `Media`. It was exported from
  the internal components barrel but never re-exported from the public
  entrypoint, and there is no `./components` subpath, so user code had
  no way to render provider-backed video. `Image` is not a substitute:
  it only handles `embed.type === "image"`.

No player library is needed. Chromium and Safari both report
`canPlayType("application/x-mpegURL") === "maybe"` and demux a Stream
manifest natively, verified on Chrome 150 and Chromium 148 (readyState
4, correct duration, currentTime advancing). The DASH source is a
declarative fallback only, as Chromium reports no native support for
`application/dash+xml`.
The fix had no coverage: existing media tests only exercised images and
provider deletion. Eight tests, scoped to behaviour this change
introduced rather than the surrounding surface.

- Detail panel emits HLS/DASH `<source>` elements with the thumbnail as
  `poster`, and specifically does not use the poster URL as `src` --
  that was the defect behind the 0:00 player. A locally stored video
  keeps asserting the plain `src` path so the new branch cannot
  regress it.
- `metaPlayback` reads the shape Stream reports, and returns undefined
  for a plain uploaded file, which is what keeps local video on the
  direct-file path.
- `providerItemToMediaItem` falls back to `meta.size`, the reason Stream
  assets used to display "0 B", and carries `meta` through so the panel
  can find playback at all.
- The Stream provider's `getEmbed()` takes its poster from `previewUrl`,
  still honouring a legacy `meta.thumbnail` when that is absent.

Each assertion was checked against the pre-fix sources: the detail panel
cases report the poster URL where null was expected and find no <source>
elements, and `getEmbed()` returns no poster at all.
A provider list row showed a dash for any item whose byte size arrives
under `meta` rather than as a first-class field -- every Cloudflare
Stream asset. The detail panel already fell back to `meta`, so the same
asset reported a real size in the panel and none in the list.

- Fall back to `meta.size` in provider list rows, matching the panel.
- Cover the library poster and the list-row size with tests; the grid
  and list poster change had none. Both assertions were checked against
  the pre-fix sources.
- Cut the explanatory comments down to the facts a reader needs, and
  rewrite the changeset as user-facing release notes.
@khoinguyenpham04
khoinguyenpham04 force-pushed the fix/admin-provider-video-preview branch from 5c82cba to ddaa948 Compare August 14, 2026 15:47
@pkg-pr-new

pkg-pr-new Bot commented Aug 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2463

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2463

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2463

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2463

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2463

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2463

emdash

npm i https://pkg.pr.new/emdash@2463

create-emdash

npm i https://pkg.pr.new/create-emdash@2463

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2463

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2463

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2463

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2463

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2463

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2463

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2463

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2463

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2463

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2463

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2463

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2463

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2463

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2463

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2463

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2463

commit: 8979400

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/approved Approved; no new commits since labels Aug 14, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Aug 17, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a focused, appropriate fix for a real bug: streaming-provider items have a poster in previewUrl/ url and playable sources in meta.playback, so using item.url as a video src produced a stalled player. The implementation correctly distinguishes streaming from direct-file video, falls back meta.sizemeta.size, and re-uses the existing EmDashMedia component by exporting it as Media from emdash/ui.

I re-read the full diff and the changed files against the prior review context. All of the previous round’s findings remain addressed:

  • Changeset is concise, user-facing release notes.
  • Inline comments in MediaDetailPanel and stream-runtime.ts describe non-obvious invariants and are narrowly scoped.
  • Provider list size now falls back to meta.size consistently.
  • Grid/list poster tests assert the observable rendered poster and size, not just implementation details.

I also checked the new code for AGENTS.md conventions: no new hard-coded UI strings, no physical Tailwind directional classes, no SQL/route/auth/db changes, no logged-out query additions, and no stale narrative comments. EmDashMedia is already exported from packages/core/src/components/index.ts, so adding the re-export in packages/core/src/ui.ts does not pull in any new runtime cost.

No new static issues were introduced. The PR is ready to merge.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 17, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Aug 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@github-actions github-actions Bot mentioned this pull request Aug 21, 2026
18 tasks

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@ascorbic
ascorbic enabled auto-merge (squash) August 24, 2026 14:51
@ascorbic
ascorbic merged commit f613a14 into emdash-cms:main Aug 24, 2026
46 checks passed
@emdashbot emdashbot Bot mentioned this pull request Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants