Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HLS video quality #76

Merged
merged 11 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from 10 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
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 @@ -86,9 +86,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 @@ -17,15 +17,13 @@ import androidx.compose.ui.Modifier
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.window.layout.WindowMetricsCalculator
import com.google.android.gms.cast.framework.CastContext
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
Expand All @@ -45,7 +43,6 @@ import org.openedx.course.presentation.CourseRouter
import org.openedx.course.presentation.ui.ConnectionErrorView
import org.openedx.course.presentation.ui.VideoSubtitles
import org.openedx.course.presentation.ui.VideoTitle
import java.util.concurrent.Executors
import kotlin.math.roundToInt

class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) {
Expand Down Expand Up @@ -192,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 @@ -244,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 @@ -268,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
Loading