diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java | 1059 |
1 files changed, 1059 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java new file mode 100644 index 0000000000..839c206ad7 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -0,0 +1,1059 @@ +/* + * 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 org.mozilla.thirdparty.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.PorterDuffXfermode; +import android.util.SparseArray; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Parses {@link Cue}s from a DVB subtitle bitstream. + */ +/* package */ final class DvbParser { + + private static final String TAG = "DvbParser"; + + // Segment types, as defined by ETSI EN 300 743 Table 2 + private static final int SEGMENT_TYPE_PAGE_COMPOSITION = 0x10; + private static final int SEGMENT_TYPE_REGION_COMPOSITION = 0x11; + private static final int SEGMENT_TYPE_CLUT_DEFINITION = 0x12; + private static final int SEGMENT_TYPE_OBJECT_DATA = 0x13; + private static final int SEGMENT_TYPE_DISPLAY_DEFINITION = 0x14; + + // Page states, as defined by ETSI EN 300 743 Table 3 + private static final int PAGE_STATE_NORMAL = 0; // Update. Only changed elements. + // private static final int PAGE_STATE_ACQUISITION = 1; // Refresh. All elements. + // private static final int PAGE_STATE_CHANGE = 2; // New. All elements. + + // Region depths, as defined by ETSI EN 300 743 Table 5 + // private static final int REGION_DEPTH_2_BIT = 1; + private static final int REGION_DEPTH_4_BIT = 2; + private static final int REGION_DEPTH_8_BIT = 3; + + // Object codings, as defined by ETSI EN 300 743 Table 8 + private static final int OBJECT_CODING_PIXELS = 0; + private static final int OBJECT_CODING_STRING = 1; + + // Pixel-data types, as defined by ETSI EN 300 743 Table 9 + private static final int DATA_TYPE_2BP_CODE_STRING = 0x10; + private static final int DATA_TYPE_4BP_CODE_STRING = 0x11; + private static final int DATA_TYPE_8BP_CODE_STRING = 0x12; + private static final int DATA_TYPE_24_TABLE_DATA = 0x20; + private static final int DATA_TYPE_28_TABLE_DATA = 0x21; + private static final int DATA_TYPE_48_TABLE_DATA = 0x22; + private static final int DATA_TYPE_END_LINE = 0xF0; + + // Clut mapping tables, as defined by ETSI EN 300 743 10.4, 10.5, 10.6 + private static final byte[] defaultMap2To4 = { + (byte) 0x00, (byte) 0x07, (byte) 0x08, (byte) 0x0F}; + private static final byte[] defaultMap2To8 = { + (byte) 0x00, (byte) 0x77, (byte) 0x88, (byte) 0xFF}; + private static final byte[] defaultMap4To8 = { + (byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33, + (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, + (byte) 0x88, (byte) 0x99, (byte) 0xAA, (byte) 0xBB, + (byte) 0xCC, (byte) 0xDD, (byte) 0xEE, (byte) 0xFF}; + + private final Paint defaultPaint; + private final Paint fillRegionPaint; + private final Canvas canvas; + private final DisplayDefinition defaultDisplayDefinition; + private final ClutDefinition defaultClutDefinition; + private final SubtitleService subtitleService; + + @MonotonicNonNull private Bitmap bitmap; + + /** + * Construct an instance for the given subtitle and ancillary page ids. + * + * @param subtitlePageId The id of the subtitle page carrying the subtitle to be parsed. + * @param ancillaryPageId The id of the ancillary page containing additional data. + */ + public DvbParser(int subtitlePageId, int ancillaryPageId) { + defaultPaint = new Paint(); + defaultPaint.setStyle(Paint.Style.FILL_AND_STROKE); + defaultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + defaultPaint.setPathEffect(null); + fillRegionPaint = new Paint(); + fillRegionPaint.setStyle(Paint.Style.FILL); + fillRegionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); + fillRegionPaint.setPathEffect(null); + canvas = new Canvas(); + defaultDisplayDefinition = new DisplayDefinition(719, 575, 0, 719, 0, 575); + defaultClutDefinition = new ClutDefinition(0, generateDefault2BitClutEntries(), + generateDefault4BitClutEntries(), generateDefault8BitClutEntries()); + subtitleService = new SubtitleService(subtitlePageId, ancillaryPageId); + } + + /** + * Resets the parser. + */ + public void reset() { + subtitleService.reset(); + } + + /** + * Decodes a subtitling packet, returning a list of parsed {@link Cue}s. + * + * @param data The subtitling packet data to decode. + * @param limit The limit in {@code data} at which to stop decoding. + * @return The parsed {@link Cue}s. + */ + public List<Cue> decode(byte[] data, int limit) { + // Parse the input data. + ParsableBitArray dataBitArray = new ParsableBitArray(data, limit); + while (dataBitArray.bitsLeft() >= 48 // sync_byte (8) + segment header (40) + && dataBitArray.readBits(8) == 0x0F) { + parseSubtitlingSegment(dataBitArray, subtitleService); + } + + @Nullable PageComposition pageComposition = subtitleService.pageComposition; + if (pageComposition == null) { + return Collections.emptyList(); + } + + // Update the canvas bitmap if necessary. + DisplayDefinition displayDefinition = subtitleService.displayDefinition != null + ? subtitleService.displayDefinition : defaultDisplayDefinition; + if (bitmap == null || displayDefinition.width + 1 != bitmap.getWidth() + || displayDefinition.height + 1 != bitmap.getHeight()) { + bitmap = Bitmap.createBitmap(displayDefinition.width + 1, displayDefinition.height + 1, + Bitmap.Config.ARGB_8888); + canvas.setBitmap(bitmap); + } + + // Build the cues. + List<Cue> cues = new ArrayList<>(); + SparseArray<PageRegion> pageRegions = pageComposition.regions; + for (int i = 0; i < pageRegions.size(); i++) { + // Save clean clipping state. + canvas.save(); + PageRegion pageRegion = pageRegions.valueAt(i); + int regionId = pageRegions.keyAt(i); + RegionComposition regionComposition = subtitleService.regions.get(regionId); + + // Clip drawing to the current region and display definition window. + int baseHorizontalAddress = pageRegion.horizontalAddress + + displayDefinition.horizontalPositionMinimum; + int baseVerticalAddress = pageRegion.verticalAddress + + displayDefinition.verticalPositionMinimum; + int clipRight = Math.min(baseHorizontalAddress + regionComposition.width, + displayDefinition.horizontalPositionMaximum); + int clipBottom = Math.min(baseVerticalAddress + regionComposition.height, + displayDefinition.verticalPositionMaximum); + canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom); + ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId); + if (clutDefinition == null) { + clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId); + if (clutDefinition == null) { + clutDefinition = defaultClutDefinition; + } + } + + SparseArray<RegionObject> regionObjects = regionComposition.regionObjects; + for (int j = 0; j < regionObjects.size(); j++) { + int objectId = regionObjects.keyAt(j); + RegionObject regionObject = regionObjects.valueAt(j); + ObjectData objectData = subtitleService.objects.get(objectId); + if (objectData == null) { + objectData = subtitleService.ancillaryObjects.get(objectId); + } + if (objectData != null) { + @Nullable Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; + paintPixelDataSubBlocks(objectData, clutDefinition, regionComposition.depth, + baseHorizontalAddress + regionObject.horizontalPosition, + baseVerticalAddress + regionObject.verticalPosition, paint, canvas); + } + } + + if (regionComposition.fillFlag) { + int color; + if (regionComposition.depth == REGION_DEPTH_8_BIT) { + color = clutDefinition.clutEntries8Bit[regionComposition.pixelCode8Bit]; + } else if (regionComposition.depth == REGION_DEPTH_4_BIT) { + color = clutDefinition.clutEntries4Bit[regionComposition.pixelCode4Bit]; + } else { + color = clutDefinition.clutEntries2Bit[regionComposition.pixelCode2Bit]; + } + fillRegionPaint.setColor(color); + canvas.drawRect(baseHorizontalAddress, baseVerticalAddress, + baseHorizontalAddress + regionComposition.width, + baseVerticalAddress + regionComposition.height, + fillRegionPaint); + } + + Bitmap cueBitmap = Bitmap.createBitmap(bitmap, baseHorizontalAddress, baseVerticalAddress, + regionComposition.width, regionComposition.height); + cues.add(new Cue(cueBitmap, (float) baseHorizontalAddress / displayDefinition.width, + Cue.ANCHOR_TYPE_START, (float) baseVerticalAddress / displayDefinition.height, + Cue.ANCHOR_TYPE_START, (float) regionComposition.width / displayDefinition.width, + (float) regionComposition.height / displayDefinition.height)); + + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + // Restore clean clipping state. + canvas.restore(); + } + + return Collections.unmodifiableList(cues); + } + + // Static parsing. + + /** + * Parses a subtitling segment, as defined by ETSI EN 300 743 7.2 + * <p> + * The {@link SubtitleService} is updated with the parsed segment data. + */ + private static void parseSubtitlingSegment(ParsableBitArray data, SubtitleService service) { + int segmentType = data.readBits(8); + int pageId = data.readBits(16); + int dataFieldLength = data.readBits(16); + int dataFieldLimit = data.getBytePosition() + dataFieldLength; + + if ((dataFieldLength * 8) > data.bitsLeft()) { + Log.w(TAG, "Data field length exceeds limit"); + // Skip to the very end. + data.skipBits(data.bitsLeft()); + return; + } + + switch (segmentType) { + case SEGMENT_TYPE_DISPLAY_DEFINITION: + if (pageId == service.subtitlePageId) { + service.displayDefinition = parseDisplayDefinition(data); + } + break; + case SEGMENT_TYPE_PAGE_COMPOSITION: + if (pageId == service.subtitlePageId) { + @Nullable PageComposition current = service.pageComposition; + PageComposition pageComposition = parsePageComposition(data, dataFieldLength); + if (pageComposition.state != PAGE_STATE_NORMAL) { + service.pageComposition = pageComposition; + service.regions.clear(); + service.cluts.clear(); + service.objects.clear(); + } else if (current != null && current.version != pageComposition.version) { + service.pageComposition = pageComposition; + } + } + break; + case SEGMENT_TYPE_REGION_COMPOSITION: + @Nullable PageComposition pageComposition = service.pageComposition; + if (pageId == service.subtitlePageId && pageComposition != null) { + RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength); + if (pageComposition.state == PAGE_STATE_NORMAL) { + @Nullable + RegionComposition existingRegionComposition = service.regions.get(regionComposition.id); + if (existingRegionComposition != null) { + regionComposition.mergeFrom(existingRegionComposition); + } + } + service.regions.put(regionComposition.id, regionComposition); + } + break; + case SEGMENT_TYPE_CLUT_DEFINITION: + if (pageId == service.subtitlePageId) { + ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength); + service.cluts.put(clutDefinition.id, clutDefinition); + } else if (pageId == service.ancillaryPageId) { + ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength); + service.ancillaryCluts.put(clutDefinition.id, clutDefinition); + } + break; + case SEGMENT_TYPE_OBJECT_DATA: + if (pageId == service.subtitlePageId) { + ObjectData objectData = parseObjectData(data); + service.objects.put(objectData.id, objectData); + } else if (pageId == service.ancillaryPageId) { + ObjectData objectData = parseObjectData(data); + service.ancillaryObjects.put(objectData.id, objectData); + } + break; + default: + // Do nothing. + break; + } + + // Skip to the next segment. + data.skipBytes(dataFieldLimit - data.getBytePosition()); + } + + /** + * Parses a display definition segment, as defined by ETSI EN 300 743 7.2.1. + */ + private static DisplayDefinition parseDisplayDefinition(ParsableBitArray data) { + data.skipBits(4); // dds_version_number (4). + boolean displayWindowFlag = data.readBit(); + data.skipBits(3); // Skip reserved. + int width = data.readBits(16); + int height = data.readBits(16); + + int horizontalPositionMinimum; + int horizontalPositionMaximum; + int verticalPositionMinimum; + int verticalPositionMaximum; + if (displayWindowFlag) { + horizontalPositionMinimum = data.readBits(16); + horizontalPositionMaximum = data.readBits(16); + verticalPositionMinimum = data.readBits(16); + verticalPositionMaximum = data.readBits(16); + } else { + horizontalPositionMinimum = 0; + horizontalPositionMaximum = width; + verticalPositionMinimum = 0; + verticalPositionMaximum = height; + } + + return new DisplayDefinition(width, height, horizontalPositionMinimum, + horizontalPositionMaximum, verticalPositionMinimum, verticalPositionMaximum); + } + + /** + * Parses a page composition segment, as defined by ETSI EN 300 743 7.2.2. + */ + private static PageComposition parsePageComposition(ParsableBitArray data, int length) { + int timeoutSecs = data.readBits(8); + int version = data.readBits(4); + int state = data.readBits(2); + data.skipBits(2); + int remainingLength = length - 2; + + SparseArray<PageRegion> regions = new SparseArray<>(); + while (remainingLength > 0) { + int regionId = data.readBits(8); + data.skipBits(8); // Skip reserved. + int regionHorizontalAddress = data.readBits(16); + int regionVerticalAddress = data.readBits(16); + remainingLength -= 6; + regions.put(regionId, new PageRegion(regionHorizontalAddress, regionVerticalAddress)); + } + + return new PageComposition(timeoutSecs, version, state, regions); + } + + /** + * Parses a region composition segment, as defined by ETSI EN 300 743 7.2.3. + */ + private static RegionComposition parseRegionComposition(ParsableBitArray data, int length) { + int id = data.readBits(8); + data.skipBits(4); // Skip region_version_number + boolean fillFlag = data.readBit(); + data.skipBits(3); // Skip reserved. + int width = data.readBits(16); + int height = data.readBits(16); + int levelOfCompatibility = data.readBits(3); + int depth = data.readBits(3); + data.skipBits(2); // Skip reserved. + int clutId = data.readBits(8); + int pixelCode8Bit = data.readBits(8); + int pixelCode4Bit = data.readBits(4); + int pixelCode2Bit = data.readBits(2); + data.skipBits(2); // Skip reserved + int remainingLength = length - 10; + + SparseArray<RegionObject> regionObjects = new SparseArray<>(); + while (remainingLength > 0) { + int objectId = data.readBits(16); + int objectType = data.readBits(2); + int objectProvider = data.readBits(2); + int objectHorizontalPosition = data.readBits(12); + data.skipBits(4); // Skip reserved. + int objectVerticalPosition = data.readBits(12); + remainingLength -= 6; + + int foregroundPixelCode = 0; + int backgroundPixelCode = 0; + if (objectType == 0x01 || objectType == 0x02) { // Only seems to affect to char subtitles. + foregroundPixelCode = data.readBits(8); + backgroundPixelCode = data.readBits(8); + remainingLength -= 2; + } + + regionObjects.put(objectId, new RegionObject(objectType, objectProvider, + objectHorizontalPosition, objectVerticalPosition, foregroundPixelCode, + backgroundPixelCode)); + } + + return new RegionComposition(id, fillFlag, width, height, levelOfCompatibility, depth, clutId, + pixelCode8Bit, pixelCode4Bit, pixelCode2Bit, regionObjects); + } + + /** + * Parses a CLUT definition segment, as defined by ETSI EN 300 743 7.2.4. + */ + private static ClutDefinition parseClutDefinition(ParsableBitArray data, int length) { + int clutId = data.readBits(8); + data.skipBits(8); // Skip clut_version_number (4), reserved (4) + int remainingLength = length - 2; + + int[] clutEntries2Bit = generateDefault2BitClutEntries(); + int[] clutEntries4Bit = generateDefault4BitClutEntries(); + int[] clutEntries8Bit = generateDefault8BitClutEntries(); + + while (remainingLength > 0) { + int entryId = data.readBits(8); + int entryFlags = data.readBits(8); + remainingLength -= 2; + + int[] clutEntries; + if ((entryFlags & 0x80) != 0) { + clutEntries = clutEntries2Bit; + } else if ((entryFlags & 0x40) != 0) { + clutEntries = clutEntries4Bit; + } else { + clutEntries = clutEntries8Bit; + } + + int y; + int cr; + int cb; + int t; + if ((entryFlags & 0x01) != 0) { + y = data.readBits(8); + cr = data.readBits(8); + cb = data.readBits(8); + t = data.readBits(8); + remainingLength -= 4; + } else { + y = data.readBits(6) << 2; + cr = data.readBits(4) << 4; + cb = data.readBits(4) << 4; + t = data.readBits(2) << 6; + remainingLength -= 2; + } + + if (y == 0x00) { + cr = 0x00; + cb = 0x00; + t = 0xFF; + } + + int a = (byte) (0xFF - (t & 0xFF)); + int r = (int) (y + (1.40200 * (cr - 128))); + int g = (int) (y - (0.34414 * (cb - 128)) - (0.71414 * (cr - 128))); + int b = (int) (y + (1.77200 * (cb - 128))); + clutEntries[entryId] = getColor(a, Util.constrainValue(r, 0, 255), + Util.constrainValue(g, 0, 255), Util.constrainValue(b, 0, 255)); + } + + return new ClutDefinition(clutId, clutEntries2Bit, clutEntries4Bit, clutEntries8Bit); + } + + /** + * Parses an object data segment, as defined by ETSI EN 300 743 7.2.5. + * + * @return The parsed object data. + */ + private static ObjectData parseObjectData(ParsableBitArray data) { + int objectId = data.readBits(16); + data.skipBits(4); // Skip object_version_number + int objectCodingMethod = data.readBits(2); + boolean nonModifyingColorFlag = data.readBit(); + data.skipBits(1); // Skip reserved. + + @Nullable byte[] topFieldData = null; + @Nullable byte[] bottomFieldData = null; + + if (objectCodingMethod == OBJECT_CODING_STRING) { + int numberOfCodes = data.readBits(8); + // TODO: Parse and use character_codes. + data.skipBits(numberOfCodes * 16); // Skip character_codes. + } else if (objectCodingMethod == OBJECT_CODING_PIXELS) { + int topFieldDataLength = data.readBits(16); + int bottomFieldDataLength = data.readBits(16); + if (topFieldDataLength > 0) { + topFieldData = new byte[topFieldDataLength]; + data.readBytes(topFieldData, 0, topFieldDataLength); + } + if (bottomFieldDataLength > 0) { + bottomFieldData = new byte[bottomFieldDataLength]; + data.readBytes(bottomFieldData, 0, bottomFieldDataLength); + } else { + bottomFieldData = topFieldData; + } + } + + return new ObjectData(objectId, nonModifyingColorFlag, topFieldData, bottomFieldData); + } + + private static int[] generateDefault2BitClutEntries() { + int[] entries = new int[4]; + entries[0] = 0x00000000; + entries[1] = 0xFFFFFFFF; + entries[2] = 0xFF000000; + entries[3] = 0xFF7F7F7F; + return entries; + } + + private static int[] generateDefault4BitClutEntries() { + int[] entries = new int[16]; + entries[0] = 0x00000000; + for (int i = 1; i < entries.length; i++) { + if (i < 8) { + entries[i] = getColor( + 0xFF, + ((i & 0x01) != 0 ? 0xFF : 0x00), + ((i & 0x02) != 0 ? 0xFF : 0x00), + ((i & 0x04) != 0 ? 0xFF : 0x00)); + } else { + entries[i] = getColor( + 0xFF, + ((i & 0x01) != 0 ? 0x7F : 0x00), + ((i & 0x02) != 0 ? 0x7F : 0x00), + ((i & 0x04) != 0 ? 0x7F : 0x00)); + } + } + return entries; + } + + private static int[] generateDefault8BitClutEntries() { + int[] entries = new int[256]; + entries[0] = 0x00000000; + for (int i = 0; i < entries.length; i++) { + if (i < 8) { + entries[i] = getColor( + 0x3F, + ((i & 0x01) != 0 ? 0xFF : 0x00), + ((i & 0x02) != 0 ? 0xFF : 0x00), + ((i & 0x04) != 0 ? 0xFF : 0x00)); + } else { + switch (i & 0x88) { + case 0x00: + entries[i] = getColor( + 0xFF, + (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)), + (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)), + (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00))); + break; + case 0x08: + entries[i] = getColor( + 0x7F, + (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)), + (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)), + (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00))); + break; + case 0x80: + entries[i] = getColor( + 0xFF, + (127 + ((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)), + (127 + ((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)), + (127 + ((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00))); + break; + case 0x88: + entries[i] = getColor( + 0xFF, + (((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)), + (((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)), + (((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00))); + break; + } + } + } + return entries; + } + + private static int getColor(int a, int r, int g, int b) { + return (a << 24) | (r << 16) | (g << 8) | b; + } + + // Static drawing. + + /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */ + private static void paintPixelDataSubBlocks( + ObjectData objectData, + ClutDefinition clutDefinition, + int regionDepth, + int horizontalAddress, + int verticalAddress, + @Nullable Paint paint, + Canvas canvas) { + int[] clutEntries; + if (regionDepth == REGION_DEPTH_8_BIT) { + clutEntries = clutDefinition.clutEntries8Bit; + } else if (regionDepth == REGION_DEPTH_4_BIT) { + clutEntries = clutDefinition.clutEntries4Bit; + } else { + clutEntries = clutDefinition.clutEntries2Bit; + } + paintPixelDataSubBlock(objectData.topFieldData, clutEntries, regionDepth, horizontalAddress, + verticalAddress, paint, canvas); + paintPixelDataSubBlock(objectData.bottomFieldData, clutEntries, regionDepth, horizontalAddress, + verticalAddress + 1, paint, canvas); + } + + /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */ + private static void paintPixelDataSubBlock( + byte[] pixelData, + int[] clutEntries, + int regionDepth, + int horizontalAddress, + int verticalAddress, + @Nullable Paint paint, + Canvas canvas) { + ParsableBitArray data = new ParsableBitArray(pixelData); + int column = horizontalAddress; + int line = verticalAddress; + @Nullable byte[] clutMapTable2To4 = null; + @Nullable byte[] clutMapTable2To8 = null; + @Nullable byte[] clutMapTable4To8 = null; + + while (data.bitsLeft() != 0) { + int dataType = data.readBits(8); + switch (dataType) { + case DATA_TYPE_2BP_CODE_STRING: + @Nullable byte[] clutMapTable2ToX; + if (regionDepth == REGION_DEPTH_8_BIT) { + clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8; + } else if (regionDepth == REGION_DEPTH_4_BIT) { + clutMapTable2ToX = clutMapTable2To4 == null ? defaultMap2To4 : clutMapTable2To4; + } else { + clutMapTable2ToX = null; + } + column = paint2BitPixelCodeString(data, clutEntries, clutMapTable2ToX, column, line, + paint, canvas); + data.byteAlign(); + break; + case DATA_TYPE_4BP_CODE_STRING: + @Nullable byte[] clutMapTable4ToX; + if (regionDepth == REGION_DEPTH_8_BIT) { + clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8; + } else { + clutMapTable4ToX = null; + } + column = paint4BitPixelCodeString(data, clutEntries, clutMapTable4ToX, column, line, + paint, canvas); + data.byteAlign(); + break; + case DATA_TYPE_8BP_CODE_STRING: + column = + paint8BitPixelCodeString( + data, clutEntries, /* clutMapTable= */ null, column, line, paint, canvas); + break; + case DATA_TYPE_24_TABLE_DATA: + clutMapTable2To4 = buildClutMapTable(4, 4, data); + break; + case DATA_TYPE_28_TABLE_DATA: + clutMapTable2To8 = buildClutMapTable(4, 8, data); + break; + case DATA_TYPE_48_TABLE_DATA: + clutMapTable4To8 = buildClutMapTable(16, 8, data); + break; + case DATA_TYPE_END_LINE: + column = horizontalAddress; + line += 2; + break; + default: + // Do nothing. + break; + } + } + } + + /** Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint2BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { + boolean endOfPixelCodeString = false; + do { + int runLength = 0; + int clutIndex = 0; + int peek = data.readBits(2); + if (peek != 0x00) { + runLength = 1; + clutIndex = peek; + } else if (data.readBit()) { + runLength = 3 + data.readBits(3); + clutIndex = data.readBits(2); + } else if (data.readBit()) { + runLength = 1; + } else { + switch (data.readBits(2)) { + case 0x00: + endOfPixelCodeString = true; + break; + case 0x01: + runLength = 2; + break; + case 0x02: + runLength = 12 + data.readBits(4); + clutIndex = data.readBits(2); + break; + case 0x03: + runLength = 29 + data.readBits(8); + clutIndex = data.readBits(2); + break; + } + } + + if (runLength != 0 && paint != null) { + paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]); + canvas.drawRect(column, line, column + runLength, line + 1, paint); + } + + column += runLength; + } while (!endOfPixelCodeString); + + return column; + } + + /** Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint4BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { + boolean endOfPixelCodeString = false; + do { + int runLength = 0; + int clutIndex = 0; + int peek = data.readBits(4); + if (peek != 0x00) { + runLength = 1; + clutIndex = peek; + } else if (!data.readBit()) { + peek = data.readBits(3); + if (peek != 0x00) { + runLength = 2 + peek; + clutIndex = 0x00; + } else { + endOfPixelCodeString = true; + } + } else if (!data.readBit()) { + runLength = 4 + data.readBits(2); + clutIndex = data.readBits(4); + } else { + switch (data.readBits(2)) { + case 0x00: + runLength = 1; + break; + case 0x01: + runLength = 2; + break; + case 0x02: + runLength = 9 + data.readBits(4); + clutIndex = data.readBits(4); + break; + case 0x03: + runLength = 25 + data.readBits(8); + clutIndex = data.readBits(4); + break; + } + } + + if (runLength != 0 && paint != null) { + paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]); + canvas.drawRect(column, line, column + runLength, line + 1, paint); + } + + column += runLength; + } while (!endOfPixelCodeString); + + return column; + } + + /** Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint8BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { + boolean endOfPixelCodeString = false; + do { + int runLength = 0; + int clutIndex = 0; + int peek = data.readBits(8); + if (peek != 0x00) { + runLength = 1; + clutIndex = peek; + } else { + if (!data.readBit()) { + peek = data.readBits(7); + if (peek != 0x00) { + runLength = peek; + clutIndex = 0x00; + } else { + endOfPixelCodeString = true; + } + } else { + runLength = data.readBits(7); + clutIndex = data.readBits(8); + } + } + + if (runLength != 0 && paint != null) { + paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]); + canvas.drawRect(column, line, column + runLength, line + 1, paint); + } + column += runLength; + } while (!endOfPixelCodeString); + + return column; + } + + private static byte[] buildClutMapTable(int length, int bitsPerEntry, ParsableBitArray data) { + byte[] clutMapTable = new byte[length]; + for (int i = 0; i < length; i++) { + clutMapTable[i] = (byte) data.readBits(bitsPerEntry); + } + return clutMapTable; + } + + // Private inner classes. + + /** + * The subtitle service definition. + */ + private static final class SubtitleService { + + public final int subtitlePageId; + public final int ancillaryPageId; + + public final SparseArray<RegionComposition> regions; + public final SparseArray<ClutDefinition> cluts; + public final SparseArray<ObjectData> objects; + public final SparseArray<ClutDefinition> ancillaryCluts; + public final SparseArray<ObjectData> ancillaryObjects; + + @Nullable public DisplayDefinition displayDefinition; + @Nullable public PageComposition pageComposition; + + public SubtitleService(int subtitlePageId, int ancillaryPageId) { + this.subtitlePageId = subtitlePageId; + this.ancillaryPageId = ancillaryPageId; + regions = new SparseArray<>(); + cluts = new SparseArray<>(); + objects = new SparseArray<>(); + ancillaryCluts = new SparseArray<>(); + ancillaryObjects = new SparseArray<>(); + } + + public void reset() { + regions.clear(); + cluts.clear(); + objects.clear(); + ancillaryCluts.clear(); + ancillaryObjects.clear(); + displayDefinition = null; + pageComposition = null; + } + + } + + /** + * Contains the geometry and active area of the subtitle service. + * <p> + * See ETSI EN 300 743 7.2.1 + */ + private static final class DisplayDefinition { + + public final int width; + public final int height; + + public final int horizontalPositionMinimum; + public final int horizontalPositionMaximum; + public final int verticalPositionMinimum; + public final int verticalPositionMaximum; + + public DisplayDefinition(int width, int height, int horizontalPositionMinimum, + int horizontalPositionMaximum, int verticalPositionMinimum, int verticalPositionMaximum) { + this.width = width; + this.height = height; + this.horizontalPositionMinimum = horizontalPositionMinimum; + this.horizontalPositionMaximum = horizontalPositionMaximum; + this.verticalPositionMinimum = verticalPositionMinimum; + this.verticalPositionMaximum = verticalPositionMaximum; + } + + } + + /** + * The page is the definition and arrangement of regions in the screen. + * <p> + * See ETSI EN 300 743 7.2.2 + */ + private static final class PageComposition { + + public final int timeOutSecs; // TODO: Use this or remove it. + public final int version; + public final int state; + public final SparseArray<PageRegion> regions; + + public PageComposition(int timeoutSecs, int version, int state, + SparseArray<PageRegion> regions) { + this.timeOutSecs = timeoutSecs; + this.version = version; + this.state = state; + this.regions = regions; + } + + } + + /** + * A region within a {@link PageComposition}. + * <p> + * See ETSI EN 300 743 7.2.2 + */ + private static final class PageRegion { + + public final int horizontalAddress; + public final int verticalAddress; + + public PageRegion(int horizontalAddress, int verticalAddress) { + this.horizontalAddress = horizontalAddress; + this.verticalAddress = verticalAddress; + } + + } + + /** + * An area of the page composed of a list of objects and a CLUT. + * <p> + * See ETSI EN 300 743 7.2.3 + */ + private static final class RegionComposition { + + public final int id; + public final boolean fillFlag; + public final int width; + public final int height; + public final int levelOfCompatibility; // TODO: Use this or remove it. + public final int depth; + public final int clutId; + public final int pixelCode8Bit; + public final int pixelCode4Bit; + public final int pixelCode2Bit; + public final SparseArray<RegionObject> regionObjects; + + public RegionComposition(int id, boolean fillFlag, int width, int height, + int levelOfCompatibility, int depth, int clutId, int pixelCode8Bit, int pixelCode4Bit, + int pixelCode2Bit, SparseArray<RegionObject> regionObjects) { + this.id = id; + this.fillFlag = fillFlag; + this.width = width; + this.height = height; + this.levelOfCompatibility = levelOfCompatibility; + this.depth = depth; + this.clutId = clutId; + this.pixelCode8Bit = pixelCode8Bit; + this.pixelCode4Bit = pixelCode4Bit; + this.pixelCode2Bit = pixelCode2Bit; + this.regionObjects = regionObjects; + } + + public void mergeFrom(RegionComposition otherRegionComposition) { + SparseArray<RegionObject> otherRegionObjects = otherRegionComposition.regionObjects; + for (int i = 0; i < otherRegionObjects.size(); i++) { + regionObjects.put(otherRegionObjects.keyAt(i), otherRegionObjects.valueAt(i)); + } + } + + } + + /** + * An object within a {@link RegionComposition}. + * <p> + * See ETSI EN 300 743 7.2.3 + */ + private static final class RegionObject { + + public final int type; // TODO: Use this or remove it. + public final int provider; // TODO: Use this or remove it. + public final int horizontalPosition; + public final int verticalPosition; + public final int foregroundPixelCode; // TODO: Use this or remove it. + public final int backgroundPixelCode; // TODO: Use this or remove it. + + public RegionObject(int type, int provider, int horizontalPosition, + int verticalPosition, int foregroundPixelCode, int backgroundPixelCode) { + this.type = type; + this.provider = provider; + this.horizontalPosition = horizontalPosition; + this.verticalPosition = verticalPosition; + this.foregroundPixelCode = foregroundPixelCode; + this.backgroundPixelCode = backgroundPixelCode; + } + + } + + /** + * CLUT family definition containing the color tables for the three bit depths defined + * <p> + * See ETSI EN 300 743 7.2.4 + */ + private static final class ClutDefinition { + + public final int id; + public final int[] clutEntries2Bit; + public final int[] clutEntries4Bit; + public final int[] clutEntries8Bit; + + public ClutDefinition(int id, int[] clutEntries2Bit, int[] clutEntries4Bit, + int[] clutEntries8bit) { + this.id = id; + this.clutEntries2Bit = clutEntries2Bit; + this.clutEntries4Bit = clutEntries4Bit; + this.clutEntries8Bit = clutEntries8bit; + } + + } + + /** + * The textual or graphical representation of an object. + * <p> + * See ETSI EN 300 743 7.2.5 + */ + private static final class ObjectData { + + public final int id; + public final boolean nonModifyingColorFlag; + public final byte[] topFieldData; + public final byte[] bottomFieldData; + + public ObjectData(int id, boolean nonModifyingColorFlag, byte[] topFieldData, + byte[] bottomFieldData) { + this.id = id; + this.nonModifyingColorFlag = nonModifyingColorFlag; + this.topFieldData = topFieldData; + this.bottomFieldData = bottomFieldData; + } + + } + +} |