Add some nullness annotations, re-jig some logic and reformat

This commit is contained in:
Ian Baker 2025-01-06 11:42:41 +00:00
parent 391b72e257
commit 8520c66fd8
3 changed files with 87 additions and 86 deletions

View File

@ -212,7 +212,7 @@ public final class Util {
private static final String ISM_HLS_FORMAT_EXTENSION = "format=m3u8-aapl"; private static final String ISM_HLS_FORMAT_EXTENSION = "format=m3u8-aapl";
private static final String ISM_DASH_FORMAT_EXTENSION = "format=mpd-time-csf"; private static final String ISM_DASH_FORMAT_EXTENSION = "format=mpd-time-csf";
private static final int INFLATE_HEADER = 0x78; private static final int ZLIB_INFLATE_HEADER = 0x78;
// Replacement map of ISO language codes used for normalization. // Replacement map of ISO language codes used for normalization.
@Nullable private static HashMap<String, String> languageTagReplacementMap; @Nullable private static HashMap<String, String> languageTagReplacementMap;
@ -3105,25 +3105,23 @@ public final class Util {
} }
/** /**
* Uncompresses the data in {@code input} if it starts with {@code INFLATE_HEADER} * Uncompresses the data in {@code input} if it starts with the zlib marker {@code 0x78}.
* ({@code 0x78}).
* *
* @param input Wraps the compressed input data. * @param input Wraps the compressed input data.
* @param output Wraps an output buffer to be used to store the uncompressed data. If {@code * @param output Wraps an output buffer to be used to store the uncompressed data. If {@code
* output.data} isn't big enough to hold the uncompressed data, a new array is created. If * output.data} isn't big enough to hold the uncompressed data, a new array is created. If
* {@code true} is returned then the output's position will be set to 0 and its limit will be * {@code true} is returned then the output's position will be set to 0 and its limit will be
* set to the length of the uncompressed data. * set to the length of the uncompressed data.
* @param inflater If not null, used to uncompressed the input. Otherwise a new {@link Inflater} * @param inflater If not null, used to uncompress the input. Otherwise a new {@link Inflater} is
* is created. * created.
* @return Whether the input is uncompressed successfully. * @return Whether the input is uncompressed successfully.
*/ */
@UnstableApi @UnstableApi
public static boolean maybeInflate( public static boolean maybeInflate(
ParsableByteArray input, ParsableByteArray output, @Nullable Inflater inflater) { ParsableByteArray input, ParsableByteArray output, @Nullable Inflater inflater) {
if (input.bytesLeft() > 0 && input.peekUnsignedByte() == INFLATE_HEADER) { return input.bytesLeft() > 0
return inflate(input, output, inflater); && input.peekUnsignedByte() == ZLIB_INFLATE_HEADER
} && inflate(input, output, inflater);
return false;
} }
/** /**

View File

@ -22,11 +22,11 @@ import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.extractor.text.dvb.DvbParser; import androidx.media3.extractor.text.dvb.DvbParser;
import androidx.media3.extractor.text.pgs.PgsParser; import androidx.media3.extractor.text.pgs.PgsParser;
import androidx.media3.extractor.text.vobsub.VobsubParser;
import androidx.media3.extractor.text.ssa.SsaParser; import androidx.media3.extractor.text.ssa.SsaParser;
import androidx.media3.extractor.text.subrip.SubripParser; import androidx.media3.extractor.text.subrip.SubripParser;
import androidx.media3.extractor.text.ttml.TtmlParser; import androidx.media3.extractor.text.ttml.TtmlParser;
import androidx.media3.extractor.text.tx3g.Tx3gParser; import androidx.media3.extractor.text.tx3g.Tx3gParser;
import androidx.media3.extractor.text.vobsub.VobsubParser;
import androidx.media3.extractor.text.webvtt.Mp4WebvttParser; import androidx.media3.extractor.text.webvtt.Mp4WebvttParser;
import androidx.media3.extractor.text.webvtt.WebvttParser; import androidx.media3.extractor.text.webvtt.WebvttParser;
import java.util.Objects; import java.util.Objects;

View File

@ -1,8 +1,10 @@
package androidx.media3.extractor.text.vobsub; package androidx.media3.extractor.text.vobsub;
import static java.lang.Math.min; import static java.lang.Math.min;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Rect;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
@ -16,12 +18,10 @@ import androidx.media3.common.util.Util;
import androidx.media3.extractor.text.CuesWithTiming; import androidx.media3.extractor.text.CuesWithTiming;
import androidx.media3.extractor.text.SubtitleParser; import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.zip.Inflater; import java.util.zip.Inflater;
import android.graphics.Rect; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Much of this is taken from or very similar to PgsParser // Much of this is taken from or very similar to PgsParser
@ -36,19 +36,18 @@ public final class VobsubParser implements SubtitleParser {
public static final @CueReplacementBehavior int CUE_REPLACEMENT_BEHAVIOR = public static final @CueReplacementBehavior int CUE_REPLACEMENT_BEHAVIOR =
Format.CUE_REPLACEMENT_BEHAVIOR_REPLACE; Format.CUE_REPLACEMENT_BEHAVIOR_REPLACE;
private static final int DEFAULT_DURATION = 5000000; private static final int DEFAULT_DURATION_US = 5_000_000;
private final ParsableByteArray buffer; private final ParsableByteArray scratch;
private final ParsableByteArray inflatedBuffer; private final ParsableByteArray inflatedScratch;
private final CueBuilder cueBuilder; private final CueBuilder cueBuilder;
@Nullable private Inflater inflater; @Nullable private Inflater inflater;
public VobsubParser(List<byte[]> initializationData) { public VobsubParser(List<byte[]> initializationData) {
scratch = new ParsableByteArray();
buffer = new ParsableByteArray(); inflatedScratch = new ParsableByteArray();
inflatedBuffer = new ParsableByteArray();
cueBuilder = new CueBuilder(); cueBuilder = new CueBuilder();
cueBuilder.parseIdx(new String(initializationData.get(0), StandardCharsets.UTF_8)); cueBuilder.parseIdx(new String(initializationData.get(0), UTF_8));
} }
@Override @Override
@ -63,52 +62,54 @@ public final class VobsubParser implements SubtitleParser {
int length, int length,
OutputOptions outputOptions, OutputOptions outputOptions,
Consumer<CuesWithTiming> output) { Consumer<CuesWithTiming> output) {
scratch.reset(data, offset + length);
scratch.setPosition(offset);
@Nullable Cue cue = parse();
output.accept(
new CuesWithTiming(
cue != null ? ImmutableList.of(cue) : ImmutableList.of(),
/* startTimeUs= */ C.TIME_UNSET,
/* durationUs= */ DEFAULT_DURATION_US));
}
buffer.reset(data, offset + length); @Nullable
buffer.setPosition(offset); private Cue parse() {
if (inflater == null) { if (inflater == null) {
inflater = new Inflater(); inflater = new Inflater();
} }
if (Util.maybeInflate(buffer, inflatedBuffer, inflater)) { if (Util.maybeInflate(scratch, inflatedScratch, inflater)) {
buffer.reset(inflatedBuffer.getData(), inflatedBuffer.limit()); scratch.reset(inflatedScratch.getData(), inflatedScratch.limit());
} }
cueBuilder.reset(); cueBuilder.reset();
Cue cue = null; Cue cue = null;
int blen = buffer.bytesLeft(); int bytesLeft = scratch.bytesLeft();
if (blen >= 2) { if (bytesLeft < 2 || scratch.readUnsignedShort() != bytesLeft) {
int len = buffer.readUnsignedShort(); return null;
if (len == blen) {
cueBuilder.parseSpu(buffer);
cue = cueBuilder.build(buffer);
}
} }
output.accept( cueBuilder.parseSpu(scratch);
new CuesWithTiming( return cueBuilder.build(scratch);
cue != null ? ImmutableList.of(cue) : ImmutableList.of(),
/* startTimeUs= */ C.TIME_UNSET,
/* durationUs= */ DEFAULT_DURATION));
} }
private static final class CueBuilder { private static final class CueBuilder {
private static final int CMD_COLORS = 3; private static final int CMD_COLORS = 3;
private static final int CMD_ALPHA = 4; private static final int CMD_ALPHA = 4;
private static final int CMD_AREA = 5; private static final int CMD_AREA = 5;
private static final int CMD_OFFSETS = 6; private static final int CMD_OFFSETS = 6;
private static final int CMD_END = 255; private static final int CMD_END = 255;
private final int[] colors;
private boolean hasPlane; private boolean hasPlane;
private boolean hasColors; private boolean hasColors;
private boolean hasDataOffsets; private boolean hasDataOffsets;
private int[] palette; private int @MonotonicNonNull [] palette;
private int planeWidth; private int planeWidth;
private int planeHeight; private int planeHeight;
private int[] colors; private @Nullable Rect boundingBox;
private Rect boundingBox; private int dataOffset0;
private int dataOffset0, dataOffset1; private int dataOffset1;
private int dataSize;
public CueBuilder() { public CueBuilder() {
colors = new int[4]; colors = new int[4];
@ -124,10 +125,8 @@ public final class VobsubParser implements SubtitleParser {
palette[i] = parseColor(values[i].trim()); palette[i] = parseColor(values[i].trim());
} }
} else if (line.startsWith("size: ")) { } else if (line.startsWith("size: ")) {
// We need this line to calculate the relative positions and size required when building
// NOTE: we need this line to calculate the relative positions // the Cue below.
// and size required by Cue.Builder() below.
String[] sizes = Util.split(line.substring("size: ".length()).trim(), "x"); String[] sizes = Util.split(line.substring("size: ".length()).trim(), "x");
if (sizes.length == 2) { if (sizes.length == 2) {
@ -151,42 +150,38 @@ public final class VobsubParser implements SubtitleParser {
} }
public void parseSpu(ParsableByteArray buffer) { public void parseSpu(ParsableByteArray buffer) {
if (palette == null || !hasPlane) {
// Give up if we don't have the color palette or the video size. // Give up if we don't have the color palette or the video size.
// (See also the NOTE above) return;
}
if (palette == null || !hasPlane) return; buffer.skipBytes(buffer.readUnsignedShort());
int pos = buffer.getPosition();
dataSize = buffer.readUnsignedShort();
pos += dataSize;
buffer.setPosition(pos);
int end = buffer.readUnsignedShort(); int end = buffer.readUnsignedShort();
parseControl(buffer, end); parseControl(buffer, end);
} }
private void parseControl(ParsableByteArray buffer, int end) { private void parseControl(ParsableByteArray buffer, int end) {
while (buffer.getPosition() < end && buffer.bytesLeft() > 0) { while (buffer.getPosition() < end && buffer.bytesLeft() > 0) {
switch (buffer.readUnsignedByte()) { switch (buffer.readUnsignedByte()) {
case CMD_COLORS: case CMD_COLORS:
if (!parseControlColors(buffer)) return; if (!parseControlColors(buffer)) {
return;
}
break; break;
case CMD_ALPHA: case CMD_ALPHA:
if (!parseControlAlpha(buffer)) return; if (!parseControlAlpha(buffer)) {
return;
}
break; break;
case CMD_AREA: case CMD_AREA:
if (!parseControlArea(buffer)) return; if (!parseControlArea(buffer)) {
return;
}
break; break;
case CMD_OFFSETS: case CMD_OFFSETS:
if (!parseControlOffsets(buffer)) return; if (!parseControlOffsets(buffer)) {
return;
}
break; break;
case CMD_END: case CMD_END:
return; return;
} }
@ -194,7 +189,9 @@ public final class VobsubParser implements SubtitleParser {
} }
private boolean parseControlColors(ParsableByteArray buffer) { private boolean parseControlColors(ParsableByteArray buffer) {
if (buffer.bytesLeft() < 2) return false; if (buffer.bytesLeft() < 2) {
return false;
}
int byte0 = buffer.readUnsignedByte(); int byte0 = buffer.readUnsignedByte();
int byte1 = buffer.readUnsignedByte(); int byte1 = buffer.readUnsignedByte();
@ -209,7 +206,9 @@ public final class VobsubParser implements SubtitleParser {
} }
private boolean parseControlAlpha(ParsableByteArray buffer) { private boolean parseControlAlpha(ParsableByteArray buffer) {
if (buffer.bytesLeft() < 2) return false; if (buffer.bytesLeft() < 2) {
return false;
}
int byte0 = buffer.readUnsignedByte(); int byte0 = buffer.readUnsignedByte();
int byte1 = buffer.readUnsignedByte(); int byte1 = buffer.readUnsignedByte();
@ -223,7 +222,9 @@ public final class VobsubParser implements SubtitleParser {
} }
private boolean parseControlArea(ParsableByteArray buffer) { private boolean parseControlArea(ParsableByteArray buffer) {
if (buffer.bytesLeft() < 6) return false; if (buffer.bytesLeft() < 6) {
return false;
}
int byte0 = buffer.readUnsignedByte(); int byte0 = buffer.readUnsignedByte();
int byte1 = buffer.readUnsignedByte(); int byte1 = buffer.readUnsignedByte();
@ -245,7 +246,9 @@ public final class VobsubParser implements SubtitleParser {
} }
private boolean parseControlOffsets(ParsableByteArray buffer) { private boolean parseControlOffsets(ParsableByteArray buffer) {
if (buffer.bytesLeft() < 4) return false; if (buffer.bytesLeft() < 4) {
return false;
}
dataOffset0 = buffer.readUnsignedShort(); dataOffset0 = buffer.readUnsignedShort();
dataOffset1 = buffer.readUnsignedShort(); dataOffset1 = buffer.readUnsignedShort();
@ -255,8 +258,7 @@ public final class VobsubParser implements SubtitleParser {
} }
private int getColor(int index) { private int getColor(int index) {
if (index >= 0 && index < palette.length) return palette[index]; return index >= 0 && index < palette.length ? palette[index] : palette[0];
return palette[0];
} }
private int setAlpha(int color, int alpha) { private int setAlpha(int color, int alpha) {
@ -283,7 +285,9 @@ public final class VobsubParser implements SubtitleParser {
bitBuffer.reset(buffer); bitBuffer.reset(buffer);
parseRleData(bitBuffer, 1, bitmapData); parseRleData(bitBuffer, 1, bitmapData);
Bitmap bitmap = Bitmap.createBitmap(bitmapData, boundingBox.width(), boundingBox.height(), Bitmap.Config.ARGB_8888); Bitmap bitmap =
Bitmap.createBitmap(
bitmapData, boundingBox.width(), boundingBox.height(), Bitmap.Config.ARGB_8888);
return new Cue.Builder() return new Cue.Builder()
.setBitmap(bitmap) .setBitmap(bitmap)
@ -297,15 +301,15 @@ public final class VobsubParser implements SubtitleParser {
} }
/** /**
* Parse run-length encoded data into the {@code bitmapData} array. The * Parse run-length encoded data into the {@code bitmapData} array. The subtitle bitmap is
* subtitle bitmap is encoded in two blocks of interlaced lines, {@code y} * encoded in two blocks of interlaced lines, {@code y} gives the index of the starting line (0
* gives the index of the starting line (0 or 1). * or 1).
* *
* @param bitBuffer The RLE encoded data. * @param bitBuffer The RLE encoded data.
* @param y Index of the first line. * @param y Index of the first line.
* @param bitmapData Output array. * @param bitmapData Output array.
*/ */
private void parseRleData(ParsableBitArray bitBuffer, int y, int[] bitmapData) { private void parseRleData(ParsableBitArray bitBuffer, int y, int[] bitmapData) {
int width = boundingBox.width(); int width = boundingBox.width();
int height = boundingBox.height(); int height = boundingBox.height();
int x = 0; int x = 0;
@ -316,11 +320,10 @@ public final class VobsubParser implements SubtitleParser {
parseRun(bitBuffer, run); parseRun(bitBuffer, run);
int length = min(run.length, width - x); int length = min(run.length, width - x);
if (length > 0) { if (length > 0) {
Arrays.fill(bitmapData, outIndex, outIndex + length, run.color); Arrays.fill(bitmapData, outIndex, outIndex + length, run.color);
outIndex += length; outIndex += length;
x += length; x += length;
} }
if (x >= width) { if (x >= width) {
y += 2; y += 2;