Commit 4158ede6 by Andrey Udovenko

Move Closed Captions processing to Eia608TrackRenderer. Use TextRenderer…

Move Closed Captions processing to Eia608TrackRenderer. Use TextRenderer interface for captions. Sort captions based on video frames DTS. Add better control characters and special characters in basic North American character set support. Fixes #156
parent fe433771
......@@ -27,9 +27,8 @@ import com.google.android.exoplayer.audio.AudioTrack;
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.text.TextRenderer;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.util.PlayerControl;
......@@ -39,7 +38,6 @@ 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;
......@@ -51,7 +49,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener,
MediaCodecAudioTrackRenderer.EventListener, Ac3PassthroughAudioTrackRenderer.EventListener,
TextTrackRenderer.TextRenderer, StreamingDrmSessionManager.EventListener {
TextRenderer, StreamingDrmSessionManager.EventListener {
/**
* Builds renderers for the player.
......@@ -173,7 +171,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
private final PlayerControl playerControl;
private final Handler mainHandler;
private final CopyOnWriteArrayList<Listener> listeners;
private final StringBuilder closedCaptionStringBuilder;
private int rendererBuildingState;
private int lastReportedPlaybackState;
......@@ -204,7 +201,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
selectedTracks = new int[RENDERER_COUNT];
// Disable text initially.
selectedTracks[TYPE_TEXT] = DISABLED_TRACK;
closedCaptionStringBuilder = new StringBuilder();
}
public PlayerControl getPlayerControl() {
......@@ -499,16 +495,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
};
}
/* package */ MetadataTrackRenderer.MetadataRenderer<List<ClosedCaption>>
getClosedCaptionMetadataRenderer() {
return new MetadataTrackRenderer.MetadataRenderer<List<ClosedCaption>>() {
@Override
public void onMetadata(List<ClosedCaption> metadata) {
processClosedCaption(metadata);
}
};
}
@Override
public void onPlayWhenReadyCommitted() {
// Do nothing.
......@@ -607,29 +593,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
textListener.onText(text);
}
/* package */ void processClosedCaption(List<ClosedCaption> metadata) {
if (textListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) {
return;
}
closedCaptionStringBuilder.setLength(0);
for (ClosedCaption caption : metadata) {
// Ignore control characters and just insert a new line in between words.
if (caption.type == ClosedCaption.TYPE_CTRL) {
if (closedCaptionStringBuilder.length() > 0
&& closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1) != '\n') {
closedCaptionStringBuilder.append('\n');
}
} else if (caption.type == ClosedCaption.TYPE_TEXT) {
closedCaptionStringBuilder.append(caption.text);
}
}
if (closedCaptionStringBuilder.length() > 0
&& closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1) == '\n') {
closedCaptionStringBuilder.deleteCharAt(closedCaptionStringBuilder.length() - 1);
}
textListener.onText(closedCaptionStringBuilder.toString());
}
private class InternalRendererBuilderCallback implements RendererBuilderCallback {
private boolean canceled;
......
......@@ -24,10 +24,9 @@ import com.google.android.exoplayer.hls.HlsChunkSource;
import com.google.android.exoplayer.hls.HlsPlaylist;
import com.google.android.exoplayer.hls.HlsPlaylistParser;
import com.google.android.exoplayer.hls.HlsSampleSource;
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.text.eia608.Eia608TrackRenderer;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.UriDataSource;
......@@ -37,7 +36,6 @@ import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import android.media.MediaCodec;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
......@@ -89,9 +87,8 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
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());
Eia608TrackRenderer closedCaptionRenderer = new Eia608TrackRenderer(sampleSource, player,
player.getMainHandler().getLooper());
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
......
......@@ -18,7 +18,7 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.metadata.Eia608Parser;
import com.google.android.exoplayer.text.eia608.Eia608Parser;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.BitArray;
......@@ -769,7 +769,7 @@ public final class TsExtractor {
if (!hasMediaFormat() && currentSample.isKeyframe) {
parseMediaFormat(currentSample);
}
seiReader.read(currentSample.data, currentSample.size, pesTimeUs);
seiReader.read(currentSample.data, currentSample.size, currentSample.timeUs);
addSample(currentSample);
}
currentSample = getSample(Sample.TYPE_VIDEO);
......
/*
* 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.text;
/**
* An interface for components that render text.
*/
public interface TextRenderer {
/**
* Invoked each time there is a change in the text to be rendered.
*
* @param text The text to render, or null if no text is to be rendered.
*/
void onText(String text);
}
......@@ -38,20 +38,6 @@ import java.io.IOException;
@TargetApi(16)
public class TextTrackRenderer extends TrackRenderer implements Callback {
/**
* An interface for components that render text.
*/
public interface TextRenderer {
/**
* Invoked each time there is a change in the text to be rendered.
*
* @param text The text to render, or null if no text is to be rendered.
*/
void onText(String text);
}
private static final int MSG_UPDATE_OVERLAY = 0;
private final Handler textRendererHandler;
......
......@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.metadata;
package com.google.android.exoplayer.text.eia608;
/**
* A Closed Caption that contains textual data associated with time indices.
*/
public final class ClosedCaption {
public final class ClosedCaption implements Comparable<ClosedCaption> {
/**
* Identifies closed captions with control characters.
......@@ -39,10 +39,24 @@ public final class ClosedCaption {
* Contains text or two control characters.
*/
public final String text;
/**
* Timestamp associated with the closed caption.
*/
public final long timeUs;
public ClosedCaption(int type, String text) {
public ClosedCaption(int type, String text, long timeUs) {
this.type = type;
this.text = text;
this.timeUs = timeUs;
}
@Override
public int compareTo(ClosedCaption another) {
long delta = this.timeUs - another.timeUs;
if (delta == 0) {
return 0;
}
return delta > 0 ? 1 : -1;
}
}
......@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.metadata;
package com.google.android.exoplayer.text.eia608;
import com.google.android.exoplayer.util.BitArray;
import com.google.android.exoplayer.util.MimeTypes;
......@@ -27,7 +27,7 @@ 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>> {
public class Eia608Parser {
private static final int PAYLOAD_TYPE_CC = 4;
private static final int COUNTRY_CODE = 0xB5;
......@@ -35,6 +35,35 @@ public class Eia608Parser implements MetadataParser<List<ClosedCaption>> {
private static final int USER_ID = 0x47413934; // "GA94"
private static final int USER_DATA_TYPE_CODE = 0x3;
// Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20).
private static final int[] BASIC_CHARACTER_SET = new int[] {
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & '
0x28, 0x29, // ( )
0xE1, // 2A: 225 'á' "Latin small letter A with acute"
0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // + , - . /
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 0 1 2 3 4 5 6 7
0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, // 8 9 : ; < = > ?
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, // @ A B C D E F G
0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, // H I J K L M N O
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, // P Q R S T U V W
0x58, 0x59, 0x5A, 0x5B, // X Y Z [
0xE9, // 5C: 233 'é' "Latin small letter E with acute"
0x5D, // ]
0xED, // 5E: 237 'í' "Latin small letter I with acute"
0xF3, // 5F: 243 'ó' "Latin small letter O with acute"
0xFA, // 60: 250 'ú' "Latin small letter U with acute"
0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, // a b c d e f g
0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, // h i j k l m n o
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, // p q r s t u v w
0x78, 0x79, 0x7A, // x y z
0xE7, // 7B: 231 'ç' "Latin small letter C with cedilla"
0xF7, // 7C: 247 '÷' "Division sign"
0xD1, // 7D: 209 'Ñ' "Latin capital letter N with tilde"
0xF1, // 7E: 241 'ñ' "Latin small letter N with tilde"
0x25A0 // 7F: "Black Square" (NB: 2588 = Full Block)
};
// Special North American 608 CC char set.
private static final int[] SPECIAL_CHARACTER_SET = new int[] {
0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol
0xB0, // 31: 176 '°' "Degree Sign"
......@@ -54,13 +83,11 @@ public class Eia608Parser implements MetadataParser<List<ClosedCaption>> {
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 {
public List<ClosedCaption> parse(byte[] data, int size, long timeUs) throws IOException {
if (size <= 0) {
return null;
}
......@@ -90,37 +117,53 @@ public class Eia608Parser implements MetadataParser<List<ClosedCaption>> {
seiBuffer.skipBits(1);
byte ccData2 = (byte) seiBuffer.readBits(7);
// Ignore empty captions.
if (ccData1 == 0 && ccData2 == 0) {
continue;
}
// Special North American character set.
if ((ccData1 == 0x11) && ((ccData2 & 0x70) == 0x30)) {
ccData2 &= 0xF;
stringBuilder.append((char) SPECIAL_CHARACTER_SET[ccData2]);
stringBuilder.append(getSpecialChar(ccData2));
continue;
}
// Control character.
if (ccData1 < 0x20) {
if (stringBuilder.length() > 0) {
captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString()));
captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString(),
timeUs));
stringBuilder.setLength(0);
}
captions.add(new ClosedCaption(ClosedCaption.TYPE_CTRL,
new String(new char[]{(char) ccData1, (char) ccData2})));
new String(new char[] {(char) ccData1, (char) ccData2}), timeUs));
continue;
}
stringBuilder.append((char) ccData1);
// Basic North American character set.
stringBuilder.append(getChar(ccData1));
if (ccData2 != 0) {
stringBuilder.append((char) ccData2);
stringBuilder.append(getChar(ccData2));
}
}
if (stringBuilder.length() > 0) {
captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString()));
captions.add(new ClosedCaption(ClosedCaption.TYPE_TEXT, stringBuilder.toString(), timeUs));
}
return Collections.unmodifiableList(captions);
}
private static char getChar(byte ccData) {
int index = (ccData & 0x7F) - 0x20;
return (char) BASIC_CHARACTER_SET[index];
}
private static char getSpecialChar(byte ccData) {
int index = ccData & 0xF;
return (char) SPECIAL_CHARACTER_SET[index];
}
/**
* 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.
......
/*
* 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.text.eia608;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.text.TextRenderer;
import com.google.android.exoplayer.util.Assertions;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Looper;
import android.os.Message;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* A {@link TrackRenderer} for EIA-608 closed captions in a media stream.
*/
public class Eia608TrackRenderer extends TrackRenderer implements Callback {
private static final int MSG_INVOKE_RENDERER = 0;
// The Number of closed captions text line to keep in memory.
private static final int ALLOWED_CAPTIONS_TEXT_LINES_COUNT = 4;
private final SampleSource source;
private final Eia608Parser eia608Parser;
private final TextRenderer textRenderer;
private final Handler metadataHandler;
private final MediaFormatHolder formatHolder;
private final SampleHolder sampleHolder;
private final StringBuilder closedCaptionStringBuilder;
//Currently displayed captions.
private final List<ClosedCaption> currentCaptions;
private final Queue<Integer> newLineIndexes;
private int trackIndex;
private long currentPositionUs;
private boolean inputStreamEnded;
private long pendingCaptionsTimestamp;
private List<ClosedCaption> pendingCaptions;
/**
* @param source A source from which samples containing EIA-608 closed captions can be read.
* @param textRenderer The text renderer.
* @param textRendererLooper The looper associated with the thread on which textRenderer should be
* invoked. If the renderer makes use of standard Android UI components, then this should
* normally be the looper associated with the applications' main thread, which can be
* 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 Eia608TrackRenderer(SampleSource source, TextRenderer textRenderer,
Looper textRendererLooper) {
this.source = Assertions.checkNotNull(source);
this.textRenderer = Assertions.checkNotNull(textRenderer);
this.metadataHandler = textRendererLooper == null ? null
: new Handler(textRendererLooper, this);
eia608Parser = new Eia608Parser();
formatHolder = new MediaFormatHolder();
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
closedCaptionStringBuilder = new StringBuilder();
currentCaptions = new LinkedList<ClosedCaption>();
newLineIndexes = new LinkedList<Integer>();
}
@Override
protected int doPrepare() throws ExoPlaybackException {
try {
boolean sourcePrepared = source.prepare();
if (!sourcePrepared) {
return TrackRenderer.STATE_UNPREPARED;
}
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
for (int i = 0; i < source.getTrackCount(); i++) {
if (eia608Parser.canParse(source.getTrackInfo(i).mimeType)) {
trackIndex = i;
return TrackRenderer.STATE_PREPARED;
}
}
return TrackRenderer.STATE_IGNORE;
}
@Override
protected void onEnabled(long positionUs, boolean joining) {
source.enable(trackIndex, positionUs);
seekToInternal(positionUs);
}
@Override
protected void seekTo(long positionUs) throws ExoPlaybackException {
source.seekToUs(positionUs);
seekToInternal(positionUs);
}
private void seekToInternal(long positionUs) {
currentPositionUs = positionUs;
pendingCaptions = null;
inputStreamEnded = false;
// Clear displayed captions.
currentCaptions.clear();
}
@Override
protected void doSomeWork(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException {
currentPositionUs = positionUs;
try {
source.continueBuffering(positionUs);
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
if (!inputStreamEnded && pendingCaptions == null) {
try {
int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ) {
pendingCaptionsTimestamp = sampleHolder.timeUs;
pendingCaptions = eia608Parser.parse(sampleHolder.data.array(), sampleHolder.size,
sampleHolder.timeUs);
sampleHolder.data.clear();
} else if (result == SampleSource.END_OF_STREAM) {
inputStreamEnded = true;
}
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
}
if (pendingCaptions != null && pendingCaptionsTimestamp <= currentPositionUs) {
invokeRenderer(pendingCaptions);
pendingCaptions = null;
}
}
@Override
protected void onDisabled() {
pendingCaptions = null;
source.disable(trackIndex);
}
@Override
protected long getDurationUs() {
return source.getTrackInfo(trackIndex).durationUs;
}
@Override
protected long getCurrentPositionUs() {
return currentPositionUs;
}
@Override
protected long getBufferedPositionUs() {
return TrackRenderer.END_OF_TRACK_US;
}
@Override
protected boolean isEnded() {
return inputStreamEnded;
}
@Override
protected boolean isReady() {
return true;
}
private void invokeRenderer(List<ClosedCaption> metadata) {
if (metadataHandler != null) {
metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
} else {
invokeRendererInternal(metadata);
}
}
@SuppressWarnings("unchecked")
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVOKE_RENDERER:
invokeRendererInternal((List<ClosedCaption>) msg.obj);
return true;
}
return false;
}
private void invokeRendererInternal(List<ClosedCaption> metadata) {
currentCaptions.addAll(metadata);
// Sort captions by the timestamp.
Collections.sort(currentCaptions);
closedCaptionStringBuilder.setLength(0);
// After processing keep only captions after cutIndex.
int cutIndex = 0;
newLineIndexes.clear();
for (int i = 0; i < currentCaptions.size(); i++) {
ClosedCaption caption = currentCaptions.get(i);
if (caption.type == ClosedCaption.TYPE_CTRL) {
int cc2 = caption.text.codePointAt(1);
switch (cc2) {
case 0x2C: // Erase Displayed Memory.
closedCaptionStringBuilder.setLength(0);
cutIndex = i;
newLineIndexes.clear();
break;
case 0x25: // Roll-Up.
case 0x26:
case 0x27:
default:
if (cc2 >= 0x20 && cc2 < 0x40) {
break;
}
if (closedCaptionStringBuilder.length() > 0
&& closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1)
!= '\n') {
closedCaptionStringBuilder.append('\n');
newLineIndexes.add(i);
if (newLineIndexes.size() >= ALLOWED_CAPTIONS_TEXT_LINES_COUNT) {
cutIndex = newLineIndexes.poll();
}
}
break;
}
} else {
closedCaptionStringBuilder.append(caption.text);
}
}
if (cutIndex > 0 && cutIndex < currentCaptions.size() - 1) {
for (int i = 0; i <= cutIndex; i++) {
currentCaptions.remove(0);
}
}
textRenderer.onText(closedCaptionStringBuilder.toString());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment