diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index daa845298b6..9fc55d5c77c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -334,7 +334,7 @@ public int getPeriodCount() { 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 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 333dd25cbe7..eb3966ae4db 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -383,18 +383,24 @@ public static final class Period { */ 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; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index d893d602628..b26ae3a6acb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -158,6 +158,7 @@ public int getIndexOfPeriod(Object uid) { int periodIndexOffset = loopCount * childPeriodCount; return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset; } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index ae367ef14ca..447839392ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -99,7 +99,7 @@ public int getPeriodCount() { 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 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ClosedSource.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ClosedSource.java deleted file mode 100644 index ea70920d207..00000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ClosedSource.java +++ /dev/null @@ -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(); -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index e4810667203..8fa89dea28e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -45,6 +45,7 @@ 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 static int constrainValue(int value, int min, int max) { 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 static int inferContentType(String fileName) { } } + /** + * 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. diff --git a/library/core/src/main/proguard-rules.txt b/library/core/src/main/proguard-rules.txt deleted file mode 100644 index 75f2d095bed..00000000000 --- a/library/core/src/main/proguard-rules.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Accessed via reflection in SubtitleDecoderFactory.DEFAULT --keepclassmembers class com.google.android.exoplayer2.text.cea.Cea608Decoder { - public (java.lang.String, int); -} --keepclassmembers class com.google.android.exoplayer2.text.cea.Cea708Decoder { - public (int); -} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index eec99521f1b..10f88c6460b 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -648,7 +648,7 @@ public Period getPeriod(int periodIndex, Period period, boolean setIdentifiers) + 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 diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java new file mode 100644 index 00000000000..85e30d207d9 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -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); + } + +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 757252297de..a5c667635a5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -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.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 @@ *
  • Type: {@link TextView}
  • * * - *
  • {@code exo_progress} - Seek bar that's updated during playback and allows seeking. + *
  • {@code exo_progress} - Time bar that's updated during playback and allows seeking. *
      - *
    • Type: {@link SeekBar}
    • + *
    • Type: {@link TimeBar}
    • *
    *
  • * @@ -144,6 +146,8 @@ */ 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 boolean dispatchSeek(ExoPlayer player, int windowIndex, long positionMs) 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 boolean dispatchSeek(ExoPlayer player, int windowIndex, long positionMs) 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 PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr 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 PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr 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 ExoPlayer getPlayer() { /** * 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 void setPlayer(ExoPlayer player) { 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 @@ private void updateNavigation() { 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; - } - setButtonEnabled(enablePrevious , previousButton); + 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(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(); + + 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 (durationView != null) { - durationView.setText(stringForTime(duration)); + durationView.setText(Util.getStringForTime(formatBuilder, formatter, duration)); } - if (positionView != null && !dragging) { - positionView.setText(stringForTime(position)); + if (positionView != null && !scrubbing) { + positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); } - - if (progressBar != null) { - if (!dragging) { - progressBar.setProgress(progressBarValue(position)); - } - long bufferedPosition = player == null ? 0 : player.getBufferedPosition(); - progressBar.setSecondaryProgress(progressBarValue(bufferedPosition)); - // Remove scheduled updates. + if (timeBar != null) { + timeBar.setPosition(position); + timeBar.setBufferedPosition(bufferedPosition); + timeBar.setDuration(duration); } + + // 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 @@ private void setViewAlphaV11(View view, float alpha) { 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 boolean dispatchMediaKeyEvent(KeyEvent event) { 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 @@ private static boolean isHandledMediaKey(int keyCode) { || 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 void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { @Override public void onTimelineChanged(Timeline timeline, Object manifest) { updateNavigation(); + updateTimeBarMode(); updateProgress(); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 9d221be60a1..c2ad8ddeb53 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -527,6 +527,16 @@ public void setFastForwardIncrementMs(int fastForwardMs) { 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. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java new file mode 100644 index 00000000000..aeb8e0255ee --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java @@ -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. + *

    + * 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). + *

    + * 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); + + } + +} diff --git a/library/ui/src/main/res/layout/exo_playback_control_view.xml b/library/ui/src/main/res/layout/exo_playback_control_view.xml index f8ef5a6fdd7..665852936b8 100644 --- a/library/ui/src/main/res/layout/exo_playback_control_view.xml +++ b/library/ui/src/main/res/layout/exo_playback_control_view.xml @@ -65,12 +65,11 @@ android:includeFontPadding="false" android:textColor="#FFBEBEBE"/> - + android:layout_height="24dp"/> + + + + + + + + + + + +