+24
-10
@@ -9,22 +9,36 @@ object AssHeaderParser {
|
||||
|
||||
private const val ASS_EVENTS = "[Events]\n" +
|
||||
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"
|
||||
private val assEventsSuffix = ("\n" + ASS_EVENTS).toByteArray(Charsets.UTF_8)
|
||||
|
||||
/**
|
||||
* Fix some ass header with error end.
|
||||
* Matroska CodecPrivate data may omit the events section and end with one or more NUL
|
||||
* terminators. Remove every terminator before checking or appending the section so an
|
||||
* embedded NUL cannot hide the event format from libass.
|
||||
*
|
||||
* https://github.com/jellyfin/jellyfin-ffmpeg/issues/506
|
||||
*/
|
||||
private fun fixAssHeaderIfNeed(buffer: ByteArray): ByteArray = if (buffer[buffer.size - 1] != 0.toByte()) {
|
||||
// validate ass header
|
||||
buffer
|
||||
} else {
|
||||
// remote the last null character and append the events tag
|
||||
(String(buffer, 0, buffer.size - 1) + "\n" + ASS_EVENTS).toByteArray()
|
||||
private fun normalizeHeader(buffer: ByteArray): ByteArray {
|
||||
var contentLength = buffer.size
|
||||
while (contentLength > 0 && buffer[contentLength - 1] == 0.toByte()) {
|
||||
contentLength--
|
||||
}
|
||||
|
||||
val header = String(buffer, 0, contentLength, Charsets.UTF_8)
|
||||
val hasEventsSection = header.lineSequence().any {
|
||||
it.trim().equals("[Events]", ignoreCase = true)
|
||||
}
|
||||
if (hasEventsSection) {
|
||||
return if (contentLength == buffer.size) buffer else buffer.copyOf(contentLength)
|
||||
}
|
||||
|
||||
return buffer.copyOf(contentLength + assEventsSuffix.size).also {
|
||||
assEventsSuffix.copyInto(it, destinationOffset = contentLength)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the headers from the initialization data of the given [format]. The original
|
||||
* headers are preserved (duplication checks are handled by libass).
|
||||
* Parses and normalizes the ASS header from the initialization data of the given [format].
|
||||
*/
|
||||
fun parse(format: Format): ByteArray = fixAssHeaderIfNeed(format.initializationData[1])
|
||||
fun parse(format: Format): ByteArray = normalizeHeader(format.initializationData[1])
|
||||
}
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.edde746.plezy.libass.media.parser
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class AssHeaderParserTest {
|
||||
|
||||
@Test
|
||||
fun doubleNulTerminatedHeaderAppendsEventsAfterContent() {
|
||||
val input = SCRIPT_HEADER.toByteArray() + byteArrayOf(0, 0)
|
||||
|
||||
assertArrayEquals(expectedHeader(), parse(input))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unterminatedHeaderWithoutEventsIsRepaired() {
|
||||
assertArrayEquals(expectedHeader(), parse(SCRIPT_HEADER.toByteArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun repairPreservesNonUtf8HeaderBytes() {
|
||||
val content = SCRIPT_HEADER.toByteArray() + byteArrayOf(0xE9.toByte())
|
||||
val input = content + byteArrayOf(0)
|
||||
val expected = content + ("\n" + EVENTS_SECTION).toByteArray()
|
||||
|
||||
assertArrayEquals(expected, parse(input))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun existingEventsSectionIsPreserved() {
|
||||
val input = expectedHeader()
|
||||
|
||||
assertArrayEquals(input, parse(input))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trailingNulsAreRemovedWithoutDuplicatingExistingEvents() {
|
||||
val input = expectedHeader() + byteArrayOf(0, 0)
|
||||
|
||||
assertArrayEquals(expectedHeader(), parse(input))
|
||||
}
|
||||
|
||||
private fun parse(header: ByteArray): ByteArray = AssHeaderParser.parse(
|
||||
Format.Builder()
|
||||
.setInitializationData(listOf(byteArrayOf(), header))
|
||||
.build()
|
||||
)
|
||||
|
||||
private fun expectedHeader(): ByteArray = "$SCRIPT_HEADER\n$EVENTS_SECTION".toByteArray()
|
||||
|
||||
private companion object {
|
||||
const val SCRIPT_HEADER = "[Script Info]\r\nScriptType: v4.00+\r\n"
|
||||
const val EVENTS_SECTION = "[Events]\n" +
|
||||
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user