Skip to content

Commit

Permalink
Fix initial aspect ratio (#466)
Browse files Browse the repository at this point in the history
  • Loading branch information
MGaetan89 authored Mar 14, 2024
1 parent 3e69226 commit c4c141c
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package ch.srgssr.pillarbox.player

import androidx.media3.common.Format
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException
Expand All @@ -14,9 +15,9 @@ import androidx.media3.common.Timeline
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.Tracks
import androidx.media3.common.VideoSize
import ch.srgssr.pillarbox.player.extension.computeAspectRatio
import ch.srgssr.pillarbox.player.extension.getCurrentMediaItems
import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed
import ch.srgssr.pillarbox.player.extension.video
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose
Expand All @@ -25,11 +26,9 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.isActive
import kotlin.time.Duration
Expand Down Expand Up @@ -289,17 +288,15 @@ fun Player.videoSizeAsFlow(): Flow<VideoSize> = callbackFlow {
}

/**
* Get aspect ratio as flow
* Get aspect ratio of the current video as [Flow].
*
* @param defaultAspectRatio Aspect ratio when [Player.getVideoSize] is unknown or audio.
* @param defaultAspectRatio The aspect ratio when the video size is unknown, or for audio content.
*/
fun Player.getAspectRatioAsFlow(defaultAspectRatio: Float): Flow<Float> =
videoSizeAsFlow()
.filterNot { it == VideoSize.UNKNOWN }
.map {
it.computeAspectRatio(defaultAspectRatio)
}
.onEmpty { emit(defaultAspectRatio) }
fun Player.getAspectRatioAsFlow(defaultAspectRatio: Float): Flow<Float> {
return getCurrentTracksAsFlow()
.map { it.getVideoAspectRatioOrElse(defaultAspectRatio) }
.distinctUntilChanged()
}

/**
* Get track selection parameters as flow [Player.getTrackSelectionParameters]
Expand Down Expand Up @@ -348,6 +345,16 @@ private suspend fun <T> ProducerScope<T>.addPlayerListener(player: Player, liste
}
}

private fun Tracks.getVideoAspectRatioOrElse(defaultAspectRatio: Float): Float {
val format = video.find { it.isSelected }?.getTrackFormat(0)

return if (format == null || format.height <= 0 || format.width == Format.NO_VALUE) {
defaultAspectRatio
} else {
format.width * format.pixelWidthHeightRatio / format.height.toFloat()
}
}

/**
* Default update interval.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
package ch.srgssr.pillarbox.player

import androidx.media3.common.C
import androidx.media3.common.Format
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.common.PlaybackException
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
import androidx.media3.common.Player.Commands
import androidx.media3.common.Timeline
import androidx.media3.common.TrackGroup
import androidx.media3.common.Tracks
import androidx.media3.common.VideoSize
import androidx.test.ext.junit.runners.AndroidJUnit4
import app.cash.turbine.test
import ch.srgssr.pillarbox.player.test.utils.PlayerListenerCommander
import ch.srgssr.pillarbox.player.utils.StringUtil
Expand All @@ -26,13 +31,16 @@ import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class TestPlayerCallbackFlow {
private lateinit var player: Player
private lateinit var dispatcher: CoroutineDispatcher

@Before
@OptIn(ExperimentalCoroutinesApi::class)
fun setUp() {
dispatcher = UnconfinedTestDispatcher()
player = mockk(relaxed = true)
Expand Down Expand Up @@ -354,4 +362,198 @@ class TestPlayerCallbackFlow {
ensureAllEventsConsumed()
}
}

@Test
fun `get aspect ratio as flow, default aspect ratio`() = runTest {
val fakePlayer = PlayerListenerCommander(player)

fakePlayer.getAspectRatioAsFlow(16 / 9f).test {
assertEquals(16 / 9f, awaitItem())
ensureAllEventsConsumed()
}
}

@Test
fun `get aspect ratio as flow, empty tracks`() = runTest {
val fakePlayer = PlayerListenerCommander(player)

fakePlayer.getAspectRatioAsFlow(0f).test {
fakePlayer.onTracksChanged(Tracks.EMPTY)

assertEquals(0f, awaitItem())
ensureAllEventsConsumed()
}
}

@Test
fun `get aspect ratio as flow, video tracks`() = runTest {
val videoTracks = Tracks(
listOf(
createTrackGroup(
selectedIndex = 1,
createVideoFormat("v1", width = 800, height = 600),
createVideoFormat("v2", width = 1440, height = 900),
createVideoFormat("v3", width = 1920, height = 1080),
)
)
)

val fakePlayer = PlayerListenerCommander(player)

fakePlayer.getAspectRatioAsFlow(0f).test {
fakePlayer.onTracksChanged(videoTracks)

assertEquals(0f, awaitItem())
assertEquals(4 / 3f, awaitItem())
ensureAllEventsConsumed()
}
}

@Test
fun `get aspect ratio as flow, video tracks no video size`() = runTest {
val videoTracks = Tracks(
listOf(
createTrackGroup(
selectedIndex = 1,
createVideoFormat("v1", width = Format.NO_VALUE, height = Format.NO_VALUE),
createVideoFormat("v2", width = Format.NO_VALUE, height = Format.NO_VALUE),
createVideoFormat("v3", width = Format.NO_VALUE, height = Format.NO_VALUE),
)
)
)

val fakePlayer = PlayerListenerCommander(player)

fakePlayer.getAspectRatioAsFlow(0f).test {
fakePlayer.onTracksChanged(videoTracks)

assertEquals(0f, awaitItem())
ensureAllEventsConsumed()
}
}

@Test
fun `get aspect ratio as flow, video tracks without selection`() = runTest {
val videoTracksWithoutSelection = Tracks(
listOf(
createTrackGroup(
selectedIndex = -1,
createVideoFormat("v1", width = 800, height = 600),
createVideoFormat("v2", width = 1440, height = 900),
createVideoFormat("v3", width = 1920, height = 1080),
)
)
)

val fakePlayer = PlayerListenerCommander(player)

fakePlayer.getAspectRatioAsFlow(0f).test {
fakePlayer.onTracksChanged(videoTracksWithoutSelection)

assertEquals(0f, awaitItem())
ensureAllEventsConsumed()
}
}

@Test
fun `get aspect ratio as flow, audio tracks`() = runTest {
val audioTracks = Tracks(
listOf(
createTrackGroup(
selectedIndex = 1,
createAudioFormat("v1"),
createAudioFormat("v2"),
createAudioFormat("v3"),
)
)
)

val fakePlayer = PlayerListenerCommander(player)

fakePlayer.getAspectRatioAsFlow(0f).test {
fakePlayer.onTracksChanged(audioTracks)

assertEquals(0f, awaitItem())
ensureAllEventsConsumed()
}
}

@Test
fun `get aspect ratio as flow, changing tracks`() = runTest {
val videoTracksWithoutSelection = Tracks(
listOf(
createTrackGroup(
selectedIndex = 1,
createVideoFormat("v1", width = 800, height = 600),
createVideoFormat("v2", width = 1440, height = 900),
createVideoFormat("v3", width = 1920, height = 1080),
)
)
)
val audioTracks = Tracks(
listOf(
createTrackGroup(
selectedIndex = 1,
createAudioFormat("v1"),
createAudioFormat("v2"),
createAudioFormat("v3"),
)
)
)

val fakePlayer = PlayerListenerCommander(player)

fakePlayer.getAspectRatioAsFlow(0f).test {
fakePlayer.onTracksChanged(videoTracksWithoutSelection)
fakePlayer.onTracksChanged(audioTracks)

assertEquals(0f, awaitItem())
assertEquals(4 / 3f, awaitItem())
assertEquals(0f, awaitItem())
ensureAllEventsConsumed()
}
}

private companion object {
private const val AUDIO_MIME_TYPE = MimeTypes.AUDIO_MP4
private const val VIDEO_MIME_TYPE = MimeTypes.VIDEO_H265

private fun createAudioFormat(label: String): Format {
return Format.Builder()
.setId("id:$label")
.setLabel(label)
.setLanguage("fr")
.setContainerMimeType(AUDIO_MIME_TYPE)
.build()
}

private fun createVideoFormat(
label: String,
width: Int,
height: Int,
): Format {
return Format.Builder()
.setId("id:$label")
.setLabel(label)
.setLanguage("fr")
.setWidth(width)
.setHeight(height)
.setContainerMimeType(VIDEO_MIME_TYPE)
.build()
}

private fun createTrackGroup(
selectedIndex: Int,
vararg formats: Format,
): Tracks.Group {
val trackGroup = TrackGroup(*formats)
val trackSupport = IntArray(formats.size) {
C.FORMAT_HANDLED
}
val selected = BooleanArray(formats.size) { index ->
index == selectedIndex
}
return Tracks.Group(trackGroup, false, trackSupport, selected)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,9 @@ fun Player.videoSizeAsState(): State<VideoSize> {
}

/**
* Get aspect ratio as state computed from [Player.getVideoSize]
* Get aspect ratio of the current video as [State].
*
* @param defaultAspectRatio The aspect ratio when video size is unknown or for audio content.
* @param defaultAspectRatio The aspect ratio when the video size is unknown, or for audio content.
*/
@Composable
fun Player.getAspectRatioAsState(defaultAspectRatio: Float): FloatState {
Expand Down
Loading

0 comments on commit c4c141c

Please sign in to comment.