lisonge / vite-plugin-monkey

A vite plugin server and build your.user.js for userscript engine like Tampermonkey, Violentmonkey, Greasemonkey, ScriptCat
MIT License
1.24k stars 65 forks source link

Unable to use ffmpeg.wasm #162

Closed philhk closed 1 month ago

philhk commented 1 month ago

Hello. I'm trying to use ffmpeg.wasm in my userscript but I'm getting the following error

yarn run v1.22.22
$ tsc && vite build -w
vite v5.2.12 building for production...

watching for file changes...

build started...
✓ 6 modules transformed.
x Build failed in 10ms
[monkey:finalBundle] [vite:worker-import-meta-url] Could not resolve entry module "public/worker-3nePXDJu.js".     
file: ./main-DV1a4Cq8.js
file: ./main-DV1a4Cq8.js

to reproduce the issue create a project with yarn create monkey, add these packages (@ffmpeg/ffmpeg @ffmpeg/util) and use this code

import { FFmpeg } from '@ffmpeg/ffmpeg';

const ffmpeg = new FFmpeg();

ffmpeg.load();

I know that ffmpeg.wasm uses a worker script but I don't know how to handle it.

lisonge commented 1 month ago
// main.tsx
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';
import { useRef, useState } from 'react';
import ReactDOM from 'react-dom/client';

const ffg = new FFmpeg();
const App = () => {
  const [loaded, setLoaded] = useState(false);
  const [loading, setLoading] = useState(false);
  const ffmpegRef = useRef(ffg);
  const videoRef = useRef<any>();
  const messageRef = useRef<any>();

  const load = async () => {
    setLoading(true);
    const baseURL =
      'https://fastly.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/umd';
    const ffmpeg = ffmpegRef.current;
    ffmpeg.on('log', ({ message }) => {
      messageRef.current!.innerHTML = message;
      console.log(message);
    });
    // toBlobURL is used to bypass CORS issue, urls with the same
    // domain can be used directly.
    await ffmpeg.load({
      coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
      wasmURL: await toBlobURL(
        `${baseURL}/ffmpeg-core.wasm`,
        'application/wasm'
      ),
    });
    setLoaded(true);
    setLoading(false);
  };

  const transcode = async () => {
    const ffmpeg = ffmpegRef.current;
    await ffmpeg.writeFile(
      'input.webm',
      await fetchFile(
        'https://raw.githubusercontent.com/ffmpegwasm/testdata/master/Big_Buck_Bunny_180_10s.webm'
      )
    );
    await ffmpeg.exec(['-i', 'input.webm', 'output.mp4']);
    const data = await ffmpeg.readFile('output.mp4');
    if (typeof data === 'object') {
      videoRef.current!.src = URL.createObjectURL(
        new Blob([data.buffer], { type: 'video/mp4' })
      );
    }
  };

  return loaded ? (
    <>
      <video ref={videoRef} controls></video>
      <br />
      <button onClick={transcode}>Transcode webm to mp4</button>
      <p ref={messageRef}></p>
      <p>Open Developer Tools (Ctrl+Shift+I) to View Logs</p>
    </>
  ) : loading ? (
    'Loading ffmpeg-core...'
  ) : (
    <button onClick={load}>Load ffmpeg-core (~31 MB)</button>
  );
};

ReactDOM.createRoot(
  (() => {
    const app = document.createElement('div');
    document.body.append(app);
    return app;
  })()
).render(<App />);
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import monkey, { cdn } from 'vite-plugin-monkey';

export default defineConfig({
  plugins: [
    react(),
    {
      name: 'replace-url',
      apply: 'build',
      transform(code, id) {
        if (id.includes('node_modules/@ffmpeg/ffmpeg/dist/esm/classes.js')) {
          // this will prevent vite create chunk for worker.js
          const header = `import MyWorker from './worker.js?worker&inline';\n`;
          return (
            header +
            code.replace(
              `new Worker(new URL('./worker.js',`,
              `new MyWorker();((`
            )
          );
        }
      },
    },
    monkey({
      entry: 'src/main.tsx',
      userscript: {
        icon: 'https://vitejs.dev/logo.svg',
        namespace: 'npm/vite-plugin-monkey',
        match: ['https://www.google.com/', 'https://songe.li/'],
      },
      build: {
        externalGlobals: {
          react: cdn.jsdelivr('React', 'umd/react.production.min.js'),
          'react-dom': cdn.jsdelivr(
            'ReactDOM',
            'umd/react-dom.production.min.js'
          ),
        },
      },
    }),
  ],
});

In Vite's server mode, Vite does not bundle inline worker code, which leads to cross-origin issues. Therefore, the code only works in build mode.

lisonge commented 1 month ago

Is there any other question?

philhk commented 1 month ago

Hey! Thanks a bunch for the detailed explanation. I figured out my initial issue was caused because import.meta.url couldn't be resolved. But you fixed that issue already it seems. But I've decided to focus on building a Chrome extension instead. Thanks for your help!