+ Connect to Remotion Studio +
+- {installStatus.message} + {installStatus.type === 'installing' + ? 'If your browser prompts you, allow local network access so this page can find Remotion Studio.' + : installStatus.message}
) : null} > diff --git a/packages/example/e2e/studio-protocol.test.mts b/packages/example/e2e/studio-protocol.test.mts index 91362523d2d..00c1db8d86a 100644 --- a/packages/example/e2e/studio-protocol.test.mts +++ b/packages/example/e2e/studio-protocol.test.mts @@ -355,11 +355,25 @@ const CloseupPlaceholder = () => { studioProtocolRequests.length = 0; await installInStudio.click(); - const dialog = studioPage.getByRole('dialog'); - await expect(dialog.getByText('Install Element')).toBeVisible(); - await expect(dialog.getByText(/Protocol Element.*MyComp/)).toBeVisible(); + const dialog = studioPage.getByRole('dialog', { + name: 'Install Protocol Element', + }); + await expect(dialog).toBeVisible(); + const currentDestination = dialog.getByRole('radio', { + name: 'Current composition', + }); + const newDestination = dialog.getByRole('radio', { + name: 'New composition', + }); + await expect(currentDestination).toBeChecked(); + await currentDestination.press('ArrowRight'); + await expect(newDestination).toBeChecked(); + await newDestination.press('ArrowLeft'); + await expect(currentDestination).toBeChecked(); await expect(dialog.getByText(senderUrl, {exact: true})).toBeVisible(); - await expect(decoyStudioPage.getByText('Install Element')).toHaveCount(0); + await expect( + decoyStudioPage.getByText('Install Protocol Element', {exact: true}), + ).toHaveCount(0); await expect(elementsIframe).toHaveCount(0); expect(studioProtocolRequests).toEqual([]); await dialog.getByRole('button', {name: /Install/}).click(); @@ -389,12 +403,12 @@ const CloseupPlaceholder = () => { await senderPage.goto(senderUrl); await senderPage.getByRole('button', {name: 'Install in Studio'}).click(); await studioPage.bringToFront(); - const newCompositionDialog = studioPage.getByRole('dialog'); - await expect( - newCompositionDialog.getByText('Install Element'), - ).toBeVisible(); + const newCompositionDialog = studioPage.getByRole('dialog', { + name: 'Install Protocol Element', + }); + await expect(newCompositionDialog).toBeVisible(); await newCompositionDialog - .getByRole('button', {name: 'New composition'}) + .getByRole('radio', {name: 'New composition'}) .click(); await expect( newCompositionDialog.getByPlaceholder('Composition ID'), diff --git a/packages/studio-protocol/src/add-element-library-to-studio.ts b/packages/studio-protocol/src/add-element-library-to-studio.ts index 7f8b3a41847..8c2718def8a 100644 --- a/packages/studio-protocol/src/add-element-library-to-studio.ts +++ b/packages/studio-protocol/src/add-element-library-to-studio.ts @@ -6,7 +6,6 @@ import { fetchWithTimeout, focusedStudioMaxAge, getAddElementLibraryCapability, - hasLegacyStudio, isAbortError, studioProtocolProbePorts, } from './studio-discovery'; @@ -154,13 +153,6 @@ export const addElementLibraryToStudioWithDependencies = async ( ); } - if (await hasLegacyStudio(dependencies)) { - return failure( - 'studio-upgrade-required', - 'This Remotion Studio cannot add an Element catalog through Studio Protocol. Upgrade Remotion to 4.0.518 or newer.', - ); - } - if (discovery.foundInvalidResponse) { return failure( 'invalid-response', diff --git a/packages/studio-protocol/src/install-in-studio.ts b/packages/studio-protocol/src/install-in-studio.ts index 0ff04f6ab0b..3789fa90649 100644 --- a/packages/studio-protocol/src/install-in-studio.ts +++ b/packages/studio-protocol/src/install-in-studio.ts @@ -9,7 +9,6 @@ import { fetchWithTimeout, focusedStudioMaxAge, getInstallCapability, - hasLegacyStudio, isAbortError, studioProtocolProbePorts, } from './studio-discovery'; @@ -21,6 +20,7 @@ import { export type InstallInStudioErrorCode = | 'unsupported-origin' | 'no-compatible-studio' + | 'loopback-network-permission-denied' | 'studio-upgrade-required' | 'no-installable-target' | 'unsupported-protocol' @@ -47,11 +47,18 @@ export type InstallInStudioResult = readonly message: string; }; +type LoopbackPermissionName = 'loopback-network' | 'local-network-access'; + +type PermissionQueryFn = (descriptor: { + readonly name: LoopbackPermissionName; +}) => Promise<{readonly state: PermissionState}>; + export type InstallInStudioDependencies = { readonly fetchFn: StudioProtocolFetcher; readonly now: () => number; readonly ports: readonly number[]; readonly pageOrigin: string | null; + readonly permissionQueryFn: PermissionQueryFn | null; }; export type StudioProtocolInstallRequest = { @@ -178,13 +185,6 @@ export const installInStudioWithDependencies = async ( ); } - if (await hasLegacyStudio(dependencies)) { - return failure( - 'studio-upgrade-required', - 'This Remotion Studio does not support the Remotion Studio Protocol. Upgrade Remotion to 4.0.502 or newer.', - ); - } - if (discovery.foundInvalidResponse) { return failure( 'invalid-response', @@ -192,6 +192,27 @@ export const installInStudioWithDependencies = async ( ); } + if (dependencies.permissionQueryFn !== null) { + for (const name of [ + 'loopback-network', + 'local-network-access', + ] as const) { + try { + const permission = await dependencies.permissionQueryFn({name}); + if (permission.state === 'denied') { + return failure( + 'loopback-network-permission-denied', + 'Access to localhost is blocked by your browser. Open the site settings, allow local network access for this site, then try again.', + ); + } + + break; + } catch { + // The compatibility alias may still be supported. + } + } + } + return failure( 'no-compatible-studio', 'Start Remotion Studio and open a composition, then try again.', @@ -363,6 +384,13 @@ export const installInStudio = async ({ typeof globalThis.location === 'undefined' ? null : globalThis.location.origin, + permissionQueryFn: + typeof globalThis.navigator === 'undefined' || + typeof globalThis.navigator.permissions?.query !== 'function' + ? null + : (descriptor) => + // @ts-expect-error Chromium's loopback permission names are not in lib.dom yet. + globalThis.navigator.permissions.query(descriptor), ports: studioProtocolProbePorts, }); }; diff --git a/packages/studio-protocol/src/set-license-key-in-studio.ts b/packages/studio-protocol/src/set-license-key-in-studio.ts index 54afb060538..221430aef83 100644 --- a/packages/studio-protocol/src/set-license-key-in-studio.ts +++ b/packages/studio-protocol/src/set-license-key-in-studio.ts @@ -6,7 +6,6 @@ import { fetchWithTimeout, focusedStudioMaxAge, getSetLicenseKeyCapability, - hasLegacyStudio, isAbortError, studioProtocolProbePorts, } from './studio-discovery'; @@ -96,15 +95,6 @@ export const setLicenseKeyInStudioWithDependencies = async ( }; } - if (await hasLegacyStudio(dependencies)) { - return { - success: false, - code: 'studio-upgrade-required', - message: - 'This Remotion Studio cannot set a license key through Studio Protocol. Upgrade Remotion to 4.0.504 or newer.', - }; - } - if (discovery.foundInvalidResponse) { return { success: false, diff --git a/packages/studio-protocol/src/studio-discovery.ts b/packages/studio-protocol/src/studio-discovery.ts index a0f2cb372d5..501acbc4dd9 100644 --- a/packages/studio-protocol/src/studio-discovery.ts +++ b/packages/studio-protocol/src/studio-discovery.ts @@ -110,14 +110,13 @@ const descriptorSchema = z return new Set(capabilityTypes).size === capabilityTypes.length; }), ); +const protocolEnvelopeSchema = z.looseObject({ + protocol: z.literal('remotion-studio-protocol'), +}); const protocolVersionEnvelopeSchema = z.looseObject({ protocol: z.literal('remotion-studio-protocol'), protocolVersion: z.unknown(), }); -const legacyStudioSchema = z.looseObject({ - type: z.literal('remotion-studio'), -}); - export const fetchWithTimeout = async ({ fetchFn, options, @@ -206,15 +205,14 @@ export const discoverStudios = async ( }> => { let foundUnsupportedProtocol = false; let foundInvalidResponse = false; + const {fetchFn} = dependencies; const studios = await Promise.all( dependencies.ports.map(async (port): Promise