webpacknext.jsweb-audio-apiwebpack-5audio-worklet

(Next JS 10.2) Audio Worklet Support


Our application is built on top of Next, and we are currently in the process of migrating our audio recording engine to Audio Worklet API from Script Processor. (with backwards compatibility of course) Part of the transition is also upgrading to Webpack 5. We are utilizing both Web Workers and Audio Worklets. Prior to switch to Webpack 5, which comes with native support for Workers, (or so it appears) we used worker-plugin. It worked wonderfully for both, however with the transition we can no longer rely on it, because it uses outdated webpack's APIs, and Next comes with its own bundled Webpack 4 and 5, which don't seem to have backwards compatibility included.

Now the challenge is to get these working with Webpack 5 bundled with Next. The first issue comes from web worker's global scope being undefined. This can be resolved with by setting config.output.globalObject to "(typeof self !== 'undefined' ? self : this)". Once workers are functional, the next issue comes when trying to bundle worklet's code. Currently, Webpack 5 doesn't support worklet import, but they expose parser config, which can be used to tell webpack to load it as a worker, as per this Github issue. This will fail with Next unless you also set config.output.publicPath = "/_next/"; Upon loading the chunk via audioContext.audioWorklet.addModule(...), the code crashes with Uncaught TypeError: Cannot read property 'webpackChunk_N_E' of undefined. If we inspect the chunk containing bundled worklet code, we'll see that this prop is being used in the following way:

var chunkLoadingGlobal = (typeof self !== 'undefined' ? self : this)["webpackChunk_N_E"] = (typeof self !== 'undefined' ? self : this)["webpackChunk_N_E"] || [];
var parentChunkLoadingFunction = chunkLoadingGlobal.push.bind(chunkLoadingGlobal);
.
.
.
(typeof self !== 'undefined' ? self : this)["webpackHotUpdate_N_E"] = function...

It's clear, that AudioWorkletGlobalScope doesn't have either self or this, so that's why it's not going too well.

So the question is, how can this be worked around? Audio Worklets seem to be getting very little attention still from both Next and Webpack, despite it being available by default now even in Safari. (since 14.5)

Below are the code snippets representing our current state. (we are using Typescript)

next.config.js

module.exports = withPlugins([...], {
  future: {webpack5: true},
  webpack: (config, options) => {
    config.output.globalObject = `(typeof self !== 'undefined' ? self : this)`;
    config.output.publicPath = "/_next/";

    config.resolve = {
      ...config.resolve,
      alias: {
        ...config.resolve.alias,
        "audio-worklet": path.resolve(__dirname, "src/util/audio-worklet")
      }
    }

    config.module.parser = {
      ...config.module.parser,
      javascript: {
        worker: ["AudioWorklet from audio-worklet", "..."]
      }
    }

    config.output.chunkFilename = options.isServer
      ? `${options.dev ? "[name].[hash]" : "[name].[chunkhash]"}.js`
      : `static/chunks/${options.dev ? "[name].[hash]" : "[name].[chunkhash]"}.js`;

    return config;
  }
});

src/util/audio-worklet.ts

export const AudioWorklet = (url: URL) => {
  return url as unknown as string;
};

.../audio-context.ts

import { AudioWorklet } from "audio-worklet";

const audioContext = new AudioContext();
audioContext.audioWorklet.addModule(new AudioWorklet(new URL("path/to/processor.worklet.ts", import.meta.url)));

.../audio-worklet-processor.worklet.ts

// import statements for utility code

class Processor extends AudioWorkletProcessor {
  // Your run of the mill processor code
}

registerProcessor("processor", Processor);

Sources:

Update

After much digging, seems like this issue is caused by webpack importing scripts in the chunk. Specifically, when we try to import any non-trivial code, such as lodash, or use data structures that babel deems necessary to use its shims for, Webpack injects installChunk function which uses importScripts, which obviously is not supported by AudioWorklets. This also inserts the code above that causes the Uncaught TypeError: Cannot read property 'webpackChunk_N_E' of undefined.

Here's a repo reproducing it one for one: https://github.com/thecynicalpaul/test-audio-worklet-webpack5 (nmp i, then npm run dev)


Solution

  • Answering this myself, as we came to a temporary solution.

    The issue is happening due to the URL loader in Webpack 5 treating worklet files as if they were web worker ones. That means, the context is of workers, and it will attempt to use APIs such as importScripts, which are obviously not supported.

    The solution we came to is to disable chunk splitting for any chunks that are related to worklets, effectively removing the need to use importScripts:

    (inside next.config.js's webpack function)

    if (!options.isServer && !options.dev) {
      config.optimization.splitChunks = {
        chunks: (chunk) => {
          // this may vary widely on your loader config
          if (chunk.name && chunk.name.includes("worklet")) {
            return false;
          }
    
          return true;
        }
      }
    }
    

    This isn't the best solution in the grand scheme of things, for obvious reasons, and webpack's team is currently looking into a better way to handle this.