Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
302f45f
Bump fflate from 0.8.2 to 0.8.3 in /packages/browser-studio
dependabot[bot] Sep 3, 2026
5c80e25
Update fflate to 0.8.3 across all monorepo packages
JonnyBurger Sep 5, 2026
9cc8817
Merge pull request #11059 from remotion-dev/dependabot/npm_and_yarn/p…
JonnyBurger Sep 5, 2026
1e17374
Expand timeline layer edge hit areas
JonnyBurger Sep 5, 2026
f49e8e5
Expand outer edge handles and avoid overlap on narrow layers
JonnyBurger Sep 5, 2026
64fa477
Add persistent frame format for Studio timeline ticks
JonnyBurger Sep 5, 2026
f772efe
Add Browser Studio leave confirmation
JonnyBurger Sep 5, 2026
2c68166
fix media playback rate reset on resource reload
JonnyBurger Sep 5, 2026
e6f4dd3
Fix timeline drags snapping back on mouse release
JonnyBurger Sep 5, 2026
17b5c14
Merge pull request #11075 from remotion-dev/codex/timeline-edge-hit-area
JonnyBurger Sep 5, 2026
a3c5098
Merge pull request #11078 from remotion-dev/codex/timeline-frame-tick…
JonnyBurger Sep 5, 2026
e9b8c43
Fix Browser Studio composition drag source paths
JonnyBurger Sep 5, 2026
dfb6ed5
Merge pull request #11080 from remotion-dev/codex/browser-studio-leav…
JonnyBurger Sep 5, 2026
009634b
Merge pull request #11081 from remotion-dev/codex/fix-media-playback-…
JonnyBurger Sep 5, 2026
1479858
Merge pull request #11082 from remotion-dev/codex/fix-timeline-drag-s…
JonnyBurger Sep 5, 2026
e995437
Handle unresolved Browser Studio composition drag sources
JonnyBurger Sep 5, 2026
df74f5f
Remove composition drag test fixtures and E2E coverage
JonnyBurger Sep 5, 2026
2209947
Merge pull request #11083 from remotion-dev/codex/browser-studio-comp…
JonnyBurger Sep 5, 2026
1a449a3
Add persistent timeline layer child collapsing
JonnyBurger Sep 5, 2026
d47f24a
Merge pull request #11088 from remotion-dev/codex/timeline-layer-coll…
JonnyBurger Sep 5, 2026
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
8 changes: 6 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/browser-studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@remotion/studio-protocol": "workspace:*",
"@remotion/studio-shared": "workspace:*",
"@rspack/browser": "2.1.1",
"fflate": "0.8.2",
"fflate": "0.8.3",
"prettier": "catalog:",
"react-refresh": "0.18.0",
"remotion": "workspace:*"
Expand Down
12 changes: 12 additions & 0 deletions packages/browser-studio/src/BrowserStudio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,18 @@ export const BrowserStudio: React.FC<BrowserStudioProps> = ({
[],
);

useEffect(() => {
const onBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault();
event.returnValue = true;
};

window.addEventListener('beforeunload', onBeforeUnload);
return () => {
window.removeEventListener('beforeunload', onBeforeUnload);
};
}, []);

useEffect(() => {
return () => publicFileManager.dispose();
}, [publicFileManager]);
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/use-media-playback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,14 @@ export const useMediaPlayback = ({
// and it is also in a useLayoutEffect.
useLayoutEffect(() => {
const playbackRateToSet = Math.max(0, playbackRate);
// Loading a resource resets playbackRate to defaultPlaybackRate.
if (
mediaRef.current &&
mediaRef.current.defaultPlaybackRate !== playbackRateToSet
) {
mediaRef.current.defaultPlaybackRate = playbackRateToSet;
}

if (
mediaRef.current &&
mediaRef.current.playbackRate !== playbackRateToSet
Expand Down
46 changes: 46 additions & 0 deletions packages/media/src/test/html5-playback-rate.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {Player} from '@remotion/player';
import React from 'react';
import {createRoot} from 'react-dom/client';
import {Html5Video} from 'remotion';
import {expect, test} from 'vitest';

test('preserves the Player media playback rate across source changes and reloads', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const root = createRoot(container);
const Composition: React.FC<{readonly src: string}> = ({src}) => (
<Html5Video src={src} playbackRate={0.6} />
);

try {
for (const [index, globalRate] of [1, 0.5].entries()) {
root.render(
<Player
acknowledgeRemotionLicense
component={Composition}
compositionHeight={720}
compositionWidth={1280}
durationInFrames={100}
fps={30}
initiallyMuted
playbackRate={globalRate}
inputProps={{src: `/bigbuckbunny.mp4?source=${index}`}}
/>,
);
await expect
.poll(() => container.querySelector('video')?.readyState)
.toBe(4);
const video = container.querySelector('video')!;
await expect.poll(() => video.playbackRate).toBe(0.6 * globalRate);

// Loading a new resource resets playbackRate to defaultPlaybackRate.
video.src = `/bigbuckbunny.mp4?reload=${index}`;
video.load();
await expect.poll(() => video.readyState).toBe(4);
expect(video.playbackRate).toBe(0.6 * globalRate);
}
} finally {
root.unmount();
container.remove();
}
});
7 changes: 6 additions & 1 deletion packages/studio/src/components/CompositionSelectorItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import React, {
useState,
} from 'react';
import {type _InternalTypes} from 'remotion';
import {getBrowserStudioOperations} from '../helpers/browser-studio-operations';
import {StudioServerConnectionCtx} from '../helpers/client-id';
import {
CURRENT_COLOR,
Expand Down Expand Up @@ -411,8 +412,12 @@ export const CompositionSelectorItem: React.FC<{
);

if (item.type === 'composition') {
const browserStudioOperations = getBrowserStudioOperations();
const compositionDragData = makeCompositionDragData({
compositionFile: resolvedLocation?.source ?? null,
compositionFile:
browserStudioOperations === null
? (resolvedLocation?.source ?? null)
: browserStudioOperations.getCompositionFile(item.composition.id),
compositionId: item.composition.id,
width: item.composition.width ?? null,
height: item.composition.height ?? null,
Expand Down
93 changes: 68 additions & 25 deletions packages/studio/src/components/TimeValue.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import {PlayerInternals} from '@remotion/player';
import React, {
useCallback,
useContext,
useEffect,
useImperativeHandle,
useRef,
} from 'react';
import {Internals, useCurrentFrame} from 'remotion';
import {LIGHT_TEXT, WHITE} from '../helpers/colors';
import {LIGHT_TEXT, TRANSPARENT, WHITE} from '../helpers/colors';
import {
FOCUS_VISIBLE_ONLY_CLASS_NAME,
HOVERABLE_CLASS_NAME,
hoverableStyle,
} from '../helpers/hoverable';
import {useIsStill} from '../helpers/is-current-selected-still';
import {useKeybinding} from '../helpers/use-keybinding';
import {renderFrame} from '../state/render-frame';
import {Flex, Spacing} from './layout';
import {InputDragger} from './NewComposition/InputDragger';
import {TimelineTickFormatContext} from './Timeline/TimelineTickFormatProvider';
import {TimelineZoomControls} from './Timeline/TimelineZoomControls';

const text: React.CSSProperties = {
Expand All @@ -25,7 +32,7 @@ const text: React.CSSProperties = {
};

const currentTimeTypography: React.CSSProperties = {
color: WHITE,
color: LIGHT_TEXT,
display: 'inline-block',
fontSize: 14,
fontVariantNumeric: 'tabular-nums',
Expand All @@ -36,16 +43,37 @@ const currentTimeTypography: React.CSSProperties = {

const currentTimeInputStyle: React.CSSProperties = {
...currentTimeTypography,
padding: '4px 6px 4px 0',
padding: 0,
};

const currentTimeButtonStyle: React.CSSProperties = {
paddingLeft: 0,
const currentTimeStack: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
lineHeight: 1,
padding: '5px 7px 5px 1px',
transform: 'translateY(-1px)',
};

const currentTimeButtonStyle = {
display: 'block',
padding: 0,
border: 'none',
lineHeight: '21px',
'--remotion-cli-internals-blue-hovered': WHITE,
} as React.CSSProperties;

const currentTimeSubtitle: React.CSSProperties = {
color: LIGHT_TEXT,
...hoverableStyle({
idleBackground: TRANSPARENT,
hoverBackground: TRANSPARENT,
idleColor: LIGHT_TEXT,
hoverColor: WHITE,
}),
background: TRANSPARENT,
border: 'none',
padding: 0,
cursor: 'default',
display: 'block',
fontFamily: 'monospace',
fontSize: 10,
Expand All @@ -57,6 +85,10 @@ const currentTimeSubtitle: React.CSSProperties = {

export const TimeValue: React.FC = () => {
const frame = useCurrentFrame();
const {showFrames, setShowFrames} = useContext(TimelineTickFormatContext);
const toggleTickFormat = useCallback(() => {
setShowFrames((previous) => !previous);
}, [setShowFrames]);
const config = Internals.useUnsafeVideoConfig();
const isStill = useIsStill();
const {seek, play, pause, toggle} = PlayerInternals.usePlayerMethods();
Expand All @@ -81,10 +113,6 @@ export const TimeValue: React.FC = () => {
},
[config],
);
const formatterSubtitle = useCallback(
(value: string | number) => String(value),
[],
);
useImperativeHandle(
Internals.timeValueRef,
() => ({
Expand Down Expand Up @@ -127,21 +155,36 @@ export const TimeValue: React.FC = () => {

return (
<div style={text}>
<InputDragger
ref={ref}
aria-label={String(frame)}
value={frame}
onTextChange={onTextChange}
onValueChange={onValueChange}
formatter={formatter}
formatterStyle={currentTimeTypography}
formatterSubtitle={formatterSubtitle}
formatterSubtitleStyle={currentTimeSubtitle}
buttonStyle={currentTimeButtonStyle}
rightAlign={false}
status="ok"
style={currentTimeInputStyle}
/>
<div style={currentTimeStack}>
<InputDragger
ref={ref}
aria-label={String(frame)}
value={frame}
onTextChange={onTextChange}
onValueChange={onValueChange}
formatter={formatter}
formatterStyle={currentTimeTypography}
buttonStyle={currentTimeButtonStyle}
rightAlign={false}
status="ok"
style={currentTimeInputStyle}
/>
<button
type="button"
className={`${HOVERABLE_CLASS_NAME} ${FOCUS_VISIBLE_ONLY_CLASS_NAME}`}
style={currentTimeSubtitle}
onClick={toggleTickFormat}
aria-label="Show timeline ticks as frames"
aria-pressed={showFrames}
title={
showFrames
? 'Show timeline ticks as timecode'
: 'Show timeline ticks as frames'
}
>
{frame}
</button>
</div>
<Spacing x={2} />
<Flex />
<TimelineZoomControls sliderMaxWidth={80} />
Expand Down
Loading
Loading