From 16af5b7da612166824ccfaad035cde03954b81b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6rkem=20G=C3=BCcl=C3=BC?= Date: Fri, 18 Nov 2022 10:59:37 +0100 Subject: [PATCH 1/6] DashManifest has been enhanced to allow fetching Thumbnail Meta Data for a video position and with that allowing to display thumbnail images while seeking through a video. The demo app has beed enhanced to use this new API and display thumbnail images above the seek bar while scrubbing. --- demos/main/src/main/assets/media.exolist.json | 17 ++++ .../exoplayer2/demo/PlayerActivity.java | 4 + .../thumbnail/ThumbnailDescription.java | 60 ++++++++++++ .../source/dash/manifest/DashManifest.java | 92 +++++++++++++++++++ .../dash/manifest/DashManifestParser.java | 4 +- 5 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/thumbnail/ThumbnailDescription.java diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index ac7b5ce749..ce1528c19d 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -631,6 +631,23 @@ } ] }, + { + "name": "Thumbnails", + "samples": [ + { + "name": "Single adaptation set, 7 tiles at 10x1, each thumb 320x180", + "uri": "https://dash.akamaized.net/akamai/bbb_30fps/bbb_with_tiled_thumbnails.mpd" + }, + { + "name": "Single adaptation set, SegmentTemplate with SegmentTimeline", + "uri": "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-tiled-thumbnails-timeline.ism/.mpd" + }, + { + "name": "Single adaptation set, SegmentTemplate with SegmentNumber", + "uri": "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-tiled-thumbnails-numbered.ism/.mpd" + } + ] + }, { "name": "Misc", "samples": [ diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 8932b0780d..d7b381baf0 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -75,6 +75,7 @@ public class PlayerActivity extends AppCompatActivity protected LinearLayout debugRootView; protected TextView debugTextView; protected @Nullable ExoPlayer player; + DefaultThumbnailTimeBar timeBar; private boolean isShowingTrackSelectionDialog; private Button selectTracksButton; @@ -117,6 +118,8 @@ public class PlayerActivity extends AppCompatActivity playerView.setErrorMessageProvider(new PlayerErrorMessageProvider()); playerView.requestFocus(); + timeBar = playerView.findViewById(R.id.exo_progress); + if (savedInstanceState != null) { trackSelectionParameters = TrackSelectionParameters.fromBundle( @@ -282,6 +285,7 @@ public class PlayerActivity extends AppCompatActivity player.setPlayWhenReady(startAutoPlay); playerView.setPlayer(player); configurePlayerWithServerSideAdsLoader(); + timeBar.setThumbnailUtils(new DefaultThumbnailProvider(player, timeBar)); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/thumbnail/ThumbnailDescription.java b/library/core/src/main/java/com/google/android/exoplayer2/thumbnail/ThumbnailDescription.java new file mode 100644 index 0000000000..8984689404 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/thumbnail/ThumbnailDescription.java @@ -0,0 +1,60 @@ +package com.google.android.exoplayer2.thumbnail; + +import android.net.Uri; + +public class ThumbnailDescription { + + private final String id; + private final Uri uri; + private final int bitrate; + private final int rows; + private final int columns; + private final long startTimeMs; + private final long durationMs; + private final int imageWidth; // Image width (Pixel) + private final int imageHeight; // Image height (Pixel) + + public ThumbnailDescription(String id, Uri uri, int bitrate, int rows, int columns, long startTimeMs, long durationMs, int imageWidth, int imageHeight) { + this.id = id; + this.uri = uri; + this.bitrate = bitrate; + this.rows = rows; + this.columns = columns; + this.startTimeMs = startTimeMs; + this.durationMs = durationMs; + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + } + + public Uri getUri() { + return uri; + } + + public int getBitrate() { + return bitrate; + } + + public int getRows() { + return rows; + } + + public int getColumns() { + return columns; + } + + public long getStartTimeMs() { + return startTimeMs; + } + + public long getDurationMs() { + return durationMs; + } + + public int getImageWidth() { + return imageWidth; + } + + public int getImageHeight() { + return imageHeight; + } +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 6bdbb0d6b0..9123ea559c 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -15,12 +15,20 @@ */ package com.google.android.exoplayer2.source.dash.manifest; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.FilterableManifest; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.source.dash.BaseUrlExclusionList; +import com.google.android.exoplayer2.source.dash.DashSegmentIndex; +import com.google.android.exoplayer2.source.dash.DashUtil; +import com.google.android.exoplayer2.thumbnail.ThumbnailDescription; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Util; +import com.google.common.base.Ascii; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; @@ -136,6 +144,90 @@ public class DashManifest implements FilterableManifest { return Util.msToUs(getPeriodDurationMs(index)); } + /** + * Returns a List of ThumbnailDescription for a given periodPosition, + * or null if no AdaptionSet of type C.TRACK_TYPE_IMAGE is available. + * @param periodPositionMs the period position to get ThumbnailDescription for, e.g. current player position. + * @return List of ThumbnailDescription from all Representations, or null if Thumbnails are not available in the DashManifest. + */ + @Nullable + public List getThumbnailDescriptions(long periodPositionMs) { + ArrayList thumbnailDescriptions = new ArrayList<>(); + + long periodPositionUs = Util.msToUs(periodPositionMs); + BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList(); + + boolean isTrackTypeImageAvailable = false; + for (int i = 0; i < getPeriodCount(); i++) { + Period period = getPeriod(i); + long periodStartUs = Util.msToUs(period.startMs); + long periodDurationUs = getPeriodDurationUs(i); + + List adaptationSets = period.adaptationSets; + for (int j = 0; j < adaptationSets.size(); j++) { + AdaptationSet adaptationSet = adaptationSets.get(j); + if (adaptationSet.type != C.TRACK_TYPE_IMAGE) { + continue; + } + isTrackTypeImageAvailable = true; + + // thumbnails found + List representations = adaptationSet.representations; + for (int k = 0; k < representations.size(); k++) { + + Representation representation = representations.get(k); + DashSegmentIndex index = representation.getIndex(); + if (index == null) { + continue; + } + + String id = representation.format.id; + int bitrate = representation.format.bitrate; + int imageWidth = representation.format.width; + int imageHeight = representation.format.height; + + // get size XxY, e.g. 10x20, where 10 is column count and 20 is row count + int rows = 1; + int cols = 1; + for (int m = 0; m < representation.essentialProperties.size(); m++) { + Descriptor descriptor = representation.essentialProperties.get(m); + if ((Ascii.equalsIgnoreCase("http://dashif.org/thumbnail_tile", descriptor.schemeIdUri) || Ascii.equalsIgnoreCase("http://dashif.org/guidelines/thumbnail_tile", descriptor.schemeIdUri)) && descriptor.value != null) { + String size = descriptor.value; + String[] sizeSplit = size.split("x"); + if (sizeSplit.length != 2) { + continue; + } + cols = Integer.parseInt(sizeSplit[0]); + rows = Integer.parseInt(sizeSplit[1]); + } + } + + long now = Util.getNowUnixTimeMs(C.TIME_UNSET); + String baseUrl = castNonNull(baseUrlExclusionList.selectBaseUrl(representation.baseUrls)).url; + + // calculate the correct positionUs, which is FirstAvailableSegment.time + playerPosition, use that to get the correct segment + long firstSegmentNum = index.getFirstAvailableSegmentNum(periodDurationUs, Util.msToUs(now)); + long firstStartTimeUs = index.getTimeUs(firstSegmentNum); + long positionUs = firstStartTimeUs + periodPositionUs; + long segmentNumber = index.getSegmentNum(positionUs, periodDurationUs); + + long segmentStartTimeUs = periodStartUs + index.getTimeUs(segmentNumber); + long segmentDurationUs = index.getDurationUs(segmentNumber, periodDurationUs); + + RangedUri rangedUri = index.getSegmentUrl(segmentNumber); + DataSpec dataSpec = DashUtil.buildDataSpec(representation, baseUrl, rangedUri, /* flags= */ 0); + Uri uri = dataSpec.uri; + ThumbnailDescription thumbnailDescription = new ThumbnailDescription(id, uri, bitrate, rows, cols, Util.usToMs(segmentStartTimeUs - (dynamic ? firstStartTimeUs : 0)), Util.usToMs(segmentDurationUs), imageWidth, imageHeight); + thumbnailDescriptions.add(thumbnailDescription); + } + } + } + if (isTrackTypeImageAvailable) { + return thumbnailDescriptions; + } + return null; + } + @Override public final DashManifest copy(List streamKeys) { LinkedList keys = new LinkedList<>(streamKeys); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 997e0e2ff7..919b1e253f 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -555,7 +555,9 @@ public class DashManifestParser extends DefaultHandler ? C.TRACK_TYPE_VIDEO : MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? C.TRACK_TYPE_TEXT - : C.TRACK_TYPE_UNKNOWN; + : MimeTypes.BASE_TYPE_IMAGE.equals(contentType) + ? C.TRACK_TYPE_IMAGE + : C.TRACK_TYPE_UNKNOWN; } /** From 158cf0c8ed00d99244ded47acba5112c3b3fc96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6rkem=20G=C3=BCcl=C3=BC?= Date: Fri, 18 Nov 2022 12:49:34 +0100 Subject: [PATCH 2/6] Added missing files --- .../demo/DefaultThumbnailProvider.java | 194 ++++ .../demo/DefaultThumbnailTimeBar.java | 1026 +++++++++++++++++ .../exoplayer2/demo/ThumbnailProvider.java | 9 + .../layout/exo_styled_player_control_view.xml | 152 +++ 4 files changed, 1381 insertions(+) create mode 100644 demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java create mode 100644 demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailTimeBar.java create mode 100644 demos/main/src/main/java/com/google/android/exoplayer2/demo/ThumbnailProvider.java create mode 100644 demos/main/src/main/res/layout/exo_styled_player_control_view.xml diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java new file mode 100644 index 0000000000..c06144a1b8 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java @@ -0,0 +1,194 @@ +package com.google.android.exoplayer2.demo; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.LruCache; +import android.view.View; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.thumbnail.ThumbnailDescription; +import com.google.android.exoplayer2.util.Log; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; + +public class DefaultThumbnailProvider implements ThumbnailProvider { + + private static final String TAG_DEBUG = DefaultThumbnailProvider.class.getSimpleName(); + + private LruCache bitmapCache; + private View parent; + + //dummy bitmap to indicate that a download is already triggered but not finished yet + private final Bitmap dummyBitmap = Bitmap.createBitmap(1,1,Bitmap.Config.ARGB_8888); + @Nullable ExoPlayer exoPlayer; + + public DefaultThumbnailProvider(ExoPlayer exoPlayer, View view) { + this.exoPlayer = exoPlayer; + + this.parent = view; + + final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + final int cacheSize = maxMemory / 4; + bitmapCache = new LruCache(cacheSize) { + @Override + protected int sizeOf(String key, Bitmap bitmap) { + return bitmap.getByteCount() / 1024; + } + }; + } + + public Bitmap getThumbnail(long position) { + return getThumbnail(position, true); + } + + private Bitmap getThumbnail(long position, boolean retrigger) { + if (exoPlayer != null) { + Object manifest = exoPlayer.getCurrentManifest(); + + ThumbnailDescription thumbnailDescription = null; + if (manifest instanceof DashManifest) { + DashManifest dashManifest = (DashManifest) manifest; + List thumbnailDescs = dashManifest.getThumbnailDescriptions(position); + //selected thumbnail description with lowest bitrate + for (ThumbnailDescription desc : thumbnailDescs) { + if (thumbnailDescription == null || thumbnailDescription.getBitrate() > desc.getBitrate()) { + thumbnailDescription = desc; + } + } + if (bitmapNotAvailableOrDownloadNotTriggeredYet(thumbnailDescription.getUri())) { + this.initThumbnailSource(thumbnailDescription); + return null; + } + } + + if (retrigger) { + //also download next and prev thumbnails to have a nicer UI user experience + getThumbnail(thumbnailDescription.getStartTimeMs() + thumbnailDescription.getDurationMs(), false); + getThumbnail(thumbnailDescription.getStartTimeMs() - thumbnailDescription.getDurationMs(), false); + } + + return getThumbnailInternal(position, thumbnailDescription); + } + return null; + } + + private boolean bitmapNotAvailableOrDownloadNotTriggeredYet(Uri uri) { + Bitmap tmp = bitmapCache.get(uri.toString()); + if (tmp != null) return false; + return true; + } + + private Bitmap getThumbnailInternal(long position, ThumbnailDescription thumbnailDescription) { + if (thumbnailDescription == null) return null; + + Bitmap thumbnailSource = bitmapCache.get(thumbnailDescription.getUri().toString()); + + if (thumbnailSource == null || thumbnailSource.getWidth() == 1) return null; + + if (position < thumbnailDescription.getStartTimeMs() || position > thumbnailDescription.getStartTimeMs() + thumbnailDescription.getDurationMs()) return null; + + int count = thumbnailDescription.getColumns() * thumbnailDescription.getRows(); + + int durationPerImage = (int)(thumbnailDescription.getDurationMs() / count); + + int imageNumberToUseWithinTile = (int)((position - thumbnailDescription.getStartTimeMs()) / durationPerImage); + + //handle special case if position == duration + if (imageNumberToUseWithinTile > count-1) imageNumberToUseWithinTile = count-1; + + int intRowToUse = (int)(imageNumberToUseWithinTile / thumbnailDescription.getColumns()); + + int intColToUse = imageNumberToUseWithinTile - intRowToUse * thumbnailDescription.getColumns(); + + double thumbnailWidth = (double) thumbnailDescription.getImageWidth() / thumbnailDescription.getColumns(); + double thumbnailHeight = (double) thumbnailDescription.getImageHeight() / thumbnailDescription.getRows(); + + int cropXLeft = (int)Math.round(intColToUse * thumbnailWidth); + int cropYTop = (int)Math.round(intRowToUse * thumbnailHeight); + + if (cropXLeft + thumbnailWidth <= thumbnailSource.getWidth() && cropYTop + thumbnailHeight <= thumbnailSource.getHeight()) { + return Bitmap.createBitmap(thumbnailSource + , cropXLeft, cropYTop, (int) thumbnailWidth, (int) thumbnailHeight); + } + else { + Log.d(TAG_DEBUG, "Image does not have expected (" + thumbnailDescription.getImageWidth() + "x" + thumbnailDescription.getImageHeight() + ") dimensions to crop. Source " + thumbnailDescription.getUri()); + return null; + } + } + + private synchronized void initThumbnailSource(ThumbnailDescription thumbnailDescription){ + String path = thumbnailDescription.getUri().toString(); + if (path == null) return; + + if (bitmapCache.get(path) != null) return; + bitmapCache.put(path, dummyBitmap); + + RetrieveThumbnailImageTask currentTask = new RetrieveThumbnailImageTask(); + currentTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, path); + } + + class RetrieveThumbnailImageTask extends AsyncTask { + + String downloadedUrl; + + RetrieveThumbnailImageTask() { + + } + + @Override + protected void onCancelled() { + super.onCancelled(); + if (downloadedUrl != null) bitmapCache.remove(downloadedUrl); + } + + protected Bitmap doInBackground(String... urls) { + downloadedUrl = urls[0]; + InputStream in =null; + Bitmap thumbnailToDownload=null; + int responseCode = -1; + + try{ + URL url = new URL(downloadedUrl); + if (!isCancelled()) { + HttpURLConnection httpURLConnection = (HttpURLConnection)url.openConnection(); + httpURLConnection.setDoInput(true); + httpURLConnection.connect(); + responseCode = httpURLConnection.getResponseCode(); + if(responseCode == HttpURLConnection.HTTP_OK) + { + if (!isCancelled()) { + in = httpURLConnection.getInputStream(); + if (!isCancelled()) { + thumbnailToDownload = BitmapFactory.decodeStream(in); + } + in.close(); + } + } + } + + } + catch(Exception ex){ + bitmapCache.remove(downloadedUrl); + System.out.println(ex); + } + + return thumbnailToDownload; + } + + protected void onPostExecute(Bitmap downloadedThumbnail) { + if (downloadedThumbnail != null) { + bitmapCache.put(downloadedUrl, downloadedThumbnail); + if (parent != null) parent.invalidate(); + } + else { + bitmapCache.remove(downloadedUrl); + } + } + } + +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailTimeBar.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailTimeBar.java new file mode 100644 index 0000000000..a3e75923e5 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailTimeBar.java @@ -0,0 +1,1026 @@ +/* + * 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.demo; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +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 androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ui.TimeBar; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.Collections; +import java.util.Formatter; +import java.util.Locale; +import java.util.concurrent.CopyOnWriteArraySet; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * A time bar that shows a current position, buffered position, duration and ad markers. + * + *

A DefaultTimeBar can be customized by setting attributes, as outlined below. + * + *

Attributes

+ * + * The following attributes can be set on a DefaultTimeBar when used in a layout XML file: + * + *
    + *
  • {@code bar_height} - Dimension for the height of the time bar. + *
      + *
    • Default: {@link #DEFAULT_BAR_HEIGHT_DP} + *
    + *
  • {@code touch_target_height} - Dimension for the height of the area in which touch + * interactions with the time bar are handled. If no height is specified, this also determines + * the height of the view. + *
      + *
    • Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP} + *
    + *
  • {@code ad_marker_width} - Dimension for the width of any ad markers shown on the + * bar. Ad markers are superimposed on the time bar to show the times at which ads will play. + *
      + *
    • Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP} + *
    + *
  • {@code scrubber_enabled_size} - Dimension for the diameter of the circular scrubber + * handle when scrubbing is enabled but not in progress. Set to zero if no scrubber handle + * should be shown. + *
      + *
    • Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP} + *
    + *
  • {@code scrubber_disabled_size} - Dimension for the diameter of the circular scrubber + * handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown. + *
      + *
    • Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP} + *
    + *
  • {@code scrubber_dragged_size} - Dimension for the diameter of the circular scrubber + * handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown. + *
      + *
    • Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP} + *
    + *
  • {@code scrubber_drawable} - Optional reference to a drawable to draw for the + * scrubber handle. If set, this overrides the default behavior, which is to draw a circle for + * the scrubber handle. + *
  • {@code played_color} - Color for the portion of the time bar representing media + * before the current playback position. + *
      + *
    • Corresponding method: {@link #setPlayedColor(int)} + *
    • Default: {@link #DEFAULT_PLAYED_COLOR} + *
    + *
  • {@code scrubber_color} - Color for the scrubber handle. + *
      + *
    • Corresponding method: {@link #setScrubberColor(int)} + *
    • Default: {@link #DEFAULT_SCRUBBER_COLOR} + *
    + *
  • {@code buffered_color} - Color for the portion of the time bar after the current + * played position up to the current buffered position. + *
      + *
    • Corresponding method: {@link #setBufferedColor(int)} + *
    • Default: {@link #DEFAULT_BUFFERED_COLOR} + *
    + *
  • {@code unplayed_color} - Color for the portion of the time bar after the current + * buffered position. + *
      + *
    • Corresponding method: {@link #setUnplayedColor(int)} + *
    • Default: {@link #DEFAULT_UNPLAYED_COLOR} + *
    + *
  • {@code ad_marker_color} - Color for unplayed ad markers. + *
      + *
    • Corresponding method: {@link #setAdMarkerColor(int)} + *
    • Default: {@link #DEFAULT_AD_MARKER_COLOR} + *
    + *
  • {@code played_ad_marker_color} - Color for played ad markers. + *
      + *
    • Corresponding method: {@link #setPlayedAdMarkerColor(int)} + *
    • Default: {@link #DEFAULT_PLAYED_AD_MARKER_COLOR} + *
    + *
+ */ +public class DefaultThumbnailTimeBar extends View implements TimeBar { + + /** Default height for the time bar, in dp. */ + public static final int DEFAULT_BAR_HEIGHT_DP = 4; + /** Default height for the touch target, in dp. */ + public static final int DEFAULT_TOUCH_TARGET_HEIGHT_DP = 26; + /** Default width for ad markers, in dp. */ + public static final int DEFAULT_AD_MARKER_WIDTH_DP = 4; + /** Default diameter for the scrubber when enabled, in dp. */ + public static final int DEFAULT_SCRUBBER_ENABLED_SIZE_DP = 12; + /** Default diameter for the scrubber when disabled, in dp. */ + public static final int DEFAULT_SCRUBBER_DISABLED_SIZE_DP = 0; + /** Default diameter for the scrubber when dragged, in dp. */ + public static final int DEFAULT_SCRUBBER_DRAGGED_SIZE_DP = 16; + /** Default color for the played portion of the time bar. */ + public static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF; + /** Default color for the unplayed portion of the time bar. */ + public static final int DEFAULT_UNPLAYED_COLOR = 0x33FFFFFF; + /** Default color for the buffered portion of the time bar. */ + public static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF; + /** Default color for the scrubber handle. */ + public static final int DEFAULT_SCRUBBER_COLOR = 0xFFFFFFFF; + /** Default color for ad markers. */ + public static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00; + /** Default color for played ad markers. */ + public static final int DEFAULT_PLAYED_AD_MARKER_COLOR = 0x33FFFF00; + + /** Vertical gravity for progress bar to be located at the center in the view. */ + public static final int BAR_GRAVITY_CENTER = 0; + /** Vertical gravity for progress bar to be located at the bottom in the view. */ + public static final int BAR_GRAVITY_BOTTOM = 1; + + /** The threshold in dps above the bar at which touch events trigger fine scrub mode. */ + private static final int FINE_SCRUB_Y_THRESHOLD_DP = -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 float SHOWN_SCRUBBER_SCALE = 1.0f; + private static final float HIDDEN_SCRUBBER_SCALE = 0.0f; + + /** + * The name of the Android SDK view that most closely resembles this custom view. Used as the + * class name for accessibility. + */ + private static final String ACCESSIBILITY_CLASS_NAME = "android.widget.SeekBar"; + + private final Rect seekBounds; + private final Rect progressBar; + private final Rect bufferedBar; + private final Rect scrubberBar; + private final Paint playedPaint; + private final Paint bufferedPaint; + private final Paint unplayedPaint; + private final Paint adMarkerPaint; + private final Paint playedAdMarkerPaint; + private final Paint scrubberPaint; + @Nullable private final Drawable scrubberDrawable; + private final int barHeight; + private final int touchTargetHeight; + private final int barGravity; + 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 final CopyOnWriteArraySet listeners; + private final Point touchPosition; + private final float density; + + private int keyCountIncrement; + private long keyTimeIncrement; + private int lastCoarseScrubXPosition; + private @MonotonicNonNull Rect lastExclusionRectangle; + + private ValueAnimator scrubberScalingAnimator; + private float scrubberScale; + private boolean scrubberPaddingDisabled; + private boolean scrubbing; + private long scrubPosition; + private long duration; + private long position; + private long bufferedPosition; + private int adGroupCount; + @Nullable private long[] adGroupTimesMs; + @Nullable private boolean[] playedAdGroups; + + private ThumbnailProvider thumbnailUtils; + //TODO put in ressource file + int targetThumbnailHeightInDp = 80; + + public DefaultThumbnailTimeBar(Context context) { + this(context, null); + } + + public DefaultThumbnailTimeBar(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public DefaultThumbnailTimeBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, attrs); + } + + public DefaultThumbnailTimeBar( + Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + @Nullable AttributeSet timebarAttrs) { + this(context, attrs, defStyleAttr, timebarAttrs, 0); + } + + // Suppress warnings due to usage of View methods in the constructor. + @SuppressWarnings("nullness:method.invocation") + public DefaultThumbnailTimeBar( + Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + @Nullable AttributeSet timebarAttrs, + int defStyleRes) { + super(context, attrs, defStyleAttr); + seekBounds = new Rect(); + progressBar = new Rect(); + bufferedBar = new Rect(); + scrubberBar = new Rect(); + playedPaint = new Paint(); + bufferedPaint = new Paint(); + unplayedPaint = new Paint(); + adMarkerPaint = new Paint(); + playedAdMarkerPaint = new Paint(); + scrubberPaint = new Paint(); + scrubberPaint.setAntiAlias(true); + listeners = new CopyOnWriteArraySet<>(); + touchPosition = new Point(); + + // Calculate the dimensions and paints for drawn elements. + Resources res = context.getResources(); + DisplayMetrics displayMetrics = res.getDisplayMetrics(); + density = displayMetrics.density; + fineScrubYThreshold = dpToPx(density, FINE_SCRUB_Y_THRESHOLD_DP); + int defaultBarHeight = dpToPx(density, DEFAULT_BAR_HEIGHT_DP); + int defaultTouchTargetHeight = dpToPx(density, DEFAULT_TOUCH_TARGET_HEIGHT_DP); + int defaultAdMarkerWidth = dpToPx(density, DEFAULT_AD_MARKER_WIDTH_DP); + int defaultScrubberEnabledSize = dpToPx(density, DEFAULT_SCRUBBER_ENABLED_SIZE_DP); + int defaultScrubberDisabledSize = dpToPx(density, DEFAULT_SCRUBBER_DISABLED_SIZE_DP); + int defaultScrubberDraggedSize = dpToPx(density, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP); + if (timebarAttrs != null) { + TypedArray a = + context + .getTheme() + .obtainStyledAttributes( + timebarAttrs, com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar, defStyleAttr, defStyleRes); + try { + scrubberDrawable = a.getDrawable(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_drawable); + if (scrubberDrawable != null) { + setDrawableLayoutDirection(scrubberDrawable); + defaultTouchTargetHeight = + Math.max(scrubberDrawable.getMinimumHeight(), defaultTouchTargetHeight); + } + barHeight = + a.getDimensionPixelSize(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_bar_height, defaultBarHeight); + touchTargetHeight = + a.getDimensionPixelSize( + com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_touch_target_height, defaultTouchTargetHeight); + barGravity = a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_bar_gravity, BAR_GRAVITY_CENTER); + adMarkerWidth = + a.getDimensionPixelSize( + com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_ad_marker_width, defaultAdMarkerWidth); + scrubberEnabledSize = + a.getDimensionPixelSize( + com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_enabled_size, defaultScrubberEnabledSize); + scrubberDisabledSize = + a.getDimensionPixelSize( + com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_disabled_size, defaultScrubberDisabledSize); + scrubberDraggedSize = + a.getDimensionPixelSize( + com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize); + int playedColor = a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR); + int scrubberColor = + a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_color, DEFAULT_SCRUBBER_COLOR); + int bufferedColor = + a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_buffered_color, DEFAULT_BUFFERED_COLOR); + int unplayedColor = + a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_unplayed_color, DEFAULT_UNPLAYED_COLOR); + int adMarkerColor = + a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_ad_marker_color, DEFAULT_AD_MARKER_COLOR); + int playedAdMarkerColor = + a.getInt( + com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_played_ad_marker_color, DEFAULT_PLAYED_AD_MARKER_COLOR); + playedPaint.setColor(playedColor); + scrubberPaint.setColor(scrubberColor); + bufferedPaint.setColor(bufferedColor); + unplayedPaint.setColor(unplayedColor); + adMarkerPaint.setColor(adMarkerColor); + playedAdMarkerPaint.setColor(playedAdMarkerColor); + } finally { + a.recycle(); + } + } else { + barHeight = defaultBarHeight; + touchTargetHeight = defaultTouchTargetHeight; + barGravity = BAR_GRAVITY_CENTER; + adMarkerWidth = defaultAdMarkerWidth; + scrubberEnabledSize = defaultScrubberEnabledSize; + scrubberDisabledSize = defaultScrubberDisabledSize; + scrubberDraggedSize = defaultScrubberDraggedSize; + playedPaint.setColor(DEFAULT_PLAYED_COLOR); + scrubberPaint.setColor(DEFAULT_SCRUBBER_COLOR); + bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR); + unplayedPaint.setColor(DEFAULT_UNPLAYED_COLOR); + adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR); + playedAdMarkerPaint.setColor(DEFAULT_PLAYED_AD_MARKER_COLOR); + scrubberDrawable = null; + } + formatBuilder = new StringBuilder(); + formatter = new Formatter(formatBuilder, Locale.getDefault()); + stopScrubbingRunnable = () -> stopScrubbing(/* canceled= */ false); + if (scrubberDrawable != null) { + scrubberPadding = (scrubberDrawable.getMinimumWidth() + 1) / 2; + } else { + scrubberPadding = + (Math.max(scrubberDisabledSize, Math.max(scrubberEnabledSize, scrubberDraggedSize)) + 1) + / 2; + } + scrubberScale = 1.0f; + scrubberScalingAnimator = new ValueAnimator(); + scrubberScalingAnimator.addUpdateListener( + animation -> { + scrubberScale = (float) animation.getAnimatedValue(); + invalidate(seekBounds); + }); + duration = C.TIME_UNSET; + keyTimeIncrement = C.TIME_UNSET; + keyCountIncrement = DEFAULT_INCREMENT_COUNT; + setFocusable(true); + if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + public void setThumbnailUtils(ThumbnailProvider thumbnailUtils) { + this.thumbnailUtils = thumbnailUtils; + } + + /** Shows the scrubber handle. */ + public void showScrubber() { + if (scrubberScalingAnimator.isStarted()) { + scrubberScalingAnimator.cancel(); + } + scrubberPaddingDisabled = false; + scrubberScale = 1; + invalidate(seekBounds); + } + + /** + * Shows the scrubber handle with animation. + * + * @param showAnimationDurationMs The duration for scrubber showing animation. + */ + public void showScrubber(long showAnimationDurationMs) { + if (scrubberScalingAnimator.isStarted()) { + scrubberScalingAnimator.cancel(); + } + scrubberPaddingDisabled = false; + scrubberScalingAnimator.setFloatValues(scrubberScale, SHOWN_SCRUBBER_SCALE); + scrubberScalingAnimator.setDuration(showAnimationDurationMs); + scrubberScalingAnimator.start(); + } + + /** Hides the scrubber handle. */ + public void hideScrubber(boolean disableScrubberPadding) { + if (scrubberScalingAnimator.isStarted()) { + scrubberScalingAnimator.cancel(); + } + scrubberPaddingDisabled = disableScrubberPadding; + scrubberScale = 0; + invalidate(seekBounds); + } + + /** + * Hides the scrubber handle with animation. + * + * @param hideAnimationDurationMs The duration for scrubber hiding animation. + */ + public void hideScrubber(long hideAnimationDurationMs) { + if (scrubberScalingAnimator.isStarted()) { + scrubberScalingAnimator.cancel(); + } + scrubberScalingAnimator.setFloatValues(scrubberScale, HIDDEN_SCRUBBER_SCALE); + scrubberScalingAnimator.setDuration(hideAnimationDurationMs); + scrubberScalingAnimator.start(); + } + + /** + * Sets the color for the portion of the time bar representing media before the playback position. + * + * @param playedColor The color for the portion of the time bar representing media before the + * playback position. + */ + public void setPlayedColor(@ColorInt int playedColor) { + playedPaint.setColor(playedColor); + invalidate(seekBounds); + } + + /** + * Sets the color for the scrubber handle. + * + * @param scrubberColor The color for the scrubber handle. + */ + public void setScrubberColor(@ColorInt int scrubberColor) { + scrubberPaint.setColor(scrubberColor); + invalidate(seekBounds); + } + + /** + * Sets the color for the portion of the time bar after the current played position up to the + * current buffered position. + * + * @param bufferedColor The color for the portion of the time bar after the current played + * position up to the current buffered position. + */ + public void setBufferedColor(@ColorInt int bufferedColor) { + bufferedPaint.setColor(bufferedColor); + invalidate(seekBounds); + } + + /** + * Sets the color for the portion of the time bar after the current played position. + * + * @param unplayedColor The color for the portion of the time bar after the current played + * position. + */ + public void setUnplayedColor(@ColorInt int unplayedColor) { + unplayedPaint.setColor(unplayedColor); + invalidate(seekBounds); + } + + /** + * Sets the color for unplayed ad markers. + * + * @param adMarkerColor The color for unplayed ad markers. + */ + public void setAdMarkerColor(@ColorInt int adMarkerColor) { + adMarkerPaint.setColor(adMarkerColor); + invalidate(seekBounds); + } + + /** + * Sets the color for played ad markers. + * + * @param playedAdMarkerColor The color for played ad markers. + */ + public void setPlayedAdMarkerColor(@ColorInt int playedAdMarkerColor) { + playedAdMarkerPaint.setColor(playedAdMarkerColor); + invalidate(seekBounds); + } + + // TimeBar implementation. + + @Override + public void addListener(OnScrubListener listener) { + Assertions.checkNotNull(listener); + listeners.add(listener); + } + + @Override + public void removeListener(OnScrubListener listener) { + listeners.remove(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) { + if (this.position == position) { + return; + } + this.position = position; + setContentDescription(getProgressText()); + update(); + } + + @Override + public void setBufferedPosition(long bufferedPosition) { + if (this.bufferedPosition == bufferedPosition) { + return; + } + this.bufferedPosition = bufferedPosition; + update(); + } + + @Override + public void setDuration(long duration) { + if (this.duration == duration) { + return; + } + this.duration = duration; + if (scrubbing && duration == C.TIME_UNSET) { + stopScrubbing(/* canceled= */ true); + } + update(); + } + + @Override + public long getPreferredUpdateDelay() { + int timeBarWidthDp = pxToDp(density, progressBar.width()); + return timeBarWidthDp == 0 || duration == 0 || duration == C.TIME_UNSET + ? Long.MAX_VALUE + : duration / timeBarWidthDp; + } + + @Override + public void setAdGroupTimesMs( + @Nullable long[] adGroupTimesMs, @Nullable boolean[] playedAdGroups, int adGroupCount) { + Assertions.checkArgument( + adGroupCount == 0 || (adGroupTimesMs != null && playedAdGroups != null)); + this.adGroupCount = adGroupCount; + this.adGroupTimesMs = adGroupTimesMs; + this.playedAdGroups = playedAdGroups; + update(); + } + + // View methods. + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (scrubbing && !enabled) { + stopScrubbing(/* canceled= */ 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)) { + positionScrubber(x); + startScrubbing(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); + } + updateScrubbing(getScrubberPosition()); + update(); + invalidate(); + return true; + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (scrubbing) { + stopScrubbing(/* canceled= */ 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) { + stopScrubbing(/* canceled= */ false); + return true; + } + break; + default: + // Do nothing. + } + } + return super.onKeyDown(keyCode, event); + } + + @Override + protected void onFocusChanged( + boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + if (scrubbing && !gainFocus) { + stopScrubbing(/* canceled= */ false); + } + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + updateDrawableState(); + } + + @Override + public void jumpDrawablesToCurrentState() { + super.jumpDrawablesToCurrentState(); + if (scrubberDrawable != null) { + scrubberDrawable.jumpToCurrentState(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int height = + heightMode == MeasureSpec.UNSPECIFIED + ? touchTargetHeight + : heightMode == MeasureSpec.EXACTLY + ? heightSize + : Math.min(touchTargetHeight, heightSize); + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height); + updateDrawableState(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int width = right - left; + int height = bottom - top; + int seekLeft = getPaddingLeft(); + int seekRight = width - getPaddingRight(); + int seekBoundsY; + int progressBarY; + int scrubberPadding = scrubberPaddingDisabled ? 0 : this.scrubberPadding; + if (barGravity == BAR_GRAVITY_BOTTOM) { + seekBoundsY = height - getPaddingBottom() - touchTargetHeight; + progressBarY = + height - getPaddingBottom() - barHeight - Math.max(scrubberPadding - (barHeight / 2), 0); + } else { + seekBoundsY = (height - touchTargetHeight) / 2; + progressBarY = (height - barHeight) / 2; + } + seekBounds.set(seekLeft, seekBoundsY, seekRight, seekBoundsY + touchTargetHeight); + progressBar.set( + seekBounds.left + scrubberPadding, + progressBarY, + seekBounds.right - scrubberPadding, + progressBarY + barHeight); + if (Util.SDK_INT >= 29) { + setSystemGestureExclusionRectsV29(width, height); + } + update(); + } + + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + if (scrubberDrawable != null && setDrawableLayoutDirection(scrubberDrawable, layoutDirection)) { + invalidate(); + } + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) { + event.getText().add(getProgressText()); + } + event.setClassName(ACCESSIBILITY_CLASS_NAME); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(ACCESSIBILITY_CLASS_NAME); + 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 { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + + @Override + public boolean performAccessibilityAction(int action, @Nullable Bundle args) { + if (super.performAccessibilityAction(action, args)) { + return true; + } + if (duration <= 0) { + return false; + } + if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + if (scrubIncrementally(-getPositionIncrement())) { + stopScrubbing(/* canceled= */ false); + } + } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { + if (scrubIncrementally(getPositionIncrement())) { + stopScrubbing(/* canceled= */ false); + } + } else { + return false; + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + return true; + } + + // Internal methods. + + private void startScrubbing(long scrubPosition) { + this.scrubPosition = scrubPosition; + scrubbing = true; + setPressed(true); + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + for (OnScrubListener listener : listeners) { + listener.onScrubStart(this, scrubPosition); + } + } + + private void updateScrubbing(long scrubPosition) { + if (this.scrubPosition == scrubPosition) { + return; + } + this.scrubPosition = scrubPosition; + for (OnScrubListener listener : listeners) { + listener.onScrubMove(this, scrubPosition); + } + } + + private void stopScrubbing(boolean canceled) { + removeCallbacks(stopScrubbingRunnable); + scrubbing = false; + setPressed(false); + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(false); + } + invalidate(); + for (OnScrubListener listener : listeners) { + listener.onScrubStop(this, scrubPosition, canceled); + } + } + + /** + * 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 previousPosition = scrubbing ? scrubPosition : position; + long scrubPosition = Util.constrainValue(previousPosition + positionChange, 0, duration); + if (scrubPosition == previousPosition) { + return false; + } + if (!scrubbing) { + startScrubbing(scrubPosition); + } else { + updateScrubbing(scrubPosition); + } + update(); + return true; + } + + 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 = Math.min(progressBar.left + bufferedPixelWidth, progressBar.right); + int scrubberPixelPosition = (int) ((progressBar.width() * newScrubberTime) / duration); + scrubberBar.right = Math.min(progressBar.left + scrubberPixelPosition, progressBar.right); + } 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) { + touchPosition.set((int) motionEvent.getX(), (int) motionEvent.getY()); + 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, unplayedPaint); + 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, unplayedPaint); + } + 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, playedPaint); + } + if (adGroupCount == 0) { + return; + } + long[] adGroupTimesMs = Assertions.checkNotNull(this.adGroupTimesMs); + boolean[] playedAdGroups = Assertions.checkNotNull(this.playedAdGroups); + int adMarkerOffset = adMarkerWidth / 2; + for (int i = 0; i < adGroupCount; i++) { + long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration); + int markerPositionOffset = + (int) (progressBar.width() * adGroupTimeMs / duration) - adMarkerOffset; + int markerLeft = + progressBar.left + + Math.min(progressBar.width() - adMarkerWidth, Math.max(0, markerPositionOffset)); + Paint paint = playedAdGroups[i] ? playedAdMarkerPaint : adMarkerPaint; + canvas.drawRect(markerLeft, barTop, markerLeft + adMarkerWidth, barBottom, paint); + } + } + + private void drawPlayhead(Canvas canvas) { + if (duration <= 0) { + return; + } + int playheadX = Util.constrainValue(scrubberBar.right, scrubberBar.left, progressBar.right); + int playheadY = scrubberBar.centerY(); + if (scrubberDrawable == null) { + int scrubberSize = + (scrubbing || isFocused()) + ? scrubberDraggedSize + : (isEnabled() ? scrubberEnabledSize : scrubberDisabledSize); + int playheadRadius = (int) ((scrubberSize * scrubberScale) / 2); + canvas.drawCircle(playheadX, playheadY, playheadRadius, scrubberPaint); + + if (scrubbing) drawThumbnail(canvas, playheadRadius, playheadX, playheadY); + } else { + int scrubberDrawableWidth = (int) (scrubberDrawable.getIntrinsicWidth() * scrubberScale); + int scrubberDrawableHeight = (int) (scrubberDrawable.getIntrinsicHeight() * scrubberScale); + scrubberDrawable.setBounds( + playheadX - scrubberDrawableWidth / 2, + playheadY - scrubberDrawableHeight / 2, + playheadX + scrubberDrawableWidth / 2, + playheadY + scrubberDrawableHeight / 2); + scrubberDrawable.draw(canvas); + } + } + + private void drawThumbnail(Canvas canvas, int playheadRadius, int playheadX, int playheadY) { + + if (thumbnailUtils == null) return; + Bitmap b = thumbnailUtils.getThumbnail(getScrubberPosition()); + + if (b == null) return; + + //adapt thumbnail to desired UI size + double arFactor = (double) b.getWidth() / b.getHeight(); + b = Bitmap.createScaledBitmap(b, dpToPx((int)(targetThumbnailHeightInDp * arFactor)), dpToPx(targetThumbnailHeightInDp), false); + + int width = b.getWidth(); + int height = b.getHeight(); + int offset = (int)width / 2; + + int left = playheadX-offset; + + //handle full left, full right position cases + if (left < 0 ) left = 0; + if (left + width > progressBar.width() + playheadRadius) left = progressBar.width() + playheadRadius - width; + + canvas.drawBitmap(b, left, playheadY-playheadRadius*2-height, null); + } + + private int dpToPx(int dp){ + return (int) (dp * getContext().getResources().getDisplayMetrics().density); + } + + private void updateDrawableState() { + if (scrubberDrawable != null + && scrubberDrawable.isStateful() + && scrubberDrawable.setState(getDrawableState())) { + invalidate(); + } + } + + @RequiresApi(29) + private void setSystemGestureExclusionRectsV29(int width, int height) { + if (lastExclusionRectangle != null + && lastExclusionRectangle.width() == width + && lastExclusionRectangle.height() == height) { + // Allocating inside onLayout is considered a DrawAllocation lint error, so avoid if possible. + return; + } + lastExclusionRectangle = new Rect(/* left= */ 0, /* top= */ 0, width, height); + setSystemGestureExclusionRects(Collections.singletonList(lastExclusionRectangle)); + } + + 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; + } + + private boolean setDrawableLayoutDirection(Drawable drawable) { + return Util.SDK_INT >= 23 && setDrawableLayoutDirection(drawable, getLayoutDirection()); + } + + private static boolean setDrawableLayoutDirection(Drawable drawable, int layoutDirection) { + return Util.SDK_INT >= 23 && drawable.setLayoutDirection(layoutDirection); + } + + private static int dpToPx(float density, int dps) { + return (int) (dps * density + 0.5f); + } + + private static int pxToDp(float density, int px) { + return (int) (px / density); + } +} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/ThumbnailProvider.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/ThumbnailProvider.java new file mode 100644 index 0000000000..129fc84b16 --- /dev/null +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/ThumbnailProvider.java @@ -0,0 +1,9 @@ +package com.google.android.exoplayer2.demo; + +import android.graphics.Bitmap; + +public interface ThumbnailProvider { + + public Bitmap getThumbnail(long position); + +} diff --git a/demos/main/src/main/res/layout/exo_styled_player_control_view.xml b/demos/main/src/main/res/layout/exo_styled_player_control_view.xml new file mode 100644 index 0000000000..2a6ae481da --- /dev/null +++ b/demos/main/src/main/res/layout/exo_styled_player_control_view.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 2f8fd87fa2b08ff01155d00d7576cb08aef3c3ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6rkem=20G=C3=BCcl=C3=BC?= Date: Mon, 5 Dec 2022 21:04:22 +0100 Subject: [PATCH 3/6] Added two new image specific properties 'tileCountHorizontal' and 'tileCountVertical' to class Format. DashManifestParser adjusted to parse these values from an EssentialProperty tag of an Image AdaptationSet. With this change, DashManifest does not have to do any parsing inside the new getThumbnailDescriptions() method. Moreover, both classes ThumbnailDescription and ThumbnailProvider adjusted to use these two properties / naming scheme accordingly. --- .../demo/DefaultThumbnailProvider.java | 10 +-- .../com/google/android/exoplayer2/Format.java | 68 +++++++++++++++++++ .../thumbnail/ThumbnailDescription.java | 18 ++--- .../source/dash/manifest/DashManifest.java | 19 +----- .../dash/manifest/DashManifestParser.java | 31 ++++++++- 5 files changed, 115 insertions(+), 31 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java index c06144a1b8..2006d68990 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java @@ -92,7 +92,7 @@ public class DefaultThumbnailProvider implements ThumbnailProvider { if (position < thumbnailDescription.getStartTimeMs() || position > thumbnailDescription.getStartTimeMs() + thumbnailDescription.getDurationMs()) return null; - int count = thumbnailDescription.getColumns() * thumbnailDescription.getRows(); + int count = thumbnailDescription.getTileCountHorizontal() * thumbnailDescription.getTileCountVertical(); int durationPerImage = (int)(thumbnailDescription.getDurationMs() / count); @@ -101,12 +101,12 @@ public class DefaultThumbnailProvider implements ThumbnailProvider { //handle special case if position == duration if (imageNumberToUseWithinTile > count-1) imageNumberToUseWithinTile = count-1; - int intRowToUse = (int)(imageNumberToUseWithinTile / thumbnailDescription.getColumns()); + int intRowToUse = (int)(imageNumberToUseWithinTile / thumbnailDescription.getTileCountHorizontal()); - int intColToUse = imageNumberToUseWithinTile - intRowToUse * thumbnailDescription.getColumns(); + int intColToUse = imageNumberToUseWithinTile - intRowToUse * thumbnailDescription.getTileCountHorizontal(); - double thumbnailWidth = (double) thumbnailDescription.getImageWidth() / thumbnailDescription.getColumns(); - double thumbnailHeight = (double) thumbnailDescription.getImageHeight() / thumbnailDescription.getRows(); + double thumbnailWidth = (double) thumbnailDescription.getImageWidth() / thumbnailDescription.getTileCountHorizontal(); + double thumbnailHeight = (double) thumbnailDescription.getImageHeight() / thumbnailDescription.getTileCountVertical(); int cropXLeft = (int)Math.round(intColToUse * thumbnailWidth); int cropYTop = (int)Math.round(intRowToUse * thumbnailHeight); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Format.java b/library/common/src/main/java/com/google/android/exoplayer2/Format.java index ea3b552e1d..2a89d4677a 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Format.java @@ -115,6 +115,13 @@ import java.util.UUID; *
    *
  • {@link #accessibilityChannel} *
+ * + *

Fields relevant to image formats

+ * + *
    + *
  • {@link #tileCountHorizontal} + *
  • {@link #tileCountVertical} + *
*/ public final class Format implements Bundleable { @@ -174,6 +181,11 @@ public final class Format implements Bundleable { private int accessibilityChannel; + // Image specific + + private int tileCountHorizontal; + private int tileCountVertical; + // Provided by the source. private @C.CryptoType int cryptoType; @@ -197,6 +209,9 @@ public final class Format implements Bundleable { pcmEncoding = NO_VALUE; // Text specific. accessibilityChannel = NO_VALUE; + // Image specific. + tileCountHorizontal = NO_VALUE; + tileCountVertical = NO_VALUE; // Provided by the source. cryptoType = C.CRYPTO_TYPE_NONE; } @@ -241,6 +256,9 @@ public final class Format implements Bundleable { this.encoderPadding = format.encoderPadding; // Text specific. this.accessibilityChannel = format.accessibilityChannel; + // Image specific. + this.tileCountHorizontal = format.tileCountHorizontal; + this.tileCountVertical = format.tileCountVertical; // Provided by the source. this.cryptoType = format.cryptoType; } @@ -616,6 +634,30 @@ public final class Format implements Bundleable { return this; } + // Image specific. + + /** + * Sets {@link Format#tileCountHorizontal}. The default value is {@link #NO_VALUE}. + * + * @param tileCountHorizontal The {@link Format#accessibilityChannel}. + * @return The builder. + */ + public Builder setTileCountHorizontal(int tileCountHorizontal) { + this.tileCountHorizontal = tileCountHorizontal; + return this; + } + + /** + * Sets {@link Format#tileCountVertical}. The default value is {@link #NO_VALUE}. + * + * @param tileCountVertical The {@link Format#accessibilityChannel}. + * @return The builder. + */ + public Builder setTileCountVertical(int tileCountVertical) { + this.tileCountVertical = tileCountVertical; + return this; + } + // Provided by source. /** @@ -788,6 +830,12 @@ public final class Format implements Bundleable { /** The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */ public final int accessibilityChannel; + // Image specific. + + /** Thumbnail tile count horizontal and vertical, or {@link #NO_VALUE} if not known or applicable. */ + public final int tileCountHorizontal; + public final int tileCountVertical; + // Provided by source. /** @@ -1011,6 +1059,9 @@ public final class Format implements Bundleable { encoderPadding = builder.encoderPadding == NO_VALUE ? 0 : builder.encoderPadding; // Text specific. accessibilityChannel = builder.accessibilityChannel; + // Image specific. + tileCountHorizontal = builder.tileCountHorizontal; + tileCountVertical = builder.tileCountVertical; // Provided by source. if (builder.cryptoType == C.CRYPTO_TYPE_NONE && drmInitData != null) { // Encrypted content cannot use CRYPTO_TYPE_NONE. @@ -1257,6 +1308,9 @@ public final class Format implements Bundleable { result = 31 * result + encoderPadding; // Text specific. result = 31 * result + accessibilityChannel; + // Image specific. + result = 31 * result + tileCountHorizontal; + result = 31 * result + tileCountVertical; // Provided by the source. result = 31 * result + cryptoType; hashCode = result; @@ -1293,6 +1347,8 @@ public final class Format implements Bundleable { && encoderDelay == other.encoderDelay && encoderPadding == other.encoderPadding && accessibilityChannel == other.accessibilityChannel + && tileCountHorizontal == other.tileCountHorizontal + && tileCountVertical == other.tileCountVertical && cryptoType == other.cryptoType && Float.compare(frameRate, other.frameRate) == 0 && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 @@ -1490,6 +1546,8 @@ public final class Format implements Bundleable { FIELD_ENCODER_PADDING, FIELD_ACCESSIBILITY_CHANNEL, FIELD_CRYPTO_TYPE, + FIELD_TILE_COUNT_HORIZONTAL, + FIELD_TILE_COUNT_VERTICAL, }) private @interface FieldNumber {} @@ -1523,6 +1581,8 @@ public final class Format implements Bundleable { private static final int FIELD_ENCODER_PADDING = 27; private static final int FIELD_ACCESSIBILITY_CHANNEL = 28; private static final int FIELD_CRYPTO_TYPE = 29; + private static final int FIELD_TILE_COUNT_HORIZONTAL = 30; + private static final int FIELD_TILE_COUNT_VERTICAL = 31; @Override public Bundle toBundle() { @@ -1578,6 +1638,9 @@ public final class Format implements Bundleable { bundle.putInt(keyForField(FIELD_ENCODER_PADDING), encoderPadding); // Text specific. bundle.putInt(keyForField(FIELD_ACCESSIBILITY_CHANNEL), accessibilityChannel); + // Image specific. + bundle.putInt(keyForField(FIELD_TILE_COUNT_HORIZONTAL), tileCountHorizontal); + bundle.putInt(keyForField(FIELD_TILE_COUNT_VERTICAL), tileCountVertical); // Source specific. bundle.putInt(keyForField(FIELD_CRYPTO_TYPE), cryptoType); return bundle; @@ -1652,6 +1715,11 @@ public final class Format implements Bundleable { // Text specific. .setAccessibilityChannel( bundle.getInt(keyForField(FIELD_ACCESSIBILITY_CHANNEL), DEFAULT.accessibilityChannel)) + // Image specific. + .setTileCountHorizontal( + bundle.getInt(keyForField(FIELD_TILE_COUNT_HORIZONTAL), DEFAULT.tileCountHorizontal)) + .setTileCountVertical( + bundle.getInt(keyForField(FIELD_TILE_COUNT_VERTICAL), DEFAULT.tileCountVertical)) // Source specific. .setCryptoType(bundle.getInt(keyForField(FIELD_CRYPTO_TYPE), DEFAULT.cryptoType)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/thumbnail/ThumbnailDescription.java b/library/core/src/main/java/com/google/android/exoplayer2/thumbnail/ThumbnailDescription.java index 8984689404..cfd1f6e8fe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/thumbnail/ThumbnailDescription.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/thumbnail/ThumbnailDescription.java @@ -7,19 +7,19 @@ public class ThumbnailDescription { private final String id; private final Uri uri; private final int bitrate; - private final int rows; - private final int columns; + private final int tileCountHorizontal; + private final int tileCountVertical; private final long startTimeMs; private final long durationMs; private final int imageWidth; // Image width (Pixel) private final int imageHeight; // Image height (Pixel) - public ThumbnailDescription(String id, Uri uri, int bitrate, int rows, int columns, long startTimeMs, long durationMs, int imageWidth, int imageHeight) { + public ThumbnailDescription(String id, Uri uri, int bitrate, int tileCountHorizontal, int tileCountVertical, long startTimeMs, long durationMs, int imageWidth, int imageHeight) { this.id = id; this.uri = uri; this.bitrate = bitrate; - this.rows = rows; - this.columns = columns; + this.tileCountHorizontal = tileCountHorizontal; + this.tileCountVertical = tileCountVertical; this.startTimeMs = startTimeMs; this.durationMs = durationMs; this.imageWidth = imageWidth; @@ -34,12 +34,12 @@ public class ThumbnailDescription { return bitrate; } - public int getRows() { - return rows; + public int getTileCountHorizontal() { + return tileCountHorizontal; } - public int getColumns() { - return columns; + public int getTileCountVertical() { + return tileCountVertical; } public long getStartTimeMs() { diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 9123ea559c..e44306a17e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -185,22 +185,9 @@ public class DashManifest implements FilterableManifest { int bitrate = representation.format.bitrate; int imageWidth = representation.format.width; int imageHeight = representation.format.height; - // get size XxY, e.g. 10x20, where 10 is column count and 20 is row count - int rows = 1; - int cols = 1; - for (int m = 0; m < representation.essentialProperties.size(); m++) { - Descriptor descriptor = representation.essentialProperties.get(m); - if ((Ascii.equalsIgnoreCase("http://dashif.org/thumbnail_tile", descriptor.schemeIdUri) || Ascii.equalsIgnoreCase("http://dashif.org/guidelines/thumbnail_tile", descriptor.schemeIdUri)) && descriptor.value != null) { - String size = descriptor.value; - String[] sizeSplit = size.split("x"); - if (sizeSplit.length != 2) { - continue; - } - cols = Integer.parseInt(sizeSplit[0]); - rows = Integer.parseInt(sizeSplit[1]); - } - } + int tileCountHorizontal = representation.format.tileCountHorizontal; + int tileCountVertical = representation.format.tileCountVertical; long now = Util.getNowUnixTimeMs(C.TIME_UNSET); String baseUrl = castNonNull(baseUrlExclusionList.selectBaseUrl(representation.baseUrls)).url; @@ -217,7 +204,7 @@ public class DashManifest implements FilterableManifest { RangedUri rangedUri = index.getSegmentUrl(segmentNumber); DataSpec dataSpec = DashUtil.buildDataSpec(representation, baseUrl, rangedUri, /* flags= */ 0); Uri uri = dataSpec.uri; - ThumbnailDescription thumbnailDescription = new ThumbnailDescription(id, uri, bitrate, rows, cols, Util.usToMs(segmentStartTimeUs - (dynamic ? firstStartTimeUs : 0)), Util.usToMs(segmentDurationUs), imageWidth, imageHeight); + ThumbnailDescription thumbnailDescription = new ThumbnailDescription(id, uri, bitrate, tileCountHorizontal, tileCountVertical, Util.usToMs(segmentStartTimeUs - (dynamic ? firstStartTimeUs : 0)), Util.usToMs(segmentDurationUs), imageWidth, imageHeight); thumbnailDescriptions.add(thumbnailDescription); } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 919b1e253f..1721149cfa 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -811,6 +811,8 @@ public class DashManifestParser extends DefaultHandler roleFlags |= parseRoleFlagsFromProperties(essentialProperties); roleFlags |= parseRoleFlagsFromProperties(supplementalProperties); + Pair tileCounts = parseTileCountFromProperties(essentialProperties); + Format.Builder formatBuilder = new Format.Builder() .setId(id) @@ -820,7 +822,9 @@ public class DashManifestParser extends DefaultHandler .setPeakBitrate(bitrate) .setSelectionFlags(selectionFlags) .setRoleFlags(roleFlags) - .setLanguage(language); + .setLanguage(language) + .setTileCountHorizontal(tileCounts != null ? tileCounts.first : Format.NO_VALUE) + .setTileCountVertical(tileCounts != null ? tileCounts.second : Format.NO_VALUE); if (MimeTypes.isVideo(sampleMimeType)) { formatBuilder.setWidth(width).setHeight(height).setFrameRate(frameRate); @@ -1629,6 +1633,31 @@ public class DashManifestParser extends DefaultHandler return attributeValue.split(","); } + // Thumbnail tile information parsing + + /** + * Parses given descriptors for thumbnail tile information + * @param essentialProperties List of descriptor that contain thumbnail tile information + * @return A pair of Integer values, where the first is the count of horizontal tiles + * and the second is the count of vertical tiles, or null if no thumbnail tile information is found. + */ + @Nullable + protected Pair parseTileCountFromProperties(List essentialProperties) { + for (Descriptor descriptor : essentialProperties) { + if ((Ascii.equalsIgnoreCase("http://dashif.org/thumbnail_tile", descriptor.schemeIdUri) || Ascii.equalsIgnoreCase("http://dashif.org/guidelines/thumbnail_tile", descriptor.schemeIdUri)) && descriptor.value != null) { + String size = descriptor.value; + String[] sizeSplit = size.split("x"); + if (sizeSplit.length != 2) { + continue; + } + int tileCountHorizontal = Integer.parseInt(sizeSplit[0]); + int tileCountVertical = Integer.parseInt(sizeSplit[1]); + return Pair.create(tileCountHorizontal, tileCountVertical); + } + } + return null; + } + // Utility methods. /** From dad74276efdfc7119476a3d62746ba5a0cf3da0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6rkem=20G=C3=BCcl=C3=BC?= Date: Fri, 9 Dec 2022 15:29:23 +0100 Subject: [PATCH 4/6] Image AdaptationSets have to be excluded from duration calculation, similar to Text AdaptationSets, see issue 4029 (https://github.com/google/ExoPlayer/issues/4029). --- .../android/exoplayer2/source/dash/DashMediaSource.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 853f76b4a4..98991b37e7 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 @@ -1056,9 +1056,9 @@ public final class DashMediaSource extends BaseMediaSource { for (int i = 0; i < period.adaptationSets.size(); i++) { AdaptationSet adaptationSet = period.adaptationSets.get(i); List representations = adaptationSet.representations; - // Exclude text adaptation sets from duration calculations, if we have at least one audio + // Exclude text and image adaptation sets from duration calculations, if we have at least one audio // or video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029 - if ((haveAudioVideoAdaptationSets && adaptationSet.type == C.TRACK_TYPE_TEXT) + if ((haveAudioVideoAdaptationSets && (adaptationSet.type == C.TRACK_TYPE_TEXT || adaptationSet.type == C.TRACK_TYPE_IMAGE)) || representations.isEmpty()) { continue; } @@ -1088,9 +1088,9 @@ public final class DashMediaSource extends BaseMediaSource { for (int i = 0; i < period.adaptationSets.size(); i++) { AdaptationSet adaptationSet = period.adaptationSets.get(i); List representations = adaptationSet.representations; - // Exclude text adaptation sets from duration calculations, if we have at least one audio + // Exclude text and image adaptation sets from duration calculations, if we have at least one audio // or video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029 - if ((haveAudioVideoAdaptationSets && adaptationSet.type == C.TRACK_TYPE_TEXT) + if ((haveAudioVideoAdaptationSets && (adaptationSet.type == C.TRACK_TYPE_TEXT || adaptationSet.type == C.TRACK_TYPE_IMAGE)) || representations.isEmpty()) { continue; } From f518cb660c52bc1244a9baeab813f3f95ad3ad65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6rkem=20G=C3=BCcl=C3=BC?= Date: Mon, 16 Jan 2023 12:12:45 +0100 Subject: [PATCH 5/6] Removing files outside of library --- .../demo/DefaultThumbnailProvider.java | 194 ---- .../demo/DefaultThumbnailTimeBar.java | 1026 ----------------- .../exoplayer2/demo/PlayerActivity.java | 6 +- .../exoplayer2/demo/ThumbnailProvider.java | 9 - .../layout/exo_styled_player_control_view.xml | 152 --- .../thumbnail/ThumbnailDescription.java | 13 +- .../source/dash/manifest/DashManifest.java | 3 + 7 files changed, 14 insertions(+), 1389 deletions(-) delete mode 100644 demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java delete mode 100644 demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailTimeBar.java delete mode 100644 demos/main/src/main/java/com/google/android/exoplayer2/demo/ThumbnailProvider.java delete mode 100644 demos/main/src/main/res/layout/exo_styled_player_control_view.xml diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java deleted file mode 100644 index 2006d68990..0000000000 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.google.android.exoplayer2.demo; - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.AsyncTask; -import android.util.LruCache; -import android.view.View; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.source.dash.manifest.DashManifest; -import com.google.android.exoplayer2.thumbnail.ThumbnailDescription; -import com.google.android.exoplayer2.util.Log; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.List; - -public class DefaultThumbnailProvider implements ThumbnailProvider { - - private static final String TAG_DEBUG = DefaultThumbnailProvider.class.getSimpleName(); - - private LruCache bitmapCache; - private View parent; - - //dummy bitmap to indicate that a download is already triggered but not finished yet - private final Bitmap dummyBitmap = Bitmap.createBitmap(1,1,Bitmap.Config.ARGB_8888); - @Nullable ExoPlayer exoPlayer; - - public DefaultThumbnailProvider(ExoPlayer exoPlayer, View view) { - this.exoPlayer = exoPlayer; - - this.parent = view; - - final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); - final int cacheSize = maxMemory / 4; - bitmapCache = new LruCache(cacheSize) { - @Override - protected int sizeOf(String key, Bitmap bitmap) { - return bitmap.getByteCount() / 1024; - } - }; - } - - public Bitmap getThumbnail(long position) { - return getThumbnail(position, true); - } - - private Bitmap getThumbnail(long position, boolean retrigger) { - if (exoPlayer != null) { - Object manifest = exoPlayer.getCurrentManifest(); - - ThumbnailDescription thumbnailDescription = null; - if (manifest instanceof DashManifest) { - DashManifest dashManifest = (DashManifest) manifest; - List thumbnailDescs = dashManifest.getThumbnailDescriptions(position); - //selected thumbnail description with lowest bitrate - for (ThumbnailDescription desc : thumbnailDescs) { - if (thumbnailDescription == null || thumbnailDescription.getBitrate() > desc.getBitrate()) { - thumbnailDescription = desc; - } - } - if (bitmapNotAvailableOrDownloadNotTriggeredYet(thumbnailDescription.getUri())) { - this.initThumbnailSource(thumbnailDescription); - return null; - } - } - - if (retrigger) { - //also download next and prev thumbnails to have a nicer UI user experience - getThumbnail(thumbnailDescription.getStartTimeMs() + thumbnailDescription.getDurationMs(), false); - getThumbnail(thumbnailDescription.getStartTimeMs() - thumbnailDescription.getDurationMs(), false); - } - - return getThumbnailInternal(position, thumbnailDescription); - } - return null; - } - - private boolean bitmapNotAvailableOrDownloadNotTriggeredYet(Uri uri) { - Bitmap tmp = bitmapCache.get(uri.toString()); - if (tmp != null) return false; - return true; - } - - private Bitmap getThumbnailInternal(long position, ThumbnailDescription thumbnailDescription) { - if (thumbnailDescription == null) return null; - - Bitmap thumbnailSource = bitmapCache.get(thumbnailDescription.getUri().toString()); - - if (thumbnailSource == null || thumbnailSource.getWidth() == 1) return null; - - if (position < thumbnailDescription.getStartTimeMs() || position > thumbnailDescription.getStartTimeMs() + thumbnailDescription.getDurationMs()) return null; - - int count = thumbnailDescription.getTileCountHorizontal() * thumbnailDescription.getTileCountVertical(); - - int durationPerImage = (int)(thumbnailDescription.getDurationMs() / count); - - int imageNumberToUseWithinTile = (int)((position - thumbnailDescription.getStartTimeMs()) / durationPerImage); - - //handle special case if position == duration - if (imageNumberToUseWithinTile > count-1) imageNumberToUseWithinTile = count-1; - - int intRowToUse = (int)(imageNumberToUseWithinTile / thumbnailDescription.getTileCountHorizontal()); - - int intColToUse = imageNumberToUseWithinTile - intRowToUse * thumbnailDescription.getTileCountHorizontal(); - - double thumbnailWidth = (double) thumbnailDescription.getImageWidth() / thumbnailDescription.getTileCountHorizontal(); - double thumbnailHeight = (double) thumbnailDescription.getImageHeight() / thumbnailDescription.getTileCountVertical(); - - int cropXLeft = (int)Math.round(intColToUse * thumbnailWidth); - int cropYTop = (int)Math.round(intRowToUse * thumbnailHeight); - - if (cropXLeft + thumbnailWidth <= thumbnailSource.getWidth() && cropYTop + thumbnailHeight <= thumbnailSource.getHeight()) { - return Bitmap.createBitmap(thumbnailSource - , cropXLeft, cropYTop, (int) thumbnailWidth, (int) thumbnailHeight); - } - else { - Log.d(TAG_DEBUG, "Image does not have expected (" + thumbnailDescription.getImageWidth() + "x" + thumbnailDescription.getImageHeight() + ") dimensions to crop. Source " + thumbnailDescription.getUri()); - return null; - } - } - - private synchronized void initThumbnailSource(ThumbnailDescription thumbnailDescription){ - String path = thumbnailDescription.getUri().toString(); - if (path == null) return; - - if (bitmapCache.get(path) != null) return; - bitmapCache.put(path, dummyBitmap); - - RetrieveThumbnailImageTask currentTask = new RetrieveThumbnailImageTask(); - currentTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, path); - } - - class RetrieveThumbnailImageTask extends AsyncTask { - - String downloadedUrl; - - RetrieveThumbnailImageTask() { - - } - - @Override - protected void onCancelled() { - super.onCancelled(); - if (downloadedUrl != null) bitmapCache.remove(downloadedUrl); - } - - protected Bitmap doInBackground(String... urls) { - downloadedUrl = urls[0]; - InputStream in =null; - Bitmap thumbnailToDownload=null; - int responseCode = -1; - - try{ - URL url = new URL(downloadedUrl); - if (!isCancelled()) { - HttpURLConnection httpURLConnection = (HttpURLConnection)url.openConnection(); - httpURLConnection.setDoInput(true); - httpURLConnection.connect(); - responseCode = httpURLConnection.getResponseCode(); - if(responseCode == HttpURLConnection.HTTP_OK) - { - if (!isCancelled()) { - in = httpURLConnection.getInputStream(); - if (!isCancelled()) { - thumbnailToDownload = BitmapFactory.decodeStream(in); - } - in.close(); - } - } - } - - } - catch(Exception ex){ - bitmapCache.remove(downloadedUrl); - System.out.println(ex); - } - - return thumbnailToDownload; - } - - protected void onPostExecute(Bitmap downloadedThumbnail) { - if (downloadedThumbnail != null) { - bitmapCache.put(downloadedUrl, downloadedThumbnail); - if (parent != null) parent.invalidate(); - } - else { - bitmapCache.remove(downloadedUrl); - } - } - } - -} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailTimeBar.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailTimeBar.java deleted file mode 100644 index a3e75923e5..0000000000 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailTimeBar.java +++ /dev/null @@ -1,1026 +0,0 @@ -/* - * 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.demo; - -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -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 androidx.annotation.ColorInt; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ui.TimeBar; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.util.Collections; -import java.util.Formatter; -import java.util.Locale; -import java.util.concurrent.CopyOnWriteArraySet; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; - -/** - * A time bar that shows a current position, buffered position, duration and ad markers. - * - *

A DefaultTimeBar can be customized by setting attributes, as outlined below. - * - *

Attributes

- * - * The following attributes can be set on a DefaultTimeBar when used in a layout XML file: - * - *
    - *
  • {@code bar_height} - Dimension for the height of the time bar. - *
      - *
    • Default: {@link #DEFAULT_BAR_HEIGHT_DP} - *
    - *
  • {@code touch_target_height} - Dimension for the height of the area in which touch - * interactions with the time bar are handled. If no height is specified, this also determines - * the height of the view. - *
      - *
    • Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP} - *
    - *
  • {@code ad_marker_width} - Dimension for the width of any ad markers shown on the - * bar. Ad markers are superimposed on the time bar to show the times at which ads will play. - *
      - *
    • Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP} - *
    - *
  • {@code scrubber_enabled_size} - Dimension for the diameter of the circular scrubber - * handle when scrubbing is enabled but not in progress. Set to zero if no scrubber handle - * should be shown. - *
      - *
    • Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP} - *
    - *
  • {@code scrubber_disabled_size} - Dimension for the diameter of the circular scrubber - * handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown. - *
      - *
    • Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP} - *
    - *
  • {@code scrubber_dragged_size} - Dimension for the diameter of the circular scrubber - * handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown. - *
      - *
    • Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP} - *
    - *
  • {@code scrubber_drawable} - Optional reference to a drawable to draw for the - * scrubber handle. If set, this overrides the default behavior, which is to draw a circle for - * the scrubber handle. - *
  • {@code played_color} - Color for the portion of the time bar representing media - * before the current playback position. - *
      - *
    • Corresponding method: {@link #setPlayedColor(int)} - *
    • Default: {@link #DEFAULT_PLAYED_COLOR} - *
    - *
  • {@code scrubber_color} - Color for the scrubber handle. - *
      - *
    • Corresponding method: {@link #setScrubberColor(int)} - *
    • Default: {@link #DEFAULT_SCRUBBER_COLOR} - *
    - *
  • {@code buffered_color} - Color for the portion of the time bar after the current - * played position up to the current buffered position. - *
      - *
    • Corresponding method: {@link #setBufferedColor(int)} - *
    • Default: {@link #DEFAULT_BUFFERED_COLOR} - *
    - *
  • {@code unplayed_color} - Color for the portion of the time bar after the current - * buffered position. - *
      - *
    • Corresponding method: {@link #setUnplayedColor(int)} - *
    • Default: {@link #DEFAULT_UNPLAYED_COLOR} - *
    - *
  • {@code ad_marker_color} - Color for unplayed ad markers. - *
      - *
    • Corresponding method: {@link #setAdMarkerColor(int)} - *
    • Default: {@link #DEFAULT_AD_MARKER_COLOR} - *
    - *
  • {@code played_ad_marker_color} - Color for played ad markers. - *
      - *
    • Corresponding method: {@link #setPlayedAdMarkerColor(int)} - *
    • Default: {@link #DEFAULT_PLAYED_AD_MARKER_COLOR} - *
    - *
- */ -public class DefaultThumbnailTimeBar extends View implements TimeBar { - - /** Default height for the time bar, in dp. */ - public static final int DEFAULT_BAR_HEIGHT_DP = 4; - /** Default height for the touch target, in dp. */ - public static final int DEFAULT_TOUCH_TARGET_HEIGHT_DP = 26; - /** Default width for ad markers, in dp. */ - public static final int DEFAULT_AD_MARKER_WIDTH_DP = 4; - /** Default diameter for the scrubber when enabled, in dp. */ - public static final int DEFAULT_SCRUBBER_ENABLED_SIZE_DP = 12; - /** Default diameter for the scrubber when disabled, in dp. */ - public static final int DEFAULT_SCRUBBER_DISABLED_SIZE_DP = 0; - /** Default diameter for the scrubber when dragged, in dp. */ - public static final int DEFAULT_SCRUBBER_DRAGGED_SIZE_DP = 16; - /** Default color for the played portion of the time bar. */ - public static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF; - /** Default color for the unplayed portion of the time bar. */ - public static final int DEFAULT_UNPLAYED_COLOR = 0x33FFFFFF; - /** Default color for the buffered portion of the time bar. */ - public static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF; - /** Default color for the scrubber handle. */ - public static final int DEFAULT_SCRUBBER_COLOR = 0xFFFFFFFF; - /** Default color for ad markers. */ - public static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00; - /** Default color for played ad markers. */ - public static final int DEFAULT_PLAYED_AD_MARKER_COLOR = 0x33FFFF00; - - /** Vertical gravity for progress bar to be located at the center in the view. */ - public static final int BAR_GRAVITY_CENTER = 0; - /** Vertical gravity for progress bar to be located at the bottom in the view. */ - public static final int BAR_GRAVITY_BOTTOM = 1; - - /** The threshold in dps above the bar at which touch events trigger fine scrub mode. */ - private static final int FINE_SCRUB_Y_THRESHOLD_DP = -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 float SHOWN_SCRUBBER_SCALE = 1.0f; - private static final float HIDDEN_SCRUBBER_SCALE = 0.0f; - - /** - * The name of the Android SDK view that most closely resembles this custom view. Used as the - * class name for accessibility. - */ - private static final String ACCESSIBILITY_CLASS_NAME = "android.widget.SeekBar"; - - private final Rect seekBounds; - private final Rect progressBar; - private final Rect bufferedBar; - private final Rect scrubberBar; - private final Paint playedPaint; - private final Paint bufferedPaint; - private final Paint unplayedPaint; - private final Paint adMarkerPaint; - private final Paint playedAdMarkerPaint; - private final Paint scrubberPaint; - @Nullable private final Drawable scrubberDrawable; - private final int barHeight; - private final int touchTargetHeight; - private final int barGravity; - 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 final CopyOnWriteArraySet listeners; - private final Point touchPosition; - private final float density; - - private int keyCountIncrement; - private long keyTimeIncrement; - private int lastCoarseScrubXPosition; - private @MonotonicNonNull Rect lastExclusionRectangle; - - private ValueAnimator scrubberScalingAnimator; - private float scrubberScale; - private boolean scrubberPaddingDisabled; - private boolean scrubbing; - private long scrubPosition; - private long duration; - private long position; - private long bufferedPosition; - private int adGroupCount; - @Nullable private long[] adGroupTimesMs; - @Nullable private boolean[] playedAdGroups; - - private ThumbnailProvider thumbnailUtils; - //TODO put in ressource file - int targetThumbnailHeightInDp = 80; - - public DefaultThumbnailTimeBar(Context context) { - this(context, null); - } - - public DefaultThumbnailTimeBar(Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public DefaultThumbnailTimeBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, attrs); - } - - public DefaultThumbnailTimeBar( - Context context, - @Nullable AttributeSet attrs, - int defStyleAttr, - @Nullable AttributeSet timebarAttrs) { - this(context, attrs, defStyleAttr, timebarAttrs, 0); - } - - // Suppress warnings due to usage of View methods in the constructor. - @SuppressWarnings("nullness:method.invocation") - public DefaultThumbnailTimeBar( - Context context, - @Nullable AttributeSet attrs, - int defStyleAttr, - @Nullable AttributeSet timebarAttrs, - int defStyleRes) { - super(context, attrs, defStyleAttr); - seekBounds = new Rect(); - progressBar = new Rect(); - bufferedBar = new Rect(); - scrubberBar = new Rect(); - playedPaint = new Paint(); - bufferedPaint = new Paint(); - unplayedPaint = new Paint(); - adMarkerPaint = new Paint(); - playedAdMarkerPaint = new Paint(); - scrubberPaint = new Paint(); - scrubberPaint.setAntiAlias(true); - listeners = new CopyOnWriteArraySet<>(); - touchPosition = new Point(); - - // Calculate the dimensions and paints for drawn elements. - Resources res = context.getResources(); - DisplayMetrics displayMetrics = res.getDisplayMetrics(); - density = displayMetrics.density; - fineScrubYThreshold = dpToPx(density, FINE_SCRUB_Y_THRESHOLD_DP); - int defaultBarHeight = dpToPx(density, DEFAULT_BAR_HEIGHT_DP); - int defaultTouchTargetHeight = dpToPx(density, DEFAULT_TOUCH_TARGET_HEIGHT_DP); - int defaultAdMarkerWidth = dpToPx(density, DEFAULT_AD_MARKER_WIDTH_DP); - int defaultScrubberEnabledSize = dpToPx(density, DEFAULT_SCRUBBER_ENABLED_SIZE_DP); - int defaultScrubberDisabledSize = dpToPx(density, DEFAULT_SCRUBBER_DISABLED_SIZE_DP); - int defaultScrubberDraggedSize = dpToPx(density, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP); - if (timebarAttrs != null) { - TypedArray a = - context - .getTheme() - .obtainStyledAttributes( - timebarAttrs, com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar, defStyleAttr, defStyleRes); - try { - scrubberDrawable = a.getDrawable(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_drawable); - if (scrubberDrawable != null) { - setDrawableLayoutDirection(scrubberDrawable); - defaultTouchTargetHeight = - Math.max(scrubberDrawable.getMinimumHeight(), defaultTouchTargetHeight); - } - barHeight = - a.getDimensionPixelSize(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_bar_height, defaultBarHeight); - touchTargetHeight = - a.getDimensionPixelSize( - com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_touch_target_height, defaultTouchTargetHeight); - barGravity = a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_bar_gravity, BAR_GRAVITY_CENTER); - adMarkerWidth = - a.getDimensionPixelSize( - com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_ad_marker_width, defaultAdMarkerWidth); - scrubberEnabledSize = - a.getDimensionPixelSize( - com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_enabled_size, defaultScrubberEnabledSize); - scrubberDisabledSize = - a.getDimensionPixelSize( - com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_disabled_size, defaultScrubberDisabledSize); - scrubberDraggedSize = - a.getDimensionPixelSize( - com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize); - int playedColor = a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR); - int scrubberColor = - a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_color, DEFAULT_SCRUBBER_COLOR); - int bufferedColor = - a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_buffered_color, DEFAULT_BUFFERED_COLOR); - int unplayedColor = - a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_unplayed_color, DEFAULT_UNPLAYED_COLOR); - int adMarkerColor = - a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_ad_marker_color, DEFAULT_AD_MARKER_COLOR); - int playedAdMarkerColor = - a.getInt( - com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_played_ad_marker_color, DEFAULT_PLAYED_AD_MARKER_COLOR); - playedPaint.setColor(playedColor); - scrubberPaint.setColor(scrubberColor); - bufferedPaint.setColor(bufferedColor); - unplayedPaint.setColor(unplayedColor); - adMarkerPaint.setColor(adMarkerColor); - playedAdMarkerPaint.setColor(playedAdMarkerColor); - } finally { - a.recycle(); - } - } else { - barHeight = defaultBarHeight; - touchTargetHeight = defaultTouchTargetHeight; - barGravity = BAR_GRAVITY_CENTER; - adMarkerWidth = defaultAdMarkerWidth; - scrubberEnabledSize = defaultScrubberEnabledSize; - scrubberDisabledSize = defaultScrubberDisabledSize; - scrubberDraggedSize = defaultScrubberDraggedSize; - playedPaint.setColor(DEFAULT_PLAYED_COLOR); - scrubberPaint.setColor(DEFAULT_SCRUBBER_COLOR); - bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR); - unplayedPaint.setColor(DEFAULT_UNPLAYED_COLOR); - adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR); - playedAdMarkerPaint.setColor(DEFAULT_PLAYED_AD_MARKER_COLOR); - scrubberDrawable = null; - } - formatBuilder = new StringBuilder(); - formatter = new Formatter(formatBuilder, Locale.getDefault()); - stopScrubbingRunnable = () -> stopScrubbing(/* canceled= */ false); - if (scrubberDrawable != null) { - scrubberPadding = (scrubberDrawable.getMinimumWidth() + 1) / 2; - } else { - scrubberPadding = - (Math.max(scrubberDisabledSize, Math.max(scrubberEnabledSize, scrubberDraggedSize)) + 1) - / 2; - } - scrubberScale = 1.0f; - scrubberScalingAnimator = new ValueAnimator(); - scrubberScalingAnimator.addUpdateListener( - animation -> { - scrubberScale = (float) animation.getAnimatedValue(); - invalidate(seekBounds); - }); - duration = C.TIME_UNSET; - keyTimeIncrement = C.TIME_UNSET; - keyCountIncrement = DEFAULT_INCREMENT_COUNT; - setFocusable(true); - if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - } - } - - public void setThumbnailUtils(ThumbnailProvider thumbnailUtils) { - this.thumbnailUtils = thumbnailUtils; - } - - /** Shows the scrubber handle. */ - public void showScrubber() { - if (scrubberScalingAnimator.isStarted()) { - scrubberScalingAnimator.cancel(); - } - scrubberPaddingDisabled = false; - scrubberScale = 1; - invalidate(seekBounds); - } - - /** - * Shows the scrubber handle with animation. - * - * @param showAnimationDurationMs The duration for scrubber showing animation. - */ - public void showScrubber(long showAnimationDurationMs) { - if (scrubberScalingAnimator.isStarted()) { - scrubberScalingAnimator.cancel(); - } - scrubberPaddingDisabled = false; - scrubberScalingAnimator.setFloatValues(scrubberScale, SHOWN_SCRUBBER_SCALE); - scrubberScalingAnimator.setDuration(showAnimationDurationMs); - scrubberScalingAnimator.start(); - } - - /** Hides the scrubber handle. */ - public void hideScrubber(boolean disableScrubberPadding) { - if (scrubberScalingAnimator.isStarted()) { - scrubberScalingAnimator.cancel(); - } - scrubberPaddingDisabled = disableScrubberPadding; - scrubberScale = 0; - invalidate(seekBounds); - } - - /** - * Hides the scrubber handle with animation. - * - * @param hideAnimationDurationMs The duration for scrubber hiding animation. - */ - public void hideScrubber(long hideAnimationDurationMs) { - if (scrubberScalingAnimator.isStarted()) { - scrubberScalingAnimator.cancel(); - } - scrubberScalingAnimator.setFloatValues(scrubberScale, HIDDEN_SCRUBBER_SCALE); - scrubberScalingAnimator.setDuration(hideAnimationDurationMs); - scrubberScalingAnimator.start(); - } - - /** - * Sets the color for the portion of the time bar representing media before the playback position. - * - * @param playedColor The color for the portion of the time bar representing media before the - * playback position. - */ - public void setPlayedColor(@ColorInt int playedColor) { - playedPaint.setColor(playedColor); - invalidate(seekBounds); - } - - /** - * Sets the color for the scrubber handle. - * - * @param scrubberColor The color for the scrubber handle. - */ - public void setScrubberColor(@ColorInt int scrubberColor) { - scrubberPaint.setColor(scrubberColor); - invalidate(seekBounds); - } - - /** - * Sets the color for the portion of the time bar after the current played position up to the - * current buffered position. - * - * @param bufferedColor The color for the portion of the time bar after the current played - * position up to the current buffered position. - */ - public void setBufferedColor(@ColorInt int bufferedColor) { - bufferedPaint.setColor(bufferedColor); - invalidate(seekBounds); - } - - /** - * Sets the color for the portion of the time bar after the current played position. - * - * @param unplayedColor The color for the portion of the time bar after the current played - * position. - */ - public void setUnplayedColor(@ColorInt int unplayedColor) { - unplayedPaint.setColor(unplayedColor); - invalidate(seekBounds); - } - - /** - * Sets the color for unplayed ad markers. - * - * @param adMarkerColor The color for unplayed ad markers. - */ - public void setAdMarkerColor(@ColorInt int adMarkerColor) { - adMarkerPaint.setColor(adMarkerColor); - invalidate(seekBounds); - } - - /** - * Sets the color for played ad markers. - * - * @param playedAdMarkerColor The color for played ad markers. - */ - public void setPlayedAdMarkerColor(@ColorInt int playedAdMarkerColor) { - playedAdMarkerPaint.setColor(playedAdMarkerColor); - invalidate(seekBounds); - } - - // TimeBar implementation. - - @Override - public void addListener(OnScrubListener listener) { - Assertions.checkNotNull(listener); - listeners.add(listener); - } - - @Override - public void removeListener(OnScrubListener listener) { - listeners.remove(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) { - if (this.position == position) { - return; - } - this.position = position; - setContentDescription(getProgressText()); - update(); - } - - @Override - public void setBufferedPosition(long bufferedPosition) { - if (this.bufferedPosition == bufferedPosition) { - return; - } - this.bufferedPosition = bufferedPosition; - update(); - } - - @Override - public void setDuration(long duration) { - if (this.duration == duration) { - return; - } - this.duration = duration; - if (scrubbing && duration == C.TIME_UNSET) { - stopScrubbing(/* canceled= */ true); - } - update(); - } - - @Override - public long getPreferredUpdateDelay() { - int timeBarWidthDp = pxToDp(density, progressBar.width()); - return timeBarWidthDp == 0 || duration == 0 || duration == C.TIME_UNSET - ? Long.MAX_VALUE - : duration / timeBarWidthDp; - } - - @Override - public void setAdGroupTimesMs( - @Nullable long[] adGroupTimesMs, @Nullable boolean[] playedAdGroups, int adGroupCount) { - Assertions.checkArgument( - adGroupCount == 0 || (adGroupTimesMs != null && playedAdGroups != null)); - this.adGroupCount = adGroupCount; - this.adGroupTimesMs = adGroupTimesMs; - this.playedAdGroups = playedAdGroups; - update(); - } - - // View methods. - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - if (scrubbing && !enabled) { - stopScrubbing(/* canceled= */ 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)) { - positionScrubber(x); - startScrubbing(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); - } - updateScrubbing(getScrubberPosition()); - update(); - invalidate(); - return true; - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - if (scrubbing) { - stopScrubbing(/* canceled= */ 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) { - stopScrubbing(/* canceled= */ false); - return true; - } - break; - default: - // Do nothing. - } - } - return super.onKeyDown(keyCode, event); - } - - @Override - protected void onFocusChanged( - boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) { - super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); - if (scrubbing && !gainFocus) { - stopScrubbing(/* canceled= */ false); - } - } - - @Override - protected void drawableStateChanged() { - super.drawableStateChanged(); - updateDrawableState(); - } - - @Override - public void jumpDrawablesToCurrentState() { - super.jumpDrawablesToCurrentState(); - if (scrubberDrawable != null) { - scrubberDrawable.jumpToCurrentState(); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int heightMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSize = MeasureSpec.getSize(heightMeasureSpec); - int height = - heightMode == MeasureSpec.UNSPECIFIED - ? touchTargetHeight - : heightMode == MeasureSpec.EXACTLY - ? heightSize - : Math.min(touchTargetHeight, heightSize); - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height); - updateDrawableState(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - int width = right - left; - int height = bottom - top; - int seekLeft = getPaddingLeft(); - int seekRight = width - getPaddingRight(); - int seekBoundsY; - int progressBarY; - int scrubberPadding = scrubberPaddingDisabled ? 0 : this.scrubberPadding; - if (barGravity == BAR_GRAVITY_BOTTOM) { - seekBoundsY = height - getPaddingBottom() - touchTargetHeight; - progressBarY = - height - getPaddingBottom() - barHeight - Math.max(scrubberPadding - (barHeight / 2), 0); - } else { - seekBoundsY = (height - touchTargetHeight) / 2; - progressBarY = (height - barHeight) / 2; - } - seekBounds.set(seekLeft, seekBoundsY, seekRight, seekBoundsY + touchTargetHeight); - progressBar.set( - seekBounds.left + scrubberPadding, - progressBarY, - seekBounds.right - scrubberPadding, - progressBarY + barHeight); - if (Util.SDK_INT >= 29) { - setSystemGestureExclusionRectsV29(width, height); - } - update(); - } - - @Override - public void onRtlPropertiesChanged(int layoutDirection) { - if (scrubberDrawable != null && setDrawableLayoutDirection(scrubberDrawable, layoutDirection)) { - invalidate(); - } - } - - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) { - event.getText().add(getProgressText()); - } - event.setClassName(ACCESSIBILITY_CLASS_NAME); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(ACCESSIBILITY_CLASS_NAME); - 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 { - info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); - info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); - } - } - - @Override - public boolean performAccessibilityAction(int action, @Nullable Bundle args) { - if (super.performAccessibilityAction(action, args)) { - return true; - } - if (duration <= 0) { - return false; - } - if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { - if (scrubIncrementally(-getPositionIncrement())) { - stopScrubbing(/* canceled= */ false); - } - } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { - if (scrubIncrementally(getPositionIncrement())) { - stopScrubbing(/* canceled= */ false); - } - } else { - return false; - } - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - return true; - } - - // Internal methods. - - private void startScrubbing(long scrubPosition) { - this.scrubPosition = scrubPosition; - scrubbing = true; - setPressed(true); - ViewParent parent = getParent(); - if (parent != null) { - parent.requestDisallowInterceptTouchEvent(true); - } - for (OnScrubListener listener : listeners) { - listener.onScrubStart(this, scrubPosition); - } - } - - private void updateScrubbing(long scrubPosition) { - if (this.scrubPosition == scrubPosition) { - return; - } - this.scrubPosition = scrubPosition; - for (OnScrubListener listener : listeners) { - listener.onScrubMove(this, scrubPosition); - } - } - - private void stopScrubbing(boolean canceled) { - removeCallbacks(stopScrubbingRunnable); - scrubbing = false; - setPressed(false); - ViewParent parent = getParent(); - if (parent != null) { - parent.requestDisallowInterceptTouchEvent(false); - } - invalidate(); - for (OnScrubListener listener : listeners) { - listener.onScrubStop(this, scrubPosition, canceled); - } - } - - /** - * 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 previousPosition = scrubbing ? scrubPosition : position; - long scrubPosition = Util.constrainValue(previousPosition + positionChange, 0, duration); - if (scrubPosition == previousPosition) { - return false; - } - if (!scrubbing) { - startScrubbing(scrubPosition); - } else { - updateScrubbing(scrubPosition); - } - update(); - return true; - } - - 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 = Math.min(progressBar.left + bufferedPixelWidth, progressBar.right); - int scrubberPixelPosition = (int) ((progressBar.width() * newScrubberTime) / duration); - scrubberBar.right = Math.min(progressBar.left + scrubberPixelPosition, progressBar.right); - } 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) { - touchPosition.set((int) motionEvent.getX(), (int) motionEvent.getY()); - 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, unplayedPaint); - 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, unplayedPaint); - } - 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, playedPaint); - } - if (adGroupCount == 0) { - return; - } - long[] adGroupTimesMs = Assertions.checkNotNull(this.adGroupTimesMs); - boolean[] playedAdGroups = Assertions.checkNotNull(this.playedAdGroups); - int adMarkerOffset = adMarkerWidth / 2; - for (int i = 0; i < adGroupCount; i++) { - long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration); - int markerPositionOffset = - (int) (progressBar.width() * adGroupTimeMs / duration) - adMarkerOffset; - int markerLeft = - progressBar.left - + Math.min(progressBar.width() - adMarkerWidth, Math.max(0, markerPositionOffset)); - Paint paint = playedAdGroups[i] ? playedAdMarkerPaint : adMarkerPaint; - canvas.drawRect(markerLeft, barTop, markerLeft + adMarkerWidth, barBottom, paint); - } - } - - private void drawPlayhead(Canvas canvas) { - if (duration <= 0) { - return; - } - int playheadX = Util.constrainValue(scrubberBar.right, scrubberBar.left, progressBar.right); - int playheadY = scrubberBar.centerY(); - if (scrubberDrawable == null) { - int scrubberSize = - (scrubbing || isFocused()) - ? scrubberDraggedSize - : (isEnabled() ? scrubberEnabledSize : scrubberDisabledSize); - int playheadRadius = (int) ((scrubberSize * scrubberScale) / 2); - canvas.drawCircle(playheadX, playheadY, playheadRadius, scrubberPaint); - - if (scrubbing) drawThumbnail(canvas, playheadRadius, playheadX, playheadY); - } else { - int scrubberDrawableWidth = (int) (scrubberDrawable.getIntrinsicWidth() * scrubberScale); - int scrubberDrawableHeight = (int) (scrubberDrawable.getIntrinsicHeight() * scrubberScale); - scrubberDrawable.setBounds( - playheadX - scrubberDrawableWidth / 2, - playheadY - scrubberDrawableHeight / 2, - playheadX + scrubberDrawableWidth / 2, - playheadY + scrubberDrawableHeight / 2); - scrubberDrawable.draw(canvas); - } - } - - private void drawThumbnail(Canvas canvas, int playheadRadius, int playheadX, int playheadY) { - - if (thumbnailUtils == null) return; - Bitmap b = thumbnailUtils.getThumbnail(getScrubberPosition()); - - if (b == null) return; - - //adapt thumbnail to desired UI size - double arFactor = (double) b.getWidth() / b.getHeight(); - b = Bitmap.createScaledBitmap(b, dpToPx((int)(targetThumbnailHeightInDp * arFactor)), dpToPx(targetThumbnailHeightInDp), false); - - int width = b.getWidth(); - int height = b.getHeight(); - int offset = (int)width / 2; - - int left = playheadX-offset; - - //handle full left, full right position cases - if (left < 0 ) left = 0; - if (left + width > progressBar.width() + playheadRadius) left = progressBar.width() + playheadRadius - width; - - canvas.drawBitmap(b, left, playheadY-playheadRadius*2-height, null); - } - - private int dpToPx(int dp){ - return (int) (dp * getContext().getResources().getDisplayMetrics().density); - } - - private void updateDrawableState() { - if (scrubberDrawable != null - && scrubberDrawable.isStateful() - && scrubberDrawable.setState(getDrawableState())) { - invalidate(); - } - } - - @RequiresApi(29) - private void setSystemGestureExclusionRectsV29(int width, int height) { - if (lastExclusionRectangle != null - && lastExclusionRectangle.width() == width - && lastExclusionRectangle.height() == height) { - // Allocating inside onLayout is considered a DrawAllocation lint error, so avoid if possible. - return; - } - lastExclusionRectangle = new Rect(/* left= */ 0, /* top= */ 0, width, height); - setSystemGestureExclusionRects(Collections.singletonList(lastExclusionRectangle)); - } - - 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; - } - - private boolean setDrawableLayoutDirection(Drawable drawable) { - return Util.SDK_INT >= 23 && setDrawableLayoutDirection(drawable, getLayoutDirection()); - } - - private static boolean setDrawableLayoutDirection(Drawable drawable, int layoutDirection) { - return Util.SDK_INT >= 23 && drawable.setLayoutDirection(layoutDirection); - } - - private static int dpToPx(float density, int dps) { - return (int) (dps * density + 0.5f); - } - - private static int pxToDp(float density, int px) { - return (int) (px / density); - } -} diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index d7b381baf0..ae01315f12 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -75,7 +75,6 @@ public class PlayerActivity extends AppCompatActivity protected LinearLayout debugRootView; protected TextView debugTextView; protected @Nullable ExoPlayer player; - DefaultThumbnailTimeBar timeBar; private boolean isShowingTrackSelectionDialog; private Button selectTracksButton; @@ -117,9 +116,6 @@ public class PlayerActivity extends AppCompatActivity playerView.setControllerVisibilityListener(this); playerView.setErrorMessageProvider(new PlayerErrorMessageProvider()); playerView.requestFocus(); - - timeBar = playerView.findViewById(R.id.exo_progress); - if (savedInstanceState != null) { trackSelectionParameters = TrackSelectionParameters.fromBundle( @@ -285,7 +281,7 @@ public class PlayerActivity extends AppCompatActivity player.setPlayWhenReady(startAutoPlay); playerView.setPlayer(player); configurePlayerWithServerSideAdsLoader(); - timeBar.setThumbnailUtils(new DefaultThumbnailProvider(player, timeBar)); + debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/ThumbnailProvider.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/ThumbnailProvider.java deleted file mode 100644 index 129fc84b16..0000000000 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/ThumbnailProvider.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.google.android.exoplayer2.demo; - -import android.graphics.Bitmap; - -public interface ThumbnailProvider { - - public Bitmap getThumbnail(long position); - -} diff --git a/demos/main/src/main/res/layout/exo_styled_player_control_view.xml b/demos/main/src/main/res/layout/exo_styled_player_control_view.xml deleted file mode 100644 index 2a6ae481da..0000000000 --- a/demos/main/src/main/res/layout/exo_styled_player_control_view.xml +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/library/core/src/main/java/com/google/android/exoplayer2/thumbnail/ThumbnailDescription.java b/library/core/src/main/java/com/google/android/exoplayer2/thumbnail/ThumbnailDescription.java index cfd1f6e8fe..2323449fb8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/thumbnail/ThumbnailDescription.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/thumbnail/ThumbnailDescription.java @@ -1,11 +1,12 @@ package com.google.android.exoplayer2.thumbnail; import android.net.Uri; +import androidx.annotation.NonNull; public class ThumbnailDescription { - private final String id; - private final Uri uri; + @NonNull private final String id; + @NonNull private final Uri uri; private final int bitrate; private final int tileCountHorizontal; private final int tileCountVertical; @@ -14,7 +15,7 @@ public class ThumbnailDescription { private final int imageWidth; // Image width (Pixel) private final int imageHeight; // Image height (Pixel) - public ThumbnailDescription(String id, Uri uri, int bitrate, int tileCountHorizontal, int tileCountVertical, long startTimeMs, long durationMs, int imageWidth, int imageHeight) { + public ThumbnailDescription(@NonNull String id, @NonNull Uri uri, int bitrate, int tileCountHorizontal, int tileCountVertical, long startTimeMs, long durationMs, int imageWidth, int imageHeight) { this.id = id; this.uri = uri; this.bitrate = bitrate; @@ -26,6 +27,12 @@ public class ThumbnailDescription { this.imageHeight = imageHeight; } + @NonNull + public String getId() { + return id; + } + + @NonNull public Uri getUri() { return uri; } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index e44306a17e..2ea76b944d 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -182,6 +182,9 @@ public class DashManifest implements FilterableManifest { } String id = representation.format.id; + if (id == null) { + continue; + } int bitrate = representation.format.bitrate; int imageWidth = representation.format.width; int imageHeight = representation.format.height; From a25022264309e2eed1e439c9ee84114c363eaa4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6rkem=20G=C3=BCcl=C3=BC?= Date: Mon, 16 Jan 2023 12:17:19 +0100 Subject: [PATCH 6/6] Removed thumbnails example streams from media.exolist.json --- demos/main/src/main/assets/media.exolist.json | 17 ----------------- .../android/exoplayer2/demo/PlayerActivity.java | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index ce1528c19d..ac7b5ce749 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -631,23 +631,6 @@ } ] }, - { - "name": "Thumbnails", - "samples": [ - { - "name": "Single adaptation set, 7 tiles at 10x1, each thumb 320x180", - "uri": "https://dash.akamaized.net/akamai/bbb_30fps/bbb_with_tiled_thumbnails.mpd" - }, - { - "name": "Single adaptation set, SegmentTemplate with SegmentTimeline", - "uri": "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-tiled-thumbnails-timeline.ism/.mpd" - }, - { - "name": "Single adaptation set, SegmentTemplate with SegmentNumber", - "uri": "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-tiled-thumbnails-numbered.ism/.mpd" - } - ] - }, { "name": "Misc", "samples": [ diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index ae01315f12..8932b0780d 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -116,6 +116,7 @@ public class PlayerActivity extends AppCompatActivity playerView.setControllerVisibilityListener(this); playerView.setErrorMessageProvider(new PlayerErrorMessageProvider()); playerView.requestFocus(); + if (savedInstanceState != null) { trackSelectionParameters = TrackSelectionParameters.fromBundle( @@ -281,7 +282,6 @@ public class PlayerActivity extends AppCompatActivity player.setPlayWhenReady(startAutoPlay); playerView.setPlayer(player); configurePlayerWithServerSideAdsLoader(); - debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); }