From 141f3aa836daf22e9daf514ca02dfe0a9474bfb8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 15 Jan 2018 14:13:30 +0000 Subject: [PATCH] Simplify PGS captions + sync with internal tree --- RELEASENOTES.md | 9 +- demos/main/src/main/assets/media.exolist.json | 2 +- extensions/vp9/README.md | 1 + .../jni/generate_libvpx_android_configs.sh | 5 +- .../text/SubtitleDecoderFactory.java | 118 ++++----- .../exoplayer2/text/dvb/DvbDecoder.java | 4 +- .../exoplayer2/text/pgs/PgsBuilder.java | 232 ------------------ .../exoplayer2/text/pgs/PgsDecoder.java | 229 ++++++++++++++++- .../exoplayer2/text/pgs/PgsSubtitle.java | 53 ++-- 9 files changed, 317 insertions(+), 336 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b0907f0a42..6d4347490e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,13 +37,14 @@ HLS source to finish preparation without downloading any chunks, which can significantly reduce initial buffering time ([#3149](https://github.com/google/ExoPlayer/issues/3149)). -* DefaultTrackSelector: Replace `DefaultTrackSelector.Parameters` copy methods - with a builder. -* DefaultTrackSelector: Support disabling of individual text track selection - flags. +* DefaultTrackSelector: + * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. + * Support disabling of individual text track selection flags. * New Cast extension: Simplifies toggling between local and Cast playbacks. * Audio: Support TrueHD passthrough for rechunked samples in Matroska files ([#2147](https://github.com/google/ExoPlayer/issues/2147)). +* Captions: Initial support for PGS subtitles + ([#3008](https://github.com/google/ExoPlayer/issues/3008)). * CacheDataSource: Check periodically if it's possible to read from/write to cache after deciding to bypass cache. * IMA extension: Add support for playing non-Extractor content MediaSources in diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 15183a4a8b..38a0c577ae 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -540,7 +540,7 @@ { "name": "VMAP pre-, mid- and post-rolls, single ads", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=" + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 8dc4974430..9601829c91 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -29,6 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. + ``` NDK_PATH="" ``` diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh index 4aabf2379e..eab6862555 100755 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -102,7 +102,10 @@ for i in $(seq 0 ${limit}); do # configure and make echo "build_android_configs: " echo "configure ${config[${i}]} ${common_params}" - ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags="-isystem $ndk/sysroot/usr/include/arm-linux-androideabi -isystem $ndk/sysroot/usr/include" + ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags=" \ + -isystem $ndk/sysroot/usr/include/arm-linux-androideabi \ + -isystem $ndk/sysroot/usr/include \ + " rm -f libvpx_srcs.txt for f in ${allowed_files}; do # the build system supports multiple different configurations. avoid 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 4720a67bba..139e403844 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 @@ -53,67 +53,69 @@ public interface SubtitleDecoderFactory { /** * Default {@link SubtitleDecoderFactory} implementation. - *

- * The formats supported by this factory are: + * + *

The formats supported by this factory are: + * *

*/ - SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() { + SubtitleDecoderFactory DEFAULT = + new SubtitleDecoderFactory() { - @Override - public boolean supportsFormat(Format format) { - String mimeType = format.sampleMimeType; - return MimeTypes.TEXT_VTT.equals(mimeType) - || MimeTypes.TEXT_SSA.equals(mimeType) - || MimeTypes.APPLICATION_TTML.equals(mimeType) - || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) - || MimeTypes.APPLICATION_SUBRIP.equals(mimeType) - || MimeTypes.APPLICATION_TX3G.equals(mimeType) - || MimeTypes.APPLICATION_CEA608.equals(mimeType) - || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) - || MimeTypes.APPLICATION_CEA708.equals(mimeType) - || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType) - || MimeTypes.APPLICATION_PGS.equals(mimeType); - } - - @Override - public SubtitleDecoder createDecoder(Format format) { - switch (format.sampleMimeType) { - case MimeTypes.TEXT_VTT: - return new WebvttDecoder(); - case MimeTypes.TEXT_SSA: - return new SsaDecoder(format.initializationData); - case MimeTypes.APPLICATION_MP4VTT: - return new Mp4WebvttDecoder(); - case MimeTypes.APPLICATION_TTML: - return new TtmlDecoder(); - case MimeTypes.APPLICATION_SUBRIP: - return new SubripDecoder(); - case MimeTypes.APPLICATION_TX3G: - return new Tx3gDecoder(format.initializationData); - case MimeTypes.APPLICATION_CEA608: - case MimeTypes.APPLICATION_MP4CEA608: - return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel); - case MimeTypes.APPLICATION_CEA708: - 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"); - } - } - - }; + @Override + public boolean supportsFormat(Format format) { + String mimeType = format.sampleMimeType; + return MimeTypes.TEXT_VTT.equals(mimeType) + || MimeTypes.TEXT_SSA.equals(mimeType) + || MimeTypes.APPLICATION_TTML.equals(mimeType) + || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) + || MimeTypes.APPLICATION_SUBRIP.equals(mimeType) + || MimeTypes.APPLICATION_TX3G.equals(mimeType) + || MimeTypes.APPLICATION_CEA608.equals(mimeType) + || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) + || MimeTypes.APPLICATION_CEA708.equals(mimeType) + || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType) + || MimeTypes.APPLICATION_PGS.equals(mimeType); + } + @Override + public SubtitleDecoder createDecoder(Format format) { + switch (format.sampleMimeType) { + case MimeTypes.TEXT_VTT: + return new WebvttDecoder(); + case MimeTypes.TEXT_SSA: + return new SsaDecoder(format.initializationData); + case MimeTypes.APPLICATION_MP4VTT: + return new Mp4WebvttDecoder(); + case MimeTypes.APPLICATION_TTML: + return new TtmlDecoder(); + case MimeTypes.APPLICATION_SUBRIP: + return new SubripDecoder(); + case MimeTypes.APPLICATION_TX3G: + return new Tx3gDecoder(format.initializationData); + case MimeTypes.APPLICATION_CEA608: + case MimeTypes.APPLICATION_MP4CEA608: + return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel); + case MimeTypes.APPLICATION_CEA708: + 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/dvb/DvbDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java index dbdc0434a1..df5b19c052 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java @@ -19,9 +19,7 @@ import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.List; -/** - * A {@link SimpleSubtitleDecoder} for DVB Subtitles. - */ +/** A {@link SimpleSubtitleDecoder} for DVB subtitles. */ public final class DvbDecoder extends SimpleSubtitleDecoder { private final DvbParser parser; 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 deleted file mode 100644 index e67178314d..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java +++ /dev/null @@ -1,232 +0,0 @@ -/* -* -* 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 index 04c3ecd0a3..7ad70397a0 100644 --- 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 @@ -1,26 +1,237 @@ +/* + * Copyright (C) 2018 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.pgs; +import android.graphics.Bitmap; +import com.google.android.exoplayer2.text.Cue; 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; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; -@SuppressWarnings("unused") -public class PgsDecoder extends SimpleSubtitleDecoder { +/** A {@link SimpleSubtitleDecoder} for PGS subtitles. */ +public final class PgsDecoder extends SimpleSubtitleDecoder { + + private static final int SECTION_TYPE_PALETTE = 0x14; + private static final int SECTION_TYPE_BITMAP_PICTURE = 0x15; + private static final int SECTION_TYPE_IDENTIFIER = 0x16; + private static final int SECTION_TYPE_END = 0x80; + + private final ParsableByteArray buffer; + private final CueBuilder cueBuilder; - @SuppressWarnings("unused") public PgsDecoder() { super("PgsDecoder"); + buffer = new ParsableByteArray(); + cueBuilder = new CueBuilder(); } @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)) + buffer.reset(data, size); + cueBuilder.reset(); + ArrayList cues = new ArrayList<>(); + while (buffer.bytesLeft() >= 3) { + Cue cue = readNextSection(buffer, cueBuilder); + if (cue != null) { + cues.add(cue); + } + } + return new PgsSubtitle(Collections.unmodifiableList(cues)); + } + + private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { + int limit = buffer.limit(); + int sectionType = buffer.readUnsignedByte(); + int sectionLength = buffer.readUnsignedShort(); + + int nextSectionPosition = buffer.getPosition() + sectionLength; + if (nextSectionPosition > limit) { + buffer.setPosition(limit); + return null; + } + + Cue cue = null; + switch (sectionType) { + case SECTION_TYPE_PALETTE: + cueBuilder.parsePaletteSection(buffer, sectionLength); break; - } while (buffer.bytesLeft() > 0); - return builder.build(); + case SECTION_TYPE_BITMAP_PICTURE: + cueBuilder.parseBitmapSection(buffer, sectionLength); + break; + case SECTION_TYPE_IDENTIFIER: + cueBuilder.parseIdentifierSection(buffer, sectionLength); + break; + case SECTION_TYPE_END: + cue = cueBuilder.build(); + cueBuilder.reset(); + break; + default: + break; + } + + buffer.setPosition(nextSectionPosition); + return cue; + } + + private static final class CueBuilder { + + private final ParsableByteArray bitmapData; + private final int[] colors; + + private boolean colorsSet; + private int planeWidth; + private int planeHeight; + private int bitmapX; + private int bitmapY; + private int bitmapWidth; + private int bitmapHeight; + + public CueBuilder() { + bitmapData = new ParsableByteArray(); + colors = new int[256]; + } + + private void parsePaletteSection(ParsableByteArray buffer, int sectionLength) { + if ((sectionLength % 5) != 2) { + // Section must be two bytes followed by a whole number of (index, y, cb, cr, a) entries. + return; + } + buffer.skipBytes(2); + + Arrays.fill(colors, 0); + int entryCount = sectionLength / 5; + for (int i = 0; i < entryCount; i++) { + int index = buffer.readUnsignedByte(); + int y = buffer.readUnsignedByte(); + int cr = buffer.readUnsignedByte(); + int cb = buffer.readUnsignedByte(); + int a = buffer.readUnsignedByte(); + 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))); + colors[index] = + (a << 24) + | (Util.constrainValue(r, 0, 255) << 16) + | (Util.constrainValue(g, 0, 255) << 8) + | Util.constrainValue(b, 0, 255); + } + colorsSet = true; + } + + private void parseBitmapSection(ParsableByteArray buffer, int sectionLength) { + if (sectionLength < 4) { + return; + } + buffer.skipBytes(3); // Id (2 bytes), version (1 byte). + boolean isBaseSection = (0x80 & buffer.readUnsignedByte()) != 0; + sectionLength -= 4; + + if (isBaseSection) { + if (sectionLength < 7) { + return; + } + int totalLength = buffer.readUnsignedInt24() - 4; + if (totalLength < 4) { + return; + } + bitmapWidth = buffer.readUnsignedShort(); + bitmapHeight = buffer.readUnsignedShort(); + bitmapData.reset(totalLength - 4); + sectionLength -= 7; + } + + int position = bitmapData.getPosition(); + int limit = bitmapData.limit(); + if (position < limit && sectionLength > 0) { + int bytesToRead = Math.min(sectionLength, limit - position); + buffer.readBytes(bitmapData.data, position, bytesToRead); + bitmapData.setPosition(position + bytesToRead); + } + } + + private void parseIdentifierSection(ParsableByteArray buffer, int sectionLength) { + if (sectionLength < 19) { + return; + } + planeWidth = buffer.readUnsignedShort(); + planeHeight = buffer.readUnsignedShort(); + buffer.skipBytes(11); + bitmapX = buffer.readUnsignedShort(); + bitmapY = buffer.readUnsignedShort(); + } + + public Cue build() { + if (planeWidth == 0 + || planeHeight == 0 + || bitmapWidth == 0 + || bitmapHeight == 0 + || bitmapData.limit() == 0 + || bitmapData.getPosition() != bitmapData.limit() + || !colorsSet) { + return null; + } + // Build the bitmapData. + bitmapData.setPosition(0); + int[] argbBitmapData = new int[bitmapWidth * bitmapHeight]; + int argbBitmapDataIndex = 0; + while (argbBitmapDataIndex < argbBitmapData.length) { + int colorIndex = bitmapData.readUnsignedByte(); + if (colorIndex != 0) { + argbBitmapData[argbBitmapDataIndex++] = colors[colorIndex]; + } else { + int switchBits = bitmapData.readUnsignedByte(); + if (switchBits != 0) { + int runLength = + (switchBits & 0x40) == 0 + ? (switchBits & 0x3F) + : (((switchBits & 0x3F) << 8) | bitmapData.readUnsignedByte()); + int color = (switchBits & 0x80) == 0 ? 0 : colors[bitmapData.readUnsignedByte()]; + Arrays.fill( + argbBitmapData, argbBitmapDataIndex, argbBitmapDataIndex + runLength, color); + argbBitmapDataIndex += runLength; + } + } + } + Bitmap bitmap = + Bitmap.createBitmap(argbBitmapData, bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + // Build the cue. + return new Cue( + bitmap, + (float) bitmapX / planeWidth, + Cue.ANCHOR_TYPE_START, + (float) bitmapY / planeHeight, + Cue.ANCHOR_TYPE_START, + (float) bitmapWidth / planeWidth, + (float) bitmapHeight / planeHeight); + } + + public void reset() { + planeWidth = 0; + planeHeight = 0; + bitmapX = 0; + bitmapY = 0; + bitmapWidth = 0; + bitmapHeight = 0; + bitmapData.reset(0); + colorsSet = false; + } } } 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 index affb2aa15b..9f9af6b6a4 100644 --- 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 @@ -1,54 +1,51 @@ +/* + * Copyright (C) 2018 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.pgs; +import com.google.android.exoplayer2.C; 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 { +/** A representation of a PGS subtitle. */ +/* package */ final class PgsSubtitle implements Subtitle { - private final Cue[] cues; - private final long[] cueTimesUs; + private final List cues; - PgsSubtitle() { - this.cues = null; - this.cueTimesUs = new long[0]; - } - - PgsSubtitle(Cue[] cues, long[] cueTimesUs) { + public PgsSubtitle(List cues) { 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; + return C.INDEX_UNSET; } @Override public int getEventTimeCount() { -return cueTimesUs.length; -} + return 1; + } @Override public long getEventTime(int index) { - Assertions.checkArgument(index >= 0); - Assertions.checkArgument(index < cueTimesUs.length); - return cueTimesUs[index]; + return 0; } @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]); + return cues; } }