expo-video-subtitle 0.7.5 → 0.7.6

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,33 @@ 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.6
11
+
12
+ ### Fixed
13
+
14
+ - **Signs stopped appearing at all.** The id lookup between the parser and the overlay had never
15
+ matched. Media3 prefixes a merged track's id with the index of the source it came from, so the
16
+ parser is handed `expo-subtitle:id:https://…` while the overlay reads
17
+ `2:expo-subtitle:id:https://…` off the selected track. Every lookup missed, and the
18
+ "use the only script on hand" fallback quietly covered for it — which is why removing that
19
+ fallback in 0.7.5 took the whole feature with it, and why the fallback's own bug (one episode's
20
+ signs drawing over the next) had been the only visible symptom until then.
21
+
22
+ Both ends now normalize the id through the same function before touching the map. Five tests pin
23
+ it, using the exact strings a device produced, and were checked by removing the normalization and
24
+ watching them fail.
25
+
26
+ - **A track with no typesetting no longer logs once a second forever.** The parser files its result
27
+ even when there are no signs in it. Absence in the store used to mean two different things —
28
+ "still parsing" and "nothing to draw" — and the overlay kept asking for the second one.
29
+
30
+ ### Changed
31
+
32
+ - A missed lookup is now logged, throttled to once a second, naming both the id being asked for and
33
+ the ids on hand. Its absence is what let this bug hide behind a fallback for three releases: the
34
+ overlay asked, got nothing, and said nothing, so a wrong key looked exactly like a file that had
35
+ not finished parsing.
36
+
10
37
  ## 0.7.5
11
38
 
12
39
  ### Fixed
@@ -109,7 +109,18 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
109
109
  val trackId = selectedTextTrackId()
110
110
  val next = AssSignStore.get(trackId)
111
111
  scriptTrackId = trackId
112
- if (next === script) return
112
+ if (next === script) {
113
+ // Reported, not swallowed. A lookup that quietly finds nothing is how this subsystem has
114
+ // failed three times running: the overlay asks, gets nothing, and says nothing, so the log
115
+ // looks identical whether the file is still parsing or the key is simply wrong. Throttled to
116
+ // once a second because the poll runs four times that.
117
+ val now = SystemClock.uptimeMillis()
118
+ if (now - lastMissLogMs >= MISS_LOG_INTERVAL_MS) {
119
+ lastMissLogMs = now
120
+ Log.w(TAG, "no ASS signs for track $trackId; parsed so far: ${AssSignStore.keys()}")
121
+ }
122
+ return
123
+ }
113
124
  // The one line worth keeping. Every failure of this subsystem has been silent — a span dropped
114
125
  // by Parcel, a timestamp rejected, a loop that never started — and each time this line would
115
126
  // have named the stage that broke.
@@ -169,6 +180,7 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
169
180
 
170
181
 
171
182
  private var lastLookupMs = 0L
183
+ private var lastMissLogMs = 0L
172
184
 
173
185
  private fun tick() {
174
186
  val player = this.player ?: return
@@ -367,6 +379,9 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
367
379
 
368
380
  /** How often to ask the store for a script that has not arrived yet. */
369
381
  private const val LOOKUP_INTERVAL_MS = 250L
382
+
383
+ /** How often to say so when the answer is still nothing. */
384
+ private const val MISS_LOG_INTERVAL_MS = 1000L
370
385
  private const val OPAQUE = 0xFF000000.toInt()
371
386
 
372
387
  /** Removes the overlay from [playerView], dropping its listener registration with it. */
@@ -41,10 +41,27 @@ internal object AssSignStore {
41
41
  size > MAX_TRACKS
42
42
  }
43
43
 
44
+ /**
45
+ * A merged track's id carries the index of the source it came from: Media3 hands the overlay
46
+ * `2:expo-subtitle:id:https://…` for the track the parser was given as `expo-subtitle:id:https://…`.
47
+ * Both ends key on the same string only if that prefix is taken off first.
48
+ *
49
+ * Measured on a device, and it is what made every id lookup miss while a fallback quietly covered
50
+ * for it. Stripped repeatedly, since sources can nest, and safe for these ids because
51
+ * `SubtitleSource` always begins them with `expo-subtitle:`, never a digit.
52
+ */
53
+ private val MERGED_SOURCE_PREFIX = Regex("""^\d+:""")
54
+
55
+ fun normalizeTrackId(trackId: String): String {
56
+ var id = trackId
57
+ while (MERGED_SOURCE_PREFIX.containsMatchIn(id)) id = id.replaceFirst(MERGED_SOURCE_PREFIX, "")
58
+ return id
59
+ }
60
+
44
61
  /** Called from the extractor's loading thread. */
45
62
  @Synchronized
46
63
  fun put(trackId: String, script: AssScript) {
47
- scripts[trackId] = script
64
+ scripts[normalizeTrackId(trackId)] = script
48
65
  }
49
66
 
50
67
  /**
@@ -61,7 +78,11 @@ internal object AssSignStore {
61
78
  * script takes about twelve seconds, and the overlay polls until it has.
62
79
  */
63
80
  @Synchronized
64
- fun get(trackId: String?): AssScript? = trackId?.let { scripts[it] }
81
+ fun get(trackId: String?): AssScript? = trackId?.let { scripts[normalizeTrackId(it)] }
82
+
83
+ /** The keys on hand, for telling a "not parsed yet" miss from a "wrong key" miss. */
84
+ @Synchronized
85
+ fun keys(): List<String> = scripts.keys.toList()
65
86
 
66
87
  @Synchronized
67
88
  fun clear() {
@@ -342,10 +342,13 @@ class StackingSsaParser(
342
342
  if (trackId == null) return@runCatching Split(data, offset, length)
343
343
 
344
344
  val script = AssSignParser.parse(String(data, offset, length, Charsets.UTF_8))
345
+ // Filed even with no signs in it. The overlay cannot tell "still parsing" from "nothing to
346
+ // draw" by absence alone, and would go on asking — and logging — for the whole episode.
347
+ AssSignStore.put(trackId, script)
348
+
345
349
  if (script.signs.isEmpty()) {
346
350
  Split(data, offset, length)
347
351
  } else {
348
- AssSignStore.put(trackId, script)
349
352
  val dialogue = script.dialogueOnly.toByteArray(Charsets.UTF_8)
350
353
  Split(dialogue, 0, dialogue.size)
351
354
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-video-subtitle",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "A cross-platform, performant video component for React Native and Expo with external subtitle sideloading and custom subtitle styling.",
5
5
  "keywords": [
6
6
  "expo",