Skip to content

Commit

Permalink
Feature/chromecast (#75)
Browse files Browse the repository at this point in the history
* Chromecast feature WIP

* Adapt casting for landscape mode

* Disable casting for HLS videos

---------

Co-authored-by: Volodymyr Chekyrta <[email protected]>
  • Loading branch information
PavloNetrebchuk and volodymyr-chekyrta authored Oct 26, 2023
1 parent 99085e0 commit 8a55fa0
Show file tree
Hide file tree
Showing 13 changed files with 305 additions and 82 deletions.
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
android:authorities="${applicationId}.firebaseinitprovider"
android:exported="false"
tools:node="remove" />

<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="androidx.media3.cast.DefaultCastOptionsProvider" />
</application>

</manifest>
4 changes: 3 additions & 1 deletion app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import org.openedx.course.presentation.outline.CourseOutlineViewModel
import org.openedx.discovery.presentation.search.CourseSearchViewModel
import org.openedx.course.presentation.section.CourseSectionViewModel
import org.openedx.course.presentation.unit.container.CourseUnitContainerViewModel
import org.openedx.course.presentation.unit.video.VideoUnitViewModel
import org.openedx.course.presentation.unit.video.VideoViewModel
import org.openedx.course.presentation.videos.CourseVideoViewModel
import org.openedx.dashboard.data.repository.DashboardRepository
Expand Down Expand Up @@ -46,6 +45,8 @@ import org.openedx.profile.presentation.settings.video.VideoSettingsViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.openedx.course.presentation.unit.video.EncodedVideoUnitViewModel
import org.openedx.course.presentation.unit.video.VideoUnitViewModel
import org.openedx.profile.presentation.anothers_account.AnothersProfileViewModel

val screenModule = module {
Expand Down Expand Up @@ -86,6 +87,7 @@ val screenModule = module {
viewModel { (courseId: String) -> CourseVideoViewModel(courseId, get(), get(), get(), get(), get(), get(), get()) }
viewModel { (courseId: String) -> VideoViewModel(courseId, 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, handoutsType: String) -> HandoutsViewModel(courseId, handoutsType, get()) }
viewModel { CourseSearchViewModel(get(), get(), get()) }
viewModel { SelectDialogViewModel(get()) }
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ ext {
fragment_version = "1.6.1"
constraintlayout_version = "2.1.4"
viewpager2_version = "1.0.0"
media3 = "1.1.1"
media3_version = "1.1.1"
youtubeplayer_version = "11.1.0"

firebase_version = "32.1.0"
Expand Down
26 changes: 21 additions & 5 deletions core/src/main/java/org/openedx/core/domain/model/Block.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ data class Block(
val descendants: List<String>,
val descendantsType: BlockType,
val completion: Double,
val downloadModel: DownloadModel? = null
val downloadModel: DownloadModel? = null,
) {
val isDownloadable: Boolean
get() {
Expand All @@ -36,6 +36,7 @@ data class Block(
BlockType.VIDEO -> {
FileType.VIDEO
}

else -> {
FileType.UNKNOWN
}
Expand All @@ -55,7 +56,7 @@ data class StudentViewData(
val duration: Any,
val transcripts: HashMap<String, String>?,
val encodedVideos: EncodedVideos?,
val topicId: String
val topicId: String,
)

data class EncodedVideos(
Expand All @@ -64,7 +65,7 @@ data class EncodedVideos(
var fallback: VideoInfo?,
var desktopMp4: VideoInfo?,
var mobileHigh: VideoInfo?,
var mobileLow: VideoInfo?
var mobileLow: VideoInfo?,
) {
val hasDownloadableVideo: Boolean
get() = isPreferredVideoInfo(hls) ||
Expand All @@ -73,6 +74,21 @@ data class EncodedVideos(
isPreferredVideoInfo(mobileHigh) ||
isPreferredVideoInfo(mobileLow)

val hasNonYoutubeVideo: Boolean
get() = mobileHigh?.url != null
|| mobileLow?.url != null
|| desktopMp4?.url != null
|| hls?.url != null
|| fallback?.url != null

val videoUrl: String
get() = mobileHigh?.url
?: mobileLow?.url
?: desktopMp4?.url
?: hls?.url
?: fallback?.url
?: ""

fun getPreferredVideoInfoForDownloading(preferredVideoQuality: VideoQuality): VideoInfo? {
var preferredVideoInfo = when (preferredVideoQuality) {
VideoQuality.OPTION_360P -> mobileLow
Expand Down Expand Up @@ -126,9 +142,9 @@ data class EncodedVideos(

data class VideoInfo(
val url: String,
val fileSize: Int
val fileSize: Int,
)

data class BlockCounts(
val video: Int
val video: Int,
)
5 changes: 3 additions & 2 deletions course/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ dependencies {
implementation project(path: ':core')
implementation project(path: ':discussion')
implementation "com.pierfrancescosoffritti.androidyoutubeplayer:core:$youtubeplayer_version"
implementation "androidx.media3:media3-exoplayer:$media3"
implementation "androidx.media3:media3-ui:$media3"
implementation "androidx.media3:media3-exoplayer:$media3_version"
implementation "androidx.media3:media3-ui:$media3_version"
implementation "androidx.media3:media3-cast:$media3_version"

androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import org.openedx.discussion.presentation.topics.DiscussionTopicsFragment

class CourseUnitContainerAdapter(
fragment: Fragment,
val blocks: List<Block>,
private val viewModel: CourseUnitContainerViewModel,
private var blocks: List<Block>
) : FragmentStateAdapter(fragment) {

override fun getItemCount(): Int = blocks.size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package org.openedx.course.presentation.unit.container
import android.content.res.Configuration
import android.os.Bundle
import android.os.SystemClock
import android.view.LayoutInflater
import android.view.View
import androidx.compose.foundation.layout.statusBarsPadding
import android.view.ViewGroup
import androidx.compose.foundation.layout.width
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.getValue
Expand All @@ -16,16 +18,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.viewpager2.widget.ViewPager2
import com.google.android.gms.cast.framework.CastButtonFactory
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.openedx.core.BlockType
import org.openedx.core.extension.serializable
import org.openedx.core.presentation.course.CourseViewMode
import org.openedx.core.presentation.global.InsetHolder
import org.openedx.core.presentation.global.viewBinding
import org.openedx.core.ui.BackBtn
import org.openedx.core.ui.rememberWindowSize
import org.openedx.core.ui.theme.OpenEdXTheme
Expand All @@ -41,7 +44,9 @@ import org.openedx.course.presentation.ui.VideoTitle

class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_container) {

private val binding by viewBinding(FragmentCourseUnitContainerBinding::bind)
private val binding: FragmentCourseUnitContainerBinding
get() = _binding!!
private var _binding: FragmentCourseUnitContainerBinding? = null

private val viewModel by viewModel<CourseUnitContainerViewModel> {
parametersOf(requireArguments().getString(ARG_COURSE_ID, ""))
Expand All @@ -55,6 +60,19 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta

private var lastClickTime = 0L

private val onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val blocks = viewModel.getUnitBlocks()
blocks.getOrNull(position)?.let { currentBlock ->
val encodedVideo = currentBlock.studentViewData?.encodedVideos
binding.mediaRouteButton.isVisible = currentBlock.type == BlockType.VIDEO
&& encodedVideo?.hasNonYoutubeVideo == true
&& !encodedVideo.videoUrl.endsWith(".m3u8")
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
Expand All @@ -63,6 +81,15 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta
viewModel.setupCurrentIndex(blockId)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentCourseUnitContainerBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

Expand All @@ -77,6 +104,9 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta
binding.cvCount.layoutParams = countParams
}

binding.mediaRouteButton.setAlwaysVisible(true)
CastButtonFactory.setUpMediaRouteButton(requireContext(), binding.mediaRouteButton)

initViewPager()
if (savedInstanceState == null) {
val currentBlockIndex = viewModel.getUnitBlocks().indexOfFirst {
Expand Down Expand Up @@ -166,7 +196,12 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta
}
}
}
}

override fun onDestroyView() {
binding.viewPager.unregisterOnPageChangeCallback(onPageChangeCallback)
super.onDestroyView()
_binding = null
}

private fun updateNavigationButtons(updatedData: (String, Boolean, Boolean) -> Unit) {
Expand All @@ -193,9 +228,10 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta
private fun initViewPager() {
binding.viewPager.orientation = ViewPager2.ORIENTATION_VERTICAL
binding.viewPager.offscreenPageLimit = 1
adapter = CourseUnitContainerAdapter(this, viewModel, viewModel.getUnitBlocks())
adapter = CourseUnitContainerAdapter(this, viewModel.getUnitBlocks(), viewModel)
binding.viewPager.adapter = adapter
binding.viewPager.isUserInputEnabled = false
binding.viewPager.registerOnPageChangeCallback(onPageChangeCallback)
}

private fun handlePrevClick(buttonChanged: (String, Boolean, Boolean) -> Unit) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.openedx.course.presentation.unit.video

import android.content.Context
import androidx.lifecycle.LifecycleOwner
import androidx.media3.cast.CastPlayer
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import com.google.android.gms.cast.framework.CastContext
import org.openedx.core.module.TranscriptManager
import org.openedx.core.system.connection.NetworkConnection
import org.openedx.core.system.notifier.CourseNotifier
import org.openedx.course.data.repository.CourseRepository
import java.util.concurrent.Executors

class EncodedVideoUnitViewModel(
courseId: String,
val blockId: String,
courseRepository: CourseRepository,
notifier: CourseNotifier,
networkConnection: NetworkConnection,
transcriptManager: TranscriptManager,
private val context: Context,
) : VideoUnitViewModel(
courseId,
courseRepository,
notifier,
networkConnection,
transcriptManager
) {

var exoPlayer: ExoPlayer? = null
private set
var castPlayer: CastPlayer? = null
private set
private var castContext: CastContext? = null

var isCastActive = false

var isPlayerSetUp = false

private val exoPlayerListener = object : Player.Listener {
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
isPlaying = playWhenReady
}

override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (playbackState == Player.STATE_ENDED) {
markBlockCompleted(blockId)
}
}
}

@androidx.media3.common.util.UnstableApi
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)

if (exoPlayer != null) {
return
}

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

val executor = Executors.newSingleThreadExecutor()
castContext = CastContext.getSharedInstance(context, executor).result
castContext?.let {
castPlayer = CastPlayer(it)
}
}

override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
exoPlayer?.addListener(exoPlayerListener)
}

override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
exoPlayer?.removeListener(exoPlayerListener)
exoPlayer?.pause()
}

fun getActivePlayer(): Player? {
return if (isCastActive) {
castPlayer
} else {
exoPlayer
}
}

@androidx.media3.common.util.UnstableApi
fun releasePlayers() {
exoPlayer?.release()
castPlayer?.release()
exoPlayer = null
castPlayer = null
}
}
Loading

0 comments on commit 8a55fa0

Please sign in to comment.