mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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.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
|
||||
|
@ -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,11 +488,32 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadata(Map<String, Object> metadata) {
|
||||
if (metadataListener != null) {
|
||||
metadataListener.onMetadata(metadata);
|
||||
}
|
||||
/* package */ MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>
|
||||
getId3MetadataRenderer() {
|
||||
return new MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>() {
|
||||
|
||||
@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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,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<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.
|
||||
*/
|
||||
|
@ -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.
|
||||
*/
|
||||
public class Id3Parser implements MetadataParser {
|
||||
public class Id3Parser implements MetadataParser<Map<String, Object>> {
|
||||
|
||||
@Override
|
||||
public boolean canParse(String mimeType) {
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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() {}
|
||||
|
Loading…
x
Reference in New Issue
Block a user