+24
-10
@@ -9,22 +9,36 @@ object AssHeaderParser {
|
|||||||
|
|
||||||
private const val ASS_EVENTS = "[Events]\n" +
|
private const val ASS_EVENTS = "[Events]\n" +
|
||||||
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"
|
"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
|
* https://github.com/jellyfin/jellyfin-ffmpeg/issues/506
|
||||||
*/
|
*/
|
||||||
private fun fixAssHeaderIfNeed(buffer: ByteArray): ByteArray = if (buffer[buffer.size - 1] != 0.toByte()) {
|
private fun normalizeHeader(buffer: ByteArray): ByteArray {
|
||||||
// validate ass header
|
var contentLength = buffer.size
|
||||||
buffer
|
while (contentLength > 0 && buffer[contentLength - 1] == 0.toByte()) {
|
||||||
} else {
|
contentLength--
|
||||||
// remote the last null character and append the events tag
|
}
|
||||||
(String(buffer, 0, buffer.size - 1) + "\n" + ASS_EVENTS).toByteArray()
|
|
||||||
|
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
|
* Parses and normalizes the ASS header from the initialization data of the given [format].
|
||||||
* headers are preserved (duplication checks are handled by libass).
|
|
||||||
*/
|
*/
|
||||||
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