Fix LivePhotoPlayer: proper init order, error logging, setOnReady

Use SwingUtilities.invokeLater + Platform.runLater for correct init
order. Player only starts in setOnReady callback. Error logging added
so we can see what's failing in the terminal.
This commit is contained in:
Kyle Bolen
2026-07-30 06:10:35 +00:00
parent 7b28c87f2c
commit f21e8dbd3a

View File

@@ -3,88 +3,86 @@ package com.bolenpad.photophetch.ui
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel import androidx.compose.ui.awt.SwingPanel
import javafx.application.Platform
import javafx.embed.swing.JFXPanel
import javafx.scene.Scene
import javafx.scene.layout.StackPane
import javafx.scene.media.Media
import javafx.scene.media.MediaPlayer
import javafx.scene.media.MediaView
import javafx.util.Duration
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.awt.BorderLayout
import java.io.File import java.io.File
import javax.swing.JPanel import javax.swing.JPanel
import java.awt.BorderLayout import javax.swing.SwingUtilities
private val log = LoggerFactory.getLogger("LivePhotoPlayer") private val log = LoggerFactory.getLogger("LivePhotoPlayer")
/** /**
* Embeds a looping JavaFX MediaPlayer inside a Compose Desktop SwingPanel. * Embeds a looping JavaFX MediaPlayer inside a Compose Desktop SwingPanel.
* Used to play Live Photo companion MOV files inline in the preview dialog.
* *
* JavaFX requires its toolkit to be initialized before use. We initialize it * JavaFX toolkit must be initialized before use. We initialize it lazily
* lazily on first use via a JFXPanel (which triggers the JavaFX runtime). * via JFXPanel on first use.
*/ */
@Composable @Composable
fun LivePhotoPlayer( fun LivePhotoPlayer(
videoPath: String, videoPath: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
// Hold a reference to the MediaPlayer so we can dispose it when the composable leaves
var mediaPlayer by remember { mutableStateOf<MediaPlayer?>(null) }
DisposableEffect(videoPath) {
onDispose {
mediaPlayer?.stop()
mediaPlayer?.dispose()
}
}
SwingPanel( SwingPanel(
modifier = modifier, modifier = modifier,
factory = { factory = {
val panel = JPanel(BorderLayout()) val panel = JPanel(BorderLayout())
// JFXPanel initializes the JavaFX runtime on first instantiation SwingUtilities.invokeLater {
val jfxPanel = JFXPanel()
panel.add(jfxPanel, BorderLayout.CENTER)
Platform.runLater {
try { try {
val media = Media(File(videoPath).toURI().toString()) // Initialize JavaFX toolkit via JFXPanel
val player = MediaPlayer(media).apply { val jfxPanel = javafx.embed.swing.JFXPanel()
isAutoPlay = true panel.add(jfxPanel, BorderLayout.CENTER)
cycleCount = MediaPlayer.INDEFINITE // loop forever panel.revalidate()
// Mute — Live Photos shouldn't have audio in preview
isMute = true
setOnEndOfMedia { seek(Duration.ZERO) }
}
mediaPlayer = player
val mediaView = MediaView(player).apply { javafx.application.Platform.runLater {
isPreserveRatio = true try {
fitWidth = 600.0 // reasonable default; panel resizes via Compose val file = File(videoPath)
if (!file.exists()) {
log.warn("LivePhotoPlayer: file not found: $videoPath")
return@runLater
} }
val root = StackPane(mediaView) log.info("LivePhotoPlayer: loading ${file.name} (${file.length() / 1024}KB)")
root.style = "-fx-background-color: black;"
jfxPanel.scene = Scene(root)
val media = javafx.scene.media.Media(file.toURI().toString())
val player = javafx.scene.media.MediaPlayer(media)
player.setOnError {
log.warn("LivePhotoPlayer: MediaPlayer error: ${player.error?.message}")
}
player.setOnReady {
log.info("LivePhotoPlayer: ready, playing")
player.cycleCount = javafx.scene.media.MediaPlayer.INDEFINITE
player.isMute = true
player.play() player.play()
}
val mediaView = javafx.scene.media.MediaView(player).apply {
isPreserveRatio = true
fitWidth = 580.0
}
val root = javafx.scene.layout.StackPane(mediaView)
root.style = "-fx-background-color: black;"
jfxPanel.scene = javafx.scene.Scene(root)
} catch (e: Exception) { } catch (e: Exception) {
log.warn("LivePhotoPlayer: could not initialize JavaFX player for $videoPath: ${e.message}") log.warn("LivePhotoPlayer: JavaFX Platform.runLater failed: ${e.message}")
}
}
} catch (e: Exception) {
log.warn("LivePhotoPlayer: SwingUtilities init failed: ${e.message}")
} }
} }
panel panel
}, },
update = { /* no dynamic updates needed */ },
) )
} }
/** /**
* Check if JavaFX media is available on the classpath. * Check if JavaFX media is available on the classpath.
* Returns true if LivePhotoPlayer can be used.
*/ */
fun isJavaFxMediaAvailable(): Boolean { fun isJavaFxMediaAvailable(): Boolean {
return try { return try {