Commit e4e02f91 by Oliver Woodman

Further improve WebVTT parser according to WebVTT spec

parent 71f542f7
WEBVTT
00:00.000 --> 00:01.234
This is the first subtitle.
00:02.345 --> 00:03.456
This is the second subtitle.
...@@ -8,7 +8,9 @@ with multiple lines ...@@ -8,7 +8,9 @@ with multiple lines
00:00.000 --> 00:01.234 00:00.000 --> 00:01.234
This is the first subtitle. This is the first subtitle.
NOTE Single line comment NOTE Single line comment with a space
NOTE Single line comment with a tab
2 2
00:02.345 --> 00:03.456 00:02.345 --> 00:03.456
......
...@@ -16,7 +16,13 @@ NOTE Line as percentage and line alignment ...@@ -16,7 +16,13 @@ NOTE Line as percentage and line alignment
00:04.000 --> 00:05.000 line:45%,end align:middle size:35% 00:04.000 --> 00:05.000 line:45%,end align:middle size:35%
This is the third subtitle. This is the third subtitle.
NOTE Line as absolute negative number and without line alignment NOTE Line as absolute negative number and without line alignment.
00:06.000 --> 00:07.000 line:-10 align:middle
This is the fourth subtitle.
NOTE The positioning alignment should be inherited from align.
00:07.000 --> 00:08.000 position:10% align:end size:10%
This is the fifth subtitle.
00:06.000 --> 00:07.000 line:-10 align:middle size:35%
This is the forth subtitle.
...@@ -23,30 +23,117 @@ import android.text.Layout.Alignment; ...@@ -23,30 +23,117 @@ import android.text.Layout.Alignment;
public class Cue { public class Cue {
/** /**
* Used by some methods to indicate that no value is set. * An unset position or width.
*/ */
public static final int UNSET_VALUE = -1; public static final float DIMEN_UNSET = Float.MIN_VALUE;
/**
* An unset anchor or line type value.
*/
public static final int TYPE_UNSET = Integer.MIN_VALUE;
/**
* Anchors the left (for horizontal positions) or top (for vertical positions) edge of the cue
* box.
*/
public static final int ANCHOR_TYPE_START = 0;
/**
* Anchors the middle of the cue box.
*/
public static final int ANCHOR_TYPE_MIDDLE = 1;
/**
* Anchors the right (for horizontal positions) or bottom (for vertical positions) edge of the cue
* box.
*/
public static final int ANCHOR_TYPE_END = 2;
/**
* Value for {@link #lineType} when {@link #line} is a fractional position.
*/
public static final int LINE_TYPE_FRACTION = 0;
/**
* Value for {@link #lineType} when {@link #line} is a line number.
*/
public static final int LINE_TYPE_NUMBER = 1;
/**
* The cue text. Note the {@link CharSequence} may be decorated with styling spans.
*/
public final CharSequence text; public final CharSequence text;
/**
public final int line; * The alignment of the cue text within the cue box.
public final int position; */
public final Alignment alignment; public final Alignment textAlignment;
public final int size; /**
* The position of the {@link #lineAnchor} of the cue box within the viewport in the direction
* orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of
* the value depends on the value of {@link #lineType}.
* <p>
* For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the
* fractional vertical position relative to the top of the viewport.
*/
public final float line;
/**
* The type of the {@link #line} value.
* <p>
* {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the
* viewport.
* <p>
* {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each
* line is taken to be the size of the first line of the cue. When {@link #line} is greater than
* or equal to 0, lines count from the start of the viewport (the first line is numbered 0). When
* {@link #line} is negative, lines count from the end of the viewport (the last line is numbered
* -1). For horizontal text the size of the first line of the cue is its height, and the start
* and end of the viewport are the top and bottom respectively.
*/
public final int lineType;
/**
* The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START},
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* <p>
* For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
* and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box
* respectively.
*/
public final int lineAnchor;
/**
* The fractional position of the {@link #positionAnchor} of the cue box within the viewport in
* the direction orthogonal to {@link #line}, or {@link #DIMEN_UNSET}.
* <p>
* For horizontal text, this is the horizontal position relative to the left of the viewport. Note
* that positioning is relative to the left of the viewport even in the case of right-to-left
* text.
*/
public final float position;
/**
* The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START},
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* <p>
* For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
* and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box
* respectively.
*/
public final int positionAnchor;
/**
* The size of the cue box in the writing direction specified as a fraction of the viewport size
* in that direction, or {@link #DIMEN_UNSET}.
*/
public final float size;
public Cue() { public Cue() {
this(null); this(null);
} }
public Cue(CharSequence text) { public Cue(CharSequence text) {
this(text, UNSET_VALUE, UNSET_VALUE, null, UNSET_VALUE); this(text, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET);
} }
public Cue(CharSequence text, int line, int position, Alignment alignment, int size) { public Cue(CharSequence text, Alignment textAlignment, float line, int lineType,
int lineAnchor, float position, int positionAnchor, float size) {
this.text = text; this.text = text;
this.textAlignment = textAlignment;
this.line = line; this.line = line;
this.lineType = lineType;
this.lineAnchor = lineAnchor;
this.position = position; this.position = position;
this.alignment = alignment; this.positionAnchor = positionAnchor;
this.size = size; this.size = size;
} }
......
...@@ -63,8 +63,13 @@ import android.util.Log; ...@@ -63,8 +63,13 @@ import android.util.Log;
// Previous input variables. // Previous input variables.
private CharSequence cueText; private CharSequence cueText;
private int cuePosition; private Alignment cueTextAlignment;
private Alignment cueAlignment; private float cueLine;
private int cueLineType;
private int cueLineAnchor;
private float cuePosition;
private int cuePositionAnchor;
private float cueSize;
private boolean applyEmbeddedStyles; private boolean applyEmbeddedStyles;
private int foregroundColor; private int foregroundColor;
private int backgroundColor; private int backgroundColor;
...@@ -120,7 +125,7 @@ import android.util.Log; ...@@ -120,7 +125,7 @@ import android.util.Log;
* @param style The style to use when drawing the cue text. * @param style The style to use when drawing the cue text.
* @param textSizePx The text size to use when drawing the cue text, in pixels. * @param textSizePx The text size to use when drawing the cue text, in pixels.
* @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is * @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is
* {@link Cue#UNSET_VALUE}, as a fraction of the viewport height * {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height
* @param canvas The canvas into which to draw. * @param canvas The canvas into which to draw.
* @param cueBoxLeft The left position of the enclosing cue box. * @param cueBoxLeft The left position of the enclosing cue box.
* @param cueBoxTop The top position of the enclosing cue box. * @param cueBoxTop The top position of the enclosing cue box.
...@@ -140,8 +145,13 @@ import android.util.Log; ...@@ -140,8 +145,13 @@ import android.util.Log;
cueText = cueText.toString(); cueText = cueText.toString();
} }
if (areCharSequencesEqual(this.cueText, cueText) if (areCharSequencesEqual(this.cueText, cueText)
&& Util.areEqual(this.cueTextAlignment, cue.textAlignment)
&& this.cueLine == cue.line
&& this.cueLineType == cue.lineType
&& Util.areEqual(this.cueLineAnchor, cue.lineAnchor)
&& this.cuePosition == cue.position && this.cuePosition == cue.position
&& Util.areEqual(this.cueAlignment, cue.alignment) && Util.areEqual(this.cuePositionAnchor, cue.positionAnchor)
&& this.cueSize == cue.size
&& this.applyEmbeddedStyles == applyEmbeddedStyles && this.applyEmbeddedStyles == applyEmbeddedStyles
&& this.foregroundColor == style.foregroundColor && this.foregroundColor == style.foregroundColor
&& this.backgroundColor == style.backgroundColor && this.backgroundColor == style.backgroundColor
...@@ -161,8 +171,13 @@ import android.util.Log; ...@@ -161,8 +171,13 @@ import android.util.Log;
} }
this.cueText = cueText; this.cueText = cueText;
this.cueTextAlignment = cue.textAlignment;
this.cueLine = cue.line;
this.cueLineType = cue.lineType;
this.cueLineAnchor = cue.lineAnchor;
this.cuePosition = cue.position; this.cuePosition = cue.position;
this.cueAlignment = cue.alignment; this.cuePositionAnchor = cue.positionAnchor;
this.cueSize = cue.size;
this.applyEmbeddedStyles = applyEmbeddedStyles; this.applyEmbeddedStyles = applyEmbeddedStyles;
this.foregroundColor = style.foregroundColor; this.foregroundColor = style.foregroundColor;
this.backgroundColor = style.backgroundColor; this.backgroundColor = style.backgroundColor;
...@@ -182,16 +197,19 @@ import android.util.Log; ...@@ -182,16 +197,19 @@ import android.util.Log;
textPaint.setTextSize(textSizePx); textPaint.setTextSize(textSizePx);
int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + 0.5f); int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + 0.5f);
int availableWidth = parentWidth - textPaddingX * 2; int availableWidth = parentWidth - textPaddingX * 2;
if (cueSize != Cue.DIMEN_UNSET) {
availableWidth = (int) (availableWidth * cueSize);
}
if (availableWidth <= 0) { if (availableWidth <= 0) {
Log.w(TAG, "Skipped drawing subtitle cue (insufficient space)"); Log.w(TAG, "Skipped drawing subtitle cue (insufficient space)");
return; return;
} }
Alignment layoutAlignment = cueAlignment == null ? Alignment.ALIGN_CENTER : cueAlignment; Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment;
textLayout = new StaticLayout(cueText, textPaint, availableWidth, layoutAlignment, spacingMult, textLayout = new StaticLayout(cueText, textPaint, availableWidth, textAlignment, spacingMult,
spacingAdd, true); spacingAdd, true);
int textHeight = textLayout.getHeight(); int textHeight = textLayout.getHeight();
int textWidth = 0; int textWidth = 0;
int lineCount = textLayout.getLineCount(); int lineCount = textLayout.getLineCount();
...@@ -202,14 +220,13 @@ import android.util.Log; ...@@ -202,14 +220,13 @@ import android.util.Log;
int textLeft; int textLeft;
int textRight; int textRight;
if (cue.position != Cue.UNSET_VALUE) { if (cuePosition != Cue.DIMEN_UNSET) {
if (cue.alignment == Alignment.ALIGN_OPPOSITE) { int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft;
textRight = (parentWidth * cue.position) / 100 + parentLeft; textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth
textLeft = Math.max(textRight - textWidth, parentLeft); : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2
} else { : anchorPosition;
textLeft = (parentWidth * cue.position) / 100 + parentLeft; textLeft = Math.max(textLeft, parentLeft);
textRight = Math.min(textLeft + textWidth, parentRight); textRight = Math.min(textLeft + textWidth, parentRight);
}
} else { } else {
textLeft = (parentWidth - textWidth) / 2; textLeft = (parentWidth - textWidth) / 2;
textRight = textLeft + textWidth; textRight = textLeft + textWidth;
...@@ -217,12 +234,29 @@ import android.util.Log; ...@@ -217,12 +234,29 @@ import android.util.Log;
int textTop; int textTop;
int textBottom; int textBottom;
if (cue.line != Cue.UNSET_VALUE) { if (cueLine != Cue.DIMEN_UNSET) {
textTop = (parentHeight * cue.line) / 100 + parentTop; int anchorPosition;
if (cueLineType == Cue.LINE_TYPE_FRACTION) {
anchorPosition = Math.round(parentHeight * cueLine) + parentTop;
} else {
// cueLineType == Cue.LINE_TYPE_NUMBER
int firstLineHeight = textLayout.getLineBottom(0) - textLayout.getLineTop(0);
if (cueLine >= 0) {
anchorPosition = Math.round(cueLine * firstLineHeight) + parentTop;
} else {
anchorPosition = Math.round(cueLine * firstLineHeight) + parentBottom;
}
}
textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight
: cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2
: anchorPosition;
textBottom = textTop + textHeight; textBottom = textTop + textHeight;
if (textBottom > parentBottom) { if (textBottom > parentBottom) {
textTop = parentBottom - textHeight; textTop = parentBottom - textHeight;
textBottom = parentBottom; textBottom = parentBottom;
} else if (textTop < parentTop) {
textTop = parentTop;
textBottom = parentTop + textHeight;
} }
} else { } else {
textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction); textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction);
...@@ -232,7 +266,7 @@ import android.util.Log; ...@@ -232,7 +266,7 @@ import android.util.Log;
textWidth = textRight - textLeft; textWidth = textRight - textLeft;
// Update the derived drawing variables. // Update the derived drawing variables.
this.textLayout = new StaticLayout(cueText, textPaint, textWidth, layoutAlignment, spacingMult, this.textLayout = new StaticLayout(cueText, textPaint, textWidth, textAlignment, spacingMult,
spacingAdd, true); spacingAdd, true);
this.textLeft = textLeft; this.textLeft = textLeft;
this.textTop = textTop; this.textTop = textTop;
......
...@@ -38,7 +38,7 @@ public final class SubtitleLayout extends View { ...@@ -38,7 +38,7 @@ public final class SubtitleLayout extends View {
public static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f; public static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f;
/** /**
* The default bottom padding to apply when {@link Cue#line} is {@link Cue#UNSET_VALUE}, as a * The default bottom padding to apply when {@link Cue#line} is {@link Cue#DIMEN_UNSET}, as a
* fraction of the viewport height. * fraction of the viewport height.
* *
* @see #setBottomPaddingFraction(float) * @see #setBottomPaddingFraction(float)
...@@ -174,7 +174,7 @@ public final class SubtitleLayout extends View { ...@@ -174,7 +174,7 @@ public final class SubtitleLayout extends View {
} }
/** /**
* Sets the bottom padding fraction to apply when {@link Cue#line} is {@link Cue#UNSET_VALUE}, * Sets the bottom padding fraction to apply when {@link Cue#line} is {@link Cue#DIMEN_UNSET},
* as a fraction of the view's remaining height after its top and bottom padding have been * as a fraction of the view's remaining height after its top and bottom padding have been
* subtracted. * subtracted.
* <p> * <p>
......
...@@ -28,16 +28,17 @@ import android.text.Layout.Alignment; ...@@ -28,16 +28,17 @@ import android.text.Layout.Alignment;
public final long endTime; public final long endTime;
public WebvttCue(CharSequence text) { public WebvttCue(CharSequence text) {
this(Cue.UNSET_VALUE, Cue.UNSET_VALUE, text); this(0, 0, text);
} }
public WebvttCue(long startTime, long endTime, CharSequence text) { public WebvttCue(long startTime, long endTime, CharSequence text) {
this(startTime, endTime, text, Cue.UNSET_VALUE, Cue.UNSET_VALUE, null, Cue.UNSET_VALUE); this(startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET,
Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
} }
public WebvttCue(long startTime, long endTime, CharSequence text, int line, int position, public WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment,
Alignment alignment, int size) { float line, int lineType, int lineAnchor, float position, int positionAnchor, float width) {
super(text, line, position, alignment, size); super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width);
this.startTime = startTime; this.startTime = startTime;
this.endTime = endTime; this.endTime = endTime;
} }
...@@ -49,7 +50,7 @@ import android.text.Layout.Alignment; ...@@ -49,7 +50,7 @@ import android.text.Layout.Alignment;
* @return True if this cue should be placed in the default position; false otherwise. * @return True if this cue should be placed in the default position; false otherwise.
*/ */
public boolean isNormalCue() { public boolean isNormalCue() {
return (line == UNSET_VALUE && position == UNSET_VALUE); return (line == DIMEN_UNSET && position == DIMEN_UNSET);
} }
} }
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