expo-video-subtitle 0.7.0 → 0.7.1
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,29 @@ 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.1
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Playing an `.ass` track crashed the player (Android).** `Regex(""""\\{([^}]*)}""")` in the sign
|
|
15
|
+
parser left the closing brace unescaped. `java.util.regex` accepts that; Android compiles regular
|
|
16
|
+
expressions with **ICU**, which does not, so the pattern threw `PatternSyntaxException` out of a
|
|
17
|
+
static initialiser on the extractor's loading thread and took playback down with it —
|
|
18
|
+
`ExceptionInInitializerError` on the first ASS subtitle the app opened.
|
|
19
|
+
|
|
20
|
+
The whole class of bug is invisible to this module's JVM tests, because the offending pattern
|
|
21
|
+
compiles there. `RegexPortabilityTest` now reads the sources instead and fails on any regex
|
|
22
|
+
literal with a brace that is neither escaped nor inside a character class.
|
|
23
|
+
|
|
24
|
+
- **A subtitle this package cannot read no longer stops the video.** `StackingSsaParser.split` runs
|
|
25
|
+
on the extractor's loading thread, where a thrown exception is not a failed subtitle — it is a
|
|
26
|
+
dead player. It is now wrapped end to end, and a track that fails falls back to Media3's own ASS
|
|
27
|
+
handling, which draws the captions flat. Subtitle files are untrusted scraper output; plain
|
|
28
|
+
captions are the honest failure, a black screen is not.
|
|
29
|
+
|
|
30
|
+
- **A multi-line sign follows its own alignment.** Every block was laid out centred, so the second
|
|
31
|
+
line of a left- or right-anchored placard pulled to the middle.
|
|
32
|
+
|
|
10
33
|
## 0.7.0
|
|
11
34
|
|
|
12
35
|
### Added
|
|
@@ -149,9 +149,16 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
149
149
|
// Wrapping is the script's job, not ours: a sign is placed by hand and its line breaks are
|
|
150
150
|
// written into the text as \N. A width of "as wide as it wants" reproduces that.
|
|
151
151
|
val textWidth = Layout.getDesiredWidth(sign.text, fillPaint).toInt() + 1
|
|
152
|
+
// A multi-line sign wraps its lines the way its own alignment says, not always centred: a
|
|
153
|
+
// left-anchored placard reads wrong with its second line pulled to the middle.
|
|
154
|
+
val blockAlignment = when ((sign.alignment - 1) % 3) {
|
|
155
|
+
0 -> Layout.Alignment.ALIGN_NORMAL
|
|
156
|
+
2 -> Layout.Alignment.ALIGN_OPPOSITE
|
|
157
|
+
else -> Layout.Alignment.ALIGN_CENTER
|
|
158
|
+
}
|
|
152
159
|
val layout = StaticLayout.Builder
|
|
153
160
|
.obtain(sign.text, 0, sign.text.length, fillPaint, textWidth)
|
|
154
|
-
.setAlignment(
|
|
161
|
+
.setAlignment(blockAlignment)
|
|
155
162
|
.setIncludePad(false)
|
|
156
163
|
.build()
|
|
157
164
|
|
|
@@ -199,7 +206,7 @@ internal class AssSignOverlay(context: Context) : View(context), Player.Listener
|
|
|
199
206
|
// fill afterwards covers that half and leaves the outline the width the script asked for.
|
|
200
207
|
StaticLayout.Builder
|
|
201
208
|
.obtain(sign.text, 0, sign.text.length, strokePaint, textWidth)
|
|
202
|
-
.setAlignment(
|
|
209
|
+
.setAlignment(blockAlignment)
|
|
203
210
|
.setIncludePad(false)
|
|
204
211
|
.build()
|
|
205
212
|
}
|
|
@@ -37,8 +37,15 @@ internal object AssSignParser {
|
|
|
37
37
|
private const val DEFAULT_PLAY_RES_X = 384f
|
|
38
38
|
private const val DEFAULT_PLAY_RES_Y = 288f
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Matches one override block, `{…}`. The contents are scanned separately.
|
|
42
|
+
*
|
|
43
|
+
* Both braces are escaped. A bare closing brace is legal in `java.util.regex` and a syntax error
|
|
44
|
+
* in ICU, which is the engine Android actually uses — so the unescaped form compiles and passes
|
|
45
|
+
* on a JVM test run, then throws `PatternSyntaxException` from a static initialiser on device and
|
|
46
|
+
* takes the playback thread down with it.
|
|
47
|
+
*/
|
|
48
|
+
private val BRACES = Regex("""\{([^}]*)\}""")
|
|
42
49
|
|
|
43
50
|
/**
|
|
44
51
|
* Splits a script into the part this package draws and the part Media3 draws.
|
|
@@ -9,6 +9,7 @@ import androidx.media3.common.Format
|
|
|
9
9
|
import androidx.media3.common.MimeTypes
|
|
10
10
|
import androidx.media3.common.text.Cue
|
|
11
11
|
import androidx.media3.common.util.Consumer
|
|
12
|
+
import androidx.media3.common.util.Log
|
|
12
13
|
import androidx.media3.common.util.UnstableApi
|
|
13
14
|
import androidx.media3.extractor.text.CuesWithTiming
|
|
14
15
|
import androidx.media3.extractor.text.DefaultSubtitleParserFactory
|
|
@@ -337,14 +338,29 @@ class StackingSsaParser(delegate: SubtitleParser) :
|
|
|
337
338
|
StackingSubtitleParser(delegate, SsaCueNormalizer::normalize) {
|
|
338
339
|
|
|
339
340
|
override fun split(data: ByteArray, offset: Int, length: Int): Split {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
341
|
+
// Nothing in here is worth a crash, and the whole body is inside the guard rather than just the
|
|
342
|
+
// parse. This runs on the extractor's loading thread, where an exception does not fail the
|
|
343
|
+
// subtitle — it takes playback down with it, which is how a stray `}` in a regex turned into a
|
|
344
|
+
// dead player on an episode that had been playing fine.
|
|
345
|
+
//
|
|
346
|
+
// A subtitle file is untrusted input from a scraper. The honest failure for one this cannot
|
|
347
|
+
// read is captions drawn Media3's own way, flat, rather than no video at all.
|
|
348
|
+
return runCatching {
|
|
349
|
+
val script = AssSignParser.parse(String(data, offset, length, Charsets.UTF_8))
|
|
350
|
+
if (script.signs.isEmpty()) {
|
|
351
|
+
Split(data, offset, length, emptyList())
|
|
352
|
+
} else {
|
|
353
|
+
val dialogue = script.dialogueOnly.toByteArray(Charsets.UTF_8)
|
|
354
|
+
Split(dialogue, 0, dialogue.size, script.signs.map(::toEntry))
|
|
355
|
+
}
|
|
356
|
+
}.getOrElse { error ->
|
|
357
|
+
Log.w(TAG, "Sign rendering unavailable for this track; falling back to Media3", error)
|
|
358
|
+
Split(data, offset, length, emptyList())
|
|
359
|
+
}
|
|
360
|
+
}
|
|
345
361
|
|
|
346
|
-
|
|
347
|
-
|
|
362
|
+
private companion object {
|
|
363
|
+
const val TAG = "StackingSsaParser"
|
|
348
364
|
}
|
|
349
365
|
|
|
350
366
|
private fun toEntry(sign: AssSign): PreparedCue {
|
package/package.json
CHANGED