Skip to content

Commit

Permalink
Merge pull request #79 from techbeloved/78-media-playback-multiplatform
Browse files Browse the repository at this point in the history
78-media-playback-multiplatform

Implement bottom ui controls and import tunes from the assets folder into the database
  • Loading branch information
odifek authored Jan 6, 2025
2 parents f8ebacf + 660a21c commit 164adf9
Show file tree
Hide file tree
Showing 36 changed files with 646 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.techbeloved.hymnbook.shared.App
import com.techbeloved.media.PlayerControlViewPreview

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
PlayerControlViewPreview()
App()
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion modules/media/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ kotlin {
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.kotlinx.collections.immutable)

}
iosMain.dependencies {
implementation(libs.kaluga.base)
implementation(libs.kaluga.media)
}
Expand Down
12 changes: 6 additions & 6 deletions modules/media/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application>
<activity
android:exported="true"
android:name=".DemoActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
android:name=".DemoActivity">
</activity>
<service android:name="com.techbeloved.media.MediaPlaybackService"
android:exported="true" />
<service
android:name="com.techbeloved.media.MediaPlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class AndroidPlaybackController(
isPlaying = mediaController.isPlaying
itemIndex = mediaController.currentMediaItemIndex
position = mediaController.currentPosition
mediaId = mediaController.currentMediaItem?.mediaId
updateDuration()
}

Expand All @@ -35,7 +36,6 @@ class AndroidPlaybackController(
updateDuration()
}
state.position = mediaController.currentPosition
println("Current position: ${mediaController.currentPosition}, controller: $this@AndroidPlaybackController")
delay(timeMillis = 100)
}
}
Expand All @@ -44,14 +44,17 @@ class AndroidPlaybackController(
mediaController.listen { events ->
if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
state.isPlaying = mediaController.isPlaying
state.mediaId = mediaController.currentMediaItem?.mediaId
}

if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
state.itemIndex = mediaController.currentMediaItemIndex
state.mediaId = mediaController.currentMediaItem?.mediaId
updateDuration()
}
if (events.contains(Player.EVENT_TRACKS_CHANGED)) {
state.itemIndex = mediaController.currentMediaItemIndex
state.mediaId = mediaController.currentMediaItem?.mediaId
updateDuration()

}
Expand All @@ -62,6 +65,7 @@ class AndroidPlaybackController(

if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
state.playerState = playerState()
state.isPlaying = mediaController.isPlaying
}
if (events.contains(Player.EVENT_TIMELINE_CHANGED)) {
updateDuration()
Expand Down Expand Up @@ -107,6 +111,7 @@ class AndroidPlaybackController(
items.map { item ->
MediaItem.Builder()
.setUri(item.uri)
.setMediaId(item.mediaId)
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(item.title)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

class DemoActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -16,3 +21,11 @@ class DemoActivity : ComponentActivity() {
}
}
}

@androidx.compose.ui.tooling.preview.Preview
@Composable
fun PlayerControlViewPreview() {
MaterialTheme {
MediaPlayerControls(modifier = Modifier.height(300.dp))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.techbeloved.media

import kotlinx.collections.immutable.ImmutableList

class DummyPlaybackController(private val playbackState: PlaybackState) : PlaybackController {
private val queue = mutableListOf<AudioItem>()
override fun play() {
playbackState.isPlaying = true
}

override fun pause() {
playbackState.isPlaying = false
}

override fun seekTo(position: Long) {
playbackState.position = position
}

override fun seekToNext() {
}

override fun seekToPrevious() {
}

override fun setItems(items: ImmutableList<AudioItem>) {
queue.clear()
queue.addAll(items)
}

override fun prepare() {
playbackState.playerState = PlayerState.Ready
}

override fun playWhenReady() {
if (playbackState.playerState == PlayerState.Ready) play()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
Expand All @@ -18,6 +19,10 @@ import kotlinx.coroutines.guava.await

@Composable
actual fun rememberPlaybackController(playbackState: PlaybackState): PlaybackController? {
if (LocalInspectionMode.current) {
return remember { DummyPlaybackController(playbackState) }
}

val context = LocalContext.current

var playbackController: PlaybackController? by remember { mutableStateOf(null) }
Expand All @@ -44,6 +49,5 @@ actual fun rememberPlaybackController(playbackState: PlaybackState): PlaybackCon
}
}
}

return playbackController
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ data class AudioItem(
val title: String,
val artist: String,
val album: String,
val mediaId: String,
) {
fun isMidi() = uri.endsWith(".mid")
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import androidx.compose.material.icons.rounded.SkipPrevious
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
Expand All @@ -31,7 +30,6 @@ import androidx.compose.ui.unit.dp
import hymnbook.modules.media.generated.resources.Res
import kotlinx.collections.immutable.persistentListOf
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.ui.tooling.preview.Preview

@Composable
fun MediaPlayerControls(modifier: Modifier = Modifier) {
Expand All @@ -41,11 +39,8 @@ fun MediaPlayerControls(modifier: Modifier = Modifier) {
var progress by remember { mutableFloatStateOf(0f) }
LaunchedEffect(playbackState) {
snapshotFlow {
playbackState.duration to playbackState.position
}
.collect {
progress = it.second.toFloat() / it.first
}
playbackState.position.toFloat() / playbackState.duration
}.collect { progress = it }
}
LaunchedEffect(playbackState) {
snapshotFlow { playbackState.playerState }
Expand Down Expand Up @@ -126,34 +121,30 @@ private fun loadMediaItems(mediaController: PlaybackController?) {
uri = Res.getUri("files/sample5.mid"),
title = "Midi with joy",
artist = "Gospel artist",
album = "Demo"
album = "Demo",
mediaId = "sample5",
),
AudioItem(
uri = Res.getUri("files/sample2.mp3"),
title = "Sample beats",
artist = "Demo demo",
album = "Demo"
album = "Demo",
mediaId = "sample2",
),
AudioItem(
uri = Res.getUri("files/sample3.mp3"),
title = "Dance with me beats",
artist = "Demo",
album = "Demo"
album = "Demo",
mediaId = "sample3",
),
AudioItem(
uri = Res.getUri("files/sample4.mp3"),
title = "Viertel vor acht",
artist = "DDD",
album = "Triple"
album = "Triple",
mediaId = "sample4",
),
)
)
}

@Preview
@Composable
fun PlayerControlViewPreview() {
MaterialTheme {
MediaPlayerControls(modifier = Modifier.height(300.dp))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class PlaybackState(
duration: Long = 0L,
itemIndex: Int = 0,
playerState: PlayerState = PlayerState.Idle,
mediaId: String? = null,
) {

var isPlaying by mutableStateOf(isPlaying)
Expand All @@ -46,16 +47,33 @@ class PlaybackState(
internal set
var playerState by mutableStateOf(playerState)

/**
* The currently playing media id.
*/
var mediaId by mutableStateOf(mediaId)
internal set


companion object {
val Saver: Saver<PlaybackState, *> = listSaver(
save = { listOf(it.isPlaying, it.position, it.itemIndex, it.duration, it.playerState) },
save = {
listOf(
it.isPlaying,
it.position,
it.itemIndex,
it.duration,
it.playerState,
it.mediaId,
)
},
restore = {
PlaybackState(
isPlaying = it[0] as Boolean,
position = it[1] as Long,
itemIndex = it[2] as Int,
duration = it[3] as Long,
playerState = it[4] as PlayerState,
mediaId = it[5] as String?,
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ class IosPlaybackController(
)
}
player?.prepare()
// Update the current playing media id
state.mediaId = audioItem.mediaId
}

override fun playWhenReady() {
Expand All @@ -80,4 +82,6 @@ class IosPlaybackController(
prepare()
player?.play()
}

fun onDispose() = player?.onDispose()
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,14 @@ class KalugaPlaybackController(
val currentItemIndex = state.itemIndex
if (!queue.indices.contains(currentItemIndex)) return

val currentPlayerItem = mediaSourceFromUrl(queue[currentItemIndex].uri) ?: return
val audioItem = queue[currentItemIndex]
val currentPlayerItem = mediaSourceFromUrl(audioItem.uri) ?: return

controllerScope.launch {
mediaPlayer.reset()
mediaPlayer.initializeFor(currentPlayerItem)
// Update the current playing media id
state.mediaId = audioItem.mediaId

controls.value.play?.perform()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
package com.techbeloved.media

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue

@Composable
actual fun rememberPlaybackController(playbackState: PlaybackState): PlaybackController? {
val coroutineScope = rememberCoroutineScope()
return remember { IosPlaybackController(playbackState, coroutineScope) }
var playbackController: PlaybackController? by remember { mutableStateOf(null) }
DisposableEffect(playbackState) {
val iosPlaybackController = IosPlaybackController(playbackState, coroutineScope)
playbackController = iosPlaybackController
onDispose {
iosPlaybackController.onDispose()
}
}
return playbackController
}
3 changes: 3 additions & 0 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ kotlin {
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.material)
implementation(compose.materialIconsExtended)
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
implementation(libs.kotlinx.collections.immutable)
Expand Down Expand Up @@ -79,6 +80,8 @@ kotlin {
androidMain.dependencies {
api(libs.compose.activity)
implementation(libs.sqldelight.android)
implementation(compose.preview)
implementation(compose.uiTooling)
}
val desktopMain by getting {
dependencies {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.techbeloved.hymnbook.shared.media

internal actual val platformSupportedAudioFormats: List<String> = listOf("ogg", "opus")
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.techbeloved.hymnbook.shared.ui

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.ui.graphics.vector.ImageVector

internal actual val navigationArrowBack: ImageVector = Icons.AutoMirrored.Filled.ArrowBack
Loading

0 comments on commit 164adf9

Please sign in to comment.