Commit 1fdc11f2 by ojw28 Committed by GitHub

Merge pull request #3277 from google/dev-v2-r2.5.3

r2.5.3
parents 9a026671 a8136cbb
# Release notes # # Release notes #
### r2.5.3 ###
* IMA extension: Support skipping of skippable ads on AndroidTV and other
non-touch devices ([#3258](https://github.com/google/ExoPlayer/issues/3258)).
* HLS: Fix broken WebVTT captions when PTS wraps around
([#2928](https://github.com/google/ExoPlayer/issues/2928)).
* Captions: Fix issues rendering CEA-608 captions
([#3250](https://github.com/google/ExoPlayer/issues/3250)).
* Workaround broken AAC decoders on Galaxy S6
([#3249](https://github.com/google/ExoPlayer/issues/3249)).
* Caching: Fix infinite loop when cache eviction fails
([#3260](https://github.com/google/ExoPlayer/issues/3260)).
* Caching: Force use of BouncyCastle on JellyBean to fix decryption issue
([#2755](https://github.com/google/ExoPlayer/issues/2755)).
### r2.5.2 ### ### r2.5.2 ###
* IMA extension: Fix issue where ad playback could end prematurely for some * IMA extension: Fix issue where ad playback could end prematurely for some
......
...@@ -24,7 +24,7 @@ project.ext { ...@@ -24,7 +24,7 @@ project.ext {
supportLibraryVersion = '25.4.0' supportLibraryVersion = '25.4.0'
dexmakerVersion = '1.2' dexmakerVersion = '1.2'
mockitoVersion = '1.9.5' mockitoVersion = '1.9.5'
releaseVersion = 'r2.5.2' releaseVersion = 'r2.5.3'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix
......
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo" package="com.google.android.exoplayer2.demo"
android:versionCode="2502" android:versionCode="2503"
android:versionName="2.5.2"> android:versionName="2.5.3">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
......
...@@ -20,6 +20,7 @@ import android.net.Uri; ...@@ -20,6 +20,7 @@ import android.net.Uri;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.webkit.WebView;
import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent; import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
...@@ -112,6 +113,14 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ...@@ -112,6 +113,14 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
*/ */
private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000; private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000;
/**
* The "Skip ad" button rendered in the IMA WebView does not gain focus by default and cannot be
* clicked via a keypress event. Workaround this issue by calling focus() on the HTML element in
* the WebView directly when an ad starts. See [Internal: b/62371030].
*/
private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:"
+ "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}";
private final Uri adTagUri; private final Uri adTagUri;
private final Timeline.Period period; private final Timeline.Period period;
private final List<VideoAdPlayerCallback> adCallbacks; private final List<VideoAdPlayerCallback> adCallbacks;
...@@ -121,6 +130,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ...@@ -121,6 +130,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
private EventListener eventListener; private EventListener eventListener;
private Player player; private Player player;
private ViewGroup adUiViewGroup;
private VideoProgressUpdate lastContentProgress; private VideoProgressUpdate lastContentProgress;
private VideoProgressUpdate lastAdProgress; private VideoProgressUpdate lastAdProgress;
...@@ -249,6 +259,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ...@@ -249,6 +259,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
ViewGroup adUiViewGroup) { ViewGroup adUiViewGroup) {
this.player = player; this.player = player;
this.eventListener = eventListener; this.eventListener = eventListener;
this.adUiViewGroup = adUiViewGroup;
lastAdProgress = null; lastAdProgress = null;
lastContentProgress = null; lastContentProgress = null;
adDisplayContainer.setAdContainer(adUiViewGroup); adDisplayContainer.setAdContainer(adUiViewGroup);
...@@ -278,6 +289,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ...@@ -278,6 +289,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
player.removeListener(this); player.removeListener(this);
player = null; player = null;
eventListener = null; eventListener = null;
adUiViewGroup = null;
} }
/** /**
...@@ -363,6 +375,11 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ...@@ -363,6 +375,11 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
imaPausedContent = true; imaPausedContent = true;
pauseContentInternal(); pauseContentInternal();
break; break;
case STARTED:
if (ad.isSkippable()) {
focusSkipButton();
}
break;
case TAPPED: case TAPPED:
if (eventListener != null) { if (eventListener != null) {
eventListener.onAdTapped(); eventListener.onAdTapped();
...@@ -727,4 +744,13 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ...@@ -727,4 +744,13 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
return adGroupTimesUs; return adGroupTimesUs;
} }
private void focusSkipButton() {
if (playingAd && adUiViewGroup != null && adUiViewGroup.getChildCount() > 0
&& adUiViewGroup.getChildAt(0) instanceof WebView) {
WebView webView = (WebView) (adUiViewGroup.getChildAt(0));
webView.requestFocus();
webView.loadUrl(FOCUS_SKIP_BUTTON_WORKAROUND_JS);
}
}
} }
...@@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { ...@@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo {
* The version of the library expressed as a string, for example "1.2.3". * The version of the library expressed as a string, for example "1.2.3".
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.5.2"; public static final String VERSION = "2.5.3";
/** /**
* The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.2"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.3";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
...@@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2005002; public static final int VERSION_INT = 2005003;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -325,7 +325,22 @@ public final class MediaCodecUtil { ...@@ -325,7 +325,22 @@ public final class MediaCodecUtil {
return false; return false;
} }
// Work around https://github.com/google/ExoPlayer/issues/548 // Work around https://github.com/google/ExoPlayer/issues/3249.
if (Util.SDK_INT < 24
&& ("OMX.SEC.aac.dec".equals(name) || "OMX.Exynos.AAC.Decoder".equals(name))
&& Util.MANUFACTURER.equals("samsung")
&& (Util.DEVICE.startsWith("zeroflte") // Galaxy S6
|| Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge
|| Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+
|| Util.DEVICE.equals("SC-05G") // Galaxy S6
|| Util.DEVICE.equals("marinelteatt") // Galaxy S6 Active
|| Util.DEVICE.equals("404SC") // Galaxy S6 Edge
|| Util.DEVICE.equals("SC-04G")
|| Util.DEVICE.equals("SCV31"))) {
return false;
}
// Work around https://github.com/google/ExoPlayer/issues/548.
// VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3/Note 2 does not render video. // VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3/Note 2 does not render video.
if (Util.SDK_INT <= 19 if (Util.SDK_INT <= 19
&& "OMX.SEC.vp8.dec".equals(name) && "samsung".equals(Util.MANUFACTURER) && "OMX.SEC.vp8.dec".equals(name) && "samsung".equals(Util.MANUFACTURER)
......
...@@ -37,6 +37,9 @@ public final class SubtitleInputBuffer extends DecoderInputBuffer ...@@ -37,6 +37,9 @@ public final class SubtitleInputBuffer extends DecoderInputBuffer
@Override @Override
public int compareTo(@NonNull SubtitleInputBuffer other) { public int compareTo(@NonNull SubtitleInputBuffer other) {
if (isEndOfStream() != other.isEndOfStream()) {
return isEndOfStream() ? 1 : -1;
}
long delta = timeUs - other.timeUs; long delta = timeUs - other.timeUs;
if (delta == 0) { if (delta == 0) {
return 0; return 0;
......
...@@ -294,9 +294,9 @@ public final class TextRenderer extends BaseRenderer implements Callback { ...@@ -294,9 +294,9 @@ public final class TextRenderer extends BaseRenderer implements Callback {
} }
private long getNextEventTime() { private long getNextEventTime() {
return ((nextSubtitleEventIndex == C.INDEX_UNSET) return nextSubtitleEventIndex == C.INDEX_UNSET
|| (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE || nextSubtitleEventIndex >= subtitle.getEventTimeCount()
: (subtitle.getEventTime(nextSubtitleEventIndex)); ? Long.MAX_VALUE : subtitle.getEventTime(nextSubtitleEventIndex);
} }
private void updateOutput(List<Cue> cues) { private void updateOutput(List<Cue> cues) {
......
...@@ -24,7 +24,7 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer; ...@@ -24,7 +24,7 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.text.SubtitleOutputBuffer; import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.TreeSet; import java.util.PriorityQueue;
/** /**
* Base class for subtitle parsers for CEA captions. * Base class for subtitle parsers for CEA captions.
...@@ -36,7 +36,7 @@ import java.util.TreeSet; ...@@ -36,7 +36,7 @@ import java.util.TreeSet;
private final LinkedList<SubtitleInputBuffer> availableInputBuffers; private final LinkedList<SubtitleInputBuffer> availableInputBuffers;
private final LinkedList<SubtitleOutputBuffer> availableOutputBuffers; private final LinkedList<SubtitleOutputBuffer> availableOutputBuffers;
private final TreeSet<SubtitleInputBuffer> queuedInputBuffers; private final PriorityQueue<SubtitleInputBuffer> queuedInputBuffers;
private SubtitleInputBuffer dequeuedInputBuffer; private SubtitleInputBuffer dequeuedInputBuffer;
private long playbackPositionUs; private long playbackPositionUs;
...@@ -50,7 +50,7 @@ import java.util.TreeSet; ...@@ -50,7 +50,7 @@ import java.util.TreeSet;
for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) { for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) {
availableOutputBuffers.add(new CeaOutputBuffer(this)); availableOutputBuffers.add(new CeaOutputBuffer(this));
} }
queuedInputBuffers = new TreeSet<>(); queuedInputBuffers = new PriorityQueue<>();
} }
@Override @Override
...@@ -73,7 +73,6 @@ import java.util.TreeSet; ...@@ -73,7 +73,6 @@ import java.util.TreeSet;
@Override @Override
public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException { public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException {
Assertions.checkArgument(inputBuffer != null);
Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); Assertions.checkArgument(inputBuffer == dequeuedInputBuffer);
if (inputBuffer.isDecodeOnly()) { if (inputBuffer.isDecodeOnly()) {
// We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow
...@@ -90,13 +89,12 @@ import java.util.TreeSet; ...@@ -90,13 +89,12 @@ import java.util.TreeSet;
if (availableOutputBuffers.isEmpty()) { if (availableOutputBuffers.isEmpty()) {
return null; return null;
} }
// iterate through all available input buffers whose timestamps are less than or equal // iterate through all available input buffers whose timestamps are less than or equal
// to the current playback position; processing input buffers for future content should // to the current playback position; processing input buffers for future content should
// be deferred until they would be applicable // be deferred until they would be applicable
while (!queuedInputBuffers.isEmpty() while (!queuedInputBuffers.isEmpty()
&& queuedInputBuffers.first().timeUs <= playbackPositionUs) { && queuedInputBuffers.peek().timeUs <= playbackPositionUs) {
SubtitleInputBuffer inputBuffer = queuedInputBuffers.pollFirst(); SubtitleInputBuffer inputBuffer = queuedInputBuffers.poll();
// If the input buffer indicates we've reached the end of the stream, we can // If the input buffer indicates we've reached the end of the stream, we can
// return immediately with an output buffer propagating that // return immediately with an output buffer propagating that
...@@ -142,7 +140,7 @@ import java.util.TreeSet; ...@@ -142,7 +140,7 @@ import java.util.TreeSet;
public void flush() { public void flush() {
playbackPositionUs = 0; playbackPositionUs = 0;
while (!queuedInputBuffers.isEmpty()) { while (!queuedInputBuffers.isEmpty()) {
releaseInputBuffer(queuedInputBuffers.pollFirst()); releaseInputBuffer(queuedInputBuffers.poll());
} }
if (dequeuedInputBuffer != null) { if (dequeuedInputBuffer != null) {
releaseInputBuffer(dequeuedInputBuffer); releaseInputBuffer(dequeuedInputBuffer);
......
...@@ -49,7 +49,7 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -49,7 +49,7 @@ import javax.crypto.spec.SecretKeySpec;
/** /**
* This class maintains the index of cached content. * This class maintains the index of cached content.
*/ */
/*package*/ final class CachedContentIndex { /*package*/ class CachedContentIndex {
public static final String FILE_NAME = "cached_content_index.exi"; public static final String FILE_NAME = "cached_content_index.exi";
...@@ -99,7 +99,7 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -99,7 +99,7 @@ import javax.crypto.spec.SecretKeySpec;
if (secretKey != null) { if (secretKey != null) {
Assertions.checkArgument(secretKey.length == 16); Assertions.checkArgument(secretKey.length == 16);
try { try {
cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher = getCipher();
secretKeySpec = new SecretKeySpec(secretKey, "AES"); secretKeySpec = new SecretKeySpec(secretKey, "AES");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) { } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e); // Should never happen. throw new IllegalStateException(e); // Should never happen.
...@@ -354,6 +354,18 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -354,6 +354,18 @@ import javax.crypto.spec.SecretKeySpec;
return cachedContent; return cachedContent;
} }
private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
// Workaround for https://issuetracker.google.com/issues/36976726
if (Util.SDK_INT == 18) {
try {
return Cipher.getInstance("AES/CBC/PKCS5PADDING", "BC");
} catch (Throwable ignored) {
// ignored
}
}
return Cipher.getInstance("AES/CBC/PKCS5PADDING");
}
/** /**
* Returns an id which isn't used in the given array. If the maximum id in the array is smaller * Returns an id which isn't used in the given array. If the maximum id in the array is smaller
* than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it * than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it
......
...@@ -74,7 +74,7 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar ...@@ -74,7 +74,7 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar
} }
private void evictCache(Cache cache, long requiredSpace) { private void evictCache(Cache cache, long requiredSpace) {
while (currentSize + requiredSpace > maxBytes) { while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) {
try { try {
cache.removeSpan(leastRecentlyUsed.first()); cache.removeSpan(leastRecentlyUsed.first());
} catch (CacheException e) { } catch (CacheException e) {
......
...@@ -48,7 +48,7 @@ public final class SimpleCache implements Cache { ...@@ -48,7 +48,7 @@ public final class SimpleCache implements Cache {
* @param evictor The evictor to be used. * @param evictor The evictor to be used.
*/ */
public SimpleCache(File cacheDir, CacheEvictor evictor) { public SimpleCache(File cacheDir, CacheEvictor evictor) {
this(cacheDir, evictor, null); this(cacheDir, evictor, null, false);
} }
/** /**
...@@ -75,10 +75,22 @@ public final class SimpleCache implements Cache { ...@@ -75,10 +75,22 @@ public final class SimpleCache implements Cache {
* @param encrypt When false, a plaintext index will be written. * @param encrypt When false, a plaintext index will be written.
*/ */
public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey, boolean encrypt) { public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey, boolean encrypt) {
this(cacheDir, evictor, new CachedContentIndex(cacheDir, secretKey, encrypt));
}
/**
* Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
* the directory cannot be used to store other files.
*
* @param cacheDir A dedicated cache directory.
* @param evictor The evictor to be used.
* @param index The CachedContentIndex to be used.
*/
/*package*/ SimpleCache(File cacheDir, CacheEvictor evictor, CachedContentIndex index) {
this.cacheDir = cacheDir; this.cacheDir = cacheDir;
this.evictor = evictor; this.evictor = evictor;
this.lockedSpans = new HashMap<>(); this.lockedSpans = new HashMap<>();
this.index = new CachedContentIndex(cacheDir, secretKey, encrypt); this.index = index;
this.listeners = new HashMap<>(); this.listeners = new HashMap<>();
// Start cache initialization. // Start cache initialization.
final ConditionVariable conditionVariable = new ConditionVariable(); final ConditionVariable conditionVariable = new ConditionVariable();
...@@ -305,11 +317,14 @@ public final class SimpleCache implements Cache { ...@@ -305,11 +317,14 @@ public final class SimpleCache implements Cache {
return; return;
} }
totalSpace -= span.length; totalSpace -= span.length;
if (removeEmptyCachedContent && cachedContent.isEmpty()) { try {
index.removeEmpty(cachedContent.key); if (removeEmptyCachedContent && cachedContent.isEmpty()) {
index.store(); index.removeEmpty(cachedContent.key);
index.store();
}
} finally {
notifySpanRemoved(span);
} }
notifySpanRemoved(span);
} }
@Override @Override
......
...@@ -141,8 +141,7 @@ import java.util.regex.Pattern; ...@@ -141,8 +141,7 @@ import java.util.regex.Pattern;
throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestamp: " + line); throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestamp: " + line);
} }
vttTimestampUs = WebvttParserUtil.parseTimestampUs(localTimestampMatcher.group(1)); vttTimestampUs = WebvttParserUtil.parseTimestampUs(localTimestampMatcher.group(1));
tsTimestampUs = TimestampAdjuster.ptsToUs( tsTimestampUs = TimestampAdjuster.ptsToUs(Long.parseLong(mediaTimestampMatcher.group(1)));
Long.parseLong(mediaTimestampMatcher.group(1)));
} }
} }
...@@ -155,8 +154,8 @@ import java.util.regex.Pattern; ...@@ -155,8 +154,8 @@ import java.util.regex.Pattern;
} }
long firstCueTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)); long firstCueTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1));
long sampleTimeUs = timestampAdjuster.adjustSampleTimestamp( long sampleTimeUs = timestampAdjuster.adjustTsTimestamp(
firstCueTimeUs + tsTimestampUs - vttTimestampUs); TimestampAdjuster.usToPts(firstCueTimeUs + tsTimestampUs - vttTimestampUs));
long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs; long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs;
// Output the track. // Output the track.
TrackOutput trackOutput = buildTrackOutput(subsampleOffsetUs); TrackOutput trackOutput = buildTrackOutput(subsampleOffsetUs);
......
...@@ -996,30 +996,30 @@ public class PlaybackControlView extends FrameLayout { ...@@ -996,30 +996,30 @@ public class PlaybackControlView extends FrameLayout {
return false; return false;
} }
if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (keyCode) { if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: fastForward();
fastForward(); } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {
break; rewind();
case KeyEvent.KEYCODE_MEDIA_REWIND: } else if (event.getRepeatCount() == 0) {
rewind(); switch (keyCode) {
break; case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());
controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); break;
break; case KeyEvent.KEYCODE_MEDIA_PLAY:
case KeyEvent.KEYCODE_MEDIA_PLAY: controlDispatcher.dispatchSetPlayWhenReady(player, true);
controlDispatcher.dispatchSetPlayWhenReady(player, true); break;
break; case KeyEvent.KEYCODE_MEDIA_PAUSE:
case KeyEvent.KEYCODE_MEDIA_PAUSE: controlDispatcher.dispatchSetPlayWhenReady(player, false);
controlDispatcher.dispatchSetPlayWhenReady(player, false); break;
break; case KeyEvent.KEYCODE_MEDIA_NEXT:
case KeyEvent.KEYCODE_MEDIA_NEXT: next();
next(); break;
break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
case KeyEvent.KEYCODE_MEDIA_PREVIOUS: previous();
previous(); break;
break; default:
default: break;
break; }
} }
} }
return true; return true;
......
...@@ -515,6 +515,13 @@ public final class SimpleExoPlayerView extends FrameLayout { ...@@ -515,6 +515,13 @@ public final class SimpleExoPlayerView extends FrameLayout {
@Override @Override
public boolean dispatchKeyEvent(KeyEvent event) { public boolean dispatchKeyEvent(KeyEvent event) {
if (player != null && player.isPlayingAd()) {
// Focus any overlay UI now, in case it's provided by a WebView whose contents may update
// dynamically. This is needed to make the "Skip ad" button focused on Android TV when using
// IMA [Internal: b/62371030].
overlayFrameLayout.requestFocus();
return super.dispatchKeyEvent(event);
}
maybeShowController(true); maybeShowController(true);
return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment