ffmpegwasm / ffmpeg.wasm

FFmpeg for browser, powered by WebAssembly
https://ffmpegwasm.netlify.app
MIT License
14.58k stars 859 forks source link

deno support? #110

Open andykais opened 4 years ago

andykais commented 4 years ago

Is your feature request related to a problem? Please describe. I have created a called ffmpeg-templates in deno which uses ffmpeg on the command line.

Describe the solution you'd like It would be fantastic if I could bundle ffmpeg into this library rather than asking users to install an external dependency

Describe alternatives you've considered The current implementation is the alternative, use the system's ffmpeg and ask developers to install it.

Additional context Deno strives for parity with browsers, including webassembly support https://deno.land/manual@v1.5.2/getting_started/webassembly.

denoify is a package that assists in porting npm modules to deno

WenheLI commented 4 years ago

@jeromewu - I could try to export it to deno. Would you mind assigning this issue to me?

jeromewu commented 4 years ago

No problem, this is great! Also invite you to the organisation, so you can create new repos if required.

andykais commented 2 years ago

Any update on this? I have created the following snippet in deno

import ffmpegCore from 'https://cdn.skypack.dev/@ffmpeg/core';
import { createFFmpeg, fetchFile } from 'https://cdn.skypack.dev/@ffmpeg/ffmpeg';

const ffmpeg = createFFmpeg({ log: true });

await ffmpeg.load();
ffmpeg.FS('writeFile', 'test.avi', await fetchFile('/Users/andrew.kaiser/Downloads/yt-dlp/youtube/Hermitcraft Season 9 - Creeper Conveyor Belts - #16.webm'));
await ffmpeg.run('-i', 'test.avi', 'test.mp4');
await Deno.writeFile('./test.mp4', ffmpeg.FS('readFile', 'test.mp4'))

which gives the following error

[info] use ffmpeg.wasm v0.11.5
[info] load ffmpeg-core
[info] loading ffmpeg-core
error: Uncaught (in promise) ReferenceError: document is not defined
      const script = document.createElement("script");
                     ^
    at https://cdn.skypack.dev/-/@ffmpeg/ffmpeg@v0.11.5-u3mWpbueYOGaLpBMFMgb/dist=es2019,mode=imports/optimized/@ffmpeg/ffmpeg.js:400:22
    at new Promise (<anonymous>)
    at getCreateFFmpegCore (https://cdn.skypack.dev/-/@ffmpeg/ffmpeg@v0.11.5-u3mWpbueYOGaLpBMFMgb/dist=es2019,mode=imports/optimized/@ffmpeg/ffmpeg.js:399:12)
    at async Object.load (https://cdn.skypack.dev/-/@ffmpeg/ffmpeg@v0.11.5-u3mWpbueYOGaLpBMFMgb/dist=es2019,mode=imports/optimized/@ffmpeg/ffmpeg.js:557:11)
    at async file:///Users/andrew/Code/scratchwork/deno-ffmpeg/mod.ts:6:1

if I had to guess, ffmpeg is detecting that we are in the browser using typeof window !== undefined somewhere, and assuming that document exists

survirtual commented 2 years ago

@andykais I went ahead and sorted out the errors to get it working on deno. You need to have deno make a vendor folder for this hack to work -- after placing the script in a file such as "ffmpeg.ts", do:

deno vendor ./ffmpeg.ts

After that is successful, run the script with an import map:

deno run --import-map=vendor/import_map.json ./ffmpeg.ts

Make sure to modify the folders / files as per what you want.

ffmpeg.ts

// needed to avoid downloading .wasm bin again
import { existsSync } from "https://deno.land/std/fs/mod.ts";

// need these imports for the vendor gen
import * as a from 'https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.worker.js';
import * as b from 'https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js';

import * as FFmpeg from 'https://cdn.skypack.dev/pin/@ffmpeg/ffmpeg@v0.11.5-u3mWpbueYOGaLpBMFMgb/mode=imports/optimized/@ffmpeg/ffmpeg.js';
const { createFFmpeg, fetchFile } = FFmpeg;

// folder where files exist for processing, relative to current module dir
let workingDir = import.meta.resolve("../test");

// folder where the core worker js is
let corePath = import.meta.resolve("https://unpkg.com/@ffmpeg/core@0.11.0/dist/").replace("file://", "");
let wasmPath = corePath + "ffmpeg-core.wasm";
if (existsSync(wasmPath)) {
    // we are good
} else {
    console.log(`Downloading ffmpeg-core.wasm to ${wasmPath}`);
    let b = await fetch("https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.wasm")
    let d = await (await b.blob()).arrayBuffer();
    await Deno.writeFile(wasmPath, d);
    console.log(`Download complete`);
}

// worker shim for Deno worker compatibility 
let DenoWorker = Worker;
window.Worker = function Worker(script) {
    return new DenoWorker(new URL(script, import.meta.url).href, {
        deno:  true,
        type: 'module',
      });
}

// we need to mod the worker file if it hasn't been modded already
let workerPath = corePath + "ffmpeg-core.worker.js";
let data = Deno.readTextFileSync(workerPath);
let newData = data.replace(/this\./gi, "self.");
if (newData != data) {
    // original file was unmodified; add the rest of the replacement and save it

    // modify importscripts to work with deno
    newData = newData.replace(/importScripts\(/gi, "self.importScripts(");

    // early return to prevent error
    newData = newData.replace("createFFmpegCore(Module).then", "return;createFFmpegCore(Module).then")

    // import scripts replacement
    newData = newData + `
self.importScripts = () => {
    let corePath = 'https://unpkg.com/@ffmpeg/core@0.11.0/dist/';
    let file = corePath + "core-dyn.js"
    import(file).then((r) => {
        r.default.createFFmpegCore(Module).then(function(instance) {
            Module = instance;
            postMessage({
                "cmd": "loaded"
            })
        })
    });
}
`
    Deno.writeTextFileSync(workerPath, newData);

}

// document shim needed for ffmpeg-wasm
window.document = {
    handler: null,
    createElement: (e) => {
        return {
            removeEventListener: (ev, handler) => {},
            src: "",
            addEventListener: (ev, handler) => {
                console.log(ev);
                document.handler = handler;
            }
        }
    },
    getElementsByTagName: (tag) => {
        return [
            {
                appendChild: (script) => {
                    let file = corePath + "core-dyn.js"
                    fetch(script.src)
                        .then((response) => response.blob())
                        .then((data) => {
                            data.arrayBuffer()
                                .then((b) => {
                                    const encoder = new TextEncoder();

                                    // export fix
                                    let append = encoder.encode(
`
var src = {
    createFFmpegCore,
};
export default src;
`
                                    );
                                    let tmp = new Uint8Array(b.byteLength + append.byteLength);
                                    tmp.set(new Uint8Array(b), 0);
                                    tmp.set(new Uint8Array(append), b.byteLength);
                                    Deno.writeFile(file, tmp);

                                })
                                .then(async (r) => {
                                    import(file)
                                        .then((r) => {
                                            const { createFFmpegCore } = r.default;
                                            window.createFFmpegCore = createFFmpegCore;
                                            document.handler();
                                        });
                                });
                        });
                }
            }
        ]
    }
};

const ffmpeg = createFFmpeg({ 
    log: true, 
    corePath: corePath + "ffmpeg-core.js"
});

await ffmpeg.load();

let f = await fetch(workingDir + '/resources/images/20220906044647.mp4');
f = await f.blob();
f = await fetchFile(f);

ffmpeg.FS('writeFile', 'test.avi', f);
await ffmpeg.run('-i', 'test.avi', 'test.mp4');
await Deno.writeFile('./test.mp4', ffmpeg.FS('readFile', 'test.mp4'));

Enjoy!

andykais commented 2 years ago

wow! Thanks for putting this together. Obviously this is very hacky though. It seems like we just need replace browser script loading with dynamic imports. Perhaps we could replace it in both browser contexts and deno contexts, is there a reason we are relying on script loading? Also just summarizing, it seems like there isnt any deno feature missing, just that we are using browser document apis within a deno context

survirtual commented 2 years ago

You'll have to play with it and get the result you want, I just wanted to see if it'd work at all without much modification and it does. That code contains all the mods I needed to make it usable standalone with deno & using existing packages. I'm sure it wouldn't be much work to make it usable in both browser and deno contexts, given that relatively small set of hacks needed -- and given it's using the ffmpeg browser js packages instead of the node packages.

Stvad commented 1 year ago

Hey there! I was wondering if there were any progress on this. Ty!

ralyodio commented 1 year ago

me too....is it working in deno 1.35.0 yet?

josephrocca commented 1 year ago

@ralyodio No, currently if I run this in 1.35.0:

deno run --allow-read=. --allow-write=. --allow-net main.js
// main.js
await import("https://unpkg.com/@ffmpeg/ffmpeg@0.12.1/dist/umd/ffmpeg.js");
await import("https://unpkg.com/@ffmpeg/util@0.12.0/dist/umd/index.js");

const { FFmpeg } = FFmpegWASM;
const { fetchFile } = FFmpegUtil;
const ffmpeg = new FFmpeg();

ffmpeg.on("log", ({ message }) => {
  console.log(message);
});

await ffmpeg.load({
  coreURL: `https://unpkg.com/@ffmpeg/core@0.12.1/dist/umd/ffmpeg-core.js`,
  wasmURL: `https://unpkg.com/@ffmpeg/core@0.12.1/dist/umd/ffmpeg-core.wasm`,
});

await ffmpeg.writeFile("input.avi", await fetchFile('./input.avi'));
await ffmpeg.exec(['-i', 'input.avi', 'output.mp4']);
const data = await ffmpeg.readFile('output.mp4');

// let outputBlob = new Blob([data.buffer], {type: 'video/mp4'});

await Deno.writeFile('output.mp4', data);

I get this error:

error: Uncaught (in promise) Error: Automatic publicPath is not supported in this browser
    at https://unpkg.com/@ffmpeg/ffmpeg@0.12.1/dist/umd/ffmpeg.js:1:982
    at https://unpkg.com/@ffmpeg/ffmpeg@0.12.1/dist/umd/ffmpeg.js:1:1124
    at https://unpkg.com/@ffmpeg/ffmpeg@0.12.1/dist/umd/ffmpeg.js:1:3364
    at https://unpkg.com/@ffmpeg/ffmpeg@0.12.1/dist/umd/ffmpeg.js:1:197
    at https://unpkg.com/@ffmpeg/ffmpeg@0.12.1/dist/umd/ffmpeg.js:1:201

@survirtual I just copy-pasted your script, ran the vendoring command, and then ran the script as instructed, and got this error:

error: Uncaught TypeError: Cannot set properties of undefined (setting 'alert')
    at file:///<working folder path>/vendor/unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.worker.js:1:353

Looks like all the import URLs are pinned (other than deno.land/std/fs which I pinned to @0.156.0 since that was the latest at the time of your comment) so I'm not sure what has changed there. I also tried with deno upgrade --version 1.25.3 which would have been the latest Deno version at the time of your comment.