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 #
### 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 ###
* IMA extension: Fix issue where ad playback could end prematurely for some
......
......@@ -24,7 +24,7 @@ project.ext {
supportLibraryVersion = '25.4.0'
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
releaseVersion = 'r2.5.2'
releaseVersion = 'r2.5.3'
modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix
......
......@@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo"
android:versionCode="2502"
android:versionName="2.5.2">
android:versionCode="2503"
android:versionName="2.5.3">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
......
......@@ -20,6 +20,7 @@ import android.net.Uri;
import android.os.SystemClock;
import android.util.Log;
import android.view.ViewGroup;
import android.webkit.WebView;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
......@@ -112,6 +113,14 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
*/
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 Timeline.Period period;
private final List<VideoAdPlayerCallback> adCallbacks;
......@@ -121,6 +130,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
private EventListener eventListener;
private Player player;
private ViewGroup adUiViewGroup;
private VideoProgressUpdate lastContentProgress;
private VideoProgressUpdate lastAdProgress;
......@@ -249,6 +259,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
ViewGroup adUiViewGroup) {
this.player = player;
this.eventListener = eventListener;
this.adUiViewGroup = adUiViewGroup;
lastAdProgress = null;
lastContentProgress = null;
adDisplayContainer.setAdContainer(adUiViewGroup);
......@@ -278,6 +289,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
player.removeListener(this);
player = null;
eventListener = null;
adUiViewGroup = null;
}
/**
......@@ -363,6 +375,11 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
imaPausedContent = true;
pauseContentInternal();
break;
case STARTED:
if (ad.isSkippable()) {
focusSkipButton();
}
break;
case TAPPED:
if (eventListener != null) {
eventListener.onAdTapped();
......@@ -727,4 +744,13 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
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 {
* 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.
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}.
*/
// 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.
......@@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// 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}
......
......@@ -325,7 +325,22 @@ public final class MediaCodecUtil {
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.
if (Util.SDK_INT <= 19
&& "OMX.SEC.vp8.dec".equals(name) && "samsung".equals(Util.MANUFACTURER)
......
......@@ -37,6 +37,9 @@ public final class SubtitleInputBuffer extends DecoderInputBuffer
@Override
public int compareTo(@NonNull SubtitleInputBuffer other) {
if (isEndOfStream() != other.isEndOfStream()) {
return isEndOfStream() ? 1 : -1;
}
long delta = timeUs - other.timeUs;
if (delta == 0) {
return 0;
......
......@@ -294,9 +294,9 @@ public final class TextRenderer extends BaseRenderer implements Callback {
}
private long getNextEventTime() {
return ((nextSubtitleEventIndex == C.INDEX_UNSET)
|| (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE
: (subtitle.getEventTime(nextSubtitleEventIndex));
return nextSubtitleEventIndex == C.INDEX_UNSET
|| nextSubtitleEventIndex >= subtitle.getEventTimeCount()
? Long.MAX_VALUE : subtitle.getEventTime(nextSubtitleEventIndex);
}
private void updateOutput(List<Cue> cues) {
......
......@@ -24,7 +24,7 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import java.util.LinkedList;
import java.util.TreeSet;
import java.util.PriorityQueue;
/**
* Base class for subtitle parsers for CEA captions.
......@@ -36,7 +36,7 @@ import java.util.TreeSet;
private final LinkedList<SubtitleInputBuffer> availableInputBuffers;
private final LinkedList<SubtitleOutputBuffer> availableOutputBuffers;
private final TreeSet<SubtitleInputBuffer> queuedInputBuffers;
private final PriorityQueue<SubtitleInputBuffer> queuedInputBuffers;
private SubtitleInputBuffer dequeuedInputBuffer;
private long playbackPositionUs;
......@@ -50,7 +50,7 @@ import java.util.TreeSet;
for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) {
availableOutputBuffers.add(new CeaOutputBuffer(this));
}
queuedInputBuffers = new TreeSet<>();
queuedInputBuffers = new PriorityQueue<>();
}
@Override
......@@ -73,7 +73,6 @@ import java.util.TreeSet;
@Override
public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException {
Assertions.checkArgument(inputBuffer != null);
Assertions.checkArgument(inputBuffer == dequeuedInputBuffer);
if (inputBuffer.isDecodeOnly()) {
// 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;
if (availableOutputBuffers.isEmpty()) {
return null;
}
// 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
// be deferred until they would be applicable
while (!queuedInputBuffers.isEmpty()
&& queuedInputBuffers.first().timeUs <= playbackPositionUs) {
SubtitleInputBuffer inputBuffer = queuedInputBuffers.pollFirst();
&& queuedInputBuffers.peek().timeUs <= playbackPositionUs) {
SubtitleInputBuffer inputBuffer = queuedInputBuffers.poll();
// If the input buffer indicates we've reached the end of the stream, we can
// return immediately with an output buffer propagating that
......@@ -142,7 +140,7 @@ import java.util.TreeSet;
public void flush() {
playbackPositionUs = 0;
while (!queuedInputBuffers.isEmpty()) {
releaseInputBuffer(queuedInputBuffers.pollFirst());
releaseInputBuffer(queuedInputBuffers.poll());
}
if (dequeuedInputBuffer != null) {
releaseInputBuffer(dequeuedInputBuffer);
......
......@@ -49,7 +49,7 @@ import javax.crypto.spec.SecretKeySpec;
/**
* 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";
......@@ -99,7 +99,7 @@ import javax.crypto.spec.SecretKeySpec;
if (secretKey != null) {
Assertions.checkArgument(secretKey.length == 16);
try {
cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher = getCipher();
secretKeySpec = new SecretKeySpec(secretKey, "AES");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e); // Should never happen.
......@@ -354,6 +354,18 @@ import javax.crypto.spec.SecretKeySpec;
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
* 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
}
private void evictCache(Cache cache, long requiredSpace) {
while (currentSize + requiredSpace > maxBytes) {
while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) {
try {
cache.removeSpan(leastRecentlyUsed.first());
} catch (CacheException e) {
......
......@@ -48,7 +48,7 @@ public final class SimpleCache implements Cache {
* @param evictor The evictor to be used.
*/
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 {
* @param encrypt When false, a plaintext index will be written.
*/
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.evictor = evictor;
this.lockedSpans = new HashMap<>();
this.index = new CachedContentIndex(cacheDir, secretKey, encrypt);
this.index = index;
this.listeners = new HashMap<>();
// Start cache initialization.
final ConditionVariable conditionVariable = new ConditionVariable();
......@@ -305,11 +317,14 @@ public final class SimpleCache implements Cache {
return;
}
totalSpace -= span.length;
if (removeEmptyCachedContent && cachedContent.isEmpty()) {
index.removeEmpty(cachedContent.key);
index.store();
try {
if (removeEmptyCachedContent && cachedContent.isEmpty()) {
index.removeEmpty(cachedContent.key);
index.store();
}
} finally {
notifySpanRemoved(span);
}
notifySpanRemoved(span);
}
@Override
......
......@@ -141,8 +141,7 @@ import java.util.regex.Pattern;
throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestamp: " + line);
}
vttTimestampUs = WebvttParserUtil.parseTimestampUs(localTimestampMatcher.group(1));
tsTimestampUs = TimestampAdjuster.ptsToUs(
Long.parseLong(mediaTimestampMatcher.group(1)));
tsTimestampUs = TimestampAdjuster.ptsToUs(Long.parseLong(mediaTimestampMatcher.group(1)));
}
}
......@@ -155,8 +154,8 @@ import java.util.regex.Pattern;
}
long firstCueTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1));
long sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(
firstCueTimeUs + tsTimestampUs - vttTimestampUs);
long sampleTimeUs = timestampAdjuster.adjustTsTimestamp(
TimestampAdjuster.usToPts(firstCueTimeUs + tsTimestampUs - vttTimestampUs));
long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs;
// Output the track.
TrackOutput trackOutput = buildTrackOutput(subsampleOffsetUs);
......
......@@ -996,30 +996,30 @@ public class PlaybackControlView extends FrameLayout {
return false;
}
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
fastForward();
break;
case KeyEvent.KEYCODE_MEDIA_REWIND:
rewind();
break;
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
controlDispatcher.dispatchSetPlayWhenReady(player, true);
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, false);
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
next();
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
previous();
break;
default:
break;
if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
fastForward();
} else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {
rewind();
} else if (event.getRepeatCount() == 0) {
switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
controlDispatcher.dispatchSetPlayWhenReady(player, true);
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, false);
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
next();
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
previous();
break;
default:
break;
}
}
}
return true;
......
......@@ -515,6 +515,13 @@ public final class SimpleExoPlayerView extends FrameLayout {
@Override
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);
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