From d8f61ad7e7090496a1d43e7a5cd31feb3abc478c Mon Sep 17 00:00:00 2001 From: Sergio Moreno Mozota Date: Thu, 23 Mar 2017 08:51:41 +0100 Subject: [PATCH 1/7] Add DVB subtitles support --- .../google/android/exoplayer2/text/Cue.java | 34 +- .../text/SubtitleDecoderFactory.java | 6 + .../text/dvbsubs/DvbSubsDecoder.java | 71 + .../text/dvbsubs/DvbSubsSubtitle.java | 59 + .../text/dvbsubs/DvbSubtitlesParser.java | 1561 +++++++++++++++++ .../android/exoplayer2/util/MimeTypes.java | 3 +- .../exoplayer2/ui/SubtitlePainter.java | 5 +- 7 files changed, 1733 insertions(+), 6 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsDecoder.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsSubtitle.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java 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 176b8ea8158..a13dbed6240 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 @@ -178,7 +178,11 @@ public class Cue { public final int windowColor; /** - * Creates an image cue. + * The Storage Aspect Ratio of the Cue + */ + public final float sar; + + /** * Creates an image cue. * * @param bitmap See {@link #bitmap}. * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed @@ -194,7 +198,28 @@ public class Cue { public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, float verticalPosition, @AnchorType int verticalPositionAnchor, float width) { this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, - horizontalPosition, horizontalPositionAnchor, width, false, Color.BLACK); + horizontalPosition, horizontalPositionAnchor, width, false, Color.BLACK, (float) 1.7777); + } + + /** + * Creates an image cue. + * + * @param bitmap See {@link #bitmap}. + * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed + * as a fraction of the viewport width. + * @param horizontalPositionAnchor The horizontal anchor. One of {@link #ANCHOR_TYPE_START}, + * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a + * 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 sar The Storage Aspect Ratio of the cue, defaults to FHD SAR unless otherwise specified. + */ + public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, + float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float sar) { + this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, + horizontalPosition, horizontalPositionAnchor, width, false, Color.BLACK, sar); } /** @@ -243,12 +268,12 @@ public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, boolean windowColorSet, int windowColor) { this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size, - windowColorSet, windowColor); + windowColorSet, windowColor, 1); } private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line, @LineType int lineType, @AnchorType int lineAnchor, float position, - @AnchorType int positionAnchor, float size, boolean windowColorSet, int windowColor) { + @AnchorType int positionAnchor, float size, boolean windowColorSet, int windowColor, float sar) { this.text = text; this.textAlignment = textAlignment; this.bitmap = bitmap; @@ -260,6 +285,7 @@ private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float lin this.size = size; this.windowColorSet = windowColorSet; this.windowColor = windowColor; + this.sar = sar; } } 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 077fc8848bc..b13a267b326 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 @@ -25,6 +25,8 @@ import com.google.android.exoplayer2.text.webvtt.WebvttDecoder; import com.google.android.exoplayer2.util.MimeTypes; +import java.util.List; + /** * A factory for {@link SubtitleDecoder} instances. */ @@ -83,6 +85,8 @@ public SubtitleDecoder createDecoder(Format format) { } else if (format.sampleMimeType.equals(MimeTypes.APPLICATION_CEA708)) { return clazz.asSubclass(SubtitleDecoder.class).getConstructor(Integer.TYPE) .newInstance(format.accessibilityChannel); + } else if (format.sampleMimeType.equals(MimeTypes.APPLICATION_DVBSUBS) && format.initializationData != null) { + return clazz.asSubclass(SubtitleDecoder.class).getConstructor(List.class).newInstance(format.initializationData); } else { return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance(); } @@ -112,6 +116,8 @@ private Class getDecoderClass(String mimeType) { return Class.forName("com.google.android.exoplayer2.text.cea.Cea608Decoder"); case MimeTypes.APPLICATION_CEA708: return Class.forName("com.google.android.exoplayer2.text.cea.Cea708Decoder"); + case MimeTypes.APPLICATION_DVBSUBS: + return Class.forName("com.google.android.exoplayer2.text.dvbsubs.DvbSubsDecoder"); default: return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsDecoder.java new file mode 100644 index 00000000000..0f4811f3392 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsDecoder.java @@ -0,0 +1,71 @@ +/* + * 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.text.dvbsubs; + +import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; + +import java.util.List; + +public final class DvbSubsDecoder extends SimpleSubtitleDecoder { + private final String TAG = "DVBSubs Decoder"; + + private int subtitilingType; + private int subtitleCompositionPage; + private int subtitleAncillaryPage; + private String subtitleContainer; + + private int flags = 0; + + DvbSubtitlesParser parser; + + public DvbSubsDecoder() { + super("dvbsubs"); + parser = new DvbSubtitlesParser(); + } + + public DvbSubsDecoder(List initializationData) { + super("dvbsubs"); + + byte[] tempByteArray; + + tempByteArray = initializationData.get(0); + subtitilingType = tempByteArray != null ? tempByteArray[0] & 0xFF: -1; + + tempByteArray = initializationData.get(3); + if (tempByteArray != null ) { + subtitleContainer = new String(tempByteArray); + if (subtitleContainer.equals("mkv")) { + flags |= DvbSubtitlesParser.FLAG_PES_STRIPPED_DVBSUB; + } + } + + if ((tempByteArray = initializationData.get(1)) != null) { + this.subtitleCompositionPage = ((tempByteArray[0] & 0xFF) << 8) | (tempByteArray[1] & 0xFF); + if ((tempByteArray = initializationData.get(2)) != null) { + this.subtitleAncillaryPage = ((tempByteArray[0] & 0xFF) << 8) | (tempByteArray[1] & 0xFF); + parser = new DvbSubtitlesParser(this.subtitleCompositionPage, this.subtitleAncillaryPage, flags); + } + } else { + parser = new DvbSubtitlesParser(); + } + + } + + @Override + protected DvbSubsSubtitle decode(byte[] data, int length) { + return new DvbSubsSubtitle(parser.dvbSubsDecode(data, length)); + } +} \ No newline at end of file diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsSubtitle.java new file mode 100644 index 00000000000..7d845a24dce --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsSubtitle.java @@ -0,0 +1,59 @@ +/* + * 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.text.dvbsubs; + + +import android.graphics.Bitmap; + +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; + +final class DvbSubsSubtitle implements Subtitle { + private final List cues; + + public DvbSubsSubtitle(Bitmap data) { + if (data == null) { + this.cues = Collections.emptyList(); + } else { + Cue cue = new Cue(data, 0, Cue.ANCHOR_TYPE_START, 0, Cue.ANCHOR_TYPE_START, 1, (float) data.getWidth()/data.getHeight()); + this.cues = Collections.singletonList(cue); + } + } + + @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 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java new file mode 100644 index 00000000000..37a22a3b8b3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java @@ -0,0 +1,1561 @@ +/* + * 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.text.dvbsubs; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +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.util.ParsableBitArray; + + +public class DvbSubtitlesParser { + + 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, + (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, + (byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb, + (byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff}; + + private ClutDefinition defaultClut = new ClutDefinition(); + + /* FLAGS */ + private final static int DISPLAY_WINDOW_FLAG = 0x01; + + private final static int REGION_FILL_FLAG = 0x01; + + private final static int OBJECT_NON_MODIFYING_COLOUR_FLAG = 0x01; + + /* Constants */ + private static final int UNDEF_PAGE = -1; + private final int NUM_BITMAPS = 4; + + /* instance variables */ + private Paint defaultPaintObject = new Paint(); + private Paint fillRegionPaintObject = new Paint(); + private Paint debugRegionPaintObject = new Paint(); + private Paint debugObjectPaintObject = new Paint(); + private Canvas[] canvasObjects = new Canvas[NUM_BITMAPS]; + private Bitmap[] bitmaps = new Bitmap[NUM_BITMAPS]; + private Bitmap lastValidBitmap; + private int currentBitmap = 0; + private static ParsableBitArray tsStream; + private SubtitleService subtitleService; + + private class SubtitleService { + int subtitlePage; + int ancillaryPage; + boolean newSubtitle = false; + + // 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 displays dimensions [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() { + for (int i = 0; i < NUM_BITMAPS; i++) { + bitmaps[i] = Bitmap.createBitmap(this.displayWidth + 1, this.displayHeight + 1, + Bitmap.Config.ARGB_8888); + canvasObjects[i] = new Canvas(bitmaps[i]); + } + } + } + + /* The page final static ints the list of regions [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 on the image [7.2.3] + * with a list of the object definitions associated 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<>(); + } + + /* The entry in the palette CLUT */ + 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); + + } + + 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)); + } + } + + /* Colours to be applied in a CLUT family [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; + } + + 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); + } + + i--; + } + + return entries; + } + + 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); + } 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; + } + + } + + i--; + } + + return entries; + } + + ClutDefinition () { + clutEntries2bit = generateDefault2bitClut(); + clutEntries4bit = generateDefault4bitClut(); + clutEntries8bit = generateDefault8bitClut(); + } + + } + + /* The object data segment contains the data 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; + } + + private class RegionObject { + int objectId; + int objectType; + int objectProvider; + int objectHorizontalPosition; + int objectVerticalPosition; + int foregroundPixelCode; + int backgroundPixelCode; + } + + DvbSubtitlesParser() { + this(1); + } + + DvbSubtitlesParser(int subtitlePge) { + this(subtitlePge, UNDEF_PAGE); + } + + DvbSubtitlesParser(int subtitlePage, int ancillaryPage) { + this(subtitlePage, ancillaryPage, 0); + } + + DvbSubtitlesParser(int subtitlePage, int ancillaryPage, @Flags int flags) { + this.subtitleService = new SubtitleService(); + this.flags = flags; + + this.defaultPaintObject.setStyle(Paint.Style.FILL_AND_STROKE); + this.defaultPaintObject.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + this.defaultPaintObject.setPathEffect(null); + + this.fillRegionPaintObject.setStyle(Paint.Style.FILL); + this.fillRegionPaintObject.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + this.fillRegionPaintObject.setPathEffect(null); + + this.debugRegionPaintObject.setColor(0xff00ff00); + this.debugRegionPaintObject.setStyle(Paint.Style.STROKE); + this.debugRegionPaintObject.setPathEffect(new DashPathEffect(new float[] {2,2}, 0)); + + this.debugObjectPaintObject.setColor(0xffff0000); + this.debugObjectPaintObject.setStyle(Paint.Style.STROKE); + this.debugObjectPaintObject.setPathEffect(new DashPathEffect(new float[]{10, 20}, 0)); + + this.subtitleService.subtitlePage = subtitlePage; + this.subtitleService.ancillaryPage = ancillaryPage; + + this.subtitleService.displayDefinition = new DisplayDefinition(); + this.subtitleService.displayDefinition.updateBitmapResolution(); + + this.currentBitmap = 0; + this.lastValidBitmap = bitmaps[currentBitmap]; + } + + 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.subtitlePage) { + 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(); + lastValidBitmap = bitmaps[currentBitmap]; + } else { + subtitleService.displayDefinition.versionNumber = tempDisplay.versionNumber; + } + + 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.subtitlePage) { + if (tempPage.pageState == DVBSUB_PCS_STATE_NORMAL && subtitleService.pageComposition == null) + break; + subtitleService.pageComposition = tempPage; + } + 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.subtitlePage) { + 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.subtitlePage) { + subtitleService.cluts.put(tempClutDefinition.clutId, tempClutDefinition); + } else if (tempClutDefinition.pageId == subtitleService.ancillaryPage) { + subtitleService.ancillaryCluts.put(tempClutDefinition.clutId, tempClutDefinition); + } + } + 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.subtitlePage) { + subtitleService.objects.put(tempObjectData.objectId, tempObjectData); + } else if (tempObjectData.pageId == subtitleService.ancillaryPage) { + subtitleService.ancillaryObjects.put(tempObjectData.objectId, tempObjectData); + } + } + 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); + 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); + break; + default: + 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(); + + 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); + } else { + display.displayWindowHorizontalPositionMinimum = 0; + display.displayWindowHorizontalPositionMaximum = display.displayWidth; + display.displayWindowVerticalPositionMinimum = 0; + display.displayWindowVerticalPositionMaximum = display.displayHeight; + } + + return display; + } + + 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.subtitlePage == 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.newSubtitle = false; + subtitleService.pageComposition = null; + subtitleService.regions = new SparseArray<>(); + subtitleService.cluts = new SparseArray<>(); + subtitleService.objects = new SparseArray<>(); + + if (BuildConfig.DEBUG) { + if (page.pageState != DVBSUB_PCS_STATE_NORMAL) { + Log.d(TAG, " New Page Composition. pageId: " + page.pageId + + " version: " + page.pageVersionNumber + + " timeout: " + page.pageTimeOut + ); + } + } + } + + remainingSegmentLength -= 2; + while (remainingSegmentLength > 0) { + PageRegion region = new PageRegion(); + + region.regionId = tsStream.readBits(8); + tsStream.skipBits(8); + region.regionHorizontalAddress = tsStream.readBits(16); + region.regionVerticalAddress = tsStream.readBits(16); + + 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 + ")"); + } + + page.pageRegions.put(region.regionId, region); + + remainingSegmentLength -= 6; + } + + return page; + } + + 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); + } + + + return region; + } + + 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); + + if ((entryFlags & 0x80) != 0) { + entry = clut.clutEntries2bit[entryId]; + } else if ((entryFlags & 0x40) != 0) { + entry = clut.clutEntries4bit[entryId]; + } else { + entry = clut.clutEntries8bit[entryId]; + } + + 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; + } else { + Y = tsStream.readBits(6) << 2; + Cr = tsStream.readBits(4) << 4; + Cb = tsStream.readBits(4) << 4; + T = tsStream.readBits(2) << 6; + remainingSegmentLength -= 4; + } + + if (Y == 0x00) { + Cr = 0x00; + Cb = 0x00; + T = 0xFF; + } + + entry.clutYCbCrT(Y, Cb, Cr, T); + } + return clut; + } + + 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) { + 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); + } + break; + case DVBSUB_DT_48_TABLE_DATA: + clutMapTable48 = new byte[16]; + for (i = 0; i < 4; i++) { + clutMapTable48[i] = (byte) data.readBits(8); + } + break; + case DVBSUB_DT_END_LINE: + column = horizontalAddress; + line += 2; + break; + default: + break; + } + } + field += lineHeight; + } + + return null; + } + + 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 + } + } + } + } + } + */ + + int savedColumn = column, peek, runLength, clutIdx = 0x00, colour; + boolean endOfPixelCodeString = false; + + 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; + } + } + } + } + + if (runLength != 0 && paint) { + colour = clutMapTable != null ? clutEntries[clutMapTable[clutIdx]].ARGB + : clutEntries[clutIdx].ARGB; + defaultPaintObject.setColor(colour); + canvasObjects[currentBitmap].drawRect( + column, line, column + runLength, line + lineHeigth, defaultPaintObject); + } + + column += runLength; + } + + return column - savedColumn; + } + + 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; + boolean endOfPixelCodeString = false; + + while (!endOfPixelCodeString) { + runLength = 0; + 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; + } + } 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; + } + } + } + } + + if (runLength != 0 && paint) { + colour = clutMapTable != null ? clutEntries[clutMapTable[clutIdx]].ARGB + : clutEntries[clutIdx].ARGB; + defaultPaintObject.setColor(colour); + canvasObjects[currentBitmap].drawRect( + column, line, column + runLength, line + lineHeigth, defaultPaintObject); + } + + column += runLength; + } + + return column - savedColumn; + } + + 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; + boolean endOfPixelCodeString = false; + + while (!endOfPixelCodeString) { + runLength = 0; + peek = data.readBits(8); + if (peek != 0x00) { + runLength = 1; + clutIdx = peek; + } else { + peek = data.readBits(1); + if (peek == 0x00) { + peek = data.readBits(7); + if (peek != 0x00) { + runLength = peek; + clutIdx = 0x00; + } else { + endOfPixelCodeString = true; + } + } else { + runLength = data.readBits(7); + clutIdx = data.readBits(8); + } + } + + if (runLength != 0 && paint) { + colour = clutMapTable != null ? clutEntries[clutMapTable[clutIdx]].ARGB + : clutEntries[clutIdx].ARGB; + defaultPaintObject.setColor(colour); + canvasObjects[currentBitmap].drawRect( + column, line, column + runLength, line + lineHeigth, defaultPaintObject); + } + + column += runLength; + } + + return column - savedColumn; + } + + public Bitmap dvbSubsDecode(byte[] input, int inputSize) { + + /* process PES PACKET. ETSI EN 300 743 7.1 + + 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' + + */ + + 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; + } + } + + if (BuildConfig.DEBUG) Log.d(TAG,"New PES subtitle packet."); + + 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); + + } + + 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) { + + 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; + } + + // clip object drawing to the current region and display definition window + canvasObjects[currentBitmap].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; + } + } + + // 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; + } + + fillRegionPaintObject.setColor(colour); + canvasObjects[currentBitmap].drawRect( + baseHorizontalAddress, baseVerticalAddress, + baseHorizontalAddress + regionComposition.regionWidth , + baseVerticalAddress + regionComposition.regionHeight, + fillRegionPaintObject); + + } + + if (BuildConfig.DEBUG) { + Log.d(TAG, " Region: " + regionKey + " (x/y/w/h): (" + + baseHorizontalAddress + "/" + baseVerticalAddress + "/" + + (baseHorizontalAddress + regionComposition.regionWidth - 1) + "/" + + (baseVerticalAddress + regionComposition.regionHeight - 1) + ")" + ); + + canvasObjects[currentBitmap].drawRect( + baseHorizontalAddress, baseVerticalAddress, + baseHorizontalAddress + regionComposition.regionWidth - 1, + baseVerticalAddress + regionComposition.regionHeight - 1, + debugRegionPaintObject); + } + + 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) + ")" + ); + + canvasObjects[currentBitmap].drawRect( + baseHorizontalAddress + regionObject.objectHorizontalPosition, + baseVerticalAddress + regionObject.objectVerticalPosition, + baseHorizontalAddress + regionObject.objectHorizontalPosition + regionComposition.regionWidth - 1, + baseVerticalAddress + regionObject.objectVerticalPosition + regionComposition.regionHeight - 1, + debugObjectPaintObject); + } + + 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); + + } + } + + lastValidBitmap = bitmaps[currentBitmap]; + currentBitmap = (currentBitmap + 1) % NUM_BITMAPS; + canvasObjects[currentBitmap].clipRect(0,0, + subtitleService.displayDefinition.displayWidth, + subtitleService.displayDefinition.displayHeight, + Region.Op.REPLACE); + canvasObjects[currentBitmap].drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + + return lastValidBitmap; + } + } else { + Log.d(TAG,"Unexpected..."); + } + return null; + } + + private boolean isSet(@Flags int flag) { + return (flags & flag) != 0; + } + +} \ No newline at end of file diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 69d4229186e..ea669e6f2a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -81,6 +81,7 @@ public final class MimeTypes { public static final String APPLICATION_SCTE35 = BASE_TYPE_APPLICATION + "/x-scte35"; public static final String APPLICATION_CAMERA_MOTION = BASE_TYPE_APPLICATION + "/x-camera-motion"; public static final String APPLICATION_EMSG = BASE_TYPE_APPLICATION + "/x-emsg"; + public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs"; private MimeTypes() {} @@ -222,7 +223,7 @@ public static int getTrackType(String mimeType) { || APPLICATION_SUBRIP.equals(mimeType) || APPLICATION_TTML.equals(mimeType) || APPLICATION_TX3G.equals(mimeType) || APPLICATION_MP4VTT.equals(mimeType) || APPLICATION_RAWCC.equals(mimeType) || APPLICATION_VOBSUB.equals(mimeType) - || APPLICATION_PGS.equals(mimeType)) { + || APPLICATION_PGS.equals(mimeType) || APPLICATION_DVBSUBS.equals(mimeType)) { return C.TRACK_TYPE_TEXT; } else if (APPLICATION_ID3.equals(mimeType) || APPLICATION_EMSG.equals(mimeType) 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 d4f09b1721d..4bf7ce0b745 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 @@ -77,6 +77,7 @@ @Cue.AnchorType private int cuePositionAnchor; private float cueSize; + private float cueSar; private boolean applyEmbeddedStyles; private int foregroundColor; private int backgroundColor; @@ -173,6 +174,7 @@ public void draw(Cue cue, boolean applyEmbeddedStyles, CaptionStyleCompat style, && this.cuePosition == cue.position && Util.areEqual(this.cuePositionAnchor, cue.positionAnchor) && this.cueSize == cue.size + && this.cueSar == cue.sar && this.applyEmbeddedStyles == applyEmbeddedStyles && this.foregroundColor == style.foregroundColor && this.backgroundColor == style.backgroundColor @@ -200,6 +202,7 @@ public void draw(Cue cue, boolean applyEmbeddedStyles, CaptionStyleCompat style, this.cuePosition = cue.position; this.cuePositionAnchor = cue.positionAnchor; this.cueSize = cue.size; + this.cueSar = cue.sar; this.applyEmbeddedStyles = applyEmbeddedStyles; this.foregroundColor = style.foregroundColor; this.backgroundColor = style.backgroundColor; @@ -312,7 +315,7 @@ private void setupBitmapLayout() { float anchorX = parentLeft + (parentWidth * cuePosition); float anchorY = parentTop + (parentHeight * cueLine); int width = Math.round(parentWidth * cueSize); - int height = Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); + int height = Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth()) / (((float) parentWidth / parentHeight) / cueSar)); int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) From 937675d1dbeb8fcaf31f2e7c67fe576115c204cc Mon Sep 17 00:00:00 2001 From: Sergio Moreno Mozota Date: Thu, 23 Mar 2017 08:54:07 +0100 Subject: [PATCH 2/7] Add DVB sustitles support to the TsExtractor --- .../ts/DefaultTsPayloadReaderFactory.java | 2 + .../extractor/ts/DvbSubtitlesReader.java | 89 +++++++++++++++++++ .../exoplayer2/extractor/ts/TsExtractor.java | 6 ++ 3 files changed, 97 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitlesReader.java 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 e8b664d5abc..d2b0acca85c 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 @@ -109,6 +109,8 @@ public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { ? null : new SectionReader(new SpliceInfoSectionReader()); case TsExtractor.TS_STREAM_TYPE_ID3: return new PesReader(new Id3Reader()); + case TsExtractor.TS_STREAM_TYPE_DVBSUBS: + return new PesReader(new DvbSubtitlesReader(esInfo)); default: return null; } 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 new file mode 100644 index 00000000000..0e5931e9fe4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitlesReader.java @@ -0,0 +1,89 @@ +/* + * 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.List; + + +public class DvbSubtitlesReader implements ElementaryStreamReader { + + private static final String TAG= "DVBSubsReader"; + private final String language; + private List initializationData = new ArrayList<>(); + + private long sampleTimeUs; + private int totalBytesWritten; + private boolean writingSample; + + private TrackOutput output; + + public DvbSubtitlesReader(TsPayloadReader.EsInfo esInfo) { + // we only support one subtitle service per PID + this.language = esInfo.language; + this.initializationData.add(new byte[] {esInfo.descriptorBytes[5]}); // subtitle subtype + this.initializationData.add(new byte[] {esInfo.descriptorBytes[6], esInfo.descriptorBytes[7]}); // subtitle compose page + this.initializationData.add(new byte[] {esInfo.descriptorBytes[8], esInfo.descriptorBytes[9]}); // subtitle ancillary page + this.initializationData.add("mp2t".getBytes()); + } + + + @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; + totalBytesWritten = 0; + } + + @Override + public void packetFinished() { + output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, totalBytesWritten, 0, null); + writingSample = false; + } + + @Override + public void consume(ParsableByteArray data) { + if (writingSample) { + totalBytesWritten += data.bytesLeft(); + output.sampleData(data, data.bytesLeft()); + } + } +} \ 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 65b97c8a738..3c163541ac5 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,6 +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; private static final int TS_PACKET_SIZE = 188; private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. @@ -356,6 +357,7 @@ private class PmtReader implements SectionPayloadReader { private static final int TS_PMT_DESC_AC3 = 0x6A; private static final int TS_PMT_DESC_EAC3 = 0x7A; private static final int TS_PMT_DESC_DTS = 0x7B; + private static final int TS_PMT_DESC_DVBSUBS = 0x59; private final ParsableBitArray pmtScratch; private final int pid; @@ -498,6 +500,10 @@ private EsInfo readEsInfo(ParsableByteArray data, int length) { } else if (descriptorTag == TS_PMT_DESC_ISO639_LANG) { language = new String(data.data, data.getPosition(), 3).trim(); // Audio type is ignored. + } else if (descriptorTag == TS_PMT_DESC_DVBSUBS) { + streamType = TS_STREAM_TYPE_DVBSUBS; + // we only support one subtitle service per PID + language = new String(data.data, data.getPosition(), 3).trim(); } // Skip unused bytes of current descriptor. data.skipBytes(positionOfNextDescriptor - data.getPosition()); From 0c49b81b48c83c906d009c1165fbe6b25e66f693 Mon Sep 17 00:00:00 2001 From: Sergio Moreno Mozota Date: Thu, 23 Mar 2017 08:55:20 +0100 Subject: [PATCH 3/7] add DVB subtitle support, based in the TVHeadend spec, to the MatroskaExtractor --- .../extractor/mkv/MatroskaExtractor.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 51ce8192829..1f3bb0192d1 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 @@ -99,6 +99,7 @@ public Extractor[] createExtractors() { private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8"; private static final String CODEC_ID_VOBSUB = "S_VOBSUB"; private static final String CODEC_ID_PGS = "S_HDMV/PGS"; + private static final String CODEC_ID_DVBSUB = "S_DVBSUB"; private static final int VORBIS_MAX_INPUT_SIZE = 8192; private static final int OPUS_MAX_INPUT_SIZE = 5760; @@ -1233,8 +1234,8 @@ private static boolean isCodecSupported(String codecId) { || CODEC_ID_PCM_INT_LIT.equals(codecId) || CODEC_ID_SUBRIP.equals(codecId) || CODEC_ID_VOBSUB.equals(codecId) - || CODEC_ID_PGS.equals(codecId); - } + || CODEC_ID_PGS.equals(codecId) + || CODEC_ID_DVBSUB.equals(codecId); } /** * Returns an array that can store (at least) {@code length} elements, which will be either a new @@ -1461,6 +1462,14 @@ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserE case CODEC_ID_PGS: mimeType = MimeTypes.APPLICATION_PGS; break; + case CODEC_ID_DVBSUB: + mimeType = MimeTypes.APPLICATION_DVBSUBS; + initializationData = new ArrayList<>(4); + initializationData.add(null); + initializationData.add(new byte[] {codecPrivate[0], codecPrivate[1]}); + initializationData.add(new byte[] {codecPrivate[2], codecPrivate[3]}); + initializationData.add("mkv".getBytes()); + break; default: throw new ParserException("Unrecognized codec identifier."); } @@ -1495,7 +1504,8 @@ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserE format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, selectionFlags, language, drmInitData); } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) - || MimeTypes.APPLICATION_PGS.equals(mimeType)) { + || MimeTypes.APPLICATION_PGS.equals(mimeType) + || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) { type = C.TRACK_TYPE_TEXT; format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, initializationData, language, drmInitData); From 7e0ea5a65b818a2985c05f1fd9c86acfe22565c4 Mon Sep 17 00:00:00 2001 From: Sergio Moreno Mozota Date: Thu, 23 Mar 2017 17:44:33 +0100 Subject: [PATCH 4/7] Optimization of memory and draw operations, and proper treatment of filled regions (as far as we can infer from the specs) --- .../text/dvbsubs/DvbSubsSubtitle.java | 10 +- .../text/dvbsubs/DvbSubtitlesParser.java | 171 +++++++++--------- 2 files changed, 86 insertions(+), 95 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsSubtitle.java index 7d845a24dce..ae4444b7a77 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsSubtitle.java @@ -15,9 +15,6 @@ */ package com.google.android.exoplayer2.text.dvbsubs; - -import android.graphics.Bitmap; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; @@ -28,12 +25,11 @@ final class DvbSubsSubtitle implements Subtitle { private final List cues; - public DvbSubsSubtitle(Bitmap data) { - if (data == null) { + public DvbSubsSubtitle(List cues) { + if (cues == null) { this.cues = Collections.emptyList(); } else { - Cue cue = new Cue(data, 0, Cue.ANCHOR_TYPE_START, 0, Cue.ANCHOR_TYPE_START, 1, (float) data.getWidth()/data.getHeight()); - this.cues = Collections.singletonList(cue); + this.cues = cues; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java index 37a22a3b8b3..7d393b3f7c8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java @@ -28,8 +28,12 @@ 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 java.util.ArrayList; +import java.util.List; + public class DvbSubtitlesParser { @@ -111,17 +115,15 @@ public class DvbSubtitlesParser { /* Constants */ private static final int UNDEF_PAGE = -1; - private final int NUM_BITMAPS = 4; /* instance variables */ - private Paint defaultPaintObject = new Paint(); - private Paint fillRegionPaintObject = new Paint(); - private Paint debugRegionPaintObject = new Paint(); - private Paint debugObjectPaintObject = new Paint(); - private Canvas[] canvasObjects = new Canvas[NUM_BITMAPS]; - private Bitmap[] bitmaps = new Bitmap[NUM_BITMAPS]; - private Bitmap lastValidBitmap; - private int currentBitmap = 0; + private Paint defaultPaint = new Paint(); + private Paint fillRegionPaint = new Paint(); + private Paint debugRegionPaint = new Paint(); + private Paint debugObjectPaint = new Paint(); + private Canvas canvas = new Canvas(); + private Bitmap bitmap; + private static ParsableBitArray tsStream; private SubtitleService subtitleService; @@ -133,13 +135,13 @@ private class SubtitleService { // subtitle page DisplayDefinition displayDefinition; PageComposition pageComposition; - SparseArray regions = new SparseArray(); - SparseArray cluts = new SparseArray(); - SparseArray objects = new SparseArray(); + SparseArray regions = new SparseArray<>(); + SparseArray cluts = new SparseArray<>(); + SparseArray objects = new SparseArray<>(); // ancillary page - SparseArray ancillaryCluts = new SparseArray(); - SparseArray ancillaryObjects = new SparseArray(); + SparseArray ancillaryCluts = new SparseArray<>(); + SparseArray ancillaryObjects = new SparseArray<>(); } /* The displays dimensions [7.2.1] */ @@ -157,11 +159,9 @@ private class DisplayDefinition { int displayWindowVerticalPositionMaximum = 575; void updateBitmapResolution() { - for (int i = 0; i < NUM_BITMAPS; i++) { - bitmaps[i] = Bitmap.createBitmap(this.displayWidth + 1, this.displayHeight + 1, - Bitmap.Config.ARGB_8888); - canvasObjects[i] = new Canvas(bitmaps[i]); - } + bitmap = Bitmap.createBitmap(this.displayWidth + 1, this.displayHeight + 1, + Bitmap.Config.ARGB_8888); + canvas = new Canvas(bitmap); } } @@ -171,7 +171,7 @@ private class PageComposition { int pageTimeOut; /* in seconds */ int pageVersionNumber; int pageState; - SparseArray pageRegions = new SparseArray(); + SparseArray pageRegions = new SparseArray<>(); } private class PageRegion { @@ -196,6 +196,7 @@ private class RegionComposition { int region4bitPixelCode; int region2bitPixelCode; SparseArray regionObjects = new SparseArray<>(); + Cue cue; } /* The entry in the palette CLUT */ @@ -435,30 +436,27 @@ private class RegionObject { this.subtitleService = new SubtitleService(); this.flags = flags; - this.defaultPaintObject.setStyle(Paint.Style.FILL_AND_STROKE); - this.defaultPaintObject.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); - this.defaultPaintObject.setPathEffect(null); + this.defaultPaint.setStyle(Paint.Style.FILL_AND_STROKE); + this.defaultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + this.defaultPaint.setPathEffect(null); - this.fillRegionPaintObject.setStyle(Paint.Style.FILL); - this.fillRegionPaintObject.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); - this.fillRegionPaintObject.setPathEffect(null); + this.fillRegionPaint.setStyle(Paint.Style.FILL); + this.fillRegionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); + this.fillRegionPaint.setPathEffect(null); - this.debugRegionPaintObject.setColor(0xff00ff00); - this.debugRegionPaintObject.setStyle(Paint.Style.STROKE); - this.debugRegionPaintObject.setPathEffect(new DashPathEffect(new float[] {2,2}, 0)); + this.debugRegionPaint.setColor(0xff00ff00); + this.debugRegionPaint.setStyle(Paint.Style.STROKE); + this.debugRegionPaint.setPathEffect(new DashPathEffect(new float[] {2,2}, 0)); - this.debugObjectPaintObject.setColor(0xffff0000); - this.debugObjectPaintObject.setStyle(Paint.Style.STROKE); - this.debugObjectPaintObject.setPathEffect(new DashPathEffect(new float[]{10, 20}, 0)); + this.debugObjectPaint.setColor(0xffff0000); + this.debugObjectPaint.setStyle(Paint.Style.STROKE); + this.debugObjectPaint.setPathEffect(new DashPathEffect(new float[]{10, 20}, 0)); this.subtitleService.subtitlePage = subtitlePage; this.subtitleService.ancillaryPage = ancillaryPage; this.subtitleService.displayDefinition = new DisplayDefinition(); this.subtitleService.displayDefinition.updateBitmapResolution(); - - this.currentBitmap = 0; - this.lastValidBitmap = bitmaps[currentBitmap]; } private void parseSubtitlingSegment() { @@ -492,7 +490,6 @@ private void parseSubtitlingSegment() { tempDisplay.flags != subtitleService.displayDefinition.flags) { subtitleService.displayDefinition = tempDisplay; subtitleService.displayDefinition.updateBitmapResolution(); - lastValidBitmap = bitmaps[currentBitmap]; } else { subtitleService.displayDefinition.versionNumber = tempDisplay.versionNumber; } @@ -938,7 +935,7 @@ private ObjectData parseObjectDataSegment() { ObjectData object = new ObjectData(); object.pageId = tsStream.readBits(16); - int remainingSegmentLength = tsStream.readBits(16); + tsStream.skipBits(16); // remainingSegmentLength object.objectId = tsStream.readBits(16); object.objectVersionNumber = tsStream.readBits(4); object.objectCodingMethod = tsStream.readBits(2); @@ -947,21 +944,17 @@ private ObjectData parseObjectDataSegment() { } tsStream.skipBits(1); - remainingSegmentLength -= 3; if (object.objectCodingMethod == DVBSUB_ODS_CHAR_CODED) { /* not implemented yet */ object.numberOfCodes = tsStream.readBits(8); tsStream.skipBits(object.numberOfCodes * 16); - remainingSegmentLength -= object.numberOfCodes * 2; } else if (object.objectCodingMethod == DVBSUB_ODS_PIXEL_CODED) { object.topFieldDataLength = tsStream.readBits(16); object.bottomFieldDataLength = tsStream.readBits(16); - remainingSegmentLength -= 4; object.topFieldData = new byte[object.topFieldDataLength]; System.arraycopy(tsStream.data, tsStream.getPosition() / 8, object.topFieldData, 0, object.topFieldDataLength); tsStream.skipBits(object.topFieldDataLength * 8); - remainingSegmentLength -= object.topFieldDataLength; object.bottomFieldData = new byte[object.bottomFieldDataLength]; if (object.bottomFieldDataLength == 0) { @@ -970,8 +963,6 @@ private ObjectData parseObjectDataSegment() { System.arraycopy(tsStream.data, tsStream.getPosition() / 8, object.bottomFieldData, 0, object.bottomFieldDataLength); tsStream.skipBits(object.bottomFieldDataLength * 8); } - remainingSegmentLength -= object.bottomFieldDataLength; - } return object; @@ -1190,9 +1181,9 @@ private int dvbSub2BitPixelCodeString(ParsableBitArray data, int lineHeigth, if (runLength != 0 && paint) { colour = clutMapTable != null ? clutEntries[clutMapTable[clutIdx]].ARGB : clutEntries[clutIdx].ARGB; - defaultPaintObject.setColor(colour); - canvasObjects[currentBitmap].drawRect( - column, line, column + runLength, line + lineHeigth, defaultPaintObject); + defaultPaint.setColor(colour); + canvas.drawRect( + column, line, column + runLength, line + lineHeigth, defaultPaint); } column += runLength; @@ -1292,9 +1283,9 @@ private int dvbSub4BitPixelCodeString(ParsableBitArray data, int lineHeigth, if (runLength != 0 && paint) { colour = clutMapTable != null ? clutEntries[clutMapTable[clutIdx]].ARGB : clutEntries[clutIdx].ARGB; - defaultPaintObject.setColor(colour); - canvasObjects[currentBitmap].drawRect( - column, line, column + runLength, line + lineHeigth, defaultPaintObject); + defaultPaint.setColor(colour); + canvas.drawRect( + column, line, column + runLength, line + lineHeigth, defaultPaint); } column += runLength; @@ -1359,9 +1350,9 @@ private int dvbSub8BitPixelCodeString(ParsableBitArray data, int lineHeigth, if (runLength != 0 && paint) { colour = clutMapTable != null ? clutEntries[clutMapTable[clutIdx]].ARGB : clutEntries[clutIdx].ARGB; - defaultPaintObject.setColor(colour); - canvasObjects[currentBitmap].drawRect( - column, line, column + runLength, line + lineHeigth, defaultPaintObject); + defaultPaint.setColor(colour); + canvas.drawRect( + column, line, column + runLength, line + lineHeigth, defaultPaint); } column += runLength; @@ -1370,7 +1361,7 @@ private int dvbSub8BitPixelCodeString(ParsableBitArray data, int lineHeigth, return column - savedColumn; } - public Bitmap dvbSubsDecode(byte[] input, int inputSize) { + List dvbSubsDecode(byte[] input, int inputSize) { /* process PES PACKET. ETSI EN 300 743 7.1 @@ -1416,6 +1407,7 @@ public Bitmap dvbSubsDecode(byte[] input, int inputSize) { 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 + @@ -1456,7 +1448,7 @@ public Bitmap dvbSubsDecode(byte[] input, int inputSize) { } // clip object drawing to the current region and display definition window - canvasObjects[currentBitmap].clipRect( + canvas.clipRect( baseHorizontalAddress, baseVerticalAddress, Math.min(baseHorizontalAddress + regionComposition.regionWidth, subtitleService.displayDefinition.displayWindowHorizontalPositionMaximum), @@ -1470,26 +1462,6 @@ public Bitmap dvbSubsDecode(byte[] input, int inputSize) { } } - // 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; - } - - fillRegionPaintObject.setColor(colour); - canvasObjects[currentBitmap].drawRect( - baseHorizontalAddress, baseVerticalAddress, - baseHorizontalAddress + regionComposition.regionWidth , - baseVerticalAddress + regionComposition.regionHeight, - fillRegionPaintObject); - - } - if (BuildConfig.DEBUG) { Log.d(TAG, " Region: " + regionKey + " (x/y/w/h): (" + baseHorizontalAddress + "/" + baseVerticalAddress + "/" + @@ -1497,11 +1469,11 @@ public Bitmap dvbSubsDecode(byte[] input, int inputSize) { (baseVerticalAddress + regionComposition.regionHeight - 1) + ")" ); - canvasObjects[currentBitmap].drawRect( + canvas.drawRect( baseHorizontalAddress, baseVerticalAddress, baseHorizontalAddress + regionComposition.regionWidth - 1, baseVerticalAddress + regionComposition.regionHeight - 1, - debugRegionPaintObject); + debugRegionPaint); } RegionObject regionObject; @@ -1517,12 +1489,12 @@ public Bitmap dvbSubsDecode(byte[] input, int inputSize) { (baseVerticalAddress + regionObject.objectVerticalPosition) + ")" ); - canvasObjects[currentBitmap].drawRect( + canvas.drawRect( baseHorizontalAddress + regionObject.objectHorizontalPosition, baseVerticalAddress + regionObject.objectVerticalPosition, baseHorizontalAddress + regionObject.objectHorizontalPosition + regionComposition.regionWidth - 1, baseVerticalAddress + regionObject.objectVerticalPosition + regionComposition.regionHeight - 1, - debugObjectPaintObject); + debugObjectPaint); } if ((object = subtitleService.objects.get(regionObject.objectId)) == null) { @@ -1536,18 +1508,41 @@ public Bitmap dvbSubsDecode(byte[] input, int inputSize) { baseVerticalAddress + regionObject.objectVerticalPosition); } - } - lastValidBitmap = bitmaps[currentBitmap]; - currentBitmap = (currentBitmap + 1) % NUM_BITMAPS; - canvasObjects[currentBitmap].clipRect(0,0, - subtitleService.displayDefinition.displayWidth, - subtitleService.displayDefinition.displayHeight, - Region.Op.REPLACE); - canvasObjects[currentBitmap].drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + // 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); - return lastValidBitmap; - } + 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) subtitleService.displayDefinition.displayWidth / subtitleService.displayDefinition.displayHeight); + cueList.add(regionComposition.cue); + } + + return cueList; } } else { Log.d(TAG,"Unexpected..."); } From 6c9656dc9a42f3977a9bf013d722810806114ee5 Mon Sep 17 00:00:00 2001 From: Sergio Moreno Mozota Date: Wed, 29 Mar 2017 00:14:32 +0200 Subject: [PATCH 5/7] some cleaning and class/method documentation --- .../text/dvbsubs/DvbSubtitlesParser.java | 134 ++++++++++++------ 1 file changed, 87 insertions(+), 47 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java index 7d393b3f7c8..47432bcf07f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java @@ -34,7 +34,9 @@ import java.util.ArrayList; import java.util.List; - +/** + * Parse and generate a list of {@link Cue}s from DVB subtitling bitstream + */ public class DvbSubtitlesParser { private static final String TAG = "DVBSubs"; @@ -104,8 +106,6 @@ public class DvbSubtitlesParser { (byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff}; - private ClutDefinition defaultClut = new ClutDefinition(); - /* FLAGS */ private final static int DISPLAY_WINDOW_FLAG = 0x01; @@ -121,16 +121,19 @@ public class DvbSubtitlesParser { private Paint fillRegionPaint = new Paint(); private Paint debugRegionPaint = new Paint(); private Paint debugObjectPaint = new Paint(); - private Canvas canvas = new Canvas(); 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 + */ private class SubtitleService { - int subtitlePage; - int ancillaryPage; - boolean newSubtitle = false; + int subtitlePageId; + int ancillaryPageId; // subtitle page DisplayDefinition displayDefinition; @@ -144,7 +147,7 @@ private class SubtitleService { SparseArray ancillaryObjects = new SparseArray<>(); } - /* The displays dimensions [7.2.1] */ + /* The display definition contains the geometry and active area of the subtitle service [7.2.1] */ private class DisplayDefinition { int pageId; int versionNumber; @@ -165,7 +168,7 @@ void updateBitmapResolution() { } } - /* The page final static ints the list of regions [7.2.2] */ + /* The page is the definition and arrangement of regions in the screen [7.2.2] */ private class PageComposition { int pageId; int pageTimeOut; /* in seconds */ @@ -174,14 +177,13 @@ private class PageComposition { SparseArray pageRegions = new SparseArray<>(); } - private class PageRegion { - int regionId; - int regionHorizontalAddress; - int regionVerticalAddress; - } + private class PageRegion { + int regionId; + int regionHorizontalAddress; + int regionVerticalAddress; + } - /* The Region is an area on the image [7.2.3] - * with a list of the object definitions associated and a CLUT */ + /* 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; @@ -196,10 +198,27 @@ private class RegionComposition { 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; } - /* The entry in the palette CLUT */ + private class RegionObject { + int objectId; + int objectType; + int objectProvider; + int objectHorizontalPosition; + int objectVerticalPosition; + int foregroundPixelCode; + int backgroundPixelCode; + } + + /* An entry in the palette CLUT and associated color space translation methods */ private class ClutEntry { int clutEntryId; byte flags; @@ -279,7 +298,7 @@ void clutRGBA (int R, int G, int B, int A) { } } - /* Colours to be applied in a CLUT family [7.2.4] */ + /* CLUT family definition containing the color tables for the three bitdepths defined [7.2.4] */ private class ClutDefinition { int pageId; int clutId; @@ -395,8 +414,7 @@ ClutEntry[] generateDefault8bitClut() { } - /* The object data segment contains the data of an object [7.2.5] - */ + /* The object data segment contains the textual/graphical representation of an object [7.2.5] */ private class ObjectData { int pageId; int objectId; @@ -410,29 +428,42 @@ private class ObjectData { int numberOfCodes; } - private class RegionObject { - int objectId; - int objectType; - int objectProvider; - int objectHorizontalPosition; - int objectVerticalPosition; - int foregroundPixelCode; - int backgroundPixelCode; - } - + /** + * Construct a subtitle service with default subtitle pageId + */ DvbSubtitlesParser() { this(1); } - DvbSubtitlesParser(int subtitlePge) { - this(subtitlePge, UNDEF_PAGE); + /** + * Construct a subtitle service for the given subtitle pageId + * + * @param subtitlePageId The subtitle page Id carrying the selected subtitle track + */ + DvbSubtitlesParser(int subtitlePageId) { + this(subtitlePageId, UNDEF_PAGE); } - DvbSubtitlesParser(int subtitlePage, int ancillaryPage) { - this(subtitlePage, ancillaryPage, 0); + /** + * 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 + */ + DvbSubtitlesParser(int subtitlePageId, int ancillaryPageId) { + this(subtitlePageId, ancillaryPageId, 0); } - DvbSubtitlesParser(int subtitlePage, int ancillaryPage, @Flags int flags) { + /** + * 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 + */ + DvbSubtitlesParser(int subtitlePageId, int ancillaryPageId, @Flags int flags) { this.subtitleService = new SubtitleService(); this.flags = flags; @@ -452,8 +483,8 @@ private class RegionObject { this.debugObjectPaint.setStyle(Paint.Style.STROKE); this.debugObjectPaint.setPathEffect(new DashPathEffect(new float[]{10, 20}, 0)); - this.subtitleService.subtitlePage = subtitlePage; - this.subtitleService.ancillaryPage = ancillaryPage; + this.subtitleService.subtitlePageId = subtitlePageId; + this.subtitleService.ancillaryPageId = ancillaryPageId; this.subtitleService.displayDefinition = new DisplayDefinition(); this.subtitleService.displayDefinition.updateBitmapResolution(); @@ -480,7 +511,7 @@ private void parseSubtitlingSegment() { case DVBSUB_ST_DISPLAY_DEFINITION: if (BuildConfig.DEBUG) Log.d(TAG, " Parse Display Definition segment."); DisplayDefinition tempDisplay = parseDisplayDefinitionSegment(); - if (tempDisplay != null && tempDisplay.pageId == subtitleService.subtitlePage) { + if (tempDisplay != null && tempDisplay.pageId == subtitleService.subtitlePageId) { if (tempDisplay.displayWidth != subtitleService.displayDefinition.displayWidth || tempDisplay.displayHeight != subtitleService.displayDefinition.displayHeight || tempDisplay.displayWindowHorizontalPositionMaximum != subtitleService.displayDefinition.displayWindowHorizontalPositionMaximum || @@ -506,7 +537,7 @@ private void parseSubtitlingSegment() { case DVBSUB_ST_PAGE_COMPOSITION: if (BuildConfig.DEBUG) Log.d(TAG, " Parse Page Composition segment."); PageComposition tempPage = parsePageCompositionSegment(); - if (tempPage != null && tempPage.pageId == subtitleService.subtitlePage) { + if (tempPage != null && tempPage.pageId == subtitleService.subtitlePageId) { if (tempPage.pageState == DVBSUB_PCS_STATE_NORMAL && subtitleService.pageComposition == null) break; subtitleService.pageComposition = tempPage; @@ -515,7 +546,7 @@ private void parseSubtitlingSegment() { case DVBSUB_ST_REGION_COMPOSITION: if (BuildConfig.DEBUG) Log.d(TAG, " Parse Region Composition segment."); RegionComposition tempRegionComposition = parseRegionCompositionSegment(); - if (tempRegionComposition != null && tempRegionComposition.pageId == subtitleService.subtitlePage) { + if (tempRegionComposition != null && tempRegionComposition.pageId == subtitleService.subtitlePageId) { subtitleService.regions.put(tempRegionComposition.regionId, tempRegionComposition); } break; @@ -523,9 +554,9 @@ private void parseSubtitlingSegment() { if (BuildConfig.DEBUG) Log.d(TAG, " Parse Clut Definition segment."); ClutDefinition tempClutDefinition = parseClutDefinitionSegment(); if (tempClutDefinition != null ) { - if (tempClutDefinition.pageId == subtitleService.subtitlePage) { + if (tempClutDefinition.pageId == subtitleService.subtitlePageId) { subtitleService.cluts.put(tempClutDefinition.clutId, tempClutDefinition); - } else if (tempClutDefinition.pageId == subtitleService.ancillaryPage) { + } else if (tempClutDefinition.pageId == subtitleService.ancillaryPageId) { subtitleService.ancillaryCluts.put(tempClutDefinition.clutId, tempClutDefinition); } } @@ -534,9 +565,9 @@ private void parseSubtitlingSegment() { if (BuildConfig.DEBUG) Log.d(TAG, " Parse Object Data segment."); ObjectData tempObjectData = parseObjectDataSegment(); if (tempObjectData != null) { - if (tempObjectData.pageId == subtitleService.subtitlePage) { + if (tempObjectData.pageId == subtitleService.subtitlePageId) { subtitleService.objects.put(tempObjectData.objectId, tempObjectData); - } else if (tempObjectData.pageId == subtitleService.ancillaryPage) { + } else if (tempObjectData.pageId == subtitleService.ancillaryPageId) { subtitleService.ancillaryObjects.put(tempObjectData.objectId, tempObjectData); } } @@ -659,7 +690,7 @@ private PageComposition parsePageCompositionSegment() { ); } - } else if (subtitleService.subtitlePage == page.pageId) { + } 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 + @@ -667,7 +698,6 @@ private PageComposition parsePageCompositionSegment() { } } - subtitleService.newSubtitle = false; subtitleService.pageComposition = null; subtitleService.regions = new SparseArray<>(); subtitleService.cluts = new SparseArray<>(); @@ -1361,6 +1391,15 @@ private int dvbSub8BitPixelCodeString(ParsableBitArray data, int lineHeigth, return column - savedColumn; } + /** + * 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 + */ List dvbSubsDecode(byte[] input, int inputSize) { /* process PES PACKET. ETSI EN 300 743 7.1 @@ -1542,7 +1581,8 @@ List dvbSubsDecode(byte[] input, int inputSize) { cueList.add(regionComposition.cue); } - return cueList; } + return cueList; + } } else { Log.d(TAG,"Unexpected..."); } From d34d3f76be7cb4b7423b61cf03d85f2ad1df9dd4 Mon Sep 17 00:00:00 2001 From: Sergio Moreno Mozota Date: Fri, 31 Mar 2017 01:48:29 +0200 Subject: [PATCH 6/7] Pull request review fixes --- library/core/proguard-rules.txt | 3 + .../extractor/mkv/MatroskaExtractor.java | 10 +- .../extractor/ts/DvbSubtitlesReader.java | 98 +- .../exoplayer2/extractor/ts/TsExtractor.java | 1 - .../google/android/exoplayer2/text/Cue.java | 48 +- .../text/SubtitleDecoderFactory.java | 4 +- .../exoplayer2/text/dvb/DvbDecoder.java | 52 + .../exoplayer2/text/dvb/DvbParser.java | 1569 ++++++++++++++++ .../DvbSubtitle.java} | 9 +- .../text/dvbsubs/DvbSubsDecoder.java | 71 - .../text/dvbsubs/DvbSubtitlesParser.java | 1596 ----------------- .../exoplayer2/ui/SubtitlePainter.java | 9 +- 12 files changed, 1707 insertions(+), 1763 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java rename library/core/src/main/java/com/google/android/exoplayer2/text/{dvbsubs/DvbSubsSubtitle.java => dvb/DvbSubtitle.java} (87%) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsDecoder.java delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 75f2d095bed..34d881f8913 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -5,3 +5,6 @@ -keepclassmembers class com.google.android.exoplayer2.text.cea.Cea708Decoder { public (int); } +-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/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 1f3bb0192d1..31b2c41f85d 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 @@ -1235,7 +1235,8 @@ private static boolean isCodecSupported(String codecId) { || CODEC_ID_SUBRIP.equals(codecId) || CODEC_ID_VOBSUB.equals(codecId) || CODEC_ID_PGS.equals(codecId) - || CODEC_ID_DVBSUB.equals(codecId); } + || CODEC_ID_DVBSUB.equals(codecId); + } /** * Returns an array that can store (at least) {@code length} elements, which will be either a new @@ -1464,11 +1465,8 @@ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserE break; case CODEC_ID_DVBSUB: mimeType = MimeTypes.APPLICATION_DVBSUBS; - initializationData = new ArrayList<>(4); - initializationData.add(null); - initializationData.add(new byte[] {codecPrivate[0], codecPrivate[1]}); - initializationData.add(new byte[] {codecPrivate[2], codecPrivate[3]}); - initializationData.add("mkv".getBytes()); + initializationData = Collections.singletonList(new byte[] { + (byte) 0x01, 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/DvbSubtitlesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitlesReader.java index 0e5931e9fe4..6caf0692aef 100644 --- 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 @@ -24,66 +24,68 @@ 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; -public class DvbSubtitlesReader implements ElementaryStreamReader { +/** + * Output PES packets to a {@link TrackOutput}. + */ +public final class DvbSubtitlesReader implements ElementaryStreamReader { - private static final String TAG= "DVBSubsReader"; - private final String language; - private List initializationData = new ArrayList<>(); + private final String language; + private List initializationData; - private long sampleTimeUs; - private int totalBytesWritten; - private boolean writingSample; + private long sampleTimeUs; + private int sampleBytesWritten; + private boolean writingSample; - private TrackOutput output; + private TrackOutput output; - public DvbSubtitlesReader(TsPayloadReader.EsInfo esInfo) { - // we only support one subtitle service per PID - this.language = esInfo.language; - this.initializationData.add(new byte[] {esInfo.descriptorBytes[5]}); // subtitle subtype - this.initializationData.add(new byte[] {esInfo.descriptorBytes[6], esInfo.descriptorBytes[7]}); // subtitle compose page - this.initializationData.add(new byte[] {esInfo.descriptorBytes[8], esInfo.descriptorBytes[9]}); // subtitle ancillary page - this.initializationData.add("mp2t".getBytes()); - } + public DvbSubtitlesReader(TsPayloadReader.EsInfo esInfo) { + this.language = esInfo.language; + initializationData = Collections.singletonList(new byte[] {(byte) 0x00, + esInfo.descriptorBytes[6], esInfo.descriptorBytes[7], + esInfo.descriptorBytes[8], esInfo.descriptorBytes[9]}); + } - @Override - public void seek() { - writingSample = false; - } + @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 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; - totalBytesWritten = 0; + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + if (!dataAlignmentIndicator) { + return; } - - @Override - public void packetFinished() { - output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, totalBytesWritten, 0, null); - writingSample = false; - } - - @Override - public void consume(ParsableByteArray data) { - if (writingSample) { - totalBytesWritten += data.bytesLeft(); - output.sampleData(data, data.bytesLeft()); - } + writingSample = true; + sampleTimeUs = pesTimeUs; + sampleBytesWritten = 0; + } + + @Override + public void packetFinished() { + 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(); + 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 3c163541ac5..f80c470b593 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 @@ -502,7 +502,6 @@ private EsInfo readEsInfo(ParsableByteArray data, int length) { // Audio type is ignored. } else if (descriptorTag == TS_PMT_DESC_DVBSUBS) { streamType = TS_STREAM_TYPE_DVBSUBS; - // we only support one subtitle service per PID language = new String(data.data, data.getPosition(), 3).trim(); } // Skip unused bytes of current descriptor. 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 a13dbed6240..f31cf4387d3 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 @@ -167,6 +167,12 @@ 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}. + */ + public final float bitmapHeight; + /** * Specifies whether or not the {@link #windowColor} property is set. */ @@ -177,30 +183,6 @@ public class Cue { */ public final int windowColor; - /** - * The Storage Aspect Ratio of the Cue - */ - public final float sar; - - /** * Creates an image cue. - * - * @param bitmap See {@link #bitmap}. - * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed - * as a fraction of the viewport width. - * @param horizontalPositionAnchor The horizontal anchor. One of {@link #ANCHOR_TYPE_START}, - * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. - * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a - * 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. - */ - public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, - float verticalPosition, @AnchorType int verticalPositionAnchor, float width) { - this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, - horizontalPosition, horizontalPositionAnchor, width, false, Color.BLACK, (float) 1.7777); - } - /** * Creates an image cue. * @@ -214,12 +196,13 @@ public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPo * @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 sar The Storage Aspect Ratio of the cue, defaults to FHD SAR unless otherwise specified. + * @param height The width of the cue, expressed as a fraction of the viewport width. */ public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, - float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float sar) { + float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float + height) { this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, - horizontalPosition, horizontalPositionAnchor, width, false, Color.BLACK, sar); + horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK); } /** @@ -265,15 +248,16 @@ 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, - windowColorSet, windowColor, 1); + -1, windowColorSet, windowColor); } private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line, @LineType int lineType, @AnchorType int lineAnchor, float position, - @AnchorType int positionAnchor, float size, boolean windowColorSet, int windowColor, float sar) { + @AnchorType int positionAnchor, float size, float bitmapHeight, boolean windowColorSet, + int windowColor) { this.text = text; this.textAlignment = textAlignment; this.bitmap = bitmap; @@ -283,9 +267,9 @@ private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float lin this.position = position; this.positionAnchor = positionAnchor; this.size = size; + this.bitmapHeight = bitmapHeight; this.windowColorSet = windowColorSet; this.windowColor = windowColor; - this.sar = sar; } } 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 b13a267b326..83f5c0a2906 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 @@ -85,7 +85,7 @@ public SubtitleDecoder createDecoder(Format format) { } else if (format.sampleMimeType.equals(MimeTypes.APPLICATION_CEA708)) { return clazz.asSubclass(SubtitleDecoder.class).getConstructor(Integer.TYPE) .newInstance(format.accessibilityChannel); - } else if (format.sampleMimeType.equals(MimeTypes.APPLICATION_DVBSUBS) && format.initializationData != null) { + } else if (format.sampleMimeType.equals(MimeTypes.APPLICATION_DVBSUBS)) { return clazz.asSubclass(SubtitleDecoder.class).getConstructor(List.class).newInstance(format.initializationData); } else { return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance(); @@ -117,7 +117,7 @@ private Class getDecoderClass(String mimeType) { case MimeTypes.APPLICATION_CEA708: return Class.forName("com.google.android.exoplayer2.text.cea.Cea708Decoder"); case MimeTypes.APPLICATION_DVBSUBS: - return Class.forName("com.google.android.exoplayer2.text.dvbsubs.DvbSubsDecoder"); + return Class.forName("com.google.android.exoplayer2.text.dvb.DvbDecoder"); default: return null; } 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 new file mode 100644 index 00000000000..6b2d3dc5e3f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java @@ -0,0 +1,52 @@ +/* + * 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.text.dvb; + +import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; + +import java.util.List; + +/** + * A {@link SimpleSubtitleDecoder} for DVB Subtitles. + */ +public final class DvbDecoder extends SimpleSubtitleDecoder { + + private final DvbParser parser; + + 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); + } + + @Override + protected DvbSubtitle decode(byte[] data, int length) { + return new DvbSubtitle(parser.dvbSubsDecode(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 new file mode 100644 index 00000000000..e2254cf0077 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -0,0 +1,1569 @@ +/* + * 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.text.dvb; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +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 java.util.ArrayList; +import java.util.List; + +/** + * Parse and generate a list of {@link Cue}s from DVB subtitling 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, + (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; + + private final static int OBJECT_NON_MODIFYING_COLOUR_FLAG = 0x01; + + /* 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 + */ + 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; + } + + private class RegionObject { + int objectId; + int objectType; + int objectProvider; + int objectHorizontalPosition; + int objectVerticalPosition; + int foregroundPixelCode; + int backgroundPixelCode; + } + + /* 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); + + } + + 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)); + } + } + + /* 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; + } + + 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); + } + + i--; + } + + return entries; + } + + 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); + } 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; + } + + } + + i--; + } + + return entries; + } + + ClutDefinition() { + clutEntries2bit = generateDefault2bitClut(); + clutEntries4bit = generateDefault4bitClut(); + clutEntries8bit = generateDefault8bitClut(); + } + + } + + /* 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; + } + + /** + * 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 + */ + 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; + } + + 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; + } + 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); + } + } + 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); + } + } + 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); + 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); + break; + default: + 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(); + + 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); + } else { + display.displayWindowHorizontalPositionMinimum = 0; + display.displayWindowHorizontalPositionMaximum = display.displayWidth; + display.displayWindowVerticalPositionMinimum = 0; + display.displayWindowVerticalPositionMaximum = display.displayHeight; + } + + return display; + } + + 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 + ); + } + } + } + + remainingSegmentLength -= 2; + while (remainingSegmentLength > 0) { + PageRegion region = new PageRegion(); + + region.regionId = tsStream.readBits(8); + tsStream.skipBits(8); + region.regionHorizontalAddress = tsStream.readBits(16); + region.regionVerticalAddress = tsStream.readBits(16); + + 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 + ")"); + } + + page.pageRegions.put(region.regionId, region); + + remainingSegmentLength -= 6; + } + + return page; + } + + 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); + } + + + return region; + } + + 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); + + if ((entryFlags & 0x80) != 0) { + entry = clut.clutEntries2bit[entryId]; + } else if ((entryFlags & 0x40) != 0) { + entry = clut.clutEntries4bit[entryId]; + } else { + entry = clut.clutEntries8bit[entryId]; + } + + 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; + } else { + Y = tsStream.readBits(6) << 2; + Cr = tsStream.readBits(4) << 4; + Cb = tsStream.readBits(4) << 4; + T = tsStream.readBits(2) << 6; + remainingSegmentLength -= 4; + } + + if (Y == 0x00) { + Cr = 0x00; + Cb = 0x00; + T = 0xFF; + } + + entry.clutYCbCrT(Y, Cb, Cr, T); + } + return clut; + } + + 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) { + 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); + } + break; + case DVBSUB_DT_48_TABLE_DATA: + clutMapTable48 = new byte[16]; + for (i = 0; i < 4; i++) { + clutMapTable48[i] = (byte) data.readBits(8); + } + break; + case DVBSUB_DT_END_LINE: + column = horizontalAddress; + line += 2; + break; + default: + break; + } + } + field += lineHeight; + } + + return null; + } + + 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 + } + } + } + } + } + */ + + int savedColumn = column, peek, runLength, clutIdx = 0x00, colour; + boolean endOfPixelCodeString = false; + + 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; + } + } + } + } + + 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); + } + + column += runLength; + } + + return column - savedColumn; + } + + 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; + boolean endOfPixelCodeString = false; + + while (!endOfPixelCodeString) { + runLength = 0; + 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; + } + } 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; + } + } + } + } + + 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); + } + + column += runLength; + } + + return column - savedColumn; + } + + 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; + boolean endOfPixelCodeString = false; + + while (!endOfPixelCodeString) { + runLength = 0; + peek = data.readBits(8); + if (peek != 0x00) { + runLength = 1; + clutIdx = peek; + } else { + peek = data.readBits(1); + if (peek == 0x00) { + peek = data.readBits(7); + if (peek != 0x00) { + runLength = peek; + clutIdx = 0x00; + } else { + endOfPixelCodeString = true; + } + } else { + runLength = data.readBits(7); + clutIdx = 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); + } + + column += runLength; + } + + return column - savedColumn; + } + + /** + * 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 + */ + List dvbSubsDecode(byte[] input, int inputSize) { + + /* process PES PACKET. ETSI EN 300 743 7.1 + + 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' + + */ + + 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; + } + } + + if (BuildConfig.DEBUG) Log.d(TAG, "New PES subtitle packet."); + + 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); + + } + + 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; + } + + // 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); + } + + 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); + + } + + // 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); + } + + return cueList; + } + } else { + Log.d(TAG, "Unexpected..."); + } + return null; + } + + private boolean isSet(@Flags int flag) { + return (flags & flag) != 0; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java similarity index 87% rename from library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsSubtitle.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java index ae4444b7a77..d614f1c498e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.text.dvbsubs; +package com.google.android.exoplayer2.text.dvb; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; @@ -22,10 +22,13 @@ import java.util.Collections; import java.util.List; -final class DvbSubsSubtitle implements Subtitle { +/** + * A representation of a DVB subtitle. + */ +/* package */ final class DvbSubtitle implements Subtitle { private final List cues; - public DvbSubsSubtitle(List cues) { + public DvbSubtitle(List cues) { if (cues == null) { this.cues = Collections.emptyList(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsDecoder.java deleted file mode 100644 index 0f4811f3392..00000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubsDecoder.java +++ /dev/null @@ -1,71 +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.text.dvbsubs; - -import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; - -import java.util.List; - -public final class DvbSubsDecoder extends SimpleSubtitleDecoder { - private final String TAG = "DVBSubs Decoder"; - - private int subtitilingType; - private int subtitleCompositionPage; - private int subtitleAncillaryPage; - private String subtitleContainer; - - private int flags = 0; - - DvbSubtitlesParser parser; - - public DvbSubsDecoder() { - super("dvbsubs"); - parser = new DvbSubtitlesParser(); - } - - public DvbSubsDecoder(List initializationData) { - super("dvbsubs"); - - byte[] tempByteArray; - - tempByteArray = initializationData.get(0); - subtitilingType = tempByteArray != null ? tempByteArray[0] & 0xFF: -1; - - tempByteArray = initializationData.get(3); - if (tempByteArray != null ) { - subtitleContainer = new String(tempByteArray); - if (subtitleContainer.equals("mkv")) { - flags |= DvbSubtitlesParser.FLAG_PES_STRIPPED_DVBSUB; - } - } - - if ((tempByteArray = initializationData.get(1)) != null) { - this.subtitleCompositionPage = ((tempByteArray[0] & 0xFF) << 8) | (tempByteArray[1] & 0xFF); - if ((tempByteArray = initializationData.get(2)) != null) { - this.subtitleAncillaryPage = ((tempByteArray[0] & 0xFF) << 8) | (tempByteArray[1] & 0xFF); - parser = new DvbSubtitlesParser(this.subtitleCompositionPage, this.subtitleAncillaryPage, flags); - } - } else { - parser = new DvbSubtitlesParser(); - } - - } - - @Override - protected DvbSubsSubtitle decode(byte[] data, int length) { - return new DvbSubsSubtitle(parser.dvbSubsDecode(data, length)); - } -} \ No newline at end of file diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java deleted file mode 100644 index 47432bcf07f..00000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvbsubs/DvbSubtitlesParser.java +++ /dev/null @@ -1,1596 +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.text.dvbsubs; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -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 java.util.ArrayList; -import java.util.List; - -/** - * Parse and generate a list of {@link Cue}s from DVB subtitling bitstream - */ -public class DvbSubtitlesParser { - - 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, - (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; - - private final static int OBJECT_NON_MODIFYING_COLOUR_FLAG = 0x01; - - /* Constants */ - private static final int UNDEF_PAGE = -1; - - /* 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 - */ - 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; - } - - private class RegionObject { - int objectId; - int objectType; - int objectProvider; - int objectHorizontalPosition; - int objectVerticalPosition; - int foregroundPixelCode; - int backgroundPixelCode; - } - - /* 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); - - } - - 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)); - } - } - - /* 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; - } - - 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); - } - - i--; - } - - return entries; - } - - 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); - } 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; - } - - } - - i--; - } - - return entries; - } - - ClutDefinition () { - clutEntries2bit = generateDefault2bitClut(); - clutEntries4bit = generateDefault4bitClut(); - clutEntries8bit = generateDefault8bitClut(); - } - - } - - /* 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; - } - - /** - * Construct a subtitle service with default subtitle pageId - */ - DvbSubtitlesParser() { - this(1); - } - - /** - * Construct a subtitle service for the given subtitle pageId - * - * @param subtitlePageId The subtitle page Id carrying the selected subtitle track - */ - DvbSubtitlesParser(int subtitlePageId) { - this(subtitlePageId, UNDEF_PAGE); - } - - /** - * 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 - */ - DvbSubtitlesParser(int subtitlePageId, int ancillaryPageId) { - this(subtitlePageId, ancillaryPageId, 0); - } - - /** - * 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 - */ - DvbSubtitlesParser(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; - } - - 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; - } - 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); - } - } - 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); - } - } - 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); - 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); - break; - default: - 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(); - - 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); - } else { - display.displayWindowHorizontalPositionMinimum = 0; - display.displayWindowHorizontalPositionMaximum = display.displayWidth; - display.displayWindowVerticalPositionMinimum = 0; - display.displayWindowVerticalPositionMaximum = display.displayHeight; - } - - return display; - } - - 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 - ); - } - } - } - - remainingSegmentLength -= 2; - while (remainingSegmentLength > 0) { - PageRegion region = new PageRegion(); - - region.regionId = tsStream.readBits(8); - tsStream.skipBits(8); - region.regionHorizontalAddress = tsStream.readBits(16); - region.regionVerticalAddress = tsStream.readBits(16); - - 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 + ")"); - } - - page.pageRegions.put(region.regionId, region); - - remainingSegmentLength -= 6; - } - - return page; - } - - 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); - } - - - return region; - } - - 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); - - if ((entryFlags & 0x80) != 0) { - entry = clut.clutEntries2bit[entryId]; - } else if ((entryFlags & 0x40) != 0) { - entry = clut.clutEntries4bit[entryId]; - } else { - entry = clut.clutEntries8bit[entryId]; - } - - 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; - } else { - Y = tsStream.readBits(6) << 2; - Cr = tsStream.readBits(4) << 4; - Cb = tsStream.readBits(4) << 4; - T = tsStream.readBits(2) << 6; - remainingSegmentLength -= 4; - } - - if (Y == 0x00) { - Cr = 0x00; - Cb = 0x00; - T = 0xFF; - } - - entry.clutYCbCrT(Y, Cb, Cr, T); - } - return clut; - } - - 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) { - 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); - } - break; - case DVBSUB_DT_48_TABLE_DATA: - clutMapTable48 = new byte[16]; - for (i = 0; i < 4; i++) { - clutMapTable48[i] = (byte) data.readBits(8); - } - break; - case DVBSUB_DT_END_LINE: - column = horizontalAddress; - line += 2; - break; - default: - break; - } - } - field += lineHeight; - } - - return null; - } - - 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 - } - } - } - } - } - */ - - int savedColumn = column, peek, runLength, clutIdx = 0x00, colour; - boolean endOfPixelCodeString = false; - - 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; - } - } - } - } - - 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); - } - - column += runLength; - } - - return column - savedColumn; - } - - 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; - boolean endOfPixelCodeString = false; - - while (!endOfPixelCodeString) { - runLength = 0; - 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; - } - } 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; - } - } - } - } - - 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); - } - - column += runLength; - } - - return column - savedColumn; - } - - 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; - boolean endOfPixelCodeString = false; - - while (!endOfPixelCodeString) { - runLength = 0; - peek = data.readBits(8); - if (peek != 0x00) { - runLength = 1; - clutIdx = peek; - } else { - peek = data.readBits(1); - if (peek == 0x00) { - peek = data.readBits(7); - if (peek != 0x00) { - runLength = peek; - clutIdx = 0x00; - } else { - endOfPixelCodeString = true; - } - } else { - runLength = data.readBits(7); - clutIdx = 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); - } - - column += runLength; - } - - return column - savedColumn; - } - - /** - * 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 - */ - List dvbSubsDecode(byte[] input, int inputSize) { - - /* process PES PACKET. ETSI EN 300 743 7.1 - - 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' - - */ - - 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; - } - } - - if (BuildConfig.DEBUG) Log.d(TAG,"New PES subtitle packet."); - - 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); - - } - - 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; - } - - // 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); - } - - 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); - - } - - // 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) subtitleService.displayDefinition.displayWidth / subtitleService.displayDefinition.displayHeight); - cueList.add(regionComposition.cue); - } - - return cueList; - } - } else { - Log.d(TAG,"Unexpected..."); - } - return null; - } - - private boolean isSet(@Flags int flag) { - return (flags & flag) != 0; - } - -} \ No newline at end of file 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 4bf7ce0b745..9255f353029 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 @@ -77,7 +77,7 @@ @Cue.AnchorType private int cuePositionAnchor; private float cueSize; - private float cueSar; + private float cueBitmapHeight; private boolean applyEmbeddedStyles; private int foregroundColor; private int backgroundColor; @@ -174,7 +174,7 @@ public void draw(Cue cue, boolean applyEmbeddedStyles, CaptionStyleCompat style, && this.cuePosition == cue.position && Util.areEqual(this.cuePositionAnchor, cue.positionAnchor) && this.cueSize == cue.size - && this.cueSar == cue.sar + && this.cueBitmapHeight == cue.bitmapHeight && this.applyEmbeddedStyles == applyEmbeddedStyles && this.foregroundColor == style.foregroundColor && this.backgroundColor == style.backgroundColor @@ -202,7 +202,7 @@ public void draw(Cue cue, boolean applyEmbeddedStyles, CaptionStyleCompat style, this.cuePosition = cue.position; this.cuePositionAnchor = cue.positionAnchor; this.cueSize = cue.size; - this.cueSar = cue.sar; + this.cueBitmapHeight = cue.bitmapHeight; this.applyEmbeddedStyles = applyEmbeddedStyles; this.foregroundColor = style.foregroundColor; this.backgroundColor = style.backgroundColor; @@ -315,7 +315,8 @@ private void setupBitmapLayout() { float anchorX = parentLeft + (parentWidth * cuePosition); float anchorY = parentTop + (parentHeight * cueLine); int width = Math.round(parentWidth * cueSize); - int height = Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth()) / (((float) parentWidth / parentHeight) / cueSar)); + int height = cueBitmapHeight != -1 ? 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); int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) From 38779de22593e3acf40ab6f7b4906021a59a0804 Mon Sep 17 00:00:00 2001 From: Sergio Moreno Mozota Date: Tue, 4 Apr 2017 15:04:47 +0200 Subject: [PATCH 7/7] Add support for multiple subtitle tracks per PID and for naming hearing impaired subtitles --- .../extractor/ts/DvbSubtitlesReader.java | 68 +++++++++++++++---- 1 file changed, 55 insertions(+), 13 deletions(-) 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 index 6caf0692aef..240bfb987d8 100644 --- 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 @@ -24,6 +24,7 @@ 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; @@ -33,20 +34,40 @@ */ public final class DvbSubtitlesReader implements ElementaryStreamReader { - private final String language; - private List initializationData; + private class SubtitleTrack { + private String language; + private List initializationData; + } + + private List subtitles = new ArrayList<>(); private long sampleTimeUs; private int sampleBytesWritten; private boolean writingSample; - private TrackOutput output; + private List outputTracks = new ArrayList<>(); public DvbSubtitlesReader(TsPayloadReader.EsInfo esInfo) { - this.language = esInfo.language; - initializationData = Collections.singletonList(new byte[] {(byte) 0x00, - esInfo.descriptorBytes[6], esInfo.descriptorBytes[7], - esInfo.descriptorBytes[8], esInfo.descriptorBytes[9]}); + 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; + } } @@ -57,10 +78,18 @@ public void seek() { @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)); + 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); + } } @@ -76,7 +105,12 @@ public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { @Override public void packetFinished() { - output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null); + 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; } @@ -84,7 +118,15 @@ public void packetFinished() { public void consume(ParsableByteArray data) { if (writingSample) { int bytesAvailable = data.bytesLeft(); - output.sampleData(data, bytesAvailable); + 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; } }