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.ui.Modifier
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 java.awt.BorderLayout
import java.io.File
import javax.swing.JPanel
import java.awt.BorderLayout
import javax.swing.SwingUtilities
private val log = LoggerFactory.getLogger("LivePhotoPlayer")
/**
* 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
* lazily on first use via a JFXPanel (which triggers the JavaFX runtime).
* JavaFX toolkit must be initialized before use. We initialize it lazily
* via JFXPanel on first use.
*/
@Composable
fun LivePhotoPlayer(
videoPath: String,
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(
modifier = modifier,
factory = {
val panel = JPanel(BorderLayout())
// JFXPanel initializes the JavaFX runtime on first instantiation
val jfxPanel = JFXPanel()
panel.add(jfxPanel, BorderLayout.CENTER)
Platform.runLater {
SwingUtilities.invokeLater {
try {
val media = Media(File(videoPath).toURI().toString())
val player = MediaPlayer(media).apply {
isAutoPlay = true
cycleCount = MediaPlayer.INDEFINITE // loop forever
// Mute — Live Photos shouldn't have audio in preview
isMute = true
setOnEndOfMedia { seek(Duration.ZERO) }
// Initialize JavaFX toolkit via JFXPanel
val jfxPanel = javafx.embed.swing.JFXPanel()
panel.add(jfxPanel, BorderLayout.CENTER)
panel.revalidate()
javafx.application.Platform.runLater {
try {
val file = File(videoPath)
if (!file.exists()) {
log.warn("LivePhotoPlayer: file not found: $videoPath")
return@runLater
}
log.info("LivePhotoPlayer: loading ${file.name} (${file.length() / 1024}KB)")
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()
}
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) {
log.warn("LivePhotoPlayer: JavaFX Platform.runLater failed: ${e.message}")
}
}
mediaPlayer = player
val mediaView = MediaView(player).apply {
isPreserveRatio = true
fitWidth = 600.0 // reasonable default; panel resizes via Compose
}
val root = StackPane(mediaView)
root.style = "-fx-background-color: black;"
jfxPanel.scene = Scene(root)
player.play()
} catch (e: Exception) {
log.warn("LivePhotoPlayer: could not initialize JavaFX player for $videoPath: ${e.message}")
log.warn("LivePhotoPlayer: SwingUtilities init failed: ${e.message}")
}
}
panel
},
update = { /* no dynamic updates needed */ },
)
}
/**
* Check if JavaFX media is available on the classpath.
* Returns true if LivePhotoPlayer can be used.
*/
fun isJavaFxMediaAvailable(): Boolean {
return try {