Add EIA-608 (CEA-608) Closed Captioning support for HLS #68
This commit is contained in:
parent
c57484f90a
commit
15d3df6a58
@ -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.DemoPlayer.RendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder;
|
import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder;
|
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.metadata.TxxxMetadata;
|
||||||
import com.google.android.exoplayer.text.CaptionStyleCompat;
|
import com.google.android.exoplayer.text.CaptionStyleCompat;
|
||||||
import com.google.android.exoplayer.text.SubtitleView;
|
import com.google.android.exoplayer.text.SubtitleView;
|
||||||
@ -56,13 +57,15 @@ import android.widget.PopupMenu;
|
|||||||
import android.widget.PopupMenu.OnMenuItemClickListener;
|
import android.widget.PopupMenu.OnMenuItemClickListener;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An activity that plays media using {@link DemoPlayer}.
|
* An activity that plays media using {@link DemoPlayer}.
|
||||||
*/
|
*/
|
||||||
public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener,
|
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";
|
private static final String TAG = "FullPlayerActivity";
|
||||||
|
|
||||||
@ -196,6 +199,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
|
|||||||
player.addListener(this);
|
player.addListener(this);
|
||||||
player.setTextListener(this);
|
player.setTextListener(this);
|
||||||
player.setMetadataListener(this);
|
player.setMetadataListener(this);
|
||||||
|
player.setClosedCaptionListener(this);
|
||||||
player.seekTo(playerPosition);
|
player.seekTo(playerPosition);
|
||||||
playerNeedsPrepare = true;
|
playerNeedsPrepare = true;
|
||||||
mediaController.setMediaPlayer(player.getPlayerControl());
|
mediaController.setMediaPlayer(player.getPlayerControl());
|
||||||
@ -415,7 +419,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
|
|||||||
// DemoPlayer.MetadataListener implementation
|
// DemoPlayer.MetadataListener implementation
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMetadata(Map<String, Object> metadata) {
|
public void onId3Metadata(Map<String, Object> metadata) {
|
||||||
for (int i = 0; i < metadata.size(); i++) {
|
for (int i = 0; i < metadata.size(); i++) {
|
||||||
if (metadata.containsKey(TxxxMetadata.TYPE)) {
|
if (metadata.containsKey(TxxxMetadata.TYPE)) {
|
||||||
TxxxMetadata txxxMetadata = (TxxxMetadata) metadata.get(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
|
// SurfaceHolder.Callback implementation
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -26,6 +26,7 @@ import com.google.android.exoplayer.TrackRenderer;
|
|||||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||||
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
|
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
|
||||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
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.metadata.MetadataTrackRenderer;
|
||||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||||
@ -37,6 +38,7 @@ import android.os.Looper;
|
|||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
@ -48,7 +50,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|||||||
public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
|
public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
|
||||||
DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener,
|
DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener,
|
||||||
MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer,
|
MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer,
|
||||||
MetadataTrackRenderer.MetadataRenderer, StreamingDrmSessionManager.EventListener {
|
StreamingDrmSessionManager.EventListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds renderers for the player.
|
* 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 {
|
public interface Id3MetadataListener {
|
||||||
void onMetadata(Map<String, Object> metadata);
|
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.
|
// 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_AUDIO = 1;
|
||||||
public static final int TYPE_TEXT = 2;
|
public static final int TYPE_TEXT = 2;
|
||||||
public static final int TYPE_TIMED_METADATA = 3;
|
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_IDLE = 1;
|
||||||
private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
|
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 int[] selectedTracks;
|
||||||
|
|
||||||
private TextListener textListener;
|
private TextListener textListener;
|
||||||
private MetadataListener metadataListener;
|
private Id3MetadataListener id3MetadataListener;
|
||||||
|
private ClosedCaptionListener closedCaptionListener;
|
||||||
private InternalErrorListener internalErrorListener;
|
private InternalErrorListener internalErrorListener;
|
||||||
private InfoListener infoListener;
|
private InfoListener infoListener;
|
||||||
|
|
||||||
@ -225,8 +236,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
textListener = listener;
|
textListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMetadataListener(MetadataListener listener) {
|
public void setMetadataListener(Id3MetadataListener listener) {
|
||||||
metadataListener = listener;
|
id3MetadataListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClosedCaptionListener(ClosedCaptionListener listener) {
|
||||||
|
closedCaptionListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSurface(Surface surface) {
|
public void setSurface(Surface surface) {
|
||||||
@ -473,11 +488,32 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/* package */ MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>
|
||||||
public void onMetadata(Map<String, Object> metadata) {
|
getId3MetadataRenderer() {
|
||||||
if (metadataListener != null) {
|
return new MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>() {
|
||||||
metadataListener.onMetadata(metadata);
|
|
||||||
}
|
@Override
|
||||||
|
public void onMetadata(Map<String, Object> 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
|
@Override
|
||||||
|
@ -26,6 +26,8 @@ import com.google.android.exoplayer.hls.HlsMasterPlaylist;
|
|||||||
import com.google.android.exoplayer.hls.HlsMasterPlaylistParser;
|
import com.google.android.exoplayer.hls.HlsMasterPlaylistParser;
|
||||||
import com.google.android.exoplayer.hls.HlsSampleSource;
|
import com.google.android.exoplayer.hls.HlsSampleSource;
|
||||||
import com.google.android.exoplayer.hls.Variant;
|
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.Id3Parser;
|
||||||
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
|
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
@ -39,6 +41,8 @@ import android.net.Uri;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link RendererBuilder} for HLS.
|
* 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);
|
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, player.getMainHandler(), player, 50);
|
||||||
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
|
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
|
||||||
|
|
||||||
MetadataTrackRenderer metadataRenderer = new MetadataTrackRenderer(sampleSource,
|
MetadataTrackRenderer<Map<String, Object>> id3Renderer =
|
||||||
new Id3Parser(), player, player.getMainHandler().getLooper());
|
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];
|
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
|
||||||
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
|
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||||
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
|
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);
|
callback.onRenderers(null, null, renderers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,6 +92,11 @@ public class MediaFormat {
|
|||||||
NO_VALUE, NO_VALUE, NO_VALUE, null);
|
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)
|
@TargetApi(16)
|
||||||
private MediaFormat(android.media.MediaFormat format) {
|
private MediaFormat(android.media.MediaFormat format) {
|
||||||
this.frameworkMediaFormat = format;
|
this.frameworkMediaFormat = format;
|
||||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer.hls;
|
|||||||
|
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
import com.google.android.exoplayer.SampleHolder;
|
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.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.BitArray;
|
import com.google.android.exoplayer.util.BitArray;
|
||||||
@ -33,7 +34,6 @@ import java.io.IOException;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
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_AAC = 0x0F;
|
||||||
private static final int TS_STREAM_TYPE_H264 = 0x1B;
|
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_ID3 = 0x15;
|
||||||
|
private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1
|
||||||
|
|
||||||
private final BitArray tsPacketBuffer;
|
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 SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
|
||||||
private final SamplePool samplePool;
|
private final SamplePool samplePool;
|
||||||
/* package */ final long firstSampleTimestamp;
|
/* package */ final long firstSampleTimestamp;
|
||||||
@ -69,7 +70,7 @@ public final class TsExtractor {
|
|||||||
this.samplePool = samplePool;
|
this.samplePool = samplePool;
|
||||||
pendingFirstSampleTimestampAdjustment = true;
|
pendingFirstSampleTimestampAdjustment = true;
|
||||||
tsPacketBuffer = new BitArray();
|
tsPacketBuffer = new BitArray();
|
||||||
pesPayloadReaders = new SparseArray<PesPayloadReader>();
|
sampleQueues = new SparseArray<SampleQueue>();
|
||||||
tsPayloadReaders = new SparseArray<TsPayloadReader>();
|
tsPayloadReaders = new SparseArray<TsPayloadReader>();
|
||||||
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
||||||
largestParsedTimestampUs = Long.MIN_VALUE;
|
largestParsedTimestampUs = Long.MIN_VALUE;
|
||||||
@ -84,7 +85,7 @@ public final class TsExtractor {
|
|||||||
*/
|
*/
|
||||||
public int getTrackCount() {
|
public int getTrackCount() {
|
||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
return pesPayloadReaders.size();
|
return sampleQueues.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,7 +98,7 @@ public final class TsExtractor {
|
|||||||
*/
|
*/
|
||||||
public MediaFormat getFormat(int track) {
|
public MediaFormat getFormat(int track) {
|
||||||
Assertions.checkState(prepared);
|
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.
|
* Flushes any pending or incomplete samples, returning them to the sample pool.
|
||||||
*/
|
*/
|
||||||
public void clear() {
|
public void clear() {
|
||||||
for (int i = 0; i < pesPayloadReaders.size(); i++) {
|
for (int i = 0; i < sampleQueues.size(); i++) {
|
||||||
pesPayloadReaders.valueAt(i).clear();
|
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() {
|
public void discardFromNextKeyframes() {
|
||||||
discardFromNextKeyframes = true;
|
discardFromNextKeyframes = true;
|
||||||
@ -143,8 +144,7 @@ public final class TsExtractor {
|
|||||||
*/
|
*/
|
||||||
public boolean getSample(int track, SampleHolder out) {
|
public boolean getSample(int track, SampleHolder out) {
|
||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
Queue<Sample> queue = pesPayloadReaders.valueAt(track).sampleQueue;
|
Sample sample = sampleQueues.valueAt(track).poll();
|
||||||
Sample sample = queue.poll();
|
|
||||||
if (sample == null) {
|
if (sample == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -161,7 +161,7 @@ public final class TsExtractor {
|
|||||||
* for any track. False otherwise.
|
* for any track. False otherwise.
|
||||||
*/
|
*/
|
||||||
public boolean hasSamples() {
|
public boolean hasSamples() {
|
||||||
for (int i = 0; i < pesPayloadReaders.size(); i++) {
|
for (int i = 0; i < sampleQueues.size(); i++) {
|
||||||
if (hasSamples(i)) {
|
if (hasSamples(i)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -177,16 +177,16 @@ public final class TsExtractor {
|
|||||||
* for the specified track. False otherwise.
|
* for the specified track. False otherwise.
|
||||||
*/
|
*/
|
||||||
public boolean hasSamples(int track) {
|
public boolean hasSamples(int track) {
|
||||||
return !pesPayloadReaders.valueAt(track).sampleQueue.isEmpty();
|
return !sampleQueues.valueAt(track).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkPrepared() {
|
private boolean checkPrepared() {
|
||||||
int pesPayloadReaderCount = pesPayloadReaders.size();
|
int pesPayloadReaderCount = sampleQueues.size();
|
||||||
if (pesPayloadReaderCount == 0) {
|
if (pesPayloadReaderCount == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < pesPayloadReaderCount; i++) {
|
for (int i = 0; i < pesPayloadReaderCount; i++) {
|
||||||
if (!pesPayloadReaders.valueAt(i).hasMediaFormat()) {
|
if (!sampleQueues.valueAt(i).hasMediaFormat()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -342,7 +342,7 @@ public final class TsExtractor {
|
|||||||
tsBuffer.skipBytes(esInfoLength);
|
tsBuffer.skipBytes(esInfoLength);
|
||||||
entriesSize -= esInfoLength + 5;
|
entriesSize -= esInfoLength + 5;
|
||||||
|
|
||||||
if (pesPayloadReaders.get(streamType) != null) {
|
if (sampleQueues.get(streamType) != null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,7 +352,9 @@ public final class TsExtractor {
|
|||||||
pesPayloadReader = new AdtsReader();
|
pesPayloadReader = new AdtsReader();
|
||||||
break;
|
break;
|
||||||
case TS_STREAM_TYPE_H264:
|
case TS_STREAM_TYPE_H264:
|
||||||
pesPayloadReader = new H264Reader();
|
SeiReader seiReader = new SeiReader();
|
||||||
|
sampleQueues.put(TS_STREAM_TYPE_EIA608, seiReader);
|
||||||
|
pesPayloadReader = new H264Reader(seiReader);
|
||||||
break;
|
break;
|
||||||
case TS_STREAM_TYPE_ID3:
|
case TS_STREAM_TYPE_ID3:
|
||||||
pesPayloadReader = new Id3Reader();
|
pesPayloadReader = new Id3Reader();
|
||||||
@ -360,7 +362,7 @@ public final class TsExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pesPayloadReader != null) {
|
if (pesPayloadReader != null) {
|
||||||
pesPayloadReaders.put(streamType, pesPayloadReader);
|
sampleQueues.put(streamType, pesPayloadReader);
|
||||||
tsPayloadReaders.put(elementaryPid, new PesReader(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 MediaFormat mediaFormat;
|
||||||
private boolean foundFirstKeyframe;
|
private boolean foundFirstKeyframe;
|
||||||
private boolean foundLastKeyframe;
|
private boolean foundLastKeyframe;
|
||||||
|
|
||||||
protected PesPayloadReader() {
|
protected SampleQueue() {
|
||||||
this.sampleQueue = new ConcurrentLinkedQueue<Sample>();
|
this.queue = new ConcurrentLinkedQueue<Sample>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasMediaFormat() {
|
public boolean hasMediaFormat() {
|
||||||
@ -495,16 +497,22 @@ public final class TsExtractor {
|
|||||||
this.mediaFormat = mediaFormat;
|
this.mediaFormat = mediaFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs);
|
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
Sample toRecycle = sampleQueue.poll();
|
Sample toRecycle = queue.poll();
|
||||||
while (toRecycle != null) {
|
while (toRecycle != null) {
|
||||||
samplePool.recycle(toRecycle);
|
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.
|
* Creates a new Sample and adds it to the queue.
|
||||||
*
|
*
|
||||||
@ -534,7 +542,7 @@ public final class TsExtractor {
|
|||||||
adjustTimestamp(sample);
|
adjustTimestamp(sample);
|
||||||
if (foundFirstKeyframe && !foundLastKeyframe) {
|
if (foundFirstKeyframe && !foundLastKeyframe) {
|
||||||
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs);
|
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs);
|
||||||
sampleQueue.add(sample);
|
queue.add(sample);
|
||||||
} else {
|
} else {
|
||||||
samplePool.recycle(sample);
|
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.
|
* 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_AUD = 9;
|
||||||
private static final int NAL_UNIT_TYPE_SPS = 7;
|
private static final int NAL_UNIT_TYPE_SPS = 7;
|
||||||
|
|
||||||
|
public final SeiReader seiReader;
|
||||||
|
|
||||||
// Used to store uncompleted sample data.
|
// Used to store uncompleted sample data.
|
||||||
private Sample currentSample;
|
private Sample currentSample;
|
||||||
|
|
||||||
|
public H264Reader(SeiReader seiReader) {
|
||||||
|
this.seiReader = seiReader;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clear() {
|
public void clear() {
|
||||||
super.clear();
|
super.clear();
|
||||||
@ -593,6 +616,7 @@ public final class TsExtractor {
|
|||||||
if (!hasMediaFormat() && (currentSample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
|
if (!hasMediaFormat() && (currentSample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
|
||||||
parseMediaFormat(currentSample);
|
parseMediaFormat(currentSample);
|
||||||
}
|
}
|
||||||
|
seiReader.read(currentSample.data, currentSample.size, pesTimeUs);
|
||||||
addSample(currentSample);
|
addSample(currentSample);
|
||||||
}
|
}
|
||||||
currentSample = samplePool.get();
|
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.
|
* Parses a continuous ADTS byte stream and extracts individual frames.
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -27,7 +27,7 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* Extracts individual TXXX text frames from raw ID3 data.
|
* Extracts individual TXXX text frames from raw ID3 data.
|
||||||
*/
|
*/
|
||||||
public class Id3Parser implements MetadataParser {
|
public class Id3Parser implements MetadataParser<Map<String, Object>> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canParse(String mimeType) {
|
public boolean canParse(String mimeType) {
|
||||||
|
@ -16,30 +16,30 @@
|
|||||||
package com.google.android.exoplayer.metadata;
|
package com.google.android.exoplayer.metadata;
|
||||||
|
|
||||||
import java.io.IOException;
|
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.
|
* 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.
|
* @return Whether the mime type is supported.
|
||||||
*/
|
*/
|
||||||
public boolean canParse(String mimeType);
|
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 data The raw binary data from which to parse the metadata.
|
||||||
* @param size The size of the input data.
|
* @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.
|
* @throws IOException If a problem occurred parsing the data.
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> parse(byte[] data, int size)
|
public T parse(byte[] data, int size) throws IOException;
|
||||||
throws IOException;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -28,32 +28,35 @@ import android.os.Looper;
|
|||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link TrackRenderer} for metadata embedded in a media stream.
|
* 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.
|
* 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.
|
* Invoked each time there is a metadata associated with current playback time.
|
||||||
*
|
*
|
||||||
* @param metadata The metadata to process.
|
* @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 static final int MSG_INVOKE_RENDERER = 0;
|
||||||
|
|
||||||
private final SampleSource source;
|
private final SampleSource source;
|
||||||
private final MetadataParser metadataParser;
|
private final MetadataParser<T> metadataParser;
|
||||||
private final MetadataRenderer metadataRenderer;
|
private final MetadataRenderer<T> metadataRenderer;
|
||||||
private final Handler metadataHandler;
|
private final Handler metadataHandler;
|
||||||
private final MediaFormatHolder formatHolder;
|
private final MediaFormatHolder formatHolder;
|
||||||
private final SampleHolder sampleHolder;
|
private final SampleHolder sampleHolder;
|
||||||
@ -63,7 +66,7 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
private boolean inputStreamEnded;
|
private boolean inputStreamEnded;
|
||||||
|
|
||||||
private long pendingMetadataTimestamp;
|
private long pendingMetadataTimestamp;
|
||||||
private Map<String, Object> pendingMetadata;
|
private T pendingMetadata;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param source A source from which samples containing metadata can be read.
|
* @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
|
* 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.
|
* renderer should be invoked directly on the player's internal rendering thread.
|
||||||
*/
|
*/
|
||||||
public MetadataTrackRenderer(SampleSource source, MetadataParser metadataParser,
|
public MetadataTrackRenderer(SampleSource source, MetadataParser<T> metadataParser,
|
||||||
MetadataRenderer metadataRenderer, Looper metadataRendererLooper) {
|
MetadataRenderer<T> metadataRenderer, Looper metadataRendererLooper) {
|
||||||
this.source = Assertions.checkNotNull(source);
|
this.source = Assertions.checkNotNull(source);
|
||||||
this.metadataParser = Assertions.checkNotNull(metadataParser);
|
this.metadataParser = Assertions.checkNotNull(metadataParser);
|
||||||
this.metadataRenderer = Assertions.checkNotNull(metadataRenderer);
|
this.metadataRenderer = Assertions.checkNotNull(metadataRenderer);
|
||||||
@ -185,7 +188,7 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invokeRenderer(Map<String, Object> metadata) {
|
private void invokeRenderer(T metadata) {
|
||||||
if (metadataHandler != null) {
|
if (metadataHandler != null) {
|
||||||
metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
|
metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
|
||||||
} else {
|
} else {
|
||||||
@ -198,13 +201,13 @@ public class MetadataTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
public boolean handleMessage(Message msg) {
|
public boolean handleMessage(Message msg) {
|
||||||
switch (msg.what) {
|
switch (msg.what) {
|
||||||
case MSG_INVOKE_RENDERER:
|
case MSG_INVOKE_RENDERER:
|
||||||
invokeRendererInternal((Map<String, Object>) msg.obj);
|
invokeRendererInternal((T) msg.obj);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invokeRendererInternal(Map<String, Object> metadata) {
|
private void invokeRendererInternal(T metadata) {
|
||||||
metadataRenderer.onMetadata(metadata);
|
metadataRenderer.onMetadata(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ public class MimeTypes {
|
|||||||
public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
|
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_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";
|
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
|
||||||
|
|
||||||
private MimeTypes() {}
|
private MimeTypes() {}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user