From b610e1144338a93e89604b095ac1792b72f7a5af Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Fri, 5 Jan 2018 23:10:51 -0500 Subject: [PATCH] PGS subtitle decoding support --- .../text/SubtitleDecoderFactory.java | 6 +- .../exoplayer2/text/pgs/PgsBuilder.java | 232 ++++++++++++++++++ .../exoplayer2/text/pgs/PgsDecoder.java | 26 ++ .../exoplayer2/text/pgs/PgsSubtitle.java | 54 ++++ 4 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java 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 6a9b83a015..4720a67bba 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 @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.cea.Cea608Decoder; import com.google.android.exoplayer2.text.cea.Cea708Decoder; import com.google.android.exoplayer2.text.dvb.DvbDecoder; +import com.google.android.exoplayer2.text.pgs.PgsDecoder; import com.google.android.exoplayer2.text.ssa.SsaDecoder; import com.google.android.exoplayer2.text.subrip.SubripDecoder; import com.google.android.exoplayer2.text.ttml.TtmlDecoder; @@ -80,7 +81,8 @@ public interface SubtitleDecoderFactory { || MimeTypes.APPLICATION_CEA608.equals(mimeType) || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) || MimeTypes.APPLICATION_CEA708.equals(mimeType) - || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType); + || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType) + || MimeTypes.APPLICATION_PGS.equals(mimeType); } @Override @@ -105,6 +107,8 @@ public interface SubtitleDecoderFactory { return new Cea708Decoder(format.accessibilityChannel); case MimeTypes.APPLICATION_DVBSUBS: return new DvbDecoder(format.initializationData); + case MimeTypes.APPLICATION_PGS: + return new PgsDecoder(); default: throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java new file mode 100644 index 0000000000..e67178314d --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java @@ -0,0 +1,232 @@ +/* +* +* Sources for this implementation PGS decoding can be founder below +* +* http://exar.ch/suprip/hddvd.php +* http://forum.doom9.org/showthread.php?t=124105 +* http://www.equasys.de/colorconversion.html + */ + +package com.google.android.exoplayer2.text.pgs; + +import android.graphics.Bitmap; + +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.util.ParsableByteArray; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +class PgsBuilder { + + private static final int SECTION_PALETTE = 0x14; + private static final int SECTION_BITMAP_PICTURE = 0x15; + private static final int SECTION_IDENTIFIER = 0x16; + private static final int SECTION_END = 0x80; + + private List list = new ArrayList<>(); + private Holder holder = new Holder(); + + boolean readNextSection(ParsableByteArray buffer) { + + if (buffer.bytesLeft() < 3) + return false; + + int sectionId = buffer.readUnsignedByte(); + int sectionLength = buffer.readUnsignedShort(); + switch(sectionId) { + case SECTION_PALETTE: + holder.parsePaletteIndexes(buffer, sectionLength); + break; + case SECTION_BITMAP_PICTURE: + holder.fetchBitmapData(buffer, sectionLength); + break; + case SECTION_IDENTIFIER: + holder.fetchIdentifierData(buffer, sectionLength); + break; + case SECTION_END: + list.add(holder); + holder = new Holder(); + break; + default: + buffer.skipBytes(Math.min(sectionLength, buffer.bytesLeft())); + break; + } + return true; + } + + public Subtitle build() { + + if (list.isEmpty()) + return new PgsSubtitle(); + + Cue[] cues = new Cue[list.size()]; + long[] cueStartTimes = new long[list.size()]; + int index = 0; + for (Holder curr : list) { + cues[index] = curr.build(); + cueStartTimes[index++] = curr.start_time; + } + return new PgsSubtitle(cues, cueStartTimes); + } + + private class Holder { + + private int[] colors = null; + private ByteBuffer rle = null; + + Bitmap bitmap = null; + int plane_width = 0; + int plane_height = 0; + int bitmap_width = 0; + int bitmap_height = 0; + public int x = 0; + public int y = 0; + long start_time = 0; + + public Cue build() { + if (rle == null || !createBitmap(new ParsableByteArray(rle.array(), rle.position()))) + return null; + float left = (float) x / plane_width; + float top = (float) y / plane_height; + return new Cue(bitmap, left, Cue.ANCHOR_TYPE_START, top, Cue.ANCHOR_TYPE_START, + (float) bitmap_width / plane_width, (float) bitmap_height / plane_height); + } + + private void parsePaletteIndexes(ParsableByteArray buffer, int dataSize) { + // must be a multi of 5 for index, y, cb, cr, alpha + if (dataSize == 0 || (dataSize - 2) % 5 != 0) + return; + // skip first two bytes + buffer.skipBytes(2); + dataSize -= 2; + colors = new int[256]; + while (dataSize > 0) { + int index = buffer.readUnsignedByte(); + int color_y = buffer.readUnsignedByte() - 16; + int color_cr = buffer.readUnsignedByte() - 128; + int color_cb = buffer.readUnsignedByte() - 128; + int color_alpha = buffer.readUnsignedByte(); + dataSize -= 5; + if (index >= colors.length) + continue; + + int color_r = (int) Math.min(Math.max(Math.round(1.1644 * color_y + 1.793 * color_cr), 0), 255); + int color_g = (int) Math.min(Math.max(Math.round(1.1644 * color_y + (-0.213 * color_cr) + (-0.533 * color_cb)), 0), 255); + int color_b = (int) Math.min(Math.max(Math.round(1.1644 * color_y + 2.112 * color_cb), 0), 255); + //ARGB_8888 + colors[index] = (color_alpha << 24) | (color_r << 16) | (color_g << 8) | color_b; + } + } + + private void fetchBitmapData(ParsableByteArray buffer, int dataSize) { + if (dataSize <= 4) { + buffer.skipBytes(dataSize); + return; + } + // skip id field (2 bytes) + // skip version field + buffer.skipBytes(3); + dataSize -= 3; + + // check to see if this section is an appended section of the base section with + // width and height values + dataSize -= 1; // decrement first + if ((0x80 & buffer.readUnsignedByte()) > 0) { + if (dataSize < 3) { + buffer.skipBytes(dataSize); + return; + } + int full_len = buffer.readUnsignedInt24(); + dataSize -= 3; + if (full_len <= 4) { + buffer.skipBytes(dataSize); + return; + } + bitmap_width = buffer.readUnsignedShort(); + dataSize -= 2; + bitmap_height = buffer.readUnsignedShort(); + dataSize -= 2; + rle = ByteBuffer.allocate(full_len - 4); // don't include width & height + buffer.readBytes(rle, Math.min(dataSize, rle.capacity())); + } else if (rle != null) { + int postSkip = dataSize > rle.capacity() ? dataSize - rle.capacity() : 0; + buffer.readBytes(rle, Math.min(dataSize, rle.capacity())); + buffer.skipBytes(postSkip); + } + } + + private void fetchIdentifierData(ParsableByteArray buffer, int dataSize) { + if (dataSize < 4) { + buffer.skipBytes(dataSize); + return; + } + plane_width = buffer.readUnsignedShort(); + plane_height = buffer.readUnsignedShort(); + dataSize -= 4; + if (dataSize < 15) { + buffer.skipBytes(dataSize); + return; + } + // skip next 11 bytes + buffer.skipBytes(11); + x = buffer.readUnsignedShort(); + y = buffer.readUnsignedShort(); + dataSize -= 15; + buffer.skipBytes(dataSize); + } + + private boolean createBitmap(ParsableByteArray rle) { + if (bitmap_width == 0 || bitmap_height == 0 + || rle == null || rle.bytesLeft() == 0 + || colors == null || colors.length == 0) + return false; + int[] argb = new int[bitmap_width * bitmap_height]; + int currPixel = 0; + int nextbits, pixel_code, switchbits; + int number_of_pixels; + int line = 0; + while (rle.bytesLeft() > 0 && line < bitmap_height) { + boolean end_of_line = false; + do { + nextbits = rle.readUnsignedByte(); + if (nextbits != 0) { + pixel_code = nextbits; + number_of_pixels = 1; + } else { + switchbits = rle.readUnsignedByte(); + if ((switchbits & 0x80) == 0) { + pixel_code = 0; + if ((switchbits & 0x40) == 0) { + if (switchbits > 0) { + number_of_pixels = switchbits; + } else { + end_of_line = true; + ++line; + continue; + } + } else { + number_of_pixels = ((switchbits & 0x3f) << 8) | rle.readUnsignedByte(); + } + } else { + if ((switchbits & 0x40) == 0) { + number_of_pixels = switchbits & 0x3f; + pixel_code = rle.readUnsignedByte(); + } else { + number_of_pixels = ((switchbits & 0x3f) << 8) | rle.readUnsignedByte(); + pixel_code = rle.readUnsignedByte(); + } + } + } + Arrays.fill(argb, currPixel, currPixel + number_of_pixels, colors[pixel_code]); + currPixel += number_of_pixels; + } while (!end_of_line); + } + bitmap = Bitmap.createBitmap(argb, 0, bitmap_width, bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888); + return bitmap != null; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java new file mode 100644 index 0000000000..04c3ecd0a3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -0,0 +1,26 @@ +package com.google.android.exoplayer2.text.pgs; + +import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.text.SubtitleDecoderException; +import com.google.android.exoplayer2.util.ParsableByteArray; + +@SuppressWarnings("unused") +public class PgsDecoder extends SimpleSubtitleDecoder { + + @SuppressWarnings("unused") + public PgsDecoder() { + super("PgsDecoder"); + } + + @Override + protected Subtitle decode(byte[] data, int size, boolean reset) throws SubtitleDecoderException { + ParsableByteArray buffer = new ParsableByteArray(data, size); + PgsBuilder builder = new PgsBuilder(); + do { + if (!builder.readNextSection(buffer)) + break; + } while (buffer.bytesLeft() > 0); + return builder.build(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java new file mode 100644 index 0000000000..affb2aa15b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java @@ -0,0 +1,54 @@ +package com.google.android.exoplayer2.text.pgs; + +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +import java.util.Collections; +import java.util.List; + +public class PgsSubtitle implements Subtitle { + + private final Cue[] cues; + private final long[] cueTimesUs; + + PgsSubtitle() { + this.cues = null; + this.cueTimesUs = new long[0]; + } + + PgsSubtitle(Cue[] cues, long[] cueTimesUs) { + this.cues = cues; + this.cueTimesUs = cueTimesUs; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); + return index < cueTimesUs.length ? index : -1; + } + + @Override + public int getEventTimeCount() { +return cueTimesUs.length; +} + + @Override + public long getEventTime(int index) { + Assertions.checkArgument(index >= 0); + Assertions.checkArgument(index < cueTimesUs.length); + return cueTimesUs[index]; + } + + @Override + public List getCues(long timeUs) { + int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); + if (index == -1 || cues == null || cues[index] == null) { + // timeUs is earlier than the start of the first cue, or we have an empty cue. + return Collections.emptyList(); + } + else + return Collections.singletonList(cues[index]); + } +}