Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ TBD
## Fixed

- **Chief of Staff pane overflow** — content panel collapses to a single `flex-1 min-h-0 min-w-0 overflow-y-auto overflow-x-hidden` div so tall tab content scrolls inside the panel instead of expanding it. Event log rows in both `EventLog` and `TerminalCoSPanel` get `break-all` so long unbreakable tokens (URLs, hashes, paths) wrap inside the 320px sidebar instead of pushing the column wider visually.
- **Videos now load in the Media Gen preview on mobile.** Both the preview-modal (lightbox) `<video>` and the Video Gen page's inline result preview autoplayed unmuted, which iOS/Android block outside a direct user gesture — so the clip never started and the area showed only black ("not loading"). They now play `muted` (autoplay-eligible everywhere; controls let you unmute) and render the thumbnail as a `poster`, so the frame is visible immediately even while the clip buffers.
26 changes: 22 additions & 4 deletions client/src/components/media/MediaLightbox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,28 @@ export default function MediaLightbox({
onTouchEnd={onTouchEnd}
>
{isVideo ? (
/* playsInline keeps iOS Safari from auto-promoting autoplay video
to a native fullscreen player — exiting that leaves the modal
laid out as a tiny strip with no reachable close button. */
<video src={item.downloadUrl} controls autoPlay loop playsInline className={imgMax} />
/* Mobile playback contract:
- playsInline keeps iOS Safari from auto-promoting autoplay video
to a native fullscreen player — exiting that leaves the modal
laid out as a tiny strip with no reachable close button.
- muted is required for autoplay under the mobile media-engagement
policy: iOS/Android block unmuted autoplay that isn't fired from
a direct user gesture, so without it the clip never starts and the
area just shows black ("not loading"). Controls let the user unmute.
- poster paints the thumbnail immediately so there's no blank box
while the clip buffers (and a visible frame even if playback is
deferred). previewUrl is the video's thumbnail; omit when absent. */
<video
src={item.downloadUrl}
poster={item.previewUrl || undefined}
controls
autoPlay
loop
muted
playsInline
preload="metadata"
className={imgMax}
/>
) : (
<MediaImage src={item.previewUrl} alt={item.prompt} className={`${imgMax} object-contain`} placeholderClassName="w-full h-full" />
)}
Expand Down
48 changes: 48 additions & 0 deletions client/src/components/media/MediaLightbox.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, it, expect, vi } from 'vitest';
import { render } from '@testing-library/react';
import MediaLightbox from './MediaLightbox';

// The footer's AddToCollectionMenu and the (closed) PromptRefineModal pull the
// whole API surface (and useProviderModels) into the import graph. Neither is
// under test here, so stub them to inert nodes — that keeps the test focused
// on MediaLightbox's own <video> markup and off the network.
vi.mock('./AddToCollectionMenu', () => ({ default: () => null }));
vi.mock('./PromptRefineModal', () => ({ default: () => null }));

const videoItem = {
kind: 'video',
key: 'video:abc',
id: 'abc',
filename: 'abc.mp4',
previewUrl: '/data/video-thumbnails/abc.jpg',
downloadUrl: '/data/videos/abc.mp4',
prompt: 'a cat',
createdAt: Date.now(),
};

describe('MediaLightbox video element (mobile playback)', () => {
it('renders the <video> with a poster + muted + playsInline so it loads/autoplays on mobile', () => {
const { container } = render(<MediaLightbox item={videoItem} onClose={() => {}} />);
const video = container.querySelector('video');
expect(video).toBeTruthy();
// src points at the full asset
expect(video.getAttribute('src')).toBe('/data/videos/abc.mp4');
// poster = thumbnail so a blank box never shows while the clip buffers,
// and the frame is visible even if mobile autoplay is deferred.
expect(video.getAttribute('poster')).toBe('/data/video-thumbnails/abc.jpg');
// muted is required for autoplay under mobile media-engagement policy.
expect(video.muted).toBe(true);
// playsInline keeps iOS from promoting to a native fullscreen player.
expect(video.hasAttribute('playsinline')).toBe(true);
expect(video.hasAttribute('loop')).toBe(true);
expect(video.hasAttribute('controls')).toBe(true);
});

it('omits poster when the video has no thumbnail rather than rendering an empty poster', () => {
const { container } = render(
<MediaLightbox item={{ ...videoItem, previewUrl: null }} onClose={() => {}} />
);
const video = container.querySelector('video');
expect(video.hasAttribute('poster')).toBe(false);
});
});
16 changes: 15 additions & 1 deletion client/src/pages/VideoGen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1384,7 +1384,21 @@ export default function VideoGen() {
</div>
<div className="aspect-video max-w-[420px] mx-auto bg-port-bg border border-port-border rounded-lg overflow-hidden flex items-center justify-center relative">
{result ? (
<video src={result.path || `/data/videos/${result.filename}`} controls autoPlay loop playsInline preload="metadata" className="w-full h-full" />
// muted so the clip autoplays under the mobile media-engagement
// policy (iOS/Android block unmuted autoplay outside a user
// gesture — otherwise it just shows black); poster paints the
// thumbnail while it buffers. Controls let the user unmute.
<video
src={result.path || `/data/videos/${result.filename}`}
poster={result.thumbnail ? `/data/video-thumbnails/${result.thumbnail}` : undefined}
controls
autoPlay
loop
muted
playsInline
preload="metadata"
className="w-full h-full"
/>
) : generating ? (
<div className="text-gray-500 text-xs flex flex-col items-center gap-1.5">
<BrailleSpinner />
Expand Down