WebView พร้อมสำหรับ snapshot เมื่อใด ()


9

JavaFX เอกสารรัฐที่WebViewพร้อมเมื่อWorker.State.SUCCEEDEDถึงแต่ถ้าคุณรอในขณะที่ (เช่นAnimation, Transition, PauseTransitionฯลฯ ) หน้าว่างที่มีการแสดง

สิ่งนี้ชี้ให้เห็นว่ามีเหตุการณ์ที่เกิดขึ้นภายใน WebView ที่พร้อมสำหรับการดักจับ แต่มันคืออะไร

มีตัวอย่างโค้ดมากกว่า 7,000 รายการใน GitHub ที่ใช้SwingFXUtils.fromFXImageแต่ส่วนใหญ่ดูเหมือนว่าไม่เกี่ยวข้องกับWebViewมันเป็นแบบอินเทอร์แอคทีฟ (มาสก์มนุษย์ที่มีสภาพการแข่งขัน) หรือใช้การเปลี่ยนแปลงตามอำเภอใจ

ฉันได้พยายาม:

  • ฟังchanged(...)จากภายในส่วนWebViewข้อมูล ( DoublePropertyใช้คุณสมบัติความสูงและความกว้างObservableValueซึ่งสามารถตรวจสอบสิ่งเหล่านี้ได้)

    • v ไม่สามารถทำงานได้ บางครั้งค่าดูเหมือนจะเปลี่ยนจากรูทีนการระบายสีซึ่งนำไปสู่เนื้อหาบางส่วน
  • บอกอะไร ๆ ทุกอย่างไปrunLater(...)ที่ FX Application Thread

    • techniques เทคนิคหลายอย่างใช้สิ่งนี้ แต่การทดสอบหน่วยของฉันเอง (เช่นเดียวกับข้อเสนอแนะที่ยอดเยี่ยมจากนักพัฒนาอื่น ๆ ) อธิบายว่าเหตุการณ์มักจะอยู่ในเธรดที่ถูกต้องและการโทรนี้ซ้ำซ้อน สิ่งที่ดีที่สุดที่ฉันสามารถนึกได้ก็คือการเพิ่มความล่าช้าเพียงพอผ่านการเข้าคิวที่ใช้งานได้สำหรับบางคน
  • การเพิ่มผู้ฟัง / ทริกเกอร์ DOM หรือทริกเกอร์ / ทริกเกอร์ JavaScript ให้กับ WebView

    • JavaScript ทั้ง JavaScript และ DOM ดูเหมือนจะโหลดอย่างถูกต้องเมื่อSUCCEEDEDมีการเรียกแม้จะมีการจับภาพเปล่า ฟัง DOM / JavaScript ดูเหมือนจะไม่ช่วย
  • การใช้AnimationหรือTransitionเพื่อ "สลีป" อย่างมีประสิทธิภาพโดยไม่ปิดกั้นเธรด FX หลัก

    • approach วิธีการนี้ใช้งานได้และหากการหน่วงเวลานั้นนานพอสามารถสร้างผลการทดสอบหน่วยได้ถึง 100% แต่เวลาการเปลี่ยนภาพนั้นดูเหมือนจะเป็นช่วงเวลาในอนาคตที่เราคาดเดาและออกแบบไม่ดี สำหรับแอพพลิเคชั่นที่มีประสิทธิภาพหรือมีภารกิจสำคัญบังคับให้โปรแกรมเมอร์ทำการแลกเปลี่ยนระหว่างความเร็วหรือความน่าเชื่อถือซึ่งเป็นประสบการณ์ที่ไม่ดีสำหรับผู้ใช้

เวลาไหนดีในการโทรWebView.snapshot(...)?

การใช้งาน:

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.
 */

ตัวอย่างโค้ด:

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

ที่เกี่ยวข้อง:


Platform.runLater ไม่ซ้ำซ้อน อาจมีเหตุการณ์ที่รอดำเนินการที่จำเป็นสำหรับ WebView เพื่อให้การเรนเดอร์เสร็จสมบูรณ์ Platform.runLater เป็นสิ่งแรกที่ฉันจะลอง
VGR

การแข่งขันเช่นเดียวกับการทดสอบหน่วยแสดงให้เห็นเหตุการณ์ที่ไม่ได้อยู่ระหว่างดำเนินการ แต่จะเกิดขึ้นในหัวข้อแยกต่างหาก Platform.runLaterได้รับการทดสอบและไม่สามารถแก้ไขได้ โปรดลองด้วยตัวคุณเองถ้าคุณไม่เห็นด้วย ฉันยินดีที่จะผิดมันจะปิดปัญหา
tresf

นอกจากนี้เอกสารอย่างเป็นทางการจัดเวทีSUCCEEDEDรัฐ (ซึ่งผู้ฟังยิงบนเธรด FX) เป็นเทคนิคที่เหมาะสม หากมีวิธีแสดงเหตุการณ์ที่อยู่ในคิวฉันจะได้ลองดู ฉันได้พบข้อเสนอแนะกระจัดกระจายผ่านความคิดเห็นในฟอรัม Oracle และคำถาม SO ที่WebViewต้องทำงานในเธรดของตัวเองโดยการออกแบบดังนั้นหลังจากวันที่ทดสอบฉันมุ่งเน้นพลังงานที่นั่น หากข้อสันนิษฐานนั้นผิด ฉันเปิดให้คำแนะนำที่สมเหตุสมผลที่แก้ไขปัญหาโดยไม่ต้องรอเวลาโดยพลการ
tresf

ฉันเขียนแบบทดสอบสั้น ๆ ของตัวเองและสามารถรับ WebView ในสแน็ปช็อตในตัวฟังสถานะของพนักงานโหลดได้สำเร็จ แต่โปรแกรมของคุณจะให้หน้าว่างกับฉัน ฉันยังคงพยายามเข้าใจความแตกต่าง
VGR

ปรากฏว่าเกิดขึ้นเมื่อใช้loadContentวิธีการหรือเมื่อโหลดไฟล์ URL เท่านั้น
VGR

คำตอบ:


1

ดูเหมือนว่านี่เป็นข้อผิดพลาดที่เกิดขึ้นเมื่อใช้loadContentวิธีการของ WebEngine นอกจากนี้ยังเกิดขึ้นเมื่อใช้loadเพื่อโหลดไฟล์ในเครื่อง แต่ในกรณีนั้นการเรียกreload ()จะชดเชยให้

นอกจากนี้เนื่องจากสเตจจะต้องแสดงเมื่อคุณถ่ายภาพสแนปชอตคุณต้องโทรshow()ก่อนที่จะโหลดเนื้อหา เนื่องจากเนื้อหาถูกโหลดแบบอะซิงโครนัสจึงเป็นไปได้ทั้งหมดว่าจะโหลดก่อนที่คำสั่งจะตามหลังการเรียกloadหรือloadContentสิ้นสุด

วิธีแก้ปัญหาคือวางเนื้อหาในไฟล์และเรียกใช้เมธอดของ WebEngine reload()เพียงครั้งเดียว ครั้งที่สองที่โหลดเนื้อหาสแน็ปช็อตสามารถทำได้สำเร็จจากผู้ฟังของคุณสมบัติสถานะของผู้ทำงานโหลด

โดยปกติสิ่งนี้จะง่าย:

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

แต่เนื่องจากคุณใช้staticทุกอย่างคุณจะต้องเพิ่มบางฟิลด์:

private static boolean reloaded;
private static volatile Path htmlFile;

และคุณสามารถใช้มันได้ที่นี่:

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

จากนั้นคุณจะต้องรีเซ็ตทุกครั้งที่คุณโหลดเนื้อหา:

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

โปรดทราบว่ามีวิธีที่ดีกว่าในการประมวลผลแบบมัลติเธรด แทนที่จะใช้คลาสอะตอมมิกคุณสามารถใช้volatileฟิลด์:

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

(เขตข้อมูลบูลีนเป็นเท็จโดยค่าเริ่มต้นและเขตข้อมูลวัตถุเป็นค่าเริ่มต้นซึ่งแตกต่างจากในโปรแกรม C นี่คือการรับประกันอย่างหนักที่ทำโดย Java; ไม่มีสิ่งเช่นหน่วยความจำเริ่มต้น)

แทนที่จะโพลในการวนซ้ำสำหรับการเปลี่ยนแปลงที่เกิดขึ้นในเธรดอื่นการใช้ซิงโครไนซ์ล็อคหรือคลาสระดับสูงกว่าเช่นCountDownLatchซึ่งใช้สิ่งเหล่านั้นภายใน:

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 ไม่ได้ประกาศว่ามีความผันผวนเนื่องจากมีการเข้าถึงได้ในเธรดแอปพลิเคชัน JavaFX เท่านั้น


1
นี่เป็นบทความที่ดีมากโดยเฉพาะการปรับปรุงโค้ดรอบเธรดและvolatileตัวแปร น่าเสียดายที่การโทรWebEngine.reload()และรอต่อมาSUCCEEDEDไม่ทำงาน หากฉันวางเคาน์เตอร์ในเนื้อหา HTML ฉันได้รับ: 0, 0, 1, 3, 3, 5แทนที่จะ0, 1, 2, 3, 4, 5แนะนำว่ามันไม่ได้แก้ไขสภาพการแข่งขันที่แท้จริง
tresf

อ้างอิง: "ใช้ดีกว่า [... ] CountDownLatch" การถอนเงินเนื่องจากข้อมูลนี้หาไม่ได้ง่ายและช่วยให้ความเร็วและความเรียบง่ายของรหัสด้วยการเริ่มต้น FX ครั้งแรก
tresf

0

เพื่อรองรับการปรับขนาดเช่นเดียวกับพฤติกรรมสแนปชอตพื้นฐานฉัน (เรา) มาพร้อมกับโซลูชันการทำงานต่อไปนี้ หมายเหตุการทดสอบเหล่านี้เรียกใช้ 2,000x (Windows, macOS และ Linux) ที่ให้ขนาด WebView แบบสุ่มด้วยความสำเร็จ 100%

ก่อนอื่นฉันจะอ้างอิงหนึ่งใน JavaFX devs นี่คือยกมาจากรายงานข้อผิดพลาดส่วนตัว (สนับสนุน):

"ฉันคิดว่าคุณเริ่มต้นการปรับขนาดใน FX AppThread และมันจะทำหลังจากถึงสถานะที่ถูกต้องในกรณีนี้ดูเหมือนว่าสำหรับฉันในขณะนั้นรอ 2 พัลส์ (โดยไม่ปิดกั้น FX AppThread) ควรให้ การใช้งาน webkit มีเวลามากพอที่จะทำการเปลี่ยนแปลงเว้นแต่จะส่งผลให้บางส่วนมีการเปลี่ยนแปลงใน JavaFX ซึ่งอาจส่งผลให้มีการเปลี่ยนแปลงขนาดใน webkit อีกครั้ง

ฉันกำลังคิดว่าจะป้อนข้อมูลนี้ในการสนทนาใน JBS แต่ฉันค่อนข้างแน่ใจว่าจะมีคำตอบว่า "คุณควรถ่ายรูปเพียงเมื่อเว็บคอมโพเนนต์นั้นมีเสถียรภาพ" ดังนั้นเพื่อที่จะคาดการณ์คำตอบนี้จะเป็นการดีที่จะเห็นว่าวิธีการนี้ใช้ได้ผลสำหรับคุณหรือไม่ หรือหากปรากฏว่าก่อให้เกิดปัญหาอื่น ๆ ก็เป็นการดีที่จะคิดถึงปัญหาเหล่านี้และดูว่าสามารถแก้ไขได้อย่างไรใน OpenJFX เอง "

  1. โดยค่าเริ่มต้น JavaFX 8 จะใช้ค่าเริ่มต้น600ว่าส่วนสูงนั้นตรง0หรือไม่ โค้ดWebViewควรใช้setMinHeight(1), setPrefHeight(1)เพื่อหลีกเลี่ยงปัญหานี้ สิ่งนี้ไม่ได้อยู่ในรหัสด้านล่าง แต่ควรกล่าวถึงสำหรับผู้ที่ปรับตัวเข้ากับโครงการของพวกเขา
  2. เพื่อรองรับความพร้อมของ WebKit รอสองจังหวะจากภายในตัวจับเวลาการเคลื่อนไหว
  3. เพื่อป้องกันบั๊กของสแนปชอตเปล่าให้ใช้ประโยชน์จากสแน็ปช็อตการโทรกลับซึ่งฟังชีพจร
// 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();
    }
});
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.