From 156bc52c8f73154bd847c352acb9227324ff7301 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 6 Apr 2017 19:05:59 +0100 Subject: [PATCH] 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. --- library/core/proguard-rules.txt | 2 +- .../text/subrip/SubripDecoderTest.java | 16 +- .../exoplayer2/text/ttml/TtmlDecoderTest.java | 2 +- .../text/webvtt/Mp4WebvttDecoderTest.java | 8 +- .../text/webvtt/WebvttDecoderTest.java | 4 +- .../extractor/mkv/MatroskaExtractor.java | 5 +- .../ts/DefaultTsPayloadReaderFactory.java | 3 +- .../extractor/ts/DvbSubtitleReader.java | 111 + .../extractor/ts/DvbSubtitlesReader.java | 133 - .../exoplayer2/extractor/ts/TsExtractor.java | 11 +- .../extractor/ts/TsPayloadReader.java | 7 +- .../google/android/exoplayer2/text/Cue.java | 22 +- .../text/SimpleSubtitleDecoder.java | 6 +- .../text/SubtitleDecoderFactory.java | 4 +- .../exoplayer2/text/dvb/DvbDecoder.java | 36 +- .../exoplayer2/text/dvb/DvbParser.java | 2188 +++++++---------- .../exoplayer2/text/dvb/DvbSubtitle.java | 60 +- .../exoplayer2/text/subrip/SubripDecoder.java | 2 +- .../exoplayer2/text/ttml/TtmlDecoder.java | 3 +- .../exoplayer2/text/tx3g/Tx3gDecoder.java | 2 +- .../text/webvtt/Mp4WebvttDecoder.java | 3 +- .../exoplayer2/text/webvtt/WebvttDecoder.java | 3 +- .../exoplayer2/util/ParsableBitArray.java | 51 + .../exoplayer2/ui/SubtitlePainter.java | 2 +- 24 files changed, 1091 insertions(+), 1593 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitlesReader.java diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 34d881f8913..c5d752f4c64 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -7,4 +7,4 @@ } -keepclassmembers class com.google.android.exoplayer2.text.dvb.DvbDecoder { public (java.util.List); -} \ No newline at end of file +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java index 502fa9a7892..880a214fb30 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java @@ -36,7 +36,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { public void testDecodeEmpty() throws IOException { SubripDecoder decoder = new SubripDecoder(); 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. assertEquals(0, subtitle.getEventTimeCount()); assertTrue(subtitle.getCues(0).isEmpty()); @@ -45,7 +45,7 @@ public void testDecodeEmpty() throws IOException { public void testDecodeTypical() throws IOException { SubripDecoder decoder = new SubripDecoder(); 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()); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -55,7 +55,7 @@ public void testDecodeTypical() throws IOException { public void testDecodeTypicalWithByteOrderMark() throws IOException { SubripDecoder decoder = new SubripDecoder(); 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()); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -65,7 +65,7 @@ public void testDecodeTypicalWithByteOrderMark() throws IOException { public void testDecodeTypicalExtraBlankLine() throws IOException { SubripDecoder decoder = new SubripDecoder(); 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()); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -76,7 +76,7 @@ public void testDecodeTypicalMissingTimecode() throws IOException { // Parsing should succeed, parsing the first and third cues only. SubripDecoder decoder = new SubripDecoder(); 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()); assertTypicalCue1(subtitle, 0); assertTypicalCue3(subtitle, 2); @@ -86,7 +86,7 @@ public void testDecodeTypicalMissingSequence() throws IOException { // Parsing should succeed, parsing the first and third cues only. SubripDecoder decoder = new SubripDecoder(); 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()); assertTypicalCue1(subtitle, 0); assertTypicalCue3(subtitle, 2); @@ -96,7 +96,7 @@ public void testDecodeTypicalNegativeTimestamps() throws IOException { // Parsing should succeed, parsing the third cue only. SubripDecoder decoder = new SubripDecoder(); 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()); assertTypicalCue3(subtitle, 0); } @@ -104,7 +104,7 @@ public void testDecodeTypicalNegativeTimestamps() throws IOException { public void testDecodeNoEndTimecodes() throws IOException { SubripDecoder decoder = new SubripDecoder(); 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. assertEquals(3, subtitle.getEventTimeCount()); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index d6017750091..381aaa34aef 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -482,7 +482,7 @@ private TtmlNode queryChildrenForTag(TtmlNode node, String tag, int pos) { private TtmlSubtitle getSubtitle(String file) throws IOException, SubtitleDecoderException { TtmlDecoder ttmlDecoder = new TtmlDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), file); - return ttmlDecoder.decode(bytes, bytes.length); + return ttmlDecoder.decode(bytes, bytes.length, false); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java index a0feaea57df..2cdad081c53 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java @@ -81,14 +81,14 @@ public final class Mp4WebvttDecoderTest extends TestCase { public void testSingleCueSample() throws SubtitleDecoderException { 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 assertMp4WebvttSubtitleEquals(result, expectedCue); } public void testTwoCuesSample() throws SubtitleDecoderException { 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 secondExpectedCue = new Cue("Bye Bye"); assertMp4WebvttSubtitleEquals(result, firstExpectedCue, secondExpectedCue); @@ -96,7 +96,7 @@ public void testTwoCuesSample() throws SubtitleDecoderException { public void testNoCueSample() throws SubtitleDecoderException { 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); } @@ -105,7 +105,7 @@ public void testNoCueSample() throws SubtitleDecoderException { public void testSampleWithIncompleteHeader() { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); try { - decoder.decode(INCOMPLETE_HEADER_SAMPLE, INCOMPLETE_HEADER_SAMPLE.length); + decoder.decode(INCOMPLETE_HEADER_SAMPLE, INCOMPLETE_HEADER_SAMPLE.length, false); } catch (SubtitleDecoderException e) { return; } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 6ed0518e3ca..e48a2b8b03b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -49,7 +49,7 @@ public void testDecodeEmpty() throws IOException { WebvttDecoder decoder = new WebvttDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE); try { - decoder.decode(bytes, bytes.length); + decoder.decode(bytes, bytes.length, false); fail(); } catch (SubtitleDecoderException expected) { // Do nothing. @@ -194,7 +194,7 @@ private WebvttSubtitle getSubtitleForTestAsset(String asset) throws IOException, SubtitleDecoderException { WebvttDecoder decoder = new WebvttDecoder(); 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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 31b2c41f85d..cf31a0bbd59 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -1465,8 +1465,9 @@ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserE break; case CODEC_ID_DVBSUB: mimeType = MimeTypes.APPLICATION_DVBSUBS; - initializationData = Collections.singletonList(new byte[] { - (byte) 0x01, codecPrivate[0], codecPrivate[1], codecPrivate[2], codecPrivate[3]}); + // Init data: composition_page (2), ancillary_page (2) + initializationData = Collections.singletonList(new byte[] {codecPrivate[0], + codecPrivate[1], codecPrivate[2], codecPrivate[3]}); break; default: throw new ParserException("Unrecognized codec identifier."); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index d2b0acca85c..1e391c3eca6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -110,7 +110,8 @@ public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { case TsExtractor.TS_STREAM_TYPE_ID3: return new PesReader(new Id3Reader()); case TsExtractor.TS_STREAM_TYPE_DVBSUBS: - return new PesReader(new DvbSubtitlesReader(esInfo)); + return new PesReader( + new DvbSubtitleReader(esInfo.language, esInfo.dvbSubtitleInitializationData)); default: return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java new file mode 100644 index 00000000000..f228432a771 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Collections; +import java.util.List; + +/** + * Parses DVB subtitle data and extracts individual frames. + */ +public final class DvbSubtitleReader implements ElementaryStreamReader { + + private final String language; + private final List initializationData; + + private TrackOutput output; + private boolean writingSample; + private int bytesToCheck; + private int sampleBytesWritten; + private long sampleTimeUs; + + /** + * @param language The subtitle language code. + * @param initializationData Initialization data to be included in the track {@link Format}. + */ + public DvbSubtitleReader(String language, byte[] initializationData) { + this.language = language; + this.initializationData = Collections.singletonList(initializationData); + } + + @Override + public void seek() { + writingSample = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + this.output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + output.format(Format.createImageSampleFormat(idGenerator.getFormatId(), + MimeTypes.APPLICATION_DVBSUBS, null, Format.NO_VALUE, initializationData, language, null)); + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + if (!dataAlignmentIndicator) { + return; + } + writingSample = true; + sampleTimeUs = pesTimeUs; + sampleBytesWritten = 0; + bytesToCheck = 2; + } + + @Override + public void packetFinished() { + if (writingSample) { + output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null); + writingSample = false; + } + } + + @Override + public void consume(ParsableByteArray data) { + if (writingSample) { + if (bytesToCheck == 2 && !checkNextByte(data, 0x20)) { + // Failed to check data_identifier + return; + } + if (bytesToCheck == 1 && !checkNextByte(data, 0x00)) { + // Check and discard the subtitle_stream_id + return; + } + int bytesAvailable = data.bytesLeft(); + output.sampleData(data, bytesAvailable); + sampleBytesWritten += bytesAvailable; + } + } + + private boolean checkNextByte(ParsableByteArray data, int expectedValue) { + if (data.bytesLeft() == 0) { + return false; + } + if (data.readUnsignedByte() != expectedValue) { + writingSample = false; + } + bytesToCheck--; + return writingSample; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitlesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitlesReader.java deleted file mode 100644 index 240bfb987d8..00000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitlesReader.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.extractor.ts; - - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.ExtractorOutput; -import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.ParsableByteArray; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - - -/** - * Output PES packets to a {@link TrackOutput}. - */ -public final class DvbSubtitlesReader implements ElementaryStreamReader { - - private class SubtitleTrack { - private String language; - private List initializationData; - } - - private List subtitles = new ArrayList<>(); - - private long sampleTimeUs; - private int sampleBytesWritten; - private boolean writingSample; - - private List outputTracks = new ArrayList<>(); - - public DvbSubtitlesReader(TsPayloadReader.EsInfo esInfo) { - int pos = 2; - - while (pos < esInfo.descriptorBytes.length) { - SubtitleTrack subtitle = new SubtitleTrack(); - 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 - public void seek() { - writingSample = false; - } - - @Override - public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - TrackOutput output; - SubtitleTrack subtitle; - - for (int i = 0; i < subtitles.size(); i++) { - 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 - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { - if (!dataAlignmentIndicator) { - return; - } - writingSample = true; - sampleTimeUs = pesTimeUs; - sampleBytesWritten = 0; - } - - @Override - public void packetFinished() { - TrackOutput output; - - for (int i = 0; i < outputTracks.size(); i++) { - output = outputTracks.get(i); - output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null); - } - writingSample = false; - } - - @Override - public void consume(ParsableByteArray data) { - if (writingSample) { - int bytesAvailable = data.bytesLeft(); - TrackOutput output; - int dataPosition = data.getPosition(); - - for (int i = 0; i < outputTracks.size(); i++) { - data.setPosition(dataPosition); - output = outputTracks.get(i); - output.sampleData(data, bytesAvailable); - } - - sampleBytesWritten += bytesAvailable; - } - } -} \ No newline at end of file diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index f80c470b593..e242414ff28 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -92,7 +92,7 @@ public Extractor[] createExtractors() { 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_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_SYNC_BYTE = 0x47; // First byte of each TS packet. @@ -408,7 +408,7 @@ public void consume(ParsableByteArray sectionData) { if (mode == MODE_HLS && id3Reader == null) { // Setup an ID3 track regardless of whether there's a corresponding entry, in case one // 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.init(timestampAdjuster, output, new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); @@ -478,6 +478,7 @@ private EsInfo readEsInfo(ParsableByteArray data, int length) { int descriptorsEndPosition = descriptorsStartPosition + length; int streamType = -1; String language = null; + byte[] dvbSubtitleInitializationData = null; while (data.getPosition() < descriptorsEndPosition) { int descriptorTag = data.readUnsignedByte(); int descriptorLength = data.readUnsignedByte(); @@ -503,12 +504,16 @@ private EsInfo readEsInfo(ParsableByteArray data, int length) { } else if (descriptorTag == TS_PMT_DESC_DVBSUBS) { streamType = TS_STREAM_TYPE_DVBSUBS; 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. data.skipBytes(positionOfNextDescriptor - data.getPosition()); } data.setPosition(descriptorsEndPosition); - return new EsInfo(streamType, language, + return new EsInfo(streamType, language, dvbSubtitleInitializationData, Arrays.copyOfRange(data.data, descriptorsStartPosition, descriptorsEndPosition)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index 4169e0f3a0b..a6ebf770b41 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -60,17 +60,22 @@ final class EsInfo { public final int streamType; public final String language; + public final byte[] dvbSubtitleInitializationData; public final byte[] descriptorBytes; /** * @param streamType The type of the stream as defined by the * {@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 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. */ - public EsInfo(int streamType, String language, byte[] descriptorBytes) { + public EsInfo(int streamType, String language, byte[] dvbSubtitleInitializationData, + byte[] descriptorBytes) { this.streamType = streamType; this.language = language; + this.dvbSubtitleInitializationData = dvbSubtitleInitializationData; this.descriptorBytes = descriptorBytes; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index f31cf4387d3..5ae1f35b7e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -168,8 +168,9 @@ public class Cue { public final float size; /** - * The bitmap height as a fraction of the of the viewport size, or -1 if the bitmap should be - * displayed at its natural height given for its specified {@link #size}. + * The bitmap height as a fraction of the of the viewport size, or {@link #DIMEN_UNSET} if the + * bitmap should be displayed at its natural height given the bitmap dimensions and the specified + * {@link #size}. */ public final float bitmapHeight; @@ -195,14 +196,15 @@ public class Cue { * fraction of the viewport height. * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, * {@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 height 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 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, - float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float - height) { + float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float height) { 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 Cue(CharSequence text, Alignment textAlignment, float line, @LineType int * @param windowColor See {@link #windowColor}. */ public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, - @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, - boolean windowColorSet, int windowColor) { + @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, + boolean windowColorSet, int windowColor) { 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, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index dd25ef83454..6955f775ddc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -67,7 +67,7 @@ protected final SubtitleDecoderException decode(SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) { try { 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); // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). outputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); @@ -82,9 +82,11 @@ protected final SubtitleDecoderException decode(SubtitleInputBuffer inputBuffer, * * @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 reset Whether the decoder must be reset before decoding. * @return The decoded {@link Subtitle}. * @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; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index 83f5c0a2906..5f318916b51 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder; import com.google.android.exoplayer2.text.webvtt.WebvttDecoder; import com.google.android.exoplayer2.util.MimeTypes; - import java.util.List; /** @@ -86,7 +85,8 @@ public SubtitleDecoder createDecoder(Format format) { return clazz.asSubclass(SubtitleDecoder.class).getConstructor(Integer.TYPE) .newInstance(format.accessibilityChannel); } 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 { return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java index 6b2d3dc5e3f..dbdc0434a1f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.text.dvb; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; - +import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.List; /** @@ -26,27 +26,25 @@ public final class DvbDecoder extends SimpleSubtitleDecoder { 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 initializationData) { super("DvbDecoder"); - - int subtitleCompositionPage = 1; - int subtitleAncillaryPage = 1; - int flags = 0; - 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); + ParsableByteArray data = new ParsableByteArray(initializationData.get(0)); + int subtitleCompositionPage = data.readUnsignedShort(); + int subtitleAncillaryPage = data.readUnsignedShort(); + parser = new DvbParser(subtitleCompositionPage, subtitleAncillaryPage); } @Override - protected DvbSubtitle decode(byte[] data, int length) { - return new DvbSubtitle(parser.dvbSubsDecode(data, length)); + protected DvbSubtitle decode(byte[] data, int length, boolean reset) { + if (reset) { + parser.reset(); + } + return new DvbSubtitle(parser.decode(data, length)); } -} \ No newline at end of file + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java index e2254cf0077..96c8a89801d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -20,1550 +20,1006 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; -import android.graphics.DashPathEffect; import android.graphics.PorterDuffXfermode; import android.graphics.Region; -import android.support.annotation.IntDef; import android.util.Log; import android.util.SparseArray; - -import com.google.android.exoplayer2.core.BuildConfig; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.ParsableBitArray; - +import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; +import java.util.Collections; 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 { - - private static final String TAG = "DVBSubs"; - - @IntDef(flag = true, value = {FLAG_PES_STRIPPED_DVBSUB}) - public @interface Flags { - } - - public static final int FLAG_PES_STRIPPED_DVBSUB = 1; - - @Flags - private final int flags; - - /* List of different SEGMENT TYPES */ - /* According to EN 300-743, table 2 */ - private final static int DVBSUB_ST_PAGE_COMPOSITION = 0x10; - private final static int DVBSUB_ST_REGION_COMPOSITION = 0x11; - private final static int DVBSUB_ST_CLUT_DEFINITION = 0x12; - private final static int DVBSUB_ST_OBJECT_DATA = 0x13; - private final static int DVBSUB_ST_DISPLAY_DEFINITION = 0x14; - private final static int DVBSUB_ST_ENDOFDISPLAY = 0x80; - private final static int DVBSUB_ST_STUFFING = 0xff; - - /* List of different Page Composition Segment state */ - /* According to EN 300-743, 7.2.1 table 3 */ - private final static int DVBSUB_PCS_STATE_NORMAL = 0b00; // Update. Only changed elements. - private final static int DVBSUB_PCS_STATE_ACQUISITION = 0b01; // Refresh. All subtitle elements. - private final static int DVBSUB_PCS_STATE_CHANGE = 0b10; // New. All subtitle elements. - - /* List of different Region Composition Segments CLUT level oc compatibility */ - /* According to EN 300-743, 7.2.1 table 4 */ - private final static int DVBSUB_RCS_CLUT_2 = 0x01; - private final static int DVBSUB_RCS_CLUT_4 = 0x02; - private final static int DVBSUB_RCS_CLUT_8 = 0x03; - - /* List of different Region Composition Segments bit depths */ - /* According to EN 300-743, 7.2.1 table 5 */ - private final static int DVBSUB_RCS_BITDEPTH_2 = 0x01; - private final static int DVBSUB_RCS_BITDEPTH_4 = 0x02; - private final static int DVBSUB_RCS_BITDEPTH_8 = 0x03; - - /* 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, +/* package */ final class DvbParser { + + private static final String TAG = "DvbParser"; + + // Segment types, as defined by ETSI EN 300 743 Table 2 + 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; + private static final int SEGMENT_TYPE_OBJECT_DATA = 0x13; + private static final int SEGMENT_TYPE_DISPLAY_DEFINITION = 0x14; + + // Page states, as defined by ETSI EN 300 743 Table 3 + private static final int PAGE_STATE_NORMAL = 0; // Update. Only changed elements. + // private static final int PAGE_STATE_ACQUISITION = 1; // Refresh. All elements. + // private static final int PAGE_STATE_CHANGE = 2; // New. All elements. + + // Region depths, as defined by ETSI EN 300 743 Table 5 + // private static final int REGION_DEPTH_2_BIT = 1; + private static final int REGION_DEPTH_4_BIT = 2; + private static final int REGION_DEPTH_8_BIT = 3; + + // Object codings, as defined by ETSI EN 300 743 Table 8 + private static final int OBJECT_CODING_PIXELS = 0; + private static final int OBJECT_CODING_STRING = 1; + + // Pixel-data types, as defined by ETSI EN 300 743 Table 9 + private static final int DATA_TYPE_2BP_CODE_STRING = 0x10; + private static final int DATA_TYPE_4BP_CODE_STRING = 0x11; + private static final int DATA_TYPE_8BP_CODE_STRING = 0x12; + private static final int DATA_TYPE_24_TABLE_DATA = 0x20; + private static final int DATA_TYPE_28_TABLE_DATA = 0x21; + private static final int DATA_TYPE_48_TABLE_DATA = 0x22; + private static final int DATA_TYPE_END_LINE = 0xF0; + + // Clut mapping tables, as defined by ETSI EN 300 743 10.4, 10.5, 10.6 + private static final byte[] defaultMap2To4 = { + (byte) 0x00, (byte) 0x07, (byte) 0x08, (byte) 0x0F}; + private static final byte[] defaultMap2To8 = { + (byte) 0x00, (byte) 0x77, (byte) 0x88, (byte) 0xFF}; + private static final byte[] defaultMap4To8 = { + (byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, - (byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb, - (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; + (byte) 0x88, (byte) 0x99, (byte) 0xAA, (byte) 0xBB, + (byte) 0xCC, (byte) 0xDD, (byte) 0xEE, (byte) 0xFF}; - 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 Canvas canvas = new Canvas(); - private ClutDefinition defaultClut = new ClutDefinition(); - private static ParsableBitArray tsStream; - private SubtitleService subtitleService; - - /* - * Contains the current subtitle service definition + /** + * 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. + * @param ancillaryPageId The id of the ancillary page containing additional data. */ - private class SubtitleService { - int subtitlePageId; - int ancillaryPageId; - - // subtitle page - DisplayDefinition displayDefinition; - PageComposition pageComposition; - SparseArray regions = new SparseArray<>(); - SparseArray cluts = new SparseArray<>(); - SparseArray objects = new SparseArray<>(); - - // ancillary page - SparseArray ancillaryCluts = new SparseArray<>(); - SparseArray ancillaryObjects = new SparseArray<>(); - } - - /* 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 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 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; + public DvbParser(int subtitlePageId, int ancillaryPageId) { + defaultPaint = new Paint(); + defaultPaint.setStyle(Paint.Style.FILL_AND_STROKE); + defaultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + defaultPaint.setPathEffect(null); + fillRegionPaint = new Paint(); + fillRegionPaint.setStyle(Paint.Style.FILL); + fillRegionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); + fillRegionPaint.setPathEffect(null); + canvas = new Canvas(); + defaultDisplayDefinition = new DisplayDefinition(719, 575, 0, 719, 0, 575); + defaultClutDefinition = new ClutDefinition(0, generateDefault2BitClutEntries(), + generateDefault4BitClutEntries(), generateDefault8BitClutEntries()); + subtitleService = new SubtitleService(subtitlePageId, ancillaryPageId); } - private class RegionObject { - int objectId; - int objectType; - int objectProvider; - int objectHorizontalPosition; - int objectVerticalPosition; - int foregroundPixelCode; - int backgroundPixelCode; + /** + * Resets the parser. + */ + public void reset() { + subtitleService.reset(); } - /* An entry in the palette CLUT and associated color space translation methods */ - private class ClutEntry { - int clutEntryId; - byte flags; - byte Y; - byte Cr; - byte Cb; - byte T; - - byte A; - byte R; - byte G; - byte B; - 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); - + /** + * Decodes a subtitling packet, returning a list of parsed {@link Cue}s. + * + * @param data The subtitling packet data to decode. + * @param limit The limit in {@code data} at which to stop decoding. + * @return The parsed {@link Cue}s. + */ + public List decode(byte[] data, int limit) { + // Parse the input data. + ParsableBitArray dataBitArray = new ParsableBitArray(data, limit); + while (dataBitArray.bitsLeft() >= 48 // sync_byte (8) + segment header (40) + && dataBitArray.readBits(8) == 0x0F) { + parseSubtitlingSegment(dataBitArray, subtitleService); } - void clutRGBA(int R, int G, int B, int A) { - - 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)); + if (subtitleService.pageComposition == null) { + return Collections.emptyList(); } - } - /* CLUT family definition containing the color tables for the three bitdepths defined [7.2.4] */ - private class ClutDefinition { - int pageId; - int clutId; - int clutVersionNumber; - ClutEntry[] clutEntries2bit; - ClutEntry[] clutEntries4bit; - ClutEntry[] clutEntries8bit; - - 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; + // Update the canvas bitmap if necessary. + DisplayDefinition displayDefinition = subtitleService.displayDefinition != null + ? subtitleService.displayDefinition : defaultDisplayDefinition; + if (bitmap == null || displayDefinition.width + 1 != bitmap.getWidth() + || displayDefinition.height + 1 != bitmap.getHeight()) { + bitmap = Bitmap.createBitmap(displayDefinition.width + 1, displayDefinition.height + 1, + Bitmap.Config.ARGB_8888); + canvas.setBitmap(bitmap); } - ClutEntry[] generateDefault4bitClut() { - ClutEntry[] entries = new ClutEntry[16]; - - entries[0] = new ClutEntry(); - entries[0].clutRGBA(0x00, 0x00, 0x00, 0x00); - - int i = 15; - 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), - 0xFF); - } else { - entries[i].clutRGBA( - ((i & 0x01) != 0 ? 0x7F : 0x00), - ((i & 0x02) != 0 ? 0x7F : 0x00), - ((i & 0x04) != 0 ? 0x7F : 0x00), - 0xFF); + // Build the cues. + List cues = new ArrayList<>(); + SparseArray pageRegions = subtitleService.pageComposition.regions; + for (int i = 0; i < pageRegions.size(); i++) { + PageRegion pageRegion = pageRegions.valueAt(i); + int regionId = pageRegions.keyAt(i); + RegionComposition regionComposition = subtitleService.regions.get(regionId); + + // Clip drawing to the current region and display definition window. + int baseHorizontalAddress = pageRegion.horizontalAddress + + displayDefinition.horizontalPositionMinimum; + int baseVerticalAddress = pageRegion.verticalAddress + + displayDefinition.verticalPositionMinimum; + int clipRight = Math.min(baseHorizontalAddress + regionComposition.width, + displayDefinition.horizontalPositionMaximum); + int clipBottom = Math.min(baseVerticalAddress + regionComposition.height, + displayDefinition.verticalPositionMaximum); + canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom, + Region.Op.REPLACE); + + 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 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() { - ClutEntry[] entries = new ClutEntry[256]; - - entries[0] = new ClutEntry(); - entries[0].clutRGBA(0x00, 0x00, 0x00, 0x00); - - 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); + if (regionComposition.fillFlag) { + int color; + if (regionComposition.depth == REGION_DEPTH_8_BIT) { + color = clutDefinition.clutEntries8Bit[regionComposition.pixelCode8Bit]; + } else if (regionComposition.depth == REGION_DEPTH_4_BIT) { + color = clutDefinition.clutEntries4Bit[regionComposition.pixelCode4Bit]; } else { - switch (i & 0x88) { - 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; - } - + color = clutDefinition.clutEntries2Bit[regionComposition.pixelCode2Bit]; } - - i--; + fillRegionPaint.setColor(color); + 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() { - clutEntries2bit = generateDefault2bitClut(); - clutEntries4bit = generateDefault4bitClut(); - clutEntries8bit = generateDefault8bitClut(); + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); } + return cues; } - /* The object data segment contains the textual/graphical representation of an object [7.2.5] */ - private class ObjectData { - int pageId; - int objectId; - int objectVersionNumber; - int objectCodingMethod; - byte flags; - int topFieldDataLength; - byte[] topFieldData; - int bottomFieldDataLength; - byte[] bottomFieldData; - int numberOfCodes; - } + // Static parsing. /** - * Construct a subtitle service for the given subtitle and ancillary pageIds - * - * @param subtitlePageId The Id of the subtitle page carrying the selected subtitle track - * @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 + * Parses a subtitling segment, as defined by ETSI EN 300 743 7.2 + *

+ * The {@link SubtitleService} is updated with the parsed segment data. */ - DvbParser(int subtitlePageId, int ancillaryPageId, @Flags int flags) { - this.subtitleService = new SubtitleService(); - this.flags = flags; - - this.defaultPaint.setStyle(Paint.Style.FILL_AND_STROKE); - this.defaultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); - this.defaultPaint.setPathEffect(null); - - this.fillRegionPaint.setStyle(Paint.Style.FILL); - this.fillRegionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); - this.fillRegionPaint.setPathEffect(null); - - 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; - } + private static void parseSubtitlingSegment(ParsableBitArray data, SubtitleService service) { + int segmentType = data.readBits(8); + int pageId = data.readBits(16); + int dataFieldLength = data.readBits(16); + int dataFieldLimit = data.getBytePosition() + dataFieldLength; + + if ((dataFieldLength * 8) > data.bitsLeft()) { + Log.w(TAG, "Data field length exceeds limit"); + // Skip to the very end. + data.skipBits(data.bitsLeft()); + return; + } - if (BuildConfig.DEBUG) - Log.d(TAG + "/DDS", " [versionNumber] = " + tempDisplay.versionNumber + - " [width/height] = " + (tempDisplay.displayWidth + 1) + "/" + (tempDisplay.displayHeight + 1) + - " Window[minX/minY/maxX/maxY] = " + tempDisplay.displayWindowHorizontalPositionMinimum + - "/" + 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; + switch (segmentType) { + case SEGMENT_TYPE_DISPLAY_DEFINITION: + if (pageId == service.subtitlePageId) { + service.displayDefinition = parseDisplayDefinition(data); } break; - case DVBSUB_ST_REGION_COMPOSITION: - if (BuildConfig.DEBUG) Log.d(TAG, " Parse Region Composition segment."); - RegionComposition tempRegionComposition = parseRegionCompositionSegment(); - if (tempRegionComposition != null && tempRegionComposition.pageId == subtitleService.subtitlePageId) { - subtitleService.regions.put(tempRegionComposition.regionId, tempRegionComposition); - } - break; - case DVBSUB_ST_CLUT_DEFINITION: - if (BuildConfig.DEBUG) Log.d(TAG, " Parse Clut Definition segment."); - ClutDefinition tempClutDefinition = parseClutDefinitionSegment(); - if (tempClutDefinition != null) { - if (tempClutDefinition.pageId == subtitleService.subtitlePageId) { - subtitleService.cluts.put(tempClutDefinition.clutId, tempClutDefinition); - } else if (tempClutDefinition.pageId == subtitleService.ancillaryPageId) { - subtitleService.ancillaryCluts.put(tempClutDefinition.clutId, tempClutDefinition); + case SEGMENT_TYPE_PAGE_COMPOSITION: + if (pageId == service.subtitlePageId) { + PageComposition current = service.pageComposition; + PageComposition pageComposition = parsePageComposition(data, dataFieldLength); + if (pageComposition.state != PAGE_STATE_NORMAL) { + service.pageComposition = pageComposition; + service.regions.clear(); + service.cluts.clear(); + service.objects.clear(); + } else if (current != null && current.version != pageComposition.version) { + service.pageComposition = pageComposition; } } break; - case DVBSUB_ST_OBJECT_DATA: - if (BuildConfig.DEBUG) Log.d(TAG, " Parse Object Data segment."); - ObjectData tempObjectData = parseObjectDataSegment(); - if (tempObjectData != null) { - if (tempObjectData.pageId == subtitleService.subtitlePageId) { - subtitleService.objects.put(tempObjectData.objectId, tempObjectData); - } else if (tempObjectData.pageId == subtitleService.ancillaryPageId) { - subtitleService.ancillaryObjects.put(tempObjectData.objectId, tempObjectData); + case SEGMENT_TYPE_REGION_COMPOSITION: + PageComposition pageComposition = service.pageComposition; + if (pageId == service.subtitlePageId && pageComposition != null) { + RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength); + if (pageComposition.state == PAGE_STATE_NORMAL) { + regionComposition.mergeFrom(service.regions.get(regionComposition.id)); } + service.regions.put(regionComposition.id, regionComposition); } break; - case DVBSUB_ST_ENDOFDISPLAY: - pageId = tsStream.readBits(16); - segmentLength = tsStream.readBits(16); - if (BuildConfig.DEBUG) - Log.d(TAG, "pageId " + pageId + "end of display size = " + segmentLength); - tsStream.skipBits(segmentLength * 8); + case SEGMENT_TYPE_CLUT_DEFINITION: + if (pageId == service.subtitlePageId) { + ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength); + service.cluts.put(clutDefinition.id, clutDefinition); + } else if (pageId == service.ancillaryPageId) { + ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength); + service.ancillaryCluts.put(clutDefinition.id, clutDefinition); + } break; - case DVBSUB_ST_STUFFING: - pageId = tsStream.readBits(16); - segmentLength = tsStream.readBits(16); - if (BuildConfig.DEBUG) Log.d(TAG, "pageId " + pageId + "stuffing size = " + segmentLength); - tsStream.skipBits(segmentLength * 8); + case SEGMENT_TYPE_OBJECT_DATA: + if (pageId == service.subtitlePageId) { + ObjectData objectData = parseObjectData(data); + service.objects.put(objectData.id, objectData); + } else if (pageId == service.ancillaryPageId) { + ObjectData objectData = parseObjectData(data); + service.ancillaryObjects.put(objectData.id, objectData); + } break; default: + // Do nothing. 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); - display.versionNumber = tsStream.readBits(4); - if (tsStream.readBits(1) == 1) { - display.flags |= DISPLAY_WINDOW_FLAG; - } - tsStream.skipBits(3); - display.displayWidth = tsStream.readBits(16); - display.displayHeight = tsStream.readBits(16); - if ((display.flags & DISPLAY_WINDOW_FLAG) != 0) { - display.displayWindowHorizontalPositionMinimum = tsStream.readBits(16); - display.displayWindowHorizontalPositionMaximum = tsStream.readBits(16); - display.displayWindowVerticalPositionMinimum = tsStream.readBits(16); - display.displayWindowVerticalPositionMaximum = tsStream.readBits(16); + /** + * Parses a display definition segment, as defined by ETSI EN 300 743 7.2.1. + */ + private static DisplayDefinition parseDisplayDefinition(ParsableBitArray data) { + data.skipBits(4); // dds_version_number (4). + boolean displayWindowFlag = data.readBit(); + data.skipBits(3); // Skip reserved. + int width = data.readBits(16); + int height = data.readBits(16); + + int horizontalPositionMinimum; + int horizontalPositionMaximum; + int verticalPositionMinimum; + int verticalPositionMaximum; + if (displayWindowFlag) { + horizontalPositionMinimum = data.readBits(16); + horizontalPositionMaximum = data.readBits(16); + verticalPositionMinimum = data.readBits(16); + verticalPositionMaximum = data.readBits(16); } else { - display.displayWindowHorizontalPositionMinimum = 0; - display.displayWindowHorizontalPositionMaximum = display.displayWidth; - display.displayWindowVerticalPositionMinimum = 0; - display.displayWindowVerticalPositionMaximum = display.displayHeight; + horizontalPositionMinimum = 0; + horizontalPositionMaximum = width; + verticalPositionMinimum = 0; + verticalPositionMaximum = height; } - return display; + return new DisplayDefinition(width, height, horizontalPositionMinimum, + horizontalPositionMaximum, verticalPositionMinimum, verticalPositionMaximum); } - private PageComposition parsePageCompositionSegment() { - - /* Parse page composition segment. ETSI EN 300 743 7.2.2 - - SYNTAX SIZE SEMANTICS - --------------------------------------- ---- ------------------------------------------ - page_composition_segment() { - sync_byte 8 - segment_type 8 - page_id 16 - segment_length 16 - page_time_out 8 The period after the page instace should be erased - page_version_number 4 Incremented when any of the contents of this segment change - page_state 2 The status of the subtitling page instance - reserved 2 - while (processed_length < segment_length) { Page region list - region_id 8 Uniquely identifies a region within a page - reserved 8 - 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 - ); - } - } + /** + * Parses a page composition segment, as defined by ETSI EN 300 743 7.2.2. + */ + private static PageComposition parsePageComposition(ParsableBitArray data, int length) { + int timeoutSecs = data.readBits(8); + int version = data.readBits(4); + int state = data.readBits(2); + data.skipBits(2); + int remainingLength = length - 2; + + SparseArray regions = new SparseArray<>(); + while (remainingLength > 0) { + int regionId = data.readBits(8); + data.skipBits(8); // Skip reserved. + int regionHorizontalAddress = data.readBits(16); + int regionVerticalAddress = data.readBits(16); + remainingLength -= 6; + regions.put(regionId, new PageRegion(regionHorizontalAddress, regionVerticalAddress)); } - remainingSegmentLength -= 2; - while (remainingSegmentLength > 0) { - PageRegion region = new PageRegion(); + return new PageComposition(timeoutSecs, version, state, regions); + } - region.regionId = tsStream.readBits(8); - tsStream.skipBits(8); - region.regionHorizontalAddress = tsStream.readBits(16); - region.regionVerticalAddress = tsStream.readBits(16); + /** + * Parses a region composition segment, as defined by ETSI EN 300 743 7.2.3. + */ + 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) { - Log.d(TAG, " " + - (page.pageRegions.get(region.regionId) == null ? "New" : "Upd.") + - " Page Region. regionId: " + region.regionId + - " (x/y): (" + region.regionHorizontalAddress + "/" + region.regionVerticalAddress + ")"); + SparseArray regionObjects = new SparseArray<>(); + while (remainingLength > 0) { + int objectId = data.readBits(16); + int objectType = data.readBits(2); + 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); - - remainingSegmentLength -= 6; + regionObjects.put(objectId, new RegionObject(objectType, objectProvider, + objectHorizontalPosition, objectVerticalPosition, foregroundPixelCode, + backgroundPixelCode)); } - return page; + return new RegionComposition(id, fillFlag, width, height, levelOfCompatibility, depth, clutId, + pixelCode8Bit, pixelCode4Bit, pixelCode2Bit, regionObjects); } - private RegionComposition parseRegionCompositionSegment() { - - /* Parse region composition segment. ETSI EN 300 743 7.2.3 - - SYNTAX SIZE SEMANTICS - --------------------------------------- ---- ------------------------------------------ - region_composition_segment() { - 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); - } - + /** + * Parses a CLUT definition segment, as defined by ETSI EN 300 743 7.2.4. + */ + private static ClutDefinition parseClutDefinition(ParsableBitArray data, int length) { + int clutId = data.readBits(8); + data.skipBits(8); // Skip clut_version_number (4), reserved (4) + int remainingLength = length - 2; - return region; - } + int[] clutEntries2Bit = generateDefault2BitClutEntries(); + int[] clutEntries4Bit = generateDefault4BitClutEntries(); + int[] clutEntries8Bit = generateDefault8BitClutEntries(); - private ClutDefinition parseClutDefinitionSegment() { - - /* Parse CLUT definition segment. ETSI EN 300 743 7.2.4 - - 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); + while (remainingLength > 0) { + int entryId = data.readBits(8); + int entryFlags = data.readBits(8); + remainingLength -= 2; + int[] clutEntries; if ((entryFlags & 0x80) != 0) { - entry = clut.clutEntries2bit[entryId]; + clutEntries = clutEntries2Bit; } else if ((entryFlags & 0x40) != 0) { - entry = clut.clutEntries4bit[entryId]; + clutEntries = clutEntries4Bit; } else { - entry = clut.clutEntries8bit[entryId]; + clutEntries = clutEntries8Bit; } - entry.flags = (byte) (entryFlags & 0xE1); - if ((entry.flags & 0x01) != 0) { - Y = tsStream.readBits(8); - Cr = tsStream.readBits(8); - Cb = tsStream.readBits(8); - T = tsStream.readBits(8); - remainingSegmentLength -= 6; + int y; + int cr; + int cb; + int t; + if ((entryFlags & 0x01) != 0) { + y = data.readBits(8); + cr = data.readBits(8); + cb = data.readBits(8); + t = data.readBits(8); + remainingLength -= 4; } else { - Y = tsStream.readBits(6) << 2; - Cr = tsStream.readBits(4) << 4; - Cb = tsStream.readBits(4) << 4; - T = tsStream.readBits(2) << 6; - remainingSegmentLength -= 4; + y = data.readBits(6) << 2; + cr = data.readBits(4) << 4; + cb = data.readBits(4) << 4; + t = data.readBits(2) << 6; + remainingLength -= 2; } - if (Y == 0x00) { - Cr = 0x00; - Cb = 0x00; - T = 0xFF; + if (y == 0x00) { + cr = 0x00; + cb = 0x00; + 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() { - - /* Parse object data segment. ETSI EN 300 743 7.2.5 - - SYNTAX SIZE SEMANTICS - --------------------------------------- ---- ------------------------------------------ - object_data_segment() { - sync_byte 8 - segment_type 8 - page_id 16 - segment_length 16 - object_id 16 Uniquely identifies within the page the object - object_version_number 4 Indicates the version of this segment data - object_coding_method 2 Specifies the method used to code the object - non_modifying_colour_flag 1 Indicates that the CLUT entry value '1' is a non modifying colour - reserved 1 - if (object_coding_method == '00'){ - top_field_data_block_length 16 Specifies the number of bytes contained in his pixel-data_sub-blocks - bottom_field_data_block_length 16 Specifies the number of bytes contained in his pixel-data_sub-blocks - while(processed_length 0) { + topFieldData = new byte[topFieldDataLength]; + data.readBytes(topFieldData, 0, topFieldDataLength); + } + if (bottomFieldDataLength > 0) { + bottomFieldData = new byte[bottomFieldDataLength]; + data.readBytes(bottomFieldData, 0, bottomFieldDataLength); } else { - System.arraycopy(tsStream.data, tsStream.getPosition() / 8, object.bottomFieldData, 0, object.bottomFieldDataLength); - tsStream.skipBits(object.bottomFieldDataLength * 8); + bottomFieldData = topFieldData; } } - return object; + return new ObjectData(objectId, nonModifyingColorFlag, topFieldData, bottomFieldData); } - private Bitmap parsePixelDataSubBlocks(ObjectData object, ClutDefinition clut, int regionDepth, - int horizontalAddress, int verticalAddress) { - - /* Parse pixel-data sub-block. ETSI EN 300 743 7.2.5.1 - - SYNTAX SIZE - --------------------------------------- ---- - 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[] generateDefault2BitClutEntries() { + int[] entries = new int[4]; + entries[0] = 0x00000000; + entries[1] = 0xFFFFFFFF; + entries[2] = 0xFF000000; + entries[3] = 0xFF7F7F7F; + return entries; + } + 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; - int field = 0; - while (field < 2) { - data = pixelData[field]; - column = horizontalAddress; - line = verticalAddress + field; - clutMapTable24 = null; - clutMapTable28 = null; - clutMapTable48 = null; - - while (data.bitsLeft() > 0) { - switch (data.readBits(8)) { - case DVBSUB_DT_2BP_CODE_STRING: - if (regionDepth == DVBSUB_RCS_BITDEPTH_8) { - clutMapTable = clutMapTable28 == null ? defaultMap28 : clutMapTable28; - } else if (regionDepth == DVBSUB_RCS_BITDEPTH_4) { - clutMapTable = clutMapTable24 == null ? defaultMap24 : clutMapTable24; - } else { - 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); - } + private static int[] generateDefault8BitClutEntries() { + int[] entries = new int[256]; + entries[0] = 0x00000000; + for (int i = 0; i < entries.length; i++) { + if (i < 8) { + entries[i] = getColor( + 0x3F, + ((i & 0x01) != 0 ? 0xFF : 0x00), + ((i & 0x02) != 0 ? 0xFF : 0x00), + ((i & 0x04) != 0 ? 0xFF : 0x00)); + } else { + switch (i & 0x88) { + case 0x00: + entries[i] = getColor( + 0xFF, + (((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; - case DVBSUB_DT_48_TABLE_DATA: - clutMapTable48 = new byte[16]; - for (i = 0; i < 4; i++) { - clutMapTable48[i] = (byte) data.readBits(8); - } + case 0x08: + entries[i] = getColor( + 0x7F, + (((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; - case DVBSUB_DT_END_LINE: - column = horizontalAddress; - line += 2; + case 0x80: + entries[i] = getColor( + 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; - 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; } } - 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, - 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 - } - } - } - } - } - */ + // Static drawing. - 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; - peek = data.readBits(2); - if (peek != 0x00) { - runLength = 1; - clutIdx = peek; - } else { - peek = data.readBits(1); - if (peek == 0x01) { - runLength = 3 + data.readBits(3); - clutIdx = data.readBits(2); - } else { - peek = data.readBits(1); - if (peek == 0x00) { - peek = data.readBits(2); - switch (peek) { - case 0x00: - endOfPixelCodeString = true; - break; - case 0x01: - runLength = 2; - clutIdx = 0x00; - break; - case 0x02: - runLength = 12 + data.readBits(4); - clutIdx = data.readBits(2); - break; - case 0x03: - runLength = 29 + data.readBits(8); - clutIdx = data.readBits(2); - break; - } + /** + * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. + */ + private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, int regionDepth, + int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + ParsableBitArray data = new ParsableBitArray(pixelData); + int column = horizontalAddress; + int line = verticalAddress; + byte[] clutMapTable2To4 = null; + byte[] clutMapTable2To8 = null; + byte[] clutMapTable4To8 = null; + + while (data.bitsLeft() != 0) { + int dataType = data.readBits(8); + switch (dataType) { + case DATA_TYPE_2BP_CODE_STRING: + byte[] clutMapTable2ToX; + if (regionDepth == REGION_DEPTH_8_BIT) { + clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8; + } else if (regionDepth == REGION_DEPTH_4_BIT) { + clutMapTable2ToX = clutMapTable2To4 == null ? defaultMap2To4 : clutMapTable2To4; + } else { + clutMapTable2ToX = null; } + 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) { - colour = clutMapTable != null ? clutEntries[clutMapTable[clutIdx]].ARGB - : clutEntries[clutIdx].ARGB; - defaultPaint.setColor(colour); - canvas.drawRect( - column, line, column + runLength, line + lineHeigth, defaultPaint); + if (runLength != 0 && paint != null) { + paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]); + canvas.drawRect(column, line, column + runLength, line + 1, paint); } column += runLength; - } + } while (!endOfPixelCodeString); - return column - savedColumn; + return column; } - private int dvbSub4BitPixelCodeString(ParsableBitArray data, int lineHeigth, - ClutEntry[] clutEntries, byte[] clutMapTable, - int column, int line, boolean paint) { - - /* Parse 4-bit/pixel code string. ETSI EN 300 743 7.2.5.2 - - 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; + /** + * Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. + */ + private static int paint4BitPixelCodeString(ParsableBitArray data, int[] clutEntries, + byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { boolean endOfPixelCodeString = false; - - while (!endOfPixelCodeString) { - runLength = 0; - peek = data.readBits(4); + do { + int runLength = 0; + int clutIndex = 0; + int peek = data.readBits(4); if (peek != 0x00) { runLength = 1; - clutIdx = peek; - } else { - peek = data.readBits(1); - if (peek == 0x00) { - peek = data.readBits(3); - if (peek != 0x00) { - runLength = 2 + peek; - clutIdx = 0x00; - } else { - endOfPixelCodeString = true; - } + clutIndex = peek; + } else if (!data.readBit()) { + peek = data.readBits(3); + if (peek != 0x00) { + runLength = 2 + peek; + clutIndex = 0x00; } else { - peek = data.readBits(1); - if (peek == 0x00) { - runLength = 4 + data.readBits(2); - clutIdx = data.readBits(4); - } else { - peek = data.readBits(2); - switch (peek) { - case 0x00: - runLength = 1; - clutIdx = 0x00; - break; - case 0x01: - runLength = 2; - clutIdx = 0x00; - break; - case 0x02: - runLength = 9 + data.readBits(4); - clutIdx = data.readBits(4); - break; - case 0x03: - runLength = 25 + data.readBits(8); - clutIdx = data.readBits(4); - break; - } - } + endOfPixelCodeString = true; + } + } else if (!data.readBit()) { + runLength = 4 + data.readBits(2); + clutIndex = data.readBits(4); + } else { + switch (data.readBits(2)) { + case 0x00: + runLength = 1; + break; + case 0x01: + runLength = 2; + break; + case 0x02: + runLength = 9 + data.readBits(4); + clutIndex = data.readBits(4); + break; + case 0x03: + runLength = 25 + data.readBits(8); + clutIndex = data.readBits(4); + break; } } - if (runLength != 0 && paint) { - colour = clutMapTable != null ? clutEntries[clutMapTable[clutIdx]].ARGB - : clutEntries[clutIdx].ARGB; - defaultPaint.setColor(colour); - canvas.drawRect( - column, line, column + runLength, line + lineHeigth, defaultPaint); + if (runLength != 0 && paint != null) { + paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]); + canvas.drawRect(column, line, column + runLength, line + 1, paint); } column += runLength; - } + } while (!endOfPixelCodeString); - return column - savedColumn; + return column; } - private int dvbSub8BitPixelCodeString(ParsableBitArray data, int lineHeigth, - ClutEntry[] clutEntries, byte[] clutMapTable, - int column, int line, boolean paint) { - - /* Parse 8-bit/pixel code string. ETSI EN 300 743 7.2.5.2 - - 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; + /** + * Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. + */ + private static int paint8BitPixelCodeString(ParsableBitArray data, int[] clutEntries, + byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { boolean endOfPixelCodeString = false; - - while (!endOfPixelCodeString) { - runLength = 0; - peek = data.readBits(8); + do { + int runLength = 0; + int clutIndex = 0; + int peek = data.readBits(8); if (peek != 0x00) { runLength = 1; - clutIdx = peek; + clutIndex = peek; } else { - peek = data.readBits(1); - if (peek == 0x00) { + if (!data.readBit()) { peek = data.readBits(7); if (peek != 0x00) { runLength = peek; - clutIdx = 0x00; + clutIndex = 0x00; } else { endOfPixelCodeString = true; } } else { runLength = data.readBits(7); - clutIdx = data.readBits(8); + clutIndex = data.readBits(8); } } - if (runLength != 0 && paint) { - colour = clutMapTable != null ? clutEntries[clutMapTable[clutIdx]].ARGB - : clutEntries[clutIdx].ARGB; - defaultPaint.setColor(colour); - canvas.drawRect( - column, line, column + runLength, line + lineHeigth, defaultPaint); + if (runLength != 0 && paint != null) { + paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]); + canvas.drawRect(column, line, column + runLength, line + 1, paint); } - 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 - * 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 + * The subtitle service definition. */ - List 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 - --------------------------------------- ---- ------------------------------------------ - PES_data_field() { - data_identifier 8 For DVB subtitle streams it shall be 0x20 - subtitle_stream_id 8 For DVB subtitling stream it shall be 0x00 - while nextbits() == '0000 1111' { - Subtitling_segment() - } - end_of_PES_data_field_marker 8 An 8-bit field with fixed contents '1111 1111' + public final SparseArray regions = new SparseArray<>(); + public final SparseArray cluts = new SparseArray<>(); + public final SparseArray objects = new SparseArray<>(); + public final SparseArray ancillaryCluts = new SparseArray<>(); + public final SparseArray ancillaryObjects = new SparseArray<>(); - */ + public DisplayDefinition displayDefinition; + public PageComposition pageComposition; - if (input != null) { - tsStream = new ParsableBitArray(input, inputSize); - } else { - 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; - } + public SubtitleService(int subtitlePageId, int ancillaryPageId) { + this.subtitlePageId = subtitlePageId; + this.ancillaryPageId = ancillaryPageId; } - 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. + *

+ * 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 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; - RegionComposition regionComposition; - int baseHorizontalAddress, baseVerticalAddress; - ObjectData object; - ClutDefinition clut; - int regionKey; - // process page regions - for (a = 0; a < subtitleService.pageComposition.pageRegions.size(); a++) { - regionKey = subtitleService.pageComposition.pageRegions.keyAt(a); - pageRegion = subtitleService.pageComposition.pageRegions.get(regionKey); - regionComposition = subtitleService.regions.get(regionKey); - - baseHorizontalAddress = pageRegion.regionHorizontalAddress; - baseVerticalAddress = pageRegion.regionVerticalAddress; - - if ((subtitleService.displayDefinition.flags & DISPLAY_WINDOW_FLAG) != 0) { - baseHorizontalAddress += - subtitleService.displayDefinition.displayWindowHorizontalPositionMinimum; - baseVerticalAddress += - subtitleService.displayDefinition.displayWindowVerticalPositionMinimum; - } + /** + * The page is the definition and arrangement of regions in the screen. + *

+ * See ETSI EN 300 743 7.2.2 + */ + private static final class PageComposition { + + public final int timeOutSecs; // TODO: Use this or remove it. + public final int version; + public final int state; + public final SparseArray regions; + + public PageComposition(int timeoutSecs, int version, int state, + SparseArray regions) { + this.timeOutSecs = timeoutSecs; + this.version = version; + this.state = state; + this.regions = regions; + } - // 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): (" + - baseHorizontalAddress + "/" + baseVerticalAddress + "/" + - (baseHorizontalAddress + regionComposition.regionWidth - 1) + "/" + - (baseVerticalAddress + regionComposition.regionHeight - 1) + ")" - ); - - canvas.drawRect( - baseHorizontalAddress, baseVerticalAddress, - baseHorizontalAddress + regionComposition.regionWidth - 1, - baseVerticalAddress + regionComposition.regionHeight - 1, - debugRegionPaint); - } + /** + * A region within a {@link PageComposition}. + *

+ * See ETSI EN 300 743 7.2.2 + */ + private static final class PageRegion { - RegionObject regionObject; - int objectKey; - // 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 final int horizontalAddress; + public final int verticalAddress; - } + 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, - regionComposition.regionWidth, regionComposition.regionHeight); - canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); - regionComposition.cue = new Cue(cueBitmap, - (float) baseHorizontalAddress / subtitleService.displayDefinition.displayWidth, Cue.ANCHOR_TYPE_START, - (float) baseVerticalAddress / subtitleService.displayDefinition.displayHeight, Cue.ANCHOR_TYPE_START, - (float) regionComposition.regionWidth / subtitleService.displayDefinition.displayWidth, - (float) regionComposition.regionHeight / subtitleService.displayDefinition.displayHeight); - cueList.add(regionComposition.cue); - } + /** + * An area of the page composed of a list of objects and a CLUT. + *

+ * See ETSI EN 300 743 7.2.3 + */ + private static final class RegionComposition { + + public final int id; + public final boolean fillFlag; + 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 regionObjects; + + public RegionComposition(int id, boolean fillFlag, int width, int height, + int levelOfCompatibility, int depth, int clutId, int pixelCode8Bit, int pixelCode4Bit, + int pixelCode2Bit, SparseArray 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 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}. + *

+ * 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 + *

+ * 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. + *

+ * 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; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java index d614f1c498e..75728359c7f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java @@ -18,41 +18,37 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; - -import java.util.Collections; import java.util.List; /** * A representation of a DVB subtitle. */ /* package */ final class DvbSubtitle implements Subtitle { - private final List cues; - - public DvbSubtitle(List cues) { - if (cues == null) { - this.cues = Collections.emptyList(); - } else { - this.cues = cues; - } - } - - @Override - public int getNextEventTimeIndex(long timeUs) { - return C.INDEX_UNSET; - } - - @Override - public int getEventTimeCount() { - return 1; - } - - @Override - public long getEventTime(int index) { - return 0; - } - - @Override - public List getCues(long timeUs) { - return cues; - } -} \ No newline at end of file + + private final List cues; + + public DvbSubtitle(List cues) { + this.cues = cues; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + return C.INDEX_UNSET; + } + + @Override + public int getEventTimeCount() { + return 1; + } + + @Override + public long getEventTime(int index) { + return 0; + } + + @Override + public List getCues(long timeUs) { + return cues; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index a848022ba9b..e76f0fd7e2e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -46,7 +46,7 @@ public SubripDecoder() { } @Override - protected SubripSubtitle decode(byte[] bytes, int length) { + protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(bytes, length); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index 34ff757e2f3..71ce17eeeda 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -94,7 +94,8 @@ public TtmlDecoder() { } @Override - protected TtmlSubtitle decode(byte[] bytes, int length) throws SubtitleDecoderException { + protected TtmlSubtitle decode(byte[] bytes, int length, boolean reset) + throws SubtitleDecoderException { try { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); Map globalStyles = new HashMap<>(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java index 7484a5244d4..dccb64caec9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java @@ -35,7 +35,7 @@ public Tx3gDecoder() { } @Override - protected Subtitle decode(byte[] bytes, int length) { + protected Subtitle decode(byte[] bytes, int length, boolean reset) { parsableByteArray.reset(bytes, length); int textLength = parsableByteArray.readUnsignedShort(); if (textLength == 0) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java index 916e67128a9..159dd4f2e0f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java @@ -45,7 +45,8 @@ public Mp4WebvttDecoder() { } @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: // first 4 bytes size and then 4 bytes type. sampleData.reset(bytes, length); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java index fb177dcea70..7c3262fbba9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java @@ -54,7 +54,8 @@ public WebvttDecoder() { } @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); // Initialization for consistent starting state. webvttCueBuilder.reset(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index 83f83c79214..df9f04f0678 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -89,6 +89,16 @@ public int getPosition() { return byteOffset * 8 + bitOffset; } + /** + * 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. * @@ -177,6 +187,47 @@ public int readBits(int numBits) { 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() { // It is fine for position to be at the end of the array, but no further. Assertions.checkState(byteOffset >= 0 diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 9255f353029..bd03464b207 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -315,7 +315,7 @@ private void setupBitmapLayout() { float anchorX = parentLeft + (parentWidth * cuePosition); float anchorY = parentTop + (parentHeight * cueLine); 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())); int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX);