From 281fc84d15e89bc4cd1d08b80eb614725e3d3326 Mon Sep 17 00:00:00 2001 From: apodob Date: Thu, 5 Aug 2021 15:42:58 +0100 Subject: [PATCH] Add CueDecoder and CueEncoder. This CL introduces two classes: * CueEncoder - encodes list of Cue object into byte array. * CueDecoder - decodes byte array into list of Cue objects. This two classes are necessary in order to push Cues through SampleQueue. This classes are meant to be used by subtitle Extractor. PiperOrigin-RevId: 388932088 --- .../android/exoplayer2/text/CueDecoder.java | 48 +++++++ .../android/exoplayer2/text/CueEncoder.java | 44 ++++++ .../exoplayer2/text/CueSerializationTest.java | 126 ++++++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 library/common/src/main/java/com/google/android/exoplayer2/text/CueDecoder.java create mode 100644 library/common/src/main/java/com/google/android/exoplayer2/text/CueEncoder.java create mode 100644 library/common/src/test/java/com/google/android/exoplayer2/text/CueSerializationTest.java diff --git a/library/common/src/main/java/com/google/android/exoplayer2/text/CueDecoder.java b/library/common/src/main/java/com/google/android/exoplayer2/text/CueDecoder.java new file mode 100644 index 0000000000..d986c32f6e --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/text/CueDecoder.java @@ -0,0 +1,48 @@ +/* + * Copyright 2021 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; + +import android.os.Bundle; +import android.os.Parcel; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.BundleableUtils; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; + +/** Decodes data encoded by {@link CueEncoder}. */ +public final class CueDecoder { + + // key under which list of cues is saved in the bundle + static final String BUNDLED_CUES = "c"; + + /** + * Decodes byte array into list of {@link Cue} objects. + * + * @param bytes byte array produced by {@link CueEncoder} + * @return decoded list of {@link Cue} objects. + */ + public ImmutableList decode(byte[] bytes) { + Parcel parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + Bundle bundle = parcel.readBundle(Bundle.class.getClassLoader()); + parcel.recycle(); + ArrayList bundledCues = + Assertions.checkNotNull(bundle.getParcelableArrayList(BUNDLED_CUES)); + + return BundleableUtils.fromBundleList(Cue.CREATOR, bundledCues); + } +} diff --git a/library/common/src/main/java/com/google/android/exoplayer2/text/CueEncoder.java b/library/common/src/main/java/com/google/android/exoplayer2/text/CueEncoder.java new file mode 100644 index 0000000000..773ec5aecf --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/text/CueEncoder.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 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; + +import android.os.Bundle; +import android.os.Parcel; +import com.google.android.exoplayer2.util.BundleableUtils; +import java.util.ArrayList; +import java.util.List; + +/** Encodes data that can be decoded by {@link CueDecoder}. */ +public final class CueEncoder { + /** + * Encodes an {@link List} of {@link Cue} to a byte array that can be decoded by {@link + * CueDecoder}. + * + * @param cues Cues to be encoded. + * @return The serialized byte array. + */ + public byte[] encode(List cues) { + ArrayList bundledCues = BundleableUtils.toBundleArrayList(cues); + Bundle allCuesBundle = new Bundle(); + allCuesBundle.putParcelableArrayList(CueDecoder.BUNDLED_CUES, bundledCues); + Parcel parcel = Parcel.obtain(); + parcel.writeBundle(allCuesBundle); + byte[] bytes = parcel.marshall(); + parcel.recycle(); + + return bytes; + } +} diff --git a/library/common/src/test/java/com/google/android/exoplayer2/text/CueSerializationTest.java b/library/common/src/test/java/com/google/android/exoplayer2/text/CueSerializationTest.java new file mode 100644 index 0000000000..16e07d77c3 --- /dev/null +++ b/library/common/src/test/java/com/google/android/exoplayer2/text/CueSerializationTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2021 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.Layout; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.SpannedString; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.testutil.truth.SpannedSubject; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test of {@link Cue} serialization and deserialization using {@link CueEncoder} and {@link + * CueDecoder}. + */ +@RunWith(AndroidJUnit4.class) +public class CueSerializationTest { + @Test + public void serializingCueWithoutSpans() { + CueEncoder encoder = new CueEncoder(); + CueDecoder decoder = new CueDecoder(); + Cue cue = + new Cue.Builder() + .setText(SpannedString.valueOf("text")) + .setTextAlignment(Layout.Alignment.ALIGN_CENTER) + .setMultiRowAlignment(Layout.Alignment.ALIGN_NORMAL) + .setLine(5, Cue.LINE_TYPE_NUMBER) + .setLineAnchor(Cue.ANCHOR_TYPE_END) + .setPosition(0.4f) + .setPositionAnchor(Cue.ANCHOR_TYPE_MIDDLE) + .setTextSize(0.2f, Cue.TEXT_SIZE_TYPE_FRACTIONAL) + .setSize(0.8f) + .setWindowColor(Color.CYAN) + .setVerticalType(Cue.VERTICAL_TYPE_RL) + .setShearDegrees(-15f) + .build(); + + // encoding and decoding + byte[] encodedCues = encoder.encode(ImmutableList.of(cue)); + List cuesAfterDecoding = decoder.decode(encodedCues); + Cue cueAfterDecoding = cuesAfterDecoding.get(0); + + assertThat(cueAfterDecoding.text.toString()).isEqualTo(cue.text.toString()); + assertThat(cueAfterDecoding.textAlignment).isEqualTo(cue.textAlignment); + assertThat(cueAfterDecoding.multiRowAlignment).isEqualTo(cue.multiRowAlignment); + assertThat(cueAfterDecoding.line).isEqualTo(cue.line); + assertThat(cueAfterDecoding.lineType).isEqualTo(cue.lineType); + assertThat(cueAfterDecoding.position).isEqualTo(cue.position); + assertThat(cueAfterDecoding.positionAnchor).isEqualTo(cue.positionAnchor); + assertThat(cueAfterDecoding.textSize).isEqualTo(cue.textSize); + assertThat(cueAfterDecoding.textSizeType).isEqualTo(cue.textSizeType); + assertThat(cueAfterDecoding.size).isEqualTo(cue.size); + assertThat(cueAfterDecoding.windowColor).isEqualTo(cue.windowColor); + assertThat(cueAfterDecoding.windowColorSet).isEqualTo(cue.windowColorSet); + assertThat(cueAfterDecoding.verticalType).isEqualTo(cue.verticalType); + assertThat(cueAfterDecoding.shearDegrees).isEqualTo(cue.shearDegrees); + } + + @Test + public void serializingBitmapCueAndCueWithAndroidSpans() { + CueEncoder encoder = new CueEncoder(); + CueDecoder decoder = new CueDecoder(); + Spannable spannable = SpannableString.valueOf("text text"); + spannable.setSpan( + new StrikethroughSpan(), 0, "text".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new StyleSpan(Typeface.BOLD), 0, "text text".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new StyleSpan(Typeface.ITALIC), 0, "text text".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new UnderlineSpan(), + "text ".length(), + "text text".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + Cue textCue = new Cue.Builder().setText(spannable).build(); + Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + Cue bitmapCue = new Cue.Builder().setBitmap(bitmap).build(); + + // encoding and decoding + byte[] encodedCues = encoder.encode(ImmutableList.of(textCue, bitmapCue)); + List cuesAfterDecoding = decoder.decode(encodedCues); + + assertThat(cuesAfterDecoding).hasSize(2); + + Cue textCueAfterDecoding = cuesAfterDecoding.get(0); + Cue bitmapCueAfterDecoding = cuesAfterDecoding.get(1); + + assertThat(textCueAfterDecoding.text.toString()).isEqualTo(textCue.text.toString()); + SpannedSubject.assertThat((Spanned) textCueAfterDecoding.text) + .hasStrikethroughSpanBetween(0, "text".length()); + SpannedSubject.assertThat((Spanned) textCueAfterDecoding.text) + .hasBoldSpanBetween(0, "text text".length()); + SpannedSubject.assertThat((Spanned) textCueAfterDecoding.text) + .hasItalicSpanBetween(0, "text text".length()); + SpannedSubject.assertThat((Spanned) textCueAfterDecoding.text) + .hasUnderlineSpanBetween("text ".length(), "text text".length()); + + assertThat(bitmapCueAfterDecoding.bitmap.sameAs(bitmap)).isTrue(); + } +}