ffmpeg.wasm icon indicating copy to clipboard operation
ffmpeg.wasm copied to clipboard

deno support?

Open andykais opened this issue 5 years ago • 6 comments

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/[email protected]/getting_started/webassembly.

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

andykais avatar Nov 12 '20 13:11 andykais

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

WenheLI avatar Nov 17 '20 03:11 WenheLI

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

jeromewu avatar Nov 17 '20 03:11 jeromewu

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/[email protected]/dist=es2019,mode=imports/optimized/@ffmpeg/ffmpeg.js:400:22
    at new Promise (<anonymous>)
    at getCreateFFmpegCore (https://cdn.skypack.dev/-/@ffmpeg/[email protected]/dist=es2019,mode=imports/optimized/@ffmpeg/ffmpeg.js:399:12)
    at async Object.load (https://cdn.skypack.dev/-/@ffmpeg/[email protected]/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

andykais avatar Sep 15 '22 16:09 andykais

@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/[email protected]/dist/ffmpeg-core.worker.js';
import * as b from 'https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg-core.js';

import * as FFmpeg from 'https://cdn.skypack.dev/pin/@ffmpeg/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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!

survirtual avatar Sep 21 '22 20:09 survirtual

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

andykais avatar Sep 21 '22 21:09 andykais

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.

survirtual avatar Sep 21 '22 21:09 survirtual

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

Stvad avatar Jul 05 '23 18:07 Stvad

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

ralyodio avatar Jul 06 '23 08:07 ralyodio

@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/[email protected]/dist/umd/ffmpeg.js");
await import("https://unpkg.com/@ffmpeg/[email protected]/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/[email protected]/dist/umd/ffmpeg-core.js`,
  wasmURL: `https://unpkg.com/@ffmpeg/[email protected]/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/[email protected]/dist/umd/ffmpeg.js:1:982
    at https://unpkg.com/@ffmpeg/[email protected]/dist/umd/ffmpeg.js:1:1124
    at https://unpkg.com/@ffmpeg/[email protected]/dist/umd/ffmpeg.js:1:3364
    at https://unpkg.com/@ffmpeg/[email protected]/dist/umd/ffmpeg.js:1:197
    at https://unpkg.com/@ffmpeg/[email protected]/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/[email protected]/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.

josephrocca avatar Jul 29 '23 09:07 josephrocca