diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt
index f61cf393f..979b9589c 100644
--- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt
+++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt
@@ -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()) }
diff --git a/core/src/main/java/org/openedx/core/domain/model/VideoSettings.kt b/core/src/main/java/org/openedx/core/domain/model/VideoSettings.kt
index 9e2b84ddb..07241824b 100644
--- a/core/src/main/java/org/openedx/core/domain/model/VideoSettings.kt
+++ b/core/src/main/java/org/openedx/core/domain/model/VideoSettings.kt
@@ -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()
}
diff --git a/core/src/main/res/values-uk/strings.xml b/core/src/main/res/values-uk/strings.xml
index 581b19366..d5623089b 100644
--- a/core/src/main/res/values-uk/strings.xml
+++ b/core/src/main/res/values-uk/strings.xml
@@ -21,7 +21,7 @@
Пароль
незабаром
Авто (Рекомендовано)
- 360p (Найменший розмір)
+ 360p (Менше використання трафіку)
540p
720p (Найкраща якість)
Офлайн
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index 975d96869..f2584d60a 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -27,7 +27,7 @@
Reload
Downloading in progress
Auto (Recommended)
- 360p (Smallest file size)
+ 360p (Lower data usage)
540p
720p (Best quality)
User account is not activated. Please activate your account first.
diff --git a/course/build.gradle b/course/build.gradle
index d7b0f2cab..fdf14b83d 100644
--- a/course/build.gradle
+++ b/course/build.gradle
@@ -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"
diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt
index dbb6b385e..5f613bc83 100644
--- a/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt
+++ b/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt
@@ -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
@@ -19,6 +30,7 @@ class EncodedVideoUnitViewModel(
notifier: CourseNotifier,
networkConnection: NetworkConnection,
transcriptManager: TranscriptManager,
+ val preferencesManager: CorePreferences,
private val context: Context,
) : VideoUnitViewModel(
courseId,
@@ -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
@@ -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) {
@@ -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
}
\ No newline at end of file
diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt
index 20ef5629c..321f8ad8f 100644
--- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt
+++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt
@@ -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
@@ -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) {
@@ -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
@@ -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()
diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt
index 8077c537e..4218bc7e4 100644
--- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt
+++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt
@@ -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
@@ -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
}
@@ -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()
}
@@ -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"
diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt
index 79f4e8acc..bfcda93c8 100644
--- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt
+++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt
@@ -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 = ""
@@ -45,4 +47,5 @@ class VideoViewModel(
}
}
+ fun getVideoQuality() = preferencesManager.videoSettings.videoQuality
}
\ No newline at end of file
diff --git a/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt
index 144698ec5..ce1799432 100644
--- a/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt
+++ b/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt
@@ -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 {
@@ -26,6 +27,7 @@ class VideoViewModelTest {
private val courseRepository = mockk()
private val notifier = mockk()
+ private val preferenceManager = mockk()
@Before
fun setUp() {
@@ -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()
@@ -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(),
@@ -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(),
diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoQualityFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoQualityFragment.kt
index 7acb2a9c4..46c645a76 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoQualityFragment.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoQualityFragment.kt
@@ -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
diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsFragment.kt
index ca86d48ec..747df792a 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsFragment.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsFragment.kt
@@ -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
)
diff --git a/profile/src/main/res/values-uk/strings.xml b/profile/src/main/res/values-uk/strings.xml
index 123d4aee6..1cbb0a60a 100644
--- a/profile/src/main/res/values-uk/strings.xml
+++ b/profile/src/main/res/values-uk/strings.xml
@@ -30,7 +30,7 @@
Налаштування відео
Завантаження тільки через Wi-Fi
Завантажуйте вміст лише тоді, коли ввімкнено wi-fi
- Якість завантаження відео
+ Якість транслювання відео
Видалити акаунт
Ви впевнені, що бажаєте
видалити свій акаунт?
diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml
index 9ad0c47c9..03f82fa8a 100644
--- a/profile/src/main/res/values/strings.xml
+++ b/profile/src/main/res/values/strings.xml
@@ -38,7 +38,7 @@
Video settings
Wi-fi only download
Only download content when wi-fi is turned on
- Video download quality
+ Video streaming quality
Leave profile?
Leave
Keep editing