Add a custom time bar view.

Also add an isAd flag to Timeline.Period so that periods can be declared as
containing ads. The times of these periods are indicated using ad markers in
the new TimeBar.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=151116208
This commit is contained in:
andrewlewis 2017-03-24 05:23:02 -07:00 committed by Oliver Woodman
parent f4c33daf77
commit 9d20a8d41c
14 changed files with 951 additions and 140 deletions

View File

@ -334,7 +334,7 @@ public final class ExoPlayerTest extends TestCase {
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex];
Object id = setIds ? periodIndex : null;
return period.set(id, id, periodIndex, windowDefinition.durationUs, 0);
return period.set(id, id, periodIndex, windowDefinition.durationUs, 0, false);
}
@Override

View File

@ -383,18 +383,24 @@ public abstract class Timeline {
*/
public long durationUs;
/**
* Whether this period contains an ad.
*/
public boolean isAd;
private long positionInWindowUs;
/**
* Sets the data held by this period.
*/
public Period set(Object id, Object uid, int windowIndex, long durationUs,
long positionInWindowUs) {
long positionInWindowUs, boolean isAd) {
this.id = id;
this.uid = uid;
this.windowIndex = windowIndex;
this.durationUs = durationUs;
this.positionInWindowUs = positionInWindowUs;
this.isAd = isAd;
return this;
}

View File

@ -158,6 +158,7 @@ public final class LoopingMediaSource implements MediaSource {
int periodIndexOffset = loopCount * childPeriodCount;
return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset;
}
}
}

View File

@ -99,7 +99,7 @@ public final class SinglePeriodTimeline extends Timeline {
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
Assertions.checkIndex(periodIndex, 0, 1);
Object id = setIds ? ID : null;
return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs);
return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false);
}
@Override

View File

@ -1,31 +0,0 @@
/*
* Copyright (C) 2016 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.exoplayer2.util;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* An annotation for classes and interfaces that should not be open sourced.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@ClosedSource(reason = "Not required")
public @interface ClosedSource {
String reason();
}

View File

@ -45,6 +45,7 @@ import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Formatter;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
@ -309,6 +310,18 @@ public final class Util {
return Math.max(min, Math.min(value, max));
}
/**
* Constrains a value to the specified bounds.
*
* @param value The value to constrain.
* @param min The lower bound.
* @param max The upper bound.
* @return The constrained value {@code Math.max(min, Math.min(value, max))}.
*/
public static long constrainValue(long value, long min, long max) {
return Math.max(min, Math.min(value, max));
}
/**
* Constrains a value to the specified bounds.
*
@ -835,6 +848,27 @@ public final class Util {
}
}
/**
* Returns the specified millisecond time formatted as a string.
*
* @param builder The builder that {@code formatter} will write to.
* @param formatter The formatter.
* @param timeMs The time to format as a string, in milliseconds.
* @return The time formatted as a string.
*/
public static String getStringForTime(StringBuilder builder, Formatter formatter, long timeMs) {
if (timeMs == C.TIME_UNSET) {
timeMs = 0;
}
long totalSeconds = (timeMs + 500) / 1000;
long seconds = totalSeconds % 60;
long minutes = (totalSeconds / 60) % 60;
long hours = totalSeconds / 3600;
builder.setLength(0);
return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
: formatter.format("%02d:%02d", minutes, seconds).toString();
}
/**
* Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C}
* {@code DEFAULT_*_BUFFER_SIZE} constant.

View File

@ -1,7 +0,0 @@
# Accessed via reflection in SubtitleDecoderFactory.DEFAULT
-keepclassmembers class com.google.android.exoplayer2.text.cea.Cea608Decoder {
public <init>(java.lang.String, int);
}
-keepclassmembers class com.google.android.exoplayer2.text.cea.Cea708Decoder {
public <init>(int);
}

View File

@ -648,7 +648,7 @@ public final class DashMediaSource implements MediaSource {
+ Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null;
return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex),
C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs)
- offsetInFirstPeriodUs);
- offsetInFirstPeriodUs, false);
}
@Override

View File

@ -0,0 +1,574 @@
/*
* Copyright (C) 2017 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.exoplayer2.ui;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.Formatter;
import java.util.Locale;
/**
* A time bar that shows a current position, buffered position, duration and ad markers.
*/
public class DefaultTimeBar extends View implements TimeBar {
/**
* The threshold in dps above the bar at which touch events trigger fine scrub mode.
*/
private static final int FINE_SCRUB_Y_THRESHOLD = -50;
/**
* The ratio by which times are reduced in fine scrub mode.
*/
private static final int FINE_SCRUB_RATIO = 3;
/**
* The time after which the scrubbing listener is notified that scrubbing has stopped after
* performing an incremental scrub using key input.
*/
private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000;
private static final int DEFAULT_INCREMENT_COUNT = 20;
private static final int DEFAULT_BAR_HEIGHT = 4;
private static final int DEFAULT_TOUCH_TARGET_HEIGHT = 24;
private static final int DEFAULT_PLAYED_COLOR = 0x33FFFFFF;
private static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF;
private static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00;
private static final int DEFAULT_AD_MARKER_WIDTH = 4;
private static final int DEFAULT_SCRUBBER_ENABLED_SIZE = 12;
private static final int DEFAULT_SCRUBBER_DISABLED_SIZE = 0;
private static final int DEFAULT_SCRUBBER_DRAGGED_SIZE = 16;
private static final int OPAQUE_COLOR = 0xFF000000;
private final Rect seekBounds;
private final Rect progressBar;
private final Rect bufferedBar;
private final Rect scrubberBar;
private final Paint progressPaint;
private final Paint bufferedPaint;
private final Paint scrubberPaint;
private final Paint adMarkerPaint;
private final int barHeight;
private final int touchTargetHeight;
private final int adMarkerWidth;
private final int scrubberEnabledSize;
private final int scrubberDisabledSize;
private final int scrubberDraggedSize;
private final int scrubberPadding;
private final int fineScrubYThreshold;
private final StringBuilder formatBuilder;
private final Formatter formatter;
private final Runnable stopScrubbingRunnable;
private int scrubberSize;
private OnScrubListener listener;
private int keyCountIncrement;
private long keyTimeIncrement;
private int lastCoarseScrubXPosition;
private int[] locationOnScreen;
private Point touchPosition;
private boolean scrubbing;
private long scrubPosition;
private long duration;
private long position;
private long bufferedPosition;
private int adBreakCount;
private long[] adBreakTimesMs;
/**
* Creates a new time bar.
*/
public DefaultTimeBar(Context context, AttributeSet attrs) {
super(context, attrs);
seekBounds = new Rect();
progressBar = new Rect();
bufferedBar = new Rect();
scrubberBar = new Rect();
progressPaint = new Paint();
bufferedPaint = new Paint();
scrubberPaint = new Paint();
adMarkerPaint = new Paint();
// Calculate the dimensions and paints for drawn elements.
Resources res = context.getResources();
DisplayMetrics displayMetrics = res.getDisplayMetrics();
fineScrubYThreshold = dpToPx(displayMetrics, FINE_SCRUB_Y_THRESHOLD);
int defaultBarHeight = dpToPx(displayMetrics, DEFAULT_BAR_HEIGHT);
int defaultTouchTargetHeight = dpToPx(displayMetrics, DEFAULT_TOUCH_TARGET_HEIGHT);
int defaultAdMarkerWidth = dpToPx(displayMetrics, DEFAULT_AD_MARKER_WIDTH);
int defaultScrubberEnabledSize = dpToPx(displayMetrics, DEFAULT_SCRUBBER_ENABLED_SIZE);
int defaultScrubberDisabledSize = dpToPx(displayMetrics, DEFAULT_SCRUBBER_DISABLED_SIZE);
int defaultScrubberDraggedSize = dpToPx(displayMetrics, DEFAULT_SCRUBBER_DRAGGED_SIZE);
if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DefaultTimeBar, 0,
0);
try {
barHeight = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_bar_height,
defaultBarHeight);
touchTargetHeight = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_touch_target_height,
defaultTouchTargetHeight);
adMarkerWidth = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_ad_marker_width,
defaultAdMarkerWidth);
scrubberEnabledSize = a.getDimensionPixelSize(
R.styleable.DefaultTimeBar_scrubber_enabled_size, defaultScrubberEnabledSize);
scrubberDisabledSize = a.getDimensionPixelSize(
R.styleable.DefaultTimeBar_scrubber_disabled_size, defaultScrubberDisabledSize);
scrubberDraggedSize = a.getDimensionPixelSize(
R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize);
int playedColor = a.getInt(R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR);
int bufferedColor = a.getInt(R.styleable.DefaultTimeBar_buffered_color,
DEFAULT_BUFFERED_COLOR);
int adMarkerColor = a.getInt(R.styleable.DefaultTimeBar_ad_marker_color,
DEFAULT_AD_MARKER_COLOR);
progressPaint.setColor(playedColor);
scrubberPaint.setColor(OPAQUE_COLOR | playedColor);
bufferedPaint.setColor(bufferedColor);
adMarkerPaint.setColor(adMarkerColor);
} finally {
a.recycle();
}
} else {
barHeight = defaultBarHeight;
touchTargetHeight = defaultTouchTargetHeight;
adMarkerWidth = defaultAdMarkerWidth;
scrubberEnabledSize = defaultScrubberEnabledSize;
scrubberDisabledSize = defaultScrubberDisabledSize;
scrubberDraggedSize = defaultScrubberDraggedSize;
scrubberPaint.setColor(OPAQUE_COLOR | DEFAULT_PLAYED_COLOR);
progressPaint.setColor(DEFAULT_PLAYED_COLOR);
bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR);
adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR);
}
formatBuilder = new StringBuilder();
formatter = new Formatter(formatBuilder, Locale.getDefault());
stopScrubbingRunnable = new Runnable() {
@Override
public void run() {
stopScrubbing(false);
}
};
scrubberSize = scrubberEnabledSize;
scrubberPadding =
(Math.max(scrubberDisabledSize, Math.max(scrubberEnabledSize, scrubberDraggedSize)) + 1)
/ 2;
duration = C.TIME_UNSET;
keyTimeIncrement = C.TIME_UNSET;
keyCountIncrement = DEFAULT_INCREMENT_COUNT;
setFocusable(true);
if (Util.SDK_INT >= 16
&& getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
}
@Override
public void setListener(OnScrubListener listener) {
this.listener = listener;
}
@Override
public void setKeyTimeIncrement(long time) {
Assertions.checkArgument(time > 0);
keyCountIncrement = C.INDEX_UNSET;
keyTimeIncrement = time;
}
@Override
public void setKeyCountIncrement(int count) {
Assertions.checkArgument(count > 0);
keyCountIncrement = count;
keyTimeIncrement = C.TIME_UNSET;
}
@Override
public void setPosition(long position) {
this.position = position;
setContentDescription(getProgressText());
}
@Override
public void setBufferedPosition(long bufferedPosition) {
this.bufferedPosition = bufferedPosition;
}
@Override
public void setDuration(long duration) {
this.duration = duration;
if (scrubbing && duration == C.TIME_UNSET) {
stopScrubbing(true);
} else {
updateScrubberState();
}
}
@Override
public void setAdBreakTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount) {
Assertions.checkArgument(adBreakCount == 0 || adBreakTimesMs != null);
this.adBreakCount = adBreakCount;
this.adBreakTimesMs = adBreakTimesMs;
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
updateScrubberState();
if (scrubbing && !enabled) {
stopScrubbing(true);
}
}
@Override
public void onDraw(Canvas canvas) {
canvas.save();
drawTimeBar(canvas);
drawPlayhead(canvas);
canvas.restore();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled() || duration <= 0) {
return false;
}
Point touchPosition = resolveRelativeTouchPosition(event);
int x = touchPosition.x;
int y = touchPosition.y;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isInSeekBar(x, y)) {
startScrubbing();
positionScrubber(x);
scrubPosition = getScrubberPosition();
update();
invalidate();
return true;
}
break;
case MotionEvent.ACTION_MOVE:
if (scrubbing) {
if (y < fineScrubYThreshold) {
int relativeX = x - lastCoarseScrubXPosition;
positionScrubber(lastCoarseScrubXPosition + relativeX / FINE_SCRUB_RATIO);
} else {
lastCoarseScrubXPosition = x;
positionScrubber(x);
}
scrubPosition = getScrubberPosition();
if (listener != null) {
listener.onScrubMove(this, scrubPosition);
}
update();
invalidate();
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (scrubbing) {
stopScrubbing(event.getAction() == MotionEvent.ACTION_CANCEL);
return true;
}
break;
default:
// Do nothing.
}
return false;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (isEnabled()) {
long positionIncrement = getPositionIncrement();
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
positionIncrement = -positionIncrement;
// Fall through.
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (scrubIncrementally(positionIncrement)) {
removeCallbacks(stopScrubbingRunnable);
postDelayed(stopScrubbingRunnable, STOP_SCRUBBING_TIMEOUT_MS);
return true;
}
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
if (scrubbing) {
removeCallbacks(stopScrubbingRunnable);
stopScrubbingRunnable.run();
return true;
}
break;
default:
// Do nothing.
}
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(measureWidth, measureHeight);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int width = right - left;
int height = bottom - top;
int barY = height - touchTargetHeight;
int seekLeft = getPaddingLeft();
int seekRight = width - getPaddingRight();
int progressY = barY + (touchTargetHeight - barHeight) / 2;
seekBounds.set(seekLeft, barY, seekRight, barY + touchTargetHeight);
progressBar.set(seekBounds.left + scrubberPadding, progressY,
seekBounds.right - scrubberPadding, progressY + barHeight);
update();
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
}
@TargetApi(14)
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) {
event.getText().add(getProgressText());
}
event.setClassName(DefaultTimeBar.class.getName());
}
@TargetApi(14)
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(DefaultTimeBar.class.getCanonicalName());
info.setContentDescription(getProgressText());
if (duration <= 0) {
return;
}
if (Util.SDK_INT >= 21) {
info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD);
} else if (Util.SDK_INT >= 16) {
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
}
}
@TargetApi(16)
@Override
public boolean performAccessibilityAction(int action, Bundle args) {
if (super.performAccessibilityAction(action, args)) {
return true;
}
if (duration <= 0) {
return false;
}
if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
if (scrubIncrementally(-getPositionIncrement())) {
stopScrubbing(false);
}
} else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
if (scrubIncrementally(getPositionIncrement())) {
stopScrubbing(false);
}
} else {
return false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
return true;
}
// Internal methods.
private void startScrubbing() {
scrubbing = true;
updateScrubberState();
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
if (listener != null) {
listener.onScrubStart(this);
}
}
private void stopScrubbing(boolean canceled) {
scrubbing = false;
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(false);
}
updateScrubberState();
invalidate();
if (listener != null) {
listener.onScrubStop(this, getScrubberPosition(), canceled);
}
}
private void updateScrubberState() {
scrubberSize = scrubbing ? scrubberDraggedSize
: (isEnabled() && duration >= 0 ? scrubberEnabledSize : scrubberDisabledSize);
}
private void update() {
bufferedBar.set(progressBar);
scrubberBar.set(progressBar);
long newScrubberTime = scrubbing ? scrubPosition : position;
if (duration > 0) {
int bufferedPixelWidth =
(int) ((progressBar.width() * bufferedPosition) / duration);
bufferedBar.right = progressBar.left + bufferedPixelWidth;
int scrubberPixelPosition =
(int) ((progressBar.width() * newScrubberTime) / duration);
scrubberBar.right = progressBar.left + scrubberPixelPosition;
} else {
bufferedBar.right = progressBar.left;
scrubberBar.right = progressBar.left;
}
invalidate(seekBounds);
}
private void positionScrubber(float xPosition) {
scrubberBar.right = Util.constrainValue((int) xPosition, progressBar.left, progressBar.right);
}
private Point resolveRelativeTouchPosition(MotionEvent motionEvent) {
if (locationOnScreen == null) {
locationOnScreen = new int[2];
touchPosition = new Point();
}
getLocationOnScreen(locationOnScreen);
touchPosition.set(
((int) motionEvent.getRawX()) - locationOnScreen[0],
((int) motionEvent.getRawY()) - locationOnScreen[1]);
return touchPosition;
}
private long getScrubberPosition() {
if (progressBar.width() <= 0 || duration == C.TIME_UNSET) {
return 0;
}
return (scrubberBar.width() * duration) / progressBar.width();
}
private boolean isInSeekBar(float x, float y) {
return seekBounds.contains((int) x, (int) y);
}
private void drawTimeBar(Canvas canvas) {
int progressBarHeight = progressBar.height();
int barTop = progressBar.centerY() - progressBarHeight / 2;
int barBottom = barTop + progressBarHeight;
if (duration <= 0) {
canvas.drawRect(progressBar.left, barTop, progressBar.right, barBottom, progressPaint);
return;
}
int bufferedLeft = bufferedBar.left;
int bufferedRight = bufferedBar.right;
int progressLeft = Math.max(Math.max(progressBar.left, bufferedRight), scrubberBar.right);
if (progressLeft < progressBar.right) {
canvas.drawRect(progressLeft, barTop, progressBar.right, barBottom, progressPaint);
}
bufferedLeft = Math.max(bufferedLeft, scrubberBar.right);
if (bufferedRight > bufferedLeft) {
canvas.drawRect(bufferedLeft, barTop, bufferedRight, barBottom, bufferedPaint);
}
if (scrubberBar.width() > 0) {
canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, scrubberPaint);
}
int adMarkerOffset = adMarkerWidth / 2;
for (int i = 0; i < adBreakCount; i++) {
long adBreakTimeMs = Util.constrainValue(adBreakTimesMs[i], 0, duration);
int markerPositionOffset =
(int) (progressBar.width() * adBreakTimeMs / duration) - adMarkerOffset;
int markerLeft = progressBar.left + Math.min(progressBar.width() - adMarkerWidth,
Math.max(0, markerPositionOffset));
canvas.drawRect(markerLeft, barTop, markerLeft + adMarkerWidth, barBottom, adMarkerPaint);
}
}
private void drawPlayhead(Canvas canvas) {
if (duration <= 0) {
return;
}
int playheadRadius = scrubberSize / 2;
int playheadCenter = Util.constrainValue(scrubberBar.right, scrubberBar.left,
progressBar.right);
canvas.drawCircle(playheadCenter, scrubberBar.centerY(), playheadRadius, scrubberPaint);
}
private String getProgressText() {
return Util.getStringForTime(formatBuilder, formatter, position);
}
private long getPositionIncrement() {
return keyTimeIncrement == C.TIME_UNSET
? (duration == C.TIME_UNSET ? 0 : (duration / keyCountIncrement)) : keyTimeIncrement;
}
/**
* Incrementally scrubs the position by {@code positionChange}.
*
* @param positionChange The change in the scrubber position, in milliseconds. May be negative.
* @return Returns whether the scrubber position changed.
*/
private boolean scrubIncrementally(long positionChange) {
if (duration <= 0) {
return false;
}
long scrubberPosition = getScrubberPosition();
scrubPosition = Util.constrainValue(scrubberPosition + positionChange, 0, duration);
if (scrubPosition == scrubberPosition) {
return false;
}
if (!scrubbing) {
startScrubbing();
}
if (listener != null) {
listener.onScrubMove(this, scrubPosition);
}
update();
return true;
}
private static int dpToPx(DisplayMetrics displayMetrics, int dps) {
return (int) (dps * displayMetrics.density + 0.5f);
}
}

View File

@ -15,16 +15,17 @@
*/
package com.google.android.exoplayer2.ui;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
@ -34,6 +35,7 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.Formatter;
import java.util.Locale;
@ -125,9 +127,9 @@ import java.util.Locale;
* <li>Type: {@link TextView}</li>
* </ul>
* </li>
* <li><b>{@code exo_progress}</b> - Seek bar that's updated during playback and allows seeking.
* <li><b>{@code exo_progress}</b> - Time bar that's updated during playback and allows seeking.
* <ul>
* <li>Type: {@link SeekBar}</li>
* <li>Type: {@link TimeBar}</li>
* </ul>
* </li>
* </ul>
@ -144,6 +146,8 @@ import java.util.Locale;
*/
public class PlaybackControlView extends FrameLayout {
private static final String TAG = "PlaybackControlView";
/**
* Listener to be notified about changes of the visibility of the UI control.
*/
@ -191,7 +195,11 @@ public class PlaybackControlView extends FrameLayout {
public static final int DEFAULT_REWIND_MS = 5000;
public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000;
private static final int PROGRESS_BAR_MAX = 1000;
/**
* The maximum number of windows that can be shown in a multi-window time bar.
*/
public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100;
private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000;
private final ComponentListener componentListener;
@ -203,21 +211,25 @@ public class PlaybackControlView extends FrameLayout {
private final View rewindButton;
private final TextView durationView;
private final TextView positionView;
private final SeekBar progressBar;
private final TimeBar timeBar;
private final StringBuilder formatBuilder;
private final Formatter formatter;
private final Timeline.Window currentWindow;
private final Timeline.Period period;
private final Timeline.Window window;
private ExoPlayer player;
private SeekDispatcher seekDispatcher;
private VisibilityListener visibilityListener;
private boolean isAttachedToWindow;
private boolean dragging;
private boolean showMultiWindowTimeBar;
private boolean multiWindowTimeBar;
private boolean scrubbing;
private int rewindMs;
private int fastForwardMs;
private int showTimeoutMs;
private long hideAtMs;
private long[] adBreakTimesMs;
private final Runnable updateProgressAction = new Runnable() {
@Override
@ -262,9 +274,11 @@ public class PlaybackControlView extends FrameLayout {
a.recycle();
}
}
currentWindow = new Timeline.Window();
period = new Timeline.Period();
window = new Timeline.Window();
formatBuilder = new StringBuilder();
formatter = new Formatter(formatBuilder, Locale.getDefault());
adBreakTimesMs = new long[0];
componentListener = new ComponentListener();
seekDispatcher = DEFAULT_SEEK_DISPATCHER;
@ -273,10 +287,9 @@ public class PlaybackControlView extends FrameLayout {
durationView = (TextView) findViewById(R.id.exo_duration);
positionView = (TextView) findViewById(R.id.exo_position);
progressBar = (SeekBar) findViewById(R.id.exo_progress);
if (progressBar != null) {
progressBar.setOnSeekBarChangeListener(componentListener);
progressBar.setMax(PROGRESS_BAR_MAX);
timeBar = (TimeBar) findViewById(R.id.exo_progress);
if (timeBar != null) {
timeBar.setListener(componentListener);
}
playButton = findViewById(R.id.exo_play);
if (playButton != null) {
@ -314,7 +327,7 @@ public class PlaybackControlView extends FrameLayout {
/**
* Sets the {@link ExoPlayer} to control.
*
* @param player the {@code ExoPlayer} to control.
* @param player The {@code ExoPlayer} to control.
*/
public void setPlayer(ExoPlayer player) {
if (this.player == player) {
@ -330,6 +343,18 @@ public class PlaybackControlView extends FrameLayout {
updateAll();
}
/**
* Sets whether the time bar should show all windows, as opposed to just the current one. If the
* timeline has more than {@link #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will
* fall back to showing a single window.
*
* @param showMultiWindowTimeBar Whether the time bar should show all windows.
*/
public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
this.showMultiWindowTimeBar = showMultiWindowTimeBar;
updateTimeBarMode();
}
/**
* Sets the {@link VisibilityListener}.
*
@ -473,51 +498,122 @@ public class PlaybackControlView extends FrameLayout {
if (!isVisible() || !isAttachedToWindow) {
return;
}
Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null;
boolean haveNonEmptyTimeline = currentTimeline != null && !currentTimeline.isEmpty();
Timeline timeline = player != null ? player.getCurrentTimeline() : null;
boolean haveNonEmptyTimeline = timeline != null && !timeline.isEmpty();
boolean isSeekable = false;
boolean enablePrevious = false;
boolean enableNext = false;
if (haveNonEmptyTimeline) {
int currentWindowIndex = player.getCurrentWindowIndex();
currentTimeline.getWindow(currentWindowIndex, currentWindow);
isSeekable = currentWindow.isSeekable;
enablePrevious = currentWindowIndex > 0 || isSeekable || !currentWindow.isDynamic;
enableNext = (currentWindowIndex < currentTimeline.getWindowCount() - 1)
|| currentWindow.isDynamic;
int windowIndex = player.getCurrentWindowIndex();
timeline.getWindow(windowIndex, window);
isSeekable = window.isSeekable;
enablePrevious = windowIndex > 0 || isSeekable || !window.isDynamic;
enableNext = (windowIndex < timeline.getWindowCount() - 1) || window.isDynamic;
}
setButtonEnabled(enablePrevious , previousButton);
setButtonEnabled(enablePrevious, previousButton);
setButtonEnabled(enableNext, nextButton);
setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton);
setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton);
if (progressBar != null) {
progressBar.setEnabled(isSeekable);
if (timeBar != null) {
timeBar.setEnabled(isSeekable);
}
}
private void updateTimeBarMode() {
if (player == null) {
return;
}
if (showMultiWindowTimeBar) {
if (player.getCurrentTimeline().getWindowCount() <= MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) {
multiWindowTimeBar = true;
return;
}
Log.w(TAG, "Too many windows for multi-window time bar. Falling back to showing one window.");
}
multiWindowTimeBar = false;
}
private void updateProgress() {
if (!isVisible() || !isAttachedToWindow) {
return;
}
long duration = player == null ? 0 : player.getDuration();
long position = player == null ? 0 : player.getCurrentPosition();
if (durationView != null) {
durationView.setText(stringForTime(duration));
long position = 0;
long bufferedPosition = 0;
long duration = 0;
if (player != null) {
if (multiWindowTimeBar) {
Timeline timeline = player.getCurrentTimeline();
int windowCount = timeline.getWindowCount();
int periodIndex = player.getCurrentPeriodIndex();
long positionUs = 0;
long bufferedPositionUs = 0;
long durationUs = 0;
boolean isInAdBreak = false;
boolean isPlayingAd = false;
int adBreakCount = 0;
for (int i = 0; i < windowCount; i++) {
timeline.getWindow(i, window);
for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) {
if (timeline.getPeriod(j, period).isAd) {
isPlayingAd |= j == periodIndex;
if (!isInAdBreak) {
isInAdBreak = true;
if (adBreakCount == adBreakTimesMs.length) {
adBreakTimesMs = Arrays.copyOf(adBreakTimesMs,
adBreakTimesMs.length == 0 ? 1 : adBreakTimesMs.length * 2);
}
adBreakTimesMs[adBreakCount++] = C.usToMs(durationUs);
}
} else {
isInAdBreak = false;
long periodDurationUs = period.getDurationUs();
if (periodDurationUs == C.TIME_UNSET) {
durationUs = C.TIME_UNSET;
break;
}
long periodDurationInWindowUs = periodDurationUs;
if (j == window.firstPeriodIndex) {
periodDurationInWindowUs -= window.positionInFirstPeriodUs;
}
if (i < periodIndex) {
positionUs += periodDurationInWindowUs;
bufferedPositionUs += periodDurationInWindowUs;
}
durationUs += periodDurationInWindowUs;
}
}
}
position = C.usToMs(positionUs);
bufferedPosition = C.usToMs(bufferedPositionUs);
duration = C.usToMs(durationUs);
if (!isPlayingAd) {
position += player.getCurrentPosition();
bufferedPosition += player.getBufferedPosition();
}
if (timeBar != null) {
timeBar.setAdBreakTimesMs(adBreakTimesMs, adBreakCount);
}
} else {
position = player.getCurrentPosition();
bufferedPosition = player.getBufferedPosition();
duration = player.getDuration();
}
}
if (positionView != null && !dragging) {
positionView.setText(stringForTime(position));
if (durationView != null) {
durationView.setText(Util.getStringForTime(formatBuilder, formatter, duration));
}
if (positionView != null && !scrubbing) {
positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
}
if (timeBar != null) {
timeBar.setPosition(position);
timeBar.setBufferedPosition(bufferedPosition);
timeBar.setDuration(duration);
}
if (progressBar != null) {
if (!dragging) {
progressBar.setProgress(progressBarValue(position));
}
long bufferedPosition = player == null ? 0 : player.getBufferedPosition();
progressBar.setSecondaryProgress(progressBarValue(bufferedPosition));
// Remove scheduled updates.
}
// Cancel any pending updates and schedule a new one if necessary.
removeCallbacks(updateProgressAction);
// Schedule an update if necessary.
int playbackState = player == null ? ExoPlayer.STATE_IDLE : player.getPlaybackState();
if (playbackState != ExoPlayer.STATE_IDLE && playbackState != ExoPlayer.STATE_ENDED) {
long delayMs;
@ -560,55 +656,31 @@ public class PlaybackControlView extends FrameLayout {
view.setAlpha(alpha);
}
private String stringForTime(long timeMs) {
if (timeMs == C.TIME_UNSET) {
timeMs = 0;
}
long totalSeconds = (timeMs + 500) / 1000;
long seconds = totalSeconds % 60;
long minutes = (totalSeconds / 60) % 60;
long hours = totalSeconds / 3600;
formatBuilder.setLength(0);
return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
: formatter.format("%02d:%02d", minutes, seconds).toString();
}
private int progressBarValue(long position) {
long duration = player == null ? C.TIME_UNSET : player.getDuration();
return duration == C.TIME_UNSET || duration == 0 ? 0
: (int) ((position * PROGRESS_BAR_MAX) / duration);
}
private long positionValue(int progress) {
long duration = player == null ? C.TIME_UNSET : player.getDuration();
return duration == C.TIME_UNSET ? 0 : ((duration * progress) / PROGRESS_BAR_MAX);
}
private void previous() {
Timeline currentTimeline = player.getCurrentTimeline();
if (currentTimeline.isEmpty()) {
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
return;
}
int currentWindowIndex = player.getCurrentWindowIndex();
currentTimeline.getWindow(currentWindowIndex, currentWindow);
if (currentWindowIndex > 0 && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS
|| (currentWindow.isDynamic && !currentWindow.isSeekable))) {
seekTo(currentWindowIndex - 1, C.TIME_UNSET);
int windowIndex = player.getCurrentWindowIndex();
timeline.getWindow(windowIndex, window);
if (windowIndex > 0 && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS
|| (window.isDynamic && !window.isSeekable))) {
seekTo(windowIndex - 1, C.TIME_UNSET);
} else {
seekTo(0);
}
}
private void next() {
Timeline currentTimeline = player.getCurrentTimeline();
if (currentTimeline.isEmpty()) {
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
return;
}
int currentWindowIndex = player.getCurrentWindowIndex();
if (currentWindowIndex < currentTimeline.getWindowCount() - 1) {
seekTo(currentWindowIndex + 1, C.TIME_UNSET);
} else if (currentTimeline.getWindow(currentWindowIndex, currentWindow, false).isDynamic) {
seekTo(currentWindowIndex, C.TIME_UNSET);
int windowIndex = player.getCurrentWindowIndex();
if (windowIndex < timeline.getWindowCount() - 1) {
seekTo(windowIndex + 1, C.TIME_UNSET);
} else if (timeline.getWindow(windowIndex, window, false).isDynamic) {
seekTo(windowIndex, C.TIME_UNSET);
}
}
@ -714,6 +786,7 @@ public class PlaybackControlView extends FrameLayout {
return true;
}
@SuppressLint("InlinedApi")
private static boolean isHandledMediaKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|| keyCode == KeyEvent.KEYCODE_MEDIA_REWIND
@ -724,33 +797,52 @@ public class PlaybackControlView extends FrameLayout {
|| keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS;
}
private final class ComponentListener implements ExoPlayer.EventListener,
SeekBar.OnSeekBarChangeListener, OnClickListener {
private final class ComponentListener implements ExoPlayer.EventListener, TimeBar.OnScrubListener,
OnClickListener {
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
public void onScrubStart(TimeBar timeBar) {
removeCallbacks(hideAction);
dragging = true;
scrubbing = true;
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
long position = positionValue(progress);
if (positionView != null) {
positionView.setText(stringForTime(position));
}
if (player != null && !dragging) {
seekTo(position);
}
public void onScrubMove(TimeBar timeBar, long position) {
if (positionView != null) {
positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
}
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
dragging = false;
if (player != null) {
seekTo(positionValue(seekBar.getProgress()));
public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {
scrubbing = false;
if (!canceled && player != null) {
if (showMultiWindowTimeBar) {
Timeline timeline = player.getCurrentTimeline();
int windowCount = timeline.getWindowCount();
long remainingMs = position;
for (int i = 0; i < windowCount; i++) {
timeline.getWindow(i, window);
if (!timeline.getPeriod(window.firstPeriodIndex, period).isAd) {
long windowDurationMs = window.getDurationMs();
if (windowDurationMs == C.TIME_UNSET) {
break;
}
if (i == windowCount - 1 && remainingMs >= windowDurationMs) {
// Seeking past the end of the last window should seek to the end of the timeline.
seekTo(i, windowDurationMs);
break;
}
if (remainingMs < windowDurationMs) {
seekTo(i, remainingMs);
break;
}
remainingMs -= windowDurationMs;
}
}
} else {
seekTo(position);
}
}
hideAfterTimeout();
}
@ -775,6 +867,7 @@ public class PlaybackControlView extends FrameLayout {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
updateNavigation();
updateTimeBarMode();
updateProgress();
}

View File

@ -527,6 +527,16 @@ public final class SimpleExoPlayerView extends FrameLayout {
controller.setFastForwardIncrementMs(fastForwardMs);
}
/**
* Sets whether the time bar should show all windows, as opposed to just the current one.
*
* @param showMultiWindowTimeBar Whether to show all windows.
*/
public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
Assertions.checkState(controller != null);
controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar);
}
/**
* Gets the view onto which video is rendered. This is either a {@link SurfaceView} (default)
* or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true.

View File

@ -0,0 +1,120 @@
/*
* Copyright (C) 2017 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.exoplayer2.ui;
import android.support.annotation.Nullable;
import android.view.View;
/**
* Interface for time bar views that can display a playback position, buffered position, duration
* and ad markers, and that have a listener for scrubbing (seeking) events.
*/
public interface TimeBar {
/**
* @see View#isEnabled()
*/
void setEnabled(boolean enabled);
/**
* Sets the listener for the scrubbing events.
*
* @param listener The listener for scrubbing events.
*/
void setListener(OnScrubListener listener);
/**
* Sets the position increment for key presses and accessibility actions, in milliseconds.
* <p>
* Clears any increment specified in a preceding call to {@link #setKeyCountIncrement(int)}.
*
* @param time The time increment, in milliseconds.
*/
void setKeyTimeIncrement(long time);
/**
* Sets the position increment for key presses and accessibility actions, as a number of
* increments that divide the duration of the media. For example, passing 20 will cause key
* presses to increment/decrement the position by 1/20th of the duration (if known).
* <p>
* Clears any increment specified in a preceding call to {@link #setKeyTimeIncrement(long)}.
*
* @param count The number of increments that divide the duration of the media.
*/
void setKeyCountIncrement(int count);
/**
* Sets the current position.
*
* @param position The current position to show, in milliseconds.
*/
void setPosition(long position);
/**
* Sets the buffered position.
*
* @param bufferedPosition The current buffered position to show, in milliseconds.
*/
void setBufferedPosition(long bufferedPosition);
/**
* Sets the duration.
*
* @param duration The duration to show, in milliseconds.
*/
void setDuration(long duration);
/**
* Sets the times of ad breaks.
*
* @param adBreakTimesMs An array where the first {@code adBreakCount} elements are the times of
* ad breaks in milliseconds. May be {@code null} if there are no ad breaks.
* @param adBreakCount The number of ad breaks.
*/
void setAdBreakTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount);
/**
* Listener for scrubbing events.
*/
interface OnScrubListener {
/**
* Called when the user starts moving the scrubber.
*
* @param timeBar The time bar.
*/
void onScrubStart(TimeBar timeBar);
/**
* Called when the user moves the scrubber.
*
* @param timeBar The time bar.
* @param position The position of the scrubber, in milliseconds.
*/
void onScrubMove(TimeBar timeBar, long position);
/**
* Called when the user stops moving the scrubber.
*
* @param timeBar The time bar.
* @param position The position of the scrubber, in milliseconds.
* @param canceled Whether scrubbing was canceled.
*/
void onScrubStop(TimeBar timeBar, long position, boolean canceled);
}
}

View File

@ -65,12 +65,11 @@
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"/>
<SeekBar android:id="@id/exo_progress"
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="32dp"
android:focusable="false"
style="?android:attr/progressBarStyleHorizontal"/>
android:layout_height="24dp"/>
<TextView android:id="@id/exo_duration"
android:layout_width="wrap_content"

View File

@ -59,4 +59,16 @@
<attr name="controller_layout_id"/>
</declare-styleable>
<declare-styleable name="DefaultTimeBar">
<attr name="bar_height" format="dimension"/>
<attr name="touch_target_height" format="dimension"/>
<attr name="ad_marker_width" format="dimension"/>
<attr name="scrubber_enabled_size" format="dimension"/>
<attr name="scrubber_disabled_size" format="dimension"/>
<attr name="scrubber_dragged_size" format="dimension"/>
<attr name="played_color" format="color"/>
<attr name="buffered_color" format="color"/>
<attr name="ad_marker_color" format="color"/>
</declare-styleable>
</resources>