expo-video-subtitle 0.7.3 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ Changes inherited from the upstream base version are not repeated here — see t
|
|
|
7
7
|
[upstream changelog](https://github.com/expo/expo/blob/main/packages/expo-video/CHANGELOG.md)
|
|
8
8
|
for the history of everything that isn't subtitle related.
|
|
9
9
|
|
|
10
|
+
## 0.7.4
|
|
11
|
+
|
|
12
|
+
Signs render. This is the release where the feature added in 0.7.0 actually reaches the screen.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **The overlay gave up looking for its signs before they existed.** The subtitle is fetched and
|
|
17
|
+
parsed on the extractor's loading thread; on a 2.7 MB script that finished **12.4 seconds** after
|
|
18
|
+
the view had bound to the player. The overlay asked the store once at bind time, got nothing, and
|
|
19
|
+
never asked again — because the only thing that would have asked again was the frame loop, and the
|
|
20
|
+
loop was gated on already having a script. Nothing ever broke that circle.
|
|
21
|
+
|
|
22
|
+
The loop now runs whenever there is a player, and polls the store four times a second until a
|
|
23
|
+
script arrives.
|
|
24
|
+
|
|
25
|
+
- **The loop declined to start at the moment it was first asked.** `attachTo` runs from
|
|
26
|
+
`configureSubtitleView`, which fires while the view is still being assembled: `player=true`,
|
|
27
|
+
`attached=false`. `onAttachedToWindow` is what starts it, and it now does.
|
|
28
|
+
|
|
29
|
+
### Known behaviour
|
|
30
|
+
|
|
31
|
+
- Signs appear a few seconds into playback on a large script, after the file has downloaded and
|
|
32
|
+
parsed. Dialogue is unaffected — it comes from the same file but is rendered by Media3 as it
|
|
33
|
+
arrives.
|
|
34
|
+
|
|
10
35
|
## 0.7.3
|
|
11
36
|
|
|
12
37
|
Hardening only. Nothing here changes what 0.7.2 draws when 0.7.2 works.
|
|
@@ -8,6 +8,7 @@ import android.graphics.Color
|
|
|
8
8
|
import android.graphics.Matrix
|
|
9
9
|
import android.graphics.Paint
|
|
10
10
|
import android.graphics.Typeface
|
|
11
|
+
import android.os.SystemClock
|
|
11
12
|
import android.text.Layout
|
|
12
13
|
import android.text.StaticLayout
|
|
13
14
|
import android.text.TextPaint
|
|
@@ -105,9 +106,10 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
105
106
|
val trackId = selectedTextTrackId()
|
|
106
107
|
val next = AssSignStore.get(trackId)
|
|
107
108
|
if (next === script) return
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
109
|
+
// The one line worth keeping. Every failure of this subsystem has been silent — a span dropped
|
|
110
|
+
// by Parcel, a timestamp rejected, a loop that never started — and each time this line would
|
|
111
|
+
// have named the stage that broke.
|
|
112
|
+
Log.i(TAG, "ASS signs for $trackId: ${next?.signs?.size ?: "none"}")
|
|
111
113
|
script = next
|
|
112
114
|
layouts = HashMap()
|
|
113
115
|
setActive(emptyList())
|
|
@@ -143,20 +145,48 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
143
145
|
frameScheduled = false
|
|
144
146
|
}
|
|
145
147
|
|
|
148
|
+
/**
|
|
149
|
+
* The loop runs whenever there is a player, script or not.
|
|
150
|
+
*
|
|
151
|
+
* Gating it on having a script deadlocked the whole feature. The subtitle is fetched and parsed on
|
|
152
|
+
* the extractor's loading thread and lands seconds after the view has bound — measured at 12.4s on
|
|
153
|
+
* a 2.7 MB script, against a bind at 0.04s. The overlay looked in the store once, found it empty,
|
|
154
|
+
* and then never looked again, because the only thing that would have looked again was the loop it
|
|
155
|
+
* had just declined to start.
|
|
156
|
+
*
|
|
157
|
+
* Paused playback ticks too: a seek while paused has to move the signs with it.
|
|
158
|
+
*/
|
|
146
159
|
private fun schedule() {
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
if (frameScheduled || script == null || !isAttachedToWindow) return
|
|
160
|
+
// Not logged: this reposts on every frame by design, and a line here would be sixty a second.
|
|
161
|
+
if (frameScheduled || player == null || !isAttachedToWindow) return
|
|
150
162
|
frameScheduled = true
|
|
151
163
|
Choreographer.getInstance().postFrameCallback(onFrame)
|
|
152
164
|
}
|
|
153
165
|
|
|
166
|
+
|
|
167
|
+
private var lastLookupMs = 0L
|
|
168
|
+
|
|
154
169
|
private fun tick() {
|
|
155
|
-
val script = this.script ?: return
|
|
156
170
|
val player = this.player ?: return
|
|
157
|
-
|
|
171
|
+
|
|
172
|
+
if (script == null) {
|
|
173
|
+
// Poll rather than wait to be told. The parser finishes on another thread whenever the file
|
|
174
|
+
// finishes downloading, and nothing in Media3 announces "a subtitle has been parsed" — the
|
|
175
|
+
// cue stream would have, which is exactly what this design gave up. Four times a second is
|
|
176
|
+
// far below the cost of the map lookup it does.
|
|
177
|
+
val now = SystemClock.uptimeMillis()
|
|
178
|
+
if (now - lastLookupMs < LOOKUP_INTERVAL_MS) return
|
|
179
|
+
lastLookupMs = now
|
|
180
|
+
resolveScript()
|
|
181
|
+
if (script == null) return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
val script = this.script ?: return
|
|
185
|
+
val positionUs = player.contentPosition * 1000L
|
|
186
|
+
setActive(script.signsAt(positionUs))
|
|
158
187
|
}
|
|
159
188
|
|
|
189
|
+
|
|
160
190
|
private fun setActive(next: List<AssSign>) {
|
|
161
191
|
if (next.size == active.size && next.indices.all { next[it] === active[it] }) return
|
|
162
192
|
active = next
|
|
@@ -324,6 +354,9 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
324
354
|
|
|
325
355
|
companion object {
|
|
326
356
|
private const val TAG = "AssSignOverlay"
|
|
357
|
+
|
|
358
|
+
/** How often to ask the store for a script that has not arrived yet. */
|
|
359
|
+
private const val LOOKUP_INTERVAL_MS = 250L
|
|
327
360
|
private const val OPAQUE = 0xFF000000.toInt()
|
|
328
361
|
|
|
329
362
|
/** Removes the overlay from [playerView], dropping its listener registration with it. */
|
|
@@ -345,8 +378,10 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
345
378
|
* produce, but `PlayerView` allows.
|
|
346
379
|
*/
|
|
347
380
|
fun install(playerView: PlayerView): AssSignOverlay? {
|
|
348
|
-
val subtitleView: SubtitleView = playerView.subtitleView
|
|
349
|
-
|
|
381
|
+
val subtitleView: SubtitleView = playerView.subtitleView
|
|
382
|
+
?: return null
|
|
383
|
+
val parent = subtitleView.parent as? ViewGroup
|
|
384
|
+
?: return null
|
|
350
385
|
|
|
351
386
|
for (index in 0 until parent.childCount) {
|
|
352
387
|
(parent.getChildAt(index) as? AssSignOverlay)?.let { return it }
|
package/package.json
CHANGED