diff --git a/examples/sound-effects/video-to-sfx/app/page.tsx b/examples/sound-effects/video-to-sfx/app/page.tsx index f934e69..8662088 100644 --- a/examples/sound-effects/video-to-sfx/app/page.tsx +++ b/examples/sound-effects/video-to-sfx/app/page.tsx @@ -13,7 +13,7 @@ import { observer } from "mobx-react"; import { cn } from "@/lib/utils"; import { autorun, reaction, when } from "mobx"; -export const HoverOverlay = ({ className }: { className?: string }) => { +const HoverOverlay = ({ className }: { className?: string }) => { return (
{ }; import { convertVideoToSFX } from "@/lib/videoToSFX"; import { useMutation } from "@tanstack/react-query"; +import { DownloadIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { mergeAndDownload } from "@/lib/mergeAndDownload"; const LoadingIndicator = () => { const { ref, replay } = useScramble({ @@ -273,16 +276,24 @@ const Home = observer(() => { {orchestrator.sfxPlayers.map((player, index) => ( - { - orchestrator.play(index); - }} - onPause={() => orchestrator.stop()} - player={player} - active={orchestrator.activeIndex === index} - /> +
+ orchestrator.play(index)} + onPause={() => orchestrator.stop()} + player={player} + active={orchestrator.activeIndex === index} + onDownload={() => { + const url = orchestrator.getAudioUrl(index); + if (!file || !url) { + window.alert("Error downloading"); + return; + } + mergeAndDownload(file, url); + }} + /> +
))}
@@ -331,12 +342,14 @@ const SoundEffect = observer( onPlay, onPause, active, + onDownload, }: { index: number; player: AudioPlayer; onPlay: () => void; onPause: () => void; active: boolean; + onDownload: () => void; }) => { return ( { if (player.playing) { onPause?.(); @@ -366,19 +379,33 @@ const SoundEffect = observer( }} > -
- - - - +
+
+ + + + +
+ ); } diff --git a/examples/sound-effects/video-to-sfx/app/state/orchestrator.ts b/examples/sound-effects/video-to-sfx/app/state/orchestrator.ts index b46314a..0f4d04e 100644 --- a/examples/sound-effects/video-to-sfx/app/state/orchestrator.ts +++ b/examples/sound-effects/video-to-sfx/app/state/orchestrator.ts @@ -55,4 +55,9 @@ export class Orchestrator { stop() { this.playing = false; } + + getAudioUrl(index: number) { + const player = this.sfxPlayers[index]; + return player.data; + } } diff --git a/examples/sound-effects/video-to-sfx/app/state/player.ts b/examples/sound-effects/video-to-sfx/app/state/player.ts index 9393e07..b5bba92 100644 --- a/examples/sound-effects/video-to-sfx/app/state/player.ts +++ b/examples/sound-effects/video-to-sfx/app/state/player.ts @@ -82,11 +82,13 @@ export class AudioPlayer { audio: HTMLAudioElement; progress: number = 0; playing: boolean; + data: string; constructor(data: string) { this.audioLoaded = false; this.waveformLoaded = false; this.playing = false; + this.data = data; this._player = new Tone.Player( data, action(() => { diff --git a/examples/sound-effects/video-to-sfx/lib/mergeAndDownload.ts b/examples/sound-effects/video-to-sfx/lib/mergeAndDownload.ts new file mode 100644 index 0000000..2168315 --- /dev/null +++ b/examples/sound-effects/video-to-sfx/lib/mergeAndDownload.ts @@ -0,0 +1,69 @@ +export async function mergeAndDownload( + videoFile: File | null, + audioData: string +) { + const { FFmpeg } = await import("@ffmpeg/ffmpeg"); + const { fetchFile, toBlobURL } = await import("@ffmpeg/util"); + const ffmpeg = new FFmpeg(); + + const load = async () => { + const baseURL = "https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd"; + ffmpeg.on("log", ({ message }) => { + console.log(message); + }); + await ffmpeg.load({ + coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"), + wasmURL: await toBlobURL( + `${baseURL}/ffmpeg-core.wasm`, + "application/wasm" + ), + }); + }; + const process = async () => { + console.log("transcoding"); + if (!videoFile) { + throw new Error("No video file"); + } + + if (videoFile) { + await ffmpeg.writeFile( + "input.mp4", + await fetchFile(URL.createObjectURL(videoFile)) + ); + } + + await ffmpeg.writeFile("audio.mpeg", await fetchFile(audioData)); + + await ffmpeg.exec(["-v", "error", "-i", "input.mp4", "-f", "null", "-"]); + + await ffmpeg.exec(["-v", "error", "-i", "audio.mpeg", "-f", "null", "-"]); + + await ffmpeg.exec([ + "-v", + "verbose", + "-i", + "input.mp4", + "-i", + "audio.mpeg", + "-c:v", + "copy", // Copy the video codec + "-c:a", + "aac", // Transcode audio to AAC + "-strict", + "experimental", + "output.mp4", + ]); + console.log("transcoding completed"); + const data = await ffmpeg.readFile("output.mp4"); + const final_url = URL.createObjectURL( + new Blob([data.buffer], { type: "video/mp4" }) + ); + const downloadLinkFinalVideo = document.createElement("a"); + downloadLinkFinalVideo.href = final_url; + downloadLinkFinalVideo.download = "final_output.mp4"; + downloadLinkFinalVideo.click(); + }; + + await load(); + await process(); +} diff --git a/examples/sound-effects/video-to-sfx/package-lock.json b/examples/sound-effects/video-to-sfx/package-lock.json index d4a90d9..05bdeda 100644 --- a/examples/sound-effects/video-to-sfx/package-lock.json +++ b/examples/sound-effects/video-to-sfx/package-lock.json @@ -8,6 +8,8 @@ "name": "video-to-sfx", "version": "0.1.0", "dependencies": { + "@ffmpeg/ffmpeg": "^0.12.10", + "@ffmpeg/util": "^0.12.1", "@hookform/resolvers": "^3.6.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", @@ -45,6 +47,7 @@ "date-fns": "^3.6.0", "embla-carousel-react": "^8.1.5", "framer-motion": "^11.2.10", + "geist": "^1.3.0", "input-otp": "^1.2.4", "lodash": "^4.17.21", "lucide-react": "^0.395.0", @@ -101,6 +104,33 @@ "node": ">=6.9.0" } }, + "node_modules/@ffmpeg/ffmpeg": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/@ffmpeg/ffmpeg/-/ffmpeg-0.12.10.tgz", + "integrity": "sha512-lVtk8PW8e+NUzGZhPTWj2P1J4/NyuCrbDD3O9IGpSeLYtUZKBqZO8CNj1WYGghep/MXoM8e1qVY1GztTkf8YYQ==", + "dependencies": { + "@ffmpeg/types": "^0.12.2" + }, + "engines": { + "node": ">=18.x" + } + }, + "node_modules/@ffmpeg/types": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@ffmpeg/types/-/types-0.12.2.tgz", + "integrity": "sha512-NJtxwPoLb60/z1Klv0ueshguWQ/7mNm106qdHkB4HL49LXszjhjCCiL+ldHJGQ9ai2Igx0s4F24ghigy//ERdA==", + "engines": { + "node": ">=16.x" + } + }, + "node_modules/@ffmpeg/util": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@ffmpeg/util/-/util-0.12.1.tgz", + "integrity": "sha512-10jjfAKWaDyb8+nAkijcsi9wgz/y26LOc1NKJradNMyCIl6usQcBbhkjX5qhALrSBcOy6TOeksunTYa+a03qNQ==", + "engines": { + "node": ">=18.x" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", @@ -2401,6 +2431,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/geist": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/geist/-/geist-1.3.0.tgz", + "integrity": "sha512-IoGBfcqVEYB4bEwsfHd35jF4+X9LHRPYZymHL4YOltHSs9LJa24DYs1Z7rEMQ/lsEvaAIc61Y9aUxgcJaQ8lrg==", + "peerDependencies": { + "next": ">=13.2.0 <15.0.0-0" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", diff --git a/examples/sound-effects/video-to-sfx/package.json b/examples/sound-effects/video-to-sfx/package.json index 6739320..f20d11b 100644 --- a/examples/sound-effects/video-to-sfx/package.json +++ b/examples/sound-effects/video-to-sfx/package.json @@ -9,6 +9,8 @@ "lint": "next lint" }, "dependencies": { + "@ffmpeg/ffmpeg": "^0.12.10", + "@ffmpeg/util": "^0.12.1", "@hookform/resolvers": "^3.6.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", @@ -46,6 +48,7 @@ "date-fns": "^3.6.0", "embla-carousel-react": "^8.1.5", "framer-motion": "^11.2.10", + "geist": "^1.3.0", "input-otp": "^1.2.4", "lodash": "^4.17.21", "lucide-react": "^0.395.0",