expo-video-subtitle 0.7.3 → 0.7.5
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,59 @@ 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.5
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **One episode's signs drew over the next one.** Opening episode 4 after episode 3 kept episode 3's
|
|
15
|
+
typesetting on screen for the whole runtime.
|
|
16
|
+
|
|
17
|
+
The cause was insurance added in 0.7.3 that turned into the bug it was insuring against.
|
|
18
|
+
`AssSignStore.get` answered a missed track id with "the only script on hand", to protect against
|
|
19
|
+
the parser and the overlay disagreeing about `Format.id`. They do not disagree — a device run
|
|
20
|
+
confirmed both name the same track — and the fallback fired in a case that was never considered:
|
|
21
|
+
a new episode misses on its own id for the first twelve seconds, while its 2.7 MB file is still
|
|
22
|
+
parsing, and the only script on hand is the previous episode's. Once resolved, the overlay stops
|
|
23
|
+
looking for a better one, so it never corrected itself.
|
|
24
|
+
|
|
25
|
+
A miss is now a miss. It means "not parsed yet", which the overlay already polls through.
|
|
26
|
+
|
|
27
|
+
- **A track change is noticed without waiting to be told.** The overlay re-resolved only when it had
|
|
28
|
+
no script at all, so it leaned on `onTracksChanged` to catch a switch. It now compares the track
|
|
29
|
+
its script came from against the one currently selected, four times a second, and re-resolves when
|
|
30
|
+
they differ.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- The store keeps two tracks rather than four. That covers switching language within an episode and
|
|
35
|
+
moving to the next one, and each entry holds every sign in an episode — 12,616 in the file this
|
|
36
|
+
was built against.
|
|
37
|
+
|
|
38
|
+
## 0.7.4
|
|
39
|
+
|
|
40
|
+
Signs render. This is the release where the feature added in 0.7.0 actually reaches the screen.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **The overlay gave up looking for its signs before they existed.** The subtitle is fetched and
|
|
45
|
+
parsed on the extractor's loading thread; on a 2.7 MB script that finished **12.4 seconds** after
|
|
46
|
+
the view had bound to the player. The overlay asked the store once at bind time, got nothing, and
|
|
47
|
+
never asked again — because the only thing that would have asked again was the frame loop, and the
|
|
48
|
+
loop was gated on already having a script. Nothing ever broke that circle.
|
|
49
|
+
|
|
50
|
+
The loop now runs whenever there is a player, and polls the store four times a second until a
|
|
51
|
+
script arrives.
|
|
52
|
+
|
|
53
|
+
- **The loop declined to start at the moment it was first asked.** `attachTo` runs from
|
|
54
|
+
`configureSubtitleView`, which fires while the view is still being assembled: `player=true`,
|
|
55
|
+
`attached=false`. `onAttachedToWindow` is what starts it, and it now does.
|
|
56
|
+
|
|
57
|
+
### Known behaviour
|
|
58
|
+
|
|
59
|
+
- Signs appear a few seconds into playback on a large script, after the file has downloaded and
|
|
60
|
+
parsed. Dialogue is unaffected — it comes from the same file but is rendered by Media3 as it
|
|
61
|
+
arrives.
|
|
62
|
+
|
|
10
63
|
## 0.7.3
|
|
11
64
|
|
|
12
65
|
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
|
|
@@ -54,6 +55,8 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
54
55
|
|
|
55
56
|
private var player: Player? = null
|
|
56
57
|
private var script: AssScript? = null
|
|
58
|
+
/** The track [script] belongs to, so a change of track is noticed even mid-poll. */
|
|
59
|
+
private var scriptTrackId: String? = null
|
|
57
60
|
|
|
58
61
|
private var active: List<AssSign> = emptyList()
|
|
59
62
|
private var prepared: List<PreparedSign> = emptyList()
|
|
@@ -92,6 +95,7 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
92
95
|
player?.removeListener(this)
|
|
93
96
|
player = null
|
|
94
97
|
script = null
|
|
98
|
+
scriptTrackId = null
|
|
95
99
|
layouts = HashMap()
|
|
96
100
|
setActive(emptyList())
|
|
97
101
|
}
|
|
@@ -104,10 +108,12 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
104
108
|
private fun resolveScript() {
|
|
105
109
|
val trackId = selectedTextTrackId()
|
|
106
110
|
val next = AssSignStore.get(trackId)
|
|
111
|
+
scriptTrackId = trackId
|
|
107
112
|
if (next === script) return
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
113
|
+
// The one line worth keeping. Every failure of this subsystem has been silent — a span dropped
|
|
114
|
+
// by Parcel, a timestamp rejected, a loop that never started — and each time this line would
|
|
115
|
+
// have named the stage that broke.
|
|
116
|
+
Log.i(TAG, "ASS signs for $trackId: ${next?.signs?.size ?: "none"}")
|
|
111
117
|
script = next
|
|
112
118
|
layouts = HashMap()
|
|
113
119
|
setActive(emptyList())
|
|
@@ -143,20 +149,54 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
143
149
|
frameScheduled = false
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The loop runs whenever there is a player, script or not.
|
|
154
|
+
*
|
|
155
|
+
* Gating it on having a script deadlocked the whole feature. The subtitle is fetched and parsed on
|
|
156
|
+
* the extractor's loading thread and lands seconds after the view has bound — measured at 12.4s on
|
|
157
|
+
* a 2.7 MB script, against a bind at 0.04s. The overlay looked in the store once, found it empty,
|
|
158
|
+
* and then never looked again, because the only thing that would have looked again was the loop it
|
|
159
|
+
* had just declined to start.
|
|
160
|
+
*
|
|
161
|
+
* Paused playback ticks too: a seek while paused has to move the signs with it.
|
|
162
|
+
*/
|
|
146
163
|
private fun schedule() {
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
if (frameScheduled || script == null || !isAttachedToWindow) return
|
|
164
|
+
// Not logged: this reposts on every frame by design, and a line here would be sixty a second.
|
|
165
|
+
if (frameScheduled || player == null || !isAttachedToWindow) return
|
|
150
166
|
frameScheduled = true
|
|
151
167
|
Choreographer.getInstance().postFrameCallback(onFrame)
|
|
152
168
|
}
|
|
153
169
|
|
|
170
|
+
|
|
171
|
+
private var lastLookupMs = 0L
|
|
172
|
+
|
|
154
173
|
private fun tick() {
|
|
155
|
-
val script = this.script ?: return
|
|
156
174
|
val player = this.player ?: return
|
|
157
|
-
|
|
175
|
+
|
|
176
|
+
// Polled rather than waited for, on two counts.
|
|
177
|
+
//
|
|
178
|
+
// A script arrives late: the parser runs on another thread whenever the file finishes
|
|
179
|
+
// downloading — about twelve seconds on a 2.7 MB script — and nothing in Media3 announces "a
|
|
180
|
+
// subtitle has been parsed". The cue stream would have, which is exactly what this design gave
|
|
181
|
+
// up.
|
|
182
|
+
//
|
|
183
|
+
// And the track changes underneath: moving to the next episode swaps the selected text track,
|
|
184
|
+
// and `onTracksChanged` is not something to stake the whole feature on. Comparing the track this
|
|
185
|
+
// script came from against the one selected now catches it either way — without that, episode
|
|
186
|
+
// three's signs kept drawing over episode four.
|
|
187
|
+
val now = SystemClock.uptimeMillis()
|
|
188
|
+
if (now - lastLookupMs >= LOOKUP_INTERVAL_MS) {
|
|
189
|
+
lastLookupMs = now
|
|
190
|
+
if (script == null || selectedTextTrackId() != scriptTrackId) resolveScript()
|
|
191
|
+
}
|
|
192
|
+
if (script == null) return
|
|
193
|
+
|
|
194
|
+
val script = this.script ?: return
|
|
195
|
+
val positionUs = player.contentPosition * 1000L
|
|
196
|
+
setActive(script.signsAt(positionUs))
|
|
158
197
|
}
|
|
159
198
|
|
|
199
|
+
|
|
160
200
|
private fun setActive(next: List<AssSign>) {
|
|
161
201
|
if (next.size == active.size && next.indices.all { next[it] === active[it] }) return
|
|
162
202
|
active = next
|
|
@@ -324,6 +364,9 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
324
364
|
|
|
325
365
|
companion object {
|
|
326
366
|
private const val TAG = "AssSignOverlay"
|
|
367
|
+
|
|
368
|
+
/** How often to ask the store for a script that has not arrived yet. */
|
|
369
|
+
private const val LOOKUP_INTERVAL_MS = 250L
|
|
327
370
|
private const val OPAQUE = 0xFF000000.toInt()
|
|
328
371
|
|
|
329
372
|
/** Removes the overlay from [playerView], dropping its listener registration with it. */
|
|
@@ -345,8 +388,10 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
345
388
|
* produce, but `PlayerView` allows.
|
|
346
389
|
*/
|
|
347
390
|
fun install(playerView: PlayerView): AssSignOverlay? {
|
|
348
|
-
val subtitleView: SubtitleView = playerView.subtitleView
|
|
349
|
-
|
|
391
|
+
val subtitleView: SubtitleView = playerView.subtitleView
|
|
392
|
+
?: return null
|
|
393
|
+
val parent = subtitleView.parent as? ViewGroup
|
|
394
|
+
?: return null
|
|
350
395
|
|
|
351
396
|
for (index in 0 until parent.childCount) {
|
|
352
397
|
(parent.getChildAt(index) as? AssSignOverlay)?.let { return it }
|
|
@@ -30,11 +30,11 @@ package expo.modules.video.utils
|
|
|
30
30
|
internal object AssSignStore {
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
* every sign in an episode — 12,616 in the file this
|
|
35
|
-
* let grow.
|
|
33
|
+
* The track being watched plus the one before it, which covers switching language mid-episode and
|
|
34
|
+
* moving to the next episode. Each entry holds every sign in an episode — 12,616 in the file this
|
|
35
|
+
* was built against — so this is not a cache to let grow.
|
|
36
36
|
*/
|
|
37
|
-
private const val MAX_TRACKS =
|
|
37
|
+
private const val MAX_TRACKS = 2
|
|
38
38
|
|
|
39
39
|
private val scripts = object : LinkedHashMap<String, AssScript>(MAX_TRACKS, 0.75f, true) {
|
|
40
40
|
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, AssScript>): Boolean =
|
|
@@ -48,20 +48,20 @@ internal object AssSignStore {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
51
|
+
* Strictly by id. A miss is a miss.
|
|
52
52
|
*
|
|
53
|
-
*
|
|
54
|
-
* from different places
|
|
55
|
-
* the
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
53
|
+
* This used to fall back to the only script on hand, as insurance against the two ends reading
|
|
54
|
+
* `Format.id` from different places and disagreeing. They do not disagree — the device confirmed
|
|
55
|
+
* the parser and the overlay name the same track — and the insurance turned out to be a bug of its
|
|
56
|
+
* own: opening a second episode misses on the new id, and "the only script on hand" is the
|
|
57
|
+
* *previous* episode's. Its signs then draw over the new one for its whole runtime, because a
|
|
58
|
+
* resolved script stops the overlay from looking again.
|
|
59
|
+
*
|
|
60
|
+
* A miss here is not a failure. It means the file has not finished parsing yet, which on a 2.7 MB
|
|
61
|
+
* script takes about twelve seconds, and the overlay polls until it has.
|
|
59
62
|
*/
|
|
60
63
|
@Synchronized
|
|
61
|
-
fun get(trackId: String?): AssScript? {
|
|
62
|
-
scripts[trackId]?.let { return it }
|
|
63
|
-
return scripts.values.singleOrNull()
|
|
64
|
-
}
|
|
64
|
+
fun get(trackId: String?): AssScript? = trackId?.let { scripts[it] }
|
|
65
65
|
|
|
66
66
|
@Synchronized
|
|
67
67
|
fun clear() {
|
package/package.json
CHANGED