Add EIA-608 (CEA-608) Closed Captioning support for HLS #68

This commit is contained in:
Andrey Udovenko 2014-11-18 14:48:40 -05:00
parent c57484f90a
commit 15d3df6a58
11 changed files with 428 additions and 66 deletions

View File

@ -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<String, Object> metadata) {
public void onId3Metadata(Map<String, Object> 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<ClosedCaption> 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

View File

@ -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<String, Object> metadata);
public interface Id3MetadataListener {
void onId3Metadata(Map<String, Object> metadata);
}
/**
* A listener for receiving closed captions parsed from the media stream.
*/
public interface ClosedCaptionListener {
void onClosedCaption(List<ClosedCaption> 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,13 +488,34 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
}
/* package */ MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>
getId3MetadataRenderer() {
return new MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>() {
@Override
public void onMetadata(Map<String, Object> metadata) {
if (metadataListener != null) {
metadataListener.onMetadata(metadata);
if (id3MetadataListener != null) {
id3MetadataListener.onId3Metadata(metadata);
}
}
};
}
/* package */ MetadataTrackRenderer.MetadataRenderer<List<ClosedCaption>>
getClosedCaptionMetadataRenderer() {
return new MetadataTrackRenderer.MetadataRenderer<List<ClosedCaption>>() {
@Override
public void onMetadata(List<ClosedCaption> metadata) {
if (closedCaptionListener != null) {
closedCaptionListener.onClosedCaption(metadata);
}
}
};
}
@Override
public void onPlayWhenReadyCommitted() {
// Do nothing.

View File

@ -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<Hls
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, player.getMainHandler(), player, 50);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
MetadataTrackRenderer metadataRenderer = new MetadataTrackRenderer(sampleSource,
new Id3Parser(), player, player.getMainHandler().getLooper());
MetadataTrackRenderer<Map<String, Object>> id3Renderer =
new MetadataTrackRenderer<Map<String, Object>>(sampleSource, new Id3Parser(),
player.getId3MetadataRenderer(), player.getMainHandler().getLooper());
MetadataTrackRenderer<List<ClosedCaption>> closedCaptionRenderer =
new MetadataTrackRenderer<List<ClosedCaption>>(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);
}

View File

@ -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;

View File

@ -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<PesPayloadReader> pesPayloadReaders; // Indexed by streamType
private final SparseArray<SampleQueue> sampleQueues; // Indexed by streamType
private final SparseArray<TsPayloadReader> 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<PesPayloadReader>();
sampleQueues = new SparseArray<SampleQueue>();
tsPayloadReaders = new SparseArray<TsPayloadReader>();
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,8 +114,8 @@ 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();
}
}
@ -143,8 +144,7 @@ public final class TsExtractor {
*/
public boolean getSample(int track, SampleHolder out) {
Assertions.checkState(prepared);
Queue<Sample> 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<Sample> sampleQueue;
private final ConcurrentLinkedQueue<Sample> queue;
private MediaFormat mediaFormat;
private boolean foundFirstKeyframe;
private boolean foundLastKeyframe;
protected PesPayloadReader() {
this.sampleQueue = new ConcurrentLinkedQueue<Sample>();
protected SampleQueue() {
this.queue = new ConcurrentLinkedQueue<Sample>();
}
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.
*/

View File

@ -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;
}
}

View File

@ -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<List<ClosedCaption>> {
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<ClosedCaption> 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<ClosedCaption> captions = new ArrayList<ClosedCaption>();
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;
}
}

View File

@ -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<Map<String, Object>> {
@Override
public boolean canParse(String mimeType) {

View File

@ -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 <T> from binary data.
*
* @param <T> The type of the metadata.
*/
public interface MetadataParser {
public interface MetadataParser<T> {
/**
* 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 <T> 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 <T>.
* @throws IOException If a problem occurred parsing the data.
*/
public Map<String, Object> parse(byte[] data, int size)
throws IOException;
public T parse(byte[] data, int size) throws IOException;
}

View File

@ -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 <T> The type of the metadata.
*/
public class MetadataTrackRenderer extends TrackRenderer implements Callback {
public class MetadataTrackRenderer<T> extends TrackRenderer implements Callback {
/**
* An interface for components that process metadata.
*
* @param <T> The type of the metadata.
*/
public interface MetadataRenderer {
public interface MetadataRenderer<T> {
/**
* Invoked each time there is a metadata associated with current playback time.
*
* @param metadata The metadata to process.
*/
void onMetadata(Map<String, Object> 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<T> metadataParser;
private final MetadataRenderer<T> 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<String, Object> 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<T> metadataParser,
MetadataRenderer<T> 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<String, Object> 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<String, Object>) msg.obj);
invokeRendererInternal((T) msg.obj);
return true;
}
return false;
}
private void invokeRendererInternal(Map<String, Object> metadata) {
private void invokeRendererInternal(T metadata) {
metadataRenderer.onMetadata(metadata);
}

View File

@ -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() {}