Very simple. Is it possible to mute or control the volume of a JavaFX WebView? I googled for a while but I can't find any mention of this. I looked at the code for WebView
and WebEngine
and there doesn't seem to be anything about controlling the volume.
I still need other MediaPlayer
s in the same app to work and produce sound, so, I can't mute the whole application.
We all know that it is a missing part of API, so what can you do to implement it on your own? Let's think about what WebView really is:
The embedded browser component is based on WebKit [...] By default WebKit doesn't support web page rendering. In order to render and display HTML content Oracle had to write its own renderer using Java Graphics 2D API.
It means that the engine has to have implementation. Implementation of WebKit is located in com.sun.webkit
package (a considerable part of classes is just a wrapper for native calls). Of course, in most cases you don't want to use com.sun.*
classes, but currently you're working with JavaFX, so it doesn't really matter.
If we jump a little in sun's sources, we can find WCMediaPlayer.class
with some abstract audio methods like:
protected abstract void setRate(float rate);
protected abstract void setVolume(float volume);
protected abstract void setMute(boolean mute);
protected abstract void setSize(int w, int h);
protected abstract void setPreservesPitch(boolean preserve);
C'mon Java, let me call you:
volumeMethod = WCMediaPlayer.class.getDeclaredMethod("setVolume", float.class);
volumeMethod.setAccessible(true);
I appreciate your help but how can I get WCMediaPlayer
instances? We have to look at references to new WCMediaPlayerImpl()
. Gotcha! WCGraphicsManager.class
creates MediaPlayer by fwkCreateMediaPlayer()
method, after all, it puts pointer and instance in refMap
:
Field refMapField = WCGraphicsManager.class.getDeclaredField("refMap");
refMapField.setAccessible(true);
Fortunately, the manager has exposed getGraphicsManager()
method to get an instance:
WCGraphicsManager graphicsManager = WCGraphicsManager.getGraphicsManager();
refMap = (Map<Integer, Ref>) refMapField.get(graphicsManager);
Catched map contains Ref
instances (there are also other WC*
instances) so you have to filter them:
Collection<WCMediaPlayer> mediaPlayers = refMap.values().stream()
.filter(ref -> ref instanceof WCMediaPlayer)
.map(ref -> (WCMediaPlayer) ref)
.collect(Collectors.toList());
You probably expect working example so here is code that I used:
public class WebEngineTest extends Application {
private Map<Integer, Ref> refMap;
private Method volumeMethod;
@Override
@SuppressWarnings("unchecked")
public void start(Stage primaryStage) throws Exception {
WebView webView = new WebView();
WebEngine engine = webView.getEngine();
engine.load("https://www.youtube.com/watch?v=hRAZBSoAsgs");
Field refMapField = WCGraphicsManager.class.getDeclaredField("refMap");
refMapField.setAccessible(true);
volumeMethod = WCMediaPlayer.class.getDeclaredMethod("setVolume", float.class);
volumeMethod.setAccessible(true);
WCGraphicsManager graphicsManager = WCGraphicsManager.getGraphicsManager();
refMap = (Map<Integer, Ref>) refMapField.get(graphicsManager);
Button button = new Button("Volume");
button.setOnAction(event -> setVolume(0.1f));
Group group = new Group();
group.getChildren().addAll(webView, button);
Scene scene = new Scene(group, 625, 625);
primaryStage.setScene(scene);
primaryStage.show();
}
public void setVolume(float volume) {
Collection<WCMediaPlayer> mediaPlayers = this.getMediaPlayers();
mediaPlayers.forEach(mediaPlayer -> setVolumeMethod(mediaPlayer, volume));
}
private void setVolumeMethod(Object instance, Object... args) {
try {
volumeMethod.invoke(instance, args);
} catch (Exception e) {
e.printStackTrace();
}
}
private Collection<WCMediaPlayer> getMediaPlayers() {
return refMap.values().stream()
.filter(ref -> ref instanceof WCMediaPlayer)
.map(ref -> (WCMediaPlayer) ref)
.collect(Collectors.toList());
}
}
Finally, remember about the order of these calls. For instance, refMap
doesn't contain all Refs
until the state of engine is not SUCCEEDED
or WCGraphicsManager.getGraphicsManager()
returns null
if graphic element is not created at all.
Probably, the best way is to combine these solutions. It's hard to support web technology mix without scenerio and poor API provided by JavaFX. You can also try to embed another browser, like Chromium.