Khi nào WebView sẵn sàng cho ảnh chụp nhanh ()?


9

Các JavaFX docs trạng thái đó một WebViewđã sẵn sàng khi Worker.State.SUCCEEDEDđạt được tuy nhiên, trừ khi bạn chờ một thời gian (ví dụ Animation, Transition,PauseTransition , vv), một trang trống được trả lại.

Điều này cho thấy rằng có một sự kiện xảy ra bên trong WebView đã sẵn sàng để chụp, nhưng đó là gì?

hơn 7.000 đoạn mã trên GitHub sử dụngSwingFXUtils.fromFXImage nhưng hầu hết chúng có vẻ không liên quan đếnWebView , đều tương tác (mặt nạ con người trong điều kiện cuộc đua) hoặc sử dụng Chuyển đổi tùy ý (bất cứ nơi nào từ 100ms đến 2.000ms).

Tôi đã thử:

  • Lắng nghe changed(...)từ bên trong các WebViewkích thước ( DoublePropertythực hiện các thuộc tính chiều cao và chiều rộng ObservableValue, có thể giám sát những điều này)

    • Không khả thi. Đôi khi, giá trị dường như thay đổi tách biệt với thói quen sơn, dẫn đến nội dung một phần.
  • Nói một cách mù quáng bất cứ điều gì và mọi thứ runLater(...)về Chủ đề ứng dụng FX.

    • TechnanyNhiều kỹ thuật sử dụng điều này, nhưng các thử nghiệm đơn vị của riêng tôi (cũng như một số phản hồi tuyệt vời từ các nhà phát triển khác) giải thích rằng các sự kiện thường đã ở đúng luồng và cuộc gọi này là không cần thiết. Điều tốt nhất tôi có thể nghĩ đến là thêm một độ trễ vừa đủ thông qua việc xếp hàng mà nó hoạt động đối với một số người.
  • Thêm trình nghe / kích hoạt DOM hoặc trình nghe / kích hoạt JavaScript vào WebView

    • OthBoth JavaScript và DOM dường như được tải đúng cách khi SUCCEEDEDđược gọi mặc dù chụp trống. Người nghe DOM / JavaScript dường như không giúp đỡ.
  • Sử dụng một Animationhoặc Transitionđể "ngủ" một cách hiệu quả mà không chặn luồng FX chính.

    • Approach Cách tiếp cận này hoạt động và nếu độ trễ đủ dài, có thể mang lại tới 100% các bài kiểm tra đơn vị, nhưng thời gian Chuyển tiếp dường như là một thời điểm trong tương lai mà chúng ta chỉ đoán và thiết kế tồi. Đối với các ứng dụng biểu diễn hoặc nhiệm vụ quan trọng, điều này buộc lập trình viên phải đánh đổi giữa tốc độ hoặc độ tin cậy, cả hai đều là một trải nghiệm tồi tệ cho người dùng.

Khi nào là thời điểm tốt để gọi WebView.snapshot(...)?

Sử dụng:

SnapshotRaceCondition.initialize();
BufferedImage bufferedImage = SnapshotRaceCondition.capture("<html style='background-color: red;'><h1>TEST</h1></html>");
/**
 * Notes:
 * - The color is to observe the otherwise non-obvious cropping that occurs
 *   with some techniques, such as `setPrefWidth`, `autosize`, etc.
 * - Call this function in a loop and then display/write `BufferedImage` to
 *   to see strange behavior on subsequent calls.
 * - Recommended, modify `<h1>TEST</h1` with a counter to see content from
 *   previous captures render much later.
 */

Đoạn mã:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.WritableImage;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;

public class SnapshotRaceCondition extends Application  {
    private static final Logger log = Logger.getLogger(SnapshotRaceCondition.class.getName());

    // self reference
    private static SnapshotRaceCondition instance = null;

    // concurrent-safe containers for flags/exceptions/image data
    private static AtomicBoolean started  = new AtomicBoolean(false);
    private static AtomicBoolean finished  = new AtomicBoolean(true);
    private static AtomicReference<Throwable> thrown = new AtomicReference<>(null);
    private static AtomicReference<BufferedImage> capture = new AtomicReference<>(null);

    // main javafx objects
    private static WebView webView = null;
    private static Stage stage = null;

    // frequency for checking fx is started
    private static final int STARTUP_TIMEOUT= 10; // seconds
    private static final int STARTUP_SLEEP_INTERVAL = 250; // millis

    // frequency for checking capture has occured 
    private static final int CAPTURE_SLEEP_INTERVAL = 10; // millis

    /** Called by JavaFX thread */
    public SnapshotRaceCondition() {
        instance = this;
    }

    /** Starts JavaFX thread if not already running */
    public static synchronized void initialize() throws IOException {
        if (instance == null) {
            new Thread(() -> Application.launch(SnapshotRaceCondition.class)).start();
        }

        for(int i = 0; i < (STARTUP_TIMEOUT * 1000); i += STARTUP_SLEEP_INTERVAL) {
            if (started.get()) { break; }

            log.fine("Waiting for JavaFX...");
            try { Thread.sleep(STARTUP_SLEEP_INTERVAL); } catch(Exception ignore) {}
        }

        if (!started.get()) {
            throw new IOException("JavaFX did not start");
        }
    }


    @Override
    public void start(Stage primaryStage) {
        started.set(true);
        log.fine("Started JavaFX, creating WebView...");
        stage = primaryStage;
        primaryStage.setScene(new Scene(webView = new WebView()));

        // Add listener for SUCCEEDED
        Worker<Void> worker = webView.getEngine().getLoadWorker();
        worker.stateProperty().addListener(stateListener);

        // Prevents JavaFX from shutting down when hiding window, useful for calling capture(...) in succession
        Platform.setImplicitExit(false);
    }

    /** Listens for a SUCCEEDED state to activate image capture **/
    private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
        if (newState == Worker.State.SUCCEEDED) {
            WritableImage snapshot = webView.snapshot(new SnapshotParameters(), null);

            capture.set(SwingFXUtils.fromFXImage(snapshot, null));
            finished.set(true);
            stage.hide();
        }
    };

    /** Listen for failures **/
    private static ChangeListener<Throwable> exceptListener = new ChangeListener<Throwable>() {
        @Override
        public void changed(ObservableValue<? extends Throwable> obs, Throwable oldExc, Throwable newExc) {
            if (newExc != null) { thrown.set(newExc); }
        }
    };

    /** Loads the specified HTML, triggering stateListener above **/
    public static synchronized BufferedImage capture(final String html) throws Throwable {
        capture.set(null);
        thrown.set(null);
        finished.set(false);

        // run these actions on the JavaFX thread
        Platform.runLater(new Thread(() -> {
            try {
                webView.getEngine().loadContent(html, "text/html");
                stage.show(); // JDK-8087569: will not capture without showing stage
                stage.toBack();
            }
            catch(Throwable t) {
                thrown.set(t);
            }
        }));

        // wait for capture to complete by monitoring our own finished flag
        while(!finished.get() && thrown.get() == null) {
            log.fine("Waiting on capture...");
            try {
                Thread.sleep(CAPTURE_SLEEP_INTERVAL);
            }
            catch(InterruptedException e) {
                log.warning(e.getLocalizedMessage());
            }
        }

        if (thrown.get() != null) {
            throw thrown.get();
        }

        return capture.get();
    }
}

Liên quan:


Platform.runLater không dư thừa. Có thể có các sự kiện đang chờ xử lý cần thiết để WebView hoàn thành kết xuất. Platform.runLater là điều đầu tiên tôi sẽ thử.
VGR

Cuộc đua cũng như các bài kiểm tra đơn vị cho thấy các sự kiện không chờ xử lý, mà xảy ra trong một luồng riêng biệt. Platform.runLaterđã được thử nghiệm và không sửa nó. Hãy thử nó cho chính mình nếu bạn không đồng ý. Tôi rất vui khi được sai, nó sẽ đóng vấn đề.
tresf

Hơn nữa, các tài liệu chính thức tạo ra SUCCEEDEDtrạng thái (trong đó người nghe kích hoạt trên luồng FX) là kỹ thuật phù hợp. Nếu có một cách để hiển thị các sự kiện được xếp hàng, tôi sẽ sẵn sàng thử. Tôi đã tìm thấy các đề xuất thưa thớt thông qua các bình luận trên các diễn đàn của Oracle và một số câu hỏi SO WebViewphải chạy theo chủ đề riêng của nó theo thiết kế, vì vậy sau nhiều ngày thử nghiệm tôi đang tập trung năng lượng ở đó. Nếu giả định đó là sai, tuyệt vời. Tôi sẵn sàng cho bất kỳ đề xuất hợp lý nào khắc phục sự cố mà không có thời gian chờ tùy ý.
tresf

Tôi đã viết bài kiểm tra rất ngắn của riêng mình và có thể lấy thành công ảnh chụp nhanh của WebView trong trình nghe trạng thái của công nhân tải. Nhưng chương trình của bạn không cho tôi một trang trống. Tôi vẫn đang cố gắng để hiểu sự khác biệt.
VGR

Có vẻ như điều này chỉ xảy ra khi sử dụng một loadContentphương thức hoặc khi tải URL tệp.
VGR

Câu trả lời:


1

Có vẻ như đây là một lỗi xảy ra khi sử dụng loadContentcác phương thức của WebEngine . Nó cũng xảy ra khi sử dụng loadđể tải một tệp cục bộ, nhưng trong trường hợp đó, gọi reload () sẽ bù cho nó.

Ngoài ra, vì Giai đoạn cần được hiển thị khi bạn chụp ảnh nhanh, bạn cần gọi show()trước khi tải nội dung. Vì nội dung được tải không đồng bộ, nên hoàn toàn có thể nó sẽ được tải trước câu lệnh sau lệnh gọi đến loadhoặcloadContent kết thúc.

Cách giải quyết, sau đó, là đặt nội dung trong một tệp và gọi WebEngine's reload() phương thức chính xác một lần. Lần thứ hai nội dung được tải, một ảnh chụp nhanh có thể được thực hiện thành công từ người nghe thuộc tính trạng thái của công nhân tải.

Thông thường, điều này sẽ dễ dàng:

Path htmlFile = Files.createTempFile("snapshot-", ".html");
Files.writeString(htmlFile, html);

WebEngine engine = myWebView.getEngine();
engine.getLoadWorker().stateProperty().addListener(
    new ChangeListener<Worker.State>() {
        private boolean reloaded;

        @Override
        public void changed(ObservableValue<? extends Worker.State> obs,
                            Worker.State oldState,
                            Worker.State newState) {
            if (reloaded) {
                Image image = myWebView.snapshot(null, null);
                doStuffWithImage(image);

                try {
                    Files.delete(htmlFile);
                } catch (IOException e) {
                    log.log(Level.WARN, "Couldn't delete " + htmlFile, e);
                }
            } else {
                reloaded = true;
                engine.reload();
            }
        }
    });


engine.load(htmlFile.toUri().toString());

Nhưng vì bạn đang sử dụng staticcho mọi thứ, bạn sẽ phải thêm một số trường:

private static boolean reloaded;
private static volatile Path htmlFile;

Và bạn có thể sử dụng chúng ở đây:

/** Listens for a SUCCEEDED state to activate image capture **/
private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
    if (newState == Worker.State.SUCCEEDED) {
        if (reloaded) {
            WritableImage snapshot = webView.snapshot(new SnapshotParameters(), null);

            capture.set(SwingFXUtils.fromFXImage(snapshot, null));
            finished.set(true);
            stage.hide();

            try {
                Files.delete(htmlFile);
            } catch (IOException e) {
                log.log(Level.WARN, "Couldn't delete " + htmlFile, e);
            }
        } else {
            reloaded = true;
            webView.getEngine().reload();
        }
    }
};

Và sau đó bạn sẽ phải đặt lại mỗi lần bạn tải nội dung:

Path htmlFile = Files.createTempFile("snapshot-", ".html");
Files.writeString(htmlFile, html);

Platform.runLater(new Thread(() -> {
    try {
        reloaded = false;
        stage.show(); // JDK-8087569: will not capture without showing stage
        stage.toBack();
        webView.getEngine().load(htmlFile);
    }
    catch(Throwable t) {
        thrown.set(t);
    }
}));

Lưu ý rằng có nhiều cách tốt hơn để thực hiện xử lý đa luồng. Thay vì sử dụng các lớp nguyên tử, bạn chỉ cần sử dụng volatilecác trường:

private static volatile boolean started;
private static volatile boolean finished = true;
private static volatile Throwable thrown;
private static volatile BufferedImage capture;

(các trường boolean theo mặc định là sai và các trường đối tượng là null theo mặc định. Không giống như trong các chương trình C, đây là một bảo đảm cứng được tạo bởi Java; không có thứ gọi là bộ nhớ chưa được khởi tạo.)

Thay vì bỏ phiếu trong một vòng lặp cho các thay đổi được thực hiện trong một luồng khác, tốt hơn là sử dụng đồng bộ hóa, Khóa hoặc lớp cấp cao hơn như CountDownLatch sử dụng những thứ đó trong nội bộ:

private static final CountDownLatch initialized = new CountDownLatch(1);
private static volatile CountDownLatch finished;
private static volatile BufferedImage capture;
private static volatile Throwable thrown;
private static boolean reloaded;

private static volatile Path htmlFile;

// main javafx objects
private static WebView webView = null;
private static Stage stage = null;

private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
    if (newState == Worker.State.SUCCEEDED) {
        if (reloaded) {
            WritableImage snapshot = webView.snapshot(null, null);
            capture = SwingFXUtils.fromFXImage(snapshot, null);
            finished.countDown();
            stage.hide();

            try {
                Files.delete(htmlFile);
            } catch (IOException e) {
                log.log(Level.WARNING, "Could not delete " + htmlFile, e);
            }
        } else {
            reloaded = true;
            webView.getEngine().reload();
        }
    }
};

@Override
public void start(Stage primaryStage) {
    log.fine("Started JavaFX, creating WebView...");
    stage = primaryStage;
    primaryStage.setScene(new Scene(webView = new WebView()));

    Worker<Void> worker = webView.getEngine().getLoadWorker();
    worker.stateProperty().addListener(stateListener);

    webView.getEngine().setOnError(e -> {
        thrown = e.getException();
    });

    // Prevents JavaFX from shutting down when hiding window, useful for calling capture(...) in succession
    Platform.setImplicitExit(false);

    initialized.countDown();
}

public static BufferedImage capture(String html)
throws InterruptedException,
       IOException {

    htmlFile = Files.createTempFile("snapshot-", ".html");
    Files.writeString(htmlFile, html);

    if (initialized.getCount() > 0) {
        new Thread(() -> Application.launch(SnapshotRaceCondition2.class)).start();
        initialized.await();
    }

    finished = new CountDownLatch(1);
    thrown = null;

    Platform.runLater(() -> {
        reloaded = false;
        stage.show(); // JDK-8087569: will not capture without showing stage
        stage.toBack();
        webView.getEngine().load(htmlFile.toUri().toString());
    });

    finished.await();

    if (thrown != null) {
        throw new IOException(thrown);
    }

    return capture;
}

reloaded không được khai báo là không ổn định vì nó chỉ được truy cập trong luồng ứng dụng JavaFX.


1
Đây là một bài viết rất hay, đặc biệt là các cải tiến mã xung quanh luồng và volatilebiến. Thật không may, gọi WebEngine.reload()và chờ đợi sau đó SUCCEEDEDkhông hoạt động. Nếu tôi đặt một bộ đếm trong nội dung HTML, tôi nhận được: 0, 0, 1, 3, 3, 5thay vì 0, 1, 2, 3, 4, 5, gợi ý rằng nó không thực sự khắc phục điều kiện cuộc đua cơ bản.
tresf

Trích dẫn: "tốt hơn để sử dụng [...] CountDownLatch". Nâng cao vì thông tin này không dễ tìm và nó giúp tăng tốc và đơn giản mã với khởi động FX ban đầu.
tresf

0

Để thay đổi kích thước thay đổi kích thước cũng như hành vi chụp nhanh bên dưới, tôi (chúng tôi) đã đưa ra giải pháp làm việc sau đây. Lưu ý, các thử nghiệm này đã được chạy 2.000x (Windows, macOS và Linux) cung cấp kích thước WebView ngẫu nhiên với thành công 100%.

Đầu tiên, tôi sẽ trích dẫn một trong những nhà phát triển JavaFX. Điều này được trích dẫn từ một báo cáo lỗi riêng (được tài trợ):

"Tôi giả sử bạn bắt đầu thay đổi kích thước trên FX AppThread và nó được thực hiện sau khi đạt đến trạng thái THÀNH CÔNG. Trong trường hợp đó, dường như với tôi rằng tại thời điểm đó, chờ 2 xung (không chặn FX AppThread) sẽ cung cấp cho việc triển khai webkit đủ thời gian để thực hiện các thay đổi của nó, trừ khi điều này dẫn đến một số kích thước được thay đổi trong JavaFX, điều này có thể dẫn đến một lần nữa các kích thước được thay đổi bên trong webkit.

Tôi đang suy nghĩ về cách đưa thông tin này vào cuộc thảo luận trong JBS, nhưng tôi khá chắc chắn sẽ có câu trả lời rằng "bạn chỉ nên chụp ảnh nhanh khi thành phần web ổn định". Vì vậy, để dự đoán câu trả lời này, sẽ tốt hơn nếu xem phương pháp này có hiệu quả với bạn không. Hoặc, nếu nó gây ra các vấn đề khác, sẽ tốt hơn khi nghĩ về những vấn đề này và xem liệu chúng có thể được khắc phục trong chính OpenJFX không. "

  1. Theo mặc định, JavaFX 8 sử dụng mặc định 600nếu chiều cao là chính xác 0. Sử dụng lại mã WebViewnên sử dụng setMinHeight(1), setPrefHeight(1)để tránh vấn đề này. Đây không phải là mã dưới đây, nhưng đáng nói cho bất cứ ai thích ứng nó với dự án của họ.
  2. Để phù hợp với sự sẵn sàng của WebKit, hãy đợi chính xác hai xung từ bên trong bộ hẹn giờ hoạt hình.
  3. Để ngăn lỗi trống snapshot, tận dụng cuộc gọi lại snapshot, cũng lắng nghe xung.
// without this runlater, the first capture is missed and all following captures are offset
Platform.runLater(new Runnable() {
    public void run() {
        // start a new animation timer which waits for exactly two pulses
        new AnimationTimer() {
            int frames = 0;

            @Override
            public void handle(long l) {
                // capture at exactly two frames
                if (++frames == 2) {
                    System.out.println("Attempting image capture");
                    webView.snapshot(new Callback<SnapshotResult,Void>() {
                        @Override
                        public Void call(SnapshotResult snapshotResult) {
                            capture.set(SwingFXUtils.fromFXImage(snapshotResult.getImage(), null));
                            unlatch();
                            return null;
                        }
                    }, null, null);

                    //stop timer after snapshot
                    stop();
                }
            }
        }.start();
    }
});
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.