diff --git a/apps/desktop/src/components/assistant-ui/markdown-text.tsx b/apps/desktop/src/components/assistant-ui/markdown-text.tsx index 15842f15695e..fdcf94e16bb0 100644 --- a/apps/desktop/src/components/assistant-ui/markdown-text.tsx +++ b/apps/desktop/src/components/assistant-ui/markdown-text.tsx @@ -27,8 +27,8 @@ import { mediaKind, mediaName, mediaPathFromMarkdownHref, - mediaStreamUrl, - resolveMediaDisplaySrc + resolveMediaDisplaySrc, + resolveMediaPlaybackSrc } from '@/lib/media' import { previewTargetFromMarkdownHref } from '@/lib/preview-targets' import { sessionRefFromMarkdownHref } from '@/lib/session-refs' @@ -98,16 +98,6 @@ function preprocessWithTailRepair(text: string): string { } } -async function mediaSrc(path: string): Promise { - // Stream audio/video through the custom protocol: data URLs are capped and - // load the whole file into memory, which broke playback for larger videos. - if (window.hermesDesktop && ['audio', 'video'].includes(mediaKind(path))) { - return mediaStreamUrl(path) - } - - return resolveMediaDisplaySrc(path) -} - function useOpenMediaFile(path: string) { const [openFailed, setOpenFailed] = useState(false) @@ -170,7 +160,7 @@ function MediaAttachment({ path }: { path: string }) { } } - void mediaSrc(path) + void resolveMediaPlaybackSrc(path) .then(value => { if (value.startsWith('blob:')) { objectUrl = value diff --git a/apps/desktop/src/lib/media.remote.test.ts b/apps/desktop/src/lib/media.remote.test.ts index 79676c691431..c16835cd676a 100644 --- a/apps/desktop/src/lib/media.remote.test.ts +++ b/apps/desktop/src/lib/media.remote.test.ts @@ -11,7 +11,8 @@ import { isInlineMediaSrc, isRemoteGateway, mediaExternalUrl, - resolveMediaDisplaySrc + resolveMediaDisplaySrc, + resolveMediaPlaybackSrc } from './media' describe('isRemoteGateway', () => { @@ -139,6 +140,40 @@ describe('resolveMediaDisplaySrc', () => { }) }) +describe('resolveMediaPlaybackSrc', () => { + afterEach(() => { + vi.unstubAllGlobals() + $connection.set(null) + }) + + it('keeps a remote HTTPS video URL unchanged', async () => { + vi.stubGlobal('window', { hermesDesktop: { api: vi.fn() } }) + $connection.set({ mode: 'remote', baseUrl: 'https://gateway.test', token: 'secret' } as never) + + await expect(resolveMediaPlaybackSrc('https://cdn.example.com/render.mp4')).resolves.toBe( + 'https://cdn.example.com/render.mp4' + ) + }) + + it('routes gateway-local video through the authenticated download endpoint', async () => { + vi.stubGlobal('window', { hermesDesktop: { api: vi.fn() } }) + $connection.set({ mode: 'remote', baseUrl: 'https://gateway.test', token: 's e/cret' } as never) + + await expect(resolveMediaPlaybackSrc('/root/outputs/render.mp4')).resolves.toBe( + 'https://gateway.test/api/files/download?path=%2Froot%2Foutputs%2Frender.mp4&token=s%20e%2Fcret' + ) + }) + + it('uses the Electron streaming protocol for local desktop video', async () => { + vi.stubGlobal('window', { hermesDesktop: { api: vi.fn() } }) + $connection.set({ mode: 'local' } as never) + + await expect(resolveMediaPlaybackSrc('C:\\renders\\demo.mp4')).resolves.toBe( + 'hermes-media://stream/C%3A%5Crenders%5Cdemo.mp4' + ) + }) +}) + describe('gatewayMediaDataUrl', () => { const api = vi.fn(async ({ path }: { path: string }) => { if (path.startsWith('/api/fs/read-data-url?')) { diff --git a/apps/desktop/src/lib/media.ts b/apps/desktop/src/lib/media.ts index c392caa4d7bd..33bcfbd963f8 100644 --- a/apps/desktop/src/lib/media.ts +++ b/apps/desktop/src/lib/media.ts @@ -82,6 +82,22 @@ export async function resolveMediaDisplaySrc(path: string): Promise { return window.hermesDesktop.readFileDataUrl(filePathFromMediaPath(path)) } +// Audio/video need a seekable source instead of a whole-file data URL. Keep +// remote URLs untouched, route gateway-local files through the authenticated +// download endpoint, and reserve the Electron protocol for files on this +// desktop machine. +export async function resolveMediaPlaybackSrc(path: string): Promise { + if (isInlineMediaSrc(path)) { + return path + } + + if (window.hermesDesktop && ['audio', 'video'].includes(mediaKind(path))) { + return isRemoteGateway() ? mediaExternalUrl(path) : mediaStreamUrl(path) + } + + return resolveMediaDisplaySrc(path) +} + // Resolve a media path to a URL the shell can open. Remote mode rewrites // gateway-local paths to an authenticated /api/files/download URL (the file // lives on the gateway, not this disk); local mode keeps the file:// form.