Skip to content

Commit

Permalink
HLS video quality (#76)
Browse files Browse the repository at this point in the history
* Chromecast feature WIP

* Fix after merge

* Adapt casting for landscape mode

* Disable casting for HLS videos

* Setting the HLS video quality value selected in the settings
  • Loading branch information
PavloNetrebchuk authored Nov 6, 2023
1 parent 2e7ead1 commit a6fd21c
Show file tree
Hide file tree
Showing 14 changed files with 135 additions and 33 deletions.
4 changes: 2 additions & 2 deletions app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ val screenModule = module {
viewModel { (courseId: String) -> CourseSectionViewModel(get(), get(), get(), get(), get(), get(), get(), get(), courseId) }
viewModel { (courseId: String) -> CourseUnitContainerViewModel(get(), get(), get(), courseId) }
viewModel { (courseId: String) -> CourseVideoViewModel(courseId, get(), get(), get(), get(), get(), get(), get()) }
viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get()) }
viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get(), get()) }
viewModel { (courseId: String) -> VideoUnitViewModel(courseId, get(), get(), get(), get()) }
viewModel { (courseId: String, blockId: String) -> EncodedVideoUnitViewModel(courseId, blockId, get(), get(), get(), get(), get()) }
viewModel { (courseId: String, blockId: String) -> EncodedVideoUnitViewModel(courseId, blockId, get(), get(), get(), get(), get(), get()) }
viewModel { (courseId:String, handoutsType: String) -> HandoutsViewModel(courseId, handoutsType, get()) }
viewModel { CourseSearchViewModel(get(), get(), get()) }
viewModel { SelectDialogViewModel(get()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ data class VideoSettings(
}
}

enum class VideoQuality(val titleResId: Int) {
AUTO(R.string.auto_recommended_text),
OPTION_360P(R.string.video_quality_p360),
OPTION_540P(R.string.video_quality_p540),
OPTION_720P(R.string.video_quality_p720);
enum class VideoQuality(val titleResId: Int, val width: Int, val height: Int) {
AUTO(R.string.auto_recommended_text, 0, 0),
OPTION_360P(R.string.video_quality_p360, 640, 360),
OPTION_540P(R.string.video_quality_p540, 960, 540),
OPTION_720P(R.string.video_quality_p720, 1280, 720);

val value: String = this.name.replace("OPTION_", "").lowercase()
}
2 changes: 1 addition & 1 deletion core/src/main/res/values-uk/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<string name="core_password">Пароль</string>
<string name="core_assessment_soon">незабаром</string>
<string name="auto_recommended_text">Авто (Рекомендовано)</string>
<string name="video_quality_p360">360p (Найменший розмір)</string>
<string name="video_quality_p360">360p (Менше використання трафіку)</string>
<string name="video_quality_p540">540p</string>
<string name="video_quality_p720">720p (Найкраща якість)</string>
<string name="core_offline">Офлайн</string>
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<string name="core_reload">Reload</string>
<string name="core_downloading_in_progress">Downloading in progress</string>
<string name="auto_recommended_text">Auto (Recommended)</string>
<string name="video_quality_p360">360p (Smallest file size)</string>
<string name="video_quality_p360">360p (Lower data usage)</string>
<string name="video_quality_p540">540p</string>
<string name="video_quality_p720">720p (Best quality)</string>
<string name="core_user_not_active">User account is not activated. Please activate your account first.</string>
Expand Down
1 change: 1 addition & 0 deletions course/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ dependencies {
implementation project(path: ':discussion')
implementation "com.pierfrancescosoffritti.androidyoutubeplayer:core:$youtubeplayer_version"
implementation "androidx.media3:media3-exoplayer:$media3_version"
implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
implementation "androidx.media3:media3-ui:$media3_version"
implementation "androidx.media3:media3-cast:$media3_version"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@ import android.content.Context
import androidx.lifecycle.LifecycleOwner
import androidx.media3.cast.CastPlayer
import androidx.media3.common.Player
import androidx.media3.common.util.Clock
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
import androidx.media3.extractor.DefaultExtractorsFactory
import com.google.android.gms.cast.framework.CastContext
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.VideoQuality
import org.openedx.core.module.TranscriptManager
import org.openedx.core.system.connection.NetworkConnection
import org.openedx.core.system.notifier.CourseNotifier
Expand All @@ -19,6 +30,7 @@ class EncodedVideoUnitViewModel(
notifier: CourseNotifier,
networkConnection: NetworkConnection,
transcriptManager: TranscriptManager,
val preferencesManager: CorePreferences,
private val context: Context,
) : VideoUnitViewModel(
courseId,
Expand Down Expand Up @@ -55,13 +67,10 @@ class EncodedVideoUnitViewModel(
@androidx.media3.common.util.UnstableApi
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)

if (exoPlayer != null) {
return
}

exoPlayer = ExoPlayer.Builder(context)
.build()
initPlayer()

val executor = Executors.newSingleThreadExecutor()
castContext = CastContext.getSharedInstance(context, executor).result
Expand All @@ -73,6 +82,7 @@ class EncodedVideoUnitViewModel(
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
exoPlayer?.addListener(exoPlayerListener)
getActivePlayer()?.playWhenReady = isPlaying
}

override fun onPause(owner: LifecycleOwner) {
Expand All @@ -96,4 +106,33 @@ class EncodedVideoUnitViewModel(
exoPlayer = null
castPlayer = null
}

@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun initPlayer() {
val videoQuality = getVideoQuality()
val params = DefaultTrackSelector.Parameters.Builder(context)
.apply {
if (videoQuality != VideoQuality.AUTO) {
setMaxVideoSize(videoQuality.width, videoQuality.height)
setViewportSize(videoQuality.width, videoQuality.height, false)
}
}
.build()

val factory = AdaptiveTrackSelection.Factory()
val selector = DefaultTrackSelector(context, factory)
selector.parameters = params

exoPlayer = ExoPlayer.Builder(
context,
DefaultRenderersFactory(context),
DefaultMediaSourceFactory(context, DefaultExtractorsFactory()),
selector,
DefaultLoadControl(),
DefaultBandwidthMeter.getSingletonInstance(context),
DefaultAnalyticsCollector(Clock.DEFAULT)
).build()
}

private fun getVideoQuality() = preferencesManager.videoSettings.videoQuality
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,21 @@ import androidx.fragment.app.Fragment
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.Clock
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector
import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
import androidx.media3.extractor.DefaultExtractorsFactory
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.openedx.core.domain.model.VideoQuality
import org.openedx.core.extension.requestApplyInsetsWhenAttached
import org.openedx.core.presentation.global.viewBinding
import org.openedx.course.R
Expand All @@ -32,6 +44,7 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
viewModel.isPlaying = playWhenReady
}

override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (playbackState == Player.STATE_ENDED) {
Expand Down Expand Up @@ -74,14 +87,35 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) {
private fun initPlayer() {
with(binding) {
if (exoPlayer == null) {
exoPlayer = ExoPlayer.Builder(requireContext())
val videoQuality = viewModel.getVideoQuality()
val params = DefaultTrackSelector.Parameters.Builder(requireContext())
.apply {
if (videoQuality != VideoQuality.AUTO) {
setMaxVideoSize(videoQuality.width, videoQuality.height)
setViewportSize(videoQuality.width, videoQuality.height, false)
}
}
.build()

val factory = AdaptiveTrackSelection.Factory()
val selector = DefaultTrackSelector(requireContext(), factory)
selector.parameters = params

exoPlayer = ExoPlayer.Builder(
requireContext(),
DefaultRenderersFactory(requireContext()),
DefaultMediaSourceFactory(requireContext(), DefaultExtractorsFactory()),
selector,
DefaultLoadControl(),
DefaultBandwidthMeter.getSingletonInstance(requireContext()),
DefaultAnalyticsCollector(Clock.DEFAULT)
).build()
}
playerView.player = exoPlayer
playerView.setShowNextButton(false)
playerView.setShowPreviousButton(false)
val mediaItem = MediaItem.fromUri(viewModel.videoUrl)
exoPlayer?.setMediaItem(mediaItem, viewModel.currentVideoTime)
setPlayerMedia(mediaItem)
exoPlayer?.prepare()
exoPlayer?.playWhenReady = viewModel.isPlaying ?: false

Expand All @@ -100,6 +134,20 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) {
}
}

@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
private fun setPlayerMedia(mediaItem: MediaItem) {
if (viewModel.videoUrl.endsWith(".m3u8")) {
val factory = DefaultDataSource.Factory(requireContext())
val mediaSource: HlsMediaSource = HlsMediaSource.Factory(factory).createMediaSource(mediaItem)
exoPlayer?.setMediaSource(mediaSource, viewModel.currentVideoTime)
} else {
exoPlayer?.setMediaItem(
mediaItem,
viewModel.currentVideoTime
)
}
}

private fun releasePlayer() {
exoPlayer?.stop()
exoPlayer?.release()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.window.layout.WindowMetricsCalculator
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
Expand Down Expand Up @@ -187,13 +189,9 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) {
.build()

if (!viewModel.isPlayerSetUp) {
viewModel.getActivePlayer()?.setMediaItem(
mediaItem,
viewModel.getCurrentVideoTime()
)
setPlayerMedia(mediaItem)
viewModel.getActivePlayer()?.prepare()
viewModel.getActivePlayer()?.playWhenReady = viewModel.isPlaying

viewModel.isPlayerSetUp = true
}

Expand Down Expand Up @@ -239,14 +237,11 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) {
}

@UnstableApi
override fun onDestroyView() {
super.onDestroyView()
override fun onDestroy() {
if (!requireActivity().isChangingConfigurations) {
viewModel.releasePlayers()
viewModel.isPlayerSetUp = false
}
}

override fun onDestroy() {
handler.removeCallbacks(videoTimeRunnable)
super.onDestroy()
}
Expand All @@ -263,6 +258,20 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) {
}
}

@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
private fun setPlayerMedia(mediaItem: MediaItem) {
if (viewModel.videoUrl.endsWith(".m3u8")) {
val factory = DefaultDataSource.Factory(requireContext())
val mediaSource: HlsMediaSource = HlsMediaSource.Factory(factory).createMediaSource(mediaItem)
viewModel.exoPlayer?.setMediaSource(mediaSource, viewModel.getCurrentVideoTime())
} else {
viewModel.getActivePlayer()?.setMediaItem(
mediaItem,
viewModel.getCurrentVideoTime()
)
}
}

companion object {
private const val ARG_BLOCK_ID = "blockId"
private const val ARG_VIDEO_URL = "videoUrl"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import org.openedx.course.data.repository.CourseRepository
import org.openedx.core.system.notifier.CourseNotifier
import org.openedx.core.system.notifier.CourseVideoPositionChanged
import kotlinx.coroutines.launch
import org.openedx.core.data.storage.CorePreferences

class VideoViewModel(
private val courseId: String,
private val courseRepository: CourseRepository,
private val notifier: CourseNotifier
private val notifier: CourseNotifier,
private val preferencesManager: CorePreferences
) : BaseViewModel() {

var videoUrl = ""
Expand Down Expand Up @@ -45,4 +47,5 @@ class VideoViewModel(
}
}

fun getVideoQuality() = preferencesManager.videoSettings.videoQuality
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.openedx.core.data.storage.CorePreferences

@OptIn(ExperimentalCoroutinesApi::class)
class VideoViewModelTest {
Expand All @@ -26,6 +27,7 @@ class VideoViewModelTest {

private val courseRepository = mockk<CourseRepository>()
private val notifier = mockk<CourseNotifier>()
private val preferenceManager = mockk<CorePreferences>()

@Before
fun setUp() {
Expand All @@ -39,7 +41,7 @@ class VideoViewModelTest {

@Test
fun `sendTime test`() = runTest {
val viewModel = VideoViewModel("", courseRepository, notifier)
val viewModel = VideoViewModel("", courseRepository, notifier, preferenceManager)
coEvery { notifier.send(CourseVideoPositionChanged("", 0, false)) } returns Unit
viewModel.sendTime()
advanceUntilIdle()
Expand All @@ -49,7 +51,7 @@ class VideoViewModelTest {

@Test
fun `markBlockCompleted exception`() = runTest {
val viewModel = VideoViewModel("", courseRepository, notifier)
val viewModel = VideoViewModel("", courseRepository, notifier, preferenceManager)
coEvery {
courseRepository.markBlocksCompletion(
any(),
Expand All @@ -69,7 +71,7 @@ class VideoViewModelTest {

@Test
fun `markBlockCompleted success`() = runTest {
val viewModel = VideoViewModel("", courseRepository, notifier)
val viewModel = VideoViewModel("", courseRepository, notifier, preferenceManager)
coEvery {
courseRepository.markBlocksCompletion(
any(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ private fun VideoQualityScreen(
Text(
modifier = Modifier
.fillMaxWidth(),
text = stringResource(id = profileR.string.profile_video_download_quality),
text = stringResource(id = profileR.string.profile_video_streaming_quality),
color = MaterialTheme.appColors.textPrimary,
textAlign = TextAlign.Center,
style = MaterialTheme.appTypography.titleMedium
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ private fun VideoSettingsScreen(
) {
Column(Modifier.weight(1f)) {
Text(
text = stringResource(id = profileR.string.profile_video_download_quality),
text = stringResource(id = profileR.string.profile_video_streaming_quality),
color = MaterialTheme.appColors.textPrimary,
style = MaterialTheme.appTypography.titleMedium
)
Expand Down
2 changes: 1 addition & 1 deletion profile/src/main/res/values-uk/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<string name="profile_video_settings">Налаштування відео</string>
<string name="profile_wifi_only_download">Завантаження тільки через Wi-Fi</string>
<string name="profile_only_download_when_wifi_turned_on">Завантажуйте вміст лише тоді, коли ввімкнено wi-fi</string>
<string name="profile_video_download_quality">Якість завантаження відео</string>
<string name="profile_video_streaming_quality">Якість транслювання відео</string>
<string name="profile_delete_account">Видалити акаунт</string>
<string name="profile_you_want_to">Ви впевнені, що бажаєте</string>
<string name="profile_delete_your_account">видалити свій акаунт?</string>
Expand Down
2 changes: 1 addition & 1 deletion profile/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<string name="profile_video_settings">Video settings</string>
<string name="profile_wifi_only_download">Wi-fi only download</string>
<string name="profile_only_download_when_wifi_turned_on">Only download content when wi-fi is turned on</string>
<string name="profile_video_download_quality">Video download quality</string>
<string name="profile_video_streaming_quality">Video streaming quality</string>
<string name="profile_leave_profile">Leave profile?</string>
<string name="profile_leave">Leave</string>
<string name="profile_keep_editing">Keep editing</string>
Expand Down

0 comments on commit a6fd21c

Please sign in to comment.