Skip to content

Commit

Permalink
feat: Add Subtitle Support for Native Player in Fullscreen Mode (#64)
Browse files Browse the repository at this point in the history
- Update Media3 library to version 1.4.1
- Remove custom HLSMediaSource in favor of DefaultMediaSource
- Add the CC Button in the ExoPlayer view
- Pass transcripts to VideoFullScreenFragment
- Configure MediaItem with subtitles
- Retain subtitle on configuration change
- Sort the subtitle listing in the ExoPlayer
- Update the video DownloadModel to include transcripts
- Download transcripts along with videos for offline mode
- Make the feature compatible with external subtitles
- Remove transcripts on removing video

Fixes: LEARNER-10259
  • Loading branch information
HamzaIsrar12 authored Nov 19, 2024
1 parent 2e3be03 commit 8779f40
Show file tree
Hide file tree
Showing 22 changed files with 236 additions and 49 deletions.
10 changes: 9 additions & 1 deletion app/src/main/java/org/openedx/app/AppRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,18 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di
blockId: String,
courseId: String,
isPlaying: Boolean,
transcripts: Map<String, String>,
) {
replaceFragmentWithBackStack(
fm,
VideoFullScreenFragment.newInstance(videoUrl, videoTime, blockId, courseId, isPlaying)
VideoFullScreenFragment.newInstance(
videoUrl = videoUrl,
videoTime = videoTime,
blockId = blockId,
courseId = courseId,
isPlaying = isPlaying,
transcripts = transcripts,
)
)
}

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/org/openedx/app/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -138,6 +139,7 @@ val appModule = module {
DATABASE_NAME
).fallbackToDestructiveMigration()
.fallbackToDestructiveMigrationOnDowngrade()
.addMigrations(Migrations.MIGRATION_1_2)
.build()
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/org/openedx/app/room/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/org/openedx/app/room/Migrations.kt
Original file line number Diff line number Diff line change
@@ -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'")
}
}
}
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
46 changes: 43 additions & 3 deletions core/src/main/java/org/openedx/core/module/DownloadWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -48,6 +54,7 @@ class DownloadWorker(
private var lastUpdateTime = 0L

private val fileDownloader by inject<FileDownloader>(FileDownloader::class.java)
private val transcriptManager by inject<TranscriptManager>(TranscriptManager::class.java)

override suspend fun doWork(): Result {
updateProgress()
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
13 changes: 13 additions & 0 deletions core/src/main/java/org/openedx/core/module/TranscriptManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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()
}
Expand Down
11 changes: 9 additions & 2 deletions core/src/main/java/org/openedx/core/module/db/DownloadModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>,
val transcriptPaths: Map<String, String>,
val transcriptDownloadedStatus: TranscriptsDownloadedState,
)

enum class DownloadedState {
Expand All @@ -25,6 +28,10 @@ enum class DownloadedState {
}
}

enum class TranscriptsDownloadedState {
DOWNLOADED, NOT_DOWNLOADED;
}

enum class FileType {
VIDEO, UNKNOWN
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -33,7 +41,10 @@ data class DownloadModelEntity(
url,
FileType.valueOf(type),
DownloadedState.valueOf(downloadedState),
progress
progress,
stringToObject<Map<String, String>>(transcriptUrls) ?: emptyMap(),
stringToObject<Map<String, String>>(transcriptPaths) ?: emptyMap(),
TranscriptsDownloadedState.valueOf(transcriptDownloadedStatus),
)

companion object {
Expand All @@ -48,11 +59,14 @@ data class DownloadModelEntity(
url,
type.name,
downloadedState.name,
progress
progress,
objectToString(transcriptUrls),
objectToString(transcriptPaths),
transcriptDownloadedStatus.name
)
}
}

}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand All @@ -145,7 +149,10 @@ abstract class BaseDownloadViewModel(
url,
block.downloadableType,
DownloadedState.WAITING,
null
null,
transcriptUrls,
transcriptPaths,
TranscriptsDownloadedState.NOT_DOWNLOADED,
)
)
}
Expand Down Expand Up @@ -249,6 +256,20 @@ abstract class BaseDownloadViewModel(
)
}

private fun getTranscriptPaths(
folder: String,
transcripts: Map<String, String>
): Map<String, String> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ interface CourseRouter {
videoTime: Long,
blockId: String,
courseId: String,
isPlaying: Boolean
isPlaying: Boolean,
transcripts: Map<String, String>
)

fun navigateToFullScreenYoutubeVideo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 8779f40

Please sign in to comment.