-
Notifications
You must be signed in to change notification settings - Fork 137
feat: add voiceover and background music layers to the editor #526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cea46c2
d74baa3
bba8e4e
6a855e8
5af120f
156e407
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,6 +103,18 @@ const ALLOWED_IMPORT_VIDEO_EXTENSIONS = new Set([ | |
| ".flv", | ||
| ".ts", | ||
| ]); | ||
| // Mirrors DocumentService's audio set — anything an <audio> element can decode. | ||
| const ALLOWED_IMPORT_AUDIO_EXTENSIONS = new Set([ | ||
| ".mp3", | ||
| ".wav", | ||
| ".m4a", | ||
| ".aac", | ||
| ".ogg", | ||
| ".oga", | ||
| ".opus", | ||
| ".flac", | ||
| ".webm", | ||
| ]); | ||
| const PREVIEW_AUDIO_DIR = path.join(app.getPath("userData"), "preview-audio"); | ||
| const nativeMacCaptureEvents = new EventEmitter(); | ||
|
|
||
|
|
@@ -180,7 +192,12 @@ function buildDialogOptions<T extends Electron.OpenDialogOptions | Electron.Save | |
| } | ||
|
|
||
| function hasAllowedImportVideoExtension(filePath: string): boolean { | ||
| return ALLOWED_IMPORT_VIDEO_EXTENSIONS.has(path.extname(filePath).toLowerCase()); | ||
| const ext = path.extname(filePath).toLowerCase(); | ||
| // Audio extensions ride the same approval gate: the read-binary-file / | ||
| // get-readable-file-info handlers serve audio-layer assets (voiceover / | ||
| // music) exactly like video assets, and both go through | ||
| // `approveReadableVideoPath`. | ||
| return ALLOWED_IMPORT_VIDEO_EXTENSIONS.has(ext) || ALLOWED_IMPORT_AUDIO_EXTENSIONS.has(ext); | ||
| } | ||
|
|
||
| function runProcess( | ||
|
|
@@ -3620,6 +3637,74 @@ export function registerIpcHandlers( | |
| } | ||
| }); | ||
|
|
||
| // Audio-layer import (voiceover file / background music). Same shape as | ||
| // open-video-file-picker, with audio filters and an audio extension gate. | ||
| ipcMain.handle("open-audio-file-picker", async () => { | ||
| try { | ||
| const dialogOptions = buildDialogOptions( | ||
| { | ||
| title: mainT("dialogs", "fileDialogs.selectAudio"), | ||
| defaultPath: RECORDINGS_DIR, | ||
| filters: [ | ||
| { | ||
| name: mainT("dialogs", "fileDialogs.audioFiles"), | ||
| extensions: [...ALLOWED_IMPORT_AUDIO_EXTENSIONS], | ||
| }, | ||
|
Comment on lines
+3649
to
+3652
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🌐 Web query:
💡 Result: No, the Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- applicable repository guidance ---'
find /tmp/coderabbit-repo-knowledge/getopenscreen-openscreen-086fd783 -type f -name '*.md' -print | sort | while IFS= read -r f; do
case "$f" in
*/learnings/*) ;;
*) printf '\n[%s]\n' "$f"; head -80 "$f" ;;
esac
done
printf '%s\n' '--- relevant source and dependency bindings ---'
rg -n -C 6 'ALLOWED_IMPORT_AUDIO_EXTENSIONS|fileDialogs\.audioFiles|showOpenDialog|electron' electron/ipc/handlers.ts package.json package-lock.json npm-shrinkwrap.json yarn.lock 2>/dev/null || trueRepository: getopenscreen/openscreen Length of output: 50380 Pass dotless extensions to
🧰 Tools🪛 ast-grep (0.45.2)[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec. (detect-child-process-typescript) 🤖 Prompt for AI Agents |
||
| { name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] }, | ||
| ], | ||
| properties: ["openFile"], | ||
| }, | ||
| getMainWindow(), | ||
| ); | ||
| const result = await dialog.showOpenDialog(dialogOptions); | ||
| if (result.canceled || result.filePaths.length === 0) { | ||
| return { success: false, canceled: true }; | ||
| } | ||
| const picked = result.filePaths[0]; | ||
| if (!ALLOWED_IMPORT_AUDIO_EXTENSIONS.has(path.extname(picked).toLowerCase())) { | ||
| return { | ||
| success: false, | ||
| message: "Selected file is not a supported audio file", | ||
| }; | ||
| } | ||
| return { | ||
| success: true, | ||
| path: picked, | ||
| }; | ||
| } catch (error) { | ||
| console.error("Failed to open audio file picker:", error); | ||
| return { | ||
| success: false, | ||
| message: "Failed to open audio file picker", | ||
| error: String(error), | ||
| }; | ||
| } | ||
| }); | ||
|
|
||
| // In-editor voiceover recording: the renderer hands over the raw | ||
| // MediaRecorder blob (webm/opus) and gets back the path it landed at, under | ||
| // the recordings dir so it lives with the project's other media and survives | ||
| // relaunches. | ||
| ipcMain.handle("save-recorded-voiceover", async (_event, data: ArrayBuffer) => { | ||
| try { | ||
| if (!(data instanceof ArrayBuffer) || data.byteLength === 0) { | ||
| return { success: false, message: "Empty recording" }; | ||
| } | ||
| await fs.mkdir(RECORDINGS_DIR, { recursive: true }); | ||
| const fileName = `voiceover-${new Date().toISOString().replace(/[:.]/g, "-")}.webm`; | ||
| const target = path.join(RECORDINGS_DIR, fileName); | ||
| await fs.writeFile(target, Buffer.from(data)); | ||
| return { success: true, path: target }; | ||
| } catch (error) { | ||
| console.error("Failed to save recorded voiceover:", error); | ||
| return { | ||
| success: false, | ||
| message: "Failed to save recorded voiceover", | ||
| error: String(error), | ||
| }; | ||
| } | ||
| }); | ||
|
|
||
| ipcMain.handle("reveal-in-folder", async (_, filePath: string) => { | ||
| try { | ||
| // showItemInFolder returns nothing, it throws on error | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Keep
primaryAssetIdvideo-only after deletion.When an audio asset remains before the primary video, deleting that video assigns
assets[0]?.idat Lines 366-369. This persists an audio asset asprimaryAssetId. A later video import will also not replace that invalid primary asset.Select the first remaining video asset, or
undefined.Proposed fix
🤖 Prompt for AI Agents