Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mute a WebView in JavaFX, is it possible?

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 MediaPlayers in the same app to work and produce sound, so, I can't mute the whole application.

like image 541
pupeno Avatar asked Mar 13 '18 16:03

pupeno


3 Answers

Right now, there is NO such API for sound system in WebView or WebEngine Control.

But you could inject JavaScript to mute/unmute and control sounds. This method, only works with HTML5 elements < video > or < audio >, because items like Flash or JS initialized audio is impossible to restrict in general. I have created example program for you:

WebView mute/unmute - program

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        WebView myWebView = new WebView();
        WebEngine engine = myWebView.getEngine();
        //dirty code xD

        Button btn = new Button("Load Youtube");
        btn.setOnAction((ActionEvent event) -> engine.load("http://www.youtube.com/embed/A6hrw6KGdtM?autoplay=1"));

        Button btn2 = new Button("Mute");
        btn2.setOnAction((ActionEvent event) -> engine.executeScript(getJSAudioVideo(true)));

        Button btn3 = new Button("Unmute");
        btn3.setOnAction((ActionEvent event) -> engine.executeScript(getJSAudioVideo(false)));

        Button btn4 = new Button("+");
        btn4.setOnAction((ActionEvent event) -> engine.executeScript(getJSSoundVolume(0.9)));

        Button btn5 = new Button("-");
        btn5.setOnAction((ActionEvent event) -> engine.executeScript(getJSSoundVolume(0.1)));

        VBox root = new VBox();
        root.getChildren().addAll(myWebView, btn, btn2, btn3, btn4, btn5);

        Scene scene = new Scene(root, 800, 500);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Mute a WebView in JavaFX, is it possible?");

        primaryStage.show();
    }

    /**
     * @param mute
     * @return JS code for mute/unmute
     */
    public String getJSAudioVideo(boolean mute){
        return "var videos = document.querySelectorAll('video'),\n" +
                "    audios = document.querySelectorAll('listen');\n" +
                "    [].forEach.call(videos, function(video) { video.muted = "+String.valueOf(mute)+"; });\n" +
                "    [].forEach.call(audios, function(audio) { audio.muted = "+String.valueOf(mute)+"; });";
    }

    /**
     * @param volume
     * @return JS code for setting volume sound
     */
    public String getJSSoundVolume(double volume){
        //max 1.0, min 0.0 Default: 1.0
        double _volume = (volume > 1.0 || volume < 0.0) ? 1.0 : volume;

        return "var videos = document.querySelectorAll('video'),\n" +
                "    audios = document.querySelectorAll('listen');\n" +
                "    [].forEach.call(videos, function(video) { video.volume = "+String.valueOf(_volume)+"; });\n" +
                "    [].forEach.call(audios, function(audio) { audio.volume = "+String.valueOf(_volume)+"; });";

    }

    public static void main(String[] args) {
        launch(args);
    }
}
like image 195
Maciej Pulikowski Avatar answered Nov 16 '22 08:11

Maciej Pulikowski


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:

WebEngineTest Preview

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.

like image 41
dzikoysk Avatar answered Nov 16 '22 09:11

dzikoysk


To say about WebView , it is an extension of the Node class. It encapsulates a WebEngine object, incorporates HTML content into an application's scene, and provides properties and methods to apply effects and transformations. The getEngine() method called on a WebView object returns a web engine associated with it. Source

So WebView doesn't support any APIs for mute/volume control right now. There is an API to mute the content loaded in the MediaPlayer (ie.,)

To get the status of muteProperty: Source link

public BooleanProperty muteProperty()

To check the mute condition : Source link

public final boolean isMute()

To mute the video set the value to true: Source link

public final void setMute(boolean value)

OR otherwise use the JavaScript API to mute the Video/Audio:

getElementById("Your video Id");

and set the control to Mute by :

vid.muted = true;

Source Link

like image 6
Subash J Avatar answered Nov 16 '22 08:11

Subash J