Commit 156bc52c by Oliver Woodman

Clean up DVB support

- Removed the PES_STRIPPED flag. It's unnecessary. We can strip
  PES in the TS extractor instead.
- Made nearly all of the object classes in DvbParser immutable.
  Else it's non-obvious that none of this state can be mutated.
- Made a a lot of the methods in DvbParser static for the same
  reason.
- Removed unnecessary null checks, code that was never executed,
  unused fields etc.
- Add proper flushing of DvbParser, to prevent corrupt output
  following a seek.
parent 8c05b5b1
Showing with 1024 additions and 1530 deletions
...@@ -7,4 +7,4 @@ ...@@ -7,4 +7,4 @@
} }
-keepclassmembers class com.google.android.exoplayer2.text.dvb.DvbDecoder { -keepclassmembers class com.google.android.exoplayer2.text.dvb.DvbDecoder {
public <init>(java.util.List); public <init>(java.util.List);
} }
\ No newline at end of file
...@@ -36,7 +36,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { ...@@ -36,7 +36,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
public void testDecodeEmpty() throws IOException { public void testDecodeEmpty() throws IOException {
SubripDecoder decoder = new SubripDecoder(); SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
// Assert that the subtitle is empty. // Assert that the subtitle is empty.
assertEquals(0, subtitle.getEventTimeCount()); assertEquals(0, subtitle.getEventTimeCount());
assertTrue(subtitle.getCues(0).isEmpty()); assertTrue(subtitle.getCues(0).isEmpty());
...@@ -45,7 +45,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { ...@@ -45,7 +45,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
public void testDecodeTypical() throws IOException { public void testDecodeTypical() throws IOException {
SubripDecoder decoder = new SubripDecoder(); SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(6, subtitle.getEventTimeCount()); assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0); assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2); assertTypicalCue2(subtitle, 2);
...@@ -55,7 +55,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { ...@@ -55,7 +55,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
public void testDecodeTypicalWithByteOrderMark() throws IOException { public void testDecodeTypicalWithByteOrderMark() throws IOException {
SubripDecoder decoder = new SubripDecoder(); SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_BYTE_ORDER_MARK); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_BYTE_ORDER_MARK);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(6, subtitle.getEventTimeCount()); assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0); assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2); assertTypicalCue2(subtitle, 2);
...@@ -65,7 +65,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { ...@@ -65,7 +65,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
public void testDecodeTypicalExtraBlankLine() throws IOException { public void testDecodeTypicalExtraBlankLine() throws IOException {
SubripDecoder decoder = new SubripDecoder(); SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_EXTRA_BLANK_LINE); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_EXTRA_BLANK_LINE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(6, subtitle.getEventTimeCount()); assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0); assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2); assertTypicalCue2(subtitle, 2);
...@@ -76,7 +76,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { ...@@ -76,7 +76,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
// Parsing should succeed, parsing the first and third cues only. // Parsing should succeed, parsing the first and third cues only.
SubripDecoder decoder = new SubripDecoder(); SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_TIMECODE); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_TIMECODE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(4, subtitle.getEventTimeCount()); assertEquals(4, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0); assertTypicalCue1(subtitle, 0);
assertTypicalCue3(subtitle, 2); assertTypicalCue3(subtitle, 2);
...@@ -86,7 +86,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { ...@@ -86,7 +86,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
// Parsing should succeed, parsing the first and third cues only. // Parsing should succeed, parsing the first and third cues only.
SubripDecoder decoder = new SubripDecoder(); SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_SEQUENCE); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_SEQUENCE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(4, subtitle.getEventTimeCount()); assertEquals(4, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0); assertTypicalCue1(subtitle, 0);
assertTypicalCue3(subtitle, 2); assertTypicalCue3(subtitle, 2);
...@@ -96,7 +96,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { ...@@ -96,7 +96,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
// Parsing should succeed, parsing the third cue only. // Parsing should succeed, parsing the third cue only.
SubripDecoder decoder = new SubripDecoder(); SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_NEGATIVE_TIMESTAMPS); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_NEGATIVE_TIMESTAMPS);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(2, subtitle.getEventTimeCount()); assertEquals(2, subtitle.getEventTimeCount());
assertTypicalCue3(subtitle, 0); assertTypicalCue3(subtitle, 0);
} }
...@@ -104,7 +104,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { ...@@ -104,7 +104,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
public void testDecodeNoEndTimecodes() throws IOException { public void testDecodeNoEndTimecodes() throws IOException {
SubripDecoder decoder = new SubripDecoder(); SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
// Test event count. // Test event count.
assertEquals(3, subtitle.getEventTimeCount()); assertEquals(3, subtitle.getEventTimeCount());
......
...@@ -482,7 +482,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { ...@@ -482,7 +482,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
private TtmlSubtitle getSubtitle(String file) throws IOException, SubtitleDecoderException { private TtmlSubtitle getSubtitle(String file) throws IOException, SubtitleDecoderException {
TtmlDecoder ttmlDecoder = new TtmlDecoder(); TtmlDecoder ttmlDecoder = new TtmlDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), file); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), file);
return ttmlDecoder.decode(bytes, bytes.length); return ttmlDecoder.decode(bytes, bytes.length, false);
} }
} }
...@@ -81,14 +81,14 @@ public final class Mp4WebvttDecoderTest extends TestCase { ...@@ -81,14 +81,14 @@ public final class Mp4WebvttDecoderTest extends TestCase {
public void testSingleCueSample() throws SubtitleDecoderException { public void testSingleCueSample() throws SubtitleDecoderException {
Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();
Subtitle result = decoder.decode(SINGLE_CUE_SAMPLE, SINGLE_CUE_SAMPLE.length); Subtitle result = decoder.decode(SINGLE_CUE_SAMPLE, SINGLE_CUE_SAMPLE.length, false);
Cue expectedCue = new Cue("Hello World"); // Line feed must be trimmed by the decoder Cue expectedCue = new Cue("Hello World"); // Line feed must be trimmed by the decoder
assertMp4WebvttSubtitleEquals(result, expectedCue); assertMp4WebvttSubtitleEquals(result, expectedCue);
} }
public void testTwoCuesSample() throws SubtitleDecoderException { public void testTwoCuesSample() throws SubtitleDecoderException {
Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();
Subtitle result = decoder.decode(DOUBLE_CUE_SAMPLE, DOUBLE_CUE_SAMPLE.length); Subtitle result = decoder.decode(DOUBLE_CUE_SAMPLE, DOUBLE_CUE_SAMPLE.length, false);
Cue firstExpectedCue = new Cue("Hello World"); Cue firstExpectedCue = new Cue("Hello World");
Cue secondExpectedCue = new Cue("Bye Bye"); Cue secondExpectedCue = new Cue("Bye Bye");
assertMp4WebvttSubtitleEquals(result, firstExpectedCue, secondExpectedCue); assertMp4WebvttSubtitleEquals(result, firstExpectedCue, secondExpectedCue);
...@@ -96,7 +96,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { ...@@ -96,7 +96,7 @@ public final class Mp4WebvttDecoderTest extends TestCase {
public void testNoCueSample() throws SubtitleDecoderException { public void testNoCueSample() throws SubtitleDecoderException {
Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();
Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length); Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length, false);
assertMp4WebvttSubtitleEquals(result); assertMp4WebvttSubtitleEquals(result);
} }
...@@ -105,7 +105,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { ...@@ -105,7 +105,7 @@ public final class Mp4WebvttDecoderTest extends TestCase {
public void testSampleWithIncompleteHeader() { public void testSampleWithIncompleteHeader() {
Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();
try { try {
decoder.decode(INCOMPLETE_HEADER_SAMPLE, INCOMPLETE_HEADER_SAMPLE.length); decoder.decode(INCOMPLETE_HEADER_SAMPLE, INCOMPLETE_HEADER_SAMPLE.length, false);
} catch (SubtitleDecoderException e) { } catch (SubtitleDecoderException e) {
return; return;
} }
......
...@@ -49,7 +49,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase { ...@@ -49,7 +49,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase {
WebvttDecoder decoder = new WebvttDecoder(); WebvttDecoder decoder = new WebvttDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE);
try { try {
decoder.decode(bytes, bytes.length); decoder.decode(bytes, bytes.length, false);
fail(); fail();
} catch (SubtitleDecoderException expected) { } catch (SubtitleDecoderException expected) {
// Do nothing. // Do nothing.
...@@ -194,7 +194,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase { ...@@ -194,7 +194,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase {
SubtitleDecoderException { SubtitleDecoderException {
WebvttDecoder decoder = new WebvttDecoder(); WebvttDecoder decoder = new WebvttDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), asset); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), asset);
return decoder.decode(bytes, bytes.length); return decoder.decode(bytes, bytes.length, false);
} }
private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) { private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) {
......
...@@ -1465,8 +1465,9 @@ public final class MatroskaExtractor implements Extractor { ...@@ -1465,8 +1465,9 @@ public final class MatroskaExtractor implements Extractor {
break; break;
case CODEC_ID_DVBSUB: case CODEC_ID_DVBSUB:
mimeType = MimeTypes.APPLICATION_DVBSUBS; mimeType = MimeTypes.APPLICATION_DVBSUBS;
initializationData = Collections.singletonList(new byte[] { // Init data: composition_page (2), ancillary_page (2)
(byte) 0x01, codecPrivate[0], codecPrivate[1], codecPrivate[2], codecPrivate[3]}); initializationData = Collections.singletonList(new byte[] {codecPrivate[0],
codecPrivate[1], codecPrivate[2], codecPrivate[3]});
break; break;
default: default:
throw new ParserException("Unrecognized codec identifier."); throw new ParserException("Unrecognized codec identifier.");
......
...@@ -110,7 +110,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact ...@@ -110,7 +110,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
case TsExtractor.TS_STREAM_TYPE_ID3: case TsExtractor.TS_STREAM_TYPE_ID3:
return new PesReader(new Id3Reader()); return new PesReader(new Id3Reader());
case TsExtractor.TS_STREAM_TYPE_DVBSUBS: case TsExtractor.TS_STREAM_TYPE_DVBSUBS:
return new PesReader(new DvbSubtitlesReader(esInfo)); return new PesReader(
new DvbSubtitleReader(esInfo.language, esInfo.dvbSubtitleInitializationData));
default: default:
return null; return null;
} }
......
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
...@@ -23,54 +22,32 @@ import com.google.android.exoplayer2.extractor.TrackOutput; ...@@ -23,54 +22,32 @@ import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
* Output PES packets to a {@link TrackOutput}. * Parses DVB subtitle data and extracts individual frames.
*/ */
public final class DvbSubtitlesReader implements ElementaryStreamReader { public final class DvbSubtitleReader implements ElementaryStreamReader {
private class SubtitleTrack {
private String language;
private List<byte[]> initializationData;
}
private List<SubtitleTrack> subtitles = new ArrayList<>(); private final String language;
private final List<byte[]> initializationData;
private long sampleTimeUs; private TrackOutput output;
private int sampleBytesWritten;
private boolean writingSample; private boolean writingSample;
private int bytesToCheck;
private int sampleBytesWritten;
private long sampleTimeUs;
private List<TrackOutput> outputTracks = new ArrayList<>(); /**
* @param language The subtitle language code.
public DvbSubtitlesReader(TsPayloadReader.EsInfo esInfo) { * @param initializationData Initialization data to be included in the track {@link Format}.
int pos = 2; */
public DvbSubtitleReader(String language, byte[] initializationData) {
while (pos < esInfo.descriptorBytes.length) { this.language = language;
SubtitleTrack subtitle = new SubtitleTrack(); this.initializationData = Collections.singletonList(initializationData);
subtitle.language = new String(new byte[] {
esInfo.descriptorBytes[pos],
esInfo.descriptorBytes[pos + 1],
esInfo.descriptorBytes[pos + 2]});
if (((esInfo.descriptorBytes[pos + 3] & 0xF0 ) >> 4 ) == 2 ) {
subtitle.language += " for hard of hearing";
}
subtitle.initializationData = Collections.singletonList(new byte[] {(byte) 0x00,
esInfo.descriptorBytes[pos + 4], esInfo.descriptorBytes[pos + 5],
esInfo.descriptorBytes[pos + 6], esInfo.descriptorBytes[pos + 7]});
subtitles.add(subtitle);
pos += 8;
}
} }
@Override @Override
public void seek() { public void seek() {
writingSample = false; writingSample = false;
...@@ -78,21 +55,12 @@ public final class DvbSubtitlesReader implements ElementaryStreamReader { ...@@ -78,21 +55,12 @@ public final class DvbSubtitlesReader implements ElementaryStreamReader {
@Override @Override
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
TrackOutput output; idGenerator.generateNewId();
SubtitleTrack subtitle; this.output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
output.format(Format.createImageSampleFormat(idGenerator.getFormatId(),
for (int i = 0; i < subtitles.size(); i++) { MimeTypes.APPLICATION_DVBSUBS, null, Format.NO_VALUE, initializationData, language, null));
subtitle = subtitles.get(i);
idGenerator.generateNewId();
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
output.format(Format.createImageSampleFormat(idGenerator.getFormatId(),
MimeTypes.APPLICATION_DVBSUBS, null, Format.NO_VALUE,
subtitle.initializationData, subtitle.language, null));
outputTracks.add(output);
}
} }
@Override @Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
if (!dataAlignmentIndicator) { if (!dataAlignmentIndicator) {
...@@ -101,33 +69,43 @@ public final class DvbSubtitlesReader implements ElementaryStreamReader { ...@@ -101,33 +69,43 @@ public final class DvbSubtitlesReader implements ElementaryStreamReader {
writingSample = true; writingSample = true;
sampleTimeUs = pesTimeUs; sampleTimeUs = pesTimeUs;
sampleBytesWritten = 0; sampleBytesWritten = 0;
bytesToCheck = 2;
} }
@Override @Override
public void packetFinished() { public void packetFinished() {
TrackOutput output; if (writingSample) {
for (int i = 0; i < outputTracks.size(); i++) {
output = outputTracks.get(i);
output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null); output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null);
writingSample = false;
} }
writingSample = false;
} }
@Override @Override
public void consume(ParsableByteArray data) { public void consume(ParsableByteArray data) {
if (writingSample) { if (writingSample) {
int bytesAvailable = data.bytesLeft(); if (bytesToCheck == 2 && !checkNextByte(data, 0x20)) {
TrackOutput output; // Failed to check data_identifier
int dataPosition = data.getPosition(); return;
for (int i = 0; i < outputTracks.size(); i++) {
data.setPosition(dataPosition);
output = outputTracks.get(i);
output.sampleData(data, bytesAvailable);
} }
if (bytesToCheck == 1 && !checkNextByte(data, 0x00)) {
// Check and discard the subtitle_stream_id
return;
}
int bytesAvailable = data.bytesLeft();
output.sampleData(data, bytesAvailable);
sampleBytesWritten += bytesAvailable; sampleBytesWritten += bytesAvailable;
} }
} }
}
\ No newline at end of file private boolean checkNextByte(ParsableByteArray data, int expectedValue) {
if (data.bytesLeft() == 0) {
return false;
}
if (data.readUnsignedByte() != expectedValue) {
writingSample = false;
}
bytesToCheck--;
return writingSample;
}
}
...@@ -92,7 +92,7 @@ public final class TsExtractor implements Extractor { ...@@ -92,7 +92,7 @@ public final class TsExtractor implements Extractor {
public static final int TS_STREAM_TYPE_H265 = 0x24; public static final int TS_STREAM_TYPE_H265 = 0x24;
public static final int TS_STREAM_TYPE_ID3 = 0x15; public static final int TS_STREAM_TYPE_ID3 = 0x15;
public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86; public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86;
public static final int TS_STREAM_TYPE_DVBSUBS = 0x59; public static final int TS_STREAM_TYPE_DVBSUBS = 0x59;
private static final int TS_PACKET_SIZE = 188; private static final int TS_PACKET_SIZE = 188;
private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
...@@ -408,7 +408,7 @@ public final class TsExtractor implements Extractor { ...@@ -408,7 +408,7 @@ public final class TsExtractor implements Extractor {
if (mode == MODE_HLS && id3Reader == null) { if (mode == MODE_HLS && id3Reader == null) {
// Setup an ID3 track regardless of whether there's a corresponding entry, in case one // Setup an ID3 track regardless of whether there's a corresponding entry, in case one
// appears intermittently during playback. See [Internal: b/20261500]. // appears intermittently during playback. See [Internal: b/20261500].
EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]); EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, null, new byte[0]);
id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo); id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo);
id3Reader.init(timestampAdjuster, output, id3Reader.init(timestampAdjuster, output,
new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE));
...@@ -478,6 +478,7 @@ public final class TsExtractor implements Extractor { ...@@ -478,6 +478,7 @@ public final class TsExtractor implements Extractor {
int descriptorsEndPosition = descriptorsStartPosition + length; int descriptorsEndPosition = descriptorsStartPosition + length;
int streamType = -1; int streamType = -1;
String language = null; String language = null;
byte[] dvbSubtitleInitializationData = null;
while (data.getPosition() < descriptorsEndPosition) { while (data.getPosition() < descriptorsEndPosition) {
int descriptorTag = data.readUnsignedByte(); int descriptorTag = data.readUnsignedByte();
int descriptorLength = data.readUnsignedByte(); int descriptorLength = data.readUnsignedByte();
...@@ -503,12 +504,16 @@ public final class TsExtractor implements Extractor { ...@@ -503,12 +504,16 @@ public final class TsExtractor implements Extractor {
} else if (descriptorTag == TS_PMT_DESC_DVBSUBS) { } else if (descriptorTag == TS_PMT_DESC_DVBSUBS) {
streamType = TS_STREAM_TYPE_DVBSUBS; streamType = TS_STREAM_TYPE_DVBSUBS;
language = new String(data.data, data.getPosition(), 3).trim(); language = new String(data.data, data.getPosition(), 3).trim();
data.skipBytes(4); // Skip language (3) + subtitling_type (1)
// Init data: composition_page (2), ancillary_page (2)
dvbSubtitleInitializationData = new byte[4];
data.readBytes(dvbSubtitleInitializationData, 0, 4);
} }
// Skip unused bytes of current descriptor. // Skip unused bytes of current descriptor.
data.skipBytes(positionOfNextDescriptor - data.getPosition()); data.skipBytes(positionOfNextDescriptor - data.getPosition());
} }
data.setPosition(descriptorsEndPosition); data.setPosition(descriptorsEndPosition);
return new EsInfo(streamType, language, return new EsInfo(streamType, language, dvbSubtitleInitializationData,
Arrays.copyOfRange(data.data, descriptorsStartPosition, descriptorsEndPosition)); Arrays.copyOfRange(data.data, descriptorsStartPosition, descriptorsEndPosition));
} }
......
...@@ -60,17 +60,22 @@ public interface TsPayloadReader { ...@@ -60,17 +60,22 @@ public interface TsPayloadReader {
public final int streamType; public final int streamType;
public final String language; public final String language;
public final byte[] dvbSubtitleInitializationData;
public final byte[] descriptorBytes; public final byte[] descriptorBytes;
/** /**
* @param streamType The type of the stream as defined by the * @param streamType The type of the stream as defined by the
* {@link TsExtractor}{@code .TS_STREAM_TYPE_*}. * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}.
* @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18. * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18.
* @param dvbSubtitleInitializationData If the descriptors include a DVB subtitle tag, this is
* the corresponding decoder initialization data. Null otherwise.
* @param descriptorBytes The descriptor bytes associated to the stream. * @param descriptorBytes The descriptor bytes associated to the stream.
*/ */
public EsInfo(int streamType, String language, byte[] descriptorBytes) { public EsInfo(int streamType, String language, byte[] dvbSubtitleInitializationData,
byte[] descriptorBytes) {
this.streamType = streamType; this.streamType = streamType;
this.language = language; this.language = language;
this.dvbSubtitleInitializationData = dvbSubtitleInitializationData;
this.descriptorBytes = descriptorBytes; this.descriptorBytes = descriptorBytes;
} }
......
...@@ -168,8 +168,9 @@ public class Cue { ...@@ -168,8 +168,9 @@ public class Cue {
public final float size; public final float size;
/** /**
* The bitmap height as a fraction of the of the viewport size, or -1 if the bitmap should be * The bitmap height as a fraction of the of the viewport size, or {@link #DIMEN_UNSET} if the
* displayed at its natural height given for its specified {@link #size}. * bitmap should be displayed at its natural height given the bitmap dimensions and the specified
* {@link #size}.
*/ */
public final float bitmapHeight; public final float bitmapHeight;
...@@ -195,14 +196,15 @@ public class Cue { ...@@ -195,14 +196,15 @@ public class Cue {
* fraction of the viewport height. * fraction of the viewport height.
* @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START},
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* @param width The width of the cue, expressed as a fraction of the viewport width. * @param width The width of the cue as a fraction of the viewport width.
* @param height The width of the cue, expressed as a fraction of the viewport width. * @param height The height of the cue as a fraction of the viewport height, or
* {@link #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the
* specified {@code width}.
*/ */
public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor,
float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float height) {
height) {
this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor,
horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK); horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK);
} }
/** /**
...@@ -248,10 +250,10 @@ public class Cue { ...@@ -248,10 +250,10 @@ public class Cue {
* @param windowColor See {@link #windowColor}. * @param windowColor See {@link #windowColor}.
*/ */
public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,
@AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size,
boolean windowColorSet, int windowColor) { boolean windowColorSet, int windowColor) {
this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size, this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size,
-1, windowColorSet, windowColor); DIMEN_UNSET, windowColorSet, windowColor);
} }
private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line, private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line,
......
...@@ -67,7 +67,7 @@ public abstract class SimpleSubtitleDecoder extends ...@@ -67,7 +67,7 @@ public abstract class SimpleSubtitleDecoder extends
SubtitleOutputBuffer outputBuffer, boolean reset) { SubtitleOutputBuffer outputBuffer, boolean reset) {
try { try {
ByteBuffer inputData = inputBuffer.data; ByteBuffer inputData = inputBuffer.data;
Subtitle subtitle = decode(inputData.array(), inputData.limit()); Subtitle subtitle = decode(inputData.array(), inputData.limit(), reset);
outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs);
// Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]).
outputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); outputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY);
...@@ -82,9 +82,11 @@ public abstract class SimpleSubtitleDecoder extends ...@@ -82,9 +82,11 @@ public abstract class SimpleSubtitleDecoder extends
* *
* @param data An array holding the data to be decoded, starting at position 0. * @param data An array holding the data to be decoded, starting at position 0.
* @param size The size of the data to be decoded. * @param size The size of the data to be decoded.
* @param reset Whether the decoder must be reset before decoding.
* @return The decoded {@link Subtitle}. * @return The decoded {@link Subtitle}.
* @throws SubtitleDecoderException If a decoding error occurs. * @throws SubtitleDecoderException If a decoding error occurs.
*/ */
protected abstract Subtitle decode(byte[] data, int size) throws SubtitleDecoderException; protected abstract Subtitle decode(byte[] data, int size, boolean reset)
throws SubtitleDecoderException;
} }
...@@ -24,7 +24,6 @@ import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder; ...@@ -24,7 +24,6 @@ import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder;
import com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder; import com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder;
import com.google.android.exoplayer2.text.webvtt.WebvttDecoder; import com.google.android.exoplayer2.text.webvtt.WebvttDecoder;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.List; import java.util.List;
/** /**
...@@ -86,7 +85,8 @@ public interface SubtitleDecoderFactory { ...@@ -86,7 +85,8 @@ public interface SubtitleDecoderFactory {
return clazz.asSubclass(SubtitleDecoder.class).getConstructor(Integer.TYPE) return clazz.asSubclass(SubtitleDecoder.class).getConstructor(Integer.TYPE)
.newInstance(format.accessibilityChannel); .newInstance(format.accessibilityChannel);
} else if (format.sampleMimeType.equals(MimeTypes.APPLICATION_DVBSUBS)) { } else if (format.sampleMimeType.equals(MimeTypes.APPLICATION_DVBSUBS)) {
return clazz.asSubclass(SubtitleDecoder.class).getConstructor(List.class).newInstance(format.initializationData); return clazz.asSubclass(SubtitleDecoder.class).getConstructor(List.class)
.newInstance(format.initializationData);
} else { } else {
return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance(); return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance();
} }
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
package com.google.android.exoplayer2.text.dvb; package com.google.android.exoplayer2.text.dvb;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.List; import java.util.List;
/** /**
...@@ -26,27 +26,25 @@ public final class DvbDecoder extends SimpleSubtitleDecoder { ...@@ -26,27 +26,25 @@ public final class DvbDecoder extends SimpleSubtitleDecoder {
private final DvbParser parser; private final DvbParser parser;
/**
* @param initializationData The initialization data for the decoder. The initialization data
* must consist of a single byte array containing 5 bytes: flag_pes_stripped (1),
* composition_page (2), ancillary_page (2).
*/
public DvbDecoder(List<byte[]> initializationData) { public DvbDecoder(List<byte[]> initializationData) {
super("DvbDecoder"); super("DvbDecoder");
ParsableByteArray data = new ParsableByteArray(initializationData.get(0));
int subtitleCompositionPage = 1; int subtitleCompositionPage = data.readUnsignedShort();
int subtitleAncillaryPage = 1; int subtitleAncillaryPage = data.readUnsignedShort();
int flags = 0; parser = new DvbParser(subtitleCompositionPage, subtitleAncillaryPage);
byte[] tempByteArray;
if ((tempByteArray = initializationData.get(0)) != null && tempByteArray.length == 5) {
if (tempByteArray[0] == 0x01) {
flags |= DvbParser.FLAG_PES_STRIPPED_DVBSUB;
}
subtitleCompositionPage = ((tempByteArray[1] & 0xFF) << 8) | (tempByteArray[2] & 0xFF);
subtitleAncillaryPage = ((tempByteArray[3] & 0xFF) << 8) | (tempByteArray[4] & 0xFF);
}
parser = new DvbParser(subtitleCompositionPage, subtitleAncillaryPage, flags);
} }
@Override @Override
protected DvbSubtitle decode(byte[] data, int length) { protected DvbSubtitle decode(byte[] data, int length, boolean reset) {
return new DvbSubtitle(parser.dvbSubsDecode(data, length)); if (reset) {
parser.reset();
}
return new DvbSubtitle(parser.decode(data, length));
} }
}
\ No newline at end of file }
...@@ -20,1550 +20,1006 @@ import android.graphics.Canvas; ...@@ -20,1550 +20,1006 @@ import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.DashPathEffect;
import android.graphics.PorterDuffXfermode; import android.graphics.PorterDuffXfermode;
import android.graphics.Region; import android.graphics.Region;
import android.support.annotation.IntDef;
import android.util.Log; import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.core.BuildConfig;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
* Parse and generate a list of {@link Cue}s from DVB subtitling bitstream * Parses {@link Cue}s from a DVB subtitle bitstream.
*/ */
public class DvbParser { /* package */ final class DvbParser {
private static final String TAG = "DVBSubs"; private static final String TAG = "DvbParser";
@IntDef(flag = true, value = {FLAG_PES_STRIPPED_DVBSUB}) // Segment types, as defined by ETSI EN 300 743 Table 2
public @interface Flags { private static final int SEGMENT_TYPE_PAGE_COMPOSITION = 0x10;
} private static final int SEGMENT_TYPE_REGION_COMPOSITION = 0x11;
private static final int SEGMENT_TYPE_CLUT_DEFINITION = 0x12;
public static final int FLAG_PES_STRIPPED_DVBSUB = 1; private static final int SEGMENT_TYPE_OBJECT_DATA = 0x13;
private static final int SEGMENT_TYPE_DISPLAY_DEFINITION = 0x14;
@Flags
private final int flags; // Page states, as defined by ETSI EN 300 743 Table 3
private static final int PAGE_STATE_NORMAL = 0; // Update. Only changed elements.
/* List of different SEGMENT TYPES */ // private static final int PAGE_STATE_ACQUISITION = 1; // Refresh. All elements.
/* According to EN 300-743, table 2 */ // private static final int PAGE_STATE_CHANGE = 2; // New. All elements.
private final static int DVBSUB_ST_PAGE_COMPOSITION = 0x10;
private final static int DVBSUB_ST_REGION_COMPOSITION = 0x11; // Region depths, as defined by ETSI EN 300 743 Table 5
private final static int DVBSUB_ST_CLUT_DEFINITION = 0x12; // private static final int REGION_DEPTH_2_BIT = 1;
private final static int DVBSUB_ST_OBJECT_DATA = 0x13; private static final int REGION_DEPTH_4_BIT = 2;
private final static int DVBSUB_ST_DISPLAY_DEFINITION = 0x14; private static final int REGION_DEPTH_8_BIT = 3;
private final static int DVBSUB_ST_ENDOFDISPLAY = 0x80;
private final static int DVBSUB_ST_STUFFING = 0xff; // Object codings, as defined by ETSI EN 300 743 Table 8
private static final int OBJECT_CODING_PIXELS = 0;
/* List of different Page Composition Segment state */ private static final int OBJECT_CODING_STRING = 1;
/* According to EN 300-743, 7.2.1 table 3 */
private final static int DVBSUB_PCS_STATE_NORMAL = 0b00; // Update. Only changed elements. // Pixel-data types, as defined by ETSI EN 300 743 Table 9
private final static int DVBSUB_PCS_STATE_ACQUISITION = 0b01; // Refresh. All subtitle elements. private static final int DATA_TYPE_2BP_CODE_STRING = 0x10;
private final static int DVBSUB_PCS_STATE_CHANGE = 0b10; // New. All subtitle elements. private static final int DATA_TYPE_4BP_CODE_STRING = 0x11;
private static final int DATA_TYPE_8BP_CODE_STRING = 0x12;
/* List of different Region Composition Segments CLUT level oc compatibility */ private static final int DATA_TYPE_24_TABLE_DATA = 0x20;
/* According to EN 300-743, 7.2.1 table 4 */ private static final int DATA_TYPE_28_TABLE_DATA = 0x21;
private final static int DVBSUB_RCS_CLUT_2 = 0x01; private static final int DATA_TYPE_48_TABLE_DATA = 0x22;
private final static int DVBSUB_RCS_CLUT_4 = 0x02; private static final int DATA_TYPE_END_LINE = 0xF0;
private final static int DVBSUB_RCS_CLUT_8 = 0x03;
// Clut mapping tables, as defined by ETSI EN 300 743 10.4, 10.5, 10.6
/* List of different Region Composition Segments bit depths */ private static final byte[] defaultMap2To4 = {
/* According to EN 300-743, 7.2.1 table 5 */ (byte) 0x00, (byte) 0x07, (byte) 0x08, (byte) 0x0F};
private final static int DVBSUB_RCS_BITDEPTH_2 = 0x01; private static final byte[] defaultMap2To8 = {
private final static int DVBSUB_RCS_BITDEPTH_4 = 0x02; (byte) 0x00, (byte) 0x77, (byte) 0x88, (byte) 0xFF};
private final static int DVBSUB_RCS_BITDEPTH_8 = 0x03; private static final byte[] defaultMap4To8 = {
(byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33,
/* List of different object types in the Region Composition Segment */
/* According to EN 300-743, table 6 */
private final static int DVBSUB_OT_BASIC_BITMAP = 0x00;
private final static int DVBSUB_OT_BASIC_CHAR = 0x01;
private final static int DVBSUB_OT_COMPOSITE_STRING = 0x02;
/* List of different object coding methods in the Object Data Segment */
/* According to EN 300-743, table 8 */
private static final int DVBSUB_ODS_PIXEL_CODED = 0x00;
private static final int DVBSUB_ODS_CHAR_CODED = 0x01;
/* Pixel DATA TYPES */
/* According to EN 300-743, table 9 */
private final static int DVBSUB_DT_2BP_CODE_STRING = 0x10;
private final static int DVBSUB_DT_4BP_CODE_STRING = 0x11;
private final static int DVBSUB_DT_8BP_CODE_STRING = 0x12;
private final static int DVBSUB_DT_24_TABLE_DATA = 0x20;
private final static int DVBSUB_DT_28_TABLE_DATA = 0x21;
private final static int DVBSUB_DT_48_TABLE_DATA = 0x22;
private final static int DVBSUB_DT_END_LINE = 0xf0;
/* Clut mapping tables */
/* According to EN 300-743, 10.4 10.5 10.6 */
private byte[] defaultMap24 = {(byte) 0x00, (byte) 0x07, (byte) 0x08, (byte) 0x0f};
private byte[] defaultMap28 = {(byte) 0x00, (byte) 0x77, (byte) 0x88, (byte) 0xff};
private byte[] defaultMap48 = {(byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33,
(byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77,
(byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0x88, (byte) 0x99, (byte) 0xAA, (byte) 0xBB,
(byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff}; (byte) 0xCC, (byte) 0xDD, (byte) 0xEE, (byte) 0xFF};
/* FLAGS */
private final static int DISPLAY_WINDOW_FLAG = 0x01;
private final static int REGION_FILL_FLAG = 0x01;
private final static int OBJECT_NON_MODIFYING_COLOUR_FLAG = 0x01; private final Paint defaultPaint;
private final Paint fillRegionPaint;
private final Canvas canvas;
private final DisplayDefinition defaultDisplayDefinition;
private final ClutDefinition defaultClutDefinition;
private final SubtitleService subtitleService;
/* instance variables */
private Paint defaultPaint = new Paint();
private Paint fillRegionPaint = new Paint();
private Paint debugRegionPaint = new Paint();
private Paint debugObjectPaint = new Paint();
private Bitmap bitmap; private Bitmap bitmap;
private Canvas canvas = new Canvas();
private ClutDefinition defaultClut = new ClutDefinition();
private static ParsableBitArray tsStream; /**
private SubtitleService subtitleService; * Construct an instance for the given subtitle and ancillary page ids.
*
/* * @param subtitlePageId The id of the subtitle page carrying the subtitle to be parsed.
* Contains the current subtitle service definition * @param ancillaryPageId The id of the ancillary page containing additional data.
*/ */
private class SubtitleService { public DvbParser(int subtitlePageId, int ancillaryPageId) {
int subtitlePageId; defaultPaint = new Paint();
int ancillaryPageId; defaultPaint.setStyle(Paint.Style.FILL_AND_STROKE);
defaultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
// subtitle page defaultPaint.setPathEffect(null);
DisplayDefinition displayDefinition; fillRegionPaint = new Paint();
PageComposition pageComposition; fillRegionPaint.setStyle(Paint.Style.FILL);
SparseArray<RegionComposition> regions = new SparseArray<>(); fillRegionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));
SparseArray<ClutDefinition> cluts = new SparseArray<>(); fillRegionPaint.setPathEffect(null);
SparseArray<ObjectData> objects = new SparseArray<>(); canvas = new Canvas();
defaultDisplayDefinition = new DisplayDefinition(719, 575, 0, 719, 0, 575);
// ancillary page defaultClutDefinition = new ClutDefinition(0, generateDefault2BitClutEntries(),
SparseArray<ClutDefinition> ancillaryCluts = new SparseArray<>(); generateDefault4BitClutEntries(), generateDefault8BitClutEntries());
SparseArray<ObjectData> ancillaryObjects = new SparseArray<>(); subtitleService = new SubtitleService(subtitlePageId, ancillaryPageId);
}
/* The display definition contains the geometry and active area of the subtitle service [7.2.1] */
private class DisplayDefinition {
int pageId;
int versionNumber;
int displayWidth = 719;
int displayHeight = 575;
int flags;
int displayWindowHorizontalPositionMinimum = 0;
int displayWindowHorizontalPositionMaximum = 719;
int displayWindowVerticalPositionMinimum = 0;
int displayWindowVerticalPositionMaximum = 575;
void updateBitmapResolution() {
bitmap = Bitmap.createBitmap(this.displayWidth + 1, this.displayHeight + 1,
Bitmap.Config.ARGB_8888);
canvas = new Canvas(bitmap);
}
}
/* The page is the definition and arrangement of regions in the screen [7.2.2] */
private class PageComposition {
int pageId;
int pageTimeOut; /* in seconds */
int pageVersionNumber;
int pageState;
SparseArray<PageRegion> pageRegions = new SparseArray<>();
}
private class PageRegion {
int regionId;
int regionHorizontalAddress;
int regionVerticalAddress;
}
/* The Region is an area of the page [7.2.3] composed of a list of objects and a CLUT */
private class RegionComposition {
int pageId;
int regionId;
int regionVersionNumber;
int flags;
int regionWidth;
int regionHeight;
int regionLevelOfCompatibility;
int regionDepth;
int clutId;
int region8bitPixelCode;
int region4bitPixelCode;
int region2bitPixelCode;
SparseArray<RegionObject> regionObjects = new SparseArray<>();
/*
* We maintain a reference to the Cue to implement future drawing optimizations, no re-render in case of:
*
* - Page updates not affecting region composition (no clut change/redefinition, no object changes)
* - Incremental subtitle display render (e.g. live captions updates)
*/
Cue cue;
} }
private class RegionObject { /**
int objectId; * Resets the parser.
int objectType; */
int objectProvider; public void reset() {
int objectHorizontalPosition; subtitleService.reset();
int objectVerticalPosition;
int foregroundPixelCode;
int backgroundPixelCode;
} }
/* An entry in the palette CLUT and associated color space translation methods */ /**
private class ClutEntry { * Decodes a subtitling packet, returning a list of parsed {@link Cue}s.
int clutEntryId; *
byte flags; * @param data The subtitling packet data to decode.
byte Y; * @param limit The limit in {@code data} at which to stop decoding.
byte Cr; * @return The parsed {@link Cue}s.
byte Cb; */
byte T; public List<Cue> decode(byte[] data, int limit) {
// Parse the input data.
byte A; ParsableBitArray dataBitArray = new ParsableBitArray(data, limit);
byte R; while (dataBitArray.bitsLeft() >= 48 // sync_byte (8) + segment header (40)
byte G; && dataBitArray.readBits(8) == 0x0F) {
byte B; parseSubtitlingSegment(dataBitArray, subtitleService);
int ARGB;
void clutYCbCrT(int Y, int Cb, int Cr, int T) {
this.Y = (byte) Y;
this.Cb = (byte) Cb;
this.Cr = (byte) Cr;
this.T = (byte) T;
int R = (int) (Y + 1.40200 * (Cr - 128));
int G = (int) (Y - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128));
int B = (int) (Y + 1.77200 * (Cb - 128));
if (R > 255) this.R = (byte) 255;
else if (R < 0) this.R = 0;
else this.R = (byte) R;
if (G > 255) this.G = (byte) 255;
else if (G < 0) this.G = 0;
else this.G = (byte) G;
if (B > 255) this.B = (byte) 255;
else if (B < 0) this.B = 0;
else this.B = (byte) B;
this.A = (byte) (0xFF - (this.T & 0xFF));
this.ARGB =
((this.A & 0xFF) << 24) |
((this.R & 0xFF) << 16) |
((this.G & 0xFF) << 8) |
(this.B & 0xFF);
} }
void clutRGBA(int R, int G, int B, int A) { if (subtitleService.pageComposition == null) {
return Collections.emptyList();
this.A = (byte) A;
this.R = (byte) R;
this.G = (byte) G;
this.B = (byte) B;
this.ARGB =
((A & 0xFF) << 24) |
((R & 0xFF) << 16) |
((G & 0xFF) << 8) |
(B & 0xFF);
int y = (int) (0.299000 * R + 0.587000 * G + 0.114000 * B);
int Cb = 128 + (int) (-0.168736 * R + -0.331264 * G + 0.500000 * B);
int Cr = 128 + (int) (0.500000 * R + -0.418688 * G + -0.081312 * B);
if (y > 255) this.Y = (byte) 255;
else if (y < 0) this.Y = 0;
else this.Y = (byte) y;
if (Cb > 255) this.Cb = (byte) 255;
else if (Cb < 0) this.Cb = 0;
else this.Cb = (byte) Cb;
if (Cr > 255) this.Cr = (byte) 255;
else if (Cr < 0) this.Cr = 0;
else this.Cr = (byte) Cr;
this.T = (byte) (0xFF - (this.A & 0xFF));
} }
}
/* CLUT family definition containing the color tables for the three bitdepths defined [7.2.4] */ // Update the canvas bitmap if necessary.
private class ClutDefinition { DisplayDefinition displayDefinition = subtitleService.displayDefinition != null
int pageId; ? subtitleService.displayDefinition : defaultDisplayDefinition;
int clutId; if (bitmap == null || displayDefinition.width + 1 != bitmap.getWidth()
int clutVersionNumber; || displayDefinition.height + 1 != bitmap.getHeight()) {
ClutEntry[] clutEntries2bit; bitmap = Bitmap.createBitmap(displayDefinition.width + 1, displayDefinition.height + 1,
ClutEntry[] clutEntries4bit; Bitmap.Config.ARGB_8888);
ClutEntry[] clutEntries8bit; canvas.setBitmap(bitmap);
ClutEntry[] generateDefault2bitClut() {
ClutEntry[] entries = new ClutEntry[4];
entries[0] = new ClutEntry();
entries[0].clutRGBA(0x00, 0x00, 0x00, 0x00);
entries[1] = new ClutEntry();
entries[1].clutRGBA(0xFF, 0xFF, 0xFF, 0xFF);
entries[2] = new ClutEntry();
entries[2].clutRGBA(0x00, 0x00, 0x00, 0xFF);
entries[3] = new ClutEntry();
entries[3].clutRGBA(0x7F, 0x7F, 0x7F, 0xFF);
return entries;
} }
ClutEntry[] generateDefault4bitClut() { // Build the cues.
ClutEntry[] entries = new ClutEntry[16]; List<Cue> cues = new ArrayList<>();
SparseArray<PageRegion> pageRegions = subtitleService.pageComposition.regions;
entries[0] = new ClutEntry(); for (int i = 0; i < pageRegions.size(); i++) {
entries[0].clutRGBA(0x00, 0x00, 0x00, 0x00); PageRegion pageRegion = pageRegions.valueAt(i);
int regionId = pageRegions.keyAt(i);
int i = 15; RegionComposition regionComposition = subtitleService.regions.get(regionId);
while (i > 0) {
entries[i] = new ClutEntry(); // Clip drawing to the current region and display definition window.
if (i < 8) { int baseHorizontalAddress = pageRegion.horizontalAddress
entries[i].clutRGBA( + displayDefinition.horizontalPositionMinimum;
((i & 0x01) != 0 ? 0xFF : 0x00), int baseVerticalAddress = pageRegion.verticalAddress
((i & 0x02) != 0 ? 0xFF : 0x00), + displayDefinition.verticalPositionMinimum;
((i & 0x04) != 0 ? 0xFF : 0x00), int clipRight = Math.min(baseHorizontalAddress + regionComposition.width,
0xFF); displayDefinition.horizontalPositionMaximum);
} else { int clipBottom = Math.min(baseVerticalAddress + regionComposition.height,
entries[i].clutRGBA( displayDefinition.verticalPositionMaximum);
((i & 0x01) != 0 ? 0x7F : 0x00), canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom,
((i & 0x02) != 0 ? 0x7F : 0x00), Region.Op.REPLACE);
((i & 0x04) != 0 ? 0x7F : 0x00),
0xFF); ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId);
if (clutDefinition == null) {
clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId);
if (clutDefinition == null) {
clutDefinition = defaultClutDefinition;
} }
i--;
} }
return entries; SparseArray<RegionObject> regionObjects = regionComposition.regionObjects;
} for (int j = 0; j < regionObjects.size(); j++) {
int objectId = regionObjects.keyAt(j);
RegionObject regionObject = regionObjects.valueAt(j);
ObjectData objectData = subtitleService.objects.get(objectId);
if (objectData == null) {
objectData = subtitleService.ancillaryObjects.get(objectId);
}
if (objectData != null) {
Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint;
paintPixelDataSubBlocks(objectData, clutDefinition, regionComposition.depth,
baseHorizontalAddress + regionObject.horizontalPosition,
baseVerticalAddress + regionObject.verticalPosition, paint, canvas);
}
}
ClutEntry[] generateDefault8bitClut() { if (regionComposition.fillFlag) {
ClutEntry[] entries = new ClutEntry[256]; int color;
if (regionComposition.depth == REGION_DEPTH_8_BIT) {
entries[0] = new ClutEntry(); color = clutDefinition.clutEntries8Bit[regionComposition.pixelCode8Bit];
entries[0].clutRGBA(0x00, 0x00, 0x00, 0x00); } else if (regionComposition.depth == REGION_DEPTH_4_BIT) {
color = clutDefinition.clutEntries4Bit[regionComposition.pixelCode4Bit];
int i = 255;
while (i > 0) {
entries[i] = new ClutEntry();
if (i < 8) {
entries[i].clutRGBA(
((i & 0x01) != 0 ? 0xFF : 0x00),
((i & 0x02) != 0 ? 0xFF : 0x00),
((i & 0x04) != 0 ? 0xFF : 0x00),
0x3F);
} else { } else {
switch (i & 0x88) { color = clutDefinition.clutEntries2Bit[regionComposition.pixelCode2Bit];
case 0x00:
entries[i].clutRGBA(
(((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)),
(((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)),
(((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00)),
0xFF);
break;
case 0x08:
entries[i].clutRGBA(
(((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)),
(((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)),
(((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00)),
0x7F);
break;
case 0x80:
entries[i].clutRGBA(
(127 + ((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)),
(127 + ((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)),
(127 + ((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00)),
0xFF);
break;
case 0x88:
entries[i].clutRGBA(
(((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)),
(((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)),
(((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00)),
0xFF);
break;
}
} }
fillRegionPaint.setColor(color);
i--; canvas.drawRect(baseHorizontalAddress, baseVerticalAddress,
baseHorizontalAddress + regionComposition.width,
baseVerticalAddress + regionComposition.height,
fillRegionPaint);
} }
return entries; Bitmap cueBitmap = Bitmap.createBitmap(bitmap, baseHorizontalAddress, baseVerticalAddress,
} regionComposition.width, regionComposition.height);
cues.add(new Cue(cueBitmap, (float) baseHorizontalAddress / displayDefinition.width,
Cue.ANCHOR_TYPE_START, (float) baseVerticalAddress / displayDefinition.height,
Cue.ANCHOR_TYPE_START, (float) regionComposition.width / displayDefinition.width,
(float) regionComposition.height / displayDefinition.height));
ClutDefinition() { canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
clutEntries2bit = generateDefault2bitClut();
clutEntries4bit = generateDefault4bitClut();
clutEntries8bit = generateDefault8bitClut();
} }
return cues;
} }
/* The object data segment contains the textual/graphical representation of an object [7.2.5] */ // Static parsing.
private class ObjectData {
int pageId;
int objectId;
int objectVersionNumber;
int objectCodingMethod;
byte flags;
int topFieldDataLength;
byte[] topFieldData;
int bottomFieldDataLength;
byte[] bottomFieldData;
int numberOfCodes;
}
/** /**
* Construct a subtitle service for the given subtitle and ancillary pageIds * Parses a subtitling segment, as defined by ETSI EN 300 743 7.2
* * <p>
* @param subtitlePageId The Id of the subtitle page carrying the selected subtitle track * The {@link SubtitleService} is updated with the parsed segment data.
* @param ancillaryPageId Id of the common subtitle page containing additional data for the current
* subtitle track
* @param flags additional initialisation info to properly configure the parser
*/ */
DvbParser(int subtitlePageId, int ancillaryPageId, @Flags int flags) { private static void parseSubtitlingSegment(ParsableBitArray data, SubtitleService service) {
this.subtitleService = new SubtitleService(); int segmentType = data.readBits(8);
this.flags = flags; int pageId = data.readBits(16);
int dataFieldLength = data.readBits(16);
this.defaultPaint.setStyle(Paint.Style.FILL_AND_STROKE); int dataFieldLimit = data.getBytePosition() + dataFieldLength;
this.defaultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
this.defaultPaint.setPathEffect(null); if ((dataFieldLength * 8) > data.bitsLeft()) {
Log.w(TAG, "Data field length exceeds limit");
this.fillRegionPaint.setStyle(Paint.Style.FILL); // Skip to the very end.
this.fillRegionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); data.skipBits(data.bitsLeft());
this.fillRegionPaint.setPathEffect(null); return;
}
this.debugRegionPaint.setColor(0xff00ff00);
this.debugRegionPaint.setStyle(Paint.Style.STROKE);
this.debugRegionPaint.setPathEffect(new DashPathEffect(new float[]{2, 2}, 0));
this.debugObjectPaint.setColor(0xffff0000);
this.debugObjectPaint.setStyle(Paint.Style.STROKE);
this.debugObjectPaint.setPathEffect(new DashPathEffect(new float[]{10, 20}, 0));
this.subtitleService.subtitlePageId = subtitlePageId;
this.subtitleService.ancillaryPageId = ancillaryPageId;
this.subtitleService.displayDefinition = new DisplayDefinition();
this.subtitleService.displayDefinition.updateBitmapResolution();
}
private void parseSubtitlingSegment() {
/* Parse subtitling segment. ETSI EN 300 743 7.2
SYNTAX SIZE SEMANTICS
--------------------------------------- ---- ------------------------------------------
Subtitling_segment() {
sync_byte 8 An 8-bit field that shall be coded with the value '0000 1111'
segment_type 8 Indicates the type of data contained in the segment data field
page_id 16 Identifies the subtitle service of the data contained in this subtitling_segment
segment_length 16 Number of bytes contained in the segment_data_field
segment_data_field() This is the payload of the segment
*/
int pageId, segmentId, segmentLength;
segmentId = tsStream.readBits(8);
switch (segmentId) {
case DVBSUB_ST_DISPLAY_DEFINITION:
if (BuildConfig.DEBUG) Log.d(TAG, " Parse Display Definition segment.");
DisplayDefinition tempDisplay = parseDisplayDefinitionSegment();
if (tempDisplay != null && tempDisplay.pageId == subtitleService.subtitlePageId) {
if (tempDisplay.displayWidth != subtitleService.displayDefinition.displayWidth ||
tempDisplay.displayHeight != subtitleService.displayDefinition.displayHeight ||
tempDisplay.displayWindowHorizontalPositionMaximum != subtitleService.displayDefinition.displayWindowHorizontalPositionMaximum ||
tempDisplay.displayWindowHorizontalPositionMinimum != subtitleService.displayDefinition.displayWindowHorizontalPositionMinimum ||
tempDisplay.displayWindowVerticalPositionMaximum != subtitleService.displayDefinition.displayWindowVerticalPositionMaximum ||
tempDisplay.displayWindowVerticalPositionMinimum != subtitleService.displayDefinition.displayWindowVerticalPositionMinimum ||
tempDisplay.flags != subtitleService.displayDefinition.flags) {
subtitleService.displayDefinition = tempDisplay;
subtitleService.displayDefinition.updateBitmapResolution();
} else {
subtitleService.displayDefinition.versionNumber = tempDisplay.versionNumber;
}
if (BuildConfig.DEBUG) switch (segmentType) {
Log.d(TAG + "/DDS", " [versionNumber] = " + tempDisplay.versionNumber + case SEGMENT_TYPE_DISPLAY_DEFINITION:
" [width/height] = " + (tempDisplay.displayWidth + 1) + "/" + (tempDisplay.displayHeight + 1) + if (pageId == service.subtitlePageId) {
" Window[minX/minY/maxX/maxY] = " + tempDisplay.displayWindowHorizontalPositionMinimum + service.displayDefinition = parseDisplayDefinition(data);
"/" + tempDisplay.displayWindowVerticalPositionMinimum +
"/" + tempDisplay.displayWindowHorizontalPositionMaximum +
"/" + tempDisplay.displayWindowVerticalPositionMaximum
);
}
break;
case DVBSUB_ST_PAGE_COMPOSITION:
if (BuildConfig.DEBUG) Log.d(TAG, " Parse Page Composition segment.");
PageComposition tempPage = parsePageCompositionSegment();
if (tempPage != null && tempPage.pageId == subtitleService.subtitlePageId) {
if (tempPage.pageState == DVBSUB_PCS_STATE_NORMAL && subtitleService.pageComposition == null)
break;
subtitleService.pageComposition = tempPage;
} }
break; break;
case DVBSUB_ST_REGION_COMPOSITION: case SEGMENT_TYPE_PAGE_COMPOSITION:
if (BuildConfig.DEBUG) Log.d(TAG, " Parse Region Composition segment."); if (pageId == service.subtitlePageId) {
RegionComposition tempRegionComposition = parseRegionCompositionSegment(); PageComposition current = service.pageComposition;
if (tempRegionComposition != null && tempRegionComposition.pageId == subtitleService.subtitlePageId) { PageComposition pageComposition = parsePageComposition(data, dataFieldLength);
subtitleService.regions.put(tempRegionComposition.regionId, tempRegionComposition); if (pageComposition.state != PAGE_STATE_NORMAL) {
} service.pageComposition = pageComposition;
break; service.regions.clear();
case DVBSUB_ST_CLUT_DEFINITION: service.cluts.clear();
if (BuildConfig.DEBUG) Log.d(TAG, " Parse Clut Definition segment."); service.objects.clear();
ClutDefinition tempClutDefinition = parseClutDefinitionSegment(); } else if (current != null && current.version != pageComposition.version) {
if (tempClutDefinition != null) { service.pageComposition = pageComposition;
if (tempClutDefinition.pageId == subtitleService.subtitlePageId) {
subtitleService.cluts.put(tempClutDefinition.clutId, tempClutDefinition);
} else if (tempClutDefinition.pageId == subtitleService.ancillaryPageId) {
subtitleService.ancillaryCluts.put(tempClutDefinition.clutId, tempClutDefinition);
} }
} }
break; break;
case DVBSUB_ST_OBJECT_DATA: case SEGMENT_TYPE_REGION_COMPOSITION:
if (BuildConfig.DEBUG) Log.d(TAG, " Parse Object Data segment."); PageComposition pageComposition = service.pageComposition;
ObjectData tempObjectData = parseObjectDataSegment(); if (pageId == service.subtitlePageId && pageComposition != null) {
if (tempObjectData != null) { RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength);
if (tempObjectData.pageId == subtitleService.subtitlePageId) { if (pageComposition.state == PAGE_STATE_NORMAL) {
subtitleService.objects.put(tempObjectData.objectId, tempObjectData); regionComposition.mergeFrom(service.regions.get(regionComposition.id));
} else if (tempObjectData.pageId == subtitleService.ancillaryPageId) {
subtitleService.ancillaryObjects.put(tempObjectData.objectId, tempObjectData);
} }
service.regions.put(regionComposition.id, regionComposition);
} }
break; break;
case DVBSUB_ST_ENDOFDISPLAY: case SEGMENT_TYPE_CLUT_DEFINITION:
pageId = tsStream.readBits(16); if (pageId == service.subtitlePageId) {
segmentLength = tsStream.readBits(16); ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength);
if (BuildConfig.DEBUG) service.cluts.put(clutDefinition.id, clutDefinition);
Log.d(TAG, "pageId " + pageId + "end of display size = " + segmentLength); } else if (pageId == service.ancillaryPageId) {
tsStream.skipBits(segmentLength * 8); ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength);
service.ancillaryCluts.put(clutDefinition.id, clutDefinition);
}
break; break;
case DVBSUB_ST_STUFFING: case SEGMENT_TYPE_OBJECT_DATA:
pageId = tsStream.readBits(16); if (pageId == service.subtitlePageId) {
segmentLength = tsStream.readBits(16); ObjectData objectData = parseObjectData(data);
if (BuildConfig.DEBUG) Log.d(TAG, "pageId " + pageId + "stuffing size = " + segmentLength); service.objects.put(objectData.id, objectData);
tsStream.skipBits(segmentLength * 8); } else if (pageId == service.ancillaryPageId) {
ObjectData objectData = parseObjectData(data);
service.ancillaryObjects.put(objectData.id, objectData);
}
break; break;
default: default:
// Do nothing.
break; break;
} }
}
private DisplayDefinition parseDisplayDefinitionSegment() {
/* Parse display definition segment. ETSI EN 300 743 7.2.1
SYNTAX SIZE SEMANTICS
--------------------------------------- ---- ------------------------------------------
display_definition_segment(){
sync_byte 8 An 8-bit field that shall be coded with the value '0000 1111'
segment_type 8 Indicates the type of data contained in the segment data field
page_id 16 Identifies the subtitle service of the data contained in this subtitling_segment
segment_length 16 Number of bytes contained in the segment_data_field
dds_version_number 4 Incremented when any of the contents of this segment change
display_window_flag 1 if "1" display the subtitle in the defined window
reserved 3
display_width 16 Specifies the maximum horizontal width of the display in pixels minus 1
display_height 16 Specifies the maximum vertical height of the display in lines minus 1
if (display_window_flag == 1) { With origin in the top-left of the screen:
display_window_horizontal_position_minimum
16 Specifies the left-hand most pixel of this DVB subtitle display set
display_window_horizontal_position_maximum
16 Specifies the right-hand most pixel of this DVB subtitle display set
display_window_vertical_position_minimum
16 Specifies the upper most line of this DVB subtitle display set
display_window_vertical_position_maximum
16 Specifies the bottom line of this DVB subtitle display set
}
}
*/
DisplayDefinition display = new DisplayDefinition(); // Skip to the next segment.
data.skipBytes(dataFieldLimit - data.getBytePosition());
}
display.pageId = tsStream.readBits(16); /**
tsStream.skipBits(16); * Parses a display definition segment, as defined by ETSI EN 300 743 7.2.1.
display.versionNumber = tsStream.readBits(4); */
if (tsStream.readBits(1) == 1) { private static DisplayDefinition parseDisplayDefinition(ParsableBitArray data) {
display.flags |= DISPLAY_WINDOW_FLAG; data.skipBits(4); // dds_version_number (4).
} boolean displayWindowFlag = data.readBit();
tsStream.skipBits(3); data.skipBits(3); // Skip reserved.
display.displayWidth = tsStream.readBits(16); int width = data.readBits(16);
display.displayHeight = tsStream.readBits(16); int height = data.readBits(16);
if ((display.flags & DISPLAY_WINDOW_FLAG) != 0) {
display.displayWindowHorizontalPositionMinimum = tsStream.readBits(16); int horizontalPositionMinimum;
display.displayWindowHorizontalPositionMaximum = tsStream.readBits(16); int horizontalPositionMaximum;
display.displayWindowVerticalPositionMinimum = tsStream.readBits(16); int verticalPositionMinimum;
display.displayWindowVerticalPositionMaximum = tsStream.readBits(16); int verticalPositionMaximum;
if (displayWindowFlag) {
horizontalPositionMinimum = data.readBits(16);
horizontalPositionMaximum = data.readBits(16);
verticalPositionMinimum = data.readBits(16);
verticalPositionMaximum = data.readBits(16);
} else { } else {
display.displayWindowHorizontalPositionMinimum = 0; horizontalPositionMinimum = 0;
display.displayWindowHorizontalPositionMaximum = display.displayWidth; horizontalPositionMaximum = width;
display.displayWindowVerticalPositionMinimum = 0; verticalPositionMinimum = 0;
display.displayWindowVerticalPositionMaximum = display.displayHeight; verticalPositionMaximum = height;
} }
return display; return new DisplayDefinition(width, height, horizontalPositionMinimum,
horizontalPositionMaximum, verticalPositionMinimum, verticalPositionMaximum);
} }
private PageComposition parsePageCompositionSegment() { /**
* Parses a page composition segment, as defined by ETSI EN 300 743 7.2.2.
/* Parse page composition segment. ETSI EN 300 743 7.2.2 */
private static PageComposition parsePageComposition(ParsableBitArray data, int length) {
SYNTAX SIZE SEMANTICS int timeoutSecs = data.readBits(8);
--------------------------------------- ---- ------------------------------------------ int version = data.readBits(4);
page_composition_segment() { int state = data.readBits(2);
sync_byte 8 data.skipBits(2);
segment_type 8 int remainingLength = length - 2;
page_id 16
segment_length 16 SparseArray<PageRegion> regions = new SparseArray<>();
page_time_out 8 The period after the page instace should be erased while (remainingLength > 0) {
page_version_number 4 Incremented when any of the contents of this segment change int regionId = data.readBits(8);
page_state 2 The status of the subtitling page instance data.skipBits(8); // Skip reserved.
reserved 2 int regionHorizontalAddress = data.readBits(16);
while (processed_length < segment_length) { Page region list int regionVerticalAddress = data.readBits(16);
region_id 8 Uniquely identifies a region within a page remainingLength -= 6;
reserved 8 regions.put(regionId, new PageRegion(regionHorizontalAddress, regionVerticalAddress));
region_horizontal_address 16 Horizontal address of the top left pixel of this region
region_vertical_address 16 Vertical address of the top line of this region
}
}
*/
PageComposition page = new PageComposition();
page.pageId = tsStream.readBits(16);
int remainingSegmentLength = tsStream.readBits(16);
page.pageTimeOut = tsStream.readBits(8);
page.pageVersionNumber = tsStream.readBits(4);
page.pageState = tsStream.readBits(2);
tsStream.skipBits(2);
if (page.pageState == DVBSUB_PCS_STATE_NORMAL &&
subtitleService.pageComposition != null &&
subtitleService.pageComposition.pageId == page.pageId &&
(subtitleService.pageComposition.pageVersionNumber + 1) % 16 == page.pageVersionNumber) {
//page.pageRegions = subtitleService.pageComposition.pageRegions;
if (BuildConfig.DEBUG) {
Log.d(TAG, " Updated Page Composition. pageId: " + page.pageId +
" version: " + page.pageVersionNumber +
" timeout: " + page.pageTimeOut
);
}
} else if (subtitleService.subtitlePageId == page.pageId) {
if (BuildConfig.DEBUG) {
if (page.pageState == DVBSUB_PCS_STATE_NORMAL) {
Log.d(TAG, " FAILED Page Composition update. pageId: " + page.pageId +
" Version(Old/New): " + (subtitleService.pageComposition != null ? subtitleService.pageComposition.pageVersionNumber : "NaN") + "/" + page.pageVersionNumber);
}
}
subtitleService.pageComposition = null;
subtitleService.regions = new SparseArray<>();
subtitleService.cluts = new SparseArray<>();
subtitleService.objects = new SparseArray<>();
if (BuildConfig.DEBUG) {
if (page.pageState != DVBSUB_PCS_STATE_NORMAL) {
Log.d(TAG, " New Page Composition. pageId: " + page.pageId +
" version: " + page.pageVersionNumber +
" timeout: " + page.pageTimeOut
);
}
}
} }
remainingSegmentLength -= 2; return new PageComposition(timeoutSecs, version, state, regions);
while (remainingSegmentLength > 0) { }
PageRegion region = new PageRegion();
region.regionId = tsStream.readBits(8); /**
tsStream.skipBits(8); * Parses a region composition segment, as defined by ETSI EN 300 743 7.2.3.
region.regionHorizontalAddress = tsStream.readBits(16); */
region.regionVerticalAddress = tsStream.readBits(16); private static RegionComposition parseRegionComposition(ParsableBitArray data, int length) {
int id = data.readBits(8);
data.skipBits(4); // Skip region_version_number
boolean fillFlag = data.readBit();
data.skipBits(3); // Skip reserved.
int width = data.readBits(16);
int height = data.readBits(16);
int levelOfCompatibility = data.readBits(3);
int depth = data.readBits(3);
data.skipBits(2); // Skip reserved.
int clutId = data.readBits(8);
int pixelCode8Bit = data.readBits(8);
int pixelCode4Bit = data.readBits(4);
int pixelCode2Bit = data.readBits(2);
data.skipBits(2); // Skip reserved
int remainingLength = length - 10;
if (BuildConfig.DEBUG) { SparseArray<RegionObject> regionObjects = new SparseArray<>();
Log.d(TAG, " " + while (remainingLength > 0) {
(page.pageRegions.get(region.regionId) == null ? "New" : "Upd.") + int objectId = data.readBits(16);
" Page Region. regionId: " + region.regionId + int objectType = data.readBits(2);
" (x/y): (" + region.regionHorizontalAddress + "/" + region.regionVerticalAddress + ")"); int objectProvider = data.readBits(2);
int objectHorizontalPosition = data.readBits(12);
data.skipBits(4); // Skip reserved.
int objectVerticalPosition = data.readBits(12);
remainingLength -= 6;
int foregroundPixelCode = 0;
int backgroundPixelCode = 0;
if (objectType == 0x01 || objectType == 0x02) { // Only seems to affect to char subtitles.
foregroundPixelCode = data.readBits(8);
backgroundPixelCode = data.readBits(8);
remainingLength -= 2;
} }
page.pageRegions.put(region.regionId, region); regionObjects.put(objectId, new RegionObject(objectType, objectProvider,
objectHorizontalPosition, objectVerticalPosition, foregroundPixelCode,
remainingSegmentLength -= 6; backgroundPixelCode));
} }
return page; return new RegionComposition(id, fillFlag, width, height, levelOfCompatibility, depth, clutId,
pixelCode8Bit, pixelCode4Bit, pixelCode2Bit, regionObjects);
} }
private RegionComposition parseRegionCompositionSegment() { /**
* Parses a CLUT definition segment, as defined by ETSI EN 300 743 7.2.4.
/* Parse region composition segment. ETSI EN 300 743 7.2.3 */
private static ClutDefinition parseClutDefinition(ParsableBitArray data, int length) {
SYNTAX SIZE SEMANTICS int clutId = data.readBits(8);
--------------------------------------- ---- ------------------------------------------ data.skipBits(8); // Skip clut_version_number (4), reserved (4)
region_composition_segment() { int remainingLength = length - 2;
sync_byte 8
segment_type 8
page_id 16
segment_length 16
region_id 8 Uniquely identifies the region
region_version_number 4 Indicates the version of this region
region_fill_flag 1 If set the region is to be filled region_n-bit_pixel_code clut index
reserved 3
region_width 16 Specifies the horizontal length of this region
region_height 16 Specifies the vertical length of the region
region_level_of_compatibility 3 Code that indicates the minimum bithdepth of CLUT
region_depth 3 Identifies the intended pixel depth for this region
reserved 2
CLUT_id 8 Identifies the family of CLUTs that applies to this region
region_8-bit_pixel_code 8 Specifies the entry of the applied 8-bit CLUT as background colour
region_4-bit_pixel-code 4 Specifies the entry of the applied 4-bit CLUT as background colour
region_2-bit_pixel-code 2 Specifies the entry of the applied 2-bit CLUT as background colour
reserved 2
while (processed_length < segment_length) { list of region objects
object_id 16 Identifies an object that is shown in the region
object_type 2 Identifies the type of object
object_provider_flag 2 How this object is provided
object_horizontal_position 12 Specifies the horizontal position of the top left pixel of this object
reserved 4
object_vertical_position 12 Specifies the vertical position of the top left pixel of this object
if (object_type ==0x01 or object_type == 0x02){ UNSUPPORTED
foreground_pixel_code 8
background_pixel_code 8
}
}
}
*/
RegionComposition region = new RegionComposition();
region.pageId = tsStream.readBits(16);
int remainingSegmentLength = tsStream.readBits(16);
region.regionId = tsStream.readBits(8);
region.regionVersionNumber = tsStream.readBits(4);
if (tsStream.readBits(1) == 1) {
region.flags |= REGION_FILL_FLAG;
}
tsStream.skipBits(3);
region.regionWidth = tsStream.readBits(16);
region.regionHeight = tsStream.readBits(16);
region.regionLevelOfCompatibility = tsStream.readBits(3);
region.regionDepth = tsStream.readBits(3);
tsStream.skipBits(2);
region.clutId = tsStream.readBits(8);
tsStream.skipBits(16);
if (BuildConfig.DEBUG) {
Log.d(TAG, " New Region Composition. regionId: " + region.regionId +
" (w/h): (" + region.regionWidth + "/" + region.regionHeight + ")");
}
int arrayIndex = 0; // index by an incremental counter to allow repeating objects in one region
if (subtitleService.pageComposition != null && subtitleService.pageComposition.pageId == region.pageId &&
subtitleService.pageComposition.pageState == DVBSUB_PCS_STATE_NORMAL) {
RegionComposition tempRegion = subtitleService.regions.get(region.regionId);
if (tempRegion != null) {
region.regionObjects = tempRegion.regionObjects;
arrayIndex = region.regionObjects.size();
}
}
remainingSegmentLength -= 10;
RegionObject object;
while (remainingSegmentLength > 0) {
object = new RegionObject();
object.objectId = tsStream.readBits(16);
object.objectType = tsStream.readBits(2);
object.objectProvider = tsStream.readBits(2);
object.objectHorizontalPosition = tsStream.readBits(12);
tsStream.skipBits(4);
object.objectVerticalPosition = tsStream.readBits(12);
remainingSegmentLength -= 6;
if (object.objectType == 0x01 || object.objectType == 0x02) { // Only seems to affect to char subtitles
object.foregroundPixelCode = tsStream.readBits(8);
object.backgroundPixelCode = tsStream.readBits(8);
remainingSegmentLength -= 2;
}
if (BuildConfig.DEBUG) {
Log.d(TAG, " New Region Object[" + arrayIndex + "]." +
" objectId: " + object.objectId +
" (x/y): (" + object.objectHorizontalPosition + "/" + object.objectVerticalPosition + ")");
}
region.regionObjects.put(arrayIndex++, object);
}
return region; int[] clutEntries2Bit = generateDefault2BitClutEntries();
} int[] clutEntries4Bit = generateDefault4BitClutEntries();
int[] clutEntries8Bit = generateDefault8BitClutEntries();
private ClutDefinition parseClutDefinitionSegment() { while (remainingLength > 0) {
int entryId = data.readBits(8);
/* Parse CLUT definition segment. ETSI EN 300 743 7.2.4 int entryFlags = data.readBits(8);
remainingLength -= 2;
SYNTAX SIZE SEMANTICS
--------------------------------------- ---- ------------------------------------------
CLUT_definition_segment() {
sync_byte 8
segment_type 8
page_id 16
segment_length 16
CLUT-id 8 Uniquely identifies within a page the CLUT family
CLUT_version_number 4 Indicates the version of this segment data
reserved 4
while (processed_length < segment_length) { Clut entries list
CLUT_entry_id 8 Specifies the entry number of the CLUT
2-bit/entry_CLUT_flag 1 Indicates this entry is to be loaded into the 2-bit/entry CLUT
4-bit/entry_CLUT_flag 1 Indicates this entry is to be loaded into the 4-bit/entry CLUT
8-bit/entry_CLUT_flag 1 Indicates this entry is to be loaded into the 8-bit/entry CLUT
reserved 4
full_range_flag 1 Indicates that the Y_value, Cr_value, Cb_value and T_value
fields have the full 8-bit resolution
if full_range_flag =='1' {
Y-value 8 The Y value for this CLUT entry.
Cr-value 8 The Cr value for this CLUT entry.
Cb-value 8 The Cb value for this CLUT entry.
T-value 8 The Transparency value for this CLUT entry. 0 = no transparency
} else {
Y-value 6 The Y value for this CLUT entry.
Cr-value 4 The Cr value for this CLUT entry.
Cb-value 4 The Cb value for this CLUT entry.
T-value 2 The Transparency value for this CLUT entry. 0 = no transparency
}
}
}
*/
ClutDefinition clut = new ClutDefinition();
clut.pageId = tsStream.readBits(16);
int remainingSegmentLength = tsStream.readBits(16);
clut.clutId = tsStream.readBits(8);
clut.clutVersionNumber = tsStream.readBits(4);
tsStream.skipBits(4);
remainingSegmentLength -= 2;
ClutEntry entry;
int Y, Cb, Cr, T;
int entryId, entryFlags;
while (remainingSegmentLength > 0) {
entryId = tsStream.readBits(8);
entryFlags = tsStream.readBits(8);
int[] clutEntries;
if ((entryFlags & 0x80) != 0) { if ((entryFlags & 0x80) != 0) {
entry = clut.clutEntries2bit[entryId]; clutEntries = clutEntries2Bit;
} else if ((entryFlags & 0x40) != 0) { } else if ((entryFlags & 0x40) != 0) {
entry = clut.clutEntries4bit[entryId]; clutEntries = clutEntries4Bit;
} else { } else {
entry = clut.clutEntries8bit[entryId]; clutEntries = clutEntries8Bit;
} }
entry.flags = (byte) (entryFlags & 0xE1); int y;
if ((entry.flags & 0x01) != 0) { int cr;
Y = tsStream.readBits(8); int cb;
Cr = tsStream.readBits(8); int t;
Cb = tsStream.readBits(8); if ((entryFlags & 0x01) != 0) {
T = tsStream.readBits(8); y = data.readBits(8);
remainingSegmentLength -= 6; cr = data.readBits(8);
cb = data.readBits(8);
t = data.readBits(8);
remainingLength -= 4;
} else { } else {
Y = tsStream.readBits(6) << 2; y = data.readBits(6) << 2;
Cr = tsStream.readBits(4) << 4; cr = data.readBits(4) << 4;
Cb = tsStream.readBits(4) << 4; cb = data.readBits(4) << 4;
T = tsStream.readBits(2) << 6; t = data.readBits(2) << 6;
remainingSegmentLength -= 4; remainingLength -= 2;
} }
if (Y == 0x00) { if (y == 0x00) {
Cr = 0x00; cr = 0x00;
Cb = 0x00; cb = 0x00;
T = 0xFF; t = 0xFF;
} }
entry.clutYCbCrT(Y, Cb, Cr, T); int a = (byte) (0xFF - (t & 0xFF));
int r = (int) (y + (1.40200 * (cr - 128)));
int g = (int) (y - (0.34414 * (cb - 128)) - (0.71414 * (cr - 128)));
int b = (int) (y + (1.77200 * (cb - 128)));
clutEntries[entryId] = getColor(a, Util.constrainValue(r, 0, 255),
Util.constrainValue(g, 0, 255), Util.constrainValue(b, 0, 255));
} }
return clut;
return new ClutDefinition(clutId, clutEntries2Bit, clutEntries4Bit, clutEntries8Bit);
} }
private ObjectData parseObjectDataSegment() { /**
* Parses an object data segment, as defined by ETSI EN 300 743 7.2.5.
/* Parse object data segment. ETSI EN 300 743 7.2.5 *
* @return The parsed object data.
SYNTAX SIZE SEMANTICS */
--------------------------------------- ---- ------------------------------------------ private static ObjectData parseObjectData(ParsableBitArray data) {
object_data_segment() { int objectId = data.readBits(16);
sync_byte 8 data.skipBits(4); // Skip object_version_number
segment_type 8 int objectCodingMethod = data.readBits(2);
page_id 16 boolean nonModifyingColorFlag = data.readBit();
segment_length 16 data.skipBits(1); // Skip reserved.
object_id 16 Uniquely identifies within the page the object
object_version_number 4 Indicates the version of this segment data byte[] topFieldData = null;
object_coding_method 2 Specifies the method used to code the object byte[] bottomFieldData = null;
non_modifying_colour_flag 1 Indicates that the CLUT entry value '1' is a non modifying colour
reserved 1 if (objectCodingMethod == OBJECT_CODING_STRING) {
if (object_coding_method == '00'){ int numberOfCodes = data.readBits(8);
top_field_data_block_length 16 Specifies the number of bytes contained in his pixel-data_sub-blocks // TODO: Parse and use character_codes.
bottom_field_data_block_length 16 Specifies the number of bytes contained in his pixel-data_sub-blocks data.skipBits(numberOfCodes * 16); // Skip character_codes.
while(processed_length<top_field_data_block_length) } else if (objectCodingMethod == OBJECT_CODING_PIXELS) {
pixel-data_sub-block() int topFieldDataLength = data.readBits(16);
while (processed_length<bottom_field_data_block_length) int bottomFieldDataLength = data.readBits(16);
pixel-data_sub-block() if (topFieldDataLength > 0) {
if (!wordaligned()) topFieldData = new byte[topFieldDataLength];
8_stuff_bits 8 data.readBytes(topFieldData, 0, topFieldDataLength);
} }
if (object_coding_method == '01') { UNSUPPORTED if (bottomFieldDataLength > 0) {
number of codes 8 bottomFieldData = new byte[bottomFieldDataLength];
for (i == 1, i <= number of codes, i ++) data.readBytes(bottomFieldData, 0, bottomFieldDataLength);
character_code 16
}
}
*/
ObjectData object = new ObjectData();
object.pageId = tsStream.readBits(16);
tsStream.skipBits(16); // remainingSegmentLength
object.objectId = tsStream.readBits(16);
object.objectVersionNumber = tsStream.readBits(4);
object.objectCodingMethod = tsStream.readBits(2);
if (tsStream.readBits(1) == 1) {
object.flags |= OBJECT_NON_MODIFYING_COLOUR_FLAG;
}
tsStream.skipBits(1);
if (object.objectCodingMethod == DVBSUB_ODS_CHAR_CODED) {
/* not implemented yet */
object.numberOfCodes = tsStream.readBits(8);
tsStream.skipBits(object.numberOfCodes * 16);
} else if (object.objectCodingMethod == DVBSUB_ODS_PIXEL_CODED) {
object.topFieldDataLength = tsStream.readBits(16);
object.bottomFieldDataLength = tsStream.readBits(16);
object.topFieldData = new byte[object.topFieldDataLength];
System.arraycopy(tsStream.data, tsStream.getPosition() / 8, object.topFieldData, 0, object.topFieldDataLength);
tsStream.skipBits(object.topFieldDataLength * 8);
object.bottomFieldData = new byte[object.bottomFieldDataLength];
if (object.bottomFieldDataLength == 0) {
object.bottomFieldData = object.topFieldData;
} else { } else {
System.arraycopy(tsStream.data, tsStream.getPosition() / 8, object.bottomFieldData, 0, object.bottomFieldDataLength); bottomFieldData = topFieldData;
tsStream.skipBits(object.bottomFieldDataLength * 8);
} }
} }
return object; return new ObjectData(objectId, nonModifyingColorFlag, topFieldData, bottomFieldData);
} }
private Bitmap parsePixelDataSubBlocks(ObjectData object, ClutDefinition clut, int regionDepth, private static int[] generateDefault2BitClutEntries() {
int horizontalAddress, int verticalAddress) { int[] entries = new int[4];
entries[0] = 0x00000000;
/* Parse pixel-data sub-block. ETSI EN 300 743 7.2.5.1 entries[1] = 0xFFFFFFFF;
entries[2] = 0xFF000000;
SYNTAX SIZE entries[3] = 0xFF7F7F7F;
--------------------------------------- ---- return entries;
pixel-data_sub-block() { }
data_type 8
if data_type =='0x10' {
repeat {
2-bit/pixel_code_string()
} until (end of 2-bit/pixel_code_string)
while (!bytealigned())
2_stuff_bits 2
if data_type =='0x11' {
repeat {
4-bit/pixel_code_string()
} until (end of 4-bit/pixel_code_string)
if (!bytealigned())
4_stuff_bits 4
}
}
if data_type =='0x12' {
repeat {
8-bit/pixel_code_string()
} until (end of 8-bit/pixel_code_string)
}
if data_type =='0x20'
2_to_4-bit_map-table 16
if data_type =='0x21'
2_to_8-bit_map-table 32
if data_type =='0x22'
4_to_8-bit_map-table 128
}
*/
int line, column;
int i;
byte[] clutMapTable, clutMapTable24, clutMapTable28, clutMapTable48;
ClutEntry[] clutEntries;
if (regionDepth == DVBSUB_RCS_BITDEPTH_8) {
clutEntries = clut.clutEntries8bit;
} else if (regionDepth == DVBSUB_RCS_BITDEPTH_4) {
clutEntries = clut.clutEntries4bit;
} else {
clutEntries = clut.clutEntries2bit;
}
int lineHeight;
ParsableBitArray[] pixelData = new ParsableBitArray[2];
pixelData[0] = new ParsableBitArray(object.topFieldData);
if (object.bottomFieldDataLength == 0) {
lineHeight = 2;
} else {
lineHeight = 1;
pixelData[1] = new ParsableBitArray(object.bottomFieldData);
private static int[] generateDefault4BitClutEntries() {
int[] entries = new int[16];
entries[0] = 0x00000000;
for (int i = 1; i < entries.length; i++) {
if (i < 8) {
entries[i] = getColor(
0xFF,
((i & 0x01) != 0 ? 0xFF : 0x00),
((i & 0x02) != 0 ? 0xFF : 0x00),
((i & 0x04) != 0 ? 0xFF : 0x00));
} else {
entries[i] = getColor(
0xFF,
((i & 0x01) != 0 ? 0x7F : 0x00),
((i & 0x02) != 0 ? 0x7F : 0x00),
((i & 0x04) != 0 ? 0x7F : 0x00));
}
} }
return entries;
}
ParsableBitArray data; private static int[] generateDefault8BitClutEntries() {
int field = 0; int[] entries = new int[256];
while (field < 2) { entries[0] = 0x00000000;
data = pixelData[field]; for (int i = 0; i < entries.length; i++) {
column = horizontalAddress; if (i < 8) {
line = verticalAddress + field; entries[i] = getColor(
clutMapTable24 = null; 0x3F,
clutMapTable28 = null; ((i & 0x01) != 0 ? 0xFF : 0x00),
clutMapTable48 = null; ((i & 0x02) != 0 ? 0xFF : 0x00),
((i & 0x04) != 0 ? 0xFF : 0x00));
while (data.bitsLeft() > 0) { } else {
switch (data.readBits(8)) { switch (i & 0x88) {
case DVBSUB_DT_2BP_CODE_STRING: case 0x00:
if (regionDepth == DVBSUB_RCS_BITDEPTH_8) { entries[i] = getColor(
clutMapTable = clutMapTable28 == null ? defaultMap28 : clutMapTable28; 0xFF,
} else if (regionDepth == DVBSUB_RCS_BITDEPTH_4) { (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)),
clutMapTable = clutMapTable24 == null ? defaultMap24 : clutMapTable24; (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)),
} else { (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00)));
clutMapTable = null;
}
column += dvbSub2BitPixelCodeString(data, lineHeight, clutEntries, clutMapTable,
column, line, (object.flags & OBJECT_NON_MODIFYING_COLOUR_FLAG) == 0);
if ((i = data.getPosition() % 8) != 0) {
data.skipBits(7 - i + 1);
}
break;
case DVBSUB_DT_4BP_CODE_STRING:
if (regionDepth == DVBSUB_RCS_BITDEPTH_8)
clutMapTable = clutMapTable48 == null ? defaultMap48 : clutMapTable48;
else
clutMapTable = null;
column += dvbSub4BitPixelCodeString(data, lineHeight, clutEntries, clutMapTable,
column, line, (object.flags & OBJECT_NON_MODIFYING_COLOUR_FLAG) == 0);
if ((i = data.getPosition() % 8) != 0) {
data.skipBits(7 - i + 1);
}
break;
case DVBSUB_DT_8BP_CODE_STRING:
column += dvbSub8BitPixelCodeString(data, lineHeight, clutEntries, null,
column, line, (object.flags & OBJECT_NON_MODIFYING_COLOUR_FLAG) == 0);
break;
case DVBSUB_DT_24_TABLE_DATA:
clutMapTable24 = new byte[4];
for (i = 0; i < 4; i++) {
clutMapTable24[i] = (byte) data.readBits(4);
}
break;
case DVBSUB_DT_28_TABLE_DATA:
clutMapTable28 = new byte[4];
for (i = 0; i < 4; i++) {
clutMapTable28[i] = (byte) data.readBits(8);
}
break; break;
case DVBSUB_DT_48_TABLE_DATA: case 0x08:
clutMapTable48 = new byte[16]; entries[i] = getColor(
for (i = 0; i < 4; i++) { 0x7F,
clutMapTable48[i] = (byte) data.readBits(8); (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)),
} (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)),
(((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00)));
break; break;
case DVBSUB_DT_END_LINE: case 0x80:
column = horizontalAddress; entries[i] = getColor(
line += 2; 0xFF,
(127 + ((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)),
(127 + ((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)),
(127 + ((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00)));
break; break;
default: case 0x88:
entries[i] = getColor(
0xFF,
(((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)),
(((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)),
(((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00)));
break; break;
} }
} }
field += lineHeight;
} }
return entries;
}
return null; private static int getColor(int a, int r, int g, int b) {
return (a << 24) | (r << 16) | (g << 8) | b;
} }
private int dvbSub2BitPixelCodeString(ParsableBitArray data, int lineHeigth, // Static drawing.
ClutEntry[] clutEntries, byte[] clutMapTable,
int column, int line, boolean paint) {
/* Parse 2-bit/pixel code string. ETSI EN 300 743 7.2.5.2
SYNTAX SIZE
--------------------------------------- ----
2-bit/pixel_code_string() {
if (nextbits() != '00') {
2-bit_pixel-code 2
} else {
2-bit_zero 2
switch_1 1 bslbf
if (switch_1 == '1') {
run_length_3-10 3
2-bit_pixel-code 2
} else {
switch_2 1
if (switch_2 == '0') {
switch_3 2
if (switch_3 == '10') {
run_length_12-27 4
2-bit_pixel-code 2
}
if (switch_3 == '11') {
run_length_29-284 8
2-bit_pixel-code 2
}
}
}
}
}
*/
int savedColumn = column, peek, runLength, clutIdx = 0x00, colour; /**
boolean endOfPixelCodeString = false; * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas.
*/
private static void paintPixelDataSubBlocks(ObjectData objectData, ClutDefinition clutDefinition,
int regionDepth, int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) {
int[] clutEntries;
if (regionDepth == REGION_DEPTH_8_BIT) {
clutEntries = clutDefinition.clutEntries8Bit;
} else if (regionDepth == REGION_DEPTH_4_BIT) {
clutEntries = clutDefinition.clutEntries4Bit;
} else {
clutEntries = clutDefinition.clutEntries2Bit;
}
paintPixelDataSubBlock(objectData.topFieldData, clutEntries, regionDepth, horizontalAddress,
verticalAddress, paint, canvas);
paintPixelDataSubBlock(objectData.bottomFieldData, clutEntries, regionDepth, horizontalAddress,
verticalAddress + 1, paint, canvas);
}
while (!endOfPixelCodeString) { /**
runLength = 0; * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas.
peek = data.readBits(2); */
if (peek != 0x00) { private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, int regionDepth,
runLength = 1; int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) {
clutIdx = peek; ParsableBitArray data = new ParsableBitArray(pixelData);
} else { int column = horizontalAddress;
peek = data.readBits(1); int line = verticalAddress;
if (peek == 0x01) { byte[] clutMapTable2To4 = null;
runLength = 3 + data.readBits(3); byte[] clutMapTable2To8 = null;
clutIdx = data.readBits(2); byte[] clutMapTable4To8 = null;
} else {
peek = data.readBits(1); while (data.bitsLeft() != 0) {
if (peek == 0x00) { int dataType = data.readBits(8);
peek = data.readBits(2); switch (dataType) {
switch (peek) { case DATA_TYPE_2BP_CODE_STRING:
case 0x00: byte[] clutMapTable2ToX;
endOfPixelCodeString = true; if (regionDepth == REGION_DEPTH_8_BIT) {
break; clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8;
case 0x01: } else if (regionDepth == REGION_DEPTH_4_BIT) {
runLength = 2; clutMapTable2ToX = clutMapTable2To4 == null ? defaultMap2To4 : clutMapTable2To4;
clutIdx = 0x00; } else {
break; clutMapTable2ToX = null;
case 0x02:
runLength = 12 + data.readBits(4);
clutIdx = data.readBits(2);
break;
case 0x03:
runLength = 29 + data.readBits(8);
clutIdx = data.readBits(2);
break;
}
} }
column = paint2BitPixelCodeString(data, clutEntries, clutMapTable2ToX, column, line,
paint, canvas);
data.byteAlign();
break;
case DATA_TYPE_4BP_CODE_STRING:
byte[] clutMapTable4ToX;
if (regionDepth == REGION_DEPTH_8_BIT) {
clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8;
} else {
clutMapTable4ToX = null;
}
column = paint4BitPixelCodeString(data, clutEntries, clutMapTable4ToX, column, line,
paint, canvas);
data.byteAlign();
break;
case DATA_TYPE_8BP_CODE_STRING:
column = paint8BitPixelCodeString(data, clutEntries, null, column, line, paint, canvas);
break;
case DATA_TYPE_24_TABLE_DATA:
clutMapTable2To4 = buildClutMapTable(4, 4, data);
break;
case DATA_TYPE_28_TABLE_DATA:
clutMapTable2To8 = buildClutMapTable(4, 8, data);
break;
case DATA_TYPE_48_TABLE_DATA:
clutMapTable2To8 = buildClutMapTable(16, 8, data);
break;
case DATA_TYPE_END_LINE:
column = horizontalAddress;
line += 2;
break;
default:
// Do nothing.
break;
}
}
}
/**
* Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas.
*/
private static int paint2BitPixelCodeString(ParsableBitArray data, int[] clutEntries,
byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) {
boolean endOfPixelCodeString = false;
do {
int runLength = 0;
int clutIndex = 0;
int peek = data.readBits(2);
if (!data.readBit()) {
runLength = 1;
clutIndex = peek;
} else if (data.readBit()) {
runLength = 3 + data.readBits(3);
clutIndex = data.readBits(2);
} else if (!data.readBit()) {
switch (data.readBits(2)) {
case 0x00:
endOfPixelCodeString = true;
break;
case 0x01:
runLength = 2;
break;
case 0x02:
runLength = 12 + data.readBits(4);
clutIndex = data.readBits(2);
break;
case 0x03:
runLength = 29 + data.readBits(8);
clutIndex = data.readBits(2);
break;
} }
} }
if (runLength != 0 && paint) { if (runLength != 0 && paint != null) {
colour = clutMapTable != null ? clutEntries[clutMapTable[clutIdx]].ARGB paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);
: clutEntries[clutIdx].ARGB; canvas.drawRect(column, line, column + runLength, line + 1, paint);
defaultPaint.setColor(colour);
canvas.drawRect(
column, line, column + runLength, line + lineHeigth, defaultPaint);
} }
column += runLength; column += runLength;
} } while (!endOfPixelCodeString);
return column - savedColumn; return column;
} }
private int dvbSub4BitPixelCodeString(ParsableBitArray data, int lineHeigth, /**
ClutEntry[] clutEntries, byte[] clutMapTable, * Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas.
int column, int line, boolean paint) { */
private static int paint4BitPixelCodeString(ParsableBitArray data, int[] clutEntries,
/* Parse 4-bit/pixel code string. ETSI EN 300 743 7.2.5.2 byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) {
SYNTAX SIZE
--------------------------------------- ----
4-bit/pixel_code_string() {
if (nextbits() != '0000') {
4-bit_pixel-code 4
} else {
4-bit_zero 4
switch_1 1
if (switch_1 == '0') {
if (nextbits() != '000')
run_length_3-9 3
else
end_of_string_signal 3
} else {
switch_2 1
if (switch_2 == '0') {
run_length_4-7 2
4-bit_pixel-code 4
} else {
switch_3 2
if (switch_3 == '10') {
run_length_9-24 4
4-bit_pixel-code 4
}
if (switch_3 == '11') {
run_length_25-280 8
4-bit_pixel-code 4
}
}
}
}
}
*/
int savedColumn = column, peek, runLength, clutIdx = 0x00, colour;
boolean endOfPixelCodeString = false; boolean endOfPixelCodeString = false;
do {
while (!endOfPixelCodeString) { int runLength = 0;
runLength = 0; int clutIndex = 0;
peek = data.readBits(4); int peek = data.readBits(4);
if (peek != 0x00) { if (peek != 0x00) {
runLength = 1; runLength = 1;
clutIdx = peek; clutIndex = peek;
} else { } else if (!data.readBit()) {
peek = data.readBits(1); peek = data.readBits(3);
if (peek == 0x00) { if (peek != 0x00) {
peek = data.readBits(3); runLength = 2 + peek;
if (peek != 0x00) { clutIndex = 0x00;
runLength = 2 + peek;
clutIdx = 0x00;
} else {
endOfPixelCodeString = true;
}
} else { } else {
peek = data.readBits(1); endOfPixelCodeString = true;
if (peek == 0x00) { }
runLength = 4 + data.readBits(2); } else if (!data.readBit()) {
clutIdx = data.readBits(4); runLength = 4 + data.readBits(2);
} else { clutIndex = data.readBits(4);
peek = data.readBits(2); } else {
switch (peek) { switch (data.readBits(2)) {
case 0x00: case 0x00:
runLength = 1; runLength = 1;
clutIdx = 0x00; break;
break; case 0x01:
case 0x01: runLength = 2;
runLength = 2; break;
clutIdx = 0x00; case 0x02:
break; runLength = 9 + data.readBits(4);
case 0x02: clutIndex = data.readBits(4);
runLength = 9 + data.readBits(4); break;
clutIdx = data.readBits(4); case 0x03:
break; runLength = 25 + data.readBits(8);
case 0x03: clutIndex = data.readBits(4);
runLength = 25 + data.readBits(8); break;
clutIdx = data.readBits(4);
break;
}
}
} }
} }
if (runLength != 0 && paint) { if (runLength != 0 && paint != null) {
colour = clutMapTable != null ? clutEntries[clutMapTable[clutIdx]].ARGB paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);
: clutEntries[clutIdx].ARGB; canvas.drawRect(column, line, column + runLength, line + 1, paint);
defaultPaint.setColor(colour);
canvas.drawRect(
column, line, column + runLength, line + lineHeigth, defaultPaint);
} }
column += runLength; column += runLength;
} } while (!endOfPixelCodeString);
return column - savedColumn; return column;
} }
private int dvbSub8BitPixelCodeString(ParsableBitArray data, int lineHeigth, /**
ClutEntry[] clutEntries, byte[] clutMapTable, * Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas.
int column, int line, boolean paint) { */
private static int paint8BitPixelCodeString(ParsableBitArray data, int[] clutEntries,
/* Parse 8-bit/pixel code string. ETSI EN 300 743 7.2.5.2 byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) {
SYNTAX SIZE
--------------------------------------- ----
8-bit/pixel_code_string() {
if (nextbits() != '0000 0000') {
8-bit_pixel-code 8
} else {
8-bit_zero 8
switch_1 1
if switch_1 == '0' {
if nextbits() != '000 0000'
run_length_1-127 7
else
end_of_string_signal 7
} else {
run_length_3-127 7
8-bit_pixel-code 8
}
}
}
*/
int savedColumn = column, peek, runLength, clutIdx = 0x00, colour;
boolean endOfPixelCodeString = false; boolean endOfPixelCodeString = false;
do {
while (!endOfPixelCodeString) { int runLength = 0;
runLength = 0; int clutIndex = 0;
peek = data.readBits(8); int peek = data.readBits(8);
if (peek != 0x00) { if (peek != 0x00) {
runLength = 1; runLength = 1;
clutIdx = peek; clutIndex = peek;
} else { } else {
peek = data.readBits(1); if (!data.readBit()) {
if (peek == 0x00) {
peek = data.readBits(7); peek = data.readBits(7);
if (peek != 0x00) { if (peek != 0x00) {
runLength = peek; runLength = peek;
clutIdx = 0x00; clutIndex = 0x00;
} else { } else {
endOfPixelCodeString = true; endOfPixelCodeString = true;
} }
} else { } else {
runLength = data.readBits(7); runLength = data.readBits(7);
clutIdx = data.readBits(8); clutIndex = data.readBits(8);
} }
} }
if (runLength != 0 && paint) { if (runLength != 0 && paint != null) {
colour = clutMapTable != null ? clutEntries[clutMapTable[clutIdx]].ARGB paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);
: clutEntries[clutIdx].ARGB; canvas.drawRect(column, line, column + runLength, line + 1, paint);
defaultPaint.setColor(colour);
canvas.drawRect(
column, line, column + runLength, line + lineHeigth, defaultPaint);
} }
column += runLength; column += runLength;
} } while (!endOfPixelCodeString);
return column - savedColumn; return column;
} }
private static byte[] buildClutMapTable(int length, int bitsPerEntry, ParsableBitArray data) {
byte[] clutMapTable = new byte[length];
for (int i = 0; i < length; i++) {
clutMapTable[i] = (byte) data.readBits(bitsPerEntry);
}
return clutMapTable;
}
// Private inner classes.
/** /**
* Takes a subtitling packet, parses the included segments and returns the list of {@link Cue}s * The subtitle service definition.
* defined in them
*
* @param input
* @param inputSize
* @return list of {@link Cue}s contained in the packet or null if there is an error or subtitle
* is incomplete
*/ */
List<Cue> dvbSubsDecode(byte[] input, int inputSize) { private static final class SubtitleService {
/* process PES PACKET. ETSI EN 300 743 7.1 public final int subtitlePageId;
public final int ancillaryPageId;
SYNTAX SIZE SEMANTICS public final SparseArray<RegionComposition> regions = new SparseArray<>();
--------------------------------------- ---- ------------------------------------------ public final SparseArray<ClutDefinition> cluts = new SparseArray<>();
PES_data_field() { public final SparseArray<ObjectData> objects = new SparseArray<>();
data_identifier 8 For DVB subtitle streams it shall be 0x20 public final SparseArray<ClutDefinition> ancillaryCluts = new SparseArray<>();
subtitle_stream_id 8 For DVB subtitling stream it shall be 0x00 public final SparseArray<ObjectData> ancillaryObjects = new SparseArray<>();
while nextbits() == '0000 1111' {
Subtitling_segment()
}
end_of_PES_data_field_marker 8 An 8-bit field with fixed contents '1111 1111'
*/ public DisplayDefinition displayDefinition;
public PageComposition pageComposition;
if (input != null) { public SubtitleService(int subtitlePageId, int ancillaryPageId) {
tsStream = new ParsableBitArray(input, inputSize); this.subtitlePageId = subtitlePageId;
} else { this.ancillaryPageId = ancillaryPageId;
return null;
}
if (!isSet(FLAG_PES_STRIPPED_DVBSUB)) {
if (tsStream.readBits(8) != 0x20) { // data_identifier
return null;
}
if (tsStream.readBits(8) != 0x00) { // subtitle_stream_id
return null;
}
} }
if (BuildConfig.DEBUG) Log.d(TAG, "New PES subtitle packet."); public void reset() {
regions.clear();
cluts.clear();
objects.clear();
ancillaryCluts.clear();
ancillaryObjects.clear();
displayDefinition = null;
pageComposition = null;
}
int sync = tsStream.readBits(8); }
// test for segment Sync Byte and account for possible additional wordalign byte in Object data segment
while (sync == 0x0f || (sync == 0x00 && (sync = tsStream.readBits(8)) == 0x0f)) {
parseSubtitlingSegment();
if (isSet(FLAG_PES_STRIPPED_DVBSUB) && tsStream.bitsLeft() == 0) {
break;
}
sync = tsStream.readBits(8);
/**
* Contains the geometry and active area of the subtitle service.
* <p>
* See ETSI EN 300 743 7.2.1
*/
private static final class DisplayDefinition {
public final int width;
public final int height;
public final int horizontalPositionMinimum;
public final int horizontalPositionMaximum;
public final int verticalPositionMinimum;
public final int verticalPositionMaximum;
public DisplayDefinition(int width, int height, int horizontalPositionMinimum,
int horizontalPositionMaximum, int verticalPositionMinimum, int verticalPositionMaximum) {
this.width = width;
this.height = height;
this.horizontalPositionMinimum = horizontalPositionMinimum;
this.horizontalPositionMaximum = horizontalPositionMaximum;
this.verticalPositionMinimum = verticalPositionMinimum;
this.verticalPositionMaximum = verticalPositionMaximum;
} }
if (sync == 0xff || (isSet(FLAG_PES_STRIPPED_DVBSUB) && tsStream.bitsLeft() == 0)) { // end_of_PES_data_field_marker }
// paint the current Subtitle definition
if (subtitleService.pageComposition != null) {
List<Cue> cueList = new ArrayList<>();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Rendering subtitle. w: " + subtitleService.displayDefinition.displayWidth +
" h: " + subtitleService.displayDefinition.displayHeight);
if ((subtitleService.displayDefinition.flags & DISPLAY_WINDOW_FLAG) != 0) {
Log.d(TAG, " Window dimensions (x/y/w/h): (" +
subtitleService.displayDefinition.displayWindowHorizontalPositionMinimum + "/" +
subtitleService.displayDefinition.displayWindowVerticalPositionMinimum + "/" +
(subtitleService.displayDefinition.displayWindowHorizontalPositionMaximum -
subtitleService.displayDefinition.displayWindowHorizontalPositionMinimum) + "/" +
(subtitleService.displayDefinition.displayWindowVerticalPositionMaximum -
subtitleService.displayDefinition.displayWindowVerticalPositionMinimum) + ")");
}
}
int a, b; /**
PageRegion pageRegion; * The page is the definition and arrangement of regions in the screen.
RegionComposition regionComposition; * <p>
int baseHorizontalAddress, baseVerticalAddress; * See ETSI EN 300 743 7.2.2
ObjectData object; */
ClutDefinition clut; private static final class PageComposition {
int regionKey;
// process page regions public final int timeOutSecs; // TODO: Use this or remove it.
for (a = 0; a < subtitleService.pageComposition.pageRegions.size(); a++) { public final int version;
regionKey = subtitleService.pageComposition.pageRegions.keyAt(a); public final int state;
pageRegion = subtitleService.pageComposition.pageRegions.get(regionKey); public final SparseArray<PageRegion> regions;
regionComposition = subtitleService.regions.get(regionKey);
public PageComposition(int timeoutSecs, int version, int state,
baseHorizontalAddress = pageRegion.regionHorizontalAddress; SparseArray<PageRegion> regions) {
baseVerticalAddress = pageRegion.regionVerticalAddress; this.timeOutSecs = timeoutSecs;
this.version = version;
if ((subtitleService.displayDefinition.flags & DISPLAY_WINDOW_FLAG) != 0) { this.state = state;
baseHorizontalAddress += this.regions = regions;
subtitleService.displayDefinition.displayWindowHorizontalPositionMinimum; }
baseVerticalAddress +=
subtitleService.displayDefinition.displayWindowVerticalPositionMinimum;
}
// clip object drawing to the current region and display definition window }
canvas.clipRect(
baseHorizontalAddress, baseVerticalAddress,
Math.min(baseHorizontalAddress + regionComposition.regionWidth,
subtitleService.displayDefinition.displayWindowHorizontalPositionMaximum),
Math.min(baseVerticalAddress + regionComposition.regionHeight,
subtitleService.displayDefinition.displayWindowVerticalPositionMaximum),
Region.Op.REPLACE);
if ((clut = subtitleService.cluts.get(regionComposition.clutId)) == null) {
if ((clut = subtitleService.ancillaryCluts.get(regionComposition.clutId)) == null) {
clut = defaultClut;
}
}
if (BuildConfig.DEBUG) { /**
Log.d(TAG, " Region: " + regionKey + " (x/y/w/h): (" + * A region within a {@link PageComposition}.
baseHorizontalAddress + "/" + baseVerticalAddress + "/" + * <p>
(baseHorizontalAddress + regionComposition.regionWidth - 1) + "/" + * See ETSI EN 300 743 7.2.2
(baseVerticalAddress + regionComposition.regionHeight - 1) + ")" */
); private static final class PageRegion {
canvas.drawRect(
baseHorizontalAddress, baseVerticalAddress,
baseHorizontalAddress + regionComposition.regionWidth - 1,
baseVerticalAddress + regionComposition.regionHeight - 1,
debugRegionPaint);
}
RegionObject regionObject; public final int horizontalAddress;
int objectKey; public final int verticalAddress;
// process regions compositions
for (b = 0; b < regionComposition.regionObjects.size(); b++) {
objectKey = regionComposition.regionObjects.keyAt(b);
regionObject = regionComposition.regionObjects.get(objectKey);
if (BuildConfig.DEBUG) {
Log.d(TAG, " Object[" + objectKey + "]. objectId: " + regionObject.objectId + " (x/y): (" +
(baseHorizontalAddress + regionObject.objectHorizontalPosition) + "/" +
(baseVerticalAddress + regionObject.objectVerticalPosition) + ")"
);
canvas.drawRect(
baseHorizontalAddress + regionObject.objectHorizontalPosition,
baseVerticalAddress + regionObject.objectVerticalPosition,
baseHorizontalAddress + regionObject.objectHorizontalPosition + regionComposition.regionWidth - 1,
baseVerticalAddress + regionObject.objectVerticalPosition + regionComposition.regionHeight - 1,
debugObjectPaint);
}
if ((object = subtitleService.objects.get(regionObject.objectId)) == null) {
if ((object = subtitleService.ancillaryObjects.get(regionObject.objectId)) == null) {
continue;
}
}
parsePixelDataSubBlocks(object, clut, regionComposition.regionDepth,
baseHorizontalAddress + regionObject.objectHorizontalPosition,
baseVerticalAddress + regionObject.objectVerticalPosition);
} public PageRegion(int horizontalAddress, int verticalAddress) {
this.horizontalAddress = horizontalAddress;
this.verticalAddress = verticalAddress;
}
// fill the region if needed }
if ((regionComposition.flags & REGION_FILL_FLAG) != 0) {
int colour;
if (regionComposition.regionDepth == DVBSUB_RCS_BITDEPTH_8) {
colour = clut.clutEntries8bit[regionComposition.region8bitPixelCode].ARGB;
} else if (regionComposition.regionDepth == DVBSUB_RCS_BITDEPTH_4) {
colour = clut.clutEntries4bit[regionComposition.region4bitPixelCode].ARGB;
} else {
colour = clut.clutEntries2bit[regionComposition.region2bitPixelCode].ARGB;
}
fillRegionPaint.setColor(colour);
canvas.drawRect(
baseHorizontalAddress, baseVerticalAddress,
baseHorizontalAddress + regionComposition.regionWidth,
baseVerticalAddress + regionComposition.regionHeight,
fillRegionPaint);
}
Bitmap cueBitmap = Bitmap.createBitmap(bitmap, /**
baseHorizontalAddress, baseVerticalAddress, * An area of the page composed of a list of objects and a CLUT.
regionComposition.regionWidth, regionComposition.regionHeight); * <p>
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); * See ETSI EN 300 743 7.2.3
regionComposition.cue = new Cue(cueBitmap, */
(float) baseHorizontalAddress / subtitleService.displayDefinition.displayWidth, Cue.ANCHOR_TYPE_START, private static final class RegionComposition {
(float) baseVerticalAddress / subtitleService.displayDefinition.displayHeight, Cue.ANCHOR_TYPE_START,
(float) regionComposition.regionWidth / subtitleService.displayDefinition.displayWidth, public final int id;
(float) regionComposition.regionHeight / subtitleService.displayDefinition.displayHeight); public final boolean fillFlag;
cueList.add(regionComposition.cue); public final int width;
} public final int height;
public final int levelOfCompatibility; // TODO: Use this or remove it.
public final int depth;
public final int clutId;
public final int pixelCode8Bit;
public final int pixelCode4Bit;
public final int pixelCode2Bit;
public final SparseArray<RegionObject> regionObjects;
public RegionComposition(int id, boolean fillFlag, int width, int height,
int levelOfCompatibility, int depth, int clutId, int pixelCode8Bit, int pixelCode4Bit,
int pixelCode2Bit, SparseArray<RegionObject> regionObjects) {
this.id = id;
this.fillFlag = fillFlag;
this.width = width;
this.height = height;
this.levelOfCompatibility = levelOfCompatibility;
this.depth = depth;
this.clutId = clutId;
this.pixelCode8Bit = pixelCode8Bit;
this.pixelCode4Bit = pixelCode4Bit;
this.pixelCode2Bit = pixelCode2Bit;
this.regionObjects = regionObjects;
}
return cueList; public void mergeFrom(RegionComposition otherRegionComposition) {
if (otherRegionComposition == null) {
return;
}
SparseArray<RegionObject> otherRegionObjects = otherRegionComposition.regionObjects;
for (int i = 0; i < otherRegionObjects.size(); i++) {
regionObjects.put(otherRegionObjects.keyAt(i), otherRegionObjects.valueAt(i));
} }
} else {
Log.d(TAG, "Unexpected...");
} }
return null;
} }
private boolean isSet(@Flags int flag) { /**
return (flags & flag) != 0; * An object within a {@link RegionComposition}.
* <p>
* See ETSI EN 300 743 7.2.3
*/
private static final class RegionObject {
public final int type; // TODO: Use this or remove it.
public final int provider; // TODO: Use this or remove it.
public final int horizontalPosition;
public final int verticalPosition;
public final int foregroundPixelCode; // TODO: Use this or remove it.
public final int backgroundPixelCode; // TODO: Use this or remove it.
public RegionObject(int type, int provider, int horizontalPosition,
int verticalPosition, int foregroundPixelCode, int backgroundPixelCode) {
this.type = type;
this.provider = provider;
this.horizontalPosition = horizontalPosition;
this.verticalPosition = verticalPosition;
this.foregroundPixelCode = foregroundPixelCode;
this.backgroundPixelCode = backgroundPixelCode;
}
}
/**
* CLUT family definition containing the color tables for the three bit depths defined
* <p>
* See ETSI EN 300 743 7.2.4
*/
private static final class ClutDefinition {
public final int id;
public final int[] clutEntries2Bit;
public final int[] clutEntries4Bit;
public final int[] clutEntries8Bit;
public ClutDefinition(int id, int[] clutEntries2Bit, int[] clutEntries4Bit,
int[] clutEntries8bit) {
this.id = id;
this.clutEntries2Bit = clutEntries2Bit;
this.clutEntries4Bit = clutEntries4Bit;
this.clutEntries8Bit = clutEntries8bit;
}
}
/**
* The textual or graphical representation of an object.
* <p>
* See ETSI EN 300 743 7.2.5
*/
private static final class ObjectData {
public final int id;
public final boolean nonModifyingColorFlag;
public final byte[] topFieldData;
public final byte[] bottomFieldData;
public ObjectData(int id, boolean nonModifyingColorFlag, byte[] topFieldData,
byte[] bottomFieldData) {
this.id = id;
this.nonModifyingColorFlag = nonModifyingColorFlag;
this.topFieldData = topFieldData;
this.bottomFieldData = bottomFieldData;
}
} }
} }
...@@ -18,41 +18,37 @@ package com.google.android.exoplayer2.text.dvb; ...@@ -18,41 +18,37 @@ package com.google.android.exoplayer2.text.dvb;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.Subtitle;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
* A representation of a DVB subtitle. * A representation of a DVB subtitle.
*/ */
/* package */ final class DvbSubtitle implements Subtitle { /* package */ final class DvbSubtitle implements Subtitle {
private final List<Cue> cues;
private final List<Cue> cues;
public DvbSubtitle(List<Cue> cues) {
if (cues == null) { public DvbSubtitle(List<Cue> cues) {
this.cues = Collections.emptyList(); this.cues = cues;
} else { }
this.cues = cues;
} @Override
} public int getNextEventTimeIndex(long timeUs) {
return C.INDEX_UNSET;
@Override }
public int getNextEventTimeIndex(long timeUs) {
return C.INDEX_UNSET; @Override
} public int getEventTimeCount() {
return 1;
@Override }
public int getEventTimeCount() {
return 1; @Override
} public long getEventTime(int index) {
return 0;
@Override }
public long getEventTime(int index) {
return 0; @Override
} public List<Cue> getCues(long timeUs) {
return cues;
@Override }
public List<Cue> getCues(long timeUs) {
return cues; }
}
}
\ No newline at end of file
...@@ -46,7 +46,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { ...@@ -46,7 +46,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
} }
@Override @Override
protected SubripSubtitle decode(byte[] bytes, int length) { protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) {
ArrayList<Cue> cues = new ArrayList<>(); ArrayList<Cue> cues = new ArrayList<>();
LongArray cueTimesUs = new LongArray(); LongArray cueTimesUs = new LongArray();
ParsableByteArray subripData = new ParsableByteArray(bytes, length); ParsableByteArray subripData = new ParsableByteArray(bytes, length);
......
...@@ -94,7 +94,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -94,7 +94,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
} }
@Override @Override
protected TtmlSubtitle decode(byte[] bytes, int length) throws SubtitleDecoderException { protected TtmlSubtitle decode(byte[] bytes, int length, boolean reset)
throws SubtitleDecoderException {
try { try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser(); XmlPullParser xmlParser = xmlParserFactory.newPullParser();
Map<String, TtmlStyle> globalStyles = new HashMap<>(); Map<String, TtmlStyle> globalStyles = new HashMap<>();
......
...@@ -35,7 +35,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { ...@@ -35,7 +35,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
} }
@Override @Override
protected Subtitle decode(byte[] bytes, int length) { protected Subtitle decode(byte[] bytes, int length, boolean reset) {
parsableByteArray.reset(bytes, length); parsableByteArray.reset(bytes, length);
int textLength = parsableByteArray.readUnsignedShort(); int textLength = parsableByteArray.readUnsignedShort();
if (textLength == 0) { if (textLength == 0) {
......
...@@ -45,7 +45,8 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { ...@@ -45,7 +45,8 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
} }
@Override @Override
protected Mp4WebvttSubtitle decode(byte[] bytes, int length) throws SubtitleDecoderException { protected Mp4WebvttSubtitle decode(byte[] bytes, int length, boolean reset)
throws SubtitleDecoderException {
// Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing: // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing:
// first 4 bytes size and then 4 bytes type. // first 4 bytes size and then 4 bytes type.
sampleData.reset(bytes, length); sampleData.reset(bytes, length);
......
...@@ -54,7 +54,8 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { ...@@ -54,7 +54,8 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
} }
@Override @Override
protected WebvttSubtitle decode(byte[] bytes, int length) throws SubtitleDecoderException { protected WebvttSubtitle decode(byte[] bytes, int length, boolean reset)
throws SubtitleDecoderException {
parsableWebvttData.reset(bytes, length); parsableWebvttData.reset(bytes, length);
// Initialization for consistent starting state. // Initialization for consistent starting state.
webvttCueBuilder.reset(); webvttCueBuilder.reset();
......
...@@ -90,6 +90,16 @@ public final class ParsableBitArray { ...@@ -90,6 +90,16 @@ public final class ParsableBitArray {
} }
/** /**
* Returns the current byte offset. Must only be called when the position is byte aligned.
*
* @throws IllegalStateException If the position isn't byte aligned.
*/
public int getBytePosition() {
Assertions.checkState(bitOffset == 0);
return byteOffset;
}
/**
* Sets the current bit offset. * Sets the current bit offset.
* *
* @param position The position to set. * @param position The position to set.
...@@ -177,6 +187,47 @@ public final class ParsableBitArray { ...@@ -177,6 +187,47 @@ public final class ParsableBitArray {
return returnValue; return returnValue;
} }
/**
* Aligns the position to the next byte boundary. Does nothing if the position is already aligned.
*/
public void byteAlign() {
if (bitOffset == 0) {
return;
}
bitOffset = 0;
byteOffset++;
assertValidOffset();
}
/**
* Reads the next {@code length} bytes into {@code buffer}. Must only be called when the position
* is byte aligned.
*
* @see System#arraycopy(Object, int, Object, int, int)
* @param buffer The array into which the read data should be written.
* @param offset The offset in {@code buffer} at which the read data should be written.
* @param length The number of bytes to read.
* @throws IllegalStateException If the position isn't byte aligned.
*/
public void readBytes(byte[] buffer, int offset, int length) {
Assertions.checkState(bitOffset == 0);
System.arraycopy(data, byteOffset, buffer, offset, length);
byteOffset += length;
assertValidOffset();
}
/**
* Skips the next {@code length} bytes. Must only be called when the position is byte aligned.
*
* @param length The number of bytes to read.
* @throws IllegalStateException If the position isn't byte aligned.
*/
public void skipBytes(int length) {
Assertions.checkState(bitOffset == 0);
byteOffset += length;
assertValidOffset();
}
private void assertValidOffset() { private void assertValidOffset() {
// It is fine for position to be at the end of the array, but no further. // It is fine for position to be at the end of the array, but no further.
Assertions.checkState(byteOffset >= 0 Assertions.checkState(byteOffset >= 0
......
...@@ -315,7 +315,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -315,7 +315,7 @@ import com.google.android.exoplayer2.util.Util;
float anchorX = parentLeft + (parentWidth * cuePosition); float anchorX = parentLeft + (parentWidth * cuePosition);
float anchorY = parentTop + (parentHeight * cueLine); float anchorY = parentTop + (parentHeight * cueLine);
int width = Math.round(parentWidth * cueSize); int width = Math.round(parentWidth * cueSize);
int height = cueBitmapHeight != -1 ? Math.round(parentHeight * cueBitmapHeight) int height = cueBitmapHeight != Cue.DIMEN_UNSET ? Math.round(parentHeight * cueBitmapHeight)
: Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); : Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth()));
int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width)
: cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment