Skip to content

Conversation

@tuancoltech
Copy link
Member

@tuancoltech tuancoltech commented Apr 25, 2025

Issue Number

Purpose

Implement subtitle parser and display it in VideoActivity view

Regression Tests

  • I tested my changes on Android 16 (API 36)
  • I tested my changes on Android 15 (API 35)
  • I tested my changes on Android 14 (API 34)
  • I tested my changes on Android 13 (API 33)
  • I tested my changes on Android 12L (API 32)
  • I tested my changes on Android 12 (API 31)
  • I tested my changes on Android 11 (API 30)
  • I tested my changes on Android 10 (API 29)
  • I tested my changes on Android 9 (API 28)
  • I tested my changes on Android 8.1 (API 27)
  • I tested my changes on Android 8.0 (API 26)

UI Tests

  • I tested my changes on a 6-7" screen (▯ portrait orientation)
  • I tested my changes on a 6-7" screen (▭ landscape orientation)
  • I tested my changes on a 7-8" screen (▯ portrait orientation)
  • I tested my changes on a 7-8" screen (▭ landscape orientation)
  • I tested my changes on a 9-10" screen (▯ portrait orientation)
  • I tested my changes on a 9-10" screen (▭ landscape orientation)

Content Tests

  • I tested my changes with English content (eng.elimu.ai)
  • I tested my changes with Hindi content (hin.elimu.ai)
  • I tested my changes with Tagalog content (tgl.elimu.ai)
  • I tested my changes with Thai content (tha.elimu.ai)
  • I tested my changes with Vietnamese content (vie.elimu.ai)

Summary by CodeRabbit

  • New Features

    • Added subtitle support to video playback, displaying synchronized subtitles at the bottom of the video player.
    • Users can now view captions in real-time while watching videos.
  • User Interface

    • Introduced a new on-screen area for subtitles, styled for readability and positioned at the bottom of the video player.

@tuancoltech tuancoltech requested a review from a team as a code owner April 25, 2025 07:17
@tuancoltech tuancoltech marked this pull request as draft April 25, 2025 07:17
@coderabbitai
Copy link

coderabbitai bot commented Apr 25, 2025

Walkthrough

This update introduces synchronized subtitle support for video playback within the application, aligning with elimu.ai's mission to build innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months. A new data class for subtitles is added, and utility functions are provided to load and parse SRT files. The video player activity and view model are extended to manage subtitle loading, synchronization, and display. The user interface is updated to include a dedicated TextView for showing subtitles at the bottom of the video screen.

Changes

File(s) Change Summary
app/src/main/java/ai/elimu/filamu/ui/video/VideoActivity.kt Added coroutine-based subtitle synchronization, subtitle loading, and lifecycle management in video activity.
app/src/main/java/ai/elimu/filamu/ui/video/model/Subtitle.kt Introduced new immutable Subtitle data class for subtitle representation.
app/src/main/java/ai/elimu/filamu/util/SubtitleUtil.kt Added utility object to load and parse SRT subtitle files asynchronously from assets.
app/src/main/res/layout/activity_video.xml Added a new TextView for displaying subtitles at the bottom of the video player UI.
data-video/src/main/java/ai/elimu/filamu/data/video/viewmodel/VideoViewModel.kt Extended interface to include subtitle state, loading, and retrieval methods.
data-video/src/main/java/ai/elimu/filamu/data/video/viewmodel/VideoViewModelImpl.kt Implemented subtitle state management, loading, and retrieval logic in the view model implementation.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant VideoActivity
    participant VideoViewModel
    participant SubtitleUtil
    participant SubtitleTextView

    User->>VideoActivity: Start video playback
    VideoActivity->>VideoViewModel: loadSubtitles("sample.srt")
    VideoViewModel->>SubtitleUtil: loadSubtitles(assetManager, "sample.srt")
    SubtitleUtil-->>VideoViewModel: List<Subtitle>
    VideoViewModel-->>VideoActivity: subtitles StateFlow update
    VideoActivity->>VideoActivity: synchronizeSubtitles()
    loop every 500ms
        VideoActivity->>VideoViewModel: getCurrentSubtitle(currentPosition)
        VideoViewModel-->>VideoActivity: Subtitle?
        VideoActivity->>SubtitleTextView: Update text if subtitle changed
    end
Loading

Assessment against linked issues

Objective Addressed Explanation
Display subtitles from SRT file at the bottom of the video (#5)
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🔭 Outside diff range comments (1)
data-video/src/main/java/ai/elimu/filamu/data/video/viewmodel/VideoViewModelImpl.kt (1)

1-94: 💡 Verification agent

🧩 Analysis chain

Documentation needed for the Subtitle model class

The code references a Subtitle model class that's imported but not documented. Adding documentation for this class would improve code maintainability and help others understand the subtitle implementation.

Let's check if the Subtitle class has proper documentation:


🏁 Script executed:

#!/bin/bash
# Find and display the Subtitle model class
echo "Looking for Subtitle model class definition:"
find . -name "*.kt" -exec grep -l "class Subtitle" {} \; | xargs cat

Length of output: 290


Add KDoc to the Subtitle data class

To keep the code clear and maintainable (and in line with elimu.ai’s mission to build innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months), please add documentation to the Subtitle model.

File to update:

  • data-video/src/main/java/ai/elimu/filamu/ui/video/model/Subtitle.kt

Suggested KDoc:

/**
 * Represents a subtitle segment displayed during video playback.
 *
 * @property startTime timestamp in milliseconds when the subtitle appears
 * @property endTime   timestamp in milliseconds when the subtitle disappears
 * @property text      the subtitle text content
 */
data class Subtitle(
    val startTime: Long,
    val endTime: Long,
    val text: String
)
🧹 Nitpick comments (6)
app/src/main/res/layout/activity_video.xml (1)

17-29: Consider enhancing subtitle visibility and accessibility

The subtitle TextView implementation is a great step towards enhancing content accessibility, aligning with elimu.ai's mission to build innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months. However, a few improvements could make this feature more effective:

  1. Consider using a semi-transparent background (e.g., #80000000) instead of solid black to maintain visibility of video content behind the subtitles
  2. Add content description for accessibility support
  3. Consider supporting larger text sizes for younger viewers or children with visual impairments
  4. Add elevation to ensure the TextView stays on top of other elements
    <TextView
        android:id="@+id/subtitle_text_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
-       android:background="@android:color/black"
+       android:background="#80000000"
        android:textColor="@android:color/white"
        android:padding="8dp"
        android:textSize="16sp"
        android:gravity="center"
+       android:contentDescription="@string/subtitle_content_description"
+       android:elevation="2dp"
+       android:lineSpacingExtra="4dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
app/src/main/java/ai/elimu/filamu/ui/video/model/Subtitle.kt (1)

1-3: Add documentation and consider additional helper methods

This Subtitle data class provides a solid foundation for implementing the subtitle feature, supporting elimu.ai's mission to build innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months. However, adding documentation and helper methods would improve maintainability.

package ai.elimu.filamu.ui.video.model

+/**
+ * Represents a single subtitle entry with start and end time boundaries.
+ * All time values are in milliseconds since the start of the video.
+ *
+ * @property startTime The time (in ms) when this subtitle should start displaying
+ * @property endTime The time (in ms) when this subtitle should stop displaying
+ * @property text The actual subtitle content to be displayed
+ */
-data class Subtitle(val startTime: Long, val endTime: Long, val text: String)
+data class Subtitle(val startTime: Long, val endTime: Long, val text: String) {
+    /**
+     * Checks if the given playback position falls within this subtitle's time range
+     *
+     * @param position Current playback position in milliseconds
+     * @return True if this subtitle should be displayed at the given position
+     */
+    fun isVisibleAt(position: Long): Boolean = position in startTime..endTime
+}
data-video/src/main/java/ai/elimu/filamu/data/video/viewmodel/VideoViewModel.kt (2)

29-29: Import the Subtitle class to avoid using fully qualified name

Using the fully qualified name makes the code more verbose and harder to maintain. Importing the class would improve readability.

package ai.elimu.filamu.data.video.viewmodel

import ai.elimu.model.v2.gson.content.VideoGson
+import ai.elimu.filamu.ui.video.model.Subtitle
import android.graphics.Bitmap
import kotlinx.coroutines.flow.StateFlow

...

interface VideoViewModel {
    val uiState: StateFlow<LoadVideosUiState>
-    val subtitles: StateFlow<List<ai.elimu.filamu.ui.video.model.Subtitle>>
+    val subtitles: StateFlow<List<Subtitle>>

34-35: Improve error handling and documentation for subtitle methods

Adding proper documentation and error handling for the subtitle-related functions would improve usability and maintainability, supporting elimu.ai's mission to build innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months.

-    fun loadSubtitles(fileName: String)
-    fun getCurrentSubtitle(currentPosition: Long): ai.elimu.filamu.ui.video.model.Subtitle?
+    /**
+     * Load subtitles from the specified file in the assets directory.
+     * The result will be emitted through the [subtitles] StateFlow.
+     *
+     * @param fileName The name of the subtitle file in the assets directory 
+     * @return Boolean indicating if the loading was initiated successfully
+     */
+    fun loadSubtitles(fileName: String): Boolean
+    
+    /**
+     * Get the subtitle that should be displayed at the current playback position.
+     *
+     * @param currentPosition Current video playback position in milliseconds
+     * @return The subtitle to display, or null if no subtitle should be shown
+     */  
+    fun getCurrentSubtitle(currentPosition: Long): Subtitle?
app/src/main/java/ai/elimu/filamu/ui/video/VideoActivity.kt (1)

121-131: Subtitle synchronization polling interval could be optimized

The current implementation uses a polling approach with a fixed 500ms interval. While functional, this approach may not be optimal for performance or battery life on mobile devices.

Consider using a more adaptive approach:

private fun synchronizeSubtitles() {
    synchronizationJob?.cancel()
    synchronizationJob = lifecycleScope.launch {
        while (true) {
            val currentPosition = videoPlayer.currentPosition
            val subtitle = videoViewModel.getCurrentSubtitle(currentPosition)
-           binding.subtitleTextView.text = subtitle?.text ?: ""
-           delay(500) // Check subtitle updates every 500ms
+           val currentText = subtitle?.text ?: ""
+           if (binding.subtitleTextView.text != currentText) {
+               binding.subtitleTextView.text = currentText
+           }
+           
+           // Calculate optimal delay based on next subtitle timing
+           val nextSubtitle = videoViewModel.getNextSubtitle(currentPosition)
+           val delayTime = if (nextSubtitle != null) {
+               // Wait until 100ms before next subtitle or at most 1000ms
+               minOf(nextSubtitle.startTime - currentPosition - 100, 1000L).coerceAtLeast(100)
+           } else {
+               500 // Default delay if no next subtitle
+           }
+           delay(delayTime)
        }
    }
}

This would require adding a getNextSubtitle method to the ViewModel.

data-video/src/main/java/ai/elimu/filamu/data/video/viewmodel/VideoViewModelImpl.kt (1)

89-93: Optimize subtitle search for better performance

The current implementation searches the entire list of subtitles on each call, which could be inefficient for large subtitle files and impact the smooth learning experience that elimu.ai aims to provide.

Consider optimizing by tracking the last matched subtitle position or using binary search:

+ private var lastSubtitleIndex = 0

override fun getCurrentSubtitle(currentPosition: Long): Subtitle? {
+   val subtitles = _subtitles.value
+   
+   // Quick check if we're still in the same subtitle
+   if (lastSubtitleIndex < subtitles.size) {
+       val current = subtitles[lastSubtitleIndex]
+       if (current.startTime <= currentPosition && current.endTime >= currentPosition) {
+           return current
+       }
+       
+       // Check next subtitle
+       if (lastSubtitleIndex + 1 < subtitles.size) {
+           val next = subtitles[lastSubtitleIndex + 1]
+           if (next.startTime <= currentPosition && next.endTime >= currentPosition) {
+               lastSubtitleIndex++
+               return next
+           }
+       }
+   }
+   
+   // Fall back to full search if quick check fails
+   for (i in subtitles.indices) {
+       val subtitle = subtitles[i]
+       if (subtitle.startTime <= currentPosition && subtitle.endTime >= currentPosition) {
+           lastSubtitleIndex = i
+           return subtitle
+       }
+   }
+   
+   return null
}

+ // Add method to get next subtitle for optimized polling
+ fun getNextSubtitle(currentPosition: Long): Subtitle? {
+   val subtitles = _subtitles.value
+   return subtitles.find { it.startTime > currentPosition }
+ }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a846490 and 1431789.

📒 Files selected for processing (6)
  • app/src/main/java/ai/elimu/filamu/ui/video/VideoActivity.kt (4 hunks)
  • app/src/main/java/ai/elimu/filamu/ui/video/model/Subtitle.kt (1 hunks)
  • app/src/main/java/ai/elimu/filamu/util/SubtitleUtil.kt (1 hunks)
  • app/src/main/res/layout/activity_video.xml (1 hunks)
  • data-video/src/main/java/ai/elimu/filamu/data/video/viewmodel/VideoViewModel.kt (1 hunks)
  • data-video/src/main/java/ai/elimu/filamu/data/video/viewmodel/VideoViewModelImpl.kt (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (8)
  • GitHub Check: test (28)
  • GitHub Check: test (29)
  • GitHub Check: build (macos-latest, 21)
  • GitHub Check: build (ubuntu-latest, 17)
  • GitHub Check: build (ubuntu-latest, 21)
  • GitHub Check: build (windows-latest, 21)
  • GitHub Check: build (windows-latest, 17)
  • GitHub Check: build (macos-latest, 17)
🔇 Additional comments (5)
app/src/main/java/ai/elimu/filamu/ui/video/VideoActivity.kt (3)

36-36: Good practice using a nullable Job property for coroutine management.

The nullable synchronizationJob property correctly allows for tracking and cancellation of the subtitle synchronization coroutine, which is essential for proper lifecycle management. This aligns with elimu.ai's mission to build innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months.


111-119: Good synchronization trigger point

Triggering subtitle synchronization when the player reaches the STATE_READY state is appropriate. This ensures subtitles start displaying only when the video is ready to play, supporting elimu.ai's mission to provide effective learning tools for children.


135-135: Good cleanup practice

Properly cancelling the synchronization job in onDestroy() prevents memory leaks and unnecessary background work, which is important for maintaining application performance. This careful resource management supports elimu.ai's mission to build effective learning software for children with potentially limited device resources.

data-video/src/main/java/ai/elimu/filamu/data/video/viewmodel/VideoViewModelImpl.kt (2)

22-26: Proper dependency injection for the Application context

Adding the Application parameter through dependency injection is the correct approach for accessing Android resources. This structured approach to accessing resources aligns with elimu.ai's mission to build robust learning software that empowers out-of-school children.


35-36: Well-structured state management using StateFlow

Using a private MutableStateFlow with a public immutable StateFlow accessor follows best practices for state management in Kotlin. This approach provides a clean, reactive way to handle subtitle data changes.

Comment on lines +46 to +49
fun parseTime(time: String): Long {
val parts = time.split(",", ":").map { it.toInt() }
return parts[0] * 3600000 + parts[1] * 60000 + parts[2] * 1000 + parts[3]
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling to parseTime method

The time parsing function lacks error handling which could lead to crashes if the subtitle file contains malformed timestamps.

-    fun parseTime(time: String): Long {
-        val parts = time.split(",", ":").map { it.toInt() }
-        return parts[0] * 3600000 + parts[1] * 60000 + parts[2] * 1000 + parts[3]
+    fun parseTime(time: String): Long {
+        return try {
+            val parts = time.split(",", ":").map { it.toInt() }
+            if (parts.size != 4) {
+                Timber.w("Invalid time format: $time, expected hh:mm:ss,ms")
+                0L
+            } else {
+                parts[0] * 3600000 + parts[1] * 60000 + parts[2] * 1000 + parts[3]
+            }
+        } catch (e: NumberFormatException) {
+            Timber.e(e, "Failed to parse time: $time")
+            0L
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun parseTime(time: String): Long {
val parts = time.split(",", ":").map { it.toInt() }
return parts[0] * 3600000 + parts[1] * 60000 + parts[2] * 1000 + parts[3]
}
fun parseTime(time: String): Long {
return try {
val parts = time.split(",", ":").map { it.toInt() }
if (parts.size != 4) {
Timber.w("Invalid time format: $time, expected hh:mm:ss,ms")
0L
} else {
parts[0] * 3600000 + parts[1] * 60000 + parts[2] * 1000 + parts[3]
}
} catch (e: NumberFormatException) {
Timber.e(e, "Failed to parse time: $time")
0L
}
}

Comment on lines +12 to +44
object SubtitleUtil {

suspend fun loadSubtitles(assetManager: AssetManager, fileName: String): List<Subtitle> {
return withContext(Dispatchers.IO) {
val subtitles = mutableListOf<Subtitle>()
try {
val inputStream = assetManager.open(fileName)
val reader = BufferedReader(InputStreamReader(inputStream))
val pattern = Pattern.compile("(\\d+)\\n(\\d{2}:\\d{2}:\\d{2},\\d{3}) --> (\\d{2}:\\d{2}:\\d{2},\\d{3})\\n(.*)")

var line: String?
val subtitleBuilder = StringBuilder()

while (reader.readLine().also { line = it } != null) {
if (line.isNullOrBlank()) {
val matcher = pattern.matcher(subtitleBuilder.toString())
if (matcher.find()) {
val startTime = parseTime(matcher.group(2)!!)
val endTime = parseTime(matcher.group(3)!!)
val text = matcher.group(4)!!
subtitles.add(Subtitle(startTime, endTime, text))
}
subtitleBuilder.clear()
} else {
subtitleBuilder.append(line).append("\n")
}
}
} catch (e: Exception) {
Timber.e("Error loading subtitles: ${e.message}")
}
subtitles
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix subtitle parsing pattern to handle multi-line subtitles and add proper resource handling

The current implementation has a few issues that could impact subtitle parsing reliability, which is important for elimu.ai's mission to build innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months.

  1. The regular expression pattern only captures single-line subtitles, but SRT files often contain multi-line text
  2. Resource leaks: InputStreams should be closed properly using use extension function
  3. The final subtitle block might not be captured if the file doesn't end with a blank line
  4. Error handling could be more specific
object SubtitleUtil {

    suspend fun loadSubtitles(assetManager: AssetManager, fileName: String): List<Subtitle> {
        return withContext(Dispatchers.IO) {
            val subtitles = mutableListOf<Subtitle>()
            try {
-               val inputStream = assetManager.open(fileName)
-               val reader = BufferedReader(InputStreamReader(inputStream))
-               val pattern = Pattern.compile("(\\d+)\\n(\\d{2}:\\d{2}:\\d{2},\\d{3}) --> (\\d{2}:\\d{2}:\\d{2},\\d{3})\\n(.*)")
+               assetManager.open(fileName).use { inputStream ->
+                   BufferedReader(InputStreamReader(inputStream)).use { reader ->
+                       // Pattern that captures subtitle index, timing and multi-line text content
+                       val pattern = Pattern.compile("(\\d+)\\n(\\d{2}:\\d{2}:\\d{2},\\d{3}) --> (\\d{2}:\\d{2}:\\d{2},\\d{3})\\n((?:.*\\n?)+)")

                        var line: String?
                        val subtitleBuilder = StringBuilder()

                        while (reader.readLine().also { line = it } != null) {
                            if (line.isNullOrBlank()) {
                                val matcher = pattern.matcher(subtitleBuilder.toString())
                                if (matcher.find()) {
                                    val startTime = parseTime(matcher.group(2)!!)
                                    val endTime = parseTime(matcher.group(3)!!)
                                    val text = matcher.group(4)!!
                                    subtitles.add(Subtitle(startTime, endTime, text))
                                }
                                subtitleBuilder.clear()
                            } else {
                                subtitleBuilder.append(line).append("\n")
                            }
                        }
                        
+                       // Process the last subtitle block if it exists
+                       if (subtitleBuilder.isNotEmpty()) {
+                           val matcher = pattern.matcher(subtitleBuilder.toString())
+                           if (matcher.find()) {
+                               val startTime = parseTime(matcher.group(2)!!)
+                               val endTime = parseTime(matcher.group(3)!!)
+                               val text = matcher.group(4)!!
+                               subtitles.add(Subtitle(startTime, endTime, text))
+                           }
+                       }
+                   }
+               }
            } catch (e: Exception) {
-               Timber.e("Error loading subtitles: ${e.message}")
+               Timber.e(e, "Error loading subtitles from %s", fileName)
            }
            subtitles
        }
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
object SubtitleUtil {
suspend fun loadSubtitles(assetManager: AssetManager, fileName: String): List<Subtitle> {
return withContext(Dispatchers.IO) {
val subtitles = mutableListOf<Subtitle>()
try {
val inputStream = assetManager.open(fileName)
val reader = BufferedReader(InputStreamReader(inputStream))
val pattern = Pattern.compile("(\\d+)\\n(\\d{2}:\\d{2}:\\d{2},\\d{3}) --> (\\d{2}:\\d{2}:\\d{2},\\d{3})\\n(.*)")
var line: String?
val subtitleBuilder = StringBuilder()
while (reader.readLine().also { line = it } != null) {
if (line.isNullOrBlank()) {
val matcher = pattern.matcher(subtitleBuilder.toString())
if (matcher.find()) {
val startTime = parseTime(matcher.group(2)!!)
val endTime = parseTime(matcher.group(3)!!)
val text = matcher.group(4)!!
subtitles.add(Subtitle(startTime, endTime, text))
}
subtitleBuilder.clear()
} else {
subtitleBuilder.append(line).append("\n")
}
}
} catch (e: Exception) {
Timber.e("Error loading subtitles: ${e.message}")
}
subtitles
}
}
object SubtitleUtil {
suspend fun loadSubtitles(assetManager: AssetManager, fileName: String): List<Subtitle> {
return withContext(Dispatchers.IO) {
val subtitles = mutableListOf<Subtitle>()
try {
assetManager.open(fileName).use { inputStream ->
BufferedReader(InputStreamReader(inputStream)).use { reader ->
// Pattern that captures subtitle index, timing and multi-line text content
val pattern = Pattern.compile(
"(\\d+)\\n" +
"(\\d{2}:\\d{2}:\\d{2},\\d{3}) --> " +
"(\\d{2}:\\d{2}:\\d{2},\\d{3})\\n" +
"((?:.*\\n?)+)"
)
var line: String?
val subtitleBuilder = StringBuilder()
while (reader.readLine().also { line = it } != null) {
if (line.isNullOrBlank()) {
val matcher = pattern.matcher(subtitleBuilder.toString())
if (matcher.find()) {
val startTime = parseTime(matcher.group(2)!!)
val endTime = parseTime(matcher.group(3)!!)
val text = matcher.group(4)!!.trimEnd()
subtitles.add(Subtitle(startTime, endTime, text))
}
subtitleBuilder.clear()
} else {
subtitleBuilder.append(line).append("\n")
}
}
// Process the last subtitle block if it exists
if (subtitleBuilder.isNotEmpty()) {
val matcher = pattern.matcher(subtitleBuilder.toString())
if (matcher.find()) {
val startTime = parseTime(matcher.group(2)!!)
val endTime = parseTime(matcher.group(3)!!)
val text = matcher.group(4)!!.trimEnd()
subtitles.add(Subtitle(startTime, endTime, text))
}
}
}
}
} catch (e: Exception) {
Timber.e(e, "Error loading subtitles from %s", fileName)
}
subtitles
}
}
}

Comment on lines +107 to +110

// Load subtitles
videoViewModel.loadSubtitles("sample.srt")

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Hardcoded subtitle filename limits flexibility

Using a hardcoded subtitle filename "sample.srt" limits the application's ability to load different subtitle files for different videos. Consider making this dynamic based on the video being played.

- // Load subtitles
- videoViewModel.loadSubtitles("sample.srt")
+ // Load subtitles based on video ID
+ val subtitleFileName = "video_${videoId}_subtitles.srt"
+ videoViewModel.loadSubtitles(subtitleFileName)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Load subtitles
videoViewModel.loadSubtitles("sample.srt")
// Load subtitles based on video ID
val subtitleFileName = "video_${videoId}_subtitles.srt"
videoViewModel.loadSubtitles(subtitleFileName)

Comment on lines +121 to 131
private fun synchronizeSubtitles() {
synchronizationJob?.cancel()
synchronizationJob = lifecycleScope.launch {
while (true) {
val currentPosition = videoPlayer.currentPosition
val subtitle = videoViewModel.getCurrentSubtitle(currentPosition)
binding.subtitleTextView.text = subtitle?.text ?: ""
delay(500) // Check subtitle updates every 500ms
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for subtitle synchronization

The synchronization coroutine doesn't include any error handling. An uncaught exception could terminate the subtitle display.

private fun synchronizeSubtitles() {
    synchronizationJob?.cancel()
    synchronizationJob = lifecycleScope.launch {
+       try {
            while (true) {
                val currentPosition = videoPlayer.currentPosition
                val subtitle = videoViewModel.getCurrentSubtitle(currentPosition)
                binding.subtitleTextView.text = subtitle?.text ?: ""
                delay(500) // Check subtitle updates every 500ms
            }
+       } catch (e: Exception) {
+           Timber.tag(TAG).e("Subtitle synchronization error: ${e.message}")
+           // Retry synchronization after a delay
+           delay(1000)
+           synchronizeSubtitles()
+       }
    }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun synchronizeSubtitles() {
synchronizationJob?.cancel()
synchronizationJob = lifecycleScope.launch {
while (true) {
val currentPosition = videoPlayer.currentPosition
val subtitle = videoViewModel.getCurrentSubtitle(currentPosition)
binding.subtitleTextView.text = subtitle?.text ?: ""
delay(500) // Check subtitle updates every 500ms
}
}
}
private fun synchronizeSubtitles() {
synchronizationJob?.cancel()
synchronizationJob = lifecycleScope.launch {
try {
while (true) {
val currentPosition = videoPlayer.currentPosition
val subtitle = videoViewModel.getCurrentSubtitle(currentPosition)
binding.subtitleTextView.text = subtitle?.text ?: ""
delay(500) // Check subtitle updates every 500ms
}
} catch (e: Exception) {
Timber.tag(TAG).e("Subtitle synchronization error: ${e.message}")
// Retry synchronization after a delay
delay(1000)
synchronizeSubtitles()
}
}
}

Comment on lines +82 to +87
override fun loadSubtitles(fileName: String) {
ioScope.launch {
val loadedSubtitles = SubtitleUtil.loadSubtitles(application.assets, fileName)
_subtitles.emit(loadedSubtitles)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for subtitle loading

The subtitle loading lacks error handling. If the subtitle file doesn't exist or is malformed, this could crash the application.

override fun loadSubtitles(fileName: String) {
    ioScope.launch {
+       try {
            val loadedSubtitles = SubtitleUtil.loadSubtitles(application.assets, fileName)
            _subtitles.emit(loadedSubtitles)
+       } catch (e: Exception) {
+           // Log error and emit empty list to avoid crashing
+           Timber.e("Failed to load subtitles: ${e.message}")
+           _subtitles.emit(emptyList())
+       }
    }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun loadSubtitles(fileName: String) {
ioScope.launch {
val loadedSubtitles = SubtitleUtil.loadSubtitles(application.assets, fileName)
_subtitles.emit(loadedSubtitles)
}
}
override fun loadSubtitles(fileName: String) {
ioScope.launch {
try {
val loadedSubtitles = SubtitleUtil.loadSubtitles(application.assets, fileName)
_subtitles.emit(loadedSubtitles)
} catch (e: Exception) {
// Log error and emit empty list to avoid crashing
Timber.e("Failed to load subtitles: ${e.message}")
_subtitles.emit(emptyList())
}
}
}

Comment on lines +89 to +93
override fun getCurrentSubtitle(currentPosition: Long): Subtitle? {
return _subtitles.value.find {
it.startTime <= currentPosition && it.endTime >= currentPosition
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add unit tests for the subtitle handling logic

The subtitle synchronization is a critical feature for educational videos, but there are no unit tests for this functionality. To ensure a reliable learning experience for children using elimu.ai's software, comprehensive testing is essential.

Please verify if there are any unit tests for the subtitle functionality by running:


🏁 Script executed:

#!/bin/bash
# Check for tests related to subtitle functionality
echo "Looking for subtitle-related test files..."
find . -path "*/src/test/*" -name "*Subtitle*Test*.kt" -o -name "*Test*Subtitle*.kt"

echo "Checking for subtitle-related test methods in VideoViewModel tests..."
find . -path "*/src/test/*" -name "*VideoViewModel*Test*.kt" | xargs grep -l "subtitle" || echo "No subtitle tests found in VideoViewModel test files"

Length of output: 533


Add unit tests for subtitle synchronization logic

To uphold elimu.ai’s mission of building innovative learning software that empowers out-of-school children to teach themselves basic reading📖, writing✍🏽 and math🔢 within 6 months, we need to ensure that subtitle timing is bullet-proof. No existing tests cover getCurrentSubtitle, so let’s add them.

• Create a new test class, e.g. VideoViewModelImplSubtitleTest.kt
• Target getCurrentSubtitle(currentPosition: Long) in VideoViewModelImpl.kt
• Cover scenarios:
– Position before any subtitle (expect null)
– Position exactly at start/end boundaries
– Position within a subtitle interval
– Multiple subtitles with overlapping times
– Empty subtitle list

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Display subtitles from SRT file

2 participants