Skip to content
Open
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
60 changes: 56 additions & 4 deletions src/export/browserExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { DEFAULT_RENDER_TIMEOUT_MS } from '../../remotion/render-timeout.mjs';
*/
export const BROWSER_ENCODER_UNSUPPORTED_REASON = '浏览器编码器不支持此导出规格';
export const BROWSER_RENDER_INCOMPLETE_REASON = '浏览器快导未能完成';
export const WINDOWS_MULTISTREAM_WEBCODECS_BLOCKER = 'Windows 上多个重叠视频的 WebCodecs 解码可能停滞,请使用本机渲染器';
// Mediabunny's messages when WebCodecs refuses the encoder the render asked for.
const ENCODER_REFUSED = /encoder configuration .* is not supported|VideoEncoder is not supported|cannot be encoded by this browser/i;

Expand Down Expand Up @@ -93,10 +94,61 @@ export function browserFallbackReason(error: unknown): string {
* Non-raster sources (svg/gif) and frame-rate retiming are handled separately by
* `staticBrowserBlocker`.
*/
export function browserTimelineBlocker(state: TimelineState, project?: ProjectDoc, timelineId?: string): string | null {
void state;
void project;
void timelineId;
function hasOverlappingVideoSources(
state: { id?: string; items: TimelineState['items'] },
project?: ProjectDoc,
): boolean {
const nestedTimelines = new Map(project?.timelines.map((timeline) => [timeline.id, timeline]) ?? []);
const intervals: Array<{ start: number; end: number }> = [];

const visit = (
timeline: { id?: string; items: TimelineState['items'] },
offset: number,
limit: number,
path: ReadonlySet<string>,
) => {
for (const item of timeline.items) {
const start = offset + item.startFrame;
const end = Math.min(limit, start + item.durationInFrames);
if (end <= start) continue;
if (item.kind === 'video' && item.src) {
intervals.push({ start, end });
} else if (item.kind === 'sequence' && item.timelineId) {
const nested = nestedTimelines.get(item.timelineId);
if (nested && !path.has(nested.id)) {
visit(nested, start, end, new Set([...path, nested.id]));
}
}
}
};

visit(state, 0, Number.POSITIVE_INFINITY, state.id ? new Set([state.id]) : new Set<string>());
const events = intervals.flatMap(({ start, end }) => [
{ frame: start, delta: 1 },
{ frame: end, delta: -1 },
]).sort((left, right) => left.frame - right.frame || left.delta - right.delta);
let active = 0;
for (const event of events) {
active += event.delta;
if (active > 1) return true;
}
return false;
}

function currentBrowserPlatform(): string {
return typeof window === 'undefined' ? '' : `${window.navigator.platform} ${window.navigator.userAgent}`;
}

export function browserTimelineBlocker(
state: TimelineState,
project?: ProjectDoc,
timelineId?: string,
platform = currentBrowserPlatform(),
): string | null {
if (/windows|win(?:32|64)/i.test(platform)
&& hasOverlappingVideoSources({ ...state, id: timelineId }, project)) {
return WINDOWS_MULTISTREAM_WEBCODECS_BLOCKER;
}
return null;
}

Expand Down
24 changes: 24 additions & 0 deletions src/export/browserExport.verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
browserTimelineBlocker,
exportVideoWithFallback,
renderTimelineInBrowser,
WINDOWS_MULTISTREAM_WEBCODECS_BLOCKER,
type BrowserExportAttempt,
} from './browserExport';
import type { ExportDestination } from './exportDestination';
Expand Down Expand Up @@ -144,6 +145,29 @@ assert.equal(browserTimelineBlocker({
}],
}), null, 'GLSL transition must not block the browser fast-export path');

const overlappingVideos: TimelineState = {
...state,
items: [
{ ...state.items[0]!, id: 'video_1', kind: 'video', src: '/a.mp4', startFrame: 0, durationInFrames: 90 },
{ ...state.items[0]!, id: 'video_2', kind: 'video', src: '/b.mp4', startFrame: 30, durationInFrames: 90 },
],
};
assert.equal(
browserTimelineBlocker(overlappingVideos, undefined, undefined, 'Win32'),
WINDOWS_MULTISTREAM_WEBCODECS_BLOCKER,
'Windows must keep overlapping video sources off the WebCodecs browser route',
);
assert.equal(
browserTimelineBlocker({ ...overlappingVideos, items: overlappingVideos.items.map((item) => ({ ...item, startFrame: item.id === 'video_1' ? 0 : 90 })) }, undefined, undefined, 'Win32'),
null,
'sequential video clips do not exhaust the simultaneous WebCodecs decoder pool',
);
assert.equal(
browserTimelineBlocker(overlappingVideos, undefined, undefined, 'Linux x86_64'),
null,
'the Windows D3D11 decoder guard must not change other platforms',
);

const capabilityCalls: Array<Record<string, unknown>> = [];
const renderCalls: Array<Record<string, unknown>> = [];
const progressSnapshots: number[] = [];
Expand Down