diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 03cf5b4e2c..b5ca2e5b49 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder; import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder; +import com.google.android.exoplayer.metadata.ClosedCaption; import com.google.android.exoplayer.metadata.TxxxMetadata; import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.SubtitleView; @@ -56,13 +57,15 @@ import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.TextView; +import java.util.List; import java.util.Map; /** * An activity that plays media using {@link DemoPlayer}. */ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, - DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.MetadataListener { + DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.Id3MetadataListener, + DemoPlayer.ClosedCaptionListener { private static final String TAG = "FullPlayerActivity"; @@ -196,6 +199,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba player.addListener(this); player.setTextListener(this); player.setMetadataListener(this); + player.setClosedCaptionListener(this); player.seekTo(playerPosition); playerNeedsPrepare = true; mediaController.setMediaPlayer(player.getPlayerControl()); @@ -415,7 +419,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba // DemoPlayer.MetadataListener implementation @Override - public void onMetadata(Map metadata) { + public void onId3Metadata(Map metadata) { for (int i = 0; i < metadata.size(); i++) { if (metadata.containsKey(TxxxMetadata.TYPE)) { TxxxMetadata txxxMetadata = (TxxxMetadata) metadata.get(TxxxMetadata.TYPE); @@ -425,6 +429,31 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } } + //DemoPlayer.ClosedCaptioListener implementation + + @Override + public void onClosedCaption(List closedCaptions) { + StringBuilder stringBuilder = new StringBuilder(); + for (ClosedCaption caption : closedCaptions) { + // Ignore control characters and just insert a new line in between words. + if (caption.type == ClosedCaption.TYPE_CTRL) { + if (stringBuilder.length() > 0 + && stringBuilder.charAt(stringBuilder.length() - 1) != '\n') { + stringBuilder.append('\n'); + } + } else if (caption.type == ClosedCaption.TYPE_TEXT) { + stringBuilder.append(caption.text); + } + } + if (stringBuilder.length() > 0 && stringBuilder.charAt(stringBuilder.length() - 1) == '\n') { + stringBuilder.deleteCharAt(stringBuilder.length() - 1); + } + if (stringBuilder.length() > 0) { + subtitleView.setVisibility(View.VISIBLE); + subtitleView.setText(stringBuilder.toString()); + } + } + // SurfaceHolder.Callback implementation @Override diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index ad046fed00..42c10d9a52 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.MultiTrackChunkSource; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.metadata.ClosedCaption; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; import com.google.android.exoplayer.text.TextTrackRenderer; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; @@ -37,6 +38,7 @@ import android.os.Looper; import android.view.Surface; import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; @@ -48,7 +50,7 @@ import java.util.concurrent.CopyOnWriteArrayList; public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer, - MetadataTrackRenderer.MetadataRenderer, StreamingDrmSessionManager.EventListener { + StreamingDrmSessionManager.EventListener { /** * Builds renderers for the player. @@ -137,10 +139,17 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } /** - * A listener for receiving metadata parsed from the media stream. + * A listener for receiving ID3 metadata parsed from the media stream. */ - public interface MetadataListener { - void onMetadata(Map metadata); + public interface Id3MetadataListener { + void onId3Metadata(Map metadata); + } + + /** + * A listener for receiving closed captions parsed from the media stream. + */ + public interface ClosedCaptionListener { + void onClosedCaption(List closedCaptions); } // Constants pulled into this class for convenience. @@ -158,7 +167,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public static final int TYPE_AUDIO = 1; public static final int TYPE_TEXT = 2; public static final int TYPE_TIMED_METADATA = 3; - public static final int TYPE_DEBUG = 4; + public static final int TYPE_CLOSED_CAPTIONS = 4; + public static final int TYPE_DEBUG = 5; private static final int RENDERER_BUILDING_STATE_IDLE = 1; private static final int RENDERER_BUILDING_STATE_BUILDING = 2; @@ -183,7 +193,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private int[] selectedTracks; private TextListener textListener; - private MetadataListener metadataListener; + private Id3MetadataListener id3MetadataListener; + private ClosedCaptionListener closedCaptionListener; private InternalErrorListener internalErrorListener; private InfoListener infoListener; @@ -225,8 +236,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi textListener = listener; } - public void setMetadataListener(MetadataListener listener) { - metadataListener = listener; + public void setMetadataListener(Id3MetadataListener listener) { + id3MetadataListener = listener; + } + + public void setClosedCaptionListener(ClosedCaptionListener listener) { + closedCaptionListener = listener; } public void setSurface(Surface surface) { @@ -473,11 +488,32 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } } - @Override - public void onMetadata(Map metadata) { - if (metadataListener != null) { - metadataListener.onMetadata(metadata); - } + /* package */ MetadataTrackRenderer.MetadataRenderer> + getId3MetadataRenderer() { + return new MetadataTrackRenderer.MetadataRenderer>() { + + @Override + public void onMetadata(Map metadata) { + if (id3MetadataListener != null) { + id3MetadataListener.onId3Metadata(metadata); + } + } + + }; + } + + /* package */ MetadataTrackRenderer.MetadataRenderer> + getClosedCaptionMetadataRenderer() { + return new MetadataTrackRenderer.MetadataRenderer>() { + + @Override + public void onMetadata(List metadata) { + if (closedCaptionListener != null) { + closedCaptionListener.onClosedCaption(metadata); + } + } + + }; } @Override diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 9451add3cb..9d1267d9cd 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -26,6 +26,8 @@ import com.google.android.exoplayer.hls.HlsMasterPlaylist; import com.google.android.exoplayer.hls.HlsMasterPlaylistParser; import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.hls.Variant; +import com.google.android.exoplayer.metadata.ClosedCaption; +import com.google.android.exoplayer.metadata.Eia608Parser; import com.google.android.exoplayer.metadata.Id3Parser; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; import com.google.android.exoplayer.upstream.DataSource; @@ -39,6 +41,8 @@ import android.net.Uri; import java.io.IOException; import java.util.Collections; +import java.util.List; +import java.util.Map; /** * A {@link RendererBuilder} for HLS. @@ -94,13 +98,19 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback> id3Renderer = + new MetadataTrackRenderer>(sampleSource, new Id3Parser(), + player.getId3MetadataRenderer(), player.getMainHandler().getLooper()); + + MetadataTrackRenderer> closedCaptionRenderer = + new MetadataTrackRenderer>(sampleSource, new Eia608Parser(), + player.getClosedCaptionMetadataRenderer(), player.getMainHandler().getLooper()); TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; - renderers[DemoPlayer.TYPE_TIMED_METADATA] = metadataRenderer; + renderers[DemoPlayer.TYPE_TIMED_METADATA] = id3Renderer; + renderers[DemoPlayer.TYPE_CLOSED_CAPTIONS] = closedCaptionRenderer; callback.onRenderers(null, null, renderers); } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index 7a79e1d5b7..bc1f1670af 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -92,6 +92,11 @@ public class MediaFormat { NO_VALUE, NO_VALUE, NO_VALUE, null); } + public static MediaFormat createEia608Format() { + return new MediaFormat(MimeTypes.APPLICATION_EIA608, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, null); + } + @TargetApi(16) private MediaFormat(android.media.MediaFormat format) { this.frameworkMediaFormat = format; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index d92c69c87e..0e0ac7b445 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.metadata.Eia608Parser; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.BitArray; @@ -33,7 +34,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; /** @@ -50,9 +50,10 @@ public final class TsExtractor { private static final int TS_STREAM_TYPE_AAC = 0x0F; private static final int TS_STREAM_TYPE_H264 = 0x1B; private static final int TS_STREAM_TYPE_ID3 = 0x15; + private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 private final BitArray tsPacketBuffer; - private final SparseArray pesPayloadReaders; // Indexed by streamType + private final SparseArray sampleQueues; // Indexed by streamType private final SparseArray tsPayloadReaders; // Indexed by pid private final SamplePool samplePool; /* package */ final long firstSampleTimestamp; @@ -69,7 +70,7 @@ public final class TsExtractor { this.samplePool = samplePool; pendingFirstSampleTimestampAdjustment = true; tsPacketBuffer = new BitArray(); - pesPayloadReaders = new SparseArray(); + sampleQueues = new SparseArray(); tsPayloadReaders = new SparseArray(); tsPayloadReaders.put(TS_PAT_PID, new PatReader()); largestParsedTimestampUs = Long.MIN_VALUE; @@ -84,7 +85,7 @@ public final class TsExtractor { */ public int getTrackCount() { Assertions.checkState(prepared); - return pesPayloadReaders.size(); + return sampleQueues.size(); } /** @@ -97,7 +98,7 @@ public final class TsExtractor { */ public MediaFormat getFormat(int track) { Assertions.checkState(prepared); - return pesPayloadReaders.valueAt(track).getMediaFormat(); + return sampleQueues.valueAt(track).getMediaFormat(); } /** @@ -113,13 +114,13 @@ public final class TsExtractor { * Flushes any pending or incomplete samples, returning them to the sample pool. */ public void clear() { - for (int i = 0; i < pesPayloadReaders.size(); i++) { - pesPayloadReaders.valueAt(i).clear(); + for (int i = 0; i < sampleQueues.size(); i++) { + sampleQueues.valueAt(i).clear(); } } /** - * For each track, discards samples from the next keyframe (inclusive). + * For each track, discards samples from the next key frame (inclusive). */ public void discardFromNextKeyframes() { discardFromNextKeyframes = true; @@ -143,8 +144,7 @@ public final class TsExtractor { */ public boolean getSample(int track, SampleHolder out) { Assertions.checkState(prepared); - Queue queue = pesPayloadReaders.valueAt(track).sampleQueue; - Sample sample = queue.poll(); + Sample sample = sampleQueues.valueAt(track).poll(); if (sample == null) { return false; } @@ -161,7 +161,7 @@ public final class TsExtractor { * for any track. False otherwise. */ public boolean hasSamples() { - for (int i = 0; i < pesPayloadReaders.size(); i++) { + for (int i = 0; i < sampleQueues.size(); i++) { if (hasSamples(i)) { return true; } @@ -177,16 +177,16 @@ public final class TsExtractor { * for the specified track. False otherwise. */ public boolean hasSamples(int track) { - return !pesPayloadReaders.valueAt(track).sampleQueue.isEmpty(); + return !sampleQueues.valueAt(track).isEmpty(); } private boolean checkPrepared() { - int pesPayloadReaderCount = pesPayloadReaders.size(); + int pesPayloadReaderCount = sampleQueues.size(); if (pesPayloadReaderCount == 0) { return false; } for (int i = 0; i < pesPayloadReaderCount; i++) { - if (!pesPayloadReaders.valueAt(i).hasMediaFormat()) { + if (!sampleQueues.valueAt(i).hasMediaFormat()) { return false; } } @@ -342,7 +342,7 @@ public final class TsExtractor { tsBuffer.skipBytes(esInfoLength); entriesSize -= esInfoLength + 5; - if (pesPayloadReaders.get(streamType) != null) { + if (sampleQueues.get(streamType) != null) { continue; } @@ -352,7 +352,9 @@ public final class TsExtractor { pesPayloadReader = new AdtsReader(); break; case TS_STREAM_TYPE_H264: - pesPayloadReader = new H264Reader(); + SeiReader seiReader = new SeiReader(); + sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader); + pesPayloadReader = new H264Reader(seiReader); break; case TS_STREAM_TYPE_ID3: pesPayloadReader = new Id3Reader(); @@ -360,7 +362,7 @@ public final class TsExtractor { } if (pesPayloadReader != null) { - pesPayloadReaders.put(streamType, pesPayloadReader); + sampleQueues.put(streamType, pesPayloadReader); tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader)); } } @@ -469,18 +471,18 @@ public final class TsExtractor { } /** - * Extracts individual samples from continuous byte stream. + * A collection of extracted samples. */ - private abstract class PesPayloadReader { + private abstract class SampleQueue { - public final ConcurrentLinkedQueue sampleQueue; + private final ConcurrentLinkedQueue queue; private MediaFormat mediaFormat; private boolean foundFirstKeyframe; private boolean foundLastKeyframe; - protected PesPayloadReader() { - this.sampleQueue = new ConcurrentLinkedQueue(); + protected SampleQueue() { + this.queue = new ConcurrentLinkedQueue(); } public boolean hasMediaFormat() { @@ -495,16 +497,22 @@ public final class TsExtractor { this.mediaFormat = mediaFormat; } - public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs); - public void clear() { - Sample toRecycle = sampleQueue.poll(); + Sample toRecycle = queue.poll(); while (toRecycle != null) { samplePool.recycle(toRecycle); - toRecycle = sampleQueue.poll(); + toRecycle = queue.poll(); } } + public Sample poll() { + return queue.poll(); + } + + public boolean isEmpty() { + return queue.isEmpty(); + } + /** * Creates a new Sample and adds it to the queue. * @@ -534,7 +542,7 @@ public final class TsExtractor { adjustTimestamp(sample); if (foundFirstKeyframe && !foundLastKeyframe) { largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); - sampleQueue.add(sample); + queue.add(sample); } else { samplePool.recycle(sample); } @@ -558,6 +566,15 @@ public final class TsExtractor { } + /** + * Extracts individual samples from continuous byte stream. + */ + private abstract class PesPayloadReader extends SampleQueue { + + public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs); + + } + /** * Parses a continuous H264 byte stream and extracts individual frames. */ @@ -567,9 +584,15 @@ public final class TsExtractor { private static final int NAL_UNIT_TYPE_AUD = 9; private static final int NAL_UNIT_TYPE_SPS = 7; + public final SeiReader seiReader; + // Used to store uncompleted sample data. private Sample currentSample; + public H264Reader(SeiReader seiReader) { + this.seiReader = seiReader; + } + @Override public void clear() { super.clear(); @@ -593,6 +616,7 @@ public final class TsExtractor { if (!hasMediaFormat() && (currentSample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) { parseMediaFormat(currentSample); } + seiReader.read(currentSample.data, currentSample.size, pesTimeUs); addSample(currentSample); } currentSample = samplePool.get(); @@ -756,6 +780,39 @@ public final class TsExtractor { } + /** + * Parses a SEI data from H.264 frames and extracts samples with closed captions data. + */ + private class SeiReader extends SampleQueue { + + // SEI data, used for Closed Captions. + private static final int NAL_UNIT_TYPE_SEI = 6; + + private final BitArray seiBuffer; + + public SeiReader() { + setMediaFormat(MediaFormat.createEia608Format()); + seiBuffer = new BitArray(); + } + + @SuppressLint("InlinedApi") + public void read(byte[] data, int size, long pesTimeUs) { + seiBuffer.reset(data, size); + while (seiBuffer.bytesLeft() > 0) { + int seiStart = seiBuffer.findNextNalUnit(NAL_UNIT_TYPE_SEI, 0); + if (seiStart == seiBuffer.bytesLeft()) { + return; + } + seiBuffer.skipBytes(seiStart + 4); + int ccDataSize = Eia608Parser.parseHeader(seiBuffer); + if (ccDataSize > 0) { + addSample(seiBuffer, ccDataSize, pesTimeUs, MediaExtractor.SAMPLE_FLAG_SYNC); + } + } + } + + } + /** * Parses a continuous ADTS byte stream and extracts individual frames. */ diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/ClosedCaption.java b/library/src/main/java/com/google/android/exoplayer/metadata/ClosedCaption.java new file mode 100644 index 0000000000..bcaa6bb8bb --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/metadata/ClosedCaption.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2014 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.exoplayer.metadata; + +/** + * A Closed Caption that contains textual data associated with time indices. + */ +public final class ClosedCaption { + + /** + * Identifies closed captions with control characters. + */ + public static final int TYPE_CTRL = 0; + /** + * Identifies closed captions with textual information. + */ + public static final int TYPE_TEXT = 1; + + /** + * The type of the closed caption data. If equals to {@link #TYPE_TEXT} the {@link #text} field + * has the textual data, if equals to {@link #TYPE_CTRL} the {@link #text} field has two control + * characters (C1, C2). + */ + public final int type; + /** + * Contains text or two control characters. + */ + public final String text; + + public ClosedCaption(int type, String text) { + this.type = type; + this.text = text; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Eia608Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Eia608Parser.java new file mode 100644 index 0000000000..8166fe610e --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Eia608Parser.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2014 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.exoplayer.metadata; + +import com.google.android.exoplayer.util.BitArray; +import com.google.android.exoplayer.util.MimeTypes; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Facilitates the extraction and parsing of EIA-608 (a.k.a. "line 21 captions" and "CEA-608") + * Closed Captions from the SEI data block from H.264. + */ +public class Eia608Parser implements MetadataParser> { + + private static final int PAYLOAD_TYPE_CC = 4; + private static final int COUNTRY_CODE = 0xB5; + private static final int PROVIDER_CODE = 0x31; + private static final int USER_ID = 0x47413934; // "GA94" + private static final int USER_DATA_TYPE_CODE = 0x3; + + private static final int[] SPECIAL_CHARACTER_SET = new int[] { + 0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol + 0xB0, // 31: 176 '°' "Degree Sign" + 0xBD, // 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol) + 0xBF, // 33: 191 '¿' "Inverted Question Mark" + 0x2122, // 34: "Trade Mark Sign" (tm superscript) + 0xA2, // 35: 162 '¢' "Cent Sign" + 0xA3, // 36: 163 '£' "Pound Sign" - pounds sterling + 0x266A, // 37: "Eighth Note" - music note + 0xE0, // 38: 224 'à' "Latin small letter A with grave" + 0x20, // 39: TRANSPARENT SPACE - for now use ordinary space + 0xE8, // 3A: 232 'è' "Latin small letter E with grave" + 0xE2, // 3B: 226 'â' "Latin small letter A with circumflex" + 0xEA, // 3C: 234 'ê' "Latin small letter E with circumflex" + 0xEE, // 3D: 238 'î' "Latin small letter I with circumflex" + 0xF4, // 3E: 244 'ô' "Latin small letter O with circumflex" + 0xFB // 3F: 251 'û' "Latin small letter U with circumflex" + }; + + @Override + public boolean canParse(String mimeType) { + return mimeType.equals(MimeTypes.APPLICATION_EIA608); + } + + @Override + public List parse(byte[] data, int size) throws IOException { + if (size <= 0) { + return null; + } + BitArray seiBuffer = new BitArray(data, size); + seiBuffer.skipBits(3); // reserved + process_cc_data_flag + zero_bit + int ccCount = seiBuffer.readBits(5); + seiBuffer.skipBytes(1); + + List captions = new ArrayList(); + + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < ccCount; i++) { + seiBuffer.skipBits(5); // one_bit + reserved + boolean ccValid = seiBuffer.readBit(); + if (!ccValid) { + seiBuffer.skipBits(18); + continue; + } + int ccType = seiBuffer.readBits(2); + if (ccType != 0 && ccType != 1) { + // Not EIA-608 captions. + seiBuffer.skipBits(16); + continue; + } + seiBuffer.skipBits(1); + byte ccData1 = (byte) seiBuffer.readBits(7); + seiBuffer.skipBits(1); + byte ccData2 = (byte) seiBuffer.readBits(7); + + if ((ccData1 == 0x11) && ((ccData2 & 0x70) == 0x30)) { + ccData2 &= 0xF; + stringBuilder.append((char) SPECIAL_CHARACTER_SET[ccData2]); + continue; + } + + // Control character. + if (ccData1 < 0x20) { + if (stringBuilder.length() > 0) { + captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString())); + stringBuilder.setLength(0); + } + captions.add(new ClosedCaption(ClosedCaption.TYPE_CTRL, + new String(new char[]{(char) ccData1, (char) ccData2}))); + continue; + } + + stringBuilder.append((char) ccData1); + if (ccData2 != 0) { + stringBuilder.append((char) ccData2); + } + + } + + if (stringBuilder.length() > 0) { + captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString())); + } + + return Collections.unmodifiableList(captions); + } + + /** + * Parses the beginning of SEI data and returns the size of underlying contains closed captions + * data following the header. Returns 0 if the SEI doesn't contain any closed captions data. + * + * @param seiBuffer The buffer to read from. + * @return The size of closed captions data. + */ + public static int parseHeader(BitArray seiBuffer) { + int b = 0; + int payloadType = 0; + + do { + b = seiBuffer.readUnsignedByte(); + payloadType += b; + } while (b == 0xFF); + + if (payloadType != PAYLOAD_TYPE_CC) { + return 0; + } + + int payloadSize = 0; + do { + b = seiBuffer.readUnsignedByte(); + payloadSize += b; + } while (b == 0xFF); + + if (payloadSize <= 0) { + return 0; + } + + int countryCode = seiBuffer.readUnsignedByte(); + if (countryCode != COUNTRY_CODE) { + return 0; + } + int providerCode = seiBuffer.readBits(16); + if (providerCode != PROVIDER_CODE) { + return 0; + } + int userIdentifier = seiBuffer.readBits(32); + if (userIdentifier != USER_ID) { + return 0; + } + int userDataTypeCode = seiBuffer.readUnsignedByte(); + if (userDataTypeCode != USER_DATA_TYPE_CODE) { + return 0; + } + return payloadSize; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index ea68984e37..efa3b66147 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -27,7 +27,7 @@ import java.util.Map; /** * Extracts individual TXXX text frames from raw ID3 data. */ -public class Id3Parser implements MetadataParser { +public class Id3Parser implements MetadataParser> { @Override public boolean canParse(String mimeType) { diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java index 8bfaca8b77..654f549b18 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java @@ -16,30 +16,30 @@ package com.google.android.exoplayer.metadata; import java.io.IOException; -import java.util.Map; /** - * Parses metadata objects from binary data. + * Parses objects of type from binary data. + * + * @param The type of the metadata. */ -public interface MetadataParser { +public interface MetadataParser { /** * Checks whether the parser supports a given mime type. * - * @param mimeType A subtitle mime type. + * @param mimeType A metadata mime type. * @return Whether the mime type is supported. */ public boolean canParse(String mimeType); /** - * Parses a map of metadata type to metadata objects from the provided binary data. + * Parses metadata objects of type from the provided binary data. * * @param data The raw binary data from which to parse the metadata. * @param size The size of the input data. - * @return A parsed {@link Map} of metadata type to metadata objects. + * @return @return A parsed metadata object of type . * @throws IOException If a problem occurred parsing the data. */ - public Map parse(byte[] data, int size) - throws IOException; + public T parse(byte[] data, int size) throws IOException; } diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java index 48ed78f20c..147a222c4f 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java @@ -28,32 +28,35 @@ import android.os.Looper; import android.os.Message; import java.io.IOException; -import java.util.Map; /** * A {@link TrackRenderer} for metadata embedded in a media stream. + * + * @param The type of the metadata. */ -public class MetadataTrackRenderer extends TrackRenderer implements Callback { +public class MetadataTrackRenderer extends TrackRenderer implements Callback { /** * An interface for components that process metadata. + * + * @param The type of the metadata. */ - public interface MetadataRenderer { + public interface MetadataRenderer { /** * Invoked each time there is a metadata associated with current playback time. * * @param metadata The metadata to process. */ - void onMetadata(Map metadata); + void onMetadata(T metadata); } private static final int MSG_INVOKE_RENDERER = 0; private final SampleSource source; - private final MetadataParser metadataParser; - private final MetadataRenderer metadataRenderer; + private final MetadataParser metadataParser; + private final MetadataRenderer metadataRenderer; private final Handler metadataHandler; private final MediaFormatHolder formatHolder; private final SampleHolder sampleHolder; @@ -63,7 +66,7 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback { private boolean inputStreamEnded; private long pendingMetadataTimestamp; - private Map pendingMetadata; + private T pendingMetadata; /** * @param source A source from which samples containing metadata can be read. @@ -75,8 +78,8 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback { * obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the * renderer should be invoked directly on the player's internal rendering thread. */ - public MetadataTrackRenderer(SampleSource source, MetadataParser metadataParser, - MetadataRenderer metadataRenderer, Looper metadataRendererLooper) { + public MetadataTrackRenderer(SampleSource source, MetadataParser metadataParser, + MetadataRenderer metadataRenderer, Looper metadataRendererLooper) { this.source = Assertions.checkNotNull(source); this.metadataParser = Assertions.checkNotNull(metadataParser); this.metadataRenderer = Assertions.checkNotNull(metadataRenderer); @@ -185,7 +188,7 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback { return true; } - private void invokeRenderer(Map metadata) { + private void invokeRenderer(T metadata) { if (metadataHandler != null) { metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget(); } else { @@ -198,13 +201,13 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback { public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_INVOKE_RENDERER: - invokeRendererInternal((Map) msg.obj); + invokeRendererInternal((T) msg.obj); return true; } return false; } - private void invokeRendererInternal(Map metadata) { + private void invokeRendererInternal(T metadata) { metadataRenderer.onMetadata(metadata); } diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index 55a59d1867..dd9d700b02 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -38,6 +38,7 @@ public class MimeTypes { public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; + public static final String APPLICATION_EIA608 = BASE_TYPE_APPLICATION + "/eia-608"; public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml"; private MimeTypes() {}