A complete solution has been implemented for the live-DVR flow that resolves UX issues related to temporal navigation in live streams. The implementation includes a fictitious timeline that represents the complete DVR window from the start, significantly improving the user experience.
- Inconsistent Timeline: The timeline was resetting when switching between live and DVR
- Lost Position: The user lost their target position during DVR transition
- Limited Duration: The timeline showed only the buffer duration (1-2 minutes) instead of the complete DVR window
- Buffer Override: ExoPlayer was overwriting the fictitious timeline when the buffer updated
// Main DVR variables
private var isDvrMode = false
private var dvrStartParameter: String? = null
private var liveDvrUrl: String? = null
private var isTimelineInteractionEnabled = false
private var isUserScrubbing = false
// Fictitious timeline variables
private var fictitiousTimelineDuration: Long = 0L
private var isUsingFictitiousTimeline = false
// DVR position tracking variables
private var targetDvrPosition: Long = 0L
private var isWaitingForDvrSeek = false- When: During
customizeUi()and initial configuration - Action: Fictitious timeline is initialized with DVR window duration
- Data source: Local configuration (
windowDvr) or API data (dvrWindowOffset)
- When: In
setupDvrTimelineInteraction() - Action: Immediate timeline configuration is forced with fictitious duration
- Result: Progress bar shows complete DVR window from the start
- When: User scrubs on the timeline
- Action: Target position is captured and percentage is calculated in fictitious timeline
- Calculation:
percentage = position / fictitiousTimelineDuration
- When: User releases scrub more than 5 seconds back
- Action: DVR URL is built with time parameters and switches to DVR stream
- Seek: Stored target position is applied
- When: Continuously during playback
- Action: Multiple protection layers to maintain fictitious timeline
- Methods: Listeners, periodic checks, automatic restorations
private fun initializeFictitiousTimeline() {
val hasDvrConfig = msConfig?.type == VideoTypes.LIVE && msConfig?.dvr == true
if (hasDvrConfig) {
val windowDuration = if (dvrWindowDuration > 0) {
dvrWindowDuration
} else {
val configWindowDvr = msConfig?.windowDvr?.toInt() ?: 0
if (configWindowDvr > 0) {
configWindowDvr * 60 // Convert minutes to seconds
} else {
0
}
}
if (windowDuration > 0) {
fictitiousTimelineDuration = (windowDuration * 1000L)
isUsingFictitiousTimeline = true
}
}
}private fun handleDvrScrubStop(position: Long) {
if (isUsingFictitiousTimeline && fictitiousTimelineDuration > 0) {
// Calculate percentage of fictitious timeline
val percentage = position.toFloat() / fictitiousTimelineDuration.toFloat()
// Calculate seek back time based on DVR window duration
val seekBackSeconds = (dvrWindowDuration * (1.0f - percentage)).toLong()
// Store target position for later use
targetDvrPosition = (dvrWindowDuration * 1000L * percentage).toLong()
isWaitingForDvrSeek = true
enableDvrMode(seekBackSeconds)
}
}override fun onTimelineChanged(timeline: Timeline, reason: Int) {
super.onTimelineChanged(timeline, reason)
if (msConfig?.type == VideoTypes.LIVE && msConfig?.dvr == true &&
!isDvrMode && isUsingFictitiousTimeline) {
Handler(Looper.getMainLooper()).postDelayed({
forceRestoreFictitiousTimeline()
}, 100)
}
}
private fun forceRestoreFictitiousTimeline() {
if (!isDvrMode && isUsingFictitiousTimeline && fictitiousTimelineDuration > 0) {
val defaultTimeBar: DefaultTimeBar? = msplayerView?.findViewById(androidx.media3.ui.R.id.exo_progress)
defaultTimeBar?.setDuration(fictitiousTimelineDuration)
defaultTimeBar?.setPosition(fictitiousTimelineDuration)
}
}private fun forceSeekbarPosition(timeBar: DefaultTimeBar?) {
val forcePositionRunnable = object : Runnable {
override fun run() {
if (!isUserScrubbing && !isTimelineInteractionEnabled) {
if (isDvrMode) {
// DVR mode: Normal ExoPlayer handling
val currentPosition = msPlayer?.currentPosition ?: 0L
val duration = msPlayer?.duration ?: 1000L
timeBar?.setPosition(currentPosition)
timeBar?.setDuration(duration)
} else {
// Live mode: Use fictitious timeline
if (isUsingFictitiousTimeline && fictitiousTimelineDuration > 0) {
timeBar?.setDuration(fictitiousTimelineDuration)
timeBar?.setPosition(fictitiousTimelineDuration)
}
}
}
forcePositionHandler.postDelayed(this, 500)
}
}
}msConfig.dvr: Enables/disables DVR functionalitymsConfig.windowDvr: DVR window duration in minutes (local configuration)msConfig.dvrStart: DVR start timestamp (ISO format)msConfig.dvrEnd: DVR end timestamp (ISO format)
mediaInfo.dvrWindowOffset.live_window_time: DVR window duration from APImediaInfo.dvrWindowOffset.account_window_time: Account DVR window duration
- API data:
live_window_time>account_window_time - Local config:
windowDvr(only if ≤ API value) - Fallback: No DVR if no valid configuration
- User loads live stream
- Timeline shows complete DVR window (e.g., 1 hour)
- User scrubs to 20% of timeline
- System calculates: 48 minutes back (60 - 12)
- DVR mode is activated with specific URL
- Player positions at minute 12 of DVR window
- User can navigate freely in DVR
- Clicking "LIVE" returns to live stream
- Problem: Buffer shows 1-2 minutes, fictitious timeline shows 1 hour
- Solution: Fictitious timeline always prevails over buffer
- Protection: Multiple layers of automatic restoration
- Problem: Buffer updates and overwrites timeline
- Solution:
onTimelineChangedrestores fictitious timeline - Backup: Checks every 500ms maintain timeline
- Critical: Always verify
isDvrModebefore applying changes - Important: Reset variables when scrub is canceled or returning to live
- Recommended: Use debug logs to track state changes
- Critical:
fictitiousTimelineDurationmust be consistent - Important: Verify that
isUsingFictitiousTimelineis synchronized - Recommended: Validate duration before applying changes
- Critical: Fallback to standard timeline if DVR fails
- Important: Clean variables in case of error
- Recommended: Detailed logs for debugging
- Critical:
forceSeekbarPositionruns every 500ms - Important: Verify it doesn't cause lag on slow devices
- Recommended: Consider adjusting interval based on performance
-
Initial Fictitious Timeline
- Verify timeline shows complete DVR duration when loading
- Confirm it's not affected by initial buffer
-
Live → DVR Transition
- Test scrub at different positions (10%, 50%, 90%)
- Verify final position is correct
- Confirm DVR URL is built correctly
-
Buffer Protection
- Let stream run for several minutes
- Verify timeline maintains DVR duration
- Confirm it doesn't reset to buffer duration
-
DVR → Live Transition
- Test "LIVE" button from different DVR positions
- Verify variables are cleaned correctly
- Confirm timeline returns to live edge
-
Scrub Cancellation
- Test canceling scrub (touching outside timeline)
- Verify variables are reset
- Confirm DVR mode is not activated
- Timeline Consistency: Timeline must maintain DVR duration 100% of the time
- Position Accuracy: Final position must be within ±2 seconds of target
- State Management: Variables must be clean in all cases
- Performance: No perceptible lag in UI
// Initialization
Log.d(TAG, "DVR: Initialized fictitious timeline with duration: ${fictitiousTimelineDuration}ms")
// State changes
Log.d(TAG, "DVR: Timeline changed - restored fictitious duration: ${fictitiousTimelineDuration}ms")
// Position calculations
Log.d(TAG, "DVR: Fictitious timeline - percentage: ${percentage * 100}%, seekBack: ${seekBackSeconds}s")
// Restorations
Log.d(TAG, "DVR: Force restored fictitious timeline - duration: ${fictitiousTimelineDuration}ms")isDvrMode: Current DVR mode stateisUsingFictitiousTimeline: Whether fictitious timeline is being usedfictitiousTimelineDuration: Current fictitious timeline durationtargetDvrPosition: Stored target positionisWaitingForDvrSeek: Whether waiting to apply seek
- Visual Timeline: Add visual indicators of available DVR window
- Caching: Cache frequent DVR positions for better performance
- Adaptive Timeline: Adjust duration based on actual DVR availability
- Analytics: DVR usage tracking for optimization
- Multi-window DVR: Support for multiple DVR windows
- DVR Bookmarks: Bookmarks in DVR timeline
- DVR Quality: Different qualities for DVR vs live
- Offline DVR: Download DVR content for offline playback
This implementation provides a smooth and consistent user experience for DVR navigation in live streams. The fictitious timeline resolves the identified UX issues and the protection architecture ensures long-term stability.
The development team should focus on:
- Validating all critical use cases
- Monitoring performance on different devices
- Verifying error handling and edge cases
- Considering improvements based on user feedback
Document prepared for development team review Date: [Current Date] Version: 1.0 Status: Implemented and in testing