app/src/main/kotlin/space/subread/overlay/Follower.kt (4962 bytes)
1 package space.subread.overlay 2 3 import android.media.session.MediaController 4 import android.media.session.PlaybackState 5 import android.os.Handler 6 import android.os.SystemClock 7 import space.subread.overlay.core.CueIndex 8 import space.subread.overlay.core.PlayClock 9 10 /** 11 * Follows the media player that plays now, and says which subtitle line is on. 12 * 13 * Each Android media player publishes a media session: Voice, VLC, mpv, YouTube, a podcast app. 14 * The session has the position, the time of that position and the speed. The follower computes 15 * the position between two reports, and sleeps until the next line starts. It does not poll. 16 * 17 * A player that publishes no position: Android makes a position for it that starts at zero when 18 * the player starts to play, and gives none while the player is paused. The follower then holds 19 * the last position, so the line stays. The user sets the timing with the line buttons. 20 */ 21 class Follower( 22 private val handler: Handler, 23 private val now: () -> Long = SystemClock::elapsedRealtime, 24 /** 25 * The index of the line that is on in [index] (-1 before the first line), true when there is 26 * a player to follow, and true when that player plays. 27 */ 28 private val onLine: (Int, Boolean, Boolean) -> Unit, 29 ) { 30 var index: CueIndex = CueIndex(emptyList()) 31 set(value) { 32 field = value 33 update() 34 } 35 36 /** Added to the position of the player, in milliseconds: a file with an intro, a player that is late. */ 37 var offsetMs: Long = 0 38 set(value) { 39 field = value 40 update() 41 } 42 43 private var controller: MediaController? = null 44 private var last: PlayClock? = null 45 private val wake = Runnable { update() } 46 47 private val callback = object : MediaController.Callback() { 48 override fun onPlaybackStateChanged(state: PlaybackState?) = update() 49 override fun onSessionDestroyed() = follow(emptyList()) 50 } 51 52 /** 53 * The sessions that are active, in the order of the system: the one that plays is first. 54 * The follower keeps its player while that player is in the list and no other one plays. 55 */ 56 fun follow(controllers: List<MediaController>) { 57 val playing = controllers.firstOrNull { it.playbackState?.state == PlaybackState.STATE_PLAYING } 58 val kept = controllers.firstOrNull { it.sessionToken == controller?.sessionToken } 59 val next = playing ?: kept ?: controllers.firstOrNull() 60 if (next?.sessionToken != controller?.sessionToken) { 61 controller?.unregisterCallback(callback) 62 controller = next 63 last = null 64 next?.registerCallback(callback, handler) 65 } 66 update() 67 } 68 69 /** True while the player that is followed plays. */ 70 val playing: Boolean 71 get() = controller?.playbackState?.state == PlaybackState.STATE_PLAYING 72 73 /** Pauses the player. True when it played. */ 74 fun pause(): Boolean { 75 if (!playing) return false 76 controller?.transportControls?.pause() 77 return true 78 } 79 80 fun play() { 81 controller?.transportControls?.play() 82 } 83 84 fun stop() { 85 handler.removeCallbacks(wake) 86 controller?.unregisterCallback(callback) 87 controller = null 88 } 89 90 /** Makes the line [steps] lines from the line that is on the current line, by a change of [offsetMs]. */ 91 fun shiftLines(steps: Int) { 92 val clock = clock() ?: return 93 if (index.size == 0) return 94 val position = clock.positionAt(now()) 95 val target = (index.indexAt(position + offsetMs) + steps).coerceIn(0, index.size - 1) 96 offsetMs = index.cues[target].startMs - position 97 } 98 99 private fun clock(): PlayClock? { 100 val state = controller?.playbackState ?: return null 101 val playing = state.state == PlaybackState.STATE_PLAYING 102 val clock = if (state.position >= 0) { 103 // Some players leave the time of the report at zero. Then the report is from now. 104 val reportedAt = state.lastPositionUpdateTime.takeIf { it > 0 } ?: now() 105 val speed = state.playbackSpeed.takeIf { it > 0f } ?: 1f 106 PlayClock(state.position, reportedAt, speed, playing) 107 } else { 108 // No position in this report: hold the position of the report before. 109 PlayClock(last?.positionAt(now()) ?: 0, now(), 1f, playing) 110 } 111 return clock.also { last = it } 112 } 113 114 private fun update() { 115 handler.removeCallbacks(wake) 116 val clock = clock() 117 if (clock == null) { 118 onLine(-1, false, false) 119 return 120 } 121 val position = clock.positionAt(now()) + offsetMs 122 val at = index.indexAt(position) 123 onLine(at, true, clock.playing) 124 val next = index.nextChangeAfter(position) ?: return 125 clock.waitUntil(next - offsetMs, now())?.let { handler.postDelayed(wake, it) } 126 } 127 }