diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 28074a46a..8294dc8a6 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -270,10 +270,18 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di blockId: String, courseId: String, isPlaying: Boolean, + transcripts: Map, ) { replaceFragmentWithBackStack( fm, - VideoFullScreenFragment.newInstance(videoUrl, videoTime, blockId, courseId, isPlaying) + VideoFullScreenFragment.newInstance( + videoUrl = videoUrl, + videoTime = videoTime, + blockId = blockId, + courseId = courseId, + isPlaying = isPlaying, + transcripts = transcripts, + ) ) } diff --git a/app/src/main/java/org/openedx/app/di/AppModule.kt b/app/src/main/java/org/openedx/app/di/AppModule.kt index 4e7f087cb..e43179344 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -18,6 +18,7 @@ import org.openedx.app.data.storage.PreferencesManager import org.openedx.app.deeplink.DeepLinkRouter import org.openedx.app.room.AppDatabase import org.openedx.app.room.DATABASE_NAME +import org.openedx.app.room.Migrations import org.openedx.auth.presentation.AgreementProvider import org.openedx.auth.presentation.AuthAnalytics import org.openedx.auth.presentation.AuthRouter @@ -138,6 +139,7 @@ val appModule = module { DATABASE_NAME ).fallbackToDestructiveMigration() .fallbackToDestructiveMigrationOnDowngrade() + .addMigrations(Migrations.MIGRATION_1_2) .build() } diff --git a/app/src/main/java/org/openedx/app/room/AppDatabase.kt b/app/src/main/java/org/openedx/app/room/AppDatabase.kt index be320bae7..03990cf28 100644 --- a/app/src/main/java/org/openedx/app/room/AppDatabase.kt +++ b/app/src/main/java/org/openedx/app/room/AppDatabase.kt @@ -14,7 +14,7 @@ import org.openedx.discovery.data.converter.DiscoveryConverter import org.openedx.discovery.data.model.room.CourseEntity import org.openedx.discovery.data.storage.DiscoveryDao -const val DATABASE_VERSION = 1 +const val DATABASE_VERSION = 2 const val DATABASE_NAME = "OpenEdX_db" @Database( diff --git a/app/src/main/java/org/openedx/app/room/Migrations.kt b/app/src/main/java/org/openedx/app/room/Migrations.kt new file mode 100644 index 000000000..f9e882900 --- /dev/null +++ b/app/src/main/java/org/openedx/app/room/Migrations.kt @@ -0,0 +1,15 @@ +package org.openedx.app.room + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +object Migrations { + + val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE download_model ADD COLUMN transcriptUrls TEXT NOT NULL DEFAULT ''") + db.execSQL("ALTER TABLE download_model ADD COLUMN transcriptPaths TEXT NOT NULL DEFAULT ''") + db.execSQL("ALTER TABLE download_model ADD COLUMN transcriptDownloadedStatus TEXT NOT NULL DEFAULT 'NOT_DOWNLOADED'") + } + } +} diff --git a/build.gradle b/build.gradle index 8d7a22062..2b7439dad 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ ext { fragment_version = "1.6.2" constraintlayout_version = "2.1.4" viewpager2_version = "1.0.0" - media3_version = "1.1.1" + media3_version = "1.4.1" youtubeplayer_version = "11.1.0" firebase_version = "33.0.0" diff --git a/core/src/main/java/org/openedx/core/module/DownloadWorker.kt b/core/src/main/java/org/openedx/core/module/DownloadWorker.kt index 736a1b1ce..06fc32e26 100644 --- a/core/src/main/java/org/openedx/core/module/DownloadWorker.kt +++ b/core/src/main/java/org/openedx/core/module/DownloadWorker.kt @@ -11,14 +11,20 @@ import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.koin.java.KoinJavaComponent.inject import org.openedx.core.R +import org.openedx.core.extension.isNotNullOrEmpty import org.openedx.core.module.db.DownloadDao import org.openedx.core.module.db.DownloadModel import org.openedx.core.module.db.DownloadModelEntity import org.openedx.core.module.db.DownloadedState +import org.openedx.core.module.db.TranscriptsDownloadedState import org.openedx.core.module.download.CurrentProgress import org.openedx.core.module.download.FileDownloader import org.openedx.core.system.notifier.DownloadNotifier @@ -48,6 +54,7 @@ class DownloadWorker( private var lastUpdateTime = 0L private val fileDownloader by inject(FileDownloader::class.java) + private val transcriptManager by inject(TranscriptManager::class.java) override suspend fun doWork(): Result { updateProgress() @@ -131,25 +138,58 @@ class DownloadWorker( ) ) ) - val isSuccess = fileDownloader.download(downloadTask.url, downloadTask.path) - if (isSuccess) { + val isVideoDownloaded = fileDownloader.download(downloadTask.url, downloadTask.path) + val isTranscriptsDownloaded = if (isVideoDownloaded) downloadTranscripts(downloadTask) + else false + + if (isVideoDownloaded) { downloadDao.updateDownloadModel( DownloadModelEntity.createFrom( downloadTask.copy( downloadedState = DownloadedState.DOWNLOADED, - size = File(downloadTask.path).length().toInt() + size = File(downloadTask.path).length().toInt(), + transcriptDownloadedStatus = if (isTranscriptsDownloaded) { + TranscriptsDownloadedState.DOWNLOADED + } else { + TranscriptsDownloadedState.NOT_DOWNLOADED + } ) ) ) } else { downloadDao.removeDownloadModel(downloadTask.id) } + newDownload() } else { return } } + private suspend fun downloadTranscripts(downloadTask: DownloadModel): Boolean = coroutineScope { + if (downloadTask.transcriptUrls.isEmpty()) { + downloadDao.updateDownloadModel( + DownloadModelEntity.createFrom( + downloadTask.copy( + transcriptDownloadedStatus = TranscriptsDownloadedState.DOWNLOADED + ) + ) + ) + return@coroutineScope true + } + + val allDownloads = downloadTask.transcriptUrls.mapNotNull { (language, url) -> + val path = downloadTask.transcriptPaths[language] + if (path.isNotNullOrEmpty()) { + async(Dispatchers.IO) { transcriptManager.download(url, path!!) } + } else { + null + } + } + + return@coroutineScope allDownloads.awaitAll().all { it } + } + @RequiresApi(Build.VERSION_CODES.O) private fun createChannel() { val notificationChannel = diff --git a/core/src/main/java/org/openedx/core/module/DownloadWorkerController.kt b/core/src/main/java/org/openedx/core/module/DownloadWorkerController.kt index a4e83c07e..f7ada9d3d 100644 --- a/core/src/main/java/org/openedx/core/module/DownloadWorkerController.kt +++ b/core/src/main/java/org/openedx/core/module/DownloadWorkerController.kt @@ -79,6 +79,13 @@ class DownloadWorkerController( } catch (e: Exception) { e.printStackTrace() } + downloadModel.transcriptPaths.values.forEach { path -> + try { + File(path).delete() + } catch (e: Exception) { + e.printStackTrace() + } + } } if (hasDownloading) fileDownloader.cancelDownloading() diff --git a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt index eb98a042b..3c42c1f84 100644 --- a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt +++ b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt @@ -61,6 +61,10 @@ class TranscriptManager( } else FileInputStream(file) } + suspend fun download(url: String, path: String): Boolean { + return transcriptDownloader.download(url, path) + } + private suspend fun startTranscriptDownload(downloadLink: String) { if (!has(downloadLink)) { val file = File(getTranscriptDir(), Sha1Util.SHA1(downloadLink)) @@ -96,6 +100,15 @@ class TranscriptManager( return transcriptObject } + fun getDownloadedTranscript(encodedUrl: String): TimedTextObject? { + return runCatching { + val file = File(encodedUrl) + FileInputStream(file).use { transcriptInputStream -> + convertIntoTimedTextObject(transcriptInputStream) + } + }.getOrNull() + } + suspend fun cancelTranscriptDownloading() { transcriptDownloader.cancelDownloading() } diff --git a/core/src/main/java/org/openedx/core/module/db/DownloadModel.kt b/core/src/main/java/org/openedx/core/module/db/DownloadModel.kt index 86bc31540..67a99f734 100644 --- a/core/src/main/java/org/openedx/core/module/db/DownloadModel.kt +++ b/core/src/main/java/org/openedx/core/module/db/DownloadModel.kt @@ -8,7 +8,10 @@ data class DownloadModel( val url: String, val type: FileType, val downloadedState: DownloadedState, - val progress: Float? + val progress: Float?, + val transcriptUrls: Map, + val transcriptPaths: Map, + val transcriptDownloadedStatus: TranscriptsDownloadedState, ) enum class DownloadedState { @@ -25,6 +28,10 @@ enum class DownloadedState { } } +enum class TranscriptsDownloadedState { + DOWNLOADED, NOT_DOWNLOADED; +} + enum class FileType { VIDEO, UNKNOWN -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/module/db/DownloadModelEntity.kt b/core/src/main/java/org/openedx/core/module/db/DownloadModelEntity.kt index cd12a4eea..0bddf00ea 100644 --- a/core/src/main/java/org/openedx/core/module/db/DownloadModelEntity.kt +++ b/core/src/main/java/org/openedx/core/module/db/DownloadModelEntity.kt @@ -3,6 +3,8 @@ package org.openedx.core.module.db import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import org.openedx.core.extension.objectToString +import org.openedx.core.extension.stringToObject @Entity(tableName = "download_model") data class DownloadModelEntity( @@ -22,7 +24,13 @@ data class DownloadModelEntity( @ColumnInfo("downloadedState") val downloadedState: String, @ColumnInfo("progress") - val progress: Float? + val progress: Float?, + @ColumnInfo("transcriptUrls") + val transcriptUrls: String, + @ColumnInfo("transcriptPaths") + val transcriptPaths: String, + @ColumnInfo("transcriptDownloadedStatus") + val transcriptDownloadedStatus: String, ) { fun mapToDomain() = DownloadModel( @@ -33,7 +41,10 @@ data class DownloadModelEntity( url, FileType.valueOf(type), DownloadedState.valueOf(downloadedState), - progress + progress, + stringToObject>(transcriptUrls) ?: emptyMap(), + stringToObject>(transcriptPaths) ?: emptyMap(), + TranscriptsDownloadedState.valueOf(transcriptDownloadedStatus), ) companion object { @@ -48,11 +59,14 @@ data class DownloadModelEntity( url, type.name, downloadedState.name, - progress + progress, + objectToString(transcriptUrls), + objectToString(transcriptPaths), + transcriptDownloadedStatus.name ) } } } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt b/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt index 40cc94e4d..1b6741cac 100644 --- a/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt +++ b/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt @@ -14,9 +14,11 @@ import org.openedx.core.module.DownloadWorkerController import org.openedx.core.module.db.DownloadDao import org.openedx.core.module.db.DownloadModel import org.openedx.core.module.db.DownloadedState +import org.openedx.core.module.db.TranscriptsDownloadedState import org.openedx.core.presentation.CoreAnalytics import org.openedx.core.presentation.CoreAnalyticsEvent import org.openedx.core.presentation.CoreAnalyticsKey +import org.openedx.core.utils.Directories import org.openedx.core.utils.Sha1Util import java.io.File @@ -135,6 +137,8 @@ abstract class BaseDownloadViewModel( val extension = url.split('.').lastOrNull() ?: "mp4" val path = folder + File.separator + "${Sha1Util.SHA1(block.displayName)}.$extension" + val transcriptUrls = block.studentViewData?.transcripts?.toMap() ?: emptyMap() + val transcriptPaths = getTranscriptPaths(folder, transcriptUrls) if (downloadModelList.find { it.id == blockId && it.downloadedState.isDownloaded } == null) { downloadModels.add( DownloadModel( @@ -145,7 +149,10 @@ abstract class BaseDownloadViewModel( url, block.downloadableType, DownloadedState.WAITING, - null + null, + transcriptUrls, + transcriptPaths, + TranscriptsDownloadedState.NOT_DOWNLOADED, ) ) } @@ -249,6 +256,20 @@ abstract class BaseDownloadViewModel( ) } + private fun getTranscriptPaths( + folder: String, + transcripts: Map + ): Map { + val videosDir = File(folder, Directories.VIDEOS.name) + val transcriptDir = File(videosDir, Directories.SUBTITLES.name) + transcriptDir.mkdirs() + + return transcripts.mapValues { + val hash = Sha1Util.SHA1(it.value) + File(transcriptDir, hash).path + } + } + private fun logSubsectionDownloadEvent(subsectionId: String, numberOfVideos: Int) { logEvent( CoreAnalyticsEvent.VIDEO_DOWNLOAD_SUBSECTION, diff --git a/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt b/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt index 65ce5f012..faa9db53c 100644 --- a/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt +++ b/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt @@ -43,7 +43,8 @@ interface CourseRouter { videoTime: Long, blockId: String, courseId: String, - isPlaying: Boolean + isPlaying: Boolean, + transcripts: Map ) fun navigateToFullScreenYoutubeVideo( diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt index 636dc6417..8d0c5b733 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt @@ -86,6 +86,7 @@ import org.openedx.core.extension.toFileSize import org.openedx.core.module.db.DownloadModel import org.openedx.core.module.db.DownloadedState import org.openedx.core.module.db.FileType +import org.openedx.core.module.db.TranscriptsDownloadedState import org.openedx.core.ui.BackBtn import org.openedx.core.ui.IconText import org.openedx.core.ui.OpenEdXOutlinePrimaryButton @@ -1273,7 +1274,10 @@ private fun OfflineQueueCardPreview() { url = "", type = FileType.VIDEO, downloadedState = DownloadedState.DOWNLOADING, - progress = 0f + progress = 0f, + transcriptUrls = emptyMap(), + transcriptPaths = emptyMap(), + transcriptDownloadedStatus = TranscriptsDownloadedState.NOT_DOWNLOADED ), progressValue = 10, progressSize = 30, diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt index 6c7e7ca8c..3054a5f13 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt @@ -27,13 +27,14 @@ class CourseUnitContainerAdapter( (block.studentViewData?.encodedVideos?.hasVideoUrl == true || block.studentViewData?.encodedVideos?.hasYoutubeUrl == true)) -> { val encodedVideos = block.studentViewData?.encodedVideos!! - val transcripts = block.studentViewData!!.transcripts with(encodedVideos) { - var isDownloaded = false - val videoUrl = if (viewModel.getDownloadModelById(block.id) != null) { - isDownloaded = true - viewModel.getDownloadModelById(block.id)!!.path - } else videoUrl + val downloadModel = viewModel.getDownloadModelById(block.id) + val isDownloaded = downloadModel != null + + val videoUrl = downloadModel?.path ?: videoUrl + val transcripts = + downloadModel?.transcriptPaths ?: block.studentViewData?.transcripts + if (videoUrl.isNotEmpty()) { VideoUnitFragment.newInstance( block.id, 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 d459eee4e..0f05317c3 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 @@ -11,13 +11,12 @@ import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player +import androidx.media3.common.Tracks 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 @@ -27,7 +26,9 @@ 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.domain.model.VideoQuality +import org.openedx.core.extension.objectToString import org.openedx.core.extension.requestApplyInsetsWhenAttached +import org.openedx.core.extension.stringToObject import org.openedx.core.presentation.dialog.appreview.AppReviewManager import org.openedx.core.presentation.global.viewBinding import org.openedx.course.R @@ -71,6 +72,9 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { if (viewModel.isPlaying == null) { viewModel.isPlaying = requireArguments().getBoolean(ARG_IS_PLAYING) } + viewModel.transcripts = stringToObject>( + requireArguments().getString(ARG_TRANSCRIPTS, "") + ) ?: emptyMap() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -122,8 +126,12 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { playerView.player = exoPlayer playerView.setShowNextButton(false) playerView.setShowPreviousButton(false) - val mediaItem = MediaItem.fromUri(viewModel.videoUrl) - setPlayerMedia(mediaItem) + playerView.setShowSubtitleButton(true) + val mediaItem = MediaItem.Builder() + .setUri(viewModel.videoUrl) + .setSubtitleConfigurations(viewModel.subtitleConfigurations) + .build() + exoPlayer?.setMediaItem(mediaItem, viewModel.currentVideoTime) exoPlayer?.prepare() exoPlayer?.playWhenReady = viewModel.isPlaying ?: false @@ -149,6 +157,16 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { } } + override fun onTracksChanged(tracks: Tracks) { + super.onTracksChanged(tracks) + viewModel.selectedLanguage = tracks.groups + .firstOrNull { it.isSelected && it.type == C.TRACK_TYPE_TEXT } + ?.getTrackFormat(0) + ?.language ?: "" + + playerView.hideController() + } + override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { super.onPlaybackParametersChanged(playbackParameters) viewModel.logVideoSpeedEvent( @@ -162,21 +180,6 @@ 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() @@ -215,6 +218,7 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { private const val ARG_BLOCK_ID = "blockId" private const val ARG_COURSE_ID = "courseId" private const val ARG_IS_PLAYING = "isPlaying" + private const val ARG_TRANSCRIPTS = "transcripts" fun newInstance( videoUrl: String, @@ -222,6 +226,7 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { blockId: String, courseId: String, isPlaying: Boolean, + transcripts: Map, ): VideoFullScreenFragment { val fragment = VideoFullScreenFragment() fragment.arguments = bundleOf( @@ -229,7 +234,8 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { ARG_VIDEO_TIME to videoTime, ARG_BLOCK_ID to blockId, ARG_COURSE_ID to courseId, - ARG_IS_PLAYING to isPlaying + ARG_IS_PLAYING to isPlaying, + ARG_TRANSCRIPTS to objectToString(transcripts), ) return fragment } 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 559b0c44a..1b8f5ac0a 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 @@ -233,7 +233,8 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { viewModel.exoPlayer?.currentPosition ?: 0L, viewModel.blockId, viewModel.courseId, - viewModel.isPlaying + viewModel.isPlaying, + viewModel.transcripts, ) } } diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt index 4300e4304..3274e25f4 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt @@ -78,7 +78,14 @@ open class VideoUnitViewModel( fun downloadSubtitles() { viewModelScope.launch(Dispatchers.IO) { - transcriptManager.downloadTranscriptsForVideo(getTranscriptUrl())?.let { result -> + val transcriptUrl = getTranscriptUrl() + val timedTextObject = if (isDownloaded) { + transcriptManager.getDownloadedTranscript(transcriptUrl) + } else { + transcriptManager.downloadTranscriptsForVideo(transcriptUrl) + } + + timedTextObject?.let { result -> _transcriptObject.postValue(result) timeList = result.captions.values.toList() .map { it.start.mseconds.toLong() } 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 4ae600eb8..752773b8a 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 @@ -1,12 +1,16 @@ package org.openedx.course.presentation.unit.video +import android.net.Uri import androidx.lifecycle.viewModelScope import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.MimeTypes import kotlinx.coroutines.launch import org.openedx.core.data.storage.CorePreferences import org.openedx.core.system.notifier.CourseCompletionSet import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseVideoPositionChanged +import org.openedx.core.utils.LocaleUtils import org.openedx.course.data.repository.CourseRepository import org.openedx.course.presentation.CourseAnalytics @@ -24,6 +28,23 @@ class VideoViewModel( private var isBlockAlreadyCompleted = false + var transcripts = emptyMap() + var selectedLanguage: String = "" + val subtitleConfigurations: List + get() = transcripts + .toSortedMap( + compareBy { LocaleUtils.getLanguageByLanguageCode(it) } + ) + .map { (language, uri) -> + val selectionFlags = + if (language == selectedLanguage) C.SELECTION_FLAG_DEFAULT else 0 + + MediaItem.SubtitleConfiguration.Builder(Uri.parse(uri)) + .setMimeType(MimeTypes.APPLICATION_SUBRIP) + .setSelectionFlags(selectionFlags) + .setLanguage(language) + .build() + } fun sendTime() { if (currentVideoTime != C.TIME_UNSET) { diff --git a/course/src/main/java/org/openedx/course/settings/download/DownloadQueueFragment.kt b/course/src/main/java/org/openedx/course/settings/download/DownloadQueueFragment.kt index 1100966b1..d9bdee476 100644 --- a/course/src/main/java/org/openedx/course/settings/download/DownloadQueueFragment.kt +++ b/course/src/main/java/org/openedx/course/settings/download/DownloadQueueFragment.kt @@ -46,6 +46,7 @@ import org.koin.core.parameter.parametersOf import org.openedx.core.module.db.DownloadModel import org.openedx.core.module.db.DownloadedState import org.openedx.core.module.db.FileType +import org.openedx.core.module.db.TranscriptsDownloadedState import org.openedx.core.ui.BackBtn import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType @@ -230,7 +231,10 @@ private fun DownloadQueueScreenPreview() { url = "", type = FileType.VIDEO, downloadedState = DownloadedState.DOWNLOADING, - progress = 0f + progress = 0f, + transcriptUrls = emptyMap(), + transcriptPaths = emptyMap(), + transcriptDownloadedStatus = TranscriptsDownloadedState.NOT_DOWNLOADED, ), DownloadModel( id = "", @@ -240,7 +244,10 @@ private fun DownloadQueueScreenPreview() { url = "", type = FileType.VIDEO, downloadedState = DownloadedState.DOWNLOADING, - progress = 0f + progress = 0f, + transcriptUrls = emptyMap(), + transcriptPaths = emptyMap(), + transcriptDownloadedStatus = TranscriptsDownloadedState.NOT_DOWNLOADED, ) ), currentProgressId = "", diff --git a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt index 7b0d16ca0..7ea17761b 100644 --- a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt @@ -51,6 +51,7 @@ import org.openedx.core.module.db.DownloadModel import org.openedx.core.module.db.DownloadModelEntity import org.openedx.core.module.db.DownloadedState import org.openedx.core.module.db.FileType +import org.openedx.core.module.db.TranscriptsDownloadedState import org.openedx.core.presentation.CoreAnalytics import org.openedx.core.presentation.CoreAnalyticsEvent import org.openedx.core.system.ResourceManager @@ -223,7 +224,10 @@ class CourseOutlineViewModelTest { "url", FileType.VIDEO, DownloadedState.NOT_DOWNLOADED, - null + null, + emptyMap(), + emptyMap(), + TranscriptsDownloadedState.NOT_DOWNLOADED, ) @Before diff --git a/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt index f326fdb2e..54ffa5dec 100644 --- a/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt @@ -40,6 +40,7 @@ import org.openedx.core.module.db.DownloadModel import org.openedx.core.module.db.DownloadModelEntity import org.openedx.core.module.db.DownloadedState import org.openedx.core.module.db.FileType +import org.openedx.core.module.db.TranscriptsDownloadedState import org.openedx.core.presentation.CoreAnalytics import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.system.ResourceManager @@ -181,7 +182,10 @@ class CourseSectionViewModelTest { "url", FileType.VIDEO, DownloadedState.NOT_DOWNLOADED, - null + null, + emptyMap(), + emptyMap(), + TranscriptsDownloadedState.NOT_DOWNLOADED, ) @Before diff --git a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt index 325c51732..2f3f6971c 100644 --- a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt @@ -46,6 +46,7 @@ import org.openedx.core.module.db.DownloadModel import org.openedx.core.module.db.DownloadModelEntity import org.openedx.core.module.db.DownloadedState import org.openedx.core.module.db.FileType +import org.openedx.core.module.db.TranscriptsDownloadedState import org.openedx.core.presentation.CoreAnalytics import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection @@ -178,7 +179,7 @@ class CourseVideoViewModelTest { ) private val downloadModelEntity = - DownloadModelEntity("", "", 1, "", "", "VIDEO", "DOWNLOADED", null) + DownloadModelEntity("", "", 1, "", "", "VIDEO", "DOWNLOADED", null, "", "", "NOT_DOWNLOADED") private val downloadModel = DownloadModel( "id", @@ -188,7 +189,10 @@ class CourseVideoViewModelTest { "url", FileType.VIDEO, DownloadedState.NOT_DOWNLOADED, - null + null, + emptyMap(), + emptyMap(), + TranscriptsDownloadedState.NOT_DOWNLOADED, ) @Before