diff --git a/mediaplayer/src/androidMain/AndroidManifest.xml b/mediaplayer/src/androidMain/AndroidManifest.xml
index 74b7379f..34a2f984 100644
--- a/mediaplayer/src/androidMain/AndroidManifest.xml
+++ b/mediaplayer/src/androidMain/AndroidManifest.xml
@@ -1,3 +1,4 @@
+
\ No newline at end of file
diff --git a/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.android.kt b/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.android.kt
index 0aeb1962..52c9b214 100644
--- a/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.android.kt
+++ b/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.android.kt
@@ -7,6 +7,8 @@ import android.content.IntentFilter
import android.net.Uri
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableDoubleStateOf
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
@@ -20,6 +22,7 @@ import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.audio.AudioSink
import androidx.media3.exoplayer.audio.DefaultAudioSink
+import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
import androidx.media3.ui.CaptionStyleCompat
import androidx.media3.ui.PlayerView
import co.touchlab.kermit.Logger
@@ -44,7 +47,12 @@ actual open class VideoPlayerState {
private var updateJob: Job? = null
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val audioProcessor = AudioLevelProcessor()
-
+
+ // Protection contre les race conditions
+ private var isPlayerReleased = false
+ private val playerInitializationLock = Object()
+ private var playerListener: Player.Listener? = null
+
// Screen lock detection
private var screenLockReceiver: BroadcastReceiver? = null
private var wasPlayingBeforeScreenLock: Boolean = false
@@ -89,57 +97,55 @@ actual open class VideoPlayerState {
return
}
- // Update current track and enable flag.
currentSubtitleTrack = track
subtitlesEnabled = true
- // We're not using ExoPlayer's native subtitle rendering anymore
- // Instead, we're using Compose-based subtitles
-
exoPlayer?.let { player ->
- // Disable native subtitles in ExoPlayer
val trackParameters = player.trackSelectionParameters.buildUpon()
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
.build()
player.trackSelectionParameters = trackParameters
- // Hide the subtitle view
playerView?.subtitleView?.visibility = android.view.View.GONE
}
}
actual fun disableSubtitles() {
- // Update state
currentSubtitleTrack = null
subtitlesEnabled = false
- // We're not using ExoPlayer's native subtitle rendering anymore
- // Instead, we're using Compose-based subtitles
-
exoPlayer?.let { player ->
- // Disable native subtitles in ExoPlayer
val parameters = player.trackSelectionParameters.buildUpon()
.setPreferredTextLanguage(null)
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
.build()
player.trackSelectionParameters = parameters
- // Hide the subtitle view
playerView?.subtitleView?.visibility = android.view.View.GONE
}
}
- internal fun attachPlayerView(view: PlayerView) {
+ internal fun attachPlayerView(view: PlayerView?) {
+ if (view == null) {
+ // Détacher la vue actuelle
+ playerView?.player = null
+ playerView = null
+ return
+ }
+
playerView = view
exoPlayer?.let { player ->
- view.player = player
- // Set default subtitle style
- view.subtitleView?.setStyle(CaptionStyleCompat.DEFAULT)
+ try {
+ view.player = player
+ view.subtitleView?.setStyle(CaptionStyleCompat.DEFAULT)
+ } catch (e: Exception) {
+ androidVideoLogger.e { "Error attaching player to view: ${e.message}" }
+ }
}
}
// Volume control
- private var _volume by mutableStateOf(1f)
+ private var _volume by mutableFloatStateOf(1f)
actual var volume: Float
get() = _volume
set(value) {
@@ -148,7 +154,7 @@ actual open class VideoPlayerState {
}
// Slider position
- private var _sliderPos by mutableStateOf(0f)
+ private var _sliderPos by mutableFloatStateOf(0f)
actual var sliderPos: Float
get() = _sliderPos
set(value) {
@@ -171,7 +177,7 @@ actual open class VideoPlayerState {
}
// Playback speed control
- private var _playbackSpeed by mutableStateOf(1.0f)
+ private var _playbackSpeed by mutableFloatStateOf(1.0f)
actual var playbackSpeed: Float
get() = _playbackSpeed
set(value) {
@@ -182,13 +188,13 @@ actual open class VideoPlayerState {
}
// Audio levels
- private var _leftLevel by mutableStateOf(0f)
- private var _rightLevel by mutableStateOf(0f)
+ private var _leftLevel by mutableFloatStateOf(0f)
+ private var _rightLevel by mutableFloatStateOf(0f)
actual val leftLevel: Float get() = _leftLevel
actual val rightLevel: Float get() = _rightLevel
// Aspect ratio
- private var _aspectRatio by mutableStateOf(16f / 9f)
+ private var _aspectRatio by mutableFloatStateOf(16f / 9f)
actual val aspectRatio: Float get() = _aspectRatio
// Fullscreen state
@@ -200,8 +206,8 @@ actual open class VideoPlayerState {
}
// Time tracking
- private var _currentTime by mutableStateOf(0.0)
- private var _duration by mutableStateOf(0.0)
+ private var _currentTime by mutableDoubleStateOf(0.0)
+ private var _duration by mutableDoubleStateOf(0.0)
actual val positionText: String get() = formatTime(_currentTime)
actual val durationText: String get() = formatTime(_duration)
actual val currentTime: Double get() = _currentTime
@@ -215,93 +221,136 @@ actual open class VideoPlayerState {
initializePlayer()
registerScreenLockReceiver()
}
-
- /**
- * Registers a BroadcastReceiver to listen for screen on/off events
- */
+
+ private fun shouldUseConservativeCodecHandling(): Boolean {
+ val device = android.os.Build.DEVICE
+ val manufacturer = android.os.Build.MANUFACTURER
+ val model = android.os.Build.MODEL
+
+ // Liste des appareils connus pour avoir des problèmes MediaCodec
+ val problematicDevices = setOf(
+ "SM-A155F", // Galaxy A15
+ "SM-A156B", // Galaxy A15 5G
+ // Ajouter d'autres modèles problématiques ici
+ )
+
+ return device in problematicDevices ||
+ model in problematicDevices ||
+ manufacturer.equals("mediatek", ignoreCase = true)
+ }
+
private fun registerScreenLockReceiver() {
- // Unregister any existing receiver first
unregisterScreenLockReceiver()
-
- // Create a new receiver
+
screenLockReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intent.ACTION_SCREEN_OFF -> {
- Logger.d { "Screen turned off (locked)" }
- // Store current playing state
- wasPlayingBeforeScreenLock = _isPlaying
-
- // Pause playback when screen is locked
- if (_isPlaying) {
- Logger.d { "Pausing playback due to screen lock" }
- exoPlayer?.pause()
- // Note: We don't need to update _isPlaying here because
- // the onIsPlayingChanged listener will handle that
+ androidVideoLogger.d { "Screen turned off (locked)" }
+ synchronized(playerInitializationLock) {
+ if (!isPlayerReleased && exoPlayer != null) {
+ wasPlayingBeforeScreenLock = _isPlaying
+ if (_isPlaying) {
+ try {
+ androidVideoLogger.d { "Pausing playback due to screen lock" }
+ exoPlayer?.pause()
+ } catch (e: Exception) {
+ androidVideoLogger.e { "Error pausing on screen lock: ${e.message}" }
+ }
+ }
+ }
}
}
Intent.ACTION_SCREEN_ON -> {
- Logger.d { "Screen turned on (unlocked)" }
- // Resume playback if it was playing before screen lock
- if (wasPlayingBeforeScreenLock) {
- Logger.d { "Resuming playback after screen unlock" }
- exoPlayer?.play()
- // Note: We don't need to update _isPlaying here because
- // the onIsPlayingChanged listener will handle that
+ androidVideoLogger.d { "Screen turned on (unlocked)" }
+ synchronized(playerInitializationLock) {
+ if (!isPlayerReleased && wasPlayingBeforeScreenLock && exoPlayer != null) {
+ try {
+ // Ajouter un petit délai pour s'assurer que le système est prêt
+ coroutineScope.launch {
+ delay(200)
+ if (!isPlayerReleased) {
+ androidVideoLogger.d { "Resuming playback after screen unlock" }
+ exoPlayer?.play()
+ }
+ }
+ } catch (e: Exception) {
+ androidVideoLogger.e { "Error resuming after screen unlock: ${e.message}" }
+ }
+ }
}
}
}
}
}
-
- // Register the receiver
+
val filter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_SCREEN_ON)
}
context.registerReceiver(screenLockReceiver, filter)
- Logger.d { "Screen lock receiver registered" }
+ androidVideoLogger.d { "Screen lock receiver registered" }
}
-
- /**
- * Unregisters the screen lock BroadcastReceiver
- */
+
private fun unregisterScreenLockReceiver() {
screenLockReceiver?.let {
try {
context.unregisterReceiver(it)
- Logger.d { "Screen lock receiver unregistered" }
+ androidVideoLogger.d { "Screen lock receiver unregistered" }
} catch (e: Exception) {
- Logger.e { "Error unregistering screen lock receiver: ${e.message}" }
+ androidVideoLogger.e { "Error unregistering screen lock receiver: ${e.message}" }
}
screenLockReceiver = null
}
}
private fun initializePlayer() {
- val audioSink = DefaultAudioSink.Builder(context)
- .setAudioProcessors(arrayOf(audioProcessor))
- .build()
-
- val renderersFactory = object : DefaultRenderersFactory(context) {
- override fun buildAudioSink(
- context: Context,
- enableFloatOutput: Boolean,
- enableAudioTrackPlaybackParams: Boolean
- ): AudioSink = audioSink
- }.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
-
- exoPlayer = ExoPlayer.Builder(context)
- .setRenderersFactory(renderersFactory)
- .build()
- .apply {
- addListener(createPlayerListener())
- volume = _volume
+ synchronized(playerInitializationLock) {
+ if (isPlayerReleased) return
+
+ val audioSink = DefaultAudioSink.Builder(context)
+ .setAudioProcessors(arrayOf(audioProcessor))
+ .build()
+
+ val renderersFactory = object : DefaultRenderersFactory(context) {
+ override fun buildAudioSink(
+ context: Context,
+ enableFloatOutput: Boolean,
+ enableAudioTrackPlaybackParams: Boolean
+ ): AudioSink = audioSink
+ }.apply {
+ setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
+ // Activer le fallback du décodeur pour une meilleure stabilité
+ setEnableDecoderFallback(true)
+
+ // Sur les appareils problématiques, utiliser des paramètres plus conservateurs
+ if (shouldUseConservativeCodecHandling()) {
+ // On ne peut pas désactiver l'async queueing car la méthode n'existe pas
+ // Mais on peut utiliser le MediaCodecSelector par défaut
+ setMediaCodecSelector(MediaCodecSelector.DEFAULT)
+ }
}
+
+ exoPlayer = ExoPlayer.Builder(context)
+ .setRenderersFactory(renderersFactory)
+ .setHandleAudioBecomingNoisy(true)
+ .setWakeMode(C.WAKE_MODE_LOCAL)
+ .setPauseAtEndOfMediaItems(false)
+ .setReleaseTimeoutMs(2000) // Augmenter le timeout de libération
+ .build()
+ .apply {
+ playerListener = createPlayerListener()
+ addListener(playerListener!!)
+ volume = _volume
+ }
+ }
}
private fun createPlayerListener() = object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
+ // Ajouter une vérification de sécurité
+ if (isPlayerReleased) return
+
when (playbackState) {
Player.STATE_BUFFERING -> {
_isLoading = true
@@ -310,12 +359,12 @@ actual open class VideoPlayerState {
Player.STATE_READY -> {
_isLoading = false
exoPlayer?.let { player ->
- _duration = player.duration.toDouble() / 1000.0
- _isPlaying = player.isPlaying
- if (player.isPlaying) startPositionUpdates()
-
- // Extract format metadata when the player is ready
- extractFormatMetadata(player)
+ if (!isPlayerReleased) {
+ _duration = player.duration.toDouble() / 1000.0
+ _isPlaying = player.isPlaying
+ if (player.isPlaying) startPositionUpdates()
+ extractFormatMetadata(player)
+ }
}
}
@@ -332,50 +381,117 @@ actual open class VideoPlayerState {
}
override fun onIsPlayingChanged(playing: Boolean) {
- _isPlaying = playing
- if (playing) {
- startPositionUpdates()
- } else {
- stopPositionUpdates()
+ if (!isPlayerReleased) {
+ _isPlaying = playing
+ if (playing) {
+ startPositionUpdates()
+ } else {
+ stopPositionUpdates()
+ }
}
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
- // Update aspect ratio when video size changes
if (videoSize.width > 0 && videoSize.height > 0) {
_aspectRatio = videoSize.width.toFloat() / videoSize.height.toFloat()
- // Update metadata
_metadata.width = videoSize.width
_metadata.height = videoSize.height
}
}
override fun onPlayerError(error: PlaybackException) {
- _error = when (error.errorCode) {
- PlaybackException.ERROR_CODE_DECODER_INIT_FAILED ->
- VideoPlayerError.CodecError("Decoder initialization failed: ${error.message}")
-
+ androidVideoLogger.e { "Player error occurred: ${error.errorCode} - ${error.message}" }
+
+ // Créer un rapport d'erreur détaillé
+ val errorDetails = mapOf(
+ "error_code" to error.errorCode.toString(),
+ "error_message" to (error.message ?: "Unknown"),
+ "device" to android.os.Build.DEVICE,
+ "model" to android.os.Build.MODEL,
+ "manufacturer" to android.os.Build.MANUFACTURER,
+ "android_version" to android.os.Build.VERSION.SDK_INT.toString(),
+ "codec_info" to error.cause?.message
+ )
+
+ // Log the error details (you can send this to your crash reporting service)
+ androidVideoLogger.e { "Detailed error info: $errorDetails" }
+
+ // Gestion des erreurs spécifiques au codec
+ when (error.errorCode) {
+ PlaybackException.ERROR_CODE_DECODER_INIT_FAILED,
+ PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED -> {
+ _error = VideoPlayerError.CodecError("Decoder error: ${error.message}")
+ // Tenter une récupération pour les erreurs de codec
+ attemptPlayerRecovery()
+ }
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
- PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT ->
- VideoPlayerError.NetworkError("Network error: ${error.message}")
-
+ PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT -> {
+ _error = VideoPlayerError.NetworkError("Network error: ${error.message}")
+ }
PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE,
- PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS ->
- VideoPlayerError.SourceError("Invalid media source: ${error.message}")
-
- else -> VideoPlayerError.UnknownError("Playback error: ${error.message}")
+ PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
+ _error = VideoPlayerError.SourceError("Invalid media source: ${error.message}")
+ }
+ else -> {
+ _error = VideoPlayerError.UnknownError("Playback error: ${error.message}")
+ }
}
_isPlaying = false
_isLoading = false
}
}
+ private fun attemptPlayerRecovery() {
+ coroutineScope.launch {
+ delay(100) // Petit délai pour laisser le système nettoyer
+
+ synchronized(playerInitializationLock) {
+ if (!isPlayerReleased) {
+ exoPlayer?.let { player ->
+ val currentPosition = player.currentPosition
+ val currentMediaItem = player.currentMediaItem
+ val wasPlaying = player.isPlaying
+
+ try {
+ // Retirer le listener avant de libérer
+ playerListener?.let { player.removeListener(it) }
+
+ // Libérer le lecteur actuel
+ player.release()
+
+ // Réinitialiser
+ initializePlayer()
+
+ // Restaurer l'élément média et la position
+ currentMediaItem?.let {
+ exoPlayer?.apply {
+ setMediaItem(it)
+ prepare()
+ seekTo(currentPosition)
+ // Restaurer l'état de lecture si nécessaire
+ if (wasPlaying) {
+ play()
+ } else {
+ pause()
+ }
+ }
+ }
+ } catch (e: Exception) {
+ androidVideoLogger.e { "Error during player recovery: ${e.message}" }
+ _error = VideoPlayerError.UnknownError("Recovery failed: ${e.message}")
+ }
+ }
+ }
+ }
+ }
+ }
+
private fun startPositionUpdates() {
stopPositionUpdates()
updateJob = coroutineScope.launch {
while (isActive) {
exoPlayer?.let { player ->
- if (player.playbackState == Player.STATE_READY) {
+ if (player.playbackState == Player.STATE_READY && !isPlayerReleased) {
_currentTime = player.currentPosition.toDouble() / 1000.0
if (!userDragging && _duration > 0) {
_sliderPos = (_currentTime / _duration * 1000).toFloat()
@@ -392,106 +508,98 @@ actual open class VideoPlayerState {
updateJob = null
}
- /**
- * Open a video URI.
- * We're not using ExoPlayer's native subtitle rendering anymore,
- * so we don't need to add subtitle configurations to the MediaItem.
- *
- * @param uri The URI of the media to open
- * @param initializeplayerState Controls whether playback starts automatically (PLAY) or remains paused (PAUSE)
- */
actual fun openUri(uri: String, initializeplayerState: InitialPlayerState) {
val mediaItemBuilder = MediaItem.Builder().setUri(uri)
- // We're not using ExoPlayer's native subtitle rendering anymore
- // Instead, we're using Compose-based subtitles
val mediaItem = mediaItemBuilder.build()
openFromMediaItem(mediaItem, initializeplayerState)
}
- /**
- * Open a video file.
- * Converts the file into a URI.
- * We're not using ExoPlayer's native subtitle rendering anymore,
- * so we don't need to add subtitle configurations to the MediaItem.
- *
- * @param file The file to open
- * @param initializeplayerState Controls whether playback starts automatically (PLAY) or remains paused (PAUSE)
- */
actual fun openFile(file: PlatformFile, initializeplayerState: InitialPlayerState) {
val mediaItemBuilder = MediaItem.Builder()
- val androidFile = file.androidFile
- val videoUri: Uri = when (androidFile) {
+ val videoUri: Uri = when (val androidFile = file.androidFile) {
is AndroidFile.UriWrapper -> androidFile.uri
is AndroidFile.FileWrapper -> Uri.fromFile(androidFile.file)
}
mediaItemBuilder.setUri(videoUri)
- // We're not using ExoPlayer's native subtitle rendering anymore
- // Instead, we're using Compose-based subtitles
val mediaItem = mediaItemBuilder.build()
openFromMediaItem(mediaItem, initializeplayerState)
}
private fun openFromMediaItem(mediaItem: MediaItem, initializeplayerState: InitialPlayerState) {
- exoPlayer?.let { player ->
- player.stop()
- player.clearMediaItems()
- try {
- _error = null
- resetStates(keepMedia = true)
- player.setMediaItem(mediaItem)
-
- // Extract metadata from the MediaItem before preparing the player
- extractMediaItemMetadata(mediaItem)
-
- player.prepare()
- player.volume = volume
- player.repeatMode = if (loop) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF
+ synchronized(playerInitializationLock) {
+ if (isPlayerReleased) return
- // Control initial playback state based on the parameter
- if (initializeplayerState == InitialPlayerState.PLAY) {
- player.play()
- _hasMedia = true
- } else {
- // Initialize player but don't start playback
- player.pause()
+ exoPlayer?.let { player ->
+ player.stop()
+ player.clearMediaItems()
+ try {
+ _error = null
+ resetStates(keepMedia = true)
+
+ // Extraire les métadonnées avant de préparer le lecteur
+ extractMediaItemMetadata(mediaItem)
+
+ player.setMediaItem(mediaItem)
+ player.prepare()
+ player.volume = volume
+ player.repeatMode = if (loop) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF
+
+ // Contrôler l'état de lecture initial
+ if (initializeplayerState == InitialPlayerState.PLAY) {
+ player.play()
+ _hasMedia = true
+ } else {
+ player.pause()
+ _isPlaying = false
+ _hasMedia = true
+ }
+ } catch (e: Exception) {
+ androidVideoLogger.d { "Error opening media: ${e.message}" }
_isPlaying = false
- _hasMedia = true
+ _hasMedia = false
+ _error = VideoPlayerError.SourceError("Failed to load media: ${e.message}")
}
- } catch (e: Exception) {
- androidVideoLogger.d { "Error opening media: ${e.message}" }
- _isPlaying = false
- _hasMedia = false // Set to false on error
- _error = VideoPlayerError.SourceError("Failed to load media: ${e.message}")
}
}
}
actual fun play() {
- exoPlayer?.let { player ->
- if (player.playbackState == Player.STATE_IDLE) {
- // If the player is in IDLE state (after stop), prepare it again
- player.prepare()
+ synchronized(playerInitializationLock) {
+ if (!isPlayerReleased) {
+ exoPlayer?.let { player ->
+ if (player.playbackState == Player.STATE_IDLE) {
+ player.prepare()
+ }
+ player.play()
+ }
+ _hasMedia = true
}
- player.play()
}
- _hasMedia = true
}
actual fun pause() {
- exoPlayer?.pause()
+ synchronized(playerInitializationLock) {
+ if (!isPlayerReleased) {
+ exoPlayer?.pause()
+ }
+ }
}
actual fun stop() {
- exoPlayer?.let { player ->
- player.stop()
- player.seekTo(0) // Reset position to beginning
+ synchronized(playerInitializationLock) {
+ if (!isPlayerReleased) {
+ exoPlayer?.let { player ->
+ player.stop()
+ player.seekTo(0)
+ }
+ _hasMedia = false
+ resetStates(keepMedia = true)
+ }
}
- _hasMedia = false
- resetStates(keepMedia = true)
}
actual fun seekTo(value: Float) {
- if (_duration > 0) {
+ if (_duration > 0 && !isPlayerReleased) {
val targetTime = (value / 1000.0) * _duration
exoPlayer?.seekTo((targetTime * 1000).toLong())
}
@@ -501,31 +609,22 @@ actual open class VideoPlayerState {
_error = null
}
- /**
- * Toggles the fullscreen state of the video player
- */
actual fun toggleFullscreen() {
_isFullscreen = !_isFullscreen
}
- /**
- * Extracts metadata from the player
- */
private fun extractFormatMetadata(player: Player) {
try {
- // Extract duration if available
if (player.duration > 0 && player.duration != C.TIME_UNSET) {
_metadata.duration = player.duration
}
- // Extract format information from tracks
player.currentTracks.groups.forEach { group ->
for (i in 0 until group.length) {
val trackFormat = group.getTrackFormat(i)
when (group.type) {
C.TRACK_TYPE_VIDEO -> {
- // Video format metadata
if (trackFormat.frameRate > 0) {
_metadata.frameRate = trackFormat.frameRate
}
@@ -540,7 +639,6 @@ actual open class VideoPlayerState {
}
C.TRACK_TYPE_AUDIO -> {
- // Audio format metadata
if (trackFormat.channelCount > 0) {
_metadata.audioChannels = trackFormat.channelCount
}
@@ -553,7 +651,6 @@ actual open class VideoPlayerState {
}
}
- // Extract media item metadata
extractMediaItemMetadata(player.currentMediaItem)
androidVideoLogger.d { "Metadata extracted: $_metadata" }
@@ -562,13 +659,9 @@ actual open class VideoPlayerState {
}
}
- /**
- * Extracts metadata from the MediaItem
- */
private fun extractMediaItemMetadata(mediaItem: MediaItem?) {
try {
mediaItem?.mediaMetadata?.let { metadata ->
- // Extract title if available
metadata.title?.toString()?.let { _metadata.title = it }
}
} catch (e: Exception) {
@@ -585,9 +678,9 @@ actual open class VideoPlayerState {
_isPlaying = false
_isLoading = false
_error = null
- _aspectRatio = 16f / 9f // Reset aspect ratio to default
- _playbackSpeed = 1.0f // Reset playback speed to default
- _metadata = VideoMetadata() // Reset metadata to a new empty instance
+ _aspectRatio = 16f / 9f
+ _playbackSpeed = 1.0f
+ _metadata = VideoMetadata()
exoPlayer?.playbackParameters = PlaybackParameters(_playbackSpeed)
if (!keepMedia) {
_hasMedia = false
@@ -595,17 +688,31 @@ actual open class VideoPlayerState {
}
actual fun dispose() {
- stopPositionUpdates()
- coroutineScope.cancel()
- playerView?.player = null
- playerView = null
- exoPlayer?.let { player ->
- player.stop()
- player.release()
+ synchronized(playerInitializationLock) {
+ isPlayerReleased = true
+ stopPositionUpdates()
+ coroutineScope.cancel()
+ playerView?.player = null
+ playerView = null
+
+ try {
+ exoPlayer?.let { player ->
+ // Retirer le listener spécifiquement
+ playerListener?.let { listener ->
+ player.removeListener(listener)
+ }
+ player.stop()
+ player.clearMediaItems()
+ player.release()
+ }
+ } catch (e: Exception) {
+ androidVideoLogger.e { "Error during player disposal: ${e.message}" }
+ }
+
+ playerListener = null
+ exoPlayer = null
+ unregisterScreenLockReceiver()
+ resetStates()
}
- exoPlayer = null
- // Unregister the screen lock receiver to prevent memory leaks
- unregisterScreenLockReceiver()
- resetStates()
}
-}
+}
\ No newline at end of file
diff --git a/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.android.kt b/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.android.kt
index 28c237b7..127ec5d8 100644
--- a/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.android.kt
+++ b/mediaplayer/src/androidMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.android.kt
@@ -2,6 +2,7 @@ package io.github.kdroidfilter.composemediaplayer
import android.content.Context
import android.view.LayoutInflater
+import androidx.annotation.OptIn
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
@@ -66,29 +67,41 @@ private fun VideoPlayerSurfaceInternal(
overlay: @Composable () -> Unit
) {
// Use rememberSaveable to preserve fullscreen state across configuration changes
- var isFullscreen by rememberSaveable {
- mutableStateOf(playerState.isFullscreen)
+ var isFullscreen by rememberSaveable {
+ mutableStateOf(playerState.isFullscreen)
}
-
+
// Keep the playerState.isFullscreen in sync with our saved state
LaunchedEffect(isFullscreen) {
if (playerState.isFullscreen != isFullscreen) {
playerState.isFullscreen = isFullscreen
}
}
-
+
// Listen for changes from playerState.isFullscreen
LaunchedEffect(playerState.isFullscreen) {
if (isFullscreen != playerState.isFullscreen) {
isFullscreen = playerState.isFullscreen
}
}
-
+
+ // Nettoyer lorsque le composable est détruit
+ DisposableEffect(playerState) {
+ onDispose {
+ try {
+ // Détacher la vue du player
+ playerState.attachPlayerView(null)
+ } catch (e: Exception) {
+ androidVideoLogger.e { "Error detaching PlayerView on dispose: ${e.message}" }
+ }
+ }
+ }
+
if (isFullscreen) {
// Use FullScreenLayout for fullscreen mode
FullScreenLayout(
modifier = Modifier,
- onDismissRequest = {
+ onDismissRequest = {
isFullscreen = false
// Call playerState.toggleFullscreen() to ensure proper cleanup
playerState.toggleFullscreen()
@@ -129,62 +142,82 @@ private fun VideoPlayerContent(
modifier = modifier,
contentAlignment = Alignment.Center
) {
- if (playerState.hasMedia) {
+ if (playerState.hasMedia && playerState.exoPlayer != null) {
AndroidView(
- modifier =
- contentScale.toCanvasModifier(playerState.aspectRatio,playerState.metadata.width,playerState.metadata.height),
+ modifier = contentScale.toCanvasModifier(
+ playerState.aspectRatio,
+ playerState.metadata.width,
+ playerState.metadata.height
+ ),
factory = { context ->
- // Create PlayerView with subtitles support
- createPlayerViewWithSurfaceType(context, surfaceType).apply {
- // Attach the player from the state
- player = playerState.exoPlayer
- useController = false
- defaultArtwork = null
- setShutterBackgroundColor(android.graphics.Color.TRANSPARENT)
- setBackgroundColor(android.graphics.Color.TRANSPARENT)
-
- // Map Compose ContentScale to ExoPlayer resize modes
- resizeMode = when (contentScale) {
- ContentScale.Crop -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
- ContentScale.FillBounds -> AspectRatioFrameLayout.RESIZE_MODE_FILL
- ContentScale.Fit, ContentScale.Inside -> AspectRatioFrameLayout.RESIZE_MODE_FIT
- ContentScale.FillWidth -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
- ContentScale.FillHeight -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
- else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
- }
+ try {
+ // Créer PlayerView avec le type de surface approprié
+ createPlayerViewWithSurfaceType(context, surfaceType).apply {
+ // Attacher le lecteur depuis l'état
+ player = playerState.exoPlayer
+ useController = false
+ defaultArtwork = null
+ setShutterBackgroundColor(android.graphics.Color.TRANSPARENT)
+ setBackgroundColor(android.graphics.Color.TRANSPARENT)
+
+ // Mapper ContentScale vers les modes de redimensionnement ExoPlayer
+ resizeMode = mapContentScaleToResizeMode(contentScale)
- // Disable native subtitle view since we're using Compose-based subtitles
- subtitleView?.visibility = android.view.View.GONE
+ // Désactiver la vue de sous-titres native car nous utilisons des sous-titres basés sur Compose
+ subtitleView?.visibility = android.view.View.GONE
- // Attach this view to the player state
- playerState.attachPlayerView(this)
+ // Attacher cette vue à l'état du lecteur
+ playerState.attachPlayerView(this)
+ }
+ } catch (e: Exception) {
+ androidVideoLogger.e { "Error creating PlayerView: ${e.message}" }
+ // Retourner une vue vide en cas d'erreur
+ PlayerView(context).apply {
+ setBackgroundColor(android.graphics.Color.BLACK)
+ }
}
},
update = { playerView ->
- // Update the resize mode when contentScale changes
- playerView.resizeMode = when (contentScale) {
- ContentScale.Crop -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
- ContentScale.FillBounds -> AspectRatioFrameLayout.RESIZE_MODE_FILL
- ContentScale.Fit, ContentScale.Inside -> AspectRatioFrameLayout.RESIZE_MODE_FIT
- ContentScale.FillWidth -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
- ContentScale.FillHeight -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
- else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
+ try {
+ // Vérifier que le player est toujours valide avant la mise à jour
+ if (playerState.exoPlayer != null && playerView.player != null) {
+ // Mettre à jour le mode de redimensionnement lorsque contentScale change
+ playerView.resizeMode = mapContentScaleToResizeMode(contentScale)
+ }
+ } catch (e: Exception) {
+ androidVideoLogger.e { "Error updating PlayerView: ${e.message}" }
}
},
onReset = { playerView ->
- // Clean up resources when the view is recycled in a LazyList
- playerView.player = null
+ try {
+ // Nettoyer les ressources lorsque la vue est recyclée dans une LazyList
+ playerView.player = null
+ playerView.onPause()
+ } catch (e: Exception) {
+ androidVideoLogger.e { "Error resetting PlayerView: ${e.message}" }
+ }
+ },
+ onRelease = { playerView ->
+ try {
+ // Nettoyer complètement la vue lors de sa libération
+ playerView.player = null
+ } catch (e: Exception) {
+ androidVideoLogger.e { "Error releasing PlayerView: ${e.message}" }
+ }
}
)
- // Add Compose-based subtitle layer
+ // Ajouter une couche de sous-titres basée sur Compose
if (playerState.subtitlesEnabled && playerState.currentSubtitleTrack != null) {
- // Calculate current time in milliseconds
- val currentTimeMs = (playerState.sliderPos / 1000f *
- playerState.durationText.toTimeMs()).toLong()
+ // Calculer le temps actuel en millisecondes
+ val currentTimeMs = remember(playerState.sliderPos, playerState.durationText) {
+ (playerState.sliderPos / 1000f * playerState.durationText.toTimeMs()).toLong()
+ }
- // Calculate duration in milliseconds
- val durationMs = playerState.durationText.toTimeMs()
+ // Calculer la durée en millisecondes
+ val durationMs = remember(playerState.durationText) {
+ playerState.durationText.toTimeMs()
+ }
ComposeSubtitleLayer(
currentTimeMs = currentTimeMs,
@@ -198,19 +231,70 @@ private fun VideoPlayerContent(
}
}
- // Render the overlay content on top of the video with fillMaxSize modifier
- // to ensure it takes the full height of the parent Box
+ // Rendre le contenu de l'overlay au-dessus de la vidéo avec le modificateur fillMaxSize
+ // pour s'assurer qu'il prend toute la hauteur du Box parent
Box(modifier = Modifier.fillMaxSize()) {
overlay()
}
}
}
-private fun createPlayerViewWithSurfaceType(context: Context, surfaceType: SurfaceType): PlayerView {
- val layoutId = when (surfaceType) {
- SurfaceType.SurfaceView -> R.layout.player_view_surface
- SurfaceType.TextureView -> R.layout.player_view_texture
+@OptIn(UnstableApi::class)
+private fun mapContentScaleToResizeMode(contentScale: ContentScale): Int {
+ return when (contentScale) {
+ ContentScale.Crop -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
+ ContentScale.FillBounds -> AspectRatioFrameLayout.RESIZE_MODE_FILL
+ ContentScale.Fit, ContentScale.Inside -> AspectRatioFrameLayout.RESIZE_MODE_FIT
+ ContentScale.FillWidth -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
+ ContentScale.FillHeight -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
+ else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
}
-
- return LayoutInflater.from(context).inflate(layoutId, null) as PlayerView
}
+
+@OptIn(UnstableApi::class)
+private fun createPlayerViewWithSurfaceType(context: Context, surfaceType: SurfaceType): PlayerView {
+ return try {
+ // Essayer d'abord d'inflater les layouts personnalisés
+ val layoutId = when (surfaceType) {
+ SurfaceType.SurfaceView -> R.layout.player_view_surface
+ SurfaceType.TextureView -> R.layout.player_view_texture
+ }
+
+ LayoutInflater.from(context).inflate(layoutId, null) as PlayerView
+ } catch (e: Exception) {
+ androidVideoLogger.e { "Error inflating PlayerView layout: ${e.message}, creating programmatically" }
+
+ // Créer PlayerView programmatiquement pour éviter les problèmes de ressources manquantes
+ try {
+ PlayerView(context).apply {
+ // Désactiver complètement les contrôles pour éviter l'inflation du layout des contrôles
+ useController = false
+
+ // Configurer le type de surface programmatiquement
+ when (surfaceType) {
+ SurfaceType.TextureView -> {
+ // Utiliser TextureView si disponible
+ videoSurfaceView?.let { view ->
+ if (view is android.view.TextureView) {
+ androidVideoLogger.d { "Using TextureView" }
+ }
+ }
+ }
+ SurfaceType.SurfaceView -> {
+ // SurfaceView est le défaut
+ androidVideoLogger.d { "Using SurfaceView" }
+ }
+ }
+
+ // Désactiver les fonctionnalités qui pourraient causer des problèmes
+ controllerAutoShow = false
+ controllerHideOnTouch = false
+ setShowBuffering(PlayerView.SHOW_BUFFERING_NEVER)
+ }
+ } catch (e2: Exception) {
+ androidVideoLogger.e { "Error creating PlayerView programmatically: ${e2.message}" }
+ // Dernier recours : créer une vue vide pour éviter le crash
+ throw RuntimeException("Unable to create PlayerView", e2)
+ }
+ }
+}
\ No newline at end of file
diff --git a/mediaplayer/src/androidMain/res/layout/exo_player_control_view_empty.xml b/mediaplayer/src/androidMain/res/layout/exo_player_control_view_empty.xml
new file mode 100644
index 00000000..007dafd7
--- /dev/null
+++ b/mediaplayer/src/androidMain/res/layout/exo_player_control_view_empty.xml
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/mediaplayer/src/androidMain/res/layout/player_view_surface.xml b/mediaplayer/src/androidMain/res/layout/player_view_surface.xml
index c18237be..9f42016c 100644
--- a/mediaplayer/src/androidMain/res/layout/player_view_surface.xml
+++ b/mediaplayer/src/androidMain/res/layout/player_view_surface.xml
@@ -1,7 +1,9 @@
\ No newline at end of file
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:surface_type="surface_view"
+ app:show_buffering="when_playing"
+ app:use_controller="false" />
\ No newline at end of file
diff --git a/mediaplayer/src/androidMain/res/layout/player_view_texture.xml b/mediaplayer/src/androidMain/res/layout/player_view_texture.xml
index c152b906..e572a759 100644
--- a/mediaplayer/src/androidMain/res/layout/player_view_texture.xml
+++ b/mediaplayer/src/androidMain/res/layout/player_view_texture.xml
@@ -1,7 +1,9 @@
\ No newline at end of file
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:surface_type="texture_view"
+ app:show_buffering="when_playing"
+ app:use_controller="false" />
\ No newline at end of file