Blog

JUCE 8 Feature Overview: WebView UIs

What's the feature?

JUCE 8 comes with a new set of tooling to help developers build UIs for audio applications and plugins with web technologies such as React, Vue, or even plain HTML, CSS and javascript.

Building with WebViews delivers many advantages:

  • You can iterate plugin UIs quickly via hot reloading, similar to a normal web development workflow.
  • You can use existing mature web frontend frameworks such as React, Vue, Svelte and their component libraries.
  • Frontend web developers can participate in developing audio application and plugin UIs without needing to touch C++.
  • You gain instant access to cross-platform hardware accelerated graphics via WebGL.

This is a new software paradigm for music software with best practices that will evolve over time. Please note:

  • JUCE does not provide actual UI controls ("widgets") out of the box. There are many frontend frameworks and component libraries available to choose from.
  • Rendering and feature support within WebViews may differ across user platform and browser versions. Be sure to use best practices around polyfills, etc.
  • Calculating and displaying plugin values and labels is currently handled by the frontend. For now, you'll have to manually create native functions if you want access the C++ convertTo0To1 and convertFrom0To1, stringFromValue, etc backend functions.

Background and Definitions

WebViews are embeddable browsers that mobile and desktop apps use to show you webpages without leaving the app.

The actual WebView software component is native to an end user's operating system. It's not something that's bundled and shipped in the application/plugin binary.

On MacOS, it's WebKit, on Windows is Edge (Chromium based), and Linux is GTK WebKit2.

A note on Windows compatibility: All Windows 11 machines have the WebView2 runtime pre-installed. The "vast majority" of Windows 10 recevied an update in mid-2022 to rollout runtimes, more details here.

How does JUCE 8's WebView support differ from JUCE 7?

There are 3 big changes:

  • JUCE's existing WebBrowserComponent has been updated with new low level functions that supports communication between C++ and javascript running in the browser. New options can be supplied to WebBrowserComponent::Options
  • There's new glue code for both the C++ backend and js frontend, allowing event handling, function calling, and JUCE parameter binding.
  • The Windows WebView2 loader dll can now be statically linked with JUCE_USE_WIN_WEBVIEW2_WITH_STATIC_LINKING

In addition, there's a new WebViewPluginDemo which demonstrates a JUCE plugin with a React frontend.

Deprecations

The WebBrowserComponent::pageAboutToLoad() function on Android now only
receives callbacks for entire page navigation events, as opposed to every
resource fetch operation. See BREAKING_CHANGES.md for more detail.

Configuring a project to use new WebView functionality

WebView functionality is found in the juce_gui_extra module.

The following two flags must be added to target_compile_definitions if you are using CMake:

JUCE_WEB_BROWSER=1 
JUCE_USE_WIN_WEBVIEW2_WITH_STATIC_LINKING=1

For Projucer, ensure these two flags are set to enabled in the settings under Modules > juce_gui_extra.

On windows, JUCE_USE_WIN_WEBVIEW2_WITH_STATIC_LINKING ensures you are in line with Windows best practices to static link the WebView loader with your binary. Note this is not the actual WebView binary. It's just a small dll that talks to Windows and asks it to acquire a WebView.

JUCE will warn you if your build machine doesn't have WebView installed and prompt you to install.

Overview of the WebView paradigm

The following illustrates the high level overview of how WebView integrates into the JUCE stack.

Let's walk through each of these pieces.

WebBrowserComponent

Although one could create a WebBrowserComponent in JUCE 7, in JUCE 8 it has been supercharged with an array of new functions and options.

New low level functions added in JUCE 8:

  • evaluateJavascript()  evaluates a string as Javascript in the WebView context, allowing for low level, direct manipulation of the javascript state
  • emitEventIfBrowserIsVisible() emits javascript events that javascript code inside the WebView can listen to

New options for WebBrowserComponent::Options have been built on top of these low level functions:

  • withInitialisationData lets you specify key value pairs accessible by javascript.
  • withUserScript lets you run javascript code before anything else is loaded in the WebView.
  • withNativeIntegrationEnabled injects JUCE's javascript shim client-side. This provides the foundation for communication between javascript and C++.
  • withNativeFunction lets you expose a named function in javascript that will asynchronously call a C++ function callback.
  • withEventListener will listen to a named event in the WebView and call a C++ function callback.
  • withResourceProvider is a callback the WebView will call whenever a web document requests a resource url (for an html document, image, css, etc). This allows you to serve data directly to the WebView from C++.

When working with WebViews, remember to specify the following options. The WebView2 backend is not yet the Windows default and plugins require a User Data Folder specified for WebViews to be happy:

.withBackend (WebBrowserComponent::Options::Backend::webview2)
.withWinWebView2Options (WebBrowserComponent::Options::WinWebView2{}
    .withUserDataFolder (File::getSpecialLocation (File::SpecialLocationType::tempDirectory)))

The only other thing left to do is load a page!

To serve from a local server (for example in Debug mode) you can call the following to load a page over the network:

webComponent.goToURL ("localhost:3000");

To load from C++ (for example in Release mode), do something like the following:

webComponent.goToURL (WebBrowserComponent::getResourceProviderRoot());

Resource Providers

By specifying withResourceProvider, you can hook into requests that the WebView makes. Essentially this callback can behave as a lightweight web server.

For example, in Release builds, you will probably want to serve most of the html, js, css and images from BinaryData via the resource provider.

In Debug builds, even though you might be hitting a development server on localhost, you still might want to grab data from the C++ backend via the resource provider. For example, the demo grabs spectrum data from C++ for a visualization.

Note that if you are loading scripts from a development server on a localhost address like localhost:3000, you'll need to supply this to withResourceProvider's optional parameter allowsOriginIn.

To reach the resource provider manually from javascript you can navigate to to juce.getBackendResourceAddress (path).

On MacOS, iOS and Linux this resolves to juce://juce.backend/ + path. On Windows and Android it will resolve to  https://juce.backend/ + path.

withNativeFunction

This lets you expose a native C++ function to your javascript frontend.

For example, lets say you want to have a "Load Preset" button in your html where you can specify a preset with an integer number:

.withNativeFunction ("loadPreset", [](auto& var, auto complete)
{
loadPresetID (var[0]); // actually load the preset
complete ("Preset Loaded: " + var[0].toString());
})

On the javascript side, with the JUCE frontend library imported in the Juce namespace, you can now call this when a button is clicked:

const loadPreset = Juce.getNativeFunction("loadPreset");
loadPreset(45).then((result => {
console.log(result);
});

Note by the usage of javascript Promises that this API is asynchronous.

JUCE Frontend Library

JUCE 8 comes with a new javascript frontend library, located at modules/juce_gui_extra/native/javascript/index.js.

The library will run happily without errors regardless of whether the backend is present. This is by design to allow the frontend to be developed and visually tested without any need for C++ machinery.

Web Parameter Attachments, Web Relays

Just like you can attach a parameter to a C++ slider, you can connect a C++ JUCE audio parameter to a javascript state object.

The following attachment types are provided out of the box: WebSliderParameterAttachment, WebToggleButtonParameterAttachment, WebComboBoxParameterAttachment.

They have corresponding Web Relays. These establish event listeners on the Javascript side.

This makes wiring up parameters to your Web frontend straightforward. For example, assuming you have a gain parameter — you'd need both an Attachment and a Relay for the parameter, like so:

WebSliderRelay gainRelay { browser, "gain" };
WebSliderParameterAttachment gainWebAttachment { *processor.parameters.getParameter ("gain"), gainRelay, nullptr };

You'll pass the relay to WebBrowserComponent's constructor and setup the javascript side by specifying .withOptionsFrom

.withOptionsFrom (gainRelay)

Javascript states

On the javascript side, you can access the javascript's representation of the parameter via getSliderState:

Juce.getSliderState("gain");

You can wire up an arbitrary piece of UI to the JUCE backend using this slider state. In raw javascript, assuming you have a html range input with id slider, you could use event listeners:

const slider = document.getElementById("slider");
const sliderState = Juce.getSliderState("gain");

// when the backend (like the DAW) changes the value, update our slider
sliderState.valueChangedEvent.addListener(() => {
slider.value = sliderState.getNormalisedValue() * slider.max;
});

slider.addEventListener("mousedown", (event) => {
sliderState.sliderDragStarted();
});

slider.addEventListener("mouseup", (event) => {
sliderState.sliderDragEnded();
});

// when our slider changes value, update the DAW
slider.addEventListener("input", (event) => {
sliderState.setNormalisedValue(slider.value / slider.max);
});

Building the WebViewPluginDemo

JUCE 8 comes with a WebView demo, showing how one might interact with a Web frontend from an audio plugin, including feeding the WebView UI audio spectrum data.

Because the demo is a plugin, you won't find it in the DemoRunner app. Instead, you can Open Example in the Projucer and export a project for your IDE.

Or configure the plugin with CMake. For example, on MacOS run this from JUCE's root directory:

cmake . -B cmake-build -DJUCE_BUILD_EXAMPLES=ON -GXcode

This will produce cmake-build/JUCE.xcodeproj which you can then open and select the WebViewPluginDemo_AU schema to build the AU plugin.

After doing so, you should see the plugin at cmake-build/examples/GUI/WebViewPluginDemo_artefacts/Debug/AU/WebViewPluginDemo.component. Manually mv this into an AU folder like ~/Library/Audio/Plug-Ins/Components/ and open in your favorite DAW.

In your DAW, it will appear as an audio effect under the JUCE manufacturer:

You will see the following "fallback" html page until we build the frontend:

After installing node and running the above npm commands, the file examples/Assets/webviewplugin-gui_1.0.0.zip will be present, which is what the Demo will load as the frontend.

Opening the plugin again, you will see a React application with a slider that manipulates a lowpass filter as well as a visualization of the audio's spectrum.

Hot Reloading with the WebViewPluginDemo

So far we've built a static zip file for the demo. This is similar to a process you might use for deploying a Release build of your application (although you might choose to just put the files in BinaryData vs. zip them up).

A lot of the power of working with Web UIs comes from the ability to edit the UI on the fly, without recompiling. You can try this out in the demo by uncommenting the line that instructs it to go to localDevServerAddress.

webComponent.goToURL (localDevServerAddress);
// webComponent.goToURL (WebBrowserComponent::getResourceProviderRoot());

You can then npm install; npm run to run the node project locally.

Load the project in an IDE like VS Code, make some modifications, hit save, and the WebView contents will automatically reload.

More

linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram