Step towards enhanced Webvtt parser to support HTML-rich captions and positioning.
This commit is contained in:
parent
709fc7735b
commit
658a7ffba5
@ -5,6 +5,7 @@
|
|||||||
* Add option to TsExtractor to allow non-IDR keyframes.
|
* Add option to TsExtractor to allow non-IDR keyframes.
|
||||||
* Added MulticastDataSource for connecting to multicast streams.
|
* Added MulticastDataSource for connecting to multicast streams.
|
||||||
* (WorkInProgress) - First steps to supporting seeking in DASH DVR window.
|
* (WorkInProgress) - First steps to supporting seeking in DASH DVR window.
|
||||||
|
* (WorkInProgress) - First steps to supporting styled + positioned subtitles.
|
||||||
|
|
||||||
### r1.3.2 (from r1.3.1) ###
|
### r1.3.2 (from r1.3.1) ###
|
||||||
|
|
||||||
|
@ -35,7 +35,8 @@ import com.google.android.exoplayer.metadata.GeobMetadata;
|
|||||||
import com.google.android.exoplayer.metadata.PrivMetadata;
|
import com.google.android.exoplayer.metadata.PrivMetadata;
|
||||||
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.Cue;
|
||||||
|
import com.google.android.exoplayer.text.SubtitleLayout;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
import com.google.android.exoplayer.util.VerboseLogUtil;
|
import com.google.android.exoplayer.util.VerboseLogUtil;
|
||||||
|
|
||||||
@ -43,12 +44,10 @@ import android.annotation.TargetApi;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Point;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Display;
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
@ -58,7 +57,6 @@ import android.view.View;
|
|||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.view.View.OnKeyListener;
|
import android.view.View.OnKeyListener;
|
||||||
import android.view.View.OnTouchListener;
|
import android.view.View.OnTouchListener;
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.view.accessibility.CaptioningManager;
|
import android.view.accessibility.CaptioningManager;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.MediaController;
|
import android.widget.MediaController;
|
||||||
@ -67,13 +65,14 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
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 PlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener,
|
public class PlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener,
|
||||||
DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.Id3MetadataListener,
|
DemoPlayer.Listener, DemoPlayer.CaptionListener, DemoPlayer.Id3MetadataListener,
|
||||||
AudioCapabilitiesReceiver.Listener {
|
AudioCapabilitiesReceiver.Listener {
|
||||||
|
|
||||||
public static final String CONTENT_TYPE_EXTRA = "content_type";
|
public static final String CONTENT_TYPE_EXTRA = "content_type";
|
||||||
@ -81,7 +80,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
private static final String TAG = "PlayerActivity";
|
private static final String TAG = "PlayerActivity";
|
||||||
|
|
||||||
private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f;
|
|
||||||
private static final int MENU_GROUP_TRACKS = 1;
|
private static final int MENU_GROUP_TRACKS = 1;
|
||||||
private static final int ID_OFFSET = 2;
|
private static final int ID_OFFSET = 2;
|
||||||
|
|
||||||
@ -92,7 +90,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
|||||||
private VideoSurfaceView surfaceView;
|
private VideoSurfaceView surfaceView;
|
||||||
private TextView debugTextView;
|
private TextView debugTextView;
|
||||||
private TextView playerStateTextView;
|
private TextView playerStateTextView;
|
||||||
private SubtitleView subtitleView;
|
private SubtitleLayout subtitleLayout;
|
||||||
private Button videoButton;
|
private Button videoButton;
|
||||||
private Button audioButton;
|
private Button audioButton;
|
||||||
private Button textButton;
|
private Button textButton;
|
||||||
@ -154,7 +152,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
|||||||
debugTextView = (TextView) findViewById(R.id.debug_text_view);
|
debugTextView = (TextView) findViewById(R.id.debug_text_view);
|
||||||
|
|
||||||
playerStateTextView = (TextView) findViewById(R.id.player_state_view);
|
playerStateTextView = (TextView) findViewById(R.id.player_state_view);
|
||||||
subtitleView = (SubtitleView) findViewById(R.id.subtitles);
|
subtitleLayout = (SubtitleLayout) findViewById(R.id.subtitles);
|
||||||
|
|
||||||
mediaController = new MediaController(this);
|
mediaController = new MediaController(this);
|
||||||
mediaController.setAnchorView(root);
|
mediaController.setAnchorView(root);
|
||||||
@ -256,7 +254,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
|||||||
if (player == null) {
|
if (player == null) {
|
||||||
player = new DemoPlayer(getRendererBuilder());
|
player = new DemoPlayer(getRendererBuilder());
|
||||||
player.addListener(this);
|
player.addListener(this);
|
||||||
player.setTextListener(this);
|
player.setCaptionListener(this);
|
||||||
player.setMetadataListener(this);
|
player.setMetadataListener(this);
|
||||||
player.seekTo(playerPosition);
|
player.seekTo(playerPosition);
|
||||||
playerNeedsPrepare = true;
|
playerNeedsPrepare = true;
|
||||||
@ -464,16 +462,11 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
|||||||
debugRootView.setVisibility(View.VISIBLE);
|
debugRootView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DemoPlayer.TextListener implementation
|
// DemoPlayer.CaptionListener implementation
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onText(String text) {
|
public void onCues(List<Cue> cues) {
|
||||||
if (TextUtils.isEmpty(text)) {
|
subtitleLayout.setCues(cues);
|
||||||
subtitleView.setVisibility(View.INVISIBLE);
|
|
||||||
} else {
|
|
||||||
subtitleView.setVisibility(View.VISIBLE);
|
|
||||||
subtitleView.setText(text);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DemoPlayer.MetadataListener implementation
|
// DemoPlayer.MetadataListener implementation
|
||||||
@ -523,24 +516,16 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
private void configureSubtitleView() {
|
private void configureSubtitleView() {
|
||||||
CaptionStyleCompat captionStyle;
|
CaptionStyleCompat captionStyle;
|
||||||
float captionTextSize = getCaptionFontSize();
|
float captionFontScale;
|
||||||
if (Util.SDK_INT >= 19) {
|
if (Util.SDK_INT >= 19) {
|
||||||
captionStyle = getUserCaptionStyleV19();
|
captionStyle = getUserCaptionStyleV19();
|
||||||
captionTextSize *= getUserCaptionFontScaleV19();
|
captionFontScale = getUserCaptionFontScaleV19();
|
||||||
} else {
|
} else {
|
||||||
captionStyle = CaptionStyleCompat.DEFAULT;
|
captionStyle = CaptionStyleCompat.DEFAULT;
|
||||||
|
captionFontScale = 1.0f;
|
||||||
}
|
}
|
||||||
subtitleView.setStyle(captionStyle);
|
subtitleLayout.setStyle(captionStyle);
|
||||||
subtitleView.setTextSize(captionTextSize);
|
subtitleLayout.setFontScale(captionFontScale);
|
||||||
}
|
|
||||||
|
|
||||||
private float getCaptionFontSize() {
|
|
||||||
Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE))
|
|
||||||
.getDefaultDisplay();
|
|
||||||
Point displaySize = new Point();
|
|
||||||
display.getSize(displaySize);
|
|
||||||
return Math.max(getResources().getDimension(R.dimen.subtitle_minimum_font_size),
|
|
||||||
CAPTION_LINE_HEIGHT_RATIO * Math.min(displaySize.x, displaySize.y));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(19)
|
@TargetApi(19)
|
||||||
|
@ -31,6 +31,7 @@ import com.google.android.exoplayer.dash.DashChunkSource;
|
|||||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
||||||
import com.google.android.exoplayer.hls.HlsSampleSource;
|
import com.google.android.exoplayer.hls.HlsSampleSource;
|
||||||
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
|
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
|
||||||
|
import com.google.android.exoplayer.text.Cue;
|
||||||
import com.google.android.exoplayer.text.TextRenderer;
|
import com.google.android.exoplayer.text.TextRenderer;
|
||||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer.util.PlayerControl;
|
import com.google.android.exoplayer.util.PlayerControl;
|
||||||
@ -41,6 +42,8 @@ import android.os.Looper;
|
|||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
@ -140,8 +143,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
/**
|
/**
|
||||||
* A listener for receiving notifications of timed text.
|
* A listener for receiving notifications of timed text.
|
||||||
*/
|
*/
|
||||||
public interface TextListener {
|
public interface CaptionListener {
|
||||||
void onText(String text);
|
void onCues(List<Cue> cues);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -193,7 +196,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
private int[] selectedTracks;
|
private int[] selectedTracks;
|
||||||
private boolean backgrounded;
|
private boolean backgrounded;
|
||||||
|
|
||||||
private TextListener textListener;
|
private CaptionListener captionListener;
|
||||||
private Id3MetadataListener id3MetadataListener;
|
private Id3MetadataListener id3MetadataListener;
|
||||||
private InternalErrorListener internalErrorListener;
|
private InternalErrorListener internalErrorListener;
|
||||||
private InfoListener infoListener;
|
private InfoListener infoListener;
|
||||||
@ -232,8 +235,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
infoListener = listener;
|
infoListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTextListener(TextListener listener) {
|
public void setCaptionListener(CaptionListener listener) {
|
||||||
textListener = listener;
|
captionListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMetadataListener(Id3MetadataListener listener) {
|
public void setMetadataListener(Id3MetadataListener listener) {
|
||||||
@ -268,8 +271,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
}
|
}
|
||||||
selectedTracks[type] = index;
|
selectedTracks[type] = index;
|
||||||
pushTrackSelection(type, true);
|
pushTrackSelection(type, true);
|
||||||
if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) {
|
if (type == TYPE_TEXT && index == DISABLED_TRACK && captionListener != null) {
|
||||||
textListener.onText(null);
|
captionListener.onCues(Collections.<Cue>emptyList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -509,8 +512,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onText(String text) {
|
public void onCues(List<Cue> cues) {
|
||||||
processText(text);
|
processCues(cues);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -617,11 +620,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ void processText(String text) {
|
/* package */ void processCues(List<Cue> cues) {
|
||||||
if (textListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) {
|
if (captionListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
textListener.onText(text);
|
captionListener.onCues(cues);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InternalRendererBuilderCallback implements RendererBuilderCallback {
|
private class InternalRendererBuilderCallback implements RendererBuilderCallback {
|
||||||
|
@ -26,14 +26,12 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center"/>
|
android:layout_gravity="center"/>
|
||||||
|
|
||||||
<com.google.android.exoplayer.text.SubtitleView android:id="@+id/subtitles"
|
<com.google.android.exoplayer.text.SubtitleLayout android:id="@+id/subtitles"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="bottom|center_horizontal"
|
|
||||||
android:layout_marginLeft="16dp"
|
android:layout_marginLeft="16dp"
|
||||||
android:layout_marginRight="16dp"
|
android:layout_marginRight="16dp"
|
||||||
android:layout_marginBottom="32dp"
|
android:layout_marginBottom="32dp"/>
|
||||||
android:visibility="invisible"/>
|
|
||||||
|
|
||||||
<View android:id="@+id/shutter"
|
<View android:id="@+id/shutter"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import android.text.Layout.Alignment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains information about a specific cue, including textual content and formatting data.
|
||||||
|
*/
|
||||||
|
public class Cue {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by some methods to indicate that no value is set.
|
||||||
|
*/
|
||||||
|
public static final int UNSET_VALUE = -1;
|
||||||
|
|
||||||
|
public final CharSequence text;
|
||||||
|
|
||||||
|
public final int line;
|
||||||
|
public final int position;
|
||||||
|
public final Alignment alignment;
|
||||||
|
public final int size;
|
||||||
|
|
||||||
|
public Cue() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cue(CharSequence text) {
|
||||||
|
this(text, UNSET_VALUE, UNSET_VALUE, null, UNSET_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cue(CharSequence text, int line, int position, Alignment alignment, int size) {
|
||||||
|
this.text = text;
|
||||||
|
this.line = line;
|
||||||
|
this.position = position;
|
||||||
|
this.alignment = alignment;
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.text;
|
package com.google.android.exoplayer.text;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A subtitle that contains textual data associated with time indices.
|
* A subtitle that contains textual data associated with time indices.
|
||||||
*/
|
*/
|
||||||
@ -39,8 +41,8 @@ public interface Subtitle {
|
|||||||
public int getNextEventTimeIndex(long timeUs);
|
public int getNextEventTimeIndex(long timeUs);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the number of event times, where events are defined as points in time at which the text
|
* Gets the number of event times, where events are defined as points in time at which the cues
|
||||||
* returned by {@link #getText(long)} changes.
|
* returned by {@link #getCues(long)} changes.
|
||||||
*
|
*
|
||||||
* @return The number of event times.
|
* @return The number of event times.
|
||||||
*/
|
*/
|
||||||
@ -62,11 +64,11 @@ public interface Subtitle {
|
|||||||
public long getLastEventTime();
|
public long getLastEventTime();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the subtitle text that should be displayed at a given time.
|
* Retrieve the subtitle cues that should be displayed at a given time.
|
||||||
*
|
*
|
||||||
* @param timeUs The time in microseconds.
|
* @param timeUs The time in microseconds.
|
||||||
* @return The text that should be displayed, or null.
|
* @return A list of cues that should be displayed, possibly empty.
|
||||||
*/
|
*/
|
||||||
public String getText(long timeUs);
|
public List<Cue> getCues(long timeUs);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.text.Layout.Alignment;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view for rendering rich-formatted captions.
|
||||||
|
*/
|
||||||
|
public final class SubtitleLayout extends ViewGroup {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the same line height ratio as WebVtt to match the display with the preview.
|
||||||
|
* WebVtt specifies line height as 5.3% of the viewport height.
|
||||||
|
*/
|
||||||
|
private static final float LINE_HEIGHT_RATIO = 0.0533f;
|
||||||
|
|
||||||
|
private final List<SubtitleView> subtitleViews;
|
||||||
|
|
||||||
|
private List<Cue> subtitleCues;
|
||||||
|
private int viewsInUse;
|
||||||
|
|
||||||
|
private float fontScale;
|
||||||
|
private float textSize;
|
||||||
|
private CaptionStyleCompat captionStyle;
|
||||||
|
|
||||||
|
public SubtitleLayout(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SubtitleLayout(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
subtitleViews = new ArrayList<SubtitleView>();
|
||||||
|
fontScale = 1;
|
||||||
|
captionStyle = CaptionStyleCompat.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the cues to be displayed by the view.
|
||||||
|
*
|
||||||
|
* @param cues The cues to display.
|
||||||
|
*/
|
||||||
|
public void setCues(List<Cue> cues) {
|
||||||
|
subtitleCues = cues;
|
||||||
|
int size = (cues == null) ? 0 : cues.size();
|
||||||
|
|
||||||
|
// create new subtitle views if necessary
|
||||||
|
if (size > subtitleViews.size()) {
|
||||||
|
for (int i = subtitleViews.size(); i < size; i++) {
|
||||||
|
SubtitleView newView = createSubtitleView();
|
||||||
|
subtitleViews.add(newView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the views we currently need, if necessary
|
||||||
|
for (int i = viewsInUse; i < size; i++) {
|
||||||
|
addView(subtitleViews.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the views we don't currently need, if necessary
|
||||||
|
for (int i = size; i < viewsInUse; i++) {
|
||||||
|
removeView(subtitleViews.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
viewsInUse = size;
|
||||||
|
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
subtitleViews.get(i).setText(cues.get(i).text);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the scale of the font.
|
||||||
|
*
|
||||||
|
* @param scale The scale of the font.
|
||||||
|
*/
|
||||||
|
public void setFontScale(float scale) {
|
||||||
|
fontScale = scale;
|
||||||
|
updateSubtitlesTextSize();
|
||||||
|
|
||||||
|
for (SubtitleView subtitleView : subtitleViews) {
|
||||||
|
subtitleView.setTextSize(textSize);
|
||||||
|
}
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the view according to the given style.
|
||||||
|
*
|
||||||
|
* @param captionStyle A style for the view.
|
||||||
|
*/
|
||||||
|
public void setStyle(CaptionStyleCompat captionStyle) {
|
||||||
|
this.captionStyle = captionStyle;
|
||||||
|
|
||||||
|
for (SubtitleView subtitleView : subtitleViews) {
|
||||||
|
subtitleView.setStyle(captionStyle);
|
||||||
|
}
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
|
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||||
|
setMeasuredDimension(width, height);
|
||||||
|
|
||||||
|
updateSubtitlesTextSize();
|
||||||
|
|
||||||
|
for (int i = 0; i < viewsInUse; i++) {
|
||||||
|
subtitleViews.get(i).setTextSize(textSize);
|
||||||
|
subtitleViews.get(i).measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
|
||||||
|
MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||||
|
int width = right - left;
|
||||||
|
int height = bottom - top;
|
||||||
|
|
||||||
|
for (int i = 0; i < viewsInUse; i++) {
|
||||||
|
SubtitleView subtitleView = subtitleViews.get(i);
|
||||||
|
Cue subtitleCue = subtitleCues.get(i);
|
||||||
|
|
||||||
|
int viewLeft = (width - subtitleView.getMeasuredWidth()) / 2;
|
||||||
|
int viewRight = viewLeft + subtitleView.getMeasuredWidth();
|
||||||
|
int viewTop = bottom - subtitleView.getMeasuredHeight();
|
||||||
|
int viewBottom = bottom;
|
||||||
|
|
||||||
|
if (subtitleCue.alignment != null) {
|
||||||
|
subtitleView.setTextAlignment(subtitleCue.alignment);
|
||||||
|
} else {
|
||||||
|
subtitleView.setTextAlignment(Alignment.ALIGN_CENTER);
|
||||||
|
}
|
||||||
|
if (subtitleCue.position != Cue.UNSET_VALUE) {
|
||||||
|
if (subtitleCue.alignment == Alignment.ALIGN_OPPOSITE) {
|
||||||
|
viewRight = (int) ((width * (double) subtitleCue.position) / 100) + left;
|
||||||
|
viewLeft = Math.max(viewRight - subtitleView.getMeasuredWidth(), left);
|
||||||
|
} else {
|
||||||
|
viewLeft = (int) ((width * (double) subtitleCue.position) / 100) + left;
|
||||||
|
viewRight = Math.min(viewLeft + subtitleView.getMeasuredWidth(), right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subtitleCue.line != Cue.UNSET_VALUE) {
|
||||||
|
viewTop = (int) (height * (double) subtitleCue.line / 100) + top;
|
||||||
|
viewBottom = viewTop + subtitleView.getMeasuredHeight();
|
||||||
|
if (viewBottom > bottom) {
|
||||||
|
viewTop = bottom - subtitleView.getMeasuredHeight();
|
||||||
|
viewBottom = bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subtitleView.layout(viewLeft, viewTop, viewRight, viewBottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSubtitlesTextSize() {
|
||||||
|
textSize = LINE_HEIGHT_RATIO * getHeight() * fontScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SubtitleView createSubtitleView() {
|
||||||
|
SubtitleView view = new SubtitleView(getContext());
|
||||||
|
view.setStyle(captionStyle);
|
||||||
|
view.setTextSize(textSize);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -28,6 +28,7 @@ import android.graphics.Paint.Join;
|
|||||||
import android.graphics.Paint.Style;
|
import android.graphics.Paint.Style;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
|
import android.text.Layout.Alignment;
|
||||||
import android.text.StaticLayout;
|
import android.text.StaticLayout;
|
||||||
import android.text.TextPaint;
|
import android.text.TextPaint;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
@ -35,10 +36,7 @@ import android.util.DisplayMetrics;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A view for rendering captions.
|
* A view for rendering a single caption.
|
||||||
* <p>
|
|
||||||
* The caption style and text size can be configured using {@link #setStyle(CaptionStyleCompat)} and
|
|
||||||
* {@link #setTextSize(float)} respectively.
|
|
||||||
*/
|
*/
|
||||||
public class SubtitleView extends View {
|
public class SubtitleView extends View {
|
||||||
|
|
||||||
@ -52,11 +50,6 @@ public class SubtitleView extends View {
|
|||||||
*/
|
*/
|
||||||
private final RectF lineBounds = new RectF();
|
private final RectF lineBounds = new RectF();
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable string builder used for holding text.
|
|
||||||
*/
|
|
||||||
private final StringBuilder textBuilder = new StringBuilder();
|
|
||||||
|
|
||||||
// Styled dimensions.
|
// Styled dimensions.
|
||||||
private final float cornerRadius;
|
private final float cornerRadius;
|
||||||
private final float outlineWidth;
|
private final float outlineWidth;
|
||||||
@ -66,6 +59,8 @@ public class SubtitleView extends View {
|
|||||||
private TextPaint textPaint;
|
private TextPaint textPaint;
|
||||||
private Paint paint;
|
private Paint paint;
|
||||||
|
|
||||||
|
private CharSequence text;
|
||||||
|
|
||||||
private int foregroundColor;
|
private int foregroundColor;
|
||||||
private int backgroundColor;
|
private int backgroundColor;
|
||||||
private int edgeColor;
|
private int edgeColor;
|
||||||
@ -75,10 +70,15 @@ public class SubtitleView extends View {
|
|||||||
private int lastMeasuredWidth;
|
private int lastMeasuredWidth;
|
||||||
private StaticLayout layout;
|
private StaticLayout layout;
|
||||||
|
|
||||||
|
private Alignment alignment;
|
||||||
private float spacingMult;
|
private float spacingMult;
|
||||||
private float spacingAdd;
|
private float spacingAdd;
|
||||||
private int innerPaddingX;
|
private int innerPaddingX;
|
||||||
|
|
||||||
|
public SubtitleView(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
public SubtitleView(Context context, AttributeSet attrs) {
|
public SubtitleView(Context context, AttributeSet attrs) {
|
||||||
this(context, attrs, 0);
|
this(context, attrs, 0);
|
||||||
}
|
}
|
||||||
@ -107,6 +107,8 @@ public class SubtitleView extends View {
|
|||||||
textPaint.setAntiAlias(true);
|
textPaint.setAntiAlias(true);
|
||||||
textPaint.setSubpixelText(true);
|
textPaint.setSubpixelText(true);
|
||||||
|
|
||||||
|
alignment = Alignment.ALIGN_CENTER;
|
||||||
|
|
||||||
paint = new Paint();
|
paint = new Paint();
|
||||||
paint.setAntiAlias(true);
|
paint.setAntiAlias(true);
|
||||||
|
|
||||||
@ -116,10 +118,6 @@ public class SubtitleView extends View {
|
|||||||
setStyle(CaptionStyleCompat.DEFAULT);
|
setStyle(CaptionStyleCompat.DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SubtitleView(Context context) {
|
|
||||||
this(context, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBackgroundColor(int color) {
|
public void setBackgroundColor(int color) {
|
||||||
backgroundColor = color;
|
backgroundColor = color;
|
||||||
@ -132,8 +130,7 @@ public class SubtitleView extends View {
|
|||||||
* @param text The text to display.
|
* @param text The text to display.
|
||||||
*/
|
*/
|
||||||
public void setText(CharSequence text) {
|
public void setText(CharSequence text) {
|
||||||
textBuilder.setLength(0);
|
this.text = text;
|
||||||
textBuilder.append(text);
|
|
||||||
forceUpdate(true);
|
forceUpdate(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,6 +147,15 @@ public class SubtitleView extends View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the text alignment.
|
||||||
|
*
|
||||||
|
* @param textAlignment The text alignment.
|
||||||
|
*/
|
||||||
|
public void setTextAlignment(Alignment textAlignment) {
|
||||||
|
alignment = textAlignment;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the view according to the given style.
|
* Configures the view according to the given style.
|
||||||
*
|
*
|
||||||
@ -227,8 +233,7 @@ public class SubtitleView extends View {
|
|||||||
|
|
||||||
hasMeasurements = true;
|
hasMeasurements = true;
|
||||||
lastMeasuredWidth = maxWidth;
|
lastMeasuredWidth = maxWidth;
|
||||||
layout = new StaticLayout(textBuilder, textPaint, maxWidth, null, spacingMult, spacingAdd,
|
layout = new StaticLayout(text, textPaint, maxWidth, alignment, spacingMult, spacingAdd, true);
|
||||||
true);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,16 +15,18 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.text;
|
package com.google.android.exoplayer.text;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface for components that render text.
|
* An interface for components that render text.
|
||||||
*/
|
*/
|
||||||
public interface TextRenderer {
|
public interface TextRenderer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked each time there is a change in the text to be rendered.
|
* Invoked each time there is a change in the {@link Cue}s to be rendered.
|
||||||
*
|
*
|
||||||
* @param text The text to render, or null if no text is to be rendered.
|
* @param cues The {@link Cue}s to be rendered, or an empty list if no cues are to be rendered.
|
||||||
*/
|
*/
|
||||||
void onText(String text);
|
void onCues(List<Cue> cues);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,8 @@ import android.os.Looper;
|
|||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link TrackRenderer} for textual subtitles. The actual rendering of each line of text to a
|
* A {@link TrackRenderer} for textual subtitles. The actual rendering of each line of text to a
|
||||||
@ -255,34 +257,36 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateTextRenderer(long positionUs) {
|
private void updateTextRenderer(long positionUs) {
|
||||||
String text = subtitle.getText(positionUs);
|
List<Cue> cues = subtitle.getCues(positionUs);
|
||||||
if (textRendererHandler != null) {
|
if (textRendererHandler != null) {
|
||||||
textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, text).sendToTarget();
|
textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, cues).sendToTarget();
|
||||||
} else {
|
} else {
|
||||||
invokeRendererInternal(text);
|
invokeRendererInternalCues(cues);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearTextRenderer() {
|
private void clearTextRenderer() {
|
||||||
if (textRendererHandler != null) {
|
if (textRendererHandler != null) {
|
||||||
textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, null).sendToTarget();
|
textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, Collections.<Cue>emptyList())
|
||||||
|
.sendToTarget();
|
||||||
} else {
|
} else {
|
||||||
invokeRendererInternal(null);
|
invokeRendererInternalCues(Collections.<Cue>emptyList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
@Override
|
@Override
|
||||||
public boolean handleMessage(Message msg) {
|
public boolean handleMessage(Message msg) {
|
||||||
switch (msg.what) {
|
switch (msg.what) {
|
||||||
case MSG_UPDATE_OVERLAY:
|
case MSG_UPDATE_OVERLAY:
|
||||||
invokeRendererInternal((String) msg.obj);
|
invokeRendererInternalCues((List<Cue>) msg.obj);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invokeRendererInternal(String text) {
|
private void invokeRendererInternalCues(List<Cue> cues) {
|
||||||
textRenderer.onText(text);
|
textRenderer.onCues(cues);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import com.google.android.exoplayer.MediaFormatHolder;
|
|||||||
import com.google.android.exoplayer.SampleHolder;
|
import com.google.android.exoplayer.SampleHolder;
|
||||||
import com.google.android.exoplayer.SampleSource;
|
import com.google.android.exoplayer.SampleSource;
|
||||||
import com.google.android.exoplayer.TrackRenderer;
|
import com.google.android.exoplayer.TrackRenderer;
|
||||||
|
import com.google.android.exoplayer.text.Cue;
|
||||||
import com.google.android.exoplayer.text.TextRenderer;
|
import com.google.android.exoplayer.text.TextRenderer;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
@ -31,6 +32,7 @@ import android.os.Looper;
|
|||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -227,8 +229,9 @@ public class Eia608TrackRenderer extends TrackRenderer implements Callback {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invokeRendererInternal(String text) {
|
private void invokeRendererInternal(String cueText) {
|
||||||
textRenderer.onText(text);
|
Cue cue = new Cue(cueText);
|
||||||
|
textRenderer.onCues(Collections.singletonList(cue));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeParsePendingSample() {
|
private void maybeParsePendingSample() {
|
||||||
|
@ -15,9 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.text.ttml;
|
package com.google.android.exoplayer.text.ttml;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.text.Cue;
|
||||||
import com.google.android.exoplayer.text.Subtitle;
|
import com.google.android.exoplayer.text.Subtitle;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A representation of a TTML subtitle.
|
* A representation of a TTML subtitle.
|
||||||
*/
|
*/
|
||||||
@ -60,8 +64,14 @@ public final class TtmlSubtitle implements Subtitle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getText(long timeUs) {
|
public List<Cue> getCues(long timeUs) {
|
||||||
return root.getText(timeUs - startTimeUs);
|
String cueText = root.getText(timeUs - startTimeUs);
|
||||||
|
if (cueText == null) {
|
||||||
|
return Collections.<Cue>emptyList();
|
||||||
|
} else {
|
||||||
|
Cue cue = new Cue(cueText);
|
||||||
|
return Collections.singletonList(cue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* 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.webvtt;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.text.Cue;
|
||||||
|
|
||||||
|
import android.text.Layout.Alignment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of a WebVTT cue.
|
||||||
|
*/
|
||||||
|
/* package */ final class WebvttCue extends Cue {
|
||||||
|
|
||||||
|
public final long startTime;
|
||||||
|
public final long endTime;
|
||||||
|
|
||||||
|
public WebvttCue(CharSequence text) {
|
||||||
|
this(Cue.UNSET_VALUE, Cue.UNSET_VALUE, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebvttCue(long startTime, long endTime, CharSequence text) {
|
||||||
|
this(startTime, endTime, text, Cue.UNSET_VALUE, Cue.UNSET_VALUE, null, Cue.UNSET_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebvttCue(long startTime, long endTime, CharSequence text, int line, int position,
|
||||||
|
Alignment alignment, int size) {
|
||||||
|
super(text, line, position, alignment, size);
|
||||||
|
this.startTime = startTime;
|
||||||
|
this.endTime = endTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not this cue should be placed in the default position and rolled-up with
|
||||||
|
* the other "normal" cues.
|
||||||
|
*
|
||||||
|
* @return True if this cue should be placed in the default position; false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isNormalCue() {
|
||||||
|
return (line == UNSET_VALUE && position == UNSET_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -17,9 +17,14 @@ package com.google.android.exoplayer.text.webvtt;
|
|||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.ParserException;
|
import com.google.android.exoplayer.ParserException;
|
||||||
|
import com.google.android.exoplayer.text.Cue;
|
||||||
import com.google.android.exoplayer.text.SubtitleParser;
|
import com.google.android.exoplayer.text.SubtitleParser;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
|
||||||
|
import android.text.Html;
|
||||||
|
import android.text.Layout.Alignment;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@ -35,6 +40,8 @@ import java.util.regex.Pattern;
|
|||||||
*/
|
*/
|
||||||
public class WebvttParser implements SubtitleParser {
|
public class WebvttParser implements SubtitleParser {
|
||||||
|
|
||||||
|
static final String TAG = "WebvttParser";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This parser allows a custom header to be prepended to the WebVTT data, in the form of a text
|
* This parser allows a custom header to be prepended to the WebVTT data, in the form of a text
|
||||||
* line starting with this string.
|
* line starting with this string.
|
||||||
@ -63,21 +70,26 @@ public class WebvttParser implements SubtitleParser {
|
|||||||
private static final String WEBVTT_TIMESTAMP_STRING = "(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}";
|
private static final String WEBVTT_TIMESTAMP_STRING = "(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}";
|
||||||
private static final Pattern WEBVTT_TIMESTAMP = Pattern.compile(WEBVTT_TIMESTAMP_STRING);
|
private static final Pattern WEBVTT_TIMESTAMP = Pattern.compile(WEBVTT_TIMESTAMP_STRING);
|
||||||
|
|
||||||
|
private static final String WEBVTT_CUE_SETTING_STRING = "\\S*:\\S*";
|
||||||
|
private static final Pattern WEBVTT_CUE_SETTING = Pattern.compile(WEBVTT_CUE_SETTING_STRING);
|
||||||
|
|
||||||
private static final Pattern MEDIA_TIMESTAMP_OFFSET = Pattern.compile(OFFSET + "\\d+");
|
private static final Pattern MEDIA_TIMESTAMP_OFFSET = Pattern.compile(OFFSET + "\\d+");
|
||||||
private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:\\d+");
|
private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:\\d+");
|
||||||
|
|
||||||
private static final String WEBVTT_CUE_TAG_STRING = "\\<.*?>";
|
private static final String NON_NUMERIC_STRING = ".*[^0-9].*";
|
||||||
|
|
||||||
|
private final StringBuilder textBuilder;
|
||||||
|
|
||||||
private final boolean strictParsing;
|
private final boolean strictParsing;
|
||||||
private final boolean filterTags;
|
|
||||||
|
|
||||||
public WebvttParser() {
|
public WebvttParser() {
|
||||||
this(true, true);
|
this(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebvttParser(boolean strictParsing, boolean filterTags) {
|
public WebvttParser(boolean strictParsing) {
|
||||||
this.strictParsing = strictParsing;
|
this.strictParsing = strictParsing;
|
||||||
this.filterTags = filterTags;
|
|
||||||
|
textBuilder = new StringBuilder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -145,6 +157,7 @@ public class WebvttParser implements SubtitleParser {
|
|||||||
|
|
||||||
// process the cues and text
|
// process the cues and text
|
||||||
while ((line = webvttData.readLine()) != null) {
|
while ((line = webvttData.readLine()) != null) {
|
||||||
|
|
||||||
// parse the cue identifier (if present) {
|
// parse the cue identifier (if present) {
|
||||||
Matcher matcher = WEBVTT_CUE_IDENTIFIER.matcher(line);
|
Matcher matcher = WEBVTT_CUE_IDENTIFIER.matcher(line);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
@ -152,11 +165,16 @@ public class WebvttParser implements SubtitleParser {
|
|||||||
line = webvttData.readLine();
|
line = webvttData.readLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long startTime = Cue.UNSET_VALUE;
|
||||||
|
long endTime = Cue.UNSET_VALUE;
|
||||||
|
CharSequence text = null;
|
||||||
|
int lineNum = Cue.UNSET_VALUE;
|
||||||
|
int position = Cue.UNSET_VALUE;
|
||||||
|
Alignment alignment = null;
|
||||||
|
int size = Cue.UNSET_VALUE;
|
||||||
|
|
||||||
// parse the cue timestamps
|
// parse the cue timestamps
|
||||||
matcher = WEBVTT_TIMESTAMP.matcher(line);
|
matcher = WEBVTT_TIMESTAMP.matcher(line);
|
||||||
long startTime;
|
|
||||||
long endTime;
|
|
||||||
String text = "";
|
|
||||||
|
|
||||||
// parse start timestamp
|
// parse start timestamp
|
||||||
if (!matcher.find()) {
|
if (!matcher.find()) {
|
||||||
@ -166,36 +184,76 @@ public class WebvttParser implements SubtitleParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parse end timestamp
|
// parse end timestamp
|
||||||
|
String endTimeString;
|
||||||
if (!matcher.find()) {
|
if (!matcher.find()) {
|
||||||
throw new ParserException("Expected cue end time: " + line);
|
throw new ParserException("Expected cue end time: " + line);
|
||||||
} else {
|
} else {
|
||||||
endTime = parseTimestampUs(matcher.group()) + mediaTimestampUs;
|
endTimeString = matcher.group();
|
||||||
|
endTime = parseTimestampUs(endTimeString) + mediaTimestampUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the (optional) cue setting list
|
||||||
|
line = line.substring(line.indexOf(endTimeString) + endTimeString.length());
|
||||||
|
matcher = WEBVTT_CUE_SETTING.matcher(line);
|
||||||
|
while (matcher.find()) {
|
||||||
|
String match = matcher.group();
|
||||||
|
String[] parts = match.split(":", 2);
|
||||||
|
String name = parts[0];
|
||||||
|
String value = parts[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ("line".equals(name)) {
|
||||||
|
if (value.endsWith("%")) {
|
||||||
|
lineNum = parseIntPercentage(value);
|
||||||
|
} else if (value.matches(NON_NUMERIC_STRING)) {
|
||||||
|
Log.w(TAG, "Invalid line value: " + value);
|
||||||
|
} else {
|
||||||
|
lineNum = Integer.parseInt(value);
|
||||||
|
}
|
||||||
|
} else if ("align".equals(name)) {
|
||||||
|
// TODO: handle for RTL languages
|
||||||
|
if ("start".equals(value)) {
|
||||||
|
alignment = Alignment.ALIGN_NORMAL;
|
||||||
|
} else if ("middle".equals(value)) {
|
||||||
|
alignment = Alignment.ALIGN_CENTER;
|
||||||
|
} else if ("end".equals(value)) {
|
||||||
|
alignment = Alignment.ALIGN_OPPOSITE;
|
||||||
|
} else if ("left".equals(value)) {
|
||||||
|
alignment = Alignment.ALIGN_NORMAL;
|
||||||
|
} else if ("right".equals(value)) {
|
||||||
|
alignment = Alignment.ALIGN_OPPOSITE;
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Invalid align value: " + value);
|
||||||
|
}
|
||||||
|
} else if ("position".equals(name)) {
|
||||||
|
position = parseIntPercentage(value);
|
||||||
|
} else if ("size".equals(name)) {
|
||||||
|
size = parseIntPercentage(value);
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Unknown cue setting " + name + ":" + value);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
Log.w(TAG, name + " contains an invalid value " + value, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse text
|
// parse text
|
||||||
|
textBuilder.setLength(0);
|
||||||
while (((line = webvttData.readLine()) != null) && (!line.isEmpty())) {
|
while (((line = webvttData.readLine()) != null) && (!line.isEmpty())) {
|
||||||
text += processCueText(line.trim()) + "\n";
|
if (textBuilder.length() > 0) {
|
||||||
|
textBuilder.append("<br>");
|
||||||
|
}
|
||||||
|
textBuilder.append(line.trim());
|
||||||
}
|
}
|
||||||
|
text = Html.fromHtml(textBuilder.toString());
|
||||||
|
|
||||||
WebvttCue cue = new WebvttCue(startTime, endTime, text);
|
WebvttCue cue = new WebvttCue(startTime, endTime, text, lineNum, position, alignment, size);
|
||||||
subtitles.add(cue);
|
subtitles.add(cue);
|
||||||
}
|
}
|
||||||
|
|
||||||
webvttData.close();
|
webvttData.close();
|
||||||
inputStream.close();
|
inputStream.close();
|
||||||
|
WebvttSubtitle subtitle = new WebvttSubtitle(subtitles, mediaTimestampUs);
|
||||||
// copy WebvttCue data into arrays for WebvttSubtitle constructor
|
|
||||||
String[] cueText = new String[subtitles.size()];
|
|
||||||
long[] cueTimesUs = new long[2 * subtitles.size()];
|
|
||||||
for (int subtitleIndex = 0; subtitleIndex < subtitles.size(); subtitleIndex++) {
|
|
||||||
int arrayIndex = subtitleIndex * 2;
|
|
||||||
WebvttCue cue = subtitles.get(subtitleIndex);
|
|
||||||
cueTimesUs[arrayIndex] = cue.startTime;
|
|
||||||
cueTimesUs[arrayIndex + 1] = cue.endTime;
|
|
||||||
cueText[subtitleIndex] = cue.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
WebvttSubtitle subtitle = new WebvttSubtitle(cueText, mediaTimestampUs, cueTimesUs);
|
|
||||||
return subtitle;
|
return subtitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,25 +266,29 @@ public class WebvttParser implements SubtitleParser {
|
|||||||
return startTimeUs;
|
return startTimeUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String processCueText(String line) {
|
|
||||||
if (filterTags) {
|
|
||||||
line = line.replaceAll(WEBVTT_CUE_TAG_STRING, "");
|
|
||||||
line = line.replaceAll("<", "<");
|
|
||||||
line = line.replaceAll(">", ">");
|
|
||||||
line = line.replaceAll(" ", " ");
|
|
||||||
line = line.replaceAll("&", "&");
|
|
||||||
return line;
|
|
||||||
} else {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void handleNoncompliantLine(String line) throws ParserException {
|
protected void handleNoncompliantLine(String line) throws ParserException {
|
||||||
if (strictParsing) {
|
if (strictParsing) {
|
||||||
throw new ParserException("Unexpected line: " + line);
|
throw new ParserException("Unexpected line: " + line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int parseIntPercentage(String s) throws NumberFormatException {
|
||||||
|
if (!s.endsWith("%")) {
|
||||||
|
throw new NumberFormatException(s + " doesn't end with '%'");
|
||||||
|
}
|
||||||
|
|
||||||
|
s = s.substring(0, s.length() - 1);
|
||||||
|
if (s.matches(NON_NUMERIC_STRING)) {
|
||||||
|
throw new NumberFormatException(s + " contains an invalid character");
|
||||||
|
}
|
||||||
|
|
||||||
|
int value = Integer.parseInt(s);
|
||||||
|
if (value < 0 || value > 100) {
|
||||||
|
throw new NumberFormatException(value + " is out of range [0-100]");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private static long parseTimestampUs(String s) throws NumberFormatException {
|
private static long parseTimestampUs(String s) throws NumberFormatException {
|
||||||
if (!s.matches(WEBVTT_TIMESTAMP_STRING)) {
|
if (!s.matches(WEBVTT_TIMESTAMP_STRING)) {
|
||||||
throw new NumberFormatException("has invalid format");
|
throw new NumberFormatException("has invalid format");
|
||||||
@ -240,16 +302,4 @@ public class WebvttParser implements SubtitleParser {
|
|||||||
return (value * 1000 + Long.parseLong(parts[1])) * 1000;
|
return (value * 1000 + Long.parseLong(parts[1])) * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class WebvttCue {
|
|
||||||
public final long startTime;
|
|
||||||
public final long endTime;
|
|
||||||
public final String text;
|
|
||||||
|
|
||||||
public WebvttCue(long startTime, long endTime, String text) {
|
|
||||||
this.startTime = startTime;
|
|
||||||
this.endTime = endTime;
|
|
||||||
this.text = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,32 +15,46 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.text.webvtt;
|
package com.google.android.exoplayer.text.webvtt;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.text.Cue;
|
||||||
import com.google.android.exoplayer.text.Subtitle;
|
import com.google.android.exoplayer.text.Subtitle;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A representation of a WebVTT subtitle.
|
* A representation of a WebVTT subtitle.
|
||||||
*/
|
*/
|
||||||
public class WebvttSubtitle implements Subtitle {
|
public class WebvttSubtitle implements Subtitle {
|
||||||
|
|
||||||
private final String[] cueText;
|
private final List<WebvttCue> cues;
|
||||||
|
private final int numCues;
|
||||||
private final long startTimeUs;
|
private final long startTimeUs;
|
||||||
private final long[] cueTimesUs;
|
private final long[] cueTimesUs;
|
||||||
private final long[] sortedCueTimesUs;
|
private final long[] sortedCueTimesUs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param cueText Text to be displayed during each cue.
|
* @param cues A list of the cues in this subtitle.
|
||||||
* @param startTimeUs The start time of the subtitle.
|
* @param startTimeUs The start time of the subtitle.
|
||||||
* @param cueTimesUs Cue event times, where cueTimesUs[2 * i] and cueTimesUs[(2 * i) + 1] are
|
|
||||||
* the start and end times, respectively, corresponding to cueText[i].
|
|
||||||
*/
|
*/
|
||||||
public WebvttSubtitle(String[] cueText, long startTimeUs, long[] cueTimesUs) {
|
public WebvttSubtitle(List<WebvttCue> cues, long startTimeUs) {
|
||||||
this.cueText = cueText;
|
this.cues = cues;
|
||||||
|
numCues = cues.size();
|
||||||
this.startTimeUs = startTimeUs;
|
this.startTimeUs = startTimeUs;
|
||||||
this.cueTimesUs = cueTimesUs;
|
|
||||||
|
this.cueTimesUs = new long[2 * numCues];
|
||||||
|
for (int cueIndex = 0; cueIndex < numCues; cueIndex++) {
|
||||||
|
WebvttCue cue = cues.get(cueIndex);
|
||||||
|
int arrayIndex = cueIndex * 2;
|
||||||
|
cueTimesUs[arrayIndex] = cue.startTime;
|
||||||
|
cueTimesUs[arrayIndex + 1] = cue.endTime;
|
||||||
|
}
|
||||||
|
|
||||||
this.sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length);
|
this.sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length);
|
||||||
Arrays.sort(sortedCueTimesUs);
|
Arrays.sort(sortedCueTimesUs);
|
||||||
}
|
}
|
||||||
@ -78,22 +92,47 @@ public class WebvttSubtitle implements Subtitle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getText(long timeUs) {
|
public List<Cue> getCues(long timeUs) {
|
||||||
StringBuilder stringBuilder = new StringBuilder();
|
ArrayList<Cue> list = null;
|
||||||
|
WebvttCue firstNormalCue = null;
|
||||||
|
SpannableStringBuilder normalCueTextBuilder = null;
|
||||||
|
|
||||||
for (int i = 0; i < cueTimesUs.length; i += 2) {
|
for (int i = 0; i < numCues; i++) {
|
||||||
if ((cueTimesUs[i] <= timeUs) && (timeUs < cueTimesUs[i + 1])) {
|
if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) {
|
||||||
stringBuilder.append(cueText[i / 2]);
|
if (list == null) {
|
||||||
|
list = new ArrayList<Cue>();
|
||||||
|
}
|
||||||
|
WebvttCue cue = cues.get(i);
|
||||||
|
if (cue.isNormalCue()) {
|
||||||
|
// we want to merge all of the normal cues into a single cue to ensure they are drawn
|
||||||
|
// correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple
|
||||||
|
// normal cues, otherwise we can just append the single normal cue
|
||||||
|
if (firstNormalCue == null) {
|
||||||
|
firstNormalCue = cue;
|
||||||
|
} else if (normalCueTextBuilder == null) {
|
||||||
|
normalCueTextBuilder = new SpannableStringBuilder();
|
||||||
|
normalCueTextBuilder.append(firstNormalCue.text).append("\n").append(cue.text);
|
||||||
|
} else {
|
||||||
|
normalCueTextBuilder.append("\n").append(cue.text);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
list.add(cue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (normalCueTextBuilder != null) {
|
||||||
int stringLength = stringBuilder.length();
|
// there were multiple normal cues, so create a new cue with all of the text
|
||||||
if (stringLength > 0 && stringBuilder.charAt(stringLength - 1) == '\n') {
|
list.add(new WebvttCue(normalCueTextBuilder));
|
||||||
// Adjust the length to remove the trailing newline character.
|
} else if (firstNormalCue != null) {
|
||||||
stringLength -= 1;
|
// there was only a single normal cue, so just add it to the list
|
||||||
|
list.add(firstNormalCue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return stringLength == 0 ? null : stringBuilder.substring(0, stringLength);
|
if (list != null) {
|
||||||
|
return list;
|
||||||
|
} else {
|
||||||
|
return Collections.<Cue>emptyList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -11,4 +11,4 @@ This is the <b><i>second</b></i> subtitle.
|
|||||||
This is the <c.red.caps>third</c> subtitle.
|
This is the <c.red.caps>third</c> subtitle.
|
||||||
|
|
||||||
00:06.000 --> 00:07.000
|
00:06.000 --> 00:07.000
|
||||||
This is the <fourth> &subtitle.
|
This is the <fourth> &subtitle.
|
||||||
|
@ -59,13 +59,13 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
|||||||
// test first cue
|
// test first cue
|
||||||
assertEquals(startTimeUs, subtitle.getEventTime(0));
|
assertEquals(startTimeUs, subtitle.getEventTime(0));
|
||||||
assertEquals("This is the first subtitle.",
|
assertEquals("This is the first subtitle.",
|
||||||
subtitle.getText(subtitle.getEventTime(0)));
|
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
|
||||||
assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1));
|
assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1));
|
||||||
|
|
||||||
// test second cue
|
// test second cue
|
||||||
assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2));
|
assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2));
|
||||||
assertEquals("This is the second subtitle.",
|
assertEquals("This is the second subtitle.",
|
||||||
subtitle.getText(subtitle.getEventTime(2)));
|
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
|
||||||
assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3));
|
assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,13 +84,13 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
|||||||
// test first cue
|
// test first cue
|
||||||
assertEquals(startTimeUs, subtitle.getEventTime(0));
|
assertEquals(startTimeUs, subtitle.getEventTime(0));
|
||||||
assertEquals("This is the first subtitle.",
|
assertEquals("This is the first subtitle.",
|
||||||
subtitle.getText(subtitle.getEventTime(0)));
|
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
|
||||||
assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1));
|
assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1));
|
||||||
|
|
||||||
// test second cue
|
// test second cue
|
||||||
assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2));
|
assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2));
|
||||||
assertEquals("This is the second subtitle.",
|
assertEquals("This is the second subtitle.",
|
||||||
subtitle.getText(subtitle.getEventTime(2)));
|
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
|
||||||
assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3));
|
assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,25 +109,25 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
|||||||
// test first cue
|
// test first cue
|
||||||
assertEquals(startTimeUs, subtitle.getEventTime(0));
|
assertEquals(startTimeUs, subtitle.getEventTime(0));
|
||||||
assertEquals("This is the first subtitle.",
|
assertEquals("This is the first subtitle.",
|
||||||
subtitle.getText(subtitle.getEventTime(0)));
|
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
|
||||||
assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1));
|
assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1));
|
||||||
|
|
||||||
// test second cue
|
// test second cue
|
||||||
assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2));
|
assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2));
|
||||||
assertEquals("This is the second subtitle.",
|
assertEquals("This is the second subtitle.",
|
||||||
subtitle.getText(subtitle.getEventTime(2)));
|
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
|
||||||
assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3));
|
assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3));
|
||||||
|
|
||||||
// test third cue
|
// test third cue
|
||||||
assertEquals(startTimeUs + 4000000, subtitle.getEventTime(4));
|
assertEquals(startTimeUs + 4000000, subtitle.getEventTime(4));
|
||||||
assertEquals("This is the third subtitle.",
|
assertEquals("This is the third subtitle.",
|
||||||
subtitle.getText(subtitle.getEventTime(4)));
|
subtitle.getCues(subtitle.getEventTime(4)).get(0).text.toString());
|
||||||
assertEquals(startTimeUs + 5000000, subtitle.getEventTime(5));
|
assertEquals(startTimeUs + 5000000, subtitle.getEventTime(5));
|
||||||
|
|
||||||
// test fourth cue
|
// test fourth cue
|
||||||
assertEquals(startTimeUs + 6000000, subtitle.getEventTime(6));
|
assertEquals(startTimeUs + 6000000, subtitle.getEventTime(6));
|
||||||
assertEquals("This is the <fourth> &subtitle.",
|
assertEquals("This is the <fourth> &subtitle.",
|
||||||
subtitle.getText(subtitle.getEventTime(6)));
|
subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString());
|
||||||
assertEquals(startTimeUs + 7000000, subtitle.getEventTime(7));
|
assertEquals(startTimeUs + 7000000, subtitle.getEventTime(7));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,8 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.text.webvtt;
|
package com.google.android.exoplayer.text.webvtt;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.text.Cue;
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link WebvttSubtitle}.
|
* Unit test for {@link WebvttSubtitle}.
|
||||||
*/
|
*/
|
||||||
@ -25,21 +30,39 @@ public class WebvttSubtitleTest extends TestCase {
|
|||||||
private static final String FIRST_SUBTITLE_STRING = "This is the first subtitle.";
|
private static final String FIRST_SUBTITLE_STRING = "This is the first subtitle.";
|
||||||
private static final String SECOND_SUBTITLE_STRING = "This is the second subtitle.";
|
private static final String SECOND_SUBTITLE_STRING = "This is the second subtitle.";
|
||||||
private static final String FIRST_AND_SECOND_SUBTITLE_STRING =
|
private static final String FIRST_AND_SECOND_SUBTITLE_STRING =
|
||||||
FIRST_SUBTITLE_STRING + SECOND_SUBTITLE_STRING;
|
FIRST_SUBTITLE_STRING + "\n" + SECOND_SUBTITLE_STRING;
|
||||||
|
|
||||||
private WebvttSubtitle emptySubtitle = new WebvttSubtitle(new String[] {}, 0, new long[] {});
|
private WebvttSubtitle emptySubtitle = new WebvttSubtitle(new ArrayList<WebvttCue>(), 0);
|
||||||
|
|
||||||
private WebvttSubtitle simpleSubtitle = new WebvttSubtitle(
|
private ArrayList<WebvttCue> simpleSubtitleCues = new ArrayList<WebvttCue>();
|
||||||
new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0,
|
{
|
||||||
new long[] {1000000, 2000000, 3000000, 4000000});
|
WebvttCue firstCue = new WebvttCue(1000000, 2000000, FIRST_SUBTITLE_STRING);
|
||||||
|
simpleSubtitleCues.add(firstCue);
|
||||||
|
|
||||||
private WebvttSubtitle overlappingSubtitle = new WebvttSubtitle(
|
WebvttCue secondCue = new WebvttCue(3000000, 4000000, SECOND_SUBTITLE_STRING);
|
||||||
new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0,
|
simpleSubtitleCues.add(secondCue);
|
||||||
new long[] {1000000, 3000000, 2000000, 4000000});
|
}
|
||||||
|
private WebvttSubtitle simpleSubtitle = new WebvttSubtitle(simpleSubtitleCues, 0);
|
||||||
|
|
||||||
private WebvttSubtitle nestedSubtitle = new WebvttSubtitle(
|
private ArrayList<WebvttCue> overlappingSubtitleCues = new ArrayList<WebvttCue>();
|
||||||
new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0,
|
{
|
||||||
new long[] {1000000, 4000000, 2000000, 3000000});
|
WebvttCue firstCue = new WebvttCue(1000000, 3000000, FIRST_SUBTITLE_STRING);
|
||||||
|
overlappingSubtitleCues.add(firstCue);
|
||||||
|
|
||||||
|
WebvttCue secondCue = new WebvttCue(2000000, 4000000, SECOND_SUBTITLE_STRING);
|
||||||
|
overlappingSubtitleCues.add(secondCue);
|
||||||
|
}
|
||||||
|
private WebvttSubtitle overlappingSubtitle = new WebvttSubtitle(overlappingSubtitleCues, 0);
|
||||||
|
|
||||||
|
private ArrayList<WebvttCue> nestedSubtitleCues = new ArrayList<WebvttCue>();
|
||||||
|
{
|
||||||
|
WebvttCue firstCue = new WebvttCue(1000000, 4000000, FIRST_SUBTITLE_STRING);
|
||||||
|
nestedSubtitleCues.add(firstCue);
|
||||||
|
|
||||||
|
WebvttCue secondCue = new WebvttCue(2000000, 3000000, SECOND_SUBTITLE_STRING);
|
||||||
|
nestedSubtitleCues.add(secondCue);
|
||||||
|
}
|
||||||
|
private WebvttSubtitle nestedSubtitle = new WebvttSubtitle(nestedSubtitleCues, 0);
|
||||||
|
|
||||||
public void testEventCount() {
|
public void testEventCount() {
|
||||||
assertEquals(0, emptySubtitle.getEventTimeCount());
|
assertEquals(0, emptySubtitle.getEventTimeCount());
|
||||||
@ -72,29 +95,29 @@ public class WebvttSubtitleTest extends TestCase {
|
|||||||
|
|
||||||
public void testSimpleSubtitleText() {
|
public void testSimpleSubtitleText() {
|
||||||
// Test before first subtitle
|
// Test before first subtitle
|
||||||
assertNull(simpleSubtitle.getText(0));
|
assertSingleCueEmpty(simpleSubtitle.getCues(0));
|
||||||
assertNull(simpleSubtitle.getText(500000));
|
assertSingleCueEmpty(simpleSubtitle.getCues(500000));
|
||||||
assertNull(simpleSubtitle.getText(999999));
|
assertSingleCueEmpty(simpleSubtitle.getCues(999999));
|
||||||
|
|
||||||
// Test first subtitle
|
// Test first subtitle
|
||||||
assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1000000));
|
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1000000));
|
||||||
assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1500000));
|
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1500000));
|
||||||
assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1999999));
|
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1999999));
|
||||||
|
|
||||||
// Test after first subtitle, before second subtitle
|
// Test after first subtitle, before second subtitle
|
||||||
assertNull(simpleSubtitle.getText(2000000));
|
assertSingleCueEmpty(simpleSubtitle.getCues(2000000));
|
||||||
assertNull(simpleSubtitle.getText(2500000));
|
assertSingleCueEmpty(simpleSubtitle.getCues(2500000));
|
||||||
assertNull(simpleSubtitle.getText(2999999));
|
assertSingleCueEmpty(simpleSubtitle.getCues(2999999));
|
||||||
|
|
||||||
// Test second subtitle
|
// Test second subtitle
|
||||||
assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3000000));
|
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3000000));
|
||||||
assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3500000));
|
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3500000));
|
||||||
assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3999999));
|
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3999999));
|
||||||
|
|
||||||
// Test after second subtitle
|
// Test after second subtitle
|
||||||
assertNull(simpleSubtitle.getText(4000000));
|
assertSingleCueEmpty(simpleSubtitle.getCues(4000000));
|
||||||
assertNull(simpleSubtitle.getText(4500000));
|
assertSingleCueEmpty(simpleSubtitle.getCues(4500000));
|
||||||
assertNull(simpleSubtitle.getText(Long.MAX_VALUE));
|
assertSingleCueEmpty(simpleSubtitle.getCues(Long.MAX_VALUE));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testOverlappingSubtitleEventTimes() {
|
public void testOverlappingSubtitleEventTimes() {
|
||||||
@ -107,29 +130,32 @@ public class WebvttSubtitleTest extends TestCase {
|
|||||||
|
|
||||||
public void testOverlappingSubtitleText() {
|
public void testOverlappingSubtitleText() {
|
||||||
// Test before first subtitle
|
// Test before first subtitle
|
||||||
assertNull(overlappingSubtitle.getText(0));
|
assertSingleCueEmpty(overlappingSubtitle.getCues(0));
|
||||||
assertNull(overlappingSubtitle.getText(500000));
|
assertSingleCueEmpty(overlappingSubtitle.getCues(500000));
|
||||||
assertNull(overlappingSubtitle.getText(999999));
|
assertSingleCueEmpty(overlappingSubtitle.getCues(999999));
|
||||||
|
|
||||||
// Test first subtitle
|
// Test first subtitle
|
||||||
assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1000000));
|
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1000000));
|
||||||
assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1500000));
|
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1500000));
|
||||||
assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1999999));
|
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1999999));
|
||||||
|
|
||||||
// Test after first and second subtitle
|
// Test after first and second subtitle
|
||||||
assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2000000));
|
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING,
|
||||||
assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2500000));
|
overlappingSubtitle.getCues(2000000));
|
||||||
assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2999999));
|
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING,
|
||||||
|
overlappingSubtitle.getCues(2500000));
|
||||||
|
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING,
|
||||||
|
overlappingSubtitle.getCues(2999999));
|
||||||
|
|
||||||
// Test second subtitle
|
// Test second subtitle
|
||||||
assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3000000));
|
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3000000));
|
||||||
assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3500000));
|
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3500000));
|
||||||
assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3999999));
|
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3999999));
|
||||||
|
|
||||||
// Test after second subtitle
|
// Test after second subtitle
|
||||||
assertNull(overlappingSubtitle.getText(4000000));
|
assertSingleCueEmpty(overlappingSubtitle.getCues(4000000));
|
||||||
assertNull(overlappingSubtitle.getText(4500000));
|
assertSingleCueEmpty(overlappingSubtitle.getCues(4500000));
|
||||||
assertNull(overlappingSubtitle.getText(Long.MAX_VALUE));
|
assertSingleCueEmpty(overlappingSubtitle.getCues(Long.MAX_VALUE));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testNestedSubtitleEventTimes() {
|
public void testNestedSubtitleEventTimes() {
|
||||||
@ -142,29 +168,29 @@ public class WebvttSubtitleTest extends TestCase {
|
|||||||
|
|
||||||
public void testNestedSubtitleText() {
|
public void testNestedSubtitleText() {
|
||||||
// Test before first subtitle
|
// Test before first subtitle
|
||||||
assertNull(nestedSubtitle.getText(0));
|
assertSingleCueEmpty(nestedSubtitle.getCues(0));
|
||||||
assertNull(nestedSubtitle.getText(500000));
|
assertSingleCueEmpty(nestedSubtitle.getCues(500000));
|
||||||
assertNull(nestedSubtitle.getText(999999));
|
assertSingleCueEmpty(nestedSubtitle.getCues(999999));
|
||||||
|
|
||||||
// Test first subtitle
|
// Test first subtitle
|
||||||
assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1000000));
|
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1000000));
|
||||||
assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1500000));
|
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1500000));
|
||||||
assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1999999));
|
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1999999));
|
||||||
|
|
||||||
// Test after first and second subtitle
|
// Test after first and second subtitle
|
||||||
assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2000000));
|
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2000000));
|
||||||
assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2500000));
|
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2500000));
|
||||||
assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2999999));
|
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2999999));
|
||||||
|
|
||||||
// Test first subtitle
|
// Test first subtitle
|
||||||
assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3000000));
|
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3000000));
|
||||||
assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3500000));
|
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3500000));
|
||||||
assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3999999));
|
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3999999));
|
||||||
|
|
||||||
// Test after second subtitle
|
// Test after second subtitle
|
||||||
assertNull(nestedSubtitle.getText(4000000));
|
assertSingleCueEmpty(nestedSubtitle.getCues(4000000));
|
||||||
assertNull(nestedSubtitle.getText(4500000));
|
assertSingleCueEmpty(nestedSubtitle.getCues(4500000));
|
||||||
assertNull(nestedSubtitle.getText(Long.MAX_VALUE));
|
assertSingleCueEmpty(nestedSubtitle.getCues(Long.MAX_VALUE));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testSubtitleEventTimesHelper(WebvttSubtitle subtitle) {
|
private void testSubtitleEventTimesHelper(WebvttSubtitle subtitle) {
|
||||||
@ -201,4 +227,13 @@ public class WebvttSubtitleTest extends TestCase {
|
|||||||
assertEquals(-1, subtitle.getNextEventTimeIndex(Long.MAX_VALUE));
|
assertEquals(-1, subtitle.getNextEventTimeIndex(Long.MAX_VALUE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertSingleCueEmpty(List<Cue> cues) {
|
||||||
|
assertTrue(cues.size() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSingleCueTextEquals(String expected, List<Cue> cues) {
|
||||||
|
assertTrue(cues.size() == 1);
|
||||||
|
assertEquals(expected, cues.get(0).text.toString());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user