在 JavaFX 中静音 WebView,这可能吗?

Mute a WebView in JavaFX, is it possible?

很简单。是否可以静音或控制 JavaFX WebView 的音量?我用谷歌搜索了一段时间,但找不到任何提及。我查看了 WebViewWebEngine 的代码,似乎没有任何关于控制音量的内容。

我仍然需要同一个应用程序中的其他 MediaPlayer 才能工作并发出声音,因此,我无法将整个应用程序静音。

目前没有这样的API声音系统在WebViewWebEngine控制。

但您可以将 JavaScript 注入 mute/unmute 并控制声音。此方法仅适用于 HTML5 元素 < 视频 >< 音频 >,因为 Flash 或者 JS 初始化的音频一般是无法限制的。我已经为您创建了示例程序:

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);
    }
}

说一下WebView,它是Nodeclass的扩展。它封装了一个 WebEngine 对象,将 HTML 内容合并到应用程序的场景中,并提供了应用效果和转换的属性和方法。 getEngine() 方法在 WebView 对象上调用 returns 与之关联的 Web 引擎。 Source

因此 WebView 目前不支持 mute/volume 控件的任何 API。有一个 API 可以使 MediaPlayer 中加载的内容静音(即,)

获取muteProperty的状态:Source link

public BooleanProperty muteProperty()

检查静音条件:Source link

public final boolean isMute()

要使视频静音,请将值设为真:Source link

public final void setMute(boolean value)

或者使用 JavaScript API 静音 Video/Audio:

getElementById("Your video Id");

并通过以下方式将控件设置为静音:

vid.muted = true;

Source Link

我们都知道它是 API 的缺失部分,那么您可以自己实现它吗?让我们想想 WebView 到底是什么:

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.

这意味着引擎必须有实现。 WebKit 的实现位于 com.sun.webkit 中(类 的相当一部分只是对原生调用的包装)。当然,在大多数情况下你不想使用 com.sun.* 类,但目前你正在使用 JavaFX,所以这并不重要。

如果我们在 sun 的源代码中稍微跳一下,我们可以找到 WCMediaPlayer.class 一些抽象的音频方法,例如:

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);

来吧Java,让我叫你:

volumeMethod = WCMediaPlayer.class.getDeclaredMethod("setVolume", float.class);
volumeMethod.setAccessible(true);

感谢您的帮助,但我怎样才能获得 WCMediaPlayer 个实例?我们必须查看对 new WCMediaPlayerImpl() 的引用。明白了! WCGraphicsManager.class通过fwkCreateMediaPlayer()方法创建MediaPlayer,毕竟它把指针和实例放在refMap:

Field refMapField = WCGraphicsManager.class.getDeclaredField("refMap");
refMapField.setAccessible(true);

幸好管理员公开了getGraphicsManager()获取实例的方法:

WCGraphicsManager graphicsManager = WCGraphicsManager.getGraphicsManager();
refMap = (Map<Integer, Ref>) refMapField.get(graphicsManager);

捕获的地图包含 Ref 个实例(还有其他 WC* 个实例)因此您必须过滤它们:

Collection<WCMediaPlayer> mediaPlayers = refMap.values().stream()
    .filter(ref -> ref instanceof WCMediaPlayer)
    .map(ref -> (WCMediaPlayer) ref)
    .collect(Collectors.toList());

您可能期望工作示例,所以这是我使用的代码:

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());
    }

}

最后,记住这些调用的顺序。例如,refMap 不包含所有 Refs 直到引擎状态不是 SUCCEEDEDWCGraphicsManager.getGraphicsManager() returns null 如果图形元素是根本没有创建。

可能最好的方法是结合这些解决方案。 JavaFX 提供的 scenerio 和糟糕的 API 很难支持网络技术组合。您也可以尝试嵌入其他浏览器,例如 Chromium。