expo-video-subtitle 0.7.2 → 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,50 @@ 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
|
+
|
|
35
|
+
## 0.7.3
|
|
36
|
+
|
|
37
|
+
Hardening only. Nothing here changes what 0.7.2 draws when 0.7.2 works.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **The sign store falls back to the only script it holds when the track id does not match.** The
|
|
42
|
+
parser reads `Format.id` from the `Format` its factory was handed; the overlay reads it from the
|
|
43
|
+
selected text track. Those should be the same value, but if they ever are not, the failure is
|
|
44
|
+
silent and total — signs parse correctly and never appear, which is precisely how the previous two
|
|
45
|
+
delivery attempts failed. With one track loaded there is nothing to be ambiguous about. With
|
|
46
|
+
several it still declines, because a wrong guess would draw one language's typesetting over
|
|
47
|
+
another's.
|
|
48
|
+
|
|
49
|
+
- **The overlay logs what it resolved.** Every way this subsystem has failed so far has been quiet:
|
|
50
|
+
a dropped span, a rejected timestamp, an id that did not match. One line under the `AssSignOverlay`
|
|
51
|
+
tag now says which text track was selected and how many signs were found for it, so the next
|
|
52
|
+
failure is one `logcat` away instead of a bisect.
|
|
53
|
+
|
|
10
54
|
## 0.7.2
|
|
11
55
|
|
|
12
56
|
### Fixed
|
|
@@ -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
|
|
@@ -18,6 +19,7 @@ import androidx.annotation.OptIn
|
|
|
18
19
|
import androidx.media3.common.C
|
|
19
20
|
import androidx.media3.common.Player
|
|
20
21
|
import androidx.media3.common.Tracks
|
|
22
|
+
import androidx.media3.common.util.Log
|
|
21
23
|
import androidx.media3.common.util.UnstableApi
|
|
22
24
|
import androidx.media3.ui.PlayerView
|
|
23
25
|
import androidx.media3.ui.SubtitleView
|
|
@@ -101,8 +103,13 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
private fun resolveScript() {
|
|
104
|
-
val
|
|
106
|
+
val trackId = selectedTextTrackId()
|
|
107
|
+
val next = AssSignStore.get(trackId)
|
|
105
108
|
if (next === script) return
|
|
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"}")
|
|
106
113
|
script = next
|
|
107
114
|
layouts = HashMap()
|
|
108
115
|
setActive(emptyList())
|
|
@@ -138,20 +145,48 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
138
145
|
frameScheduled = false
|
|
139
146
|
}
|
|
140
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
|
+
*/
|
|
141
159
|
private fun schedule() {
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
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
|
|
145
162
|
frameScheduled = true
|
|
146
163
|
Choreographer.getInstance().postFrameCallback(onFrame)
|
|
147
164
|
}
|
|
148
165
|
|
|
166
|
+
|
|
167
|
+
private var lastLookupMs = 0L
|
|
168
|
+
|
|
149
169
|
private fun tick() {
|
|
150
|
-
val script = this.script ?: return
|
|
151
170
|
val player = this.player ?: return
|
|
152
|
-
|
|
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))
|
|
153
187
|
}
|
|
154
188
|
|
|
189
|
+
|
|
155
190
|
private fun setActive(next: List<AssSign>) {
|
|
156
191
|
if (next.size == active.size && next.indices.all { next[it] === active[it] }) return
|
|
157
192
|
active = next
|
|
@@ -318,6 +353,10 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
318
353
|
}
|
|
319
354
|
|
|
320
355
|
companion object {
|
|
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
|
|
321
360
|
private const val OPAQUE = 0xFF000000.toInt()
|
|
322
361
|
|
|
323
362
|
/** Removes the overlay from [playerView], dropping its listener registration with it. */
|
|
@@ -339,8 +378,10 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
339
378
|
* produce, but `PlayerView` allows.
|
|
340
379
|
*/
|
|
341
380
|
fun install(playerView: PlayerView): AssSignOverlay? {
|
|
342
|
-
val subtitleView: SubtitleView = playerView.subtitleView
|
|
343
|
-
|
|
381
|
+
val subtitleView: SubtitleView = playerView.subtitleView
|
|
382
|
+
?: return null
|
|
383
|
+
val parent = subtitleView.parent as? ViewGroup
|
|
384
|
+
?: return null
|
|
344
385
|
|
|
345
386
|
for (index in 0 until parent.childCount) {
|
|
346
387
|
(parent.getChildAt(index) as? AssSignOverlay)?.let { return it }
|
|
@@ -47,9 +47,21 @@ internal object AssSignStore {
|
|
|
47
47
|
scripts[trackId] = script
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Called from the main thread, once per track change.
|
|
52
|
+
*
|
|
53
|
+
* Falls back to the only script on hand when the id does not match. The two ends read `Format.id`
|
|
54
|
+
* from different places — the parser from the `Format` its factory was handed, the overlay from
|
|
55
|
+
* the selected track — and if those ever disagree the failure is silent and total: signs parse
|
|
56
|
+
* perfectly and never appear, which is exactly how the previous delivery route failed. With one
|
|
57
|
+
* track loaded there is no ambiguity to protect against, so the safe answer is the obvious one.
|
|
58
|
+
* With several, a wrong guess would draw one language's typesetting over another's, so it declines.
|
|
59
|
+
*/
|
|
51
60
|
@Synchronized
|
|
52
|
-
fun get(trackId: String?): AssScript?
|
|
61
|
+
fun get(trackId: String?): AssScript? {
|
|
62
|
+
scripts[trackId]?.let { return it }
|
|
63
|
+
return scripts.values.singleOrNull()
|
|
64
|
+
}
|
|
53
65
|
|
|
54
66
|
@Synchronized
|
|
55
67
|
fun clear() {
|
package/package.json
CHANGED