Files
RUWIO/dev_docs/09_export-environment-copy-plan.md
2026-06-07 13:24:34 +02:00

9.5 KiB

Export Environment Copy Plan

Date: 2026-06-07

Goal

Make the export dialog copy describe what actually happens in each runtime:

  • Browser/PWA: MP4 uses the render server; WebM records through browser MediaRecorder.
  • Electron desktop: MP4 uses the desktop render bridge and bundled ffmpeg; WebM still records through the Chromium MediaRecorder runtime inside Electron.

The current labels are accurate for the browser build, but misleading inside Electron:

  • SERVER - FRAME-ACCURATE on MP4, even when Electron is rendering locally.
  • BROWSER - REALTIME on WebM, even when the app is running as Electron.

Current Code

Primary files:

  • src/components/ExportDialog.vue
  • src/i18n/messages.ts
  • src/lib/desktopRenderExport.ts
  • src/types/desktop.d.ts

Runtime detection already exists as an exported helper in desktopRenderExport.ts:

export function hasDesktopRender(): boolean {
  return !!window.ruwioDesktop?.renderMp4
}

This is the right signal for MP4 copy because ExportDialog.vue routes MP4 through renderMp4OnDesktop() when hasDesktopRender() returns true, otherwise it calls downloadFromServer(). The MP4 copy should reuse this exact function rather than reimplementing the predicate, so the text can never drift from the route (see Implementation Plan step 1).

window.ruwioDesktop is injected by the Electron preload before any app module loads and never changes during a session, so a runtime check is stable at module-init time — no reactivity is required to read it correctly.

Proposed Copy Matrix

MP4

Browser/PWA:

  • Badge: Server · Frame-by-frame
  • Description: H.264 export via render server
  • Progress: Rendering MP4 with render server...

Electron:

  • Badge: Desktop · Frame-by-frame
  • Description: H.264 export via bundled ffmpeg
  • Progress: Rendering MP4 in desktop app...

Rationale: both MP4 paths render deterministic frames through server/render.html. The key difference is where rendering happens: HTTP render server vs Electron desktop worker plus ffmpeg.

Avoid Frame-accurate in the short badge. It sounds like a stronger quality claim than the dialog needs, and it obscures the real user-facing distinction: server vs desktop. The help text can explain frame-by-frame rendering if needed.

Badge style: store badges in title case with a · middot separator, matching the existing sibling badges (gifBadge: 'Browser · Frame-by-frame', apngBadge: 'Browser · Lossless'). The .badge CSS already applies text-transform: uppercase, so do not write the strings in all-caps or switch to a - hyphen separator — that would render MP4/WebM inconsistently next to GIF/APNG in the same column.

WebM

Browser/PWA:

  • Badge: Browser · Realtime
  • Description: Recorded in real time with MediaRecorder
  • Progress: Rendering WebM in browser realtime...

Electron:

  • Badge: Chromium · Realtime
  • Description: Recorded in real time by Electron's Chromium runtime
  • Progress: Rendering WebM in Chromium realtime...

Rationale: the implementation does not become a desktop ffmpeg pipeline in Electron. It still uses MediaRecorder from the renderer process. Calling that browser in the desktop app is imprecise, but Chromium is technically true and short enough for the badge.

Implementation Plan

  1. Add environment booleans in ExportDialog.vue.

    import { hasDesktopRender } from '@/lib/desktopRenderExport'
    
    const isDesktop = computed(() => !!window.ruwioDesktop)
    const hasDesktopMp4 = computed(() => hasDesktopRender())
    

    Use hasDesktopMp4 for MP4 text. It wraps hasDesktopRender() — the same function the MP4 route already calls in exportMp4Server() — so the copy decision and the actual route share one predicate and can never diverge. Do not reimplement !!window.ruwioDesktop?.renderMp4 inline.

    Use isDesktop for WebM text because WebM is still available through renderer Chromium even if MP4 desktop rendering is unavailable; there is no existing helper for that distinction, so a local boolean is fine.

  2. Rename exportMp4Server() to exportMp4().

    The current name is now misleading because the function already branches:

    • Desktop: renderMp4OnDesktop(scene, ...)
    • Browser/server: downloadFromServer(scene, ...)
  3. Replace static MP4 bindings.

    Current:

    {{ t('export.mp4Badge') }}
    {{ t('export.mp4Desc') }}
    

    Proposed:

    {{ t(hasDesktopMp4 ? 'export.mp4DesktopBadge' : 'export.mp4ServerBadge') }}
    {{ t(hasDesktopMp4 ? 'export.mp4DesktopDesc' : 'export.mp4ServerDesc') }}
    
  4. Replace static WebM bindings.

    Current:

    {{ t('export.webmBadge') }}
    {{ videoSupported ? t('export.webmDesc') : t('export.webmNotSupported') }}
    

    Proposed:

    {{ t(isDesktop ? 'export.webmDesktopBadge' : 'export.webmBrowserBadge') }}
    {{
      videoSupported
        ? t(isDesktop ? 'export.webmDesktopDesc' : 'export.webmBrowserDesc')
        : t('export.webmNotSupported')
    }}
    
  5. Split progress labels.

    MP4 currently uses:

    label: t('export.labelMp4')
    

    Proposed:

    label: t(hasDesktopMp4.value ? 'export.labelMp4Desktop' : 'export.labelMp4Server')
    

    WebM currently uses:

    label: t('export.labelWebm')
    

    Proposed:

    label: t(isDesktop.value ? 'export.labelWebmDesktop' : 'export.labelWebmBrowser')
    
  6. Update src/i18n/messages.ts in both locales.

    English keys to add:

    mp4ServerBadge: 'Server · Frame-by-frame',
    mp4ServerDesc: 'H.264 export via render server',
    mp4DesktopBadge: 'Desktop · Frame-by-frame',
    mp4DesktopDesc: 'H.264 export via bundled ffmpeg',
    webmBrowserBadge: 'Browser · Realtime',
    webmBrowserDesc: 'Recorded in real time with MediaRecorder',
    webmDesktopBadge: 'Chromium · Realtime',
    webmDesktopDesc: "Recorded in real time by Electron's Chromium runtime",
    labelMp4Server: 'Rendering MP4 with render server...',
    labelMp4Desktop: 'Rendering MP4 in desktop app...',
    labelWebmBrowser: 'Rendering WebM in browser realtime...',
    labelWebmDesktop: 'Rendering WebM in Chromium realtime...',
    

    Badges stay title case with a · separator; the .badge CSS uppercases them at render time (see the MP4 copy-matrix note above). German keys should mirror the meaning rather than preserve the old wording.

  7. Fix the export-button help string exportHelp in messages.ts.

    This is separate from the help dialog and is easy to miss. Both locales currently hardcode the misleading claim:

    • messages.ts:43 (en): "...MP4 needs the render server; WebM records in real time in the browser."
    • messages.ts:1153 (de): "...MP4 braucht den Render-Server. WebM wird direkt im Browser in Echtzeit aufgenommen."

    Add runtime-branched variants (e.g. exportHelpServer / exportHelpDesktop) and select with the same isDesktop signal wherever t('...exportHelp') is consumed. This is exactly the "globally visible text saying MP4 always needs the render server" that must not be left in place.

  8. Update help-dialog format text in messages.ts, and branch it now.

    The strings live at help.content.export.formats.mp4 / .webm (messages.ts:1081/1083 en, :1923/1925 de) and exportHelp (handled in step 7). Browser help should describe:

    • MP4 as render-server frame-by-frame export.
    • WebM as browser MediaRecorder realtime recording.

    Electron help should describe:

    • MP4 as bundled-ffmpeg desktop frame-by-frame export.
    • WebM as Chromium MediaRecorder realtime recording.

    Wiring the branch is cheaper than the old draft assumed. HelpDialog.vue defines CHAPTERS: Chapter[] as a module-level const with static bodyKey strings, but window.ruwioDesktop is set at preload before that module loads and never changes, so a plain ternary on the bodyKey resolves correctly at module-init time without making CHAPTERS reactive:

    { bodyKey: window.ruwioDesktop?.renderMp4
        ? 'help.content.export.formats.mp4Desktop'
        : 'help.content.export.formats.mp4' },
    { bodyKey: window.ruwioDesktop
        ? 'help.content.export.formats.webmDesktop'
        : 'help.content.export.formats.webm' },
    

    Add the *Desktop keys in both locales and wire the branch in the same change. Do not defer this and leave the help dialog asserting MP4 always needs the render server.

Verification

Browser/Vite:

  • Open the export dialog.
  • MP4 badge renders SERVER · FRAME-BY-FRAME (stored title case, uppercased by CSS).
  • MP4 description mentions the render server.
  • WebM badge renders BROWSER · REALTIME.
  • WebM description mentions MediaRecorder.
  • Export-button help (exportHelp) and help dialog describe MP4 as a render-server export.

Electron:

  • Open the export dialog.
  • MP4 badge renders DESKTOP · FRAME-BY-FRAME.
  • MP4 description mentions bundled ffmpeg.
  • WebM badge renders CHROMIUM · REALTIME.
  • WebM description mentions Electron Chromium.
  • Export-button help (exportHelp) and help dialog describe MP4 as a bundled-ffmpeg desktop export, not the render server.

Build checks:

npm run build
npm run desktop:dir

Optional automated coverage:

  • Add a small in-app devtools test that stubs/removes window.ruwioDesktop and verifies the selected i18n keys or rendered badge text. src/devtools/headless.ts already reads !!window.ruwioDesktop (line 19) and is the natural home for it.
  • Add an Electron harness assertion that export dialog MP4 copy is desktop copy.