mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
Add sidecar audio fallback for silent recordings
This commit is contained in:
Vendored
+3
@@ -137,6 +137,9 @@ interface Window {
|
||||
readLocalFile: (
|
||||
filePath: string,
|
||||
) => Promise<{ success: boolean; data?: Uint8Array; error?: string }>;
|
||||
getVideoAudioFallbackPaths: (
|
||||
videoPath: string,
|
||||
) => Promise<{ success: boolean; paths: string[]; error?: string }>;
|
||||
setRecordingState: (recording: boolean) => Promise<void>;
|
||||
getCursorTelemetry: (videoPath?: string) => Promise<{
|
||||
success: boolean;
|
||||
|
||||
+93
-22
@@ -34,6 +34,10 @@ const RECORDING_SESSION_MANIFEST_SUFFIX = '.recordly-session.json'
|
||||
const WHISPER_MODEL_DOWNLOAD_URL = 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin'
|
||||
const WHISPER_MODEL_DIR = path.join(USER_DATA_PATH, 'whisper')
|
||||
const WHISPER_SMALL_MODEL_PATH = path.join(WHISPER_MODEL_DIR, 'ggml-small.bin')
|
||||
const COMPANION_AUDIO_LAYOUTS = [
|
||||
{ platform: 'mac' as const, systemSuffix: '.system.m4a', micSuffix: '.mic.m4a' },
|
||||
{ platform: 'win' as const, systemSuffix: '.system.wav', micSuffix: '.mic.wav' },
|
||||
]
|
||||
|
||||
function getAssetRootPath() {
|
||||
if (app.isPackaged) {
|
||||
@@ -309,6 +313,77 @@ function parseFfmpegDurationSeconds(stderr: string) {
|
||||
return hours * 3600 + minutes * 60 + seconds
|
||||
}
|
||||
|
||||
type CompanionAudioCandidate = {
|
||||
platform: (typeof COMPANION_AUDIO_LAYOUTS)[number]['platform']
|
||||
systemPath: string
|
||||
micPath: string
|
||||
usablePaths: string[]
|
||||
}
|
||||
|
||||
async function getUsableCompanionAudioCandidates(videoPath: string): Promise<CompanionAudioCandidate[]> {
|
||||
const basePath = videoPath.replace(/\.[^.]+$/u, '')
|
||||
const candidates: CompanionAudioCandidate[] = []
|
||||
|
||||
for (const layout of COMPANION_AUDIO_LAYOUTS) {
|
||||
const systemPath = `${basePath}${layout.systemSuffix}`
|
||||
const micPath = `${basePath}${layout.micSuffix}`
|
||||
const usablePaths: string[] = []
|
||||
|
||||
for (const companionPath of [systemPath, micPath]) {
|
||||
try {
|
||||
const stat = await fs.stat(companionPath)
|
||||
if (stat.size > 0) {
|
||||
usablePaths.push(companionPath)
|
||||
}
|
||||
} catch {
|
||||
// Missing companion audio is expected for many recordings.
|
||||
}
|
||||
}
|
||||
|
||||
if (usablePaths.length > 0) {
|
||||
candidates.push({
|
||||
platform: layout.platform,
|
||||
systemPath,
|
||||
micPath,
|
||||
usablePaths,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
async function hasEmbeddedAudioStream(videoPath: string) {
|
||||
const ffmpegPath = getFfmpegBinaryPath()
|
||||
let stderr = ''
|
||||
|
||||
try {
|
||||
const result = await execFileAsync(
|
||||
ffmpegPath,
|
||||
['-hide_banner', '-i', videoPath, '-map', '0:a:0', '-frames:a', '1', '-f', 'null', '-'],
|
||||
{ timeout: 20000, maxBuffer: 10 * 1024 * 1024 },
|
||||
)
|
||||
stderr = result.stderr
|
||||
} catch (error) {
|
||||
stderr = (error as NodeJS.ErrnoException & { stderr?: string }).stderr ?? ''
|
||||
}
|
||||
|
||||
return /Stream #.*Audio:/i.test(stderr)
|
||||
}
|
||||
|
||||
async function getCompanionAudioFallbackPaths(videoPath: string) {
|
||||
const companionCandidates = await getUsableCompanionAudioCandidates(videoPath)
|
||||
if (companionCandidates.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (await hasEmbeddedAudioStream(videoPath)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return companionCandidates.flatMap((candidate) => candidate.usablePaths)
|
||||
}
|
||||
|
||||
async function validateRecordedVideo(videoPath: string) {
|
||||
const stat = await fs.stat(videoPath)
|
||||
if (!stat.isFile()) {
|
||||
@@ -2804,32 +2879,15 @@ function snapshotCursorTelemetryForPersistence() {
|
||||
async function finalizeStoredVideo(videoPath: string) {
|
||||
// Safety net: if companion audio files still exist, the mux was skipped — attempt it now
|
||||
if (videoPath.endsWith('.mp4')) {
|
||||
const base = videoPath.replace(/\.mp4$/i, '')
|
||||
// macOS uses .m4a, Windows uses .wav
|
||||
const candidates = [
|
||||
{ system: `${base}.system.m4a`, mic: `${base}.mic.m4a`, platform: 'mac' as const },
|
||||
{ system: `${base}.system.wav`, mic: `${base}.mic.wav`, platform: 'win' as const },
|
||||
]
|
||||
for (const { system, mic, platform } of candidates) {
|
||||
let hasUnmuxedAudio = false
|
||||
for (const siblingPath of [system, mic]) {
|
||||
try {
|
||||
const stat = await fs.stat(siblingPath)
|
||||
if (stat.size > 0) {
|
||||
hasUnmuxedAudio = true
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
// file doesn't exist — expected if mux already succeeded or audio wasn't enabled
|
||||
}
|
||||
}
|
||||
if (hasUnmuxedAudio) {
|
||||
const companionCandidates = await getUsableCompanionAudioCandidates(videoPath)
|
||||
for (const { systemPath, micPath, platform } of companionCandidates) {
|
||||
if (platform === 'mac' || platform === 'win') {
|
||||
console.log(`[finalize] Detected un-muxed ${platform} audio files alongside video — attempting safety-net mux`)
|
||||
try {
|
||||
if (platform === 'win') {
|
||||
await muxNativeWindowsVideoWithAudio(videoPath, system, mic)
|
||||
await muxNativeWindowsVideoWithAudio(videoPath, systemPath, micPath)
|
||||
} else {
|
||||
await muxNativeMacRecordingWithAudio(videoPath, system, mic)
|
||||
await muxNativeMacRecordingWithAudio(videoPath, systemPath, micPath)
|
||||
}
|
||||
console.log('[finalize] Safety-net mux completed successfully')
|
||||
} catch (error) {
|
||||
@@ -4095,6 +4153,19 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
|
||||
return { success: true, diagnostics: lastNativeCaptureDiagnostics }
|
||||
})
|
||||
|
||||
ipcMain.handle('get-video-audio-fallback-paths', async (_event, videoPath: string) => {
|
||||
if (!videoPath) {
|
||||
return { success: true, paths: [] }
|
||||
}
|
||||
|
||||
try {
|
||||
return { success: true, paths: await getCompanionAudioFallbackPaths(videoPath) }
|
||||
} catch (error) {
|
||||
console.error('Failed to resolve companion audio fallback paths:', error)
|
||||
return { success: false, paths: [], error: String(error) }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('mux-native-windows-recording', async (_event, pauseSegments?: PauseSegment[]) => {
|
||||
const videoPath = windowsPendingVideoPath
|
||||
windowsPendingVideoPath = null
|
||||
|
||||
@@ -37,6 +37,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
readLocalFile: (filePath: string) => {
|
||||
return ipcRenderer.invoke("read-local-file", filePath);
|
||||
},
|
||||
getVideoAudioFallbackPaths: (videoPath: string) => {
|
||||
return ipcRenderer.invoke("get-video-audio-fallback-paths", videoPath);
|
||||
},
|
||||
getSources: async (opts: Electron.SourcesOptions) => {
|
||||
return await ipcRenderer.invoke("get-sources", opts);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user