JavaFX MediaPlayer does not release file handle after dispose

XMLWordPrintable

    • windows

      ADDITIONAL SYSTEM INFORMATION :
      Windows 11, JavaFX 25

      A DESCRIPTION OF THE PROBLEM :
      Referencing an old bug ticket, that was closed, but describes the same problem: JDK-8209876

      Media files are not released by the JVM, even though stop and dispose is called on the JavaFX Media Player.
      Workaround: Calling System.gc() after disposing, releases the file handle, otherwise the whole JVM would need to be shut down to relase the lock.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Run the code below, klick the butons 1 to 5 to select a file, play it, dispose the player and try to rename and delete it.

      ---------- BEGIN SOURCE ----------
      public class MediaPlayerFileHandleDemo extends Application {

      private MediaPlayer player;
      private File selectedFile;

      private final TextArea log = new TextArea();
      private final Label fileLabel = new Label("No file selected");

      @Override
      public void start(final Stage stage) {
      this.log.setEditable(false);
      this.log.setPrefRowCount(18);

      final Button choose = new Button("1) Choose media file");
      choose.setOnAction(e -> chooseFile(stage));

      final Button createAndPlay = new Button("2) Create + Play (short)");
      createAndPlay.setOnAction(e -> createAndPlayShort());

      final Button stopDispose = new Button("3) Stop + Dispose");
      stopDispose.setOnAction(e -> stopAndDispose());

      final Button rename = new Button("4) Rename file (tests handle lock)");
      rename.setOnAction(e -> tryRename());

      final Button delete = new Button("5) Delete file (tests handle lock)");
      delete.setOnAction(e -> tryDelete());

      final HBox row1 = new HBox(10, choose, createAndPlay, stopDispose);
      final HBox row2 = new HBox(10, rename, delete);
      row1.setPadding(new Insets(10));
      row2.setPadding(new Insets(10));

      final VBox root = new VBox(10,
      new VBox(5, new Label("Selected:"), this.fileLabel),
      row1,
      row2,
      new Separator(),
      new Label("Log:"),
      this.log);
      root.setPadding(new Insets(10));

      stage.setScene(new Scene(root, 900, 500));
      stage.setTitle("JavaFX MediaPlayer File Handle Demo");
      stage.show();

      stage.setOnCloseRequest(e -> {
      safeStopDispose();
      Platform.exit();
      });
      }

      private void chooseFile(final Stage stage) {
      final FileChooser fc = new FileChooser();
      fc.setTitle("Pick an audio/video file (mp3/mp4/wav/...)");
      final File f = fc.showOpenDialog(stage);
      if (f != null) {
      this.selectedFile = f;
      this.fileLabel.setText(f.getAbsolutePath());
      log("Selected: " + f);
      }
      }

      private void createAndPlayShort() {
      if (!ensureFile()) {
      return;
      }

      safeStopDispose(); // clean up existing instance first

      try {
      final String uri = this.selectedFile.toURI().toString();
      final Media media = new Media(uri);
      this.player = new MediaPlayer(media);

      this.player.setOnError(() -> log("MediaPlayer error: " + this.player.getError()));
      media.setOnError(() -> log("Media error: " + media.getError()));

      // Play briefly, then stop (to ensure pipeline actually opened the file)
      this.player.play();
      log("player.play() called");

      // Stop after ~700ms (enough to trigger file open)
      new Thread(() -> {
      try {
      Thread.sleep(700);
      } catch (final InterruptedException ignored) {}
      Platform.runLater(() -> {
      if (this.player != null) {
      this.player.stop();
      log("player.stop() called (after short play)");
      }
      });
      }, "StopLater").start();

      } catch (final Exception ex) {
      log("Create/play failed: " + ex);
      }
      }

      private void stopAndDispose() {
      if (this.player == null) {
      log("No player to dispose");
      return;
      }

      try {
      this.player.stop();
      log("player.stop()");

      this.player.dispose();
      log("player.dispose()");

      this.player = null;

      // System.gc();
      // log("System.gc() requested");

      } catch (final Exception ex) {
      log("Stop/dispose failed: " + ex);
      }
      }

      private void tryRename() {
      if (!ensureFile()) {
      return;
      }

      final Path p = this.selectedFile.toPath();
      final Path target = p.resolveSibling(p.getFileName().toString() + ".renamed.tmp");

      try {
      // Ensure target not existing
      Files.deleteIfExists(target);

      Files.move(p, target, StandardCopyOption.ATOMIC_MOVE);
      log("RENAME OK: " + p.getFileName() + " -> " + target.getFileName());

      // move back
      Files.move(target, p, StandardCopyOption.ATOMIC_MOVE);
      log("Rename back OK");

      } catch (final Exception ex) {
      log("RENAME FAILED (possible handle lock): " + ex.getClass().getName() + ": " + ex.getMessage());
      }
      }

      private void tryDelete() {
      if (!ensureFile()) {
      return;
      }

      final Path p = this.selectedFile.toPath();
      try {
      Files.delete(p);
      log("DELETE OK: " + p.getFileName());
      this.selectedFile = null;
      this.fileLabel.setText("No file selected");

      } catch (final Exception ex) {
      log("DELETE FAILED (possible handle lock): " + ex.getClass().getName() + ": " + ex.getMessage());
      }
      }

      private boolean ensureFile() {
      if (this.selectedFile == null) {
      log("Select a file first");
      return false;
      }
      if (!this.selectedFile.exists()) {
      log("Selected file no longer exists: " + this.selectedFile);
      return false;
      }
      return true;
      }

      private void safeStopDispose() {
      if (this.player != null) {
      try {
      this.player.stop();
      } catch (final Exception ignored) {}
      try {
      this.player.dispose();
      } catch (final Exception ignored) {}
      this.player = null;
      }
      }

      private void log(final String msg) {
      final String line = "[" + LocalTime.now().withNano(0) + "] " + msg;
      System.out.println(line);
      this.log.appendText(line + "\n");
      }

      public static void main(final String[] args) {
      launch(args);
      }

      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      If there is a System.gc() call after dispose (see line 139), then steps 4 and 5 will work.

      FREQUENCY :
      ALWAYS

            Assignee:
            Alexander Matveev
            Reporter:
            Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated: