Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .run/Browser.run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<GradleProfilingDisabled>true</GradleProfilingDisabled>
<GradleCoverageDisabled>true</GradleCoverageDisabled>
<method v="2" />
</configuration>
</component>
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
jna-platform-jpms = { module = "net.java.dev.jna:jna-platform-jpms", version.ref = "jna"}
platformtools-darkmodedetector = { module = "io.github.kdroidfilter:platformtools.darkmodedetector", version.ref = "platformtoolsDarkmodedetector" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4jSimple" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "kotlin" }

[plugins]

Expand Down
6 changes: 2 additions & 4 deletions mediaplayer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ tasks.withType<DokkaTask>().configureEach {
offlineMode.set(true)
}


kotlin {
jvmToolchain(17)
androidTarget { publishLibraryVariants("release") }
Expand Down Expand Up @@ -96,6 +95,7 @@ kotlin {
implementation(libs.androidx.media3.ui)
implementation(libs.androidx.activityCompose)
implementation(libs.androidx.core)
implementation(libs.androidx.lifecycle.runtime.ktx)
}

androidUnitTest.dependencies {
Expand Down Expand Up @@ -230,7 +230,5 @@ mavenPublishing {
}
}

publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)

signAllPublications()
this.signAllPublications()
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package io.github.kdroidfilter.composemediaplayer

import android.app.Activity
import android.app.PictureInPictureParams
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Rational
import androidx.annotation.OptIn
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
Expand All @@ -29,10 +38,13 @@ import androidx.media3.ui.PlayerView
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import com.kdroid.androidcontextprovider.ContextProvider
import io.github.kdroidfilter.composemediaplayer.util.PipResult
import io.github.kdroidfilter.composemediaplayer.util.formatTime
import io.github.vinceglb.filekit.AndroidFile
import io.github.vinceglb.filekit.PlatformFile
import kotlinx.coroutines.*
import kotlinx.coroutines.android.awaitFrame
import java.lang.ref.WeakReference

@OptIn(UnstableApi::class)
actual fun createVideoPlayerState(): VideoPlayerState =
Expand Down Expand Up @@ -75,7 +87,24 @@ internal val androidVideoLogger = Logger.withTag("AndroidVideoPlayerSurface")

@UnstableApi
@Stable
open class DefaultVideoPlayerState: VideoPlayerState {
open class DefaultVideoPlayerState : VideoPlayerState {
companion object {
var activity: WeakReference<Activity> = WeakReference(null)

private var currentPlayerState: WeakReference<DefaultVideoPlayerState>? = null

/**
* Call this from Activity.onPictureInPictureModeChanged()
*/
fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
currentPlayerState?.get()?.isPipActive = isInPictureInPictureMode
}

internal fun register(state: DefaultVideoPlayerState) {
currentPlayerState = WeakReference(state)
}
}

private val context: Context = ContextProvider.getContext()
internal var exoPlayer: ExoPlayer? = null
private var updateJob: Job? = null
Expand Down Expand Up @@ -231,6 +260,7 @@ open class DefaultVideoPlayerState: VideoPlayerState {
private var _aspectRatio by mutableFloatStateOf(16f / 9f)
override val aspectRatio: Float get() = _aspectRatio


// Fullscreen state
private var _isFullscreen by mutableStateOf(false)
override var isFullscreen: Boolean
Expand All @@ -239,15 +269,30 @@ open class DefaultVideoPlayerState: VideoPlayerState {
_isFullscreen = value
}

var isPipFullScreen by mutableStateOf(false)

// Time tracking
private var _currentTime by mutableDoubleStateOf(0.0)
private var _duration by mutableDoubleStateOf(0.0)
override val positionText: String get() = formatTime(_currentTime)
override val durationText: String get() = formatTime(_duration)
override val currentTime: Double get() = _currentTime

override val isPipSupported: Boolean
get() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val ctx = activity.get() ?: ContextProvider.getContext()
return ctx.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
}
return false
}

override var isPipEnabled by mutableStateOf(false)
override var isPipActive by mutableStateOf(false)


init {
register(this)
audioProcessor.setOnAudioLevelUpdateListener { left, right ->
_leftLevel = left
_rightLevel = right
Expand Down Expand Up @@ -295,6 +340,7 @@ open class DefaultVideoPlayerState: VideoPlayerState {
}
}
}

Intent.ACTION_SCREEN_ON -> {
androidVideoLogger.d { "Screen turned on (unlocked)" }
synchronized(playerInitializationLock) {
Expand Down Expand Up @@ -458,14 +504,17 @@ open class DefaultVideoPlayerState: VideoPlayerState {
// 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 -> {
_error = VideoPlayerError.NetworkError("Network error: ${error.message}")
}

PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE,
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
_error = VideoPlayerError.SourceError("Invalid media source: ${error.message}")
}

else -> {
_error = VideoPlayerError.UnknownError("Playback error: ${error.message}")
}
Expand Down Expand Up @@ -632,6 +681,44 @@ open class DefaultVideoPlayerState: VideoPlayerState {
}
}

fun togglePipFullScreen() {
isPipFullScreen = !isPipFullScreen
}


override suspend fun enterPip(): PipResult {
if (!isPipSupported) return PipResult.NotSupported
if (!isPipEnabled) return PipResult.NotEnabled
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return PipResult.NotPossible

val currentActivity = activity.get() ?: return PipResult.NotPossible

if (!isPipFullScreen) {
togglePipFullScreen()
// Wait for Compose to recompose with fullscreen layout
withFrameNanos { }
withFrameNanos { } // two frames to be safe
}

val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
setAutoEnterEnabled(true)
}
}
.build()

val result = currentActivity.enterPictureInPictureMode(params)

return if (result) {
isPipActive = true
PipResult.Success
} else {
PipResult.NotPossible
}
}

override fun seekTo(value: Float) {
if (_duration > 0 && !isPlayerReleased) {
val targetTime = (value / 1000.0) * _duration
Expand Down
Loading