From 3f3191bb7590bf2aa3d504176fb974d6dff8a9f7 Mon Sep 17 00:00:00 2001 From: Justin Yorke Date: Thu, 19 Apr 2018 17:08:16 -0700 Subject: [PATCH 01/40] OkHttp extension - properly use response headers instead of request headers when throwing InvalidResponseCodeException --- .../google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 0519673e50..4094b3a5d2 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -170,7 +170,7 @@ public class OkHttpDataSource implements HttpDataSource { // Check for a valid response code. if (!response.isSuccessful()) { - Map> headers = request.headers().toMultimap(); + Map> headers = response.headers().toMultimap(); closeConnectionQuietly(); InvalidResponseCodeException exception = new InvalidResponseCodeException( responseCode, headers, dataSpec); From 693fe2841d7844e990cb7a491b900593a1649c9b Mon Sep 17 00:00:00 2001 From: cdotchen Date: Tue, 24 Apr 2018 22:05:45 +0800 Subject: [PATCH 02/40] fix timebar scrubber notify wrong start position --- .../java/com/google/android/exoplayer2/ui/DefaultTimeBar.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index a15ee1b88e..3866cf704a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -408,8 +408,8 @@ public class DefaultTimeBar extends View implements TimeBar { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (isInSeekBar(x, y)) { - startScrubbing(); positionScrubber(x); + startScrubbing(); scrubPosition = getScrubberPosition(); update(); invalidate(); From 81c37698807c9be6e001ca1dcc1d619c9d60f54a Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 9 May 2018 03:28:00 -0700 Subject: [PATCH 03/40] Add missing @Nullable to equals implementations. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=195947382 --- .../google/android/exoplayer2/ext/cast/CastTimeline.java | 3 ++- .../android/exoplayer2/ext/okhttp/OkHttpDataSource.java | 2 +- .../google/android/exoplayer2/PlaybackParameters.java | 3 ++- .../google/android/exoplayer2/RendererConfiguration.java | 4 +++- .../com/google/android/exoplayer2/SeekParameters.java | 3 ++- .../google/android/exoplayer2/audio/AudioAttributes.java | 3 ++- .../android/exoplayer2/audio/AudioCapabilities.java | 3 ++- .../com/google/android/exoplayer2/drm/DrmInitData.java | 4 ++-- .../com/google/android/exoplayer2/extractor/SeekMap.java | 3 ++- .../google/android/exoplayer2/extractor/SeekPoint.java | 4 +++- .../google/android/exoplayer2/extractor/TrackOutput.java | 3 ++- .../android/exoplayer2/mediacodec/MediaCodecUtil.java | 2 +- .../com/google/android/exoplayer2/metadata/Metadata.java | 3 ++- .../android/exoplayer2/metadata/emsg/EventMessage.java | 3 ++- .../android/exoplayer2/metadata/id3/ApicFrame.java | 3 ++- .../android/exoplayer2/metadata/id3/BinaryFrame.java | 3 ++- .../android/exoplayer2/metadata/id3/ChapterFrame.java | 3 ++- .../android/exoplayer2/metadata/id3/ChapterTocFrame.java | 3 ++- .../android/exoplayer2/metadata/id3/CommentFrame.java | 3 ++- .../android/exoplayer2/metadata/id3/GeobFrame.java | 3 ++- .../android/exoplayer2/metadata/id3/PrivFrame.java | 3 ++- .../exoplayer2/metadata/id3/TextInformationFrame.java | 3 ++- .../android/exoplayer2/metadata/id3/UrlLinkFrame.java | 3 ++- .../android/exoplayer2/offline/DownloadAction.java | 2 +- .../exoplayer2/offline/ProgressiveDownloadAction.java | 2 +- .../exoplayer2/offline/SegmentDownloadAction.java | 2 +- .../google/android/exoplayer2/source/MediaSource.java | 2 +- .../com/google/android/exoplayer2/source/TrackGroup.java | 3 ++- .../android/exoplayer2/source/TrackGroupArray.java | 3 ++- .../exoplayer2/trackselection/BaseTrackSelection.java | 3 ++- .../exoplayer2/trackselection/DefaultTrackSelector.java | 9 +++++---- .../exoplayer2/trackselection/TrackSelectionArray.java | 3 ++- .../android/exoplayer2/upstream/cache/CachedContent.java | 3 ++- .../upstream/cache/DefaultContentMetadata.java | 3 ++- .../com/google/android/exoplayer2/video/ColorInfo.java | 4 ++-- .../exoplayer2/analytics/AnalyticsCollectorTest.java | 2 +- library/ui/src/main/res/values-de/strings.xml | 4 ++-- 37 files changed, 72 insertions(+), 43 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java index 24d815bae2..396f6f8769 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.cast; +import android.support.annotation.Nullable; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; @@ -110,7 +111,7 @@ import java.util.Map; // equals and hashCode implementations. @Override - public boolean equals(Object other) { + public boolean equals(@Nullable Object other) { if (this == other) { return true; } else if (!(other instanceof CastTimeline)) { diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index f2898005c1..172159b7af 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -170,7 +170,7 @@ public class OkHttpDataSource implements HttpDataSource { // Check for a valid response code. if (!response.isSuccessful()) { - Map> headers = response.headers().toMultimap(); + Map> headers = request.headers().toMultimap(); closeConnectionQuietly(); InvalidResponseCodeException exception = new InvalidResponseCodeException( responseCode, headers, dataSpec); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java index a7de96a2de..6f2db4ff5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; /** @@ -87,7 +88,7 @@ public final class PlaybackParameters { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java index 93bbd1e4b6..684072efc6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; + /** * The configuration of a {@link Renderer}. */ @@ -41,7 +43,7 @@ public final class RendererConfiguration { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java index 2df9840cf8..ca0433f96d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; /** @@ -71,7 +72,7 @@ public final class SeekParameters { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java index 337200da8f..5e963a2540 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.audio; import android.annotation.TargetApi; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; /** @@ -119,7 +120,7 @@ public final class AudioAttributes { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java index 499ea488c7..4b03a5047b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.media.AudioFormat; import android.media.AudioManager; +import android.support.annotation.Nullable; import java.util.Arrays; /** @@ -96,7 +97,7 @@ public final class AudioCapabilities { } @Override - public boolean equals(Object other) { + public boolean equals(@Nullable Object other) { if (this == other) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index 4a59667dc8..c2de662010 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -195,7 +195,7 @@ public final class DrmInitData implements Comparator, Parcelable { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -338,7 +338,7 @@ public final class DrmInitData implements Comparator, Parcelable { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (!(obj instanceof SchemeData)) { return false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java index aa718c23e5..b7aaa2a31b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; @@ -92,7 +93,7 @@ public interface SeekMap { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java index 93cfbd9200..8b920bc024 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor; +import android.support.annotation.Nullable; + /** Defines a seek point in a media stream. */ public final class SeekPoint { @@ -42,7 +44,7 @@ public final class SeekPoint { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java index a12a0315a4..6a8cef6b64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -69,7 +70,7 @@ public interface TrackOutput { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 49f7361bc5..bf795c5857 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -639,7 +639,7 @@ public final class MediaCodecUtil { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index a8c9d0b5a8..a2ad7fe2ce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import java.util.Arrays; import java.util.List; @@ -76,7 +77,7 @@ public final class Metadata implements Parcelable { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index 0612c18e18..5f521aada6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.emsg; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -104,7 +105,7 @@ public final class EventMessage implements Metadata.Entry { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index eafb0286ce..ae78f712c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -49,7 +50,7 @@ public final class ApicFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java index f662c1d06f..129803299c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import java.util.Arrays; /** @@ -37,7 +38,7 @@ public final class BinaryFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java index c82f982aa7..aca530cdee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -80,7 +81,7 @@ public final class ChapterFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java index 939c00b9db..56b08bbee3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -70,7 +71,7 @@ public final class ChapterTocFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java index b43a46349c..e84b776790 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; /** @@ -45,7 +46,7 @@ public final class CommentFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java index 0ed429055b..8b665fce00 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -49,7 +50,7 @@ public final class GeobFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java index db6db2ea4f..1b5ba67c11 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -43,7 +44,7 @@ public final class PrivFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index 3374db5d8d..dbab4ca7a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; /** @@ -40,7 +41,7 @@ public final class TextInformationFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java index 775ab5dd3e..f657eefc30 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; /** @@ -40,7 +41,7 @@ public final class UrlLinkFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java index cf061f3745..98360b909c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java @@ -140,7 +140,7 @@ public abstract class DownloadAction { DownloaderConstructorHelper downloaderConstructorHelper); @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (o == null || getClass() != o.getClass()) { return false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java index 02ef7a7aa7..d8db6f96c2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java @@ -84,7 +84,7 @@ public final class ProgressiveDownloadAction extends DownloadAction { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java index f6a32a1253..ae57131641 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java @@ -112,7 +112,7 @@ public abstract class SegmentDownloadAction> extends Dow protected abstract void writeKey(DataOutputStream output, K key) throws IOException; @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index f8c2f8b3e1..1a243a8bf0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -145,7 +145,7 @@ public interface MediaSource { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java index 2e5b259a88..a9fb261768 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; @@ -96,7 +97,7 @@ public final class TrackGroup implements Parcelable { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java index 72afa3463e..a155032a9f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.util.Arrays; @@ -98,7 +99,7 @@ public final class TrackGroupArray implements Parcelable { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index 9a58ac07aa..81eb5dd888 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.trackselection; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; @@ -183,7 +184,7 @@ public abstract class BaseTrackSelection implements TrackSelection { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index f2b4c7ed3e..71d2544784 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -20,6 +20,7 @@ import android.graphics.Point; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; import android.util.SparseArray; @@ -771,7 +772,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -992,7 +993,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -2020,7 +2021,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } @@ -2074,7 +2075,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java index 2d457750e4..b37c8cad67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.trackselection; +import android.support.annotation.Nullable; import java.util.Arrays; /** An array of {@link TrackSelection}s. */ @@ -64,7 +65,7 @@ public final class TrackSelectionArray { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index 7b0b459dd9..89835f31de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import java.io.DataInputStream; @@ -236,7 +237,7 @@ import java.util.TreeSet; } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java index b855befe00..aefb0f6852 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -131,7 +132,7 @@ public final class DefaultContentMetadata implements ContentMetadata { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java index a983a0a6a3..faedaaf273 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java @@ -17,10 +17,10 @@ package com.google.android.exoplayer2.video; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Util; - import java.util.Arrays; /** @@ -85,7 +85,7 @@ public final class ColorInfo implements Parcelable { // Parcelable implementation. @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 829fa5a2b8..623506ad0d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -846,7 +846,7 @@ public final class AnalyticsCollectorTest { } @Override - public boolean equals(Object other) { + public boolean equals(@Nullable Object other) { if (!(other instanceof EventWindowAndPeriodId)) { return false; } diff --git a/library/ui/src/main/res/values-de/strings.xml b/library/ui/src/main/res/values-de/strings.xml index 3e83396678..6ac92acf9d 100644 --- a/library/ui/src/main/res/values-de/strings.xml +++ b/library/ui/src/main/res/values-de/strings.xml @@ -21,7 +21,7 @@ Video Audio Text - Keiner + Ohne Automatisch Unbekannt %1$d × %2$d @@ -31,5 +31,5 @@ 5.1-Surround-Sound 7.1-Surround-Sound %1$.2f Mbit/s - %1$s und %2$s + %1$s, %2$s From 18a679fe07784dc2637a6cf774ad3a8d54736257 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 9 May 2018 13:27:37 -0700 Subject: [PATCH 04/40] Update release notes --- RELEASENOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 979543f7be..242fe2c119 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,7 +2,8 @@ ### dev-v2 (not yet released) ### -* Coming soon... +* OkHttp extension: Fix to correctly include response headers in thrown + `InvalidResponseCodeException`s. ### 2.8.0 ### From 8d5cab8acfca829d6975aaf16b2c31ac7675c389 Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Fri, 11 May 2018 16:53:41 +0800 Subject: [PATCH 05/40] [extension-ffmpeg] repeatable build instructions Ensure the build instructions are repeatable once the ffmpeg repository is checked out. --- extensions/ffmpeg/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index fa7ac6b9fa..1f1b93ce53 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -70,7 +70,7 @@ COMMON_OPTIONS="\ --enable-decoder=flac \ " && \ cd "${FFMPEG_EXT_PATH}/jni" && \ -git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ +(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && cd ffmpeg && \ ./configure \ --libdir=android-libs/armeabi-v7a \ --arch=arm \ From f7d4fa882939100fd467c280fa7739b38b84f79c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 9 May 2018 13:56:07 -0700 Subject: [PATCH 06/40] Update moe equivalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196024195 --- .../google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 172159b7af..f2898005c1 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -170,7 +170,7 @@ public class OkHttpDataSource implements HttpDataSource { // Check for a valid response code. if (!response.isSuccessful()) { - Map> headers = request.headers().toMultimap(); + Map> headers = response.headers().toMultimap(); closeConnectionQuietly(); InvalidResponseCodeException exception = new InvalidResponseCodeException( responseCode, headers, dataSpec); From b779b1599f9f8ad7848530c948e582404306adbd Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 9 May 2018 18:09:45 -0700 Subject: [PATCH 07/40] Support setting some DefaultTimeBar attributes Issue: #4207 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196059445 --- .../android/exoplayer2/ui/DefaultTimeBar.java | 119 ++++++++++++++---- 1 file changed, 92 insertions(+), 27 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index 3866cf704a..05c645d9c7 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -25,6 +25,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.support.annotation.ColorInt; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.DisplayMetrics; @@ -44,87 +45,83 @@ import java.util.concurrent.CopyOnWriteArraySet; /** * 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. + * + *

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}
    • + *
    • 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}
    • + *
    • 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}
    • + *
    • 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}
    • + *
    • 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}
    • + *
    • 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}
    • + *
    • 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. *
      - *
    • Default: {@link #DEFAULT_PLAYED_COLOR}
    • + *
    • Corresponding method: {@link #setPlayedColor(int)} + *
    • Default: {@link #DEFAULT_PLAYED_COLOR} *
    - *
  • *
  • {@code scrubber_color} - Color for the scrubber handle. *
      - *
    • Default: see {@link #getDefaultScrubberColor(int)}
    • + *
    • Corresponding method: {@link #setScrubberColor(int)} + *
    • Default: see {@link #getDefaultScrubberColor(int)} *
    - *
  • *
  • {@code buffered_color} - Color for the portion of the time bar after the current * played position up to the current buffered position. *
      - *
    • Default: see {@link #getDefaultBufferedColor(int)}
    • + *
    • Corresponding method: {@link #setBufferedColor(int)} + *
    • Default: see {@link #getDefaultBufferedColor(int)} *
    - *
  • *
  • {@code unplayed_color} - Color for the portion of the time bar after the current * buffered position. *
      - *
    • Default: see {@link #getDefaultUnplayedColor(int)}
    • + *
    • Corresponding method: {@link #setUnplayedColor(int)} + *
    • Default: see {@link #getDefaultUnplayedColor(int)} *
    - *
  • *
  • {@code ad_marker_color} - Color for unplayed ad markers. *
      - *
    • Default: {@link #DEFAULT_AD_MARKER_COLOR}
    • + *
    • Corresponding method: {@link #setAdMarkerColor(int)} + *
    • Default: {@link #DEFAULT_AD_MARKER_COLOR} *
    - *
  • *
  • {@code played_ad_marker_color} - Color for played ad markers. *
      - *
    • Default: see {@link #getDefaultPlayedAdMarkerColor(int)}
    • + *
    • Corresponding method: {@link #setPlayedAdMarkerColor(int)} + *
    • Default: see {@link #getDefaultPlayedAdMarkerColor(int)} *
    - *
  • *
*/ public class DefaultTimeBar extends View implements TimeBar { @@ -324,6 +321,72 @@ public class DefaultTimeBar extends View implements TimeBar { } } + /** + * 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) { listeners.add(listener); @@ -381,6 +444,8 @@ public class DefaultTimeBar extends View implements TimeBar { update(); } + // View methods. + @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); From d6d7c41065df08e174eccbe650e4b73e5be96d8a Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 10 May 2018 14:11:36 -0700 Subject: [PATCH 08/40] Expose manifests/playlists from download helpers This is useful to get hold of the manifest to then obtain DRM init data in the download flow for protected content (without having to download the manifest again). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196168938 --- .../exoplayer2/source/dash/offline/DashDownloadHelper.java | 6 ++++++ .../exoplayer2/source/hls/offline/HlsDownloadHelper.java | 6 ++++++ .../source/smoothstreaming/offline/SsDownloadHelper.java | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java index 8a6069e477..bd19ff6587 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java @@ -56,6 +56,12 @@ public final class DashDownloadHelper extends DownloadHelper { manifestDataSourceFactory.createDataSource(), new DashManifestParser(), uri); } + /** Returns the DASH manifest. Must not be called until after preparation completes. */ + public DashManifest getManifest() { + Assertions.checkNotNull(manifest); + return manifest; + } + @Override public int getPeriodCount() { Assertions.checkNotNull(manifest); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java index 773aec49ee..37aa181970 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java @@ -57,6 +57,12 @@ public final class HlsDownloadHelper extends DownloadHelper { playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri); } + /** Returns the HLS playlist. Must not be called until after preparation completes. */ + public HlsPlaylist getPlaylist() { + Assertions.checkNotNull(playlist); + return playlist; + } + @Override public int getPeriodCount() { Assertions.checkNotNull(playlist); diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java index 82464101d6..e60be93c93 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java @@ -52,6 +52,12 @@ public final class SsDownloadHelper extends DownloadHelper { manifest = ParsingLoadable.load(dataSource, new SsManifestParser(), uri); } + /** Returns the SmoothStreaming manifest. Must not be called until after preparation completes. */ + public SsManifest getManifest() { + Assertions.checkNotNull(manifest); + return manifest; + } + @Override public int getPeriodCount() { Assertions.checkNotNull(manifest); From 36d24c7aaa93d5c6aa4bb4af611bebadeb4b5424 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 10 May 2018 15:23:31 -0700 Subject: [PATCH 09/40] Catch all errors in loadAd Issue: #4231 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196180271 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index d3dbaaec96..2d9ddfb288 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -649,18 +649,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void loadAd(String adUriString) { - if (adGroupIndex == C.INDEX_UNSET) { - Log.w( - TAG, - "Unexpected loadAd without LOADED event; assuming ad group index is actually " - + expectedAdGroupIndex); - adGroupIndex = expectedAdGroupIndex; - adsManager.start(); - } - if (DEBUG) { - Log.d(TAG, "loadAd in ad group " + adGroupIndex); - } try { + if (adGroupIndex == C.INDEX_UNSET) { + Log.w( + TAG, + "Unexpected loadAd without LOADED event; assuming ad group index is actually " + + expectedAdGroupIndex); + adGroupIndex = expectedAdGroupIndex; + adsManager.start(); + } + if (DEBUG) { + Log.d(TAG, "loadAd in ad group " + adGroupIndex); + } int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); if (adIndexInAdGroup == C.INDEX_UNSET) { Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); From 0fce0a0bcbe36cfb5e8bb3c557b6d0e867e2447e Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 11 May 2018 04:33:34 -0700 Subject: [PATCH 10/40] Refactor DummySurfaceThread, move code to generate SurfaceTexture to new class This makes way for reusing EGLSurfaceTexture in other places, such as metadata and frame retriever. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196240576 --- .../exoplayer2/util/EGLSurfaceTexture.java | 259 ++++++++++++++++++ .../exoplayer2/video/DummySurface.java | 163 ++--------- 2 files changed, 285 insertions(+), 137 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java new file mode 100644 index 0000000000..6fe76b9b2c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.util; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLSurface; +import android.opengl.GLES20; +import android.os.Handler; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Generates a {@link SurfaceTexture} using EGL/GLES functions. */ +@TargetApi(17) +public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableListener, Runnable { + + /** Secure mode to be used by the EGL surface and context. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER}) + public @interface SecureMode {} + + /** No secure EGL surface and context required. */ + public static final int SECURE_MODE_NONE = 0; + /** Creating a surfaceless, secured EGL context. */ + public static final int SECURE_MODE_SURFACELESS_CONTEXT = 1; + /** Creating a secure surface backed by a pixel buffer. */ + public static final int SECURE_MODE_PROTECTED_PBUFFER = 2; + + private static final int[] EGL_CONFIG_ATTRIBUTES = + new int[] { + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + EGL14.EGL_DEPTH_SIZE, 0, + EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE, + EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT, + EGL14.EGL_NONE + }; + + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; + + /** A runtime exception to be thrown if some EGL operations failed. */ + public static final class GlException extends RuntimeException { + private GlException(String msg) { + super(msg); + } + } + + private final Handler handler; + private final int[] textureIdHolder; + + private @Nullable EGLDisplay display; + private @Nullable EGLContext context; + private @Nullable EGLSurface surface; + private @Nullable SurfaceTexture texture; + + /** + * @param handler The {@link Handler} that will be used to call {@link + * SurfaceTexture#updateTexImage()} to update images on the {@link SurfaceTexture}. Note that + * {@link #init(int)} has to be called on the same looper thread as the {@link Handler}'s + * looper. + */ + public EGLSurfaceTexture(Handler handler) { + this.handler = handler; + textureIdHolder = new int[1]; + } + + /** + * Initializes required EGL parameters and creates the {@link SurfaceTexture}. + * + * @param secureMode The {@link SecureMode} to be used for EGL surface. + */ + public void init(@SecureMode int secureMode) { + display = getDefaultDisplay(); + EGLConfig config = chooseEGLConfig(display); + context = createEGLContext(display, config, secureMode); + surface = createEGLSurface(display, config, context, secureMode); + generateTextureIds(textureIdHolder); + texture = new SurfaceTexture(textureIdHolder[0]); + texture.setOnFrameAvailableListener(this); + } + + /** Releases all allocated resources. */ + @SuppressWarnings({"nullness:argument.type.incompatible"}) + public void release() { + handler.removeCallbacks(this); + try { + if (texture != null) { + texture.release(); + GLES20.glDeleteTextures(1, textureIdHolder, 0); + } + } finally { + if (surface != null && !surface.equals(EGL14.EGL_NO_SURFACE)) { + EGL14.eglDestroySurface(display, surface); + } + if (context != null) { + EGL14.eglDestroyContext(display, context); + } + display = null; + context = null; + surface = null; + texture = null; + } + } + + /** + * Returns the wrapped {@link SurfaceTexture}. This can only be called after {@link #init(int)}. + */ + public SurfaceTexture getSurfaceTexture() { + return Assertions.checkNotNull(texture); + } + + // SurfaceTexture.OnFrameAvailableListener + + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + handler.post(this); + } + + // Runnable + + @Override + public void run() { + if (texture != null) { + texture.updateTexImage(); + } + } + + private static EGLDisplay getDefaultDisplay() { + EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (display == null) { + throw new GlException("eglGetDisplay failed"); + } + + int[] version = new int[2]; + boolean eglInitialized = + EGL14.eglInitialize(display, version, /* majorOffset= */ 0, version, /* minorOffset= */ 1); + if (!eglInitialized) { + throw new GlException("eglInitialize failed"); + } + return display; + } + + private static EGLConfig chooseEGLConfig(EGLDisplay display) { + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + boolean success = + EGL14.eglChooseConfig( + display, + EGL_CONFIG_ATTRIBUTES, + /* attrib_listOffset= */ 0, + configs, + /* configsOffset= */ 0, + /* config_size= */ 1, + numConfigs, + /* num_configOffset= */ 0); + if (!success || numConfigs[0] <= 0 || configs[0] == null) { + throw new GlException( + Util.formatInvariant( + /* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s", + success, numConfigs[0], configs[0])); + } + + return configs[0]; + } + + private static EGLContext createEGLContext( + EGLDisplay display, EGLConfig config, @SecureMode int secureMode) { + int[] glAttributes; + if (secureMode == SECURE_MODE_NONE) { + glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE}; + } else { + glAttributes = + new int[] { + EGL14.EGL_CONTEXT_CLIENT_VERSION, + 2, + EGL_PROTECTED_CONTENT_EXT, + EGL14.EGL_TRUE, + EGL14.EGL_NONE + }; + } + EGLContext context = + EGL14.eglCreateContext( + display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0); + if (context == null) { + throw new GlException("eglCreateContext failed"); + } + return context; + } + + private static EGLSurface createEGLSurface( + EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode) { + EGLSurface surface; + if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { + surface = EGL14.EGL_NO_SURFACE; + } else { + int[] pbufferAttributes; + if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { + pbufferAttributes = + new int[] { + EGL14.EGL_WIDTH, + 1, + EGL14.EGL_HEIGHT, + 1, + EGL_PROTECTED_CONTENT_EXT, + EGL14.EGL_TRUE, + EGL14.EGL_NONE + }; + } else { + pbufferAttributes = + new int[] { + EGL14.EGL_WIDTH, 1, + EGL14.EGL_HEIGHT, 1, + EGL14.EGL_NONE + }; + } + surface = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, /* offset= */ 0); + if (surface == null) { + throw new GlException("eglCreatePbufferSurface failed"); + } + } + + boolean eglMadeCurrent = + EGL14.eglMakeCurrent(display, /* draw= */ surface, /* read= */ surface, context); + if (!eglMadeCurrent) { + throw new GlException("eglMakeCurrent failed"); + } + return surface; + } + + private static void generateTextureIds(int[] textureIdHolder) { + GLES20.glGenTextures(/* n= */ 1, textureIdHolder, /* offset= */ 0); + int errorCode = GLES20.glGetError(); + if (errorCode != GLES20.GL_NO_ERROR) { + throw new GlException("glGenTextures failed. Error: " + Integer.toHexString(errorCode)); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index fc31a33097..2f41831a5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -15,29 +15,29 @@ */ package com.google.android.exoplayer2.video; +import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_NONE; +import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_PROTECTED_PBUFFER; +import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_SURFACELESS_CONTEXT; + import android.annotation.TargetApi; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; -import android.graphics.SurfaceTexture.OnFrameAvailableListener; import android.opengl.EGL14; -import android.opengl.EGLConfig; -import android.opengl.EGLContext; import android.opengl.EGLDisplay; -import android.opengl.EGLSurface; -import android.opengl.GLES20; import android.os.Handler; import android.os.Handler.Callback; import android.os.HandlerThread; import android.os.Message; -import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.EGLSurfaceTexture; +import com.google.android.exoplayer2.util.EGLSurfaceTexture.SecureMode; import com.google.android.exoplayer2.util.Util; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import javax.microedition.khronos.egl.EGL10; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A dummy {@link Surface}. @@ -50,16 +50,6 @@ public final class DummySurface extends Surface { private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; - private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER}) - private @interface SecureMode {} - - private static final int SECURE_MODE_NONE = 0; - private static final int SECURE_MODE_SURFACELESS_CONTEXT = 1; - private static final int SECURE_MODE_PROTECTED_PBUFFER = 2; - /** * Whether the surface is secure. */ @@ -161,32 +151,25 @@ public final class DummySurface extends Surface { : SECURE_MODE_PROTECTED_PBUFFER; } - private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, - Callback { + private static class DummySurfaceThread extends HandlerThread implements Callback { private static final int MSG_INIT = 1; - private static final int MSG_UPDATE_TEXTURE = 2; - private static final int MSG_RELEASE = 3; + private static final int MSG_RELEASE = 2; - private final int[] textureIdHolder; - private EGLDisplay display; - private EGLContext context; - private EGLSurface pbuffer; - private Handler handler; - private SurfaceTexture surfaceTexture; - - private Error initError; - private RuntimeException initException; - private DummySurface surface; + private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexure; + private @MonotonicNonNull Handler handler; + private @Nullable Error initError; + private @Nullable RuntimeException initException; + private @Nullable DummySurface surface; public DummySurfaceThread() { super("dummySurface"); - textureIdHolder = new int[1]; } public DummySurface init(@SecureMode int secureMode) { start(); - handler = new Handler(getLooper(), this); + handler = new Handler(getLooper(), /* callback= */ this); + eglSurfaceTexure = new EGLSurfaceTexture(handler); boolean wasInterrupted = false; synchronized (this) { handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget(); @@ -207,19 +190,15 @@ public final class DummySurface extends Surface { } else if (initError != null) { throw initError; } else { - return surface; + return Assertions.checkNotNull(surface); } } public void release() { + Assertions.checkNotNull(handler); handler.sendEmptyMessage(MSG_RELEASE); } - @Override - public void onFrameAvailable(SurfaceTexture surfaceTexture) { - handler.sendEmptyMessage(MSG_UPDATE_TEXTURE); - } - @Override public boolean handleMessage(Message msg) { switch (msg.what) { @@ -238,9 +217,6 @@ public final class DummySurface extends Surface { } } return true; - case MSG_UPDATE_TEXTURE: - surfaceTexture.updateTexImage(); - return true; case MSG_RELEASE: try { releaseInternal(); @@ -256,103 +232,16 @@ public final class DummySurface extends Surface { } private void initInternal(@SecureMode int secureMode) { - display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); - Assertions.checkState(display != null, "eglGetDisplay failed"); - - int[] version = new int[2]; - boolean eglInitialized = EGL14.eglInitialize(display, version, 0, version, 1); - Assertions.checkState(eglInitialized, "eglInitialize failed"); - - int[] eglAttributes = - new int[] { - EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, - EGL14.EGL_RED_SIZE, 8, - EGL14.EGL_GREEN_SIZE, 8, - EGL14.EGL_BLUE_SIZE, 8, - EGL14.EGL_ALPHA_SIZE, 8, - EGL14.EGL_DEPTH_SIZE, 0, - EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE, - EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT, - EGL14.EGL_NONE - }; - EGLConfig[] configs = new EGLConfig[1]; - int[] numConfigs = new int[1]; - boolean eglChooseConfigSuccess = - EGL14.eglChooseConfig(display, eglAttributes, 0, configs, 0, 1, numConfigs, 0); - Assertions.checkState(eglChooseConfigSuccess && numConfigs[0] > 0 && configs[0] != null, - "eglChooseConfig failed"); - - EGLConfig config = configs[0]; - int[] glAttributes; - if (secureMode == SECURE_MODE_NONE) { - glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE}; - } else { - glAttributes = - new int[] { - EGL14.EGL_CONTEXT_CLIENT_VERSION, - 2, - EGL_PROTECTED_CONTENT_EXT, - EGL14.EGL_TRUE, - EGL14.EGL_NONE - }; - } - context = - EGL14.eglCreateContext( - display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0); - Assertions.checkState(context != null, "eglCreateContext failed"); - - EGLSurface surface; - if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { - surface = EGL14.EGL_NO_SURFACE; - } else { - int[] pbufferAttributes; - if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { - pbufferAttributes = - new int[] { - EGL14.EGL_WIDTH, - 1, - EGL14.EGL_HEIGHT, - 1, - EGL_PROTECTED_CONTENT_EXT, - EGL14.EGL_TRUE, - EGL14.EGL_NONE - }; - } else { - pbufferAttributes = new int[] {EGL14.EGL_WIDTH, 1, EGL14.EGL_HEIGHT, 1, EGL14.EGL_NONE}; - } - pbuffer = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, 0); - Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); - surface = pbuffer; - } - - boolean eglMadeCurrent = EGL14.eglMakeCurrent(display, surface, surface, context); - Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed"); - - GLES20.glGenTextures(1, textureIdHolder, 0); - surfaceTexture = new SurfaceTexture(textureIdHolder[0]); - surfaceTexture.setOnFrameAvailableListener(this); - this.surface = new DummySurface(this, surfaceTexture, secureMode != SECURE_MODE_NONE); + Assertions.checkNotNull(eglSurfaceTexure); + eglSurfaceTexure.init(secureMode); + this.surface = + new DummySurface( + this, eglSurfaceTexure.getSurfaceTexture(), secureMode != SECURE_MODE_NONE); } private void releaseInternal() { - try { - if (surfaceTexture != null) { - surfaceTexture.release(); - GLES20.glDeleteTextures(1, textureIdHolder, 0); - } - } finally { - if (pbuffer != null) { - EGL14.eglDestroySurface(display, pbuffer); - } - if (context != null) { - EGL14.eglDestroyContext(display, context); - } - pbuffer = null; - context = null; - display = null; - surface = null; - surfaceTexture = null; - } + Assertions.checkNotNull(eglSurfaceTexure); + eglSurfaceTexure.release(); } } From 74df3766f9da2f49a6cf3fafaf177c4c19d69276 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 11 May 2018 07:05:48 -0700 Subject: [PATCH 11/40] Include checkerframework annotatons with compileOnly and remove lint exclusion The lint error suppression only works locally and not for external developers who still see the lint error and need to suppress it themselves. This changes 'implementation' to 'compileOnly' in gradle to prevent the dependency from being exported. Also removes the local lint suppression. Issue:#4234 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196251407 --- checker-framework-lint.xml | 19 ------------------- library/core/build.gradle | 6 +----- library/dash/build.gradle | 6 +----- library/hls/build.gradle | 6 +----- library/smoothstreaming/build.gradle | 6 +----- 5 files changed, 4 insertions(+), 39 deletions(-) delete mode 100644 checker-framework-lint.xml diff --git a/checker-framework-lint.xml b/checker-framework-lint.xml deleted file mode 100644 index 1d45f9de05..0000000000 --- a/checker-framework-lint.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - diff --git a/library/core/build.gradle b/library/core/build.gradle index 52249220e0..d2fa5e25f8 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -42,15 +42,11 @@ android { // testCoverageEnabled = true // } } - - lintOptions { - lintConfig file("../../checker-framework-lint.xml") - } } dependencies { implementation 'com.android.support:support-annotations:' + supportLibraryVersion - implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion diff --git a/library/dash/build.gradle b/library/dash/build.gradle index 81b247d047..867b288498 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -30,15 +30,11 @@ android { // testCoverageEnabled = true // } } - - lintOptions { - lintConfig file("../../checker-framework-lint.xml") - } } dependencies { implementation project(modulePrefix + 'library-core') - implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index c599931a68..6aeb33e195 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -30,15 +30,11 @@ android { // testCoverageEnabled = true // } } - - lintOptions { - lintConfig file("../../checker-framework-lint.xml") - } } dependencies { implementation 'com.android.support:support-annotations:' + supportLibraryVersion - implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index e71f9baa99..6f85d1572d 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -30,15 +30,11 @@ android { // testCoverageEnabled = true // } } - - lintOptions { - lintConfig file("../../checker-framework-lint.xml") - } } dependencies { implementation project(modulePrefix + 'library-core') - implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion testImplementation project(modulePrefix + 'testutils-robolectric') } From f848d0e3393e159128e3a764582f867458d799fb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 11 May 2018 16:09:06 -0700 Subject: [PATCH 12/40] Remove stray space ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196323463 --- .../com/google/android/exoplayer2/DefaultLoadControl.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index b5b364a327..f8b7f5f5c2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -46,11 +46,10 @@ public class DefaultLoadControl implements LoadControl { public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500; /** - * The default duration of media that must be buffered for playback to resume after a rebuffer, - * in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user - * action. + * The default duration of media that must be buffered for playback to resume after a rebuffer, in + * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; + public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; /** * The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control From 1af93341881a1ee2b207738688ae555946886427 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 11 May 2018 23:33:28 -0700 Subject: [PATCH 13/40] Add option to keep content visible in PlayerView when player is reset Issue: #2843 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196349533 --- RELEASENOTES.md | 4 + .../android/exoplayer2/ui/PlayerView.java | 73 +++++++++++++++---- library/ui/src/main/res/values/attrs.xml | 1 + 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 242fe2c119..5e6c222b9a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,10 @@ * OkHttp extension: Fix to correctly include response headers in thrown `InvalidResponseCodeException`s. +* UI components: + * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed + video frame or media artwork visible when the player is reset + ([#2843](https://github.com/google/ExoPlayer/issues/2843)). ### 2.8.0 ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 25c4318768..a7aa48c0db 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -133,6 +133,12 @@ import java.util.List; *
  • Corresponding method: {@link #setShutterBackgroundColor(int)} *
  • Default: {@code unset} * + *
  • {@code keep_content_on_player_reset} - Whether the currently displayed video frame + * or media artwork is kept visible when the player is reset. + *
      + *
    • Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)} + *
    • Default: {@code false} + *
    *
  • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below * for more details. *
      @@ -242,6 +248,7 @@ public class PlayerView extends FrameLayout { private boolean useArtwork; private Bitmap defaultArtwork; private boolean showBuffering; + private boolean keepContentOnPlayerReset; private @Nullable ErrorMessageProvider errorMessageProvider; private @Nullable CharSequence customErrorMessage; private int controllerShowTimeoutMs; @@ -313,6 +320,9 @@ public class PlayerView extends FrameLayout { a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch); controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow); showBuffering = a.getBoolean(R.styleable.PlayerView_show_buffering, showBuffering); + keepContentOnPlayerReset = + a.getBoolean( + R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset); controllerHideDuringAds = a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds); } finally { @@ -472,14 +482,12 @@ public class PlayerView extends FrameLayout { if (useController) { controller.setPlayer(player); } - if (shutterView != null) { - shutterView.setVisibility(VISIBLE); - } if (subtitleView != null) { subtitleView.setCues(null); } updateBuffering(); updateErrorMessage(); + updateForCurrentTrackSelections(/* isNewPlayer= */ true); if (player != null) { Player.VideoComponent newVideoComponent = player.getVideoComponent(); if (newVideoComponent != null) { @@ -496,10 +504,8 @@ public class PlayerView extends FrameLayout { } player.addListener(componentListener); maybeShowController(false); - updateForCurrentTrackSelections(); } else { hideController(); - hideArtwork(); } } @@ -542,7 +548,7 @@ public class PlayerView extends FrameLayout { Assertions.checkState(!useArtwork || artworkView != null); if (this.useArtwork != useArtwork) { this.useArtwork = useArtwork; - updateForCurrentTrackSelections(); + updateForCurrentTrackSelections(/* isNewPlayer= */ false); } } @@ -560,7 +566,7 @@ public class PlayerView extends FrameLayout { public void setDefaultArtwork(Bitmap defaultArtwork) { if (this.defaultArtwork != defaultArtwork) { this.defaultArtwork = defaultArtwork; - updateForCurrentTrackSelections(); + updateForCurrentTrackSelections(/* isNewPlayer= */ false); } } @@ -600,6 +606,32 @@ public class PlayerView extends FrameLayout { } } + /** + * Sets whether the currently displayed video frame or media artwork is kept visible when the + * player is reset. A player reset is defined to mean the player being re-prepared with different + * media, {@link Player#stop(boolean)} being called with {@code reset=true}, or the player being + * replaced or cleared by calling {@link #setPlayer(Player)}. + * + *

      If enabled, the currently displayed video frame or media artwork will be kept visible until + * the player set on the view has been successfully prepared with new media and loaded enough of + * it to have determined the available tracks. Hence enabling this option allows transitioning + * from playing one piece of media to another, or from using one player instance to another, + * without clearing the view's content. + * + *

      If disabled, the currently displayed video frame or media artwork will be hidden as soon as + * the player is reset. Note that the video frame is hidden by making {@code exo_shutter} visible. + * Hence the video frame will not be hidden if using a custom layout that omits this view. + * + * @param keepContentOnPlayerReset Whether the currently displayed video frame or media artwork is + * kept visible when the player is reset. + */ + public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) { + if (this.keepContentOnPlayerReset != keepContentOnPlayerReset) { + this.keepContentOnPlayerReset = keepContentOnPlayerReset; + updateForCurrentTrackSelections(/* isNewPlayer= */ false); + } + } + /** * Sets whether a buffering spinner is displayed when the player is in the buffering state. The * buffering spinner is not displayed by default. @@ -961,10 +993,20 @@ public class PlayerView extends FrameLayout { return player != null && player.isPlayingAd() && player.getPlayWhenReady(); } - private void updateForCurrentTrackSelections() { - if (player == null) { + private void updateForCurrentTrackSelections(boolean isNewPlayer) { + if (player == null || player.getCurrentTrackGroups().isEmpty()) { + if (!keepContentOnPlayerReset) { + hideArtwork(); + closeShutter(); + } return; } + + if (isNewPlayer && !keepContentOnPlayerReset) { + // Hide any video from the previous player. + closeShutter(); + } + TrackSelectionArray selections = player.getCurrentTrackSelections(); for (int i = 0; i < selections.length; i++) { if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { @@ -974,10 +1016,9 @@ public class PlayerView extends FrameLayout { return; } } + // Video disabled so the shutter must be closed. - if (shutterView != null) { - shutterView.setVisibility(VISIBLE); - } + closeShutter(); // Display artwork if enabled and available, else hide it. if (useArtwork) { for (int i = 0; i < selections.length; i++) { @@ -1034,6 +1075,12 @@ public class PlayerView extends FrameLayout { } } + private void closeShutter() { + if (shutterView != null) { + shutterView.setVisibility(View.VISIBLE); + } + } + private void updateBuffering() { if (bufferingView != null) { boolean showBufferingSpinner = @@ -1177,7 +1224,7 @@ public class PlayerView extends FrameLayout { @Override public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { - updateForCurrentTrackSelections(); + updateForCurrentTrackSelections(/* isNewPlayer= */ false); } // Player.EventListener implementation diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 9eefc027ed..e127f181e9 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -51,6 +51,7 @@ + From edd237e196b44398b057ee5098dff34d9cf5609a Mon Sep 17 00:00:00 2001 From: Pedro Machado Date: Mon, 14 May 2018 15:34:06 +0100 Subject: [PATCH 14/40] Saving current subtitle cues on SimpleExoPlayer --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 482e2c970a..11091f9968 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -92,6 +92,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player private AudioAttributes audioAttributes; private float audioVolume; private MediaSource mediaSource; + private List currentCues; /** * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. @@ -502,6 +503,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void addTextOutput(TextOutput listener) { + listener.onCues(currentCues); textOutputs.add(listener); } @@ -775,6 +777,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player mediaSource = null; analyticsCollector.resetForNewMediaSource(); } + currentCues = null; } @Override @@ -790,6 +793,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player if (mediaSource != null) { mediaSource.removeEventListener(analyticsCollector); } + currentCues = null; } @Override @@ -1095,6 +1099,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void onCues(List cues) { + currentCues = cues; for (TextOutput textOutput : textOutputs) { textOutput.onCues(cues); } From 2c55f5893826aae396c7b48bcf36d3de5c7cb286 Mon Sep 17 00:00:00 2001 From: Pedro Machado Date: Mon, 14 May 2018 17:50:44 +0100 Subject: [PATCH 15/40] Fixed nullability issues --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 11091f9968..5539337257 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -92,7 +92,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player private AudioAttributes audioAttributes; private float audioVolume; private MediaSource mediaSource; - private List currentCues; + private @Nullable List currentCues; /** * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. @@ -503,7 +503,9 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void addTextOutput(TextOutput listener) { - listener.onCues(currentCues); + if(currentCues != null) { + listener.onCues(currentCues); + } textOutputs.add(listener); } From b3c3717007b36ad2162c20b3dae39d6983ea1856 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 14 May 2018 07:43:36 -0700 Subject: [PATCH 16/40] Prevent NPE in PlayerNotificationManager. The app can set the player to null while messages from the player are still in flight. This may cause NPEs. Issue:#4238 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196504077 --- library/ui/build.gradle | 1 + .../ui/PlayerNotificationManager.java | 199 +++++++++--------- 2 files changed, 103 insertions(+), 97 deletions(-) diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 017d7e3e14..42ec0bba0a 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -36,6 +36,7 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'com.android.support:support-media-compat:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } ext { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 4c258c748f..19051ba932 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -50,6 +50,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * A notification manager to start, update and cancel a media style notification reflecting the @@ -205,7 +206,9 @@ public class PlayerNotificationManager { new Runnable() { @Override public void run() { - if (notificationTag == currentNotificationTag && isNotificationStarted) { + if (player != null + && notificationTag == currentNotificationTag + && isNotificationStarted) { updateNotification(bitmap); } } @@ -260,7 +263,7 @@ public class PlayerNotificationManager { private final String channelId; private final int notificationId; private final MediaDescriptionAdapter mediaDescriptionAdapter; - private final CustomActionReceiver customActionReceiver; + private final @Nullable CustomActionReceiver customActionReceiver; private final Handler mainHandler; private final NotificationManagerCompat notificationManager; private final IntentFilter intentFilter; @@ -269,12 +272,12 @@ public class PlayerNotificationManager { private final Map playbackActions; private final Map customActions; - private Player player; + private @Nullable Player player; private ControlDispatcher controlDispatcher; private boolean isNotificationStarted; private int currentNotificationTag; - private NotificationListener notificationListener; - private MediaSessionCompat.Token mediaSessionToken; + private @Nullable NotificationListener notificationListener; + private @Nullable MediaSessionCompat.Token mediaSessionToken; private boolean useNavigationActions; private boolean usePlayPauseActions; private @Nullable String stopAction; @@ -365,6 +368,20 @@ public class PlayerNotificationManager { playerListener = new PlayerListener(); notificationBroadcastReceiver = new NotificationBroadcastReceiver(); intentFilter = new IntentFilter(); + useNavigationActions = true; + usePlayPauseActions = true; + ongoing = true; + colorized = true; + useChronometer = true; + color = Color.TRANSPARENT; + smallIconResourceId = R.drawable.exo_notification_small_icon; + defaults = 0; + priority = NotificationCompat.PRIORITY_LOW; + fastForwardMs = DEFAULT_FAST_FORWARD_MS; + rewindMs = DEFAULT_REWIND_MS; + stopAction = ACTION_STOP; + badgeIconType = NotificationCompat.BADGE_ICON_SMALL; + visibility = NotificationCompat.VISIBILITY_PUBLIC; // initialize actions playbackActions = createPlaybackActions(context); @@ -378,22 +395,7 @@ public class PlayerNotificationManager { for (String action : customActions.keySet()) { intentFilter.addAction(action); } - - setStopAction(ACTION_STOP); - - useNavigationActions = true; - usePlayPauseActions = true; - ongoing = true; - colorized = true; - useChronometer = true; - color = Color.TRANSPARENT; - smallIconResourceId = R.drawable.exo_notification_small_icon; - defaults = 0; - priority = NotificationCompat.PRIORITY_LOW; - fastForwardMs = DEFAULT_FAST_FORWARD_MS; - rewindMs = DEFAULT_REWIND_MS; - setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL); - setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent; } /** @@ -512,10 +514,9 @@ public class PlayerNotificationManager { } this.stopAction = stopAction; if (ACTION_STOP.equals(stopAction)) { - stopPendingIntent = playbackActions.get(ACTION_STOP).actionIntent; + stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent; } else if (stopAction != null) { - Assertions.checkArgument(customActions.containsKey(stopAction)); - stopPendingIntent = customActions.get(stopAction).actionIntent; + stopPendingIntent = Assertions.checkNotNull(customActions.get(stopAction)).actionIntent; } else { stopPendingIntent = null; } @@ -698,25 +699,28 @@ public class PlayerNotificationManager { maybeUpdateNotification(); } - private Notification updateNotification(Bitmap bitmap) { + @RequiresNonNull("player") + private Notification updateNotification(@Nullable Bitmap bitmap) { Notification notification = createNotification(player, bitmap); notificationManager.notify(notificationId, notification); return notification; } private void startOrUpdateNotification() { - Notification notification = updateNotification(null); - if (!isNotificationStarted) { - isNotificationStarted = true; - context.registerReceiver(notificationBroadcastReceiver, intentFilter); - if (notificationListener != null) { - notificationListener.onNotificationStarted(notificationId, notification); + if (player != null) { + Notification notification = updateNotification(null); + if (!isNotificationStarted) { + isNotificationStarted = true; + context.registerReceiver(notificationBroadcastReceiver, intentFilter); + if (notificationListener != null) { + notificationListener.onNotificationStarted(notificationId, notification); + } } } } private void maybeUpdateNotification() { - if (isNotificationStarted) { + if (isNotificationStarted && player != null) { updateNotification(null); } } @@ -732,64 +736,6 @@ public class PlayerNotificationManager { } } - private Map createPlaybackActions(Context context) { - Map actions = new HashMap<>(); - Intent playIntent = new Intent(ACTION_PLAY).setPackage(context.getPackageName()); - actions.put( - ACTION_PLAY, - new NotificationCompat.Action( - R.drawable.exo_notification_play, - context.getString(R.string.exo_controls_play_description), - PendingIntent.getBroadcast(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - Intent pauseIntent = new Intent(ACTION_PAUSE).setPackage(context.getPackageName()); - actions.put( - ACTION_PAUSE, - new NotificationCompat.Action( - R.drawable.exo_notification_pause, - context.getString(R.string.exo_controls_pause_description), - PendingIntent.getBroadcast( - context, 0, pauseIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - Intent stopIntent = new Intent(ACTION_STOP).setPackage(context.getPackageName()); - actions.put( - ACTION_STOP, - new NotificationCompat.Action( - R.drawable.exo_notification_stop, - context.getString(R.string.exo_controls_stop_description), - PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - Intent rewindIntent = new Intent(ACTION_REWIND).setPackage(context.getPackageName()); - actions.put( - ACTION_REWIND, - new NotificationCompat.Action( - R.drawable.exo_notification_rewind, - context.getString(R.string.exo_controls_rewind_description), - PendingIntent.getBroadcast( - context, 0, rewindIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - Intent fastForwardIntent = new Intent(ACTION_FAST_FORWARD).setPackage(context.getPackageName()); - actions.put( - ACTION_FAST_FORWARD, - new NotificationCompat.Action( - R.drawable.exo_notification_fastforward, - context.getString(R.string.exo_controls_fastforward_description), - PendingIntent.getBroadcast( - context, 0, fastForwardIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - Intent previousIntent = new Intent(ACTION_PREVIOUS).setPackage(context.getPackageName()); - actions.put( - ACTION_PREVIOUS, - new NotificationCompat.Action( - R.drawable.exo_notification_previous, - context.getString(R.string.exo_controls_previous_description), - PendingIntent.getBroadcast( - context, 0, previousIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - Intent nextIntent = new Intent(ACTION_NEXT).setPackage(context.getPackageName()); - actions.put( - ACTION_NEXT, - new NotificationCompat.Action( - R.drawable.exo_notification_next, - context.getString(R.string.exo_controls_next_description), - PendingIntent.getBroadcast(context, 0, nextIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - return actions; - } - /** * Creates the notification given the current player state. * @@ -821,7 +767,7 @@ public class PlayerNotificationManager { // Configure stop action (eg. when user dismisses the notification when !isOngoing). boolean useStopAction = stopAction != null && !isPlayingAd; mediaStyle.setShowCancelButton(useStopAction); - if (useStopAction) { + if (useStopAction && stopPendingIntent != null) { builder.setDeleteIntent(stopPendingIntent); mediaStyle.setCancelButtonIntent(stopPendingIntent); } @@ -905,7 +851,7 @@ public class PlayerNotificationManager { if (useNavigationActions && player.getNextWindowIndex() != C.INDEX_UNSET) { stringActions.add(ACTION_NEXT); } - if (!customActions.isEmpty()) { + if (customActionReceiver != null) { stringActions.addAll(customActionReceiver.getCustomActions(player)); } if (ACTION_STOP.equals(stopAction)) { @@ -932,6 +878,64 @@ public class PlayerNotificationManager { return new int[] {actionIndex}; } + private static Map createPlaybackActions(Context context) { + Map actions = new HashMap<>(); + Intent playIntent = new Intent(ACTION_PLAY).setPackage(context.getPackageName()); + actions.put( + ACTION_PLAY, + new NotificationCompat.Action( + R.drawable.exo_notification_play, + context.getString(R.string.exo_controls_play_description), + PendingIntent.getBroadcast(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent pauseIntent = new Intent(ACTION_PAUSE).setPackage(context.getPackageName()); + actions.put( + ACTION_PAUSE, + new NotificationCompat.Action( + R.drawable.exo_notification_pause, + context.getString(R.string.exo_controls_pause_description), + PendingIntent.getBroadcast( + context, 0, pauseIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent stopIntent = new Intent(ACTION_STOP).setPackage(context.getPackageName()); + actions.put( + ACTION_STOP, + new NotificationCompat.Action( + R.drawable.exo_notification_stop, + context.getString(R.string.exo_controls_stop_description), + PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent rewindIntent = new Intent(ACTION_REWIND).setPackage(context.getPackageName()); + actions.put( + ACTION_REWIND, + new NotificationCompat.Action( + R.drawable.exo_notification_rewind, + context.getString(R.string.exo_controls_rewind_description), + PendingIntent.getBroadcast( + context, 0, rewindIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent fastForwardIntent = new Intent(ACTION_FAST_FORWARD).setPackage(context.getPackageName()); + actions.put( + ACTION_FAST_FORWARD, + new NotificationCompat.Action( + R.drawable.exo_notification_fastforward, + context.getString(R.string.exo_controls_fastforward_description), + PendingIntent.getBroadcast( + context, 0, fastForwardIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent previousIntent = new Intent(ACTION_PREVIOUS).setPackage(context.getPackageName()); + actions.put( + ACTION_PREVIOUS, + new NotificationCompat.Action( + R.drawable.exo_notification_previous, + context.getString(R.string.exo_controls_previous_description), + PendingIntent.getBroadcast( + context, 0, previousIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent nextIntent = new Intent(ACTION_NEXT).setPackage(context.getPackageName()); + actions.put( + ACTION_NEXT, + new NotificationCompat.Action( + R.drawable.exo_notification_next, + context.getString(R.string.exo_controls_next_description), + PendingIntent.getBroadcast(context, 0, nextIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + return actions; + } + private class PlayerListener extends Player.DefaultEventListener { @Override @@ -946,7 +950,7 @@ public class PlayerNotificationManager { @Override public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { - if (player.getPlaybackState() == Player.STATE_IDLE) { + if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { return; } startOrUpdateNotification(); @@ -954,7 +958,7 @@ public class PlayerNotificationManager { @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - if (player.getPlaybackState() == Player.STATE_IDLE) { + if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { return; } startOrUpdateNotification(); @@ -967,7 +971,7 @@ public class PlayerNotificationManager { @Override public void onRepeatModeChanged(int repeatMode) { - if (player.getPlaybackState() == Player.STATE_IDLE) { + if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { return; } startOrUpdateNotification(); @@ -985,7 +989,8 @@ public class PlayerNotificationManager { @Override public void onReceive(Context context, Intent intent) { - if (!isNotificationStarted) { + Player player = PlayerNotificationManager.this.player; + if (player == null || !isNotificationStarted) { return; } String action = intent.getAction(); @@ -1013,7 +1018,7 @@ public class PlayerNotificationManager { } else if (ACTION_STOP.equals(action)) { controlDispatcher.dispatchStop(player, true); stopNotification(); - } else if (customActions.containsKey(action)) { + } else if (customActionReceiver != null && customActions.containsKey(action)) { customActionReceiver.onCustomAction(player, action, intent); } } From eb151a79e6f8f64ecec679a0a3d11f28a08d1fd9 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 14 May 2018 11:45:32 -0700 Subject: [PATCH 17/40] Small DownloadManager fixes Fix suppressing initial "state changed to paused" listener invocations for new added tasks that are immediately started. Notify listeners for loaded actions queued state if they are not started immediately. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196542693 --- .../exoplayer2/offline/DownloadManager.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 8be822b6ca..0e2c5874b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; @@ -250,7 +251,6 @@ public final class DownloadManager { Assertions.checkState(!released); Task task = addTaskForAction(action); if (initialized) { - notifyListenersTaskStateChange(task); saveActions(); maybeStartTasks(); if (task.currentState == STATE_QUEUED) { @@ -413,7 +413,6 @@ public final class DownloadManager { if (released) { return; } - logd("Task state is changed", task); boolean stopped = !task.isActive(); if (stopped) { activeDownloadTasks.remove(task); @@ -430,6 +429,7 @@ public final class DownloadManager { } private void notifyListenersTaskStateChange(Task task) { + logd("Task state is changed", task); TaskState taskState = task.getDownloadState(); for (Listener listener : listeners) { listener.onTaskStateChanged(this, taskState); @@ -468,18 +468,16 @@ public final class DownloadManager { listener.onInitialized(DownloadManager.this); } if (!pendingTasks.isEmpty()) { - for (int i = 0; i < pendingTasks.size(); i++) { - tasks.add(pendingTasks.get(i)); - } + tasks.addAll(pendingTasks); saveActions(); } maybeStartTasks(); - for (int i = 0; i < pendingTasks.size(); i++) { - Task pendingTask = pendingTasks.get(i); - if (pendingTask.currentState == STATE_QUEUED) { + for (int i = 0; i < tasks.size(); i++) { + Task task = tasks.get(i); + if (task.currentState == STATE_QUEUED) { // Task did not change out of its initial state, and so its initial state // won't have been reported to listeners. Do so now. - notifyListenersTaskStateChange(pendingTask); + notifyListenersTaskStateChange(task); } } } @@ -699,9 +697,19 @@ public final class DownloadManager { + ' ' + (action.isRemoveAction ? "remove" : "download") + ' ' + + toString(action.data) + + ' ' + getStateString(); } + private static String toString(byte[] data) { + if (data.length > 100) { + return ""; + } else { + return '\'' + Util.fromUtf8Bytes(data) + '\''; + } + } + private String getStateString() { switch (currentState) { case STATE_QUEUED_CANCELING: From d3d4b33cacebcfbcf6f91186dc84e449bbf00c52 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 14 May 2018 13:55:59 -0700 Subject: [PATCH 18/40] Blacklist Moto E(4) from setOutputSurface. GitHub: #4134. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196562078 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5e6c222b9a..a918b2b06e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,8 @@ * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed video frame or media artwork visible when the player is reset ([#2843](https://github.com/google/ExoPlayer/issues/2843)). +* Fix crash when switching surface on Moto E(4) + ([#4134](https://github.com/google/ExoPlayer/issues/4134)). ### 2.8.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 34a3eb7284..579f7c45f4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1178,6 +1178,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // https://github.com/google/ExoPlayer/issues/4006, // https://github.com/google/ExoPlayer/issues/4084, // https://github.com/google/ExoPlayer/issues/4104. + // https://github.com/google/ExoPlayer/issues/4134. return (("deb".equals(Util.DEVICE) // Nexus 7 (2013) || "flo".equals(Util.DEVICE) // Nexus 7 (2013) || "mido".equals(Util.DEVICE) // Redmi Note 4 @@ -1190,7 +1191,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { || "F3311".equals(Util.DEVICE) // Sony Xperia E5 || "M5c".equals(Util.DEVICE) // Meizu M5C || "QM16XE_U".equals(Util.DEVICE) // Philips QM163E - || "A7010a48".equals(Util.DEVICE)) // Lenovo K4 Note + || "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note + || "woods_f".equals(Util.MODEL)) // Moto E (4) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)) || (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite || "CAM-L21".equals(Util.MODEL)) // Huawei Y6II From 8a0af84c425a47dcac480381f9276575d47e5376 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 14 May 2018 16:35:08 -0700 Subject: [PATCH 19/40] Supports seeking for FLAC stream using binary search. Added FlacBinarySearchSeeker, which supports seeking in a FLAC stream by searching for individual frames within the file using binary search. Github: #1808. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196587198 --- .../src/androidTest/assets/bear_no_seek.flac | Bin 0 -> 173311 bytes .../ext/flac/FlacBinarySearchSeekerTest.java | 72 ++++ .../ext/flac/FlacBinarySearchSeeker.java | 340 ++++++++++++++++++ .../exoplayer2/ext/flac/FlacDecoder.java | 10 +- .../exoplayer2/ext/flac/FlacDecoderJni.java | 93 ++++- .../exoplayer2/ext/flac/FlacExtractor.java | 22 +- extensions/flac/src/main/jni/flac_jni.cc | 19 +- .../flac/src/main/jni/include/flac_parser.h | 15 +- .../exoplayer2/util/FlacStreamInfo.java | 54 ++- 9 files changed, 593 insertions(+), 32 deletions(-) create mode 100644 extensions/flac/src/androidTest/assets/bear_no_seek.flac create mode 100644 extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java create mode 100644 extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java diff --git a/extensions/flac/src/androidTest/assets/bear_no_seek.flac b/extensions/flac/src/androidTest/assets/bear_no_seek.flac new file mode 100644 index 0000000000000000000000000000000000000000..cd3271178bed59c0de3582b8f92acae077bbe66e GIT binary patch literal 173311 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Ν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%0DNvvzbx< z>6<$r*z(0Fjp*!25d0t&N0eN)i=g0@)Yn53h?}Toi7lwagpQj&!~OT@zHD~r8y#Ba z#SOchY?J<$Lw6Q5T>e@W8T0J$<-|+f$rFXX8Xxy=Dd(Qcv{7BJwMY3$W9)_*zNb%L zw11>c%+pNGHApNGs&IjS|ArQAyglx_C7L~% z7%bZ9y0Rh-d2HpU+|v>V(G%0gpz|pfHXgF9>Rq!#FM`$29kuxL(2COC)Z20c7dsRy zB4!Re@jTe4oMVeNJj8G^PtvjfJb2~N$l)7)kDH7RJXy<5dexSddSb1{#Lxp-Nz`e= z9S^S2uFKz=;-zO>t|UbVUG(0i=D4j+UbM;e=EtTO`t0-VuvMlLwRd68JrPjTTlSMB$*glGZOmfiKG4bXEJdOEl8>Ps2EcIxbMarF~yddXdsInds4ojws;d-JhcK=zQH zjZcxk2iXC;vk0G_*m~kpAAGGMz1)7zSh5=*^=_egTL6iwI#vLsdSdI|`P~+ooMkMF z20K}Qv*jFbT^QrwW^_jLdbRcPW5C`)MtGmkY`g`l*)%GBe*FTqMuRplB|Us_WVr&R zb!%_3hlUc~k*~HkF}&`Mm!7-*;L7T?rU@UtR-8DmP`VG@s!g(K4q}8RX>s&RD?ev! zx8PQE`R?6;y)q$x%NqC@+48ja>m|&o<=zCQ=jQxxj}xG8hMlDMo1OnC`objq!--7| zJ&e1~yD#2yOgZ_sD!a>W8@-^achVo=t2X5kbJNt0I$!-;t=M z-SD&*lLzZq6SVE}tC*jUt-`i=l{#v)ZAvv8xp-{-z@gKDCC=QVp6XFXT28Q9``9XX z*q5P`+Y+1aUDon5`Euw3dd;a-mM??ZE$eXC_&aZ-wADBH0r=MH_UJOL<1&B4g85Uk2=8_uHsVHVkgEhHHAo8GNPFE==Rb9b?VWA-}&u z+h2qU)XqQ78CCt+UAjYRNbJdlx!C@XL1}?DZEHD2og? zDGVJ{JYc7pmD~2lnW~YNRC|9M5(WEZm6AWb@p4_kIpX|2z$U(Nm59J2So)d|mI`yx-V zyxGzfv~h)@r)1Tfrt;NtC#yE;*@(1qSES+RXVY&A@Oxh-oEU?yLTDdoufNDINI0(Qo;zr_ zc5!sYWK~;9f>Y@IX?ub%{{||fOS)IgX*|t|iPg6?rvGux)29$^SfPw&P+{ioD84*sbVU-9=U4qSI@%5x4X>?KW#|xT5w_=d4s?`8yOZa`}xL!E$x8+ zDTll`JF`9||ILC?-w&Pq;w2pFy8}hUW&I<*4O;k=GE(7U@zqhx@j}Xe-9Q7K_pajfub!S?DwmU4=?}->TsXV&Yv1u> zv4_6D6FUET_IQh_=0X(g_Vu^p`MMjqSK;ply8693<=$SrXY4oB$8i5`$k|Ex=AY-! zsk&*fBdwx8Ybcf24_jV>-Ila&B1BcpW~I)U?{dbw?EiU{X6l)`YFW$UH*OQR@nW)lAcEju?{L96w&zJ1Dd?O03_MDTR z;L9)wB$%q$UPdxIxL%9CZS6KlUc(uyi4_NQTVoGcpdLN=p5700{&;!E^o)lA*EVcy zaiz6BZ0+)$yXj5a=~+=*M-R+iemO~6s**)+mvFf$Dr`sNC=S~$wAc&V%4=L*@?|aPQ1=jE^xdzZ~e+eJRi(!gzY?4`!V*8Xp9|CB{H>M zzSVzxY3Kf;6sN9_!_8Cy%_hQk8uI1l^BiH@=Bf){*G}*T=0kDTs@aVLng^o|6LWPk zE0Q7$@7&k{zZ7A$t%Q}Pq^m9m4aIbZdR|> zzS65>zWa8KamDuyvq;zARW_nbyU!aMc1T;&e0-j-yVJk$pnC9nf56(dQqGelv{y2IO6dx4-!w zGcrDX*cZ;~cclIvyW*Pub7QA*bsnwF%6)d*-@lWy+m;4_Z5@!Qh+-HxidJ}>C{_<9 zmNrM{pe)T`Xb6chWn1%4lMIxnmX<*=1P0Kmgt2z(x^#*#?a(G=w$fU?|T4n^KS|g1;nZNCmaj;r?&~Fs%jQ z5~Uu9SSWn+YVbuBbge9CA`&Y)$DiFtgmQ=A!0Q3FvYjz|s9-QnDbUWxQ3X`cf)rAC zH9#m^hqO`+>!P;MO>{;;-;tEf8Ww}xBpad=mJ0#`Pbe@&v`~ksbUy*jFQQHlX6f%i z1$JQ|90?JCA-5&cgT#k|g{Y?~r3NujsE5Tu2z>xe$E!gDHLry8Z=U*(Qz)r%tr*CX z#l&C=)a4P<5doP7l2HpE2e8!i_f`G?X56lzN*CDbN&_ZmP%Km94)bB4Hq{DdW(p57 z=6ren4o|p!55Y^8DhosXwnE(+fru*QcLAXaI(}I9)L#D3= z7=9cs;CF(pF(LvusRu1BfL;QGNCCLEw^9GemB6SK!wRZI&_Xd&W>k!USlJ9} zi2;UPihoE8P!-B{LqXqG4_J;M z2%sUM(WDgsc|gr#MAW#F!C5QsE5ahd2z%55Tyt1h2+%fn0ENi*G<#K`+Gz#lUVkLD z0_c<=gh9Y?l|4J9u3$8k0IyTUKoHUr43!t)9l}m#14Ls$wg9X{VxB_Ap!h;a3C$_n z**P^#Ud139RxPZSGKQFZi1Co7whUjfIE1GUa6oSocvfnG_#EIoF#>@f$j?B|$JYa0 zNRXhB|7lYIhfq5sLP%kU<&gLqoD794Aeso5;;Cv3R>0q!0^O3Rn(xs zr-%=Q*8=`?LImTl@D3?}Dezid z1$1qYug8EJ4oGkwm;r>Te1J~^^6LSBf5cOe5^%6-K1fx$usP5L;Jm_^=yG<%oyz# zsDJHQ8iqK z$%^o8zm*ZmQG$wN=dHbZ22|4TV@jtSx}fWdRQ>}UF1HVr{8y-}3-PH5CyZ2s;N^&k zH#_gKOFDutSJI;W8~oI0V)fQMY2k(2)2smvCvRpW{P17DzkZgTaOTU5vjnAXt0IDO zg!ryAuHcxzicJCJR>N{$KhF1dXh2iJX0e*yEhIWGRoVyO!NfU z*SGvm_o4d#c%tZ57~|j_@=I1cE>#kFm~5c@a#FWTM>E)gZjZo!qi;9r45Z(wa$)xA%3C388e==CukYpm zdQd)b_h|OBzJo`${jI)}=6>bv>3eeq_Xx+=LD|b^y;OIw3ml+n(B^Q5yq+50_`bbq zFYwy3dQS6r_|N~ULvF+7-WRm295HNLE^| zqN*wH$r)CztiT@~nAqa)xi^ZqTQ!?hlq&)L_ww_sH6gKAxJw&{SryxFzuspRaL)p> zD)Gzm=slmjF#i$ixfL7F-csGQyC+69kHxo>ZB?gmQtuoIl=nuHuHGdvj8oAiw9^_m!kng@f zC{+`Ey=MRWZ}~X}B82n^??30X0tXnO9^;igT(~Wxuwuwi|IE54sl}q3MoZ{gI;^5G z&+9Zt(%iiZG4r=nO)hS}x%5a5>%DS8n&-t=@3|tEsWi0z8CD z%CnCA!8<*xzV31dKOS;O{l}WBz3-R5+O@SS?=-^b(q{KB=Gxzv4JkV85qXOv&lX9ohGojzZ#DqFou<^0h$FmG3Q(^H@{d&kj z|0V8!vBYTWG~;2V5A3Oq1#eMy%|!m1_QQfZ$?}uojkl|e@rL)R^T_KxG6;U3i&b)p z9dAXWOFQY6zMAjdh8`@Q-?LX?foQgj9Djmg<;aZiyEScCDH}%;qDt*tsEbzr5$my+}@8=$YUrXg+>zuEpD6<&{MDAQOb?uWe>22X?K@ z-5c<_BC}ed%MBrGuXHy$qSPAXf?E**9}4e%=ca5FM&0boP}#oTK8&)V;e4#$t9XYt zik_Tt}jX6IZ><|T{1c=^+oP24{j2C7~)EwTU4^4(;{t9FFuY`2HR4q4B>hi1cB1@DJjwm-V{)p4CqV zSSItoW9%x^KP}u{{WpK-Nk?yIOg(Qcjxb+nhI(I`4}V6QIsADrb*KBphIjMvrtlx0 z7nB&+#VsrMQGSFcj>7QxH_jaGa{D^%0IpuIQ1T_hMNMt4Xu0m$yPLVMM-ZP};=d3) z10N(=;zy@V(3vBza`G(e>hA7%48iaJ1kuXAktjG^lzVgItcab%NSb|7a!Z}=?)qoq z>s|A^w{Sm3|Li_`=G%Y4$FJ8NI`A=M&Cr&wgmc>wRug+@Gb&DVvI~u$E|k?@C$}13 zyrh%+BOOK6;P=>iyUw2)o6@+F+28-uW}j!5EElqj@Eg6#Jo8%iTi*#tV7=|a1y{j2 zJr&3@T#atVt_z}fYHccl@^h1YZ%Bl6SD7=)WZ4;lzxhk@nztLZ>Qhf&^)$_Ly0)ZGrTc}Y zTJKWA(vT%VOU6`BCc~_z#~F61zRUimDte~e!!ob>2b(QWn4fjlH#awX`x-hvP8++Q z`XjX=;ZpY_>blKU*m{CrMN5UY<+eP^CqLtM~HBK^hjIA z_T1FBYv7(v&RxGx)2YA8svFNwRUdRLbKldWwSskWa>sg^MTdU&t*I#MM{R4CqPOj_ zDs#Pez%h3u%VN96;?SG)xqC;qFD2CKt|X*fFg*FkDJMqdCKWrat$bp5?+9AU{9pyI zr#q>eU`;n1&D~G(6vx}&sQFO$w)-}#PE$)$CEhdl_n#|TiVgv2#|O;2JD*s4eBM~c zZo9O?MBJ~1)+5!ujJ$dA&hwzf+@}REgU3l{?wvnrWqQtwxzpk^p`3Tj)@G>W)=1JO zHeH)UnjbzKJ%U42v$XzeTsTGx(?P4xx7yg~#c?N-pt7<7S4c<5GsEB%xmhsr4}pj~ zyQgIN!J@e4{8c-?UGJDbNeapQb$>S9J2_ZrVp!XJeKlLHsVw%TQqrpUGcTJzs~stp z-ZYtn@^8jNqD+|;C;HyS&Z`Jd6$WkZ+-#VK`>AE2)ZBI^aC@UFh zh3W06^Burtx#R3sX2^Or9*<8vg65Fk#G_Sa@_Nm^S3W!)psSZuR2!{P9{&=4O9bD) z4OyYvyD|Cq=^}U2JHJ=;u^wQ`Sr3umZj&VCCG5el?fL?!iTv{C)khTQ-Gi%!KT>iI5@$x{k(rCOUcIIAqyp@Sn zhSI9Hx;nR4xoE`t>-@0)zSY%KzEq|AR=n|Db{O*A{e3R)lJ+d$a|WmSHtyH@d8wJ| z`uk?wH_yaVzmIw4FjA58=Z25msG57teo}EIpf_@snBdkR-ZXCaLEED<*XOTsG52Y| z`tSPC{#Hw6_cidBH^W~=@9690$L-<;K=YQK*dPinQ^*g#7hZ4r7HL^G`zSs{P+@EM z7n!TSr1mm2XE!%t$M`7#iwiLFE?0dg_@(Tw>AoAjoY6An?j`ncr)U@^cWtxv78nadsx?&%^ zPD(h5YuqBVH`_5jFZ?)AbyUSvWphVOt7|}K$1Ozmygc&nc4eLmLAwdTKS#NemlXOq z&tmGnu?R1j72ickL8*P0OvIbwNoz?W;X&Fas! z^4Cq2X_feSYoRrq78ae;U7{+8ZxdQPe?yzcKa^2ZE+sJ!Zt7X(dgVx9a{HmnvEF9& z8(P*yKGx{+TFVpGIePCraWYl+i8$}&X0KPtcg!qOdhHr`QQb&0_t~q5w#GIo#FW$D zqS^{SDK~nkyk|%ti zS*LaO{ah)M;LOmq-|A+orsns%J=HVtFf3T^ z#h!!THyquPtO04(R&k70T^`8c{91F?X=1g98oWeyVCjEfBE^s3n%)d^qyCRGo+;0B z@v#L5F?vlH>Xa~GOI#lLt#VOMElm%Z`2BT$`TeG(i=EOf$5?!oYw>ezUslq9p6w@N zrtJhf>*2|o;Jawmfl|x2{~h)Ukxdb2$FKh}(O-FJ)g~j`k)roTE&YCk8J_Vm(%j4W zv+N7IR(t$1NVAEbN5l* z3u9gTjgJl##MJFXJ}M|4Qje}`-8-@R1Y=0D&i zAo|x({hLvHM7Lv^c$w24eadVzUgWdY!=N{TxsSAHVR@f+8805m=K8ei!Bo=|8h&?q}46s$F2R7(L_^K)x z%}nPw5mpb2#9BlQfiTIMB8oew3}?X!aMhu}N_FcHtv+FmvZikLDb-BqzeXzH3J+$} zwG>EX5crKBQ-O81CV~-3T#gCMC_N^SXz2`EWUf3U%2G8PU!cn9Pq)P~gl3?zsx2E1 z%hgi@CD0VZKHvohn_=UyTquHs1dCG~XsS-x{R?UYz~%y)rrIn8g4Z0YouRPao)K-7 zo}dqv~@a!7-9XjaUDjlDJ_oFpS>Sc%<$$W5!Ut&zer~+=H9ds6Fh70VN+TWa$5PZ2eem=k zYz%HMru!V|J*L!4IkJjyO^7QFuV^BsI}rWp;to-adkzcOpU~i~fte`Jg0Xu?9gEKRdW%4dlNAQ^F`(2*2JsUEtbtvY zE>o7v!&ADDC@r}sOk&+3tYOTyVx(e;fMPC-)4~BKw;du}AEA~d!^L#~5lpi!jgrs7 zVJKi8Z4Rs`IojD7aCd7Hh^m4+tkqov<*@J&GP|z|Vx9x=i6MZ!2qexFv0O1=ksWwS zWmZ^z@)Q*8m)U(KNNQlcuTBJjhQ3`yZI?^F6r9livHX zeoyVubNF^rxjHGa!Ex==eUsvO+Nj+yqL}-4{_QrI$Uav%XF- za)&bTk3eK%$>hr+0u@o;RE!LncitErH{weR6vm2hTbqE@8!90rcpGXJQx zhI-cBh+Dd-<+0vB7ZSy27TH|yQ8wYdGCY}oXDd9sToIz5dECzg2XjO8bFs|^_^eU( z=)PxJ>`*2HT2bj_+*aeb*{+yW7iheY(h3W*E)w{2xczV~xk#%g#RbT7Dwi%zOQaZO zfDUnio^F9!78lI?yBa)L1K`pL3fH8D<y4! z8ula;^^ne*glHm~b?=-u4*)}@LZHwR@KwV-e89CRvG($b6AD}W0h9m@6NCE+7(qEM z1{*^j9RQtLkeyQ~K53iHG(aC1IILvxR9`ijiwc}(q6VOnYBF#%6X=8NzU}F8C0b!b zZdfK^5CIsH@H!<;a2p5*X7%zRM!KeH_*#>6O&|cGnCXVODT5mhjEG?62$4^mmv(v# zVk!X{ScHXPl^IA-l`Zs0cc?;Qz?~UjGtVaU5%tuq4Q16eY0FI&&4IZXVLcI%l`*Kw z(9Q!t)YDWpS~0FQ0B&+9!g6(Fc&r5kswm8s4+9p3@UoWNMHNi(IlLy3xrEiv0rh1b zAaLZEcV`m99l&JXO}9Wf4zN0v;NZfD>(oxyG*MN^@0|hAT1bzBi4H@k#Spj9Wwt>~ z$&@CEpHLILVJOIGfxPqkD zmIy!w;4@BA2ch<7D;RSmZ!bL-gJGRhLjfPD*1#XdXT=$H=W4TLIDfEK4_~gNiNxe! z)Aea4C(|+_Bo;{B9s?@HOd~@8$eV`UCBCWy13vLVjB!Yh0nkB3RM50!0xg*}(MMAm zd`ddQEm;iaVBq$2cnC+pmf>k!?jX1Y(TQn>cjTwXXtPGZVRR3>iU@~G83j;iHd_(v zhY(Obw9_3-SmQ?7K6@}c4oe{j%)ycv(e;q5ENAO|f&cPoZ8yr2FG~%Uyjt>L$+smx zm)vftj%ci3E?80cbSZmpx^4AXMn)KT45UspqaCv33nr6Xm z_Jn7bh#lk_@bvgSdgxinxo_dO2v>i6Dt;Nq zyZ12o;D@V5W=-3&{<7-qmd+jj;R~0N6aPr&d2VCSh?nMb5n~SHlpo0fi8Grfq1X1l z3Ym#;u6|XCe%E14N>Ulr;mOKQXD5XW#K-@98XU`7M4n5Wer}^Z@9`+7W$awXo zEY9_<0_W`CUbBy(Mp+5IUl`^lDlcN&Q>NnoATg#iTj+Dsl$)`i%(Etjk|JrL!PIQk zORgq%A#Y4nEGaLx;wYA#CLsZrEFZ0Jm{nG-`Fzx@xl~o8GpO2o!twin?%Ku+Bahdp z9p2Ylzl(XL&*SJOm@20TjSsNv%p0U!6IbOoHT(9mZH_}d(kr8oX2{D&|GMzbn&+oUVX7ft$X?kG zQ_a{{8=M<1qF(#8ezdOn^ZGONM})h>=NDJoMs(66zlXfEIgpyGbcZ;nrcnDd8mj}p zz@YqHfe}_)qsCf;Z>|V;PCbr{cK@HFbB|{F|KoTUYI7;fwTs*~Hup=)rE;0u%x&hH ziZL;lP*jp{gv?xr!F=D1DD$B}T%00TGh*0QCx8K{(e;hmec)vgUoa6a^z8;Ss z>7~bS`7zwa`TK`za~uCVh&r0!Y&pU8>?g^utX8PWRlci)XY(FB_BiIGWl+N%3_830 zhmgpYlPC75O0o{!oLO>JEUM_JQ1xZp_McsS`1{WCtxyc<-d7 z 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 f zw{p0thHWJDZT9B^g@$rd<4k@)6Eub9HMyd=mSmKBTX;Fl$w=^6F%L$Z`nj4A_|JzS zb23>iBa_5q;Gr9U$+V*7YpyVpejJ)TpA)(!4u`xQ-ZC~kP#2fS8tmO$oB;; zurD%CV1dqGSB|;+*d|2g~s|du}u;b(2$%>-F zCm@a422X5AJYK!DQi?0jlSJ7G1b8$U(0vKM`WT27*XnDej`bz{WxKh$FvS9`2~jL| z=qYVJPwWm1b;uIus<8yZ#}4pm-jQah!RaFiSuhZkkAa#^WG2YV0)D!);a}{h<4^fx z^2hN{8k8K?{n`J=yS{X#;DWW6((ayQWy@zP9WV!jp9mh2A4G~jr49}3+OWChRR1T- z2=(Sl{z}aMZp5A+usC?a^Jw%?qUf%&hCW{GQ@m65jDqPgoqNMce{5&lTWREXo99ix zds=9&(wW+-UAXJUp43AIJMVk+c9Nam^7q{5MGwe(MEfe2>aO1={&pxm=bvlS*M@M_ zwL6$CY?H{bQS-xy#7;(?woUuijI{9@i4v6PGv&HEyYfC`3o}@pUC3U z(XAu7T{W>~Z-O$W^mD}MO=Q{Q4AD zvG$@qzgBw}A4qwPxc>rSbJPB5?9^TjeVT8Q?!c_{H8JXYWyjh4!gki_Uvawz8&HQY zBlsp|_39pHY7g@kutRWRT!o#?f>N!L)4k^Fxl!d8$ef#z(N)Z4+2b8EQSTSm8=xG6 z6N7tG@|%7sdroN2-qml|(qVB0^_6=1ZmKzHmEQu^Xzd}dy@K48Hf-DN<-NWt+C2)^|WUKbHq z8_fH=8qc0K8y4NZsOyngpvfHB$6MjQF(%1=e7oB8Il~p*bH{D=?M!J~UUz#w@#!39 zm#%5@ux76L_(|W`)SmUZ`_HnsUBOL-9ZFjm5Spdwni{R!mCW4^&va?Ln=aX(>^KZM#nAV^41NtuPsq^NRD_#0&;+DIq;~fVK+?TubCvwo= z%i_)4Vq)5dlQP$IBsP89{q2uC-sO;WS?XF*?D^+**!bKdYKELWzH4be z-TM`#H(uncYMmQ!rD~<^ny0iO?~Nv{()`F8s4EIyTrPa2I)2LICGBzZGs9+=EoV=w zW$0GqWykHR+cNg_ja&Sf$l3H$=lE!3pNd()dOOmmZF}af>{*yMP`LK9`n>Jx8>lTF zD{RfLVS6kniBx#) zbn+w=wB?_DQG1G0ol&#BQC+m-yJbDuvnUguN-nc_*0wD3C$(^$+oq0gTJxA=*zdA6 zW~Oo2l&8j?-_a3n@82KWT&sRMdRfO_6W7?-nvne3o9etUs#f&%2jYZfvV-@f9%s!aABI%7897KLzZIP}ZO95ewY+}lPnMMYBuQR9-y4bE~)RL=V=;t>bwNnH}Ede@68MDoHd zuiw$-wGzt@HPd`(EO;7%8JKy}!)U7j}j}xA{TWCBT zzN3<^YZJJ1+tZ-6-V?2ide*9n=lPhQD!uCj@0N}{k!+3hH&jChmF<;$u()V%u}k;Ff|H^D4+)URDV?EC5`mwL;NYN{U^{pA%ne&J5J z#&^>%yIa4Nh{v}T#WYW@ZD@|m-Rp5=OzC``d!N4FG4@$L+RC0MqsvD%a<9~$7}cun zCGKkVqV2mxuXQMTYBIuEjRtr3>_x7V67??YOeVVko-cnv`_& z{GZA6OSh69$7(BF+CJ+0zS!~goLl~DRm0fhdM0MeHX@ER@iP+_No#+lo;9fYNTWob zoT4@9$#qJF3=&Elwz(XEGB_~v*|kgYZsuuMj~PbN&K_N#dT()Lf${bJ2c17nnzadEZ)mPbuZ=sYaaK0_ zBfJF3yI%E8H>=Hl$;aWs9EY=8)l{DUYIJgXxGqRGLQXJRosqC(`oo`N<$?_1ulEzm z(l3z_d$wnnA3=AR zaDFstTKhw^dDw#q2iUSSe|ZrQ|bxm9QpNj}iHN%i*zI#Y5gKwC{7z z-XL#$OYoKxjR~9g>!Vi&cb?bn{nWMXUzXhl^$!x}a^}@vr;_wKFNyPvtUX<)GnSo6 zEFqG^{&}RcFRnevgft}gyLfB8$}>*jX*H9}7u9mnBuSL~dhYQzA?HHue)q3N-&kU= zM8TQlS?qo5_W5RdmNR#lZ`u2^7&UIcwN3WQ5&3GmWqXdY@M?rDs-<{w<9%X2kGpxL@Lt{7)%@sn<7+qahQK7t z;~ab6pRcixI-c1NbVqM=!cXe0!WSj;-Qs8qho4qjSeKZ|%_%R9<3~-jmU}l}J8;qL zNRz^ImgluQDv(DMqW#XVJ=L#Qz2meK-hryp_NjcS-Ru0{kAJ*e@u^DueG!w`zeUYD=xDQbJR7*9tAXd;VBJ%=r~Qj4#?NZx~(7Q-9%o?ppKhkuN6m2D^IH z^70QK`I)^li1b@_YJQ{tB+l9D!PQk!zm!foukEeqv2iQaa=&U6fi_Jv9`&0Fi_moY zQ{SW~en}Y4vp#FcTM^RkFAHlMIs9Yb-POJU9H+pGku56@+^BZ za#hilGjrNHGllH>8bZ+{lbIz}TA1`R-QK6ybf2hmyI-K-^OG*L^lrA*+}43dtyt~3 zd5;r{li0;A^p%~oA9V29;nYs9v@jA3M(5=6_G(z%C@kRaXelFWA3n@3IQ;f@v9*@V z?nTRGd#^q_m}d6Jq-sZE^t(<2=DKt)(L-Q{^|W@>--_Y2DZjqpt6^fM9mpK0z7g?7 z-r{vUM#FC2$@b@#hy&lQR}L2M{5P>-##3oX4{=9dubHu# zu6EtLB*?a-y(*<;hRDpK z70alX_GIiV56xW_)VtNzDU(^TnwRH;`tWD(!5T}U@pqA;q~jCo+u;+Z8?zk0jSyG* zxUWwS_dfNXKn3+?Ztspd6$j;!K<9C^S15CrrrE@1O=VOsPL+J2)eDS4Ul zgGf<^A4i`uU!)l>IvU|xJ1zY(O!KXC2)5iV&pULo)+f5|nVzv=2)D-9-Qp5@cS2TV zb!_Oxp%*&JVQ!fI-;66oD_^De(WlK%Uvb0QWItYTr|Za*A*<7IhXUg&@$dFe^qf6X z)QkvI-SEW1ZQp{AzHgFo1DQ;t;3|cR=bL7O!Rm|(tDx3>hU){u`FaiKD=*;XT$RTo ziR>fMJ02yda=Tt>`;X2S-MVHzQNV33Ib5VQV8~Vcv~>5)D9{z{ye*hiZU}8X<{xpI zUC|O&f7kYGtEb!Mqgz+89zPj3zZ0D+xxKL}H7(=9)n30Xoef#h)kLeJ!nva4;)aOL zo9&`iyB=6Meoapn1|u|nZuIZ}8CZ&NyH_S3xsZ16_MQJduo{fw1%@SBKOS(JtR?Sx z7PZav;MM$6m+g<#S7~Nzyke@{otS^H$zuO%E&g(Z)O#=+SCiRfly}Jp5uUM#qG7nk z_nS>e>07_;8p1bMt^K0ANWLRIt87ZS=`M+fA3mHBkzVaCNq)w`nNPhT=o8VKu=^zE zzU7FvA2@jFByvm2R2RF}H0=%hPD+*YRwZNF%-!k)3(I}Eo;e5II&qT?p=XfWw}<&I zXt}-ZO_u=`BWQlm%Pf$-xy3j|_pGSp$!E*1$ z)m%gF!Yz;dLt37`GTkluW~}@>HpL|I{=4Xnt6p1eQfc?OY&d>4RxM|8N$QfkRVISh z8|*!`tCy#jvMRUPelcA|`r?z3IjfVIcy>f@$FAwXi@3|z)Anh5>rNV)9kvm?)^(gm`hP8Uk7^J!zoSl$TRP(2>OxNx>G%45OH9XYdh7Aym$>Nq=QQSbeZ)0!@X=2Wu@H=0G8J zY@&-vnbnm*B(YDo!ChJ}>-u|d-rkW9Ioe7P9l>-I^bUH3l zjMqqxYz7xgkcAVYDh1ZcuypILpygdR)fN!L^Q=KJWAg=Y9}YL~Y`#jWTmYjk#;K6R zbSEQ7mf#OU&d^p@LIsXlfz1NM>0t>Y85C>fP9iJP3-)6eFkH$-pxj|_*;LIEi*N(d zf`)_2DX~LCyUt0~|y1DLp`ASM7CECmpW14x>9Br%s|3q@}vv-W-KDD(@%>;G2ivH984mJ}g z4PpsELa_+q+&ZxH4fDL=d0XN9l}jOqo+##nNj26F12upm&Dtg~B)%1%M9hG55EQn^ z0i6p&e4BrIKMB!jmKeQ?0|cr|G-gaWOD zXj%}1$_JyeBcB8ySdqnHy&wcgEXrjH60Fxmf#8Ytr_-2y?DBdk05VZ%F+xjJlcpl% z325{n^)N^FUq1|Tx{4A4L~|8gu=@eqFW`oXA?gecCzcMI)oZXCDe|^4u?_+yVnK#h zfEI9J&ty;}lb=@t=`va%l8O}2(C99N;{^T~RO5w1O(OJfB2xOr61bKvVILS-E(NQO z5$%s5yNkubDAurXhHX4t)o!7sO14ok7%MDv?jpqkN|P5APF9K}3XVJj^j&DA8}w~cDo6rq{}9_ZicN<7@*s3Z zs=^eDtK@Lm!^p zCObE)9iCM%a7=4u1s5{imH$6@1kxadi=_x81V#*&I_QOx_`)1axT#u}6;&EUDHnsc z3NtBLG*cZF0S$WnyXRA?&FGjvf<}BoJhmHMonpNG?KKAjsfr z)r(1BRzge~Xl0ng=43nLEP;OIrWh07+Ovoj$wA6J3(C;lHt z=N`}W|9J85M=H0<{jM0sE-rJ+r5i>zbDNplklEZq2&t$DnIXpJ63Up&EfzvbWUeJd zMK||CD&3@;-^=gcQnvSH56=5}&XdgtJDpIFQGz`{@bF?13^o$+U?g!nYvkeC<4pE= z&GxBL7wsi`MzgA0sN|>+11}u#=zuRrd1A;EFYu!~_*5f#HJO4oe7SrTKDY=9^iPxj z3xBa4{K?1LZRKy>+A7_;vIVrKr3WjBTPYr4h2gctlwV(}YMxZ5Qpsti)s64OH5QIN zHXu!U4rMreuZf+Eje9y5VD(T{_mWWUvH#+2ci$ecQpsGtal!t-H-C(U&u|XWqv@_) zCet}BP7%DrFIvV^9;RqbDvnQv7+ngCEY`UO)5K>=tj=xjnJeif|8D%GRw7G2vZmJJuPmTQW|54|WaJ+)A z-i>%x8m--P!jft&B~yIhsOmty`f+t^Jte42)Hf^i7FNbJlyTqYsz>|MuMS0HtwGJ_ zEtm2KT-;CiXVDlg-p~|_w}D;iM_A4o!nzeAE0*$g^OpY0WJ#flm}p4EsoM_^KicVf z*r3wvEbo0f=lr9GY5U+&!4H0pcF83fTzQVinC$%pYb!GxwP`XrG&J6Nd@Q3?_umD& zl+3ruBwX#md~Cj?I6F7`nUz8Lm$u`dx-u8ijQkp%+l`1_h`xIv*}@|SwRNoD;gRsw z%>~Ubqz86xB0pvyR2#zJD)IJ~R*{!K_8H@}wM<5UL<1!;BN0i(h&-k|4tkQe_geb1 znqceVK6S-ejsJ8ZsELf9T6~+8R_I>V$gGmgp0QJug~j)}e~T?YNb8);#G<-IRcOKnA9vIoG>MR2{uC$uK3-yTz>1IG=iLm{UJ} zW$&3u#xummZqZ$QmwKz0UJAw#g#L94r!VMVx&8i^Ma~DWV~vap4zTNnr{ZTC=AO6( zBOf*VK8q&>pKRWF#Oz3z738>xdmur`tS5ygRs1aW?}K8qY3%UO*fC?JTmJ>vGNsIP zUd&XAXV^RzucGJ<#K*;sXC-Tkz_N&e4L!J?&(6h#bFGv@?v1pS`=5nZNruu8gm@qoK-;|*P!!{+qiKlZfU zvOD@GRQ0?QZ$^6gn>RXPB`<4WywyZZ-0Znp9fmPJy5@$$awXK2Hl-ggSmQT5ciB2l zOuL8kVeYnJM`!S-&M$wyn;YTDPA@%eK73bBO-hu$@*_5LCOdRKoX%#*IG*-f-GMh2 z9gt0b%2A#3S@v*yIkQo-2 z;*j=Gsd!i*V_axFezRh-z)0=-yq2T+Le6cCdPxO5mwY-Cy(Vb%D9evQd$;Mf*S|$w zbo@YVS|786phGoNMl(M*RdkRw}djriVO~y>-_fIT=1`P{s#9u)O zdiqyzRAfdpZB$t*!hXr<=XP2-7#D*psdAC|*qxXL=UPeWjtt2SzjQjbEtCH#!uH87 zDmv5d`LpVQ>GGXNvCF#F0(R&x^fx9}szSw5D>-Kx zlF$+TEzZvLleZjv45j^9?^L?a%BkdCRkqY>+Pa)ZR>59uZ%C{2%_klUiOcW2cfGoF zz0Xs9%IN*xg1e7Aq{WwhTP%ir?4FZLySjM%t|VU}=VUli#WKD3-u%IvLVu};|Cxp% zCO@raDmGrLc&f7hVAB=awHzTsTFMvOCphG2PNVDVtb%{*)X%k9&7KIsG~VT&$B1#G4{I1gjrt?Bv|u2CSbP^LL&p|cBFXOpUgeG|I>j< zOn&3ta4L=QC#%1067iPRO^tyIiT!6W8(DYhUG#uWqE+I_pIr%~hfQ+F0`B;$?YnQ@ z@Ta0g*(xr{UTl)dKQ}y&q1b0G9qJx4Uim4>4fOn+s;OT z!^dxj_vl7We~8luV_!rf7h7B&-M~U%GDjokoU6Z;9DhCWiZS*3-4?s_D?a+%(^_8# zK01$bZ}^VZQ^qiAIj*SH+rc28g0u=)50Km0lZN6Dn30{I9!_EG?2XwK%uN12MY3Z_ zjK#N7#c;FD$B6V-r1uG$RTG5~=g_tPkW1Mw88<4Xe|WN8J>P!9?K6eKz?z(KwlQV4 zMElb~cgEw5c(Wwv{?Hl$gAw;UIPo<~7_4S1rOTDSG0YDSWVP&k)HZabOCl}i=jS0) z4~XgIfZd4w`@F39LouxdCbO&1eGq8$2ESIKnl>I~`RW_xlSV}0*6WfO{g$wQTyCwC)(X_ECJs&i>!hbJy!SekZo8nyyUC{!=T( znH(qOAYl%VIUfGfiY^tJjJByqVTc!tyDMMDdZVv;KG%Q5PZrXA;+)kNw&|Phm(y73 z^>?B+Dy6_}VL}61+*Cql4AZ>^&!+CS2|ukDy}XpH$?UU-|F8E(Df`^_bo6Cir+4ZV zI(y}gj$Hi`;F|I2WleEl8BdrN`i%Dcnkd;df%h3zv=q~Yer9%|U>K=7O%Vlu$G!PXUPZfL;)&dzClMO_mtV@Ezka;_ zxfr;#GD@4RoIPiA-l=AIeZ_dlqv@O5rJ{ihf`jgPOx_apr&_tpy~LL1@TACm+HT#L zO@$yqxAi}cdvw&yeMuOSHd!n^B1`d;n+n;=L9d!wle);X={?AwkQS~*7 zS}Wteu)?9Fr&)f8nmbp8R&4MCfpN|c%>VTsv0xGkoenRBd=NR2J+3oQ>2%-c4&v{{ zpi}g!JDrB}mkTtyDy8#W_b|zJfnl~9v(65*rN7;=VY5ZgWHLhP53)?~UyNu9LlsKk zCsWLYckczmwnB6DOroJTo>$56A~ywWT^nrU2uU;FGrwbdOYRpUNUaB=R9n{4+NgK4 zzkhgFV2ZR4c9ZNmc~@f;E}@%GT%Q87f2ghtrtE1t!c%bBDXwhWJKeYA{CT;mqjKr_jW~kfBha9 zBgaCH8D9-I2nxA6CsQ}?ZWf{)^5~qX)4ASKl+W{@jnNaU-V97l&a0P%Ep=7rmdrtc zTeyxqA)##Fe#2nKl)zmK;Igjk>b7#UZx989fP>trbo|j!ing(0Z*O6SW zO;$r&2UBSfKJbo$vxbt#5!1FmrG1AN)gTsojw(sK$hw@usPVHftNhivAY!9a(Y^Yg zMl;vSDA{tihkgpzjCD~t#`al(l%=4)n?4sGBOp01_p);NGM?ufobq-RAs5@@Xp={| zdU}6Bf={xMp>WDtJz+}aV_Bu7lt*sUj=KDF1+$P!Ph2fw>erH>e49}-EdpJ0#93Q? zIT`x#$K`Lmwu_HODeL*62Jg%DfZ6Y27s{^ovBDm(xmLKm*eG2wGv;qxw+zQNS>T-b zPxO69epr}Xw9v#LFYx4L#VO{)$+E++KirFr7qX523Om;sMcslxJ0*e$*S}w`w9sBO zlBrJ3(5y?p^GdDUuqE3Bwg%i!xpK8v|up45}TDw>|qxWF-;#hh<# zQvb_>O&>Nj(T6_KL^!d;#eZUk!=?$h)9gR8TDWDmZuh{kjF*8uB*014`bD(2eDW_Ag#cwK!LJfKF(JPg8^e}2sj|+GU%Q*$VwpPE%rom zfn35HP*}3b!M2!`5PT&U&z_s3aN%GQPF-+l0@r2@TBDNsB|20PzhHG_U4mOQ_<-SS zZm!aiNaa7%^#TBq_<-sOD4_RxjBsEi4#0Z;!NqLQUiNgs4pPC~7w|!YtP%lQx?sCY z0c1l^p#f`Q11`p3h()TYVKG?w+b!W-&;<9(Ef@NU?@>^FocR>OwxL&)L{aT zig6PHOOycYv@3D}V{44wpw7#)tT`3U~tOR)bzDXeg$f=mfS}5W+$9Ng%K!fs$l_(D)#yKn{VX*vh4k zpOx4o4Ib_1=2^@|C>Tmd%qwmZJEES5cJN}f0_sNY87jRCu}c(-XW2^7xp z!{{EAHgE3#FQV=954z^+qF~0(S_T-a4cJ~FHUtBbO$Iy`SdJ4StBYENP%HBQo(O8= zXo?qL^;oQQz8zSH8`JN!1+S;D6X+poKpm4Y(PRL;5xyi+o@V7BwRZ?CO!Bemy_ry! zXNbK^lmR;gqLsG#K9R%mPQqTq243J1>)I5g0V z2CNwh36!{?JI$klOcF4`+d(SR%?*IU8N*;!z>5x%mlGv`qOo1i0B1{NiHo-a5PfG@z=%7CEKci2yHItwz zBQ+i<}ypBfDVrWif(yzya7%6NXQ z5Z_V(E`>!Fp?S1!BTpoOJr0~KfD6We7;%WMqfvv$;q%kM-X}<3fwCB^5J+i4K&^a? z07%N3Z5=1h z*!=1uEZq})(&=s&!u_K{d%$dInjDr^0Gv6+x;g;B1QV1Xy3M82dxtWByJvieJO?6r zmnc9?_sorjY;VAV!9pFVRLUS8Aju4{e(2=}C-8s-25b;`6hVc%x1WFj40EPzVLNP8 zxQN?I-ip|wZw+j{+v?oPYOYL;QoduCd}}S~62d#`?RN!KVhybP&L&M{#HBkfX2@r^ zueTnCvv`%wb94ylO`o*(M`?wYD+J0K62;x!uE1pEgHQXX_PT6s20r)cUGo_JmE!j9 zN!KDnDdeOsPP?vqaEE~CwHx{p9;f|b2iZF$Ij0mG5}ruzeIjvECr*6`nrLhT4_eoD zundBr1QX`H9^0gb*$EW{mCrW)=M?s-VnXm)?2SA5^3k0E>$t;Fs(e%c)9A;E+p{a` zH*i;-@U2hB5U2ZPNp2Utb}8iQTh43$G#s(2{vtY>Pz1lya6&A(C(7_=<)C+Ou*J%$ z0I7dI*KU7ad_XPJiFZ&WQ9{V>@OjVM0-uO^6+e6& zo{PrHiT#?3#?zvn_NrJK79FTlMX0%5jY3Hx-DYl5dJ1H3bpJ?{wo$x!N7eCL%hAWE zI;zbA67$sWu9G($?+7Rj3Eq^IsUGIkDR0*XWGp{DnD z<{9!310JHE@p_1R25O>ZaYOI8v^@0RRbv6|i?_^j*s}BeBD%~DQZ3T(G-|#Fe_@vn z%GUBGaaHNnEjx(o4*m9uD;^qX=Mz>rsud<~N_O;a<;vPuXJV_5YAdfsrwDsFw-lW| zhkUwc!-@)>R{M}@5G~C~eqFv_xK|Ul@!69;WtW~+IVPpnoNwVf^yJgqD^<1{uOshP znl)=xR_uwIXeWkd4BdYF8V|EjX~`wsec{v@({J&8D9^0;@*U*OkVN z&(8_%lh?Hlql8UG*KiU72J4<^wZ2fcS?_naTUTMd3lVpEaPx2Uf%Ee%i%qpC-|JYh zxUUzkkN8i$(av6ybn=3_m{}@i@Fc6w#HeKSHlEW*8RtA^@@t!N`{;6q4OZf6h76I( zv0P4=?eagVSONjJ(cv>hP(Lbo(3)^2GV54L}o;60MJvx`$5Wp2*=o85_vx!wMKB2B7)LKk*KM1vG; z@btQmz5mISJ2qthRQ-ud9Xs1@o_Fm*@5-0kUsH8$Z_zDV=QDXvC!RU>TP@HKYM#Ui#)FqRD4Ne5LLF>!E!mVYtF- z-9WqhLNY=n8`wY3uE%b=iw-JP8hswD772{(C&Bo&W%~>?4I6p;!;%S zsbxLb+cLxvkwIxRSJW!YU9#uW#yu{d>v(kPYt8M;JlK8PZK`uY2D9JD5@NWD-%-<(>W6=CX-T9drVPL^Fm5 z*3#OSH2apYlnfhpT`w$)s8C*vH_oW4ws=_HaE*<{>C|eU=EMt+TMQy> zgD&So6rlKMx|a5E^UeZlSz`16^;WEjhYgmwfR8XEWmrGn% zxnG4eO!Z8JRHWZBY%KdKqb`e`otNBKT{l*Ka2hM4U7T}hd17YAc5 zGMmN%s%6w@@NbjwEdIW0G$scHQwc`{ zDA6tZRsK;#QR>IUMLJ~G{q1C>jb!xW)E9VHbB)IMWB*#D?zEs!t%)zPZN5PX|7sfM zW6C1DiDqm}$TVXoH9YHWR6hlG>cmG_v~R`oOp!s`lts7MP^|?&I;CK`twQq=CRL*4 z^L7H=CXnU%M9)8(@D&C6yc)QKlXY%zS|azr@U5Hhz9FWS^^Z{S;C5#>y4$%A+ESd%jLR z`Dc8LAl1`#d_`utE=A(u{=U;a?;X*n{J27tDNBVT>GuN`t<^h*?$9b6 zIW|wD19*!IvEi)ii_MXg-*$@-=u3-(XU?C^)hdk*S$tq!e>J5+(AxFc9*1$^=wtsT z51kuq&PtNGaN|Pw+TPJXu6NW2!Qrl=>jDnzEuzgf?{B_VJNr%O!kbBTNfDY|DI59* zccahs6FyciX91ovc=x~s-fOWhHbM91WfZxrE0O$?VrXjL8=~Hhei`WIJJ`5+-GcUN z=afhEqd%0SBFy^M-$3zfs^Y+c@xNXb-N?gfOWy+K%keQks!=ZoH(Q>!`Z2D&WgI^v zDi!zuexCZZ-*PLcUo}|$H~&i0`%5?>$QEquDKXP zee~1bZ7H6;I5ueVM^dWjLU-gH@3{?hsR+MYuee3~SbS*7&m^~qgdQD~ zw_Edm9h9uHW$B)VZ^}G)zUIN(KjH81AD8dTAE~I6Xr^UVMna2s?KLk<2_MZkEubx9 zP_R=_;J8<`u|~LaQrMr98#8=?FhuO;ar=~Wfo_t%9O8fw&!2Wv02E~HS)VxAoeI~L}>hT>-w z$?IxdXY(kx!%+fknYx$V1uXDC=U+js#r~tlo{Ifbdlw%WILTh=7u=PfT#+Po=goao z9C25f;3l)cj?*HdX9hidx&fi99nKCoItafZVYIpKDUfmxE-Z*!qa93?L)vg3Zmj*y zvUC1k#1JW=*vIT{*9MELX~TBEGxfL-3hXXceY|laHAa7^}&IBtiM)mWS2-r#_fyd zqywtgYKkBA9ZrPYd~+>#-mxj?>48rkw>39*NKhX&sf}wL41qr~88Kek9DWreG?p$H zrtW{eD0gJNmQa&if#)2(8&8}5@}Ic_RgV5b585z=3AtbAk*StoSzz7qO5!CvGz0VQ z71qWm{Cuj#Isc1KXN~XX($&jn@8Wm*`)_`;$kqBr99Zoy^vpdlmuT`f7M*uO>V_4B z8n9na2ZFF(Ha@b}oAAad^D^zonWJ7iSj! zOQrJdjh!QB%Zuv;+}b3nva4V%)<@uWc0w_@-yHL~Dsyr4M~$v7HSNUTBD809)050Q zP!q`o?*pkWBVVQPmQUfTrAuwb(#7v8gbLCb9Q)OErOWmci;2|-wpK~mmyl38#)}S; z4hm3x07U?U8%56d_Dhff8%TRztX%8JF}_B19Z|d&*UV$-Vwo}^nt4Xo zg#s>XP`_0Qo54&WH}V)D_$oKH2aEff7#HBHfMLKk{=~NGl*b97QbB|VR@6A4v^hj$ zQ`obi9(65nLJe4-1F9219C8Yfzz_lmun^5{=Ln6x?vW*%OEcgB3xO0wUKfr82}gCN z&zJTFs#YN2!zy_*HxYthoXG0vf1WLBQuACEbGx0V&3y zJkb-dl^_`9NP%ONW+ev9As~ie5pbOCDZt)=?T#*(f*Tr!62(uHKx^f;Rk~6=roMpJ zlERYent|$;H{1hoIDEM&F24n?t79*l0mLl8{t6;Y5Rd`QZSPDP%!4;a0Tx=|IpOn> zg>}4f4%kh^4`%f8`8Y2Yp-Bu-bI9sUUD@U)R4|T%Z5Kew|E^S*X3@#1~OT&r%-RibI1Q(em-OG1wf$I>}A@W*u zux{rM3aEO5C>>ck*jog795|rbMJ^OR5U2o|H_A&#N6KD~?h?!cVHSCgs&6d~(q_=G zcJl-=7RfNn(G0)iLB)DcoTUt!hEI_1Xy8%)g^~r@HCL(f_||k z3~R50b%PU$M4(mHtr^Aw?FkDC&%lB~LR%dvDJzh*d$d!5Y6r;!e(p?K#;}L2Mo$PC zq?S+wLYSk?8w6p#M=>6Bk{RUjYRB!8t{m`O*b_J~o9bo*f{wN8p%fE%U?R_6gAYdnA8*PmkRN9f0aU3j z0!9F2^IK$jL+qw#4rpY%fLTT;0W2PtxXDVT*64uELur3w;7Armfn2XGQwIsMSRgE- z45bW!DzK*u3K%aCvMfCv*=7!q=B7}=eGOtfbs1?}gxogQ3$_>GI%HI&HR$Q%K+6_D zv-z~tZeT&7<%;R*c!GXA6IqbcqW| z;2p{0noMyyz;%4WTAI&bf_GTm++0ZVB+s$8ghYm1;%#Y65b1%Ll}uU+cuwKv zmG4O=mm82L9wa5(3ikx#h_0Fi7~oV#ZcCJL)vbakiIh1WRl5ob?w0Mzf=sz&F@&Ie zo@{<^VW(OKc^rh`AQyE&7lK(wCt+~D@?gQZ^q$5v7{*_WM&kfi51>W?d5Nn8;|O2~ z6I7lFZ18FcO;J=BRF0v4Txie$RMPDn?r$fBig9*Z0$Z9}xUImgo6R?ypK)6o(u70; zX{UCOzPRK)Ozp@UAr7DoD5M+FH^X-C& zyK+On7C$k~s~!x@yH&jN%uMs$p(8>!p9c&3J6S%7n+O(~45$>+#?wWlJUb#__1GKfO75*xlZ62{ z%X7_Le3ZmVyMaAj8mB}RU|!8x8n98bD;o$eQH-%e5ao2C_t+(EOCR};7Zvj7IB(dd z^{G7&-hY__e}i~mA_5@&uVjxUY*DFKzVzJBawE0$G*`@&iZ|`%?62WX!wkGn8r}L< zZ&VV0Ck{IWdo391C^AHJl(r_ZPL{*`bN5Zk#?BcH34fjLb~P@L%<+kz^@t38@Oob6 zkF3}YbPA1JQo&40f(*#Xm7eDvnmSz5IVxm-!^W)WM8G@oWA&C_Up^nzZd$3O-|K%7{3RT>9O_H6kV$Thputi1-7_+uq<`^Hd>j1!kgI?4rAyJy*WaENS7r0| zlT|karpl4gL9vlBq_BVAx*lj94I`S+qZjtBol8_cCs&pjagClN6{S^faDy4Bg@cp@ zKnzt7S`4?MI@tlZ+{W*+cdrrBJ6~LLtS7vV)t{I-d$?WpyT-y_Hw zrt;kz9~^;lPEEz2cg6^5-pKs$d+eQz$70=xiuu`Um+pAS2Hgsa zR^pZ4kG)Y$UEM{D=_K|FwY8Z~3S2(_PxIG*Gp{LOXn%91=W6Mmj%~Jwq?HwAPN?Lv zth8o+joV)MD4uIH#oXC_WPuwYxHtJhMbG-cH?`f%4`)*!P~BDQQVW~D-V}VlrQBlt z;cx=gDS&ZId&EGdJLj7OO$0P$`YJVe3juTFzA;oO@@|=|w2_jjhz$>~KRGqZ`+D)x zi37O}FA}4?j#m{ZB)rRm)yqwnsA%c#+-HdypzO6C{TV4|2WzhMz2P`45zI>(mEp5{ zAa5Sn{I%oee1H~NO8%N3gFIS|&Jd8>c#A{kKF(11li!?vtVpG^Li{nxwF>%}f9<@w zns`ZxhaRdk;j@!M7w+L1=%YT2z2>Pj>+ZHQCk-Jfu`P1nXMXi%_|`xU`d+m}2j__< zR_rp*!OkKiUoZ_UT#6?TXgxCctgof}`DDSVt2j|%H7zZ3Jt0AL|K7TL&>)>YQemNa zUA^Ur)#%(&s}H5FFG4TP&z3TpE?J=^9z}OrVln~|3XtgU5ezT5soPtc|Ka02ZrNXPeImB<=s7>|y$1^`uKmAFUJa)q?e&#KqklnJ!z!C4U*^u~JQEvmuX>>G4b7E~*(zr#Pfq%Z%#< zzhh|Xd2H2)wgv^*R!w0B2fgbZzaQ4uv;3872YaACy1+<<#M&;VkH||sl|v*mdnrBn zHR-snOyk$+E6Lx=WQ26rbYbT17ix#F0w?car@C{pYQJz9<%Yr-eMV)3#XX3}QKjUR zl};b>1QyKIQ?gc4CDM;79V{LknC{{!jGLDXc)S?(F)eF?J~|=S+`la~#)w(5zX}dckhI%HTQ0S=Ye9 zt^nn!J|`&UERWE9q#EXRH_7zQ$7eEX%gKF%wg75T7TkRfucp&|=mNaDdq={~#kXB! zXUEz+5?Cs7drj_3&!wigxV;me_>8pG71dK++Y>hN3q7dDrz1kItS}XSPMrD>Cw}7u z;S2G*q0@fEdvi~{476X7(r+uD)#1>w@l6c^CHuXs>=(?5-rA>=mip8K`0Dld3#ljn zm-N}PA`V+WsM=wzZ!^^{vi{I$RRSV{mQ*S{gqu6-&N$TTR??~ZE82SOVXqwew{@bV zm#kddv|53JQ>S=2tyRIsC{ID=NU`UDTeT*fTB^l045<8g4dGROPQcmmiZ?Dhu+%RJ zB5f{5_Q)*#7FPOfYOlW9123zF^y8lwf`rDy)-b4s;{z@YM?%kiE_Tk7(yzWkd2tBV zWN&S%Y#VJQ1(8c{dFisQg!)L$oXA(Jn_EI4oqfKj|2g|BWw)#_Z|7zO@%w`)jR@bH z>>ZHwH1w-mkE?r=Yo78lykcr? zHXGFwR5^8KPLNG~*>~r{CJLR}_sdVRG*CjP{k2EuVf#V)LzBPrau+K784D{H@*4*h zz=Na}=gDiy?_Vq&pCvtXk;^?IRd|PZ+~M=?U3esdEm(cfD8K$$UzECY2ic}V?c5=8 z%>}uHG&P-64)EFBt19)bIn&rJ;i6(?EjNHwR2H9&(UF^HO9}+wdkjuK6WSGhRQuH1 zeb5K;wMJr3A~sxpE|&%8Yb9ui$<#(=FyHcJA30}63dD%x?q%5(SH?}X4k_JLQF+sE z37N_5tdch-7KSIFVIU!ZR z$=ze_i(p_%mcFCzU)lJnwpB%`It`(zQw|4On%&h&9hWLXUTOx>4QSw3d`jOCwy;@f zY1nVBRxeLFkPRPQzaDo$GDq^+Q?0>Y-$P_guczC*GJa)stbe1l-}S$;+JV{6g{KW~ zW%<6(3Yh)7ng&n8h&!Fp80n`rC@E;gj;ATh*2}9(YWTJl`4bVjV%(YN7mck4pZj^k zbi&~e1x}aS4_CJb?oN{2dmz{AxF(-5!|7 znTx9~8(EldN6$MhT>SVGko%ZfJ|&-Sm1u!U=tFJ6J0NvqXA_*{P%?w;#%Sw=LZ z@33*Vy^-YmGu|t8Uj>sm3R;8cm|TZ82TO9fmB=piK3UV1UUaLo>69^l_qY1CkG0tM z#lks?zL18pckX4T_-x{qnHYM*@m`AUNtJ7;OqRJcNIdRtT z&++{nY{@fsl4yR>89HA2XgO~qK9F?1>x4zS>jd##+U21XWzVzf8h&!9rLHVkb%4H9 z{HRu-f~Vo)M})^i$AYj2cHIm1EUcT`=_=f>ROcHXa+a*-VbS}e?+ND@%UtxLtPBl&>v-JxLbvC+ELSGQ%OV?DL|Oi^rp4 zKl)|#x19_ucMW83iX8u%HE%{*Werjv-H<+bXKhcRh1;^q^W`5$1i!ubS3FgKIPJ@K z7*~!(?{`tF$}ZX=%8Wxas`}{-XVnI!K=?;fh|8+4#YBrnV4!)ge}uO_S*K$=7R+@u zPWeyZe>W-}^<#dukUublt(2N}UnGu*061i8?%{1u{-T+t*&Pw#p|e^DjNkk7z1c>b z^|;af9D71STpG+7w!#UxoGvG)`^bbA{gbbO>}`BSya&63mYKr!mC-^ z`ppDBWD^npHi(c<(mgD}_1jOb1tJIo=}55kp|n!}oPF%M}e1 z($7EfTUoRG7&0Sq>w9jh7dh2cny#0n33qF>$Rq|nZ{tIUUNdzPaS9~20V5=2)HCt+NO*AiaGjW}DRxlJI3v$d-+aHvo(3TmmwYSDTgr8+GnO<( z;>0w6PZP1oCyHh6(~MNeJCegCYrNH;8C)-!eUus;&lmk#ZqXVu=IJ2XZ>te%99OKo z=fX*+={$O;iDXl}yxQTMTN?^d(^d3S8;f@o`DXKho672Y zo3ya>9e(|3V}DXyzVF^w)DiyO!d`4d{ay2W;}w}GlO&D8Q?toP)w0LW9;IX@1)fav zH-78>pMqqZQOmmd|Ngh@xyA!g2xt(C0v^jjsAn9H_Y8rB*Z`7=%^nW{%V%EfAwKw< z6GhG-XP^V52xgzWYo{1UD$oEEqyzB)D#|{CnF~al*bFR($*Sa1Gr$_1e`H}B5lUmx zGFSv#xD*5hQpaTZTrmFSmG6b$=7~yhpr}c13N8kSVLq7U($5m6u~{HhDb@uIT1+St zXe{#GARyPlk2AIh->86_19N#G(oq&dsU;hz!?0{lC=v)r5Wq-M16Z#lWl$LeMOG4y zL}XRB}puDa+9=Yy+iY9F#@}=p!A?x50yjzBUJ>`mrlq5f#Z5+aBpe15 zoEbpxI%~SkWD)_+$AP5`WI9TTx)=cjum(glnsh#gP=xGsfP;h;L_InPC>r#~=eA)? zN}(;NwSx4)MV@CX!LjM(_knA_0cqfE(FWK#T}zK8|*gC$^GL1=0k~8Wzy$I^e+{_l|)h$R1+Q?Iq}` z^)M8k#uVm&w`C6p;t?!JJZNl?kJ(G5oG1~}Zv*9GK<9BlPZW?@fcx{ZNAgCCqPdh_ za$_;LitL4Y!BiojBr!}bm5ee_1EChUIzW4tPX?JNefxOm3F?D`gMaW6)J42MkxAgY{<4$!yAGA6dY;x;v707H(n zhD&O5_41oi#>`q-F zeeehb2%&&8U4I*ZeUL`Sf(ITVmqX;BiZjyX;CUCic1CvR@@y3kt6!5H~1)Ni;?sjk5)1w$`k0+BWuhnkHkV;W@ z|J$`SyY*nJYwLD1$JpV(lY`0WB0=*%Dhanmx@RFDuUAq6ua927l-0ZUc)K|~FnAz+ zX+mP)UWqo^DKOTc?U}Y5qkUw;w%z68G9ls`Tlpex*xZT!Juh?kO@8V6BIUWs$8*cB z&1U=yr!7d2^iGt|3TgQ(Vvd?xlgfmdQ6|mP)DpjOW=$=k0LCp}*Nk~mGOT5* zZ#?`Y!$t`kE*>>j5pP#EfBCTM%SS8X=Riw7=rlI)ksWcw++}Y0>>kGupWu?pI=)ob zMk>6Fe1G&!{*o$h{eKSgqxWnX zMfOAiOTVI<2W95)J2g}q&`zktK%b(o?_is+2QhHXy{7Ozg4JJE7etV`C#r@q&5$GY z%@WkjCRuqaANT=9;TM?5otc`x^z^Ecpom30jIRQX#=N;ezamDaqL%Q<7!#~nN( zvGZM1(Q=0^(l&k5VjptdIlcJ3+}#`f`Mb{Lb{(?G;i`GAm@#i){u7^_|5ir9SihT+ z4yk6;FaNm>t5xlK_(kwpil_SIoDt?Xd#XE&r;zc9$GSf zpWLs+&$J!A<-4TizZBJ1K1N?-YI#c36QPj)0s5fY2k~s!Lxn6!5>K~nqH1( z2i}xjzpe#|K`6i5ab)cPOt{JC$bPonZ6rSz^W8AQRyCs7@zy1e&-g0M4eR2b^!K_C z?pvF7ZN|T#KFudSar4uKn$6$+@bXk?@U4+sF@aM$>6^E8&Ntt8U6UZ!%pTWsQ~l&$ zs_`K`w-b8&@tLTyx6zKF|CY=SLPK5mt{^+PsVSN!*MvKk{!HhE_!<6qk?xtf(`qT? zF}~XO838suXpxv+ScjPLSt6!O=h9zPYbeCoIKa)GJnZoouu{^Nx__#2tbRin^CJt4 zmvmtmooIi$bl9T+af$s#5*I>tiDolp!#~dLn)7>R5EV9N60F*JX4)~@IopSzckyD2 zNPQsl;j6J`6YBi>qZ%jT*r8hoju9=u0@Z;+3#VfsZmvcg0QVIrVfA5)CsGHjz_z?EJ zwt`8nnl`d)>)3T#Y?qQ~Q+RMmnyly%k5^>aXQxWcA?a~M`x?4(sF@gH`x4H3;)oH~+ zIRC1I^$K5&ox0O+w$WSESm0;;kYpg&7=p-tINhpgyzr?`Qqa%ilA1dE;6Vd>ZL$g> z@EIp_v_yQmz+UiElj4OiiWxtpuebSMeecT|t@bZ|^%0@YC3k%?fHye;9U(40bk(Fd zR^d{{tejHnN&fV(V#r#%r(f%eLo2hqOZW_Ft|rdQ<0-!VG;;{rO3@q|A`M(>DU+pvU==F~|q2 z-j6SJ-u`nN|30msUG&|1=)*-p=YERIkr(py2y2<-{f) z(Q(ZBcR^2oabKABQj2$y8X?ZINx)EpFU`8)yZm_M?(GJ*!|b&Z2)0%sCpt07%fZjE z-`6hFF4`o^LE}zNgdg1WrmV}Cx9**1FoYFJZS4h2$K^oN1;_HUclKs*PS%U&&?`#x-+Rf4eo9@WAZ<>Q1yliDi?3!*(@ z!v=a;-wzJW{u)OJmgP#vMehHcs~%OxxGi=5W<~7VFll8K%+pD|5cO*GVvW(~JeI1+ zTghK*UNV&V00RNr1t@paXyGyg#qb$pa39Q~G>0K;8J5^0PKYs@4hQE~NBKXF&IFw4|AFJ>M>)&P-R0WYm^)ND%#0nb znPteawj4?Blr9>XA;xBu%9xBP7IJl1n_L~P4qb>!H=R2FAOGj+`8_?qdbIQVJm2@{ z{eHc2XIjtK}1b`TVpL1tmjHhi#bo zZOu@n*QhSHh&Z@7mqrI2x z1_@7DqtdEM<+MA))I)9O%v4h^ADNMw#J0`*ZolGSDVovF*Unhjjm_bUxL3NXoe|fAsrrCmeCtY9G{Lt5ClZlWl%DpIbgE^_RDVB1Ox& z4@y-N#op;rDLd4_hCKN48Q{-T(D%dix5yPY=qh*93_mLq`h| z(r+7epO2=UP|$c(TlerrNq&=E=+_JCI@J-Wk#-+4ZnVy`_q?yAq_rRQJT9&nx@&IR zxPfaCHG1omqc}MASHXi~eSGAf^1p}EL<74o1`4hgHhe-}vkDq-wp-$`+IIT|WlP>)RA3G&n<_ zzS8d2>3x3payFIB4ee~q85p3K~W>4>#+4gMym-2JB&I6^n&wDXO$Rjx5uZq75m9P&e{ZNZ*<+P?YdtsCmj^IIaF`B)I+&^XWlsPS3~ks z!ju>LRvhAimVU?NT?Y;E{C75`Yc`X8OHtap>zvk$HqFhi&pB6#;1r{4-hQ@houWlH5J-fkoNUPu@&FP$)$8J-SV<-E%ukV?^>Qttu-;4h2(tuc71pw^G^US3*(`XZGVvX`+5G49?#JbSOO!g56fE=0 z0lw)^E_|QO8&r1&~_r0gvI^hc=%WlaC8WEaYPsnT@gZ}nrvH0zENI={Ze{Ghl_NpxzHIA(X2 z?KpK2It)o~1CEG&vtK3jmQeq_rr}PhxtYnv#W{L+^+b1+J|u5@#vf}uy5q#Z(-&Pj zjPh>c(D`0Z;^rf*VqTWLDa;Tb`KtQwpoW>t;`M@iCQ%m8O`-)c&ayw5qG5=3PZv_~ z^azY55Ua(mog8814d97*2xj7iFDm!hXQH0$?0c(#Eq>y8cY4FaZ{;VSp4qs?&q8@+ zKX&p-rNx-?52t0?%i#K|?w=GAqHumY=ub-9&Rx^@m2S)S+VOhZ+Gl5%UAfn?X_aTs z@y*)i#3ZqL@7tA`39Mzyl=gCL70XF-Ss%aELM(Vq^{30ENrQm7rpx2h*8?*uz?69- zL&f3_UwZ9K9Edd=Zw$zSFu7%@Rr6pQIU?mpVw(x=E zfX4;ZC&@UFpaz$Ma#BkTppBJiOA`w8tXfKiKrU-frIKVLfHp?wQ-Xu?{lQu70TLTCXNjx6LwgpZUSyaGrS$3w1OJlRLsB(kQ=N-v{}%#1b$&TSS*jURFD8DkIgEr zYvBX=4iFg!2dZ!)W9R@;X+Vt?%6Ur`3Ybt>Dgt7g9Pr!*Wb|er3nsw8=w7R|Ihx;6 zS_R$u2+V#40qBG6l3JE%h+sEQrMH54 z{y3KefR@5Y`({Rux__3vpr6C=XVieED6lEpKsfY#1VAYQwjWl9jf7K{+IpZ7nip{b zvH}c&twBRod4vX@JP1oc=0hj8@TfR{nnXTr2RBvW=E@{Z`6aNWz!=2xfW?TjF9u>F zlF&o6gf8_Ei?MJpNiG!u#Wjlql*Muo8H<3aJ3xiU#)EeRsB(04#ggek&^3et#2_3P zh`4NwbPNas19P>d(ll5s>w=R3xe#zm z860pbp-ihQCkn;wSOS6t5SH~qkf>Lbh61+@+}er-#5Oj0K5d>I0xEiS206lYi^Fh**1}H~Q zCvN~KrJ;C0%tVoth4OCSW-EI@XY2&oJYaC`ykon#(q zBoMF`K?n#P`!dL^_CX zk?=)XtrD?ldCHlD6CMiEdc6fP@WAA z|cxUh}VYe zTNNx+mH$BBC@J15Z*D(?KDB_g7Tu`&Z)Qpvs+r&uR^;(3cEB;fn{8T`p{i#>?7`j_sQTZ?yN; z<%|7t>1gVa2HDeGSP{bQ;<*}CUT6v}MD5e2z>a2kBFx3oyVDX(oi?`>3Z{FY&Ome;uMH6HIC zi=^c++fw6+^Xk?y@?h;O z{Mu0QJG4qp{sz5t$3@0_`sIn3+z#LDbl6aOl`aesXyMkP;`&v5dXtHo%P;ml#v1MC zTKm@u@=IGj4LIJ$O#RH!cwAFw?0VWYf5S#UoeG{Vvy0)KZJ1YWB_o&A)_K}IvrJu% zwthSxjX&TFldaCZ1tlH-V}Qu8=f{o_GfzL9$~q7rJKUBhBX231U1OQFi9X&Qc0twf z@0YA7HF-Rw$qI8a*Z1pa*@lazv7u;X=e-i+?;Bq}@y>^DB4J;vkErBPiAK3H`pk*t zBHN1|{PTCu_2uX8xuz7*V4T}`^G~w!KSy$V-gZdA4RyKW39 z8ffo2G;e5GKbd>|tVr^?Rike-YIi2|av-0!&uvHcI3b61Ztazg=*mm|p(f?ixaQ3> zQOCQe4hsgrk&f^qo#TKSBwbZ9071 z?fSw*-=*s(-IfQTxBSkwba-)V?}GfllP_=d4c;qd{6K${Ch(CbE(BDRU8N*B|A^by zp`*I_=El&DJsq8rWG(jdn%xGuxuaSXFu#DOm&Qhi z{QI6a0(+iR7q{&WCdyc3MpsSWFa}>8@R)qX`n5yzu4)&UC$N->X2?sxLPw<%vTHZU^Mua@NgkPiK953wkqJoEif zVf?wj3b)#fF8lW>{n6(C)V+A|x4SlGl$VnayG?8wW9w)j&-hKg?nblNulMEBA_bip zKI_LCpt-u!*_)ko6n%^Jp=VRS8H61C$8Agl&eR!$FvA&M zi=)$tJJ|xBCLHU%_Q&dz!fV1R`CPwn&y^R?{p`5PSN=oaKFH|)C0m61O-)2hbzS>!;3c`2H!7dukP!<`E}F*R_!=Tm%qQp!T-^{B#%BU z4gIWl+tYka5;NZ?^6HvIR7aX%hk0w?Q_=dBnmNlisI#EOz4<%*obgzJ$HEfHM~XfIYladWvefX)jtJi8=PHAU% zfATpux6ktzY`23HTJZpfP{2I?qem)=^ULVA_Ewpgkel(o8DGODnOQ|GH-6CZ$~Z+{ zf4AcAadgjSJA>O_#i93;U&S{$);gapE8HT~sI8Me(T{r=_f*vI)-NeP&hKi;6L@3O z6+>A(#rzQdyZV-oj$s|}Pjl$nC!M|q$DFN4p6;KfFmEoqaQLK;@8R28Kk|0+TDA5> zaSnLU3%NE*f2g+v8`xXjDn5?j(R(ocV`VQ#R_LA{kDM$jvsQD)XbTEB_*-e#;eqP3Fo&brz~2@O0nj9TDn{x*lMBu zjJm0}&LB(z6Pn5U$RB6YAmy0K6|dWar;?UyB?&G|VXBH%3rcnB)5mMLyL6V&t!xBn_SL!JrKELPER^vt}yZ|ELK&c_qjiv=~bWwV$8bVIlk&~?DJOf z91~l-O0m0fd~RK?gxQ>6(ClR5`sqzjeeSBWMw_!M_mMv6UpR83>dK!r*E-pxIt*q=W||NA58E)oxcX+iz-cb|Chn;*Rm>2RIb2Os2%D!=HT(mIC~2-mL+?P zJ$U7PW5a_5tB1ryx>9W_PdF;lz;Aol;Dp9#s0~*8ZdvH6vb+24Y>>ytoZn$u>y5RD zDaOyVS|C~%+4=Dc*F46~v~(i_;YfoTz1Id_CA}p@6NT7qvaFHGfT(X`%Q@o%#M$Ct zmp_il3SpsiJ31|^s5(0)l?hs2(gE?wznM7t_c7r=7X=9UgkkA-SW3gk@N*cSrb}66-T^h& zKV6zr4;k5YYsJMtZReA^8b=;E4-P!M@Y^Riwqg5YNB0vAxuX*S0Z#w$kmTnZSCckc zUK_oxlQHwUqG+l^z}Gc0(&pdjE%UnN8>GFW%f0%=Mj}4+4pY!H^?J%bUc(;e?NpJi zW)X3+vgAo2j^;%nHM$h#m;>IWxUZXd_L9r|$%ip&G@ZX|%z(ABUw?%$JWfG5e4~sbgSpcPD;F!fK5ggZGZno?ygV28 z#d{4j7E^RZ!l3&v<%ZW&La{&d1JU+7ElO{+u2Ol3Le65O4i5RWv24%5ck;-NxwOPc zT-PaPr6Xi(cXv0r?XOb1NugYMgZ|rWfA&cGeyyH$>bZ%BxEy3zNUGzb$F9`og|{K| zWBH{CsD0$o1HowR1mPLfZ7X`_LH}lo`ag;d(a6lTQ1{E*o%wz)^Gl)fDtl3 z&VFH*e0k@BJf7`nk%79;wA^(V;&MH&Azo!rZKn-Gcz4L}C+1Zn?cOg1#nh*73!{G3 zwFtY~&+B<>depeF=}t!`v~S}OA=1>XES0ja`(Gl;1FEjhLB4LPZ^>g-Xa^r^RPUz7 zF5l!=Kvw+VRPsQ{pWi0Vt^Stxn6tZ8-`%iYX1U?2B;UX#X;j!1Co_>&kXLD(^jhs`Bh3UGDE} zX&6rD*kH3~U5YmEUH8E)=%>|WuIGlt;;ngcW_jZp%e7ydZlYkTPMT>Q9$t6gQ+Vor zR7Cszkkr4sWp_spAMXi(<;OUE%U99Xy@TQOs#FGgi27PH>4B^7c?8ew`;z){!Pe~L zlZSTGNAFG{>u|qg`!6NelZO<3<|rtiz7sUIxKn7U$&L)=0ZFQcdSJ^=ap{+>mjRBtVVc^P)hf7_}R>Ypq~wt}HFi|-DQ7`Lt!7}a+=3yYY)7AIHF9f>t8 zTDFYRVY-ZBje@XQxpJYUp0YIBn2qaQJqnXJ%J|~0oz=- zo(?-(pl=Q;bUYwOs0XeDIMf4h0=;pzIXD*AOpfLdz4i*H!$_Zi+{)vthGVqo71E3@yo29NYl{7*UB6-1ZtGbTp z{9uwS(m=CEzt)Ni#o=fPU}N3SsA~tfx)xB90wp>mfh1=DZLhqa7LA2L!4BO&hM=b- z6wyGd69%F%e|?ZW2{CjmRJsu~^ML7tC9DBnUYXnjJ!N0hoKOzc8mC$T#2S?r1IkxO zln0W43@05McTi*{F%TM+}aY(rFJ;I%##l1nnVIsOEv+x zUzcpr~Wh$I;im91=5HA@{5(3yJ2xky9kw19bOLW;D9yQ>S2iL9BV1m61CG1YL30$^CA72yAB~C# z84P^CXaHw4)~_Jf@p%*#KnoNtiI}ZR%w#n1tZ1`PJqtduJ$S4{530*+fLmIX$KYa1 z8^LB*zXZ>#5rX0!ZP;!vu<{b?$3XN0kamgs-cZY+mKqURG98f*v>m!2ULEuYfIYy% zEVg6d{J=oi67>8Syx=N3dy+uVkJIOcdPB19Ho+-aJQzWbfmykk6mUE_<4`;mA50oF zlgaX-(!`ip0vC!0kWc-39J|Mm%77B!B%mMXhr$Uk3Z%+@5DDOzLKJN2ZN&vrST#}x zE`cu@z{sms0CKc)WKccSBljMVc7qQv6-=RtWGZrmMoYkI%cc6ca{9e;IFSAdz@{CK z7jQrg6C|>Mfe4y}k40puvl^f{!26R3OTjZI*Ec%~z_|l>h%s z_oOD;b{l8Y!Nmdyk?xfz#()iQP_^P@K45q9$i~@|IUeUya;?Dme7t+ zU7(GRAvKfwNg@V1x|v}i4r*`e7A;XG?MQi|7*IK-2Dxk5!!32KK@JKQ`aFO+Kmd0& zcnkL5WE z-;__RlK`<2oC#F#gKS)}T{L(;2oRAf)0*7DTwFgyMHuN~)I$W!R~tqBTzx%7K$sL0 zi-84PHbMymJ#>(&+usj0H5&jgJQp*Z1TN!N#@P}1IDn~?2QEj(7nTg+loiswJ*CK`ef9a z*Vjq972=}WaMoPA|4Hb%SVQ>0V|}ImeivlKnc<>{FpdT57$+?sfJ| zi?BU#uR4d#gxlw9n#$+0GFGeH*0^1?YrTH?$DgNaeLVKg(&PrSFCOJi^;y?jw1!pr zr#F-0rtJjs-x?-)NAIXa=mZazKjXjSZo|G*rockI4?QG{k4j&)97VTuGQY|nr4MHA z3t0SpGS{2Cp~UmohWQMq7@fWISAU!C%s`cLaNFp#Np6RIs>+9|*?r}j-$p}T?I9;! zi(a3q&Mwl{tWK?FAWF?z{kvuk`)3q4@5SQmm6C!DW|D5zi1V2l}j z;r$S|Yj56#%0%JrmivU!SyvSGqVy~v85aY`jVn#k9-ks?lfT&d>&B$-gxjWNG&O2q zmHGjdnE0xmL()jvMXz8fMgC}@J2W^p^rmo(O z9nI=^jg-k#*;WBR*K9fB{Rt`GCVTeXD?3rcy($_5GsGLmbbT#bqcv4NLHj zeYM^%df>DB>qp-rdc)$EI5l`F1S;W#xH z;}a^mX!`6klS^-1BZ~j=PF2G#_r*Tc>&v=3_BuxG#HGVt$*&T3P*?Pg`Eme@4QcS-n+2$+%K7X>)P@X5qd=pjMg<(Cf8r3 zI_@`AxR>d^cSSh$j7rtZbmrQDMxP6`C}*`5H21)f;f2zLs+$+eqa-vA_HDw^y8L?M z>y)(4k{MKlXQ&|LO_u7ZYq)h-_#`HCp}hQ_%zmhsDU=yVc76>Bzjzs zd61B0{m)^(fHhZp$FCK8b$wmAU@T4W=gv7H)ns2q?zu{i9Y5TJ>Q|Xy*uD)sQPfZ6t`b6}feO=;{ zL|4lnr`NLAerS50=B-^CxhqcdXP1X+`b;*`6!F!-rN3tXc}+je0D9wF>W6EN{SOm% zN~}~K?CEiEGL$D32W$zqIbD_I@haRtaB{e}j=17>aA?*8|1+w}qoz)o+q5#Iv52%l zglOZfZM7H6p4A}l>{n)YtVvYis+6yeqfi%Dy-mXmWYw!(H8{j?{Vyv+^sG~B^&|fW zcm4NGWzC~gmGl$*vy4%0cCWj_k3ps#YPkP+ z=X%>2yz9V<$N8%9_x9G)O8U#OnVH*UtPT52a9N;&@RjuN^WZLPgcqT9Y$oJO+9^E! zK(U(*2~3Y0oL>n8o{p2XWD`f-G2=CgShc+lbEtiJ1Y(J8u6Ci2-6>vf8E zzvY=wocngSt6_o0vKC`2tSsV0_MDaKGb@N&=2iEa#rr`@U|x1J&7{zHM&8wg5#dz98JXLF>0@8U86@)}zwIcXlxvrPJ(Zmp9CBqL|FI z7k%%&Z>%&oZaflJBNLGyJw4=a42cP5+wS{Nx;ettKscK_MpJamDKqIQ2(%R-?`RPu zS+6lK`fuBq?!Rg8yp3#bq z364A757lr%Iy@CwN>|O$ZirKgm&g2czOYWwK=b93xy8hI-Wf}jVoNq@y>8Jt<6|U; zM9=I~N^bl!cFi#nKd@O;>ZwuP*x7JJ#l(=)Hlh#19}r{m`vE?W#AVy;131Q=CsqnA zo;yZ7JF%uL*Mmt>KhWIcVp?QueYq_qKqdK4liJqc(h5LkRT`#Wt zTq=E&60)f+PSyWL{1>JyCq8X;6Y`iE>5FuE)9jW&WD#`F+4*j7rR`=LG#`%W+*8?` zP6@c1;^1MEJ=Z>2ld!P|h@WGz9z-MQ9r;HGnQ zF@kw?CMvYEe6459q3=dLf8y8ZtXma+xxF*=voXGA=`|1IQC_RMT;Hx zo!abv>txyf_40ESy`G&XNA@Rsy>(rGtVcSvP0gTOS$XB>t~UbC7g~T$?K|ZH{R0yP z|7P))F`<*%rmOtQsnrAhj{ctqWrlN~cUu*AKR10Wi%s&GW2;3j=s!DW+`>X%sAH4Oyi8;6Zul@XtYPzP zQr6)Y+Hm%wTFm;c3tz{@?^ZqMRi(Zuue4gbcg2jEJ3qn)U0K>lcuRYD;;-2t=h^uu zLVKxc#nW4|U|U?%N%o%){j2_2;F~HnOLw;UG#_Fft*bGMIZPN=N-|Beha$XhMiPGP za!*G@TLjna=eubZ|JV6Ft^=TXGv_xEYOIP6Ejy1X^4&Q?p!kbw^H#3)`cBy;ro=`jgu;}ZSk**JB{d+xX zx4~<6JF6Dz)bp%6-nP4#N0=UGj}5jRz*-))fSz`0+~8lm7F+o0&Z2RA*VD6IF2aP{ zrizlS2AvZHhUx3eQ#Axw>x_5C!IthdG0RuBRVpK5zb(IFZE2D40rOVxrKRcA`xbB0 z*J>47v$u62o#fV|+q!K+TJ}7NHhr~W7s>)b_;<>`Vn|B5np5V(EKQlNYLX`f=D3LIoLlUmIgz1H^7)i(ZJ1@&u$ zr*yyTn~1{%p23?H;&J6yW}EfQab;g5SF*pB?z_D#;qy5N?R3XljCE#EXRW`Z?7k{d0_}$$?Ke_9u8DSm2WUXAme7}aqNYZ;P!e`8E!p)#BH!+hp3KRD~=V+AG8Cr~LU$gaV z@kCw#zK9J>=Bn+YvbXm7))~4J#zDsv3>d%9TX+?2-KqJy*v06M!4P?1+N*AjK;Qzc+FK!62AVf=yrfA>^EnoOpZ4c#=s}6unhW8^p*4 zNWtJQu<{~Z;UJAtoE~H&Ep^f|AX)qjAoW%8H5vi06es&%3XoJ=k_?t#D zAe{CFD%w~QH60tE45){qpbm?cI!R4Uofv32Oc=4`a%v)2v_=|O@x2i=yJ#?(6+y95Ci74AQIIlf}W-i$VnmD)3hl^VTn!&z-J1D4N!c2Fcl9*n*?j92ab6U zY$b!?Y+X)i4JgyWK_#vLO`Ibn)k_>8v0T?Hz-_~1^CIqGw9smalXjP!o$Af2q)*RAHI$bIO?I#hL zUq>W?J+F`prmX-;NE3oktV%zWObV(iCvjO^5V@5=!J|Y4H|2{ZF?ga#Pg?~4e{czs z8YJLv29}EUg5VVIA5;N{$^!#{P#J{4fR~7*0R2y-i>a}FP>YZR|rJwPU^Rd4b*`G zWl2$wODL%Raf84dUSvn&04E3$$e4qTvmCKraxmL1!3?rXc?8{3Qy7cG31s#ID@#mF z42bA~;@OcJWU8YDhR`@b>Kmp5Xe11Lcle>fK-r@VsCuB$2o}yPE`l@$@@!%`NvNk| zB^1zz<|=G9a%n9NR4xP@j%c}4pDT}jHuS-NvNce0ctcqKMdI0 zk)SjRGJ0Zg@m>Tj2zZ-NFdmyiHq|BINmOdk8BKuOlr8|bA9G}o1hTS54_saz&!3(T zK3Px?lx{E?n9^CLLaRCn2l&^)mrMjz;n-Ga_9QX_!DJXb?sBq_Hd|U(p(*!huxj^) z0@aMHA1w7n9#iTRIIK)J5CNw^INc;;23aRA@>;I z)ktQ+p$nc&^=rJh6fLfgB`)h&zHWKs^0ehq%RQGZwA8FSR$-Hz`EMrkQ{Hp&t8TB( z7xY%O>*=y5=TFlP4kw&R4?b6L#rYZpLCUiD>c&3JPMpb-Th4SCYXaCEGTVRZtc!A4ze~qxlt>UaVs&B?~{XHv;j=4p=#5|2k$W%)`vc~<%+fwfv zXA`woY_J2%ddaQ^)6S zOkcn4^5N`cyX}{ctuzy5*Uz6RIDN-|^ywO_`p?RimS#38f$WrV4d+wg2S^SH|2`HD z%FCnpm||RF1(!MBJJVnB7z(-4`Q~T#Go7ID_^zug5tndUKNpZb_+ryxTQqF8u*@;Rz zo_}R3jYBr|*EWQ%HTkvvc>B+?2ctn_UtrGYQr4vmBpBVgl>9du7Ssu#b4infrCL zuF{tJ;mwO3weL-;CbDd3dx$#{dp0^4)n(0@O4Mz>^eX35Kb&bL`>B>(j|}UYIplMG zs5KttpQBx-qgO+v3SQWSydox9+ZWgVLOqmwAGwdNz9DGQtiJlbd`^<_c5TFtqXtz8 z@gaoeL9FC0%Kj~SNMn4Q6GQWO|1Sz_{Crs1zrV=~4knN3qpmhN@$f&_;O$(pv(3{? z%k}-Wj#|)#DJ>J7JCp}5#7{LpYpQ+6^FwQY`nJCF(No0V-2O}Q-0WB9hhmjr)1!|Y zP#W7enjf0h*fmJ&`1Uo6Y(YQzavaiGgh_GI8@vx|0%^0b=gl5tCy{(HBMGoR=0tDdy3|XCKws`MdYHvnKx3IvV9^ zU!Vjj+w{|a*9Mte4P|eJxl@iD?zG%r$MyV*vFaZ6JyZMXs(f5~-%s=|%24KH`K~a| z6`=;}To){z-rmrYRDW|dr=sPBAfAmp=HYm&Z~s za>wN;{TaS+YvD$?m9c3cIsfU=&btOZ)gAWXJI)6j@ipn^>?E{*$a#0)bx&u?%xay> zS68hsRaBfnx*YV$=in-5Yu4q{B}}}cXR|SSVp*4#Gqz?Q|825I%mb2f{NtS-8%3y% zl<2mus0RvXBV#giPQpFuuPt|c>It@A_3lRhc<}%dp&UEu5!Q!FYej51`z?VFy=u6; z=(xB}Gv!@=4m=XUUkv)^=h19|)?M+akZ{Qq!pkyF>Ug~EPk|{FwLJiBr(7{@B1t@N zYtIZf-n*k?{R$Uq``wiY)jXx;Rffh=;eT$9Z!zAV)jgg4(G5^ePp;2Zw~S2M-Fr3l z`c~21u<}nCS-n|w-07+aFN?2=OgE3~rpk%8KQ$PS#tVz8)X_FtWOzt?d4r;2L7^hD zWBU$+lx)bd%A;x4B zmYH#EWu|6%=+2*|34d1==pc(`3Kd$MZS?j9{h1%8ek)ebGI=xfw;jHYSou)+^1qF0 zIR+LAeWD+}zk7$)oK0aJu{MI#GQIyx;xAu0dhE?X3L-k+cdGb5aV~qeu5J)<(%WEp z#>=QS3#SNHW2<&Lh6Rbn{C$;evT%>NV{RhyZiPRNkR^n;Y&pV~zDU1Yw!(hJ`7-U= zJ*aUA8T~a&!R?h``RY8!t&s7w`S`Zi{RcREvA5PFm+?xb;ZU6{d+n=U_XR%ZqzM$2 zS@;egdBB5`iAu>bqs`$e$ZCvn$#BGtd~(U>nA~bzkGZw zSRZrHAVuDK?Q1W0e^Lqb*gc(Zo+$6LgV=cPif>fcdpJkU!1$-{ zy}L4cJCYeGvxRz7q5hsa<5OsO5ILIoVsoW)UT@nkwdT&<*D9-0whxLQWkPP&ncb~w zxok0zc7gQ%Vu|(US^tnkH>WiRue#%!>ML8~9%1+O2LG*k9+2>P&#KbjLSNCN={Amy z%^UdV8r8E-<1a4OoR;6#v-)*gv}yh`bK>Vv`$fbVUwrq(MzpstJL@Jd^u(Dpsc_lc5nm1R!DlYE2ryqD#QO(|K`?1 z^NEqkY@t$+r`FFBa)Em3!cg8lukF6fl(5pPE5F10~D`R-L=bMdwd?@c&IpI?cQY7eSb9i%4tR52e0=8*q6 z6#Ud+X4;ngyt41kTjiF4;(-F;g?rh5Kf8{+_ttJA&X0J>njQwFNZT)v-=i$wd=npT zP+8cX_?+NWX*?SoK9RynblVUH?cJ9h>I=yMfpA+kDWzw=XBd6Sx^;G&J#|rYTY6qu zsX5^Y{eE0Ai8XjyCgDU=>f$+-u+3Qi3Z~CG$B&3>6sPRP%6(=J167{Ff_%rhHiOdf znhUWLEzD~C1Z0}Ax?mMkNv6hw;~uO#cnoXXcJHGl{`2cSqrLY&RHc;oYe!xw`*LlE zh%qxfOzTV_8^Qg=M_)AqO?$UM2u^oR z?CUcSL<(+O%^~UGGSbtA*(bY*BZ2q-lqV*w5_$|l@Pj<0jC*hw_H<-h@$BMP2zuTv zwTp11cEYo4nz4#ykRjJa6JU zvaJj}wbF)}XYh2Q-y@CNQT;XzM||haaT8ScpZPI{JNp*$c=pldC#u~|d>6P`>g|Qb zh>4Ks@+pWy93|~04b5;Uy+Q~J_FzyvRyQqHPb-qnn2JaI&YB0Fvv@Op)|7WHIr-ar z!~khvsJpZB)&00kE&0&q?2?!;Z2CoW$L$TN>J#>bPJ);{yt|f;5Q5X$;+QQ5B^yp- z2F!hq`DK$250A>9Jq?Ast;LcqIoI_T|Nr4eSB%hiklmi6)0m4DF*@7(M`H4BdWjwx z4YGd4*t;FY`gKW7rSwZjeH?jyPsph=e_Eg+Uv7>4Yb!CUBz>xB8-^+k+F+pfi@)kv zg>uqw-S<}hI&B8+g~ag6f z`jwvS9L}#k)lQz;XoX4GtgcX1J!cf^x0rK8?X&9yq0~A-gq_&+D(Hz&H~=Aq2c*Z zy&B58QN1LujIH5MMtG+zf{a`(xK>kBCwjGR-!_VN(OACYlQd&%fw35;l1Z~&dN!3BEo<-Lha<)FulKSlTSo-NyrCdj0^9u#{EKe0p0;i@3r#EAxJpv`V zTA1yefRjfxHvK-yf7XbZyNR{-$DI7nt%p`|Tc)M0rjQw_){lOqp{$|q^Y7pBW!L|m zU*`L7a+%t{zsr`levw!u|6RV?kVjn1C^W^U6_{<)c?9 zxP&M&K=cI+n=xG4U{H5xFb|GkkMP6QLLuNDH@MjLs;a(<++V*$Ptl$XnL)#^S=0t+BT*zVry#+BZG6{Hq`Q9FWIXc-wU0_`0QGr!L zYjVepGbE`#q{!`fKBu3SNHouK)s)5#$mC=psE}#O_>zbmOKUQhIYNb7`ItqTnv`;x z{T{kvZx~vi#{&Xmk4Q(3%+?1Q;qD=)Hyc>BfRY^DTX}$$J^g7%57l~UJRGK{gNfW| zGDZaIUb$Ql=^~~W=T_+Hub_?;+ZPFMU>EppmNtD zDYggMM4n$hTHV+H;0_NmN|8vtY?-`7xi3d~m;z;cNEtz`ykN};x7;@6DHpgH+){(| zso5SLJOXF?GXPkPX@{bHssLN+Sb}a%IRdY2%M(bZn~8x;1{~^jVtE{dLj+tkQ#B3? zyoVwh5s=(rTr{Ju1 zs1_#1>T$3N>I5rOlPP=9e&iD+RZG5KIRgg+ckL82Il!6?TKOo;Yxt(Hetj{|Ke`=M zHE}>d0X1s@I6JIbzqJk+N%q^tK$wH5AYhEn$H*~Y+!_e5moZ?M7)b;li%9!YQ{a+e zuu7X5v=I{cX(o&sEWk0=h{+sTM61al(;m-ZSQY0Da6y?Zk|7uiDyL%PF;plzox)<; zwn$2eRN6>TD8*FQTV?6J1p1Dk4p6t^1^Y7uQ;xuQ3~aEbYI1RyvK<}dXli*J&LF}< zVjEh+hjUC7?FcyAGgdhYYf+$i$f(jc)zJZKS-37pvSj6CA~n(g!UV-Y9^S_SHJ}Rv< z&*O=U8IiOHxQJ+{Q@LDjKraRe$t2mfC*p+wUK6>4ED+nu4YX>VR@0qsrqi7h2^!u^ z0D*w34b9HIcE=5ochkc^&`PzYiEJIytsWEcmIu3-$#_jU9VLo4=A4yKUJK5 zRhuat{`LozV>@=P&8*}#KD{qMK1#LJT{ zRm$+`L0hpUscn$Hz4dR@4c-4f_2gh^NEA5!hkM(q-hN#08+6O^e;hbI)5!nTw*$$a z42xC{Jfrl7S`O}F=^KJwGfI*8A78#Yn&_Hq{HVyBg5c!Seb0YSMJ70+K4^!O+5lO!(H@UX)UqKydzO;0f$$k+^uoDhE6bg0e=@qiEVqOuZl*mE~6BzhPO%TsySHo1k zr(IBZxh-M+386O+T6wUVn@K0RDmg?qDW8Wvoyj}5^8%%kTi&zOW%T!m1NZWFcgICu zG?oYw`SoV@oz@d4mVYfvUfLmmAtMk`Q(2xz)853MJ;M6aTNRohK~a{RLJpzLL{{rc zAwsV*zlyWdb_*}y&rFHNnyT)MRuzj3uFFBeHf@L4BU*XBmL0gOa{HVhB9ugi!k|F!q%Z zxBdV#Fd~Osr$yRcXJ*`PVrd~oENbD7oV~&EYkDDlV{S<==x%M<#tC-10cEMd=}xkY_hklq zcbhhL%N5MMGT>H2H3zl5$`l1TN{kDrw8VL6LFF-Y3@h{`m%y97<^VFW!$w6wk1Kqw z(d*U~x>BaZ^cvOa8=XRs^U7&oR_e1_Z&=wrhInBhYy@BVLVj~InHa+WfNbxJ zZYH8pU~1ZF*Fp;PgSm0gVA?FRUHO}Gi4@$d#WixRFsPzZ=HB6A0RW-}(Rsu_1Mtnh zD9B)?cu)$kia20d6Bb|qL-~O%1;o%)8`5ZkTbT&9fu@saZdNEHe0Mg$xJ?O%13GkE zime48>L?b2nZ)u4RxIU+R&XW{gdmivEC5k}d1wMug$sbNs{<(_N3Q{HBbEdd^dWXM zzi!FHH3QN~&Ne;5SL#8Yku-K9Y+%L)O=D6>V4BTK*ijX@(Fe^A%9yak0A+F)dc^H= z+*=AX1@H;0n~^2}@9NudySW#xtkDhd2f+X04pVFf;*K)Y2d$2Fq!Dfr@F+H;Ymq_V>$VeP zz|L(m0=i+`u2N^SaHoo^?#!w}*bJiC@C?4gtHx)+;NSdL|2*_5U4mM$y8*aLS?33B zv&p5!v}(|uh|QE{nh+fr2L-~m-pSOdG1Jiv_XmGr_$_XG`5T>%Y9l$xdHfaE$KT(xg1EQ0V1o90ec9na+ z1*WNV9K@c?s>%%0TN)TSy{bNAFd9p*8+G80*VibZKc{f$2;Lsb5o|{A#-R6REyz1` z9QUo6R#p0nIzl2<=+X=h>2Ba)XKq1YS%$LAjv;gwnra+o&=(qKnECBH*=5-@+0{i^ zJB6`)bj<{sAJl6A*=2Jr8y|xJI-jj{ah6&tuBZ_I`Na`+etr_v2H$ zUYeh}^oyq-9{>I0M_ZOOyBd04dE(U{pBW188-4tTM_c#4bnfbAj!-8}~2QHCImBihCOFi%zv3Jlg)-SN2^Te|GO@e`voN{#N~? zGmrl1>ePAHPapf?SMcMP-!1f?ysY`8{+TE1fBApv`u%lJ?fcybRebs+zA?Lz-_w#% z4rO*fc9i=~{fPKeape5<$-<9L{~5oo-oL;8&6gjJh_8P>S$y+zedx@1-LpN{-#a}1 z(`Wn79F&}Y|D5Eo{#S=b|F-vPVJ|hcAA3P%q9sT)q0H>+c!$fBuW( z;xY9|)$P!~EX&`a$8c)=#`+`9~%>Sbzc2nJDaIXqxR#cx!q@W;Q2w(gdSPhW>|Z$>c6}F`FHbQ{sTVN zoWH+5dr>`KugBlXsrI%Cn6K-OkG5Igb==B-b6$AdMdZ`v+PE@ZU~ zm^{ejbXPiy@JpF=o(@9o3ax-YRy0|eUQiH0y~eC%dchHh#bOY7r+`@u1HqSa=nYvc z&De5sxm>3JK^r=qm4*VXnxHN&<;doBvP@zpi?Pk(rZz+XE)f1&qX~vIi=8&pX2vz| zCJRiopduvv?s2&c%o}OSZh!$HnZu~&s=}R6GfP>1M`0{F44+i6q(r>EHV{!R&?d5Q zr?pb4mjOCSH=vV0t6D~!B(8a+z!7Vl{ zzh4WVqccIQa0i&&7Q0+%GR)h=zEG{uE}M6yry#zy5s`X}C8sDBB{Fj<0yhyBnswPM zL|0t6P?qUPlYV}SOXy*6j>pM_lWiP6Hq18*YuIGkND$r?QCo#bQ90aV@cW%ABj0mb z!kUw?TU3~w!`J$?MXGj4oTQ-eHo%`-xM5!)<@}hPQ#Q8=MRYzSWM0)JLU;OH_VsmJg_O-qqGRf zh%Cq_(;c=LQp2eZ$YMg$%&)0nkZO<8!QmH55hkMJYrCgyv@GqV3Og}9$K8z z@h}qyHBee5J{+Uv0yAGWmo%OjVLJ^OS}v5bl@e}(&0p!S zhX461eYOA(SaC0Y|L$l(=(~^J1}pk7{*Q;WTMRNwYv!ZZEA{LRuw)4nd1jR48dy(Wdi;qp*hz zv^;M_AR|odVc%q0S2dSr`!X^eE5c{{#3z>6-f#vQsVl-m2_uJtdJN@;CxHf33A;87 z`*|p4m_iLU3!jW|swOFPIi&55Vg{S8QXm`>HC8Y}v^y`;Rr80G!3w7=B7g>ndM?kw zxfvvt4at~5Tb#_mRf25v^e#PB#5p`|MNTpXDP}`T1SU+`nFT6#a!+OqP?e#BaG8ghvwQu(KhenT(*CuZA zqgwe4*0-VQ@f(ly-$Z;F|5$%);_U98$R2~gKGT!pMe6H!X?NF=m#_N6VZZ;ex~Y8q z_#O<`U%7T8Ri_fG)kg;<{3pcXDFilj;V)8s`YT~mf9Q7V2{n%E$$mo*sVDWtE2(+0 zT7zq~IEFH=es5ZTHS*pbm!9gQeE!k8DSmx~m*3NWJGQ~*#{J}Hx5ME`gd0xPBNEtp zGzGn={Pmuz-y}tu*1>)0J%9b2cx`LkomRFhYTz-Qk`s0ukYfPkE4zXYvQ*-s?Foei z(rAG*2y2Z>sAi4#DQvvGfV)0Jm7p^EWA8cMDq+XMVWbQ&Uw#h%*| leg-gwv^CKP0n=?H#KW8w2I8`Xw5q4eGMjV2lxThis seeker performs seeking by using binary search within the stream, until it finds the + * frame that contains the target sample. + */ +/* package */ final class FlacBinarySearchSeeker { + + /** + * When seeking within the source, if the offset is smaller than or equal to this value, the seek + * operation will be performed using a skip operation. Otherwise, the source will be reloaded at + * the new seek position. + */ + private static final long MAX_SKIP_BYTES = 256 * 1024; + + private final FlacStreamInfo streamInfo; + private final FlacBinarySearchSeekMap seekMap; + private final FlacDecoderJni decoderJni; + + private final long firstFramePosition; + private final long inputLength; + private final long approxBytesPerFrame; + + private @Nullable SeekOperationParams pendingSeekOperationParams; + + public FlacBinarySearchSeeker( + FlacStreamInfo streamInfo, + long firstFramePosition, + long inputLength, + FlacDecoderJni decoderJni) { + this.streamInfo = Assertions.checkNotNull(streamInfo); + this.decoderJni = Assertions.checkNotNull(decoderJni); + this.firstFramePosition = firstFramePosition; + this.inputLength = inputLength; + this.approxBytesPerFrame = streamInfo.getApproxBytesPerFrame(); + + pendingSeekOperationParams = null; + seekMap = + new FlacBinarySearchSeekMap( + streamInfo, + firstFramePosition, + inputLength, + streamInfo.durationUs(), + approxBytesPerFrame); + } + + /** Returns the seek map for the wrapped FLAC stream. */ + public SeekMap getSeekMap() { + return seekMap; + } + + /** Sets the target time in microseconds within the stream to seek to. */ + public void setSeekTargetUs(long timeUs) { + if (pendingSeekOperationParams != null && pendingSeekOperationParams.seekTimeUs == timeUs) { + return; + } + + pendingSeekOperationParams = + new SeekOperationParams( + timeUs, + streamInfo.getSampleIndex(timeUs), + /* floorSample= */ 0, + /* ceilingSample= */ streamInfo.totalSamples, + /* floorPosition= */ firstFramePosition, + /* ceilingPosition= */ inputLength, + approxBytesPerFrame); + } + + /** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */ + public boolean hasPendingSeek() { + return pendingSeekOperationParams != null; + } + + /** + * Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from + * {@link Extractor}. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated + * to hold the position of the required seek. + * @param outputBuffer If {@link Extractor#RESULT_CONTINUE} is returned, this byte buffer maybe + * updated to hold the extracted frame that contains the target sample. The caller needs to + * check the byte buffer limit to see if an extracted frame is available. + * @return One of the {@code RESULT_} values defined in {@link Extractor}. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public int handlePendingSeek( + ExtractorInput input, PositionHolder seekPositionHolder, ByteBuffer outputBuffer) + throws InterruptedException, IOException { + outputBuffer.position(0); + outputBuffer.limit(0); + while (true) { + long floorPosition = pendingSeekOperationParams.floorPosition; + long ceilingPosition = pendingSeekOperationParams.ceilingPosition; + long searchPosition = pendingSeekOperationParams.nextSearchPosition; + + // streamInfo may not contain minFrameSize, in which case this value will be 0. + int minFrameSize = Math.max(1, streamInfo.minFrameSize); + if (floorPosition + minFrameSize >= ceilingPosition) { + // The seeking range is too small for more than 1 frame, so we can just continue from + // the floor position. + pendingSeekOperationParams = null; + decoderJni.reset(floorPosition); + return seekToPosition(input, floorPosition, seekPositionHolder); + } + + if (!skipInputUntilPosition(input, searchPosition)) { + return seekToPosition(input, searchPosition, seekPositionHolder); + } + + decoderJni.reset(searchPosition); + try { + decoderJni.decodeSampleWithBacktrackPosition( + outputBuffer, /* retryPosition= */ searchPosition); + } catch (FlacDecoderJni.FlacFrameDecodeException e) { + // For some reasons, the extractor can't find a frame mid-stream. + // Stop the seeking and let it re-try playing at the last search position. + pendingSeekOperationParams = null; + throw new IOException("Cannot read frame at position " + searchPosition, e); + } + if (outputBuffer.limit() == 0) { + return Extractor.RESULT_END_OF_INPUT; + } + + long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex(); + long nextFrameSampleIndex = decoderJni.getNextFrameFirstSampleIndex(); + long nextFrameSamplePosition = decoderJni.getDecodePosition(); + + boolean targetSampleInLastFrame = + lastFrameSampleIndex <= pendingSeekOperationParams.targetSample + && nextFrameSampleIndex > pendingSeekOperationParams.targetSample; + + if (targetSampleInLastFrame) { + pendingSeekOperationParams = null; + return Extractor.RESULT_CONTINUE; + } + + if (nextFrameSampleIndex <= pendingSeekOperationParams.targetSample) { + pendingSeekOperationParams.updateSeekFloor(nextFrameSampleIndex, nextFrameSamplePosition); + } else { + pendingSeekOperationParams.updateSeekCeiling(lastFrameSampleIndex, searchPosition); + } + } + } + + private boolean skipInputUntilPosition(ExtractorInput input, long position) + throws IOException, InterruptedException { + long bytesToSkip = position - input.getPosition(); + if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) { + input.skipFully((int) bytesToSkip); + return true; + } + return false; + } + + private int seekToPosition( + ExtractorInput input, long position, PositionHolder seekPositionHolder) { + if (position == input.getPosition()) { + return Extractor.RESULT_CONTINUE; + } else { + seekPositionHolder.position = position; + return Extractor.RESULT_SEEK; + } + } + + /** + * Contains parameters for a pending seek operation by {@link FlacBinarySearchSeeker}. + * + *

      This class holds parameters for a binary-search for the {@code targetSample} in the range + * [floorPosition, ceilingPosition). + */ + private static final class SeekOperationParams { + private final long seekTimeUs; + private final long targetSample; + private final long approxBytesPerFrame; + private long floorSample; + private long ceilingSample; + private long floorPosition; + private long ceilingPosition; + private long nextSearchPosition; + + private SeekOperationParams( + long seekTimeUs, + long targetSample, + long floorSample, + long ceilingSample, + long floorPosition, + long ceilingPosition, + long approxBytesPerFrame) { + this.seekTimeUs = seekTimeUs; + this.floorSample = floorSample; + this.ceilingSample = ceilingSample; + this.floorPosition = floorPosition; + this.ceilingPosition = ceilingPosition; + this.targetSample = targetSample; + this.approxBytesPerFrame = approxBytesPerFrame; + updateNextSearchPosition(); + } + + /** Updates the floor constraints (inclusive) of the seek operation. */ + private void updateSeekFloor(long floorSample, long floorPosition) { + this.floorSample = floorSample; + this.floorPosition = floorPosition; + updateNextSearchPosition(); + } + + /** Updates the ceiling constraints (exclusive) of the seek operation. */ + private void updateSeekCeiling(long ceilingSample, long ceilingPosition) { + this.ceilingSample = ceilingSample; + this.ceilingPosition = ceilingPosition; + updateNextSearchPosition(); + } + + private void updateNextSearchPosition() { + this.nextSearchPosition = + getNextSearchPosition( + targetSample, + floorSample, + ceilingSample, + floorPosition, + ceilingPosition, + approxBytesPerFrame); + } + + /** + * Returns the next position in FLAC stream to search for target sample, given [floorPosition, + * ceilingPosition). + */ + private static long getNextSearchPosition( + long targetSample, + long floorSample, + long ceilingSample, + long floorPosition, + long ceilingPosition, + long approxBytesPerFrame) { + if (floorPosition + 1 >= ceilingPosition || floorSample + 1 >= ceilingSample) { + return floorPosition; + } + long samplesToSkip = targetSample - floorSample; + long estimatedBytesPerSample = + Math.max(1, (ceilingPosition - floorPosition) / (ceilingSample - floorSample)); + // In the stream, the samples are accessed in a group of frame. Given a stream position, the + // seeker will be able to find the first frame following that position. + // Hence, if our target sample is in the middle of a frame, and our estimate position is + // correct, or very near the actual sample position, the seeker will keep accessing the next + // frame, rather than the frame that contains the target sample. + // Moreover, it's better to under-estimate rather than over-estimate, because the extractor + // input can skip forward easily, but cannot rewind easily (it may require a new connection + // to be made). + // Therefore, we should reduce the estimated position by some amount, so it will converge to + // the correct frame earlier. + long bytesToSkip = samplesToSkip * estimatedBytesPerSample; + long confidenceInterval = bytesToSkip / 20; + + long estimatedFramePosition = floorPosition + bytesToSkip - (approxBytesPerFrame - 1); + long estimatedPosition = estimatedFramePosition - confidenceInterval; + + return Util.constrainValue(estimatedPosition, floorPosition, ceilingPosition - 1); + } + } + + /** + * A {@link SeekMap} implementation that returns the estimated byte location from {@link + * SeekOperationParams#getNextSearchPosition(long, long, long, long, long, long)} for each {@link + * #getSeekPoints(long)} query. + */ + private static final class FlacBinarySearchSeekMap implements SeekMap { + private final FlacStreamInfo streamInfo; + private final long firstFramePosition; + private final long inputLength; + private final long approxBytesPerFrame; + private final long durationUs; + + private FlacBinarySearchSeekMap( + FlacStreamInfo streamInfo, + long firstFramePosition, + long inputLength, + long durationUs, + long approxBytesPerFrame) { + this.streamInfo = streamInfo; + this.firstFramePosition = firstFramePosition; + this.inputLength = inputLength; + this.approxBytesPerFrame = approxBytesPerFrame; + this.durationUs = durationUs; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + long nextSearchPosition = + SeekOperationParams.getNextSearchPosition( + streamInfo.getSampleIndex(timeUs), + /* floorSample= */ 0, + /* ceilingSample= */ streamInfo.totalSamples, + /* floorPosition= */ firstFramePosition, + /* ceilingPosition= */ inputLength, + approxBytesPerFrame); + return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition)); + } + + @Override + public long getDurationUs() { + return durationUs; + } + } +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index 15d294a35a..e8a04e06ae 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -92,18 +92,14 @@ import java.util.List; } decoderJni.setData(inputBuffer.data); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize); - int result; try { - result = decoderJni.decodeSample(outputData); + decoderJni.decodeSample(outputData); + } catch (FlacDecoderJni.FlacFrameDecodeException e) { + return new FlacDecoderException("Frame decoding failed", e); } catch (IOException | InterruptedException e) { // Never happens. throw new IllegalStateException(e); } - if (result < 0) { - return new FlacDecoderException("Frame decoding failed"); - } - outputData.position(0); - outputData.limit(result); return null; } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index ce787712da..69c0d082ee 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -26,6 +26,17 @@ import java.nio.ByteBuffer; */ /* package */ final class FlacDecoderJni { + /** Exception to be thrown if {@link #decodeSample(ByteBuffer)} fails to decode a frame. */ + public static final class FlacFrameDecodeException extends Exception { + + public final int errorCode; + + public FlacFrameDecodeException(String message, int errorCode) { + super(message); + this.errorCode = errorCode; + } + } + private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has private final long nativeDecoderContext; @@ -116,14 +127,50 @@ import java.nio.ByteBuffer; return byteCount; } + /** Decodes and consumes the StreamInfo section from the FLAC stream. */ public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { return flacDecodeMetadata(nativeDecoderContext); } - public int decodeSample(ByteBuffer output) throws IOException, InterruptedException { - return output.isDirect() - ? flacDecodeToBuffer(nativeDecoderContext, output) - : flacDecodeToArray(nativeDecoderContext, output.array()); + /** + * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO + * error occurs, resets the stream and input to the given {@code retryPosition}. + * + * @param output The byte buffer to hold the decoded frame. + * @param retryPosition If any error happens, the input will be rewound to {@code retryPosition}. + */ + public void decodeSampleWithBacktrackPosition(ByteBuffer output, long retryPosition) + throws InterruptedException, IOException, FlacFrameDecodeException { + try { + decodeSample(output); + } catch (IOException e) { + if (retryPosition >= 0) { + reset(retryPosition); + if (extractorInput != null) { + extractorInput.setRetryPosition(retryPosition, e); + } + } + throw e; + } + } + + /** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */ + public void decodeSample(ByteBuffer output) + throws IOException, InterruptedException, FlacFrameDecodeException { + output.clear(); + int frameSize = + output.isDirect() + ? flacDecodeToBuffer(nativeDecoderContext, output) + : flacDecodeToArray(nativeDecoderContext, output.array()); + if (frameSize < 0) { + if (!isDecoderAtEndOfInput()) { + throw new FlacFrameDecodeException("Cannot decode FLAC frame", frameSize); + } + // The decoder has read to EOI. Return a 0-size frame to indicate the EOI. + output.limit(0); + } else { + output.limit(frameSize); + } } /** @@ -133,8 +180,19 @@ import java.nio.ByteBuffer; return flacGetDecodePosition(nativeDecoderContext); } - public long getLastSampleTimestamp() { - return flacGetLastTimestamp(nativeDecoderContext); + /** Returns the timestamp for the first sample in the last decoded frame. */ + public long getLastFrameTimestamp() { + return flacGetLastFrameTimestamp(nativeDecoderContext); + } + + /** Returns the first sample index of the last extracted frame. */ + public long getLastFrameFirstSampleIndex() { + return flacGetLastFrameFirstSampleIndex(nativeDecoderContext); + } + + /** Returns the first sample index of the frame to be extracted next. */ + public long getNextFrameFirstSampleIndex() { + return flacGetNextFrameFirstSampleIndex(nativeDecoderContext); } /** @@ -153,6 +211,11 @@ import java.nio.ByteBuffer; return flacGetStateString(nativeDecoderContext); } + /** Returns whether the decoder has read to the end of the input. */ + public boolean isDecoderAtEndOfInput() { + return flacIsDecoderAtEndOfStream(nativeDecoderContext); + } + public void flush() { flacFlush(nativeDecoderContext); } @@ -181,18 +244,34 @@ import java.nio.ByteBuffer; } private native long flacInit(); + private native FlacStreamInfo flacDecodeMetadata(long context) throws IOException, InterruptedException; + private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) throws IOException, InterruptedException; + private native int flacDecodeToArray(long context, byte[] outputArray) throws IOException, InterruptedException; + private native long flacGetDecodePosition(long context); - private native long flacGetLastTimestamp(long context); + + private native long flacGetLastFrameTimestamp(long context); + + private native long flacGetLastFrameFirstSampleIndex(long context); + + private native long flacGetNextFrameFirstSampleIndex(long context); + private native long flacGetSeekPosition(long context, long timeUs); + private native String flacGetStateString(long context); + + private native boolean flacIsDecoderAtEndOfStream(long context); + private native void flacFlush(long context); + private native void flacReset(long context, long newPosition); + private native void flacRelease(long context); } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 34a6e6820d..7672f2f8ec 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -179,24 +179,20 @@ public final class FlacExtractor implements Extractor { outputByteBuffer = ByteBuffer.wrap(outputBuffer.data); } - outputBuffer.reset(); long lastDecodePosition = decoderJni.getDecodePosition(); - int size; try { - size = decoderJni.decodeSample(outputByteBuffer); - } catch (IOException e) { - if (lastDecodePosition >= 0) { - decoderJni.reset(lastDecodePosition); - input.setRetryPosition(lastDecodePosition, e); - } - throw e; + decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition); + } catch (FlacDecoderJni.FlacFrameDecodeException e) { + throw new IOException("Cannot read frame at position " + lastDecodePosition, e); } - if (size <= 0) { + int outputSize = outputByteBuffer.limit(); + if (outputSize == 0) { return RESULT_END_OF_INPUT; } - trackOutput.sampleData(outputBuffer, size); - trackOutput.sampleMetadata(decoderJni.getLastSampleTimestamp(), C.BUFFER_FLAG_KEY_FRAME, size, - 0, null); + outputBuffer.setPosition(0); + trackOutput.sampleData(outputBuffer, outputSize); + trackOutput.sampleMetadata( + decoderJni.getLastFrameTimestamp(), C.BUFFER_FLAG_KEY_FRAME, outputSize, 0, null); return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 59f37b0c2e..298719d48d 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -133,9 +133,19 @@ DECODER_FUNC(jlong, flacGetDecodePosition, jlong jContext) { return context->parser->getDecodePosition(); } -DECODER_FUNC(jlong, flacGetLastTimestamp, jlong jContext) { +DECODER_FUNC(jlong, flacGetLastFrameTimestamp, jlong jContext) { Context *context = reinterpret_cast(jContext); - return context->parser->getLastTimestamp(); + return context->parser->getLastFrameTimestamp(); +} + +DECODER_FUNC(jlong, flacGetLastFrameFirstSampleIndex, jlong jContext) { + Context *context = reinterpret_cast(jContext); + return context->parser->getLastFrameFirstSampleIndex(); +} + +DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) { + Context *context = reinterpret_cast(jContext); + return context->parser->getNextFrameFirstSampleIndex(); } DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) { @@ -149,6 +159,11 @@ DECODER_FUNC(jstring, flacGetStateString, jlong jContext) { return env->NewStringUTF(str); } +DECODER_FUNC(jboolean, flacIsDecoderAtEndOfStream, jlong jContext) { + Context *context = reinterpret_cast(jContext); + return context->parser->isDecoderAtEndOfStream(); +} + DECODER_FUNC(void, flacFlush, jlong jContext) { Context *context = reinterpret_cast(jContext); context->parser->flush(); diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index 8a769b66d4..cea7fbe33b 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -44,10 +44,18 @@ class FLACParser { return mStreamInfo; } - int64_t getLastTimestamp() const { + int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); } + int64_t getLastFrameFirstSampleIndex() const { + return mWriteHeader.number.sample_number; + } + + int64_t getNextFrameFirstSampleIndex() const { + return mWriteHeader.number.sample_number + mWriteHeader.blocksize; + } + bool decodeMetadata(); size_t readBuffer(void *output, size_t output_size); @@ -83,6 +91,11 @@ class FLACParser { return FLAC__stream_decoder_get_resolved_state_string(mDecoder); } + bool isDecoderAtEndOfStream() const { + return FLAC__stream_decoder_get_state(mDecoder) == + FLAC__STREAM_DECODER_END_OF_STREAM; + } + private: DataSource *mDataSource; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java index b08f4a31e3..0df39e103d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.util; +import com.google.android.exoplayer2.C; + /** * Holder for FLAC stream info. */ @@ -52,8 +54,29 @@ public final class FlacStreamInfo { // Remaining 16 bytes is md5 value } - public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize, - int sampleRate, int channels, int bitsPerSample, long totalSamples) { + /** + * Constructs a FlacStreamInfo given the parameters. + * + * @param minBlockSize Minimum block size of the FLAC stream. + * @param maxBlockSize Maximum block size of the FLAC stream. + * @param minFrameSize Minimum frame size of the FLAC stream. + * @param maxFrameSize Maximum frame size of the FLAC stream. + * @param sampleRate Sample rate of the FLAC stream. + * @param channels Number of channels of the FLAC stream. + * @param bitsPerSample Number of bits per sample of the FLAC stream. + * @param totalSamples Total samples of the FLAC stream. + * @see FLAC format + * METADATA_BLOCK_STREAMINFO + */ + public FlacStreamInfo( + int minBlockSize, + int maxBlockSize, + int minFrameSize, + int maxFrameSize, + int sampleRate, + int channels, + int bitsPerSample, + long totalSamples) { this.minBlockSize = minBlockSize; this.maxBlockSize = maxBlockSize; this.minFrameSize = minFrameSize; @@ -64,16 +87,43 @@ public final class FlacStreamInfo { this.totalSamples = totalSamples; } + /** Returns the maximum size for a decoded frame from the FLAC stream. */ public int maxDecodedFrameSize() { return maxBlockSize * channels * (bitsPerSample / 8); } + /** Returns the bit-rate of the FLAC stream. */ public int bitRate() { return bitsPerSample * sampleRate; } + /** Returns the duration of the FLAC stream in microseconds. */ public long durationUs() { return (totalSamples * 1000000L) / sampleRate; } + /** + * Returns the sample index for the sample at given position. + * + * @param timeUs Time position in microseconds in the FLAC stream. + * @return The sample index for the sample at given position. + */ + public long getSampleIndex(long timeUs) { + long sampleIndex = (timeUs * sampleRate) / C.MICROS_PER_SECOND; + return Util.constrainValue(sampleIndex, 0, totalSamples - 1); + } + + /** Returns the approximate number of bytes per frame for the current FLAC stream. */ + public long getApproxBytesPerFrame() { + long approxBytesPerFrame; + if (maxFrameSize > 0) { + approxBytesPerFrame = ((long) maxFrameSize + minFrameSize) / 2 + 1; + } else { + // Uses the stream's block-size if it's a known fixed block-size stream, otherwise uses the + // default value for FLAC block-size, which is 4096. + long blockSize = (minBlockSize == maxBlockSize && minBlockSize > 0) ? minBlockSize : 4096; + approxBytesPerFrame = (blockSize * channels * bitsPerSample) / 8 + 64; + } + return approxBytesPerFrame; + } } From 0c3b1a64016c507e055c428a2954fdd2122fa07c Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 15 May 2018 02:39:10 -0700 Subject: [PATCH 20/40] Allow canceling player messages. This adds a cancel method to PlayerMessage. Issue:#4230 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196638901 --- RELEASENOTES.md | 1 + .../exoplayer2/ExoPlayerImplInternal.java | 5 +- .../android/exoplayer2/PlayerMessage.java | 19 ++++ .../android/exoplayer2/ExoPlayerTest.java | 89 ++++++++++++++++++- 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a918b2b06e..cf184da968 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,7 @@ * OkHttp extension: Fix to correctly include response headers in thrown `InvalidResponseCodeException`s. +* Add possibility to cancel `PlayerMessage`s. * UI components: * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed video frame or media artwork visible when the player is reset diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index ceee25af82..fc946804f4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -854,6 +854,9 @@ import java.util.Collections; } private void deliverMessage(PlayerMessage message) throws ExoPlaybackException { + if (message.isCanceled()) { + return; + } try { message.getTarget().handleMessage(message.getType(), message.getPayload()); } finally { @@ -945,7 +948,7 @@ import java.util.Collections; && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { sendMessageToTarget(nextInfo.message); - if (nextInfo.message.getDeleteAfterDelivery()) { + if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { pendingMessages.remove(nextPendingMessageIndex); } else { nextPendingMessageIndex++; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index 408cbecaf1..2c7aee834e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -63,6 +63,7 @@ public final class PlayerMessage { private boolean isSent; private boolean isDelivered; private boolean isProcessed; + private boolean isCanceled; /** * Creates a new message. @@ -242,6 +243,24 @@ public final class PlayerMessage { return this; } + /** + * Cancels the message delivery. + * + * @return This message. + * @throws IllegalStateException If this method is called before {@link #send()}. + */ + public synchronized PlayerMessage cancel() { + Assertions.checkState(isSent); + isCanceled = true; + markAsProcessed(/* isDelivered= */ false); + return this; + } + + /** Returns whether the message delivery has been canceled. */ + public synchronized boolean isCanceled() { + return isCanceled; + } + /** * Blocks until after the message has been delivered or the player is no longer able to deliver * the message. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index ed91f6651c..0df854cddb 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -51,6 +51,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -1812,6 +1813,88 @@ public final class ExoPlayerTest { assertThat(target3.windowIndex).isEqualTo(2); } + @Test + public void testCancelMessageBeforeDelivery() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + final AtomicReference message = new AtomicReference<>(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testCancelMessage") + .pause() + .waitForPlaybackState(Player.STATE_BUFFERING) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + message.set( + player.createMessage(target).setPosition(/* positionMs= */ 50).send()); + } + }) + // Play a bit to ensure message arrived in internal player. + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 30) + .executeRunnable( + new Runnable() { + @Override + public void run() { + message.get().cancel(); + } + }) + .play() + .build(); + new Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertThat(message.get().isCanceled()).isTrue(); + assertThat(target.messageCount).isEqualTo(0); + } + + @Test + public void testCancelRepeatedMessageAfterDelivery() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + final AtomicReference message = new AtomicReference<>(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testCancelMessage") + .pause() + .waitForPlaybackState(Player.STATE_BUFFERING) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + message.set( + player + .createMessage(target) + .setPosition(/* positionMs= */ 50) + .setDeleteAfterDelivery(/* deleteAfterDelivery= */ false) + .send()); + } + }) + // Play until the message has been delivered. + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 51) + // Seek back, cancel the message, and play past the same position again. + .seek(/* positionMs= */ 0) + .executeRunnable( + new Runnable() { + @Override + public void run() { + message.get().cancel(); + } + }) + .play() + .build(); + new Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertThat(message.get().isCanceled()).isTrue(); + assertThat(target.messageCount).isEqualTo(1); + } + @Test public void testSetAndSwitchSurface() throws Exception { final List rendererMessages = new ArrayList<>(); @@ -1934,8 +2017,10 @@ public final class ExoPlayerTest { @Override public void handleMessage(SimpleExoPlayer player, int messageType, Object message) { - windowIndex = player.getCurrentWindowIndex(); - positionMs = player.getCurrentPosition(); + if (player != null) { + windowIndex = player.getCurrentWindowIndex(); + positionMs = player.getCurrentPosition(); + } messageCount++; } } From 75db04d51d383a104f7e2b24e1b69286c1a5545f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 15 May 2018 06:56:19 -0700 Subject: [PATCH 21/40] Fix extraction of PCM (sowt) in MP4/MOV The sample size from the stsd box takes precedence over the sample size in the stsz box. Also remove assumption that C.INDEX_UNSET is -1 in ChunkIterator (which is a no-op change). Issue: #4228 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196661751 --- RELEASENOTES.md | 2 ++ .../exoplayer2/extractor/mp4/AtomParsers.java | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cf184da968..e657289cef 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,8 @@ ([#2843](https://github.com/google/ExoPlayer/issues/2843)). * Fix crash when switching surface on Moto E(4) ([#4134](https://github.com/google/ExoPlayer/issues/4134)). +* Audio: Fix extraction of PCM in MP4/MOV + ([#4228](https://github.com/google/ExoPlayer/issues/4228)). ### 2.8.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index a6e2524f0b..a2b787d6b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -189,11 +189,13 @@ import java.util.List; } } - // True if we can rechunk fixed-sample-size data. Note that we only rechunk raw audio. - boolean isRechunkable = sampleSizeBox.isFixedSampleSize() - && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType) - && remainingTimestampDeltaChanges == 0 && remainingTimestampOffsetChanges == 0 - && remainingSynchronizationSamples == 0; + // Fixed sample size raw audio may need to be rechunked. + boolean isFixedSampleSizeRawAudio = + sampleSizeBox.isFixedSampleSize() + && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType) + && remainingTimestampDeltaChanges == 0 + && remainingTimestampOffsetChanges == 0 + && remainingSynchronizationSamples == 0; long[] offsets; int[] sizes; @@ -203,7 +205,7 @@ import java.util.List; long timestampTimeUnits = 0; long duration; - if (!isRechunkable) { + if (!isFixedSampleSizeRawAudio) { offsets = new long[sampleCount]; sizes = new int[sampleCount]; timestamps = new long[sampleCount]; @@ -296,7 +298,8 @@ import java.util.List; chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset; chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples; } - int fixedSampleSize = sampleSizeBox.readNextSampleSize(); + int fixedSampleSize = + Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount); FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk( fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits); offsets = rechunkedResults.offsets; @@ -1224,7 +1227,7 @@ import java.util.List; stsc.setPosition(Atom.FULL_HEADER_SIZE); remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt(); Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1"); - index = C.INDEX_UNSET; + index = -1; } public boolean moveNext() { From 5ffb4d8f55b5973de0c436b81ba8d012c6975e86 Mon Sep 17 00:00:00 2001 From: pfxing Date: Tue, 15 May 2018 18:54:40 -0700 Subject: [PATCH 22/40] Rollback set content length and redirect URI ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196765970 --- .../upstream/cache/CacheDataSource.java | 70 ++++++++++++------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 045fc25338..023567e7df 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; import android.support.annotation.IntDef; import android.support.annotation.Nullable; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; @@ -51,6 +52,8 @@ public final class CacheDataSource implements DataSource { */ public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024; + private static final String TAG = "CacheDataSource"; + /** * Flags controlling the cache's behavior. */ @@ -218,7 +221,7 @@ public final class CacheDataSource implements DataSource { try { key = CacheUtil.getKey(dataSpec); uri = dataSpec.uri; - actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri); + actualUri = loadRedirectedUriOrReturnGivenUri(cache, key, uri); flags = dataSpec.flags; readPosition = dataSpec.position; @@ -269,7 +272,7 @@ public final class CacheDataSource implements DataSource { bytesRemaining -= bytesRead; } } else if (currentDataSpecLengthUnset) { - setNoBytesRemainingAndMaybeStoreLength(); + setBytesRemainingAndMaybeStoreLength(0); } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { closeCurrentSource(); openNextSource(false); @@ -278,7 +281,7 @@ public final class CacheDataSource implements DataSource { return bytesRead; } catch (IOException e) { if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) { - setNoBytesRemainingAndMaybeStoreLength(); + setBytesRemainingAndMaybeStoreLength(0); return C.RESULT_END_OF_INPUT; } handleBeforeThrow(e); @@ -399,38 +402,46 @@ public final class CacheDataSource implements DataSource { currentDataSource = nextDataSource; currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET; long resolvedLength = nextDataSource.open(nextDataSpec); - - // Update bytesRemaining, actualUri and (if writing to cache) the cache metadata. - ContentMetadataMutations mutations = new ContentMetadataMutations(); if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { - bytesRemaining = resolvedLength; - ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining); + setBytesRemainingAndMaybeStoreLength(resolvedLength); } - if (isReadingFromUpstream()) { - actualUri = currentDataSource.getUri(); - boolean isRedirected = !uri.equals(actualUri); - if (isRedirected) { - ContentMetadataInternal.setRedirectedUri(mutations, actualUri); - } else { - ContentMetadataInternal.removeRedirectedUri(mutations); - } + // TODO find a way to store length and redirected uri in one metadata mutation. + maybeUpdateActualUriFieldAndRedirectedUriMetadata(); + } + + private void maybeUpdateActualUriFieldAndRedirectedUriMetadata() { + if (!isReadingFromUpstream()) { + return; } - if (isWritingToCache()) { + actualUri = currentDataSource.getUri(); + maybeUpdateRedirectedUriMetadata(); + } + + private void maybeUpdateRedirectedUriMetadata() { + if (!isWritingToCache()) { + return; + } + ContentMetadataMutations mutations = new ContentMetadataMutations(); + boolean isRedirected = !uri.equals(actualUri); + if (isRedirected) { + ContentMetadataInternal.setRedirectedUri(mutations, actualUri); + } else { + ContentMetadataInternal.removeRedirectedUri(mutations); + } + try { cache.applyContentMetadataMutations(key, mutations); + } catch (CacheException e) { + String message = + "Couldn't update redirected URI. " + + "This might cause relative URIs get resolved incorrectly."; + Log.w(TAG, message, e); } } - private void setNoBytesRemainingAndMaybeStoreLength() throws IOException { - bytesRemaining = 0; - if (isWritingToCache()) { - cache.setContentLength(key, readPosition); - } - } - - private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) { + private static Uri loadRedirectedUriOrReturnGivenUri(Cache cache, String key, Uri uri) { ContentMetadata contentMetadata = cache.getContentMetadata(key); Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata); - return redirectedUri == null ? defaultUri : redirectedUri; + return redirectedUri == null ? uri : redirectedUri; } private static boolean isCausedByPositionOutOfRange(IOException e) { @@ -447,6 +458,13 @@ public final class CacheDataSource implements DataSource { return false; } + private void setBytesRemainingAndMaybeStoreLength(long bytesRemaining) throws IOException { + this.bytesRemaining = bytesRemaining; + if (isWritingToCache()) { + cache.setContentLength(key, readPosition + bytesRemaining); + } + } + private boolean isReadingFromUpstream() { return !isReadingFromCache(); } From 9a45d504d386aadfb4175e584ab2bdc485c0c64e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 16 May 2018 02:01:25 -0700 Subject: [PATCH 23/40] Fix playback of live HLS streams with #EXT-X-PROGRAM-DATE-TIME tags Issue:#4239 Issue:#4254 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196796569 --- RELEASENOTES.md | 3 ++ .../exoplayer2/source/hls/HlsChunkSource.java | 41 ++++++++++--------- .../source/hls/playlist/HlsMediaPlaylist.java | 4 +- .../hls/playlist/HlsPlaylistTracker.java | 5 ++- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e657289cef..39ef439f40 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,9 @@ ([#4134](https://github.com/google/ExoPlayer/issues/4134)). * Audio: Fix extraction of PCM in MP4/MOV ([#4228](https://github.com/google/ExoPlayer/issues/4228)). +* HLS: + * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags + ([#4239](https://github.com/google/ExoPlayer/issues/4239)). ### 2.8.0 ### diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 9a02bd785a..0fb1b6a969 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -198,24 +198,24 @@ import java.util.List; /** * Returns the next chunk to load. - *

      - * If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream has - * been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available but - * the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to + * + *

      If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream + * has been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available + * but the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to * contain the {@link HlsUrl} that refers to the playlist that needs refreshing. * * @param previous The most recently loaded media chunk. - * @param playbackPositionUs The current playback position in microseconds. If playback of the - * period to which this chunk source belongs has not yet started, the value will be the - * starting position in the period minus the duration of any media in previous periods still - * to be played. - * @param loadPositionUs The current load position in microseconds. If {@code previous} is null, - * this is the starting position from which chunks should be provided. Else it's equal to - * {@code previous.endTimeUs}. + * @param playbackPositionUs The current playback position relative to the period start in + * microseconds. If playback of the period to which this chunk source belongs has not yet + * started, the value will be the starting position in the period minus the duration of any + * media in previous periods still to be played. + * @param loadPositionUs The current load position relative to the period start in microseconds. + * If {@code previous} is null, this is the starting position from which chunks should be + * provided. Else it's equal to {@code previous.endTimeUs}. * @param out A holder to populate. */ - public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs, - HlsChunkHolder out) { + public void getNextChunk( + HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs, HlsChunkHolder out) { int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); long bufferedDurationUs = loadPositionUs - playbackPositionUs; @@ -261,12 +261,13 @@ import java.util.List; // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { - // The playlist start time is subtracted from the target position because the segment start - // times are relative to the start of the playlist, but the target position is not. + long positionOfPlaylistInPeriodUs = + mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); + long targetPositionInPlaylistUs = targetPositionUs - positionOfPlaylistInPeriodUs; chunkMediaSequence = Util.binarySearchFloor( mediaPlaylist.segments, - /* value= */ targetPositionUs - mediaPlaylist.startTimeUs, + /* value= */ targetPositionInPlaylistUs, /* inclusive= */ true, /* stayInBounds= */ !playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence; @@ -330,9 +331,9 @@ import java.util.List; } // Compute start time of the next chunk. - long offsetFromInitialStartTimeUs = + long positionOfPlaylistInPeriodUs = mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); - long startTimeUs = offsetFromInitialStartTimeUs + segment.relativeStartTimeUs; + long segmentStartTimeInPeriodUs = positionOfPlaylistInPeriodUs + segment.relativeStartTimeUs; int discontinuitySequence = mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence; TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( @@ -352,8 +353,8 @@ import java.util.List; muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), - startTimeUs, - startTimeUs + segment.durationUs, + segmentStartTimeInPeriodUs, + segmentStartTimeInPeriodUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, segment.hasGapTag, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index f905def54b..5ac6f37550 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -146,7 +146,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public final long startOffsetUs; /** - * The start time of the playlist in playback timebase in microseconds. + * If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch. + * Otherwise, contains the aggregated duration of removed segments up to this snapshot of the + * playlist. */ public final long startTimeUs; /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 4ed6aa1656..c9757a10e1 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -208,7 +208,10 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Wed, 16 May 2018 04:33:57 -0700 Subject: [PATCH 24/40] Fix a bug with TTML font styling. Due to a bug, for each TTML node, when applying its style to the encompassed regions, it applies child nodes's styling several time for each region (the number of time is equal to the number of region). This leads to a styling issue if there are multiple regions in a node displayed at the same time in TTML file. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196810046 --- RELEASENOTES.md | 3 + .../exoplayer2/text/ttml/TtmlNode.java | 61 ++++++++++++------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 39ef439f40..ddc5d06fdb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,9 @@ * HLS: * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags ([#4239](https://github.com/google/ExoPlayer/issues/4239)). +* Caption: + * Fix a TTML styling issue when there are multiple regions displayed at the + same time that can make text size of each region much smaller than defined. ### 2.8.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java index 43fa7a1bd9..1facb73567 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -175,7 +175,7 @@ import java.util.TreeSet; Map regionMap) { TreeMap regionOutputs = new TreeMap<>(); traverseForText(timeUs, false, regionId, regionOutputs); - traverseForStyle(globalStyles, regionOutputs); + traverseForStyle(timeUs, globalStyles, regionOutputs); List cues = new ArrayList<>(); for (Entry entry : regionOutputs.entrySet()) { TtmlRegion region = regionMap.get(entry.getKey()); @@ -185,25 +185,31 @@ import java.util.TreeSet; return cues; } - private void traverseForText(long timeUs, boolean descendsPNode, - String inheritedRegion, Map regionOutputs) { + private void traverseForText( + long timeUs, + boolean descendsPNode, + String inheritedRegion, + Map regionOutputs) { nodeStartsByRegion.clear(); nodeEndsByRegion.clear(); - String resolvedRegionId = regionId; - if (ANONYMOUS_REGION_ID.equals(resolvedRegionId)) { - resolvedRegionId = inheritedRegion; + if (TAG_METADATA.equals(tag)) { + // Ignore metadata tag. + return; } + + String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId; + if (isTextNode && descendsPNode) { getRegionOutput(resolvedRegionId, regionOutputs).append(text); } else if (TAG_BR.equals(tag) && descendsPNode) { getRegionOutput(resolvedRegionId, regionOutputs).append('\n'); - } else if (TAG_METADATA.equals(tag)) { - // Do nothing. } else if (isActive(timeUs)) { - boolean isPNode = TAG_P.equals(tag); + // This is a container node, which can contain zero or more children. for (Entry entry : regionOutputs.entrySet()) { nodeStartsByRegion.put(entry.getKey(), entry.getValue().length()); } + + boolean isPNode = TAG_P.equals(tag); for (int i = 0; i < getChildCount(); i++) { getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId, regionOutputs); @@ -211,39 +217,50 @@ import java.util.TreeSet; if (isPNode) { TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs)); } + for (Entry entry : regionOutputs.entrySet()) { nodeEndsByRegion.put(entry.getKey(), entry.getValue().length()); } } } - private static SpannableStringBuilder getRegionOutput(String resolvedRegionId, - Map regionOutputs) { + private static SpannableStringBuilder getRegionOutput( + String resolvedRegionId, Map regionOutputs) { if (!regionOutputs.containsKey(resolvedRegionId)) { regionOutputs.put(resolvedRegionId, new SpannableStringBuilder()); } return regionOutputs.get(resolvedRegionId); } - private void traverseForStyle(Map globalStyles, + private void traverseForStyle( + long timeUs, + Map globalStyles, Map regionOutputs) { + if (!isActive(timeUs)) { + return; + } for (Entry entry : nodeEndsByRegion.entrySet()) { String regionId = entry.getKey(); int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0; - applyStyleToOutput(globalStyles, regionOutputs.get(regionId), start, entry.getValue()); - for (int i = 0; i < getChildCount(); ++i) { - getChild(i).traverseForStyle(globalStyles, regionOutputs); + int end = entry.getValue(); + if (start != end) { + SpannableStringBuilder regionOutput = regionOutputs.get(regionId); + applyStyleToOutput(globalStyles, regionOutput, start, end); } } + for (int i = 0; i < getChildCount(); ++i) { + getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs); + } } - private void applyStyleToOutput(Map globalStyles, - SpannableStringBuilder regionOutput, int start, int end) { - if (start != end) { - TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); - if (resolvedStyle != null) { - TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle); - } + private void applyStyleToOutput( + Map globalStyles, + SpannableStringBuilder regionOutput, + int start, + int end) { + TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); + if (resolvedStyle != null) { + TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle); } } From 972304f16b0f5aba21439785b4c9e2b831e364f6 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 16 May 2018 05:58:42 -0700 Subject: [PATCH 25/40] Supports seeking for FLAC files without a SEEKTABLE. Currently, ExoPlayer only supports seeking for FLAC files with a SEEKTABLE. This CL adds support seeking for cases when the FLAC files do not have a SEEKTABLE by searching for individual frames within the file using binary search. Github: #1088. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196816398 --- RELEASENOTES.md | 7 +- .../ext/flac/FlacExtractorSeekTest.java | 281 ++++++++++++++++++ .../exoplayer2/ext/flac/FlacExtractor.java | 152 +++++++--- .../exoplayer2/testutil/FakeTrackOutput.java | 22 ++ 4 files changed, 414 insertions(+), 48 deletions(-) create mode 100644 extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ddc5d06fdb..1c60071326 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,8 +11,11 @@ ([#2843](https://github.com/google/ExoPlayer/issues/2843)). * Fix crash when switching surface on Moto E(4) ([#4134](https://github.com/google/ExoPlayer/issues/4134)). -* Audio: Fix extraction of PCM in MP4/MOV - ([#4228](https://github.com/google/ExoPlayer/issues/4228)). +* Audio: + * Fix extraction of PCM in MP4/MOV + ([#4228](https://github.com/google/ExoPlayer/issues/4228)). + * FLAC: Supports seeking for FLAC files without SEEKTABLE + ([#1808](https://github.com/google/ExoPlayer/issues/1808)). * HLS: * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags ([#4239](https://github.com/google/ExoPlayer/issues/4239)). diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java new file mode 100644 index 0000000000..58ab260277 --- /dev/null +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2018 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.ext.flac; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.Nullable; +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.DefaultExtractorInput; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeExtractorOutput; +import com.google.android.exoplayer2.testutil.FakeTrackOutput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DefaultDataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.List; +import java.util.Random; + +/** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */ +public final class FlacExtractorSeekTest extends InstrumentationTestCase { + + private static final String NO_SEEKTABLE_FLAC = "bear_no_seek.flac"; + private static final int DURATION_US = 2_741_000; + private static final Uri FILE_URI = Uri.parse("file:///android_asset/" + NO_SEEKTABLE_FLAC); + private static final Random RANDOM = new Random(1234L); + + private FakeExtractorOutput expectedOutput; + private FakeTrackOutput expectedTrackOutput; + + private DefaultDataSource dataSource; + private PositionHolder positionHolder; + private long totalInputLength; + + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!FlacLibrary.isAvailable()) { + fail("Flac library not available."); + } + expectedOutput = new FakeExtractorOutput(); + extractAllSamplesFromFileToExpectedOutput(getInstrumentation().getContext(), NO_SEEKTABLE_FLAC); + expectedTrackOutput = expectedOutput.trackOutputs.get(0); + + dataSource = + new DefaultDataSourceFactory(getInstrumentation().getContext(), "UserAgent") + .createDataSource(); + totalInputLength = readInputLength(); + positionHolder = new PositionHolder(); + } + + public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap() + throws IOException, InterruptedException { + FlacExtractor extractor = new FlacExtractor(); + + SeekMap seekMap = extractSeekMap(extractor, new FakeExtractorOutput()); + + assertThat(seekMap).isNotNull(); + assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US); + assertThat(seekMap.isSeekable()).isTrue(); + } + + public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame() + throws IOException, InterruptedException { + FlacExtractor extractor = new FlacExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = extractSeekMap(extractor, extractorOutput); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = 987_000; + int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame() + throws IOException, InterruptedException { + FlacExtractor extractor = new FlacExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = extractSeekMap(extractor, extractorOutput); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long targetSeekTimeUs = seekMap.getDurationUs(); + + int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame() + throws IOException, InterruptedException { + FlacExtractor extractor = new FlacExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = extractSeekMap(extractor, extractorOutput); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long firstSeekTimeUs = 987_000; + seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput); + + long targetSeekTimeUs = 0; + int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame() + throws IOException, InterruptedException { + FlacExtractor extractor = new FlacExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = extractSeekMap(extractor, extractorOutput); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long firstSeekTimeUs = 987_000; + seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput); + + long targetSeekTimeUs = 1_234_000; + int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + + public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame() + throws IOException, InterruptedException { + FlacExtractor extractor = new FlacExtractor(); + + FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); + SeekMap seekMap = extractSeekMap(extractor, extractorOutput); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + + long numSeek = 100; + for (long i = 0; i < numSeek; i++) { + long targetSeekTimeUs = RANDOM.nextInt(DURATION_US + 1); + int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput); + + assertThat(extractedFrameIndex).isNotEqualTo(-1); + assertFirstFrameAfterSeekContainTargetSeekTime( + trackOutput, targetSeekTimeUs, extractedFrameIndex); + } + } + + // Internal methods + + private long readInputLength() throws IOException { + DataSpec dataSpec = new DataSpec(FILE_URI, 0, C.LENGTH_UNSET, null); + long totalInputLength = dataSource.open(dataSpec); + Util.closeQuietly(dataSource); + return totalInputLength; + } + + /** + * Seeks to the given seek time and keeps reading from input until we can extract at least one + * frame from the seek position, or until end-of-input is reached. + * + * @return The index of the first extracted frame written to the given {@code trackOutput} after + * the seek is completed, or -1 if the seek is completed without any extracted frame. + */ + private int seekToTimeUs( + FlacExtractor flacExtractor, SeekMap seekMap, long seekTimeUs, FakeTrackOutput trackOutput) + throws IOException, InterruptedException { + int numSampleBeforeSeek = trackOutput.getSampleCount(); + SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs); + + long initialSeekLoadPosition = seekPoints.first.position; + flacExtractor.seek(initialSeekLoadPosition, seekTimeUs); + + positionHolder.position = C.POSITION_UNSET; + ExtractorInput extractorInput = getExtractorInputFromPosition(initialSeekLoadPosition); + int extractorReadResult = Extractor.RESULT_CONTINUE; + while (true) { + try { + // Keep reading until we can read at least one frame after seek + while (extractorReadResult == Extractor.RESULT_CONTINUE + && trackOutput.getSampleCount() == numSampleBeforeSeek) { + extractorReadResult = flacExtractor.read(extractorInput, positionHolder); + } + } finally { + Util.closeQuietly(dataSource); + } + + if (extractorReadResult == Extractor.RESULT_SEEK) { + extractorInput = getExtractorInputFromPosition(positionHolder.position); + extractorReadResult = Extractor.RESULT_CONTINUE; + } else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT) { + return -1; + } else if (trackOutput.getSampleCount() > numSampleBeforeSeek) { + // First index after seek = num sample before seek. + return numSampleBeforeSeek; + } + } + } + + private @Nullable SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output) + throws IOException, InterruptedException { + try { + ExtractorInput input = getExtractorInputFromPosition(0); + extractor.init(output); + while (output.seekMap == null) { + extractor.read(input, positionHolder); + } + } finally { + Util.closeQuietly(dataSource); + } + return output.seekMap; + } + + private void assertFirstFrameAfterSeekContainTargetSeekTime( + FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) { + int expectedSampleIndex = findTargetFrameInExpectedOutput(seekTimeUs); + // Assert that after seeking, the first sample frame written to output contains the sample + // at seek time. + trackOutput.assertSample( + firstFrameIndexAfterSeek, + expectedTrackOutput.getSampleData(expectedSampleIndex), + expectedTrackOutput.getSampleTimeUs(expectedSampleIndex), + expectedTrackOutput.getSampleFlags(expectedSampleIndex), + expectedTrackOutput.getSampleCryptoData(expectedSampleIndex)); + } + + private int findTargetFrameInExpectedOutput(long seekTimeUs) { + List sampleTimes = expectedTrackOutput.getSampleTimesUs(); + for (int i = 0; i < sampleTimes.size() - 1; i++) { + long currentSampleTime = sampleTimes.get(i); + long nextSampleTime = sampleTimes.get(i + 1); + if (currentSampleTime <= seekTimeUs && nextSampleTime > seekTimeUs) { + return i; + } + } + return sampleTimes.size() - 1; + } + + private ExtractorInput getExtractorInputFromPosition(long position) throws IOException { + DataSpec dataSpec = new DataSpec(FILE_URI, position, totalInputLength, /* key= */ null); + dataSource.open(dataSpec); + return new DefaultExtractorInput(dataSource, position, totalInputLength); + } + + private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName) + throws IOException, InterruptedException { + byte[] data = TestUtil.getByteArray(context, fileName); + + FlacExtractor extractor = new FlacExtractor(); + extractor.init(expectedOutput); + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); + + while (extractor.read(input, new PositionHolder()) != Extractor.RESULT_END_OF_INPUT) {} + } +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 7672f2f8ec..a5efeb69f9 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -88,10 +88,12 @@ public final class FlacExtractor implements Extractor { private ParsableByteArray outputBuffer; private ByteBuffer outputByteBuffer; + private FlacStreamInfo streamInfo; private Metadata id3Metadata; + private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker; - private boolean metadataParsed; + private boolean readPastStreamInfo; /** Constructs an instance with flags = 0. */ public FlacExtractor() { @@ -136,47 +138,10 @@ public final class FlacExtractor implements Extractor { } decoderJni.setData(input); + readPastStreamInfo(input); - if (!metadataParsed) { - final FlacStreamInfo streamInfo; - try { - streamInfo = decoderJni.decodeMetadata(); - if (streamInfo == null) { - throw new IOException("Metadata decoding failed"); - } - } catch (IOException e) { - decoderJni.reset(0); - input.setRetryPosition(0, e); - throw e; // never executes - } - metadataParsed = true; - - boolean isSeekable = decoderJni.getSeekPosition(0) != -1; - extractorOutput.seekMap( - isSeekable - ? new FlacSeekMap(streamInfo.durationUs(), decoderJni) - : new SeekMap.Unseekable(streamInfo.durationUs(), 0)); - Format mediaFormat = - Format.createAudioSampleFormat( - /* id= */ null, - MimeTypes.AUDIO_RAW, - /* codecs= */ null, - streamInfo.bitRate(), - streamInfo.maxDecodedFrameSize(), - streamInfo.channels, - streamInfo.sampleRate, - getPcmEncoding(streamInfo.bitsPerSample), - /* encoderDelay= */ 0, - /* encoderPadding= */ 0, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null, - isId3MetadataDisabled ? null : id3Metadata); - trackOutput.format(mediaFormat); - - outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); - outputByteBuffer = ByteBuffer.wrap(outputBuffer.data); + if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) { + return handlePendingSeek(input, seekPosition); } long lastDecodePosition = decoderJni.getDecodePosition(); @@ -189,26 +154,27 @@ public final class FlacExtractor implements Extractor { if (outputSize == 0) { return RESULT_END_OF_INPUT; } - outputBuffer.setPosition(0); - trackOutput.sampleData(outputBuffer, outputSize); - trackOutput.sampleMetadata( - decoderJni.getLastFrameTimestamp(), C.BUFFER_FLAG_KEY_FRAME, outputSize, 0, null); + writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp()); return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } @Override public void seek(long position, long timeUs) { if (position == 0) { - metadataParsed = false; + readPastStreamInfo = false; } if (decoderJni != null) { decoderJni.reset(position); } + if (flacBinarySearchSeeker != null) { + flacBinarySearchSeeker.setSeekTargetUs(timeUs); + } } @Override public void release() { + flacBinarySearchSeeker = null; if (decoderJni != null) { decoderJni.release(); decoderJni = null; @@ -240,6 +206,100 @@ public final class FlacExtractor implements Extractor { return Arrays.equals(header, FLAC_SIGNATURE); } + private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException { + if (readPastStreamInfo) { + return; + } + + FlacStreamInfo streamInfo = decodeStreamInfo(input); + readPastStreamInfo = true; + if (this.streamInfo == null) { + updateFlacStreamInfo(input, streamInfo); + } + } + + private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) { + this.streamInfo = streamInfo; + outputSeekMap(input, streamInfo); + outputFormat(streamInfo); + outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); + outputByteBuffer = ByteBuffer.wrap(outputBuffer.data); + } + + private FlacStreamInfo decodeStreamInfo(ExtractorInput input) + throws InterruptedException, IOException { + try { + FlacStreamInfo streamInfo = decoderJni.decodeMetadata(); + if (streamInfo == null) { + throw new IOException("Metadata decoding failed"); + } + return streamInfo; + } catch (IOException e) { + decoderJni.reset(0); + input.setRetryPosition(0, e); + throw e; + } + } + + private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) { + boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1; + SeekMap seekMap = + hasSeekTable + ? new FlacSeekMap(streamInfo.durationUs(), decoderJni) + : getSeekMapForNonSeekTableFlac(input, streamInfo); + extractorOutput.seekMap(seekMap); + } + + private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) { + long inputLength = input.getLength(); + if (inputLength != C.LENGTH_UNSET) { + long firstFramePosition = decoderJni.getDecodePosition(); + flacBinarySearchSeeker = + new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni); + return flacBinarySearchSeeker.getSeekMap(); + } else { // can't seek at all, because there's no SeekTable and the input length is unknown. + return new SeekMap.Unseekable(streamInfo.durationUs()); + } + } + + private void outputFormat(FlacStreamInfo streamInfo) { + Format mediaFormat = + Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + streamInfo.bitRate(), + streamInfo.maxDecodedFrameSize(), + streamInfo.channels, + streamInfo.sampleRate, + getPcmEncoding(streamInfo.bitsPerSample), + /* encoderDelay= */ 0, + /* encoderPadding= */ 0, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null, + isId3MetadataDisabled ? null : id3Metadata); + trackOutput.format(mediaFormat); + } + + private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) + throws InterruptedException, IOException { + int seekResult = + flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputByteBuffer); + if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { + writeLastSampleToOutput(outputByteBuffer.limit(), decoderJni.getLastFrameTimestamp()); + } + return seekResult; + } + + private void writeLastSampleToOutput(int size, long lastSampleTimestamp) { + outputBuffer.setPosition(0); + trackOutput.sampleData(outputBuffer, size); + trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null); + } + + /** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */ private static final class FlacSeekMap implements SeekMap { private final long durationUs; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java index 639cb82c2d..6432842df4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java @@ -26,6 +26,8 @@ import java.io.EOFException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * A fake {@link TrackOutput}. @@ -114,6 +116,26 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { sampleEndOffsets.get(index)); } + public long getSampleTimeUs(int index) { + return sampleTimesUs.get(index); + } + + public int getSampleFlags(int index) { + return sampleFlags.get(index); + } + + public CryptoData getSampleCryptoData(int index) { + return cryptoDatas.get(index); + } + + public int getSampleCount() { + return sampleTimesUs.size(); + } + + public List getSampleTimesUs() { + return Collections.unmodifiableList(sampleTimesUs); + } + public void assertEquals(FakeTrackOutput expected) { assertThat(format).isEqualTo(expected.format); assertThat(sampleTimesUs).hasSize(expected.sampleTimesUs.size()); From 24b16f34195070794bcd98db27d84e8dc716a3d2 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 16 May 2018 07:09:55 -0700 Subject: [PATCH 26/40] Fix a bug with TTML font styling that displays empty lines. If the caption line has no text (empty line or only line break), we should not display its background. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196823319 --- RELEASENOTES.md | 7 +++++-- .../android/exoplayer2/ui/SubtitlePainter.java | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1c60071326..0debc6d2d3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,8 +20,11 @@ * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags ([#4239](https://github.com/google/ExoPlayer/issues/4239)). * Caption: - * Fix a TTML styling issue when there are multiple regions displayed at the - same time that can make text size of each region much smaller than defined. + * TTML: + * Fix a styling issue when there are multiple regions displayed at the same + time that can make text size of each region much smaller than defined. + * Fix an issue when the caption line has no text (empty line or only line + break), and the line's background is still displayed. ### 2.8.0 ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index b6cfc9a6f3..c5d264b310 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -372,12 +372,22 @@ import com.google.android.exoplayer2.util.Util; float previousBottom = layout.getLineTop(0); int lineCount = layout.getLineCount(); for (int i = 0; i < lineCount; i++) { - lineBounds.left = layout.getLineLeft(i) - textPaddingX; - lineBounds.right = layout.getLineRight(i) + textPaddingX; + float lineTextBoundLeft = layout.getLineLeft(i); + float lineTextBoundRight = layout.getLineRight(i); + lineBounds.left = lineTextBoundLeft - textPaddingX; + lineBounds.right = lineTextBoundRight + textPaddingX; lineBounds.top = previousBottom; lineBounds.bottom = layout.getLineBottom(i); previousBottom = lineBounds.bottom; - canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint); + float lineTextWidth = lineTextBoundRight - lineTextBoundLeft; + if (lineTextWidth > 0) { + // Do not draw a line's background color if it has no text. + // For some reason, calculating the width manually is more reliable than + // layout.getLineWidth(). + // Sometimes, lineTextBoundRight == lineTextBoundLeft, and layout.getLineWidth() still + // returns non-zero value. + canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint); + } } } From 7d76685e6030818ed04b063ba0224f5ede35f6da Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 16 May 2018 09:22:37 -0700 Subject: [PATCH 27/40] Fix padding oracle in CachedContentIndex ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196838184 --- .../exoplayer2/upstream/cache/CachedContentIndex.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 7b5fd2c598..3bcfac5053 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.upstream.cache; -import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; @@ -26,7 +25,6 @@ import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -53,8 +51,6 @@ import javax.crypto.spec.SecretKeySpec; private static final int FLAG_ENCRYPTED_INDEX = 1; - private static final String TAG = "CachedContentIndex"; - private final HashMap keyToContent; private final SparseArray idToKey; private final AtomicFile atomicFile; @@ -248,13 +244,12 @@ import javax.crypto.spec.SecretKeySpec; add(cachedContent); hashCode += cachedContent.headerHashCode(version); } - if (input.readInt() != hashCode) { + int fileHashCode = input.readInt(); + boolean isEOF = input.read() == -1; + if (fileHashCode != hashCode || !isEOF) { return false; } - } catch (FileNotFoundException e) { - return false; } catch (IOException e) { - Log.e(TAG, "Error reading cache content index file.", e); return false; } finally { if (input != null) { From e23392a4fe8e5fea44790c60e79c817a2f4d66e7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 16 May 2018 10:34:38 -0700 Subject: [PATCH 28/40] Update views when a new track name provider is set Also update TrackSelectionView with nullness annotations. Issue: #4263 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196849706 --- .../exoplayer2/ui/TrackSelectionView.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java index 45ccd783e7..be0babf5a8 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java @@ -21,6 +21,8 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.TypedArray; +import android.support.annotation.AttrRes; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Pair; import android.view.LayoutInflater; @@ -54,7 +56,7 @@ public class TrackSelectionView extends LinearLayout { private int rendererIndex; private TrackGroupArray trackGroups; private boolean isDisabled; - private SelectionOverride override; + private @Nullable SelectionOverride override; /** * Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it. @@ -100,11 +102,13 @@ public class TrackSelectionView extends LinearLayout { this(context, null); } - public TrackSelectionView(Context context, AttributeSet attrs) { + public TrackSelectionView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public TrackSelectionView(Context context, AttributeSet attrs, int defStyleAttr) { + @SuppressWarnings("nullness") + public TrackSelectionView( + Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray attributeArray = context @@ -152,7 +156,7 @@ public class TrackSelectionView extends LinearLayout { * @param allowAdaptiveSelections Whether adaptive selection is enabled. */ public void setAllowAdaptiveSelections(boolean allowAdaptiveSelections) { - if (!this.allowAdaptiveSelections == allowAdaptiveSelections) { + if (this.allowAdaptiveSelections != allowAdaptiveSelections) { this.allowAdaptiveSelections = allowAdaptiveSelections; updateViews(); } @@ -168,12 +172,14 @@ public class TrackSelectionView extends LinearLayout { } /** - * Sets the {@link TrackNameProvider} used to generate the user visible name of each track. + * Sets the {@link TrackNameProvider} used to generate the user visible name of each track and + * updates the view with track names queried from the specified provider. * * @param trackNameProvider The {@link TrackNameProvider} to use. */ public void setTrackNameProvider(TrackNameProvider trackNameProvider) { this.trackNameProvider = Assertions.checkNotNull(trackNameProvider); + updateViews(); } /** @@ -306,20 +312,20 @@ public class TrackSelectionView extends LinearLayout { override = new SelectionOverride(groupIndex, trackIndex); } else { // An existing override is being modified. - boolean isEnabled = ((CheckedTextView) view).isChecked(); int overrideLength = override.length; - if (isEnabled) { + int[] overrideTracks = override.tracks; + if (((CheckedTextView) view).isChecked()) { // Remove the track from the override. if (overrideLength == 1) { // The last track is being removed, so the override becomes empty. override = null; isDisabled = true; } else { - int[] tracks = getTracksRemoving(override.tracks, trackIndex); + int[] tracks = getTracksRemoving(overrideTracks, trackIndex); override = new SelectionOverride(groupIndex, tracks); } } else { - int[] tracks = getTracksAdding(override.tracks, trackIndex); + int[] tracks = getTracksAdding(overrideTracks, trackIndex); override = new SelectionOverride(groupIndex, tracks); } } From 5e1b430839bb38f7795d78915e0e42cdb0f2614d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 17 May 2018 02:04:36 -0700 Subject: [PATCH 29/40] Fix check for missing profile/level SparseIntArray.get(key) defaults to zero for missing keys (the null check was left over from when a Map was used). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196957452 --- .../android/exoplayer2/mediacodec/MediaCodecUtil.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index bf795c5857..347afe29fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -482,13 +482,13 @@ public final class MediaCodecUtil { return null; } - Integer profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger); - if (profile == null) { + int profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1); + if (profile == -1) { Log.w(TAG, "Unknown AVC profile: " + profileInteger); return null; } - Integer level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger); - if (level == null) { + int level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { Log.w(TAG, "Unknown AVC level: " + levelInteger); return null; } From 15413548193863a7377fb0326c5cd5e44498f605 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Thu, 17 May 2018 06:52:56 -0700 Subject: [PATCH 30/40] Support TTML font size using % correctly. For TTML, if the font size is expressed in %, the font size should be relative to the cellResolution of the document which we did not support before. This CL adds support for handling this correctly. Note that this still does not support font size using c unit. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=196985694 --- RELEASENOTES.md | 4 +- .../google/android/exoplayer2/text/Cue.java | 265 ++++++++++++++---- .../exoplayer2/text/ttml/TtmlDecoder.java | 76 ++++- .../exoplayer2/text/ttml/TtmlNode.java | 14 +- .../exoplayer2/text/ttml/TtmlRegion.java | 29 +- .../android/exoplayer2/ui/SubtitleView.java | 74 ++++- 6 files changed, 372 insertions(+), 90 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0debc6d2d3..2238a95a15 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,8 @@ time that can make text size of each region much smaller than defined. * Fix an issue when the caption line has no text (empty line or only line break), and the line's background is still displayed. + * Support TTML font size using % correctly (as percentage of document cell + resolution). ### 2.8.0 ### @@ -101,7 +103,7 @@ * Allow multiple listeners for `DefaultDrmSessionManager`. * Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`. * Change minimum API requirement for CBC and pattern encryption from 24 to 25 - ([#4022][https://github.com/google/ExoPlayer/issues/4022]). + ([#4022](https://github.com/google/ExoPlayer/issues/4022)). * Fix handling of 307/308 redirects when making license requests ([#4108](https://github.com/google/ExoPlayer/issues/4108)). * HLS: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index 5ae1f35b7e..8bc0b8e136 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -78,6 +78,25 @@ public class Cue { */ public static final int LINE_TYPE_NUMBER = 1; + /** The type of default text size for this cue, which may be unset. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TYPE_UNSET, + TEXT_SIZE_TYPE_FRACTIONAL, + TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING, + TEXT_SIZE_TYPE_ABSOLUTE + }) + public @interface TextSizeType {} + + /** Text size is measured as a fraction of the viewport size minus the view padding. */ + public static final int TEXT_SIZE_TYPE_FRACTIONAL = 0; + + /** Text size is measured as a fraction of the viewport size, ignoring the view padding */ + public static final int TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING = 1; + + /** Text size is measured in number of pixels. */ + public static final int TEXT_SIZE_TYPE_ABSOLUTE = 2; + /** * The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated * with styling spans. @@ -106,40 +125,39 @@ public class Cue { /** * The type of the {@link #line} value. - *

      - * {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the + * + *

      {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the * viewport. - *

      - * {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each - * line is taken to be the size of the first line of the cue. When {@link #line} is greater than - * or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset from - * the start edge. When {@link #line} is negative lines count from the end of the viewport, with - * -1 indicating zero offset from the end edge. For horizontal text the line spacing is the height - * of the first line of the cue, and the start and end of the viewport are the top and bottom - * respectively. - *

      - * Note that it's particularly important to consider the effect of {@link #lineAnchor} when using - * {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a - * (potentially multi-line) cue at the very top of the viewport. - * {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue - * at the very bottom of the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} - * and {@code (line == -1 && lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of - * the viewport. {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only - * the last line is visible at the top of the viewport. - * {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its first - * line is visible at the bottom of the viewport. + * + *

      {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of + * each line is taken to be the size of the first line of the cue. When {@link #line} is greater + * than or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset + * from the start edge. When {@link #line} is negative lines count from the end of the viewport, + * with -1 indicating zero offset from the end edge. For horizontal text the line spacing is the + * height of the first line of the cue, and the start and end of the viewport are the top and + * bottom respectively. + * + *

      Note that it's particularly important to consider the effect of {@link #lineAnchor} when + * using {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} + * positions a (potentially multi-line) cue at the very top of the viewport. {@code (line == -1 && + * lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue at the very bottom of + * the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 && + * lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. {@code (line + * == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the last line is visible + * at the top of the viewport. {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a + * cue so that only its first line is visible at the bottom of the viewport. */ - @LineType public final int lineType; + public final @LineType int lineType; /** - * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, - * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. - *

      - * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE} - * and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box - * respectively. + * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * + *

      For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of + * the cue box respectively. */ - @AnchorType public final int lineAnchor; + public final @AnchorType int lineAnchor; /** * The fractional position of the {@link #positionAnchor} of the cue box within the viewport in @@ -152,14 +170,14 @@ public class Cue { public final float position; /** - * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, - * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. - *

      - * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE} - * and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box - * respectively. + * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * + *

      For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of + * the cue box respectively. */ - @AnchorType public final int positionAnchor; + public final @AnchorType int positionAnchor; /** * The size of the cue box in the writing direction specified as a fraction of the viewport size @@ -184,6 +202,18 @@ public class Cue { */ public final int windowColor; + /** + * The default text size type for this cue's text, or {@link #TYPE_UNSET} if this cue has no + * default text size. + */ + public final @TextSizeType int textSizeType; + + /** + * The default text size for this cue's text, or {@link #DIMEN_UNSET} if this cue has no default + * text size. + */ + public final float textSize; + /** * Creates an image cue. * @@ -194,17 +224,36 @@ public class Cue { * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a * fraction of the viewport height. - * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, - * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. * @param width The width of the cue as a fraction of the viewport width. - * @param height The height of the cue as a fraction of the viewport height, or - * {@link #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the - * specified {@code width}. + * @param height The height of the cue as a fraction of the viewport height, or {@link + * #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the specified + * {@code width}. */ - public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, - float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float height) { - this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, - horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK); + public Cue( + Bitmap bitmap, + float horizontalPosition, + @AnchorType int horizontalPositionAnchor, + float verticalPosition, + @AnchorType int verticalPositionAnchor, + float width, + float height) { + this( + /* text= */ null, + /* textAlignment= */ null, + bitmap, + verticalPosition, + /* lineType= */ LINE_TYPE_FRACTION, + verticalPositionAnchor, + horizontalPosition, + horizontalPositionAnchor, + /* textSizeType= */ TYPE_UNSET, + /* textSize= */ DIMEN_UNSET, + width, + height, + /* windowColorSet= */ false, + /* windowColor= */ Color.BLACK); } /** @@ -214,7 +263,15 @@ public class Cue { * @param text See {@link #text}. */ public Cue(CharSequence text) { - this(text, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET); + this( + text, + /* textAlignment= */ null, + /* line= */ DIMEN_UNSET, + /* lineType= */ TYPE_UNSET, + /* lineAnchor= */ TYPE_UNSET, + /* position= */ DIMEN_UNSET, + /* positionAnchor= */ TYPE_UNSET, + /* size= */ DIMEN_UNSET); } /** @@ -229,10 +286,68 @@ public class Cue { * @param positionAnchor See {@link #positionAnchor}. * @param size See {@link #size}. */ - public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, - @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) { - this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, false, - Color.BLACK); + public Cue( + CharSequence text, + Alignment textAlignment, + float line, + @LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + float size) { + this( + text, + textAlignment, + line, + lineType, + lineAnchor, + position, + positionAnchor, + size, + /* windowColorSet= */ false, + /* windowColor= */ Color.BLACK); + } + + /** + * Creates a text cue. + * + * @param text See {@link #text}. + * @param textAlignment See {@link #textAlignment}. + * @param line See {@link #line}. + * @param lineType See {@link #lineType}. + * @param lineAnchor See {@link #lineAnchor}. + * @param position See {@link #position}. + * @param positionAnchor See {@link #positionAnchor}. + * @param size See {@link #size}. + * @param textSizeType See {@link #textSizeType}. + * @param textSize See {@link #textSize}. + */ + public Cue( + CharSequence text, + Alignment textAlignment, + float line, + @LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + float size, + @TextSizeType int textSizeType, + float textSize) { + this( + text, + textAlignment, + /* bitmap= */ null, + line, + lineType, + lineAnchor, + position, + positionAnchor, + textSizeType, + textSize, + size, + /* bitmapHeight= */ DIMEN_UNSET, + /* windowColorSet= */ false, + /* windowColor= */ Color.BLACK); } /** @@ -249,16 +364,48 @@ public class Cue { * @param windowColorSet See {@link #windowColorSet}. * @param windowColor See {@link #windowColor}. */ - public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, - @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, - boolean windowColorSet, int windowColor) { - this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size, - DIMEN_UNSET, windowColorSet, windowColor); + public Cue( + CharSequence text, + Alignment textAlignment, + float line, + @LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + float size, + boolean windowColorSet, + int windowColor) { + this( + text, + textAlignment, + /* bitmap= */ null, + line, + lineType, + lineAnchor, + position, + positionAnchor, + /* textSizeType= */ TYPE_UNSET, + /* textSize= */ DIMEN_UNSET, + size, + /* bitmapHeight= */ DIMEN_UNSET, + windowColorSet, + windowColor); } - private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line, - @LineType int lineType, @AnchorType int lineAnchor, float position, - @AnchorType int positionAnchor, float size, float bitmapHeight, boolean windowColorSet, + private Cue( + CharSequence text, + Alignment textAlignment, + Bitmap bitmap, + float line, + @LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + @TextSizeType int textSizeType, + float textSize, + float size, + float bitmapHeight, + boolean windowColorSet, int windowColor) { this.text = text; this.textAlignment = textAlignment; @@ -272,6 +419,8 @@ public class Cue { this.bitmapHeight = bitmapHeight; this.windowColorSet = windowColorSet; this.windowColor = windowColor; + this.textSizeType = textSizeType; + this.textSize = textSize; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index a215bf3cc9..ad8f849c60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -38,6 +38,7 @@ import org.xmlpull.v1.XmlPullParserFactory; /** * A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features * supported by this decoder are: + * *

        *
      • content *
      • core @@ -51,7 +52,9 @@ import org.xmlpull.v1.XmlPullParserFactory; *
      • time-clock *
      • time-offset-with-frames *
      • time-offset-with-ticks + *
      • cell-resolution *
      + * * @see TTML specification */ public final class TtmlDecoder extends SimpleSubtitleDecoder { @@ -74,11 +77,14 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$"); private static final Pattern PERCENTAGE_COORDINATES = Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$"); + private static final Pattern CELL_RESOLUTION = Pattern.compile("^(\\d+) (\\d+)$"); private static final int DEFAULT_FRAME_RATE = 30; private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE = new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1); + private static final CellResolution DEFAULT_CELL_RESOLUTION = + new CellResolution(/* columns= */ 32, /* rows= */ 15); private final XmlPullParserFactory xmlParserFactory; @@ -107,6 +113,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { int unsupportedNodeDepth = 0; int eventType = xmlParser.getEventType(); FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE; + CellResolution cellResolution = DEFAULT_CELL_RESOLUTION; while (eventType != XmlPullParser.END_DOCUMENT) { TtmlNode parent = nodeStack.peekLast(); if (unsupportedNodeDepth == 0) { @@ -114,12 +121,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { if (eventType == XmlPullParser.START_TAG) { if (TtmlNode.TAG_TT.equals(name)) { frameAndTickRate = parseFrameAndTickRates(xmlParser); + cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION); } if (!isSupportedTag(name)) { Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); unsupportedNodeDepth++; } else if (TtmlNode.TAG_HEAD.equals(name)) { - parseHeader(xmlParser, globalStyles, regionMap); + parseHeader(xmlParser, globalStyles, regionMap, cellResolution); } else { try { TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate); @@ -193,8 +201,36 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate); } - private Map parseHeader(XmlPullParser xmlParser, - Map globalStyles, Map globalRegions) + private CellResolution parseCellResolution(XmlPullParser xmlParser, CellResolution defaultValue) + throws SubtitleDecoderException { + String cellResolution = xmlParser.getAttributeValue(TTP, "cellResolution"); + if (cellResolution == null) { + return defaultValue; + } + + Matcher cellResolutionMatcher = CELL_RESOLUTION.matcher(cellResolution); + if (!cellResolutionMatcher.matches()) { + Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution); + return defaultValue; + } + try { + int columns = Integer.parseInt(cellResolutionMatcher.group(1)); + int rows = Integer.parseInt(cellResolutionMatcher.group(2)); + if (columns == 0 || rows == 0) { + throw new SubtitleDecoderException("Invalid cell resolution " + columns + " " + rows); + } + return new CellResolution(columns, rows); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution); + return defaultValue; + } + } + + private Map parseHeader( + XmlPullParser xmlParser, + Map globalStyles, + Map globalRegions, + CellResolution cellResolution) throws IOException, XmlPullParserException { do { xmlParser.next(); @@ -210,7 +246,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { globalStyles.put(style.getId(), style); } } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { - TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser); + TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution); if (ttmlRegion != null) { globalRegions.put(ttmlRegion.id, ttmlRegion); } @@ -221,12 +257,12 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { /** * Parses a region declaration. - *

      - * If the region defines an origin and extent, it is required that they're defined as percentages - * of the viewport. Region declarations that define origin and extent in other formats are - * unsupported, and null is returned. + * + *

      If the region defines an origin and extent, it is required that they're defined as + * percentages of the viewport. Region declarations that define origin and extent in other formats + * are unsupported, and null is returned. */ - private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) { + private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser, CellResolution cellResolution) { String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); if (regionId == null) { return null; @@ -305,7 +341,16 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } } - return new TtmlRegion(regionId, position, line, Cue.LINE_TYPE_FRACTION, lineAnchor, width); + float regionTextHeight = 1.0f / cellResolution.rows; + return new TtmlRegion( + regionId, + position, + line, + /* lineType= */ Cue.LINE_TYPE_FRACTION, + lineAnchor, + width, + /* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING, + /* textSize= */ regionTextHeight); } private String[] parseStyleIds(String parentStyleIds) { @@ -594,4 +639,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { this.tickRate = tickRate; } } + + /** Represents the cell resolution for a TTML file. */ + private static final class CellResolution { + final int columns; + final int rows; + + CellResolution(int columns, int rows) { + this.columns = columns; + this.rows = rows; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java index 1facb73567..c8b9a59de4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -179,8 +179,18 @@ import java.util.TreeSet; List cues = new ArrayList<>(); for (Entry entry : regionOutputs.entrySet()) { TtmlRegion region = regionMap.get(entry.getKey()); - cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType, - region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width)); + cues.add( + new Cue( + cleanUpText(entry.getValue()), + /* textAlignment= */ null, + region.line, + region.lineType, + region.lineAnchor, + region.position, + /* positionAnchor= */ Cue.TYPE_UNSET, + region.width, + region.textSizeType, + region.textSize)); } return cues; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java index 98823d7a84..2b1e9cf99a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java @@ -25,22 +25,41 @@ import com.google.android.exoplayer2.text.Cue; public final String id; public final float position; public final float line; - @Cue.LineType public final int lineType; - @Cue.AnchorType public final int lineAnchor; + public final @Cue.LineType int lineType; + public final @Cue.AnchorType int lineAnchor; public final float width; + public final @Cue.TextSizeType int textSizeType; + public final float textSize; public TtmlRegion(String id) { - this(id, Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); + this( + id, + /* position= */ Cue.DIMEN_UNSET, + /* line= */ Cue.DIMEN_UNSET, + /* lineType= */ Cue.TYPE_UNSET, + /* lineAnchor= */ Cue.TYPE_UNSET, + /* width= */ Cue.DIMEN_UNSET, + /* textSizeType= */ Cue.TYPE_UNSET, + /* textSize= */ Cue.DIMEN_UNSET); } - public TtmlRegion(String id, float position, float line, @Cue.LineType int lineType, - @Cue.AnchorType int lineAnchor, float width) { + public TtmlRegion( + String id, + float position, + float line, + @Cue.LineType int lineType, + @Cue.AnchorType int lineAnchor, + float width, + int textSizeType, + float textSize) { this.id = id; this.position = position; this.line = line; this.lineType = lineType; this.lineAnchor = lineAnchor; this.width = width; + this.textSizeType = textSizeType; + this.textSize = textSize; } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index d89f82b7c4..4dbd4d5fec 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -51,14 +51,10 @@ public final class SubtitleView extends View implements TextOutput { */ public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f; - private static final int FRACTIONAL = 0; - private static final int FRACTIONAL_IGNORE_PADDING = 1; - private static final int ABSOLUTE = 2; - private final List painters; private List cues; - private int textSizeType; + private @Cue.TextSizeType int textSizeType; private float textSize; private boolean applyEmbeddedStyles; private boolean applyEmbeddedFontSizes; @@ -72,7 +68,7 @@ public final class SubtitleView extends View implements TextOutput { public SubtitleView(Context context, AttributeSet attrs) { super(context, attrs); painters = new ArrayList<>(); - textSizeType = FRACTIONAL; + textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; textSize = DEFAULT_TEXT_SIZE_FRACTION; applyEmbeddedStyles = true; applyEmbeddedFontSizes = true; @@ -120,7 +116,9 @@ public final class SubtitleView extends View implements TextOutput { } else { resources = context.getResources(); } - setTextSize(ABSOLUTE, TypedValue.applyDimension(unit, size, resources.getDisplayMetrics())); + setTextSize( + Cue.TEXT_SIZE_TYPE_ABSOLUTE, + TypedValue.applyDimension(unit, size, resources.getDisplayMetrics())); } /** @@ -154,10 +152,14 @@ public final class SubtitleView extends View implements TextOutput { * height after the top and bottom padding has been subtracted. */ public void setFractionalTextSize(float fractionOfHeight, boolean ignorePadding) { - setTextSize(ignorePadding ? FRACTIONAL_IGNORE_PADDING : FRACTIONAL, fractionOfHeight); + setTextSize( + ignorePadding + ? Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING + : Cue.TEXT_SIZE_TYPE_FRACTIONAL, + fractionOfHeight); } - private void setTextSize(int textSizeType, float textSize) { + private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) { if (this.textSizeType == textSizeType && this.textSize == textSize) { return; } @@ -255,17 +257,61 @@ public final class SubtitleView extends View implements TextOutput { // No space to draw subtitles. return; } + int rawViewHeight = rawBottom - rawTop; + int viewHeightMinusPadding = bottom - top; - float textSizePx = textSizeType == ABSOLUTE ? textSize - : textSize * (textSizeType == FRACTIONAL ? (bottom - top) : (rawBottom - rawTop)); - if (textSizePx <= 0) { + float defaultViewTextSizePx = + resolveTextSize(textSizeType, textSize, rawViewHeight, viewHeightMinusPadding); + if (defaultViewTextSizePx <= 0) { // Text has no height. return; } for (int i = 0; i < cueCount; i++) { - painters.get(i).draw(cues.get(i), applyEmbeddedStyles, applyEmbeddedFontSizes, style, - textSizePx, bottomPaddingFraction, canvas, left, top, right, bottom); + Cue cue = cues.get(i); + float textSizePx = + resolveTextSizeForCue(cue, rawViewHeight, viewHeightMinusPadding, defaultViewTextSizePx); + SubtitlePainter painter = painters.get(i); + painter.draw( + cue, + applyEmbeddedStyles, + applyEmbeddedFontSizes, + style, + textSizePx, + bottomPaddingFraction, + canvas, + left, + top, + right, + bottom); + } + } + + private float resolveTextSizeForCue( + Cue cue, int rawViewHeight, int viewHeightMinusPadding, float defaultViewTextSizePx) { + if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) { + return defaultViewTextSizePx; + } + float defaultCueTextSizePx = + resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding); + return defaultCueTextSizePx > 0 ? defaultCueTextSizePx : defaultViewTextSizePx; + } + + private float resolveTextSize( + @Cue.TextSizeType int textSizeType, + float textSize, + int rawViewHeight, + int viewHeightMinusPadding) { + switch (textSizeType) { + case Cue.TEXT_SIZE_TYPE_ABSOLUTE: + return textSize; + case Cue.TEXT_SIZE_TYPE_FRACTIONAL: + return textSize * viewHeightMinusPadding; + case Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING: + return textSize * rawViewHeight; + case Cue.TYPE_UNSET: + default: + return Cue.DIMEN_UNSET; } } From 317d2c7b5c54398d4c5ecca76870829be920dab1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 17 May 2018 09:50:31 -0700 Subject: [PATCH 31/40] Fail for non-blacklistable playlist load errors in HLS This CL allows failure if a playlist load fails with a non-blacklistable error. For example, loss of internet connection. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=197006579 --- .../exoplayer2/source/hls/playlist/HlsPlaylistTracker.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index c9757a10e1..9986f5b65b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -570,7 +570,8 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Fri, 18 May 2018 04:17:57 -0700 Subject: [PATCH 32/40] Notify consistent event information to listeners Issue: #4262 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=197126757 --- RELEASENOTES.md | 3 +++ .../main/java/com/google/android/exoplayer2/ExoPlayer.java | 4 ---- .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 6 ++++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2238a95a15..fd20664692 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,9 @@ ([#2843](https://github.com/google/ExoPlayer/issues/2843)). * Fix crash when switching surface on Moto E(4) ([#4134](https://github.com/google/ExoPlayer/issues/4134)). +* Fix a bug that could cause event listeners to be called with inconsistent + information if an event listener interacted with the player + ([#4262](https://github.com/google/ExoPlayer/issues/4262)). * Audio: * Fix extraction of PCM in MP4/MOV ([#4228](https://github.com/google/ExoPlayer/issues/4228)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 6d8dd5b7a8..39a6243933 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -185,10 +185,6 @@ public interface ExoPlayer extends Player { */ Looper getPlaybackLooper(); - @Override - @Nullable - ExoPlaybackException getPlaybackError(); - /** * Prepares the player to play the provided {@link MediaSource}. Equivalent to * {@code prepare(mediaSource, true, true)}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 5ca5994b6e..4125a203a6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -193,6 +193,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (this.playWhenReady != playWhenReady) { this.playWhenReady = playWhenReady; internalPlayer.setPlayWhenReady(playWhenReady); + PlaybackInfo playbackInfo = this.playbackInfo; for (Player.EventListener listener : listeners) { listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); } @@ -570,7 +571,8 @@ import java.util.concurrent.CopyOnWriteArraySet; } break; case ExoPlayerImplInternal.MSG_ERROR: - playbackError = (ExoPlaybackException) msg.obj; + ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj; + this.playbackError = playbackError; for (Player.EventListener listener : listeners) { listener.onPlayerError(playbackError); } @@ -652,7 +654,7 @@ import java.util.concurrent.CopyOnWriteArraySet; boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState; boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading; boolean trackSelectorResultChanged = - this.playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult; + playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult; playbackInfo = newPlaybackInfo; if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { for (Player.EventListener listener : listeners) { From 2b9c31a14f177bbfd6c190726dcb5d3fcb75f17b Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 18 May 2018 05:48:48 -0700 Subject: [PATCH 33/40] Add MediaSource and DataSource to inject playback nonce into URLs. A new playback nonce is created for each playback of the same item. Thus we need to inject the nonce dynamically into the data source factory. This CL adds the DataSource which does the actual insertion into the request URLs and a MediaSource which listens to new media periods, to request the nonce and to configure the data source factory for this media period to use this nonce. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=197134217 --- .../exoplayer2/upstream/DataSource.java | 3 +- .../android/exoplayer2/upstream/DataSpec.java | 27 +++++++++++---- .../upstream/PriorityDataSource.java | 3 +- .../android/exoplayer2/util/UriUtil.java | 20 +++++++++++ .../android/exoplayer2/util/UriUtilTest.java | 34 +++++++++++++++++++ .../exoplayer2/testutil/FakeDataSource.java | 5 +++ .../exoplayer2/testutil/FakeMediaSource.java | 5 +++ 7 files changed, 89 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java index 4a2354e180..ce3230fa43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.IOException; @@ -79,7 +80,7 @@ public interface DataSource { * * @return The {@link Uri} from which data is being read, or null if the source is not open. */ - Uri getUri(); + @Nullable Uri getUri(); /** * Closes the source. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index a6b89a334d..ad7a9d0147 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -61,7 +61,7 @@ public final class DataSpec { /** * Body for a POST request, null otherwise. */ - public final byte[] postBody; + public final @Nullable byte[] postBody; /** * The absolute position of the data in the full stream. */ @@ -81,12 +81,12 @@ public final class DataSpec { * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the * {@link DataSpec} is not intended to be used in conjunction with a cache. */ - @Nullable public final String key; + public final @Nullable String key; /** * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags. */ - @Flags public final int flags; + public final @Flags int flags; /** * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. @@ -128,7 +128,8 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, @Flags int flags) { + public DataSpec( + Uri uri, long absoluteStreamPosition, long length, @Nullable String key, @Flags int flags) { this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags); } @@ -143,7 +144,12 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long position, long length, String key, + public DataSpec( + Uri uri, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, @Flags int flags) { this(uri, null, absoluteStreamPosition, position, length, key, flags); } @@ -162,7 +168,7 @@ public final class DataSpec { */ public DataSpec( Uri uri, - byte[] postBody, + @Nullable byte[] postBody, long absoluteStreamPosition, long position, long length, @@ -222,4 +228,13 @@ public final class DataSpec { } } + /** + * Returns a copy of this {@link DataSpec} with the specified Uri. + * + * @param uri The new source {@link Uri}. + * @return The copied {@link DataSpec} with the specified Uri. + */ + public DataSpec withUri(Uri uri) { + return new DataSpec(uri, postBody, absoluteStreamPosition, position, length, key, flags); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java index a36ccd11b1..729f7fe179 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; @@ -63,7 +64,7 @@ public final class PriorityDataSource implements DataSource { } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return upstream.getUri(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java index 6592273d03..071ebf2084 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java @@ -143,6 +143,26 @@ public final class UriUtil { } } + /** + * Removes query parameter from an Uri, if present. + * + * @param uri The uri. + * @param queryParameterName The name of the query parameter. + * @return The uri without the query parameter. + */ + public static Uri removeQueryParameter(Uri uri, String queryParameterName) { + Uri.Builder builder = uri.buildUpon(); + builder.clearQuery(); + for (String key : uri.getQueryParameterNames()) { + if (!key.equals(queryParameterName)) { + for (String value : uri.getQueryParameters(key)) { + builder.appendQueryParameter(key, value); + } + } + } + return builder.build(); + } + /** * Removes dot segments from the path of a URI. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java index a52867e1b2..82c62ecb3e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer2.util; +import static com.google.android.exoplayer2.util.UriUtil.removeQueryParameter; import static com.google.android.exoplayer2.util.UriUtil.resolve; import static com.google.common.truth.Truth.assertThat; +import android.net.Uri; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -104,4 +106,36 @@ public final class UriUtilTest { assertThat(resolve("a:b", "../c")).isEqualTo("a:c"); } + @Test + public void removeOnlyQueryParameter() { + Uri uri = Uri.parse("http://uri?query=value"); + assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri"); + } + + @Test + public void removeFirstQueryParameter() { + Uri uri = Uri.parse("http://uri?query=value&second=value2"); + assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri?second=value2"); + } + + @Test + public void removeMiddleQueryParameter() { + Uri uri = Uri.parse("http://uri?first=value1&query=value&last=value2"); + assertThat(removeQueryParameter(uri, "query").toString()) + .isEqualTo("http://uri?first=value1&last=value2"); + } + + @Test + public void removeLastQueryParameter() { + Uri uri = Uri.parse("http://uri?first=value1&query=value"); + assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri?first=value1"); + } + + @Test + public void removeNonExistentQueryParameter() { + Uri uri = Uri.parse("http://uri"); + assertThat(removeQueryParameter(uri, "foo").toString()).isEqualTo("http://uri"); + uri = Uri.parse("http://uri?query=value"); + assertThat(removeQueryParameter(uri, "foo").toString()).isEqualTo("http://uri?query=value"); + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index 2675e1f0d7..de623b59c9 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -217,6 +217,11 @@ public class FakeDataSource implements DataSource { return dataSpecs; } + /** Returns whether the data source is currently opened. */ + public final boolean isOpened() { + return opened; + } + protected void onDataRead(int bytesRead) throws IOException { // Do nothing. Can be overridden. } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 905adae092..ffc877bf42 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -159,6 +159,11 @@ public class FakeMediaSource extends BaseMediaSource { } } + /** Asserts that the source has been prepared. */ + public void assertPrepared() { + assertThat(preparedSource).isTrue(); + } + /** * Assert that the source and all periods have been released. */ From f3e650b8c7173ceb6d28cb9776a153f1536d889f Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 18 May 2018 07:08:39 -0700 Subject: [PATCH 34/40] Update InstrumentationTestCase to use JUnit4. InstrumentationTestCase has been deprecated, and it does not offer some useful features, such as targeting SDK version level for tests, or skipping tests if necessary. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=197141082 --- constants.gradle | 1 + library/core/build.gradle | 9 ++++ .../upstream/ContentDataSourceTest.java | 45 ++++++++++++------- .../cache/CachedContentIndexTest.java | 39 +++++++++++----- .../upstream/cache/SimpleCacheSpanTest.java | 29 +++++++----- 5 files changed, 82 insertions(+), 41 deletions(-) diff --git a/constants.gradle b/constants.gradle index dcadcceb4f..8871236417 100644 --- a/constants.gradle +++ b/constants.gradle @@ -33,6 +33,7 @@ project.ext { robolectricVersion = '3.7.1' autoValueVersion = '1.6' checkerframeworkVersion = '2.5.0' + testRunnerVersion = '1.0.2' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/library/core/build.gradle b/library/core/build.gradle index d2fa5e25f8..bb331b615c 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -22,6 +22,13 @@ android { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + // The following argument makes the Android Test Orchestrator run its + // "pm clear" command after each test invocation. This command ensures + // that the app's state is completely cleared between tests. + testInstrumentationRunnerArguments clearPackageData: 'true' } // Workaround to prevent circular dependency on project :testutils. @@ -51,6 +58,8 @@ dependencies { androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion + androidTestImplementation 'com.android.support.test:runner:' + testRunnerVersion + androidTestUtil 'com.android.support.test:orchestrator:' + testRunnerVersion testImplementation 'com.google.truth:truth:' + truthVersion testImplementation 'junit:junit:' + junitVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index 3465393853..1133928e91 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.upstream; import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.fail; -import android.app.Instrumentation; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; @@ -28,48 +28,58 @@ import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.test.InstrumentationTestCase; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; -/** - * Unit tests for {@link ContentDataSource}. - */ -public final class ContentDataSourceTest extends InstrumentationTestCase { +/** Unit tests for {@link ContentDataSource}. */ +@RunWith(AndroidJUnit4.class) +public final class ContentDataSourceTest { private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; + @Test public void testRead() throws Exception { - assertData(getInstrumentation(), 0, C.LENGTH_UNSET, false); + assertData(0, C.LENGTH_UNSET, false); } + @Test public void testReadPipeMode() throws Exception { - assertData(getInstrumentation(), 0, C.LENGTH_UNSET, true); + assertData(0, C.LENGTH_UNSET, true); } + @Test public void testReadFixedLength() throws Exception { - assertData(getInstrumentation(), 0, 100, false); + assertData(0, 100, false); } + @Test public void testReadFromOffsetToEndOfInput() throws Exception { - assertData(getInstrumentation(), 1, C.LENGTH_UNSET, false); + assertData(1, C.LENGTH_UNSET, false); } + @Test public void testReadFromOffsetToEndOfInputPipeMode() throws Exception { - assertData(getInstrumentation(), 1, C.LENGTH_UNSET, true); + assertData(1, C.LENGTH_UNSET, true); } + @Test public void testReadFromOffsetFixedLength() throws Exception { - assertData(getInstrumentation(), 1, 100, false); + assertData(1, 100, false); } + @Test public void testReadInvalidUri() throws Exception { - ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); + ContentDataSource dataSource = + new ContentDataSource(InstrumentationRegistry.getTargetContext()); Uri contentUri = TestContentProvider.buildUri("does/not.exist", false); DataSpec dataSpec = new DataSpec(contentUri); try { @@ -83,13 +93,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { } } - private static void assertData(Instrumentation instrumentation, int offset, int length, - boolean pipeMode) throws IOException { + private static void assertData(int offset, int length, boolean pipeMode) throws IOException { Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode); - ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext()); + ContentDataSource dataSource = + new ContentDataSource(InstrumentationRegistry.getTargetContext()); try { DataSpec dataSpec = new DataSpec(contentUri, offset, length, null); - byte[] completeData = TestUtil.getByteArray(instrumentation.getContext(), DATA_PATH); + byte[] completeData = + TestUtil.getByteArray(InstrumentationRegistry.getTargetContext(), DATA_PATH); byte[] expectedData = Arrays.copyOfRange(completeData, offset, length == C.LENGTH_UNSET ? completeData.length : offset + length); TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index 58531346ab..be4a2a96dc 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -19,7 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import android.net.Uri; -import android.test.InstrumentationTestCase; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; @@ -29,9 +30,14 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.Collection; import java.util.Set; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; /** Tests {@link CachedContentIndex}. */ -public class CachedContentIndexTest extends InstrumentationTestCase { +@RunWith(AndroidJUnit4.class) +public class CachedContentIndexTest { private final byte[] testIndexV1File = { 0, 0, 0, 1, // version @@ -70,19 +76,19 @@ public class CachedContentIndexTest extends InstrumentationTestCase { private CachedContentIndex index; private File cacheDir; - @Override + @Before public void setUp() throws Exception { - super.setUp(); - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cacheDir = + Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() { Util.recursiveDelete(cacheDir); - super.tearDown(); } + @Test public void testAddGetRemove() throws Exception { final String key1 = "key1"; final String key2 = "key2"; @@ -132,10 +138,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(cacheSpanFile.exists()).isTrue(); } + @Test public void testStoreAndLoad() throws Exception { assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir)); } + @Test public void testLoadV1() throws Exception { FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); fos.write(testIndexV1File); @@ -153,6 +161,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); } + @Test public void testLoadV2() throws Exception { FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); fos.write(testIndexV2File); @@ -171,7 +180,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); } - public void testAssignIdForKeyAndGetKeyForId() throws Exception { + @Test + public void testAssignIdForKeyAndGetKeyForId() { final String key1 = "key1"; final String key2 = "key2"; int id1 = index.assignIdForKey(key1); @@ -183,7 +193,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(index.assignIdForKey(key2)).isEqualTo(id2); } - public void testGetNewId() throws Exception { + @Test + public void testGetNewId() { SparseArray idToKey = new SparseArray<>(); assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0); idToKey.put(10, ""); @@ -194,6 +205,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1); } + @Test public void testEncryption() throws Exception { byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key @@ -250,7 +262,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key)); } - public void testRemoveEmptyNotLockedCachedContent() throws Exception { + @Test + public void testRemoveEmptyNotLockedCachedContent() { CachedContent cachedContent = index.getOrAdd("key1"); index.maybeRemove(cachedContent.key); @@ -258,6 +271,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(index.get(cachedContent.key)).isNull(); } + @Test public void testCantRemoveNotEmptyCachedContent() throws Exception { CachedContent cachedContent = index.getOrAdd("key1"); File cacheSpanFile = @@ -270,7 +284,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(index.get(cachedContent.key)).isNotNull(); } - public void testCantRemoveLockedCachedContent() throws Exception { + @Test + public void testCantRemoveLockedCachedContent() { CachedContent cachedContent = index.getOrAdd("key1"); cachedContent.setLocked(true); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index 637a19cdd2..afbbf6605f 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -18,7 +18,8 @@ package com.google.android.exoplayer2.upstream.cache; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import android.test.InstrumentationTestCase; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.FileOutputStream; @@ -26,11 +27,14 @@ import java.io.IOException; import java.util.HashMap; import java.util.Set; import java.util.TreeSet; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; -/** - * Unit tests for {@link SimpleCacheSpan}. - */ -public class SimpleCacheSpanTest extends InstrumentationTestCase { +/** Unit tests for {@link SimpleCacheSpan}. */ +@RunWith(AndroidJUnit4.class) +public class SimpleCacheSpanTest { private CachedContentIndex index; private File cacheDir; @@ -49,19 +53,19 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { return SimpleCacheSpan.createCacheEntry(cacheFile, index); } - @Override - protected void setUp() throws Exception { - super.setUp(); - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + @Before + public void setUp() throws Exception { + cacheDir = + Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() { Util.recursiveDelete(cacheDir); - super.tearDown(); } + @Test public void testCacheFile() throws Exception { assertCacheSpan("key1", 0, 0); assertCacheSpan("key2", 1, 2); @@ -80,6 +84,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { + "A paragraph-separator character \u2029", 1, 2); } + @Test public void testUpgradeFileName() throws Exception { String key = "asd\u00aa"; int id = index.assignIdForKey(key); From e162d35689ee5b0b086a2e3087a477e2c0cd666b Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 21 May 2018 08:37:51 -0700 Subject: [PATCH 35/40] Fix permissions in demo app. The ACCESS_NETWORK_STATE permission is only included indirectly which doesn't work in all build systems. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=197399274 --- demos/main/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 3bedefc60e..2232a8b3eb 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ package="com.google.android.exoplayer2.demo"> + From 7b855e45e60a2932105c1ffbf091c937cf3b7679 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 21 May 2018 10:34:40 -0700 Subject: [PATCH 36/40] Enable HLS sample queues as soon as possible. Currently, the sample queues are lazily enabled when they are first read from. This causes problems when the player tries to discard buffer and the HlsSampleStreamWrapper assumes the sample queue is disabled even though it's actually enabled but hasn't been read from. This change moves setting the sample queue index of the sample stream back into HlsSampleStreamWrapper. It enables the sample queues at track selection if the queues are already built, or immediately after they have been built for chunkless preparation. Issue:#4241 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=197415741 --- RELEASENOTES.md | 2 ++ .../source/hls/HlsSampleStream.java | 20 ++++++++++------- .../source/hls/HlsSampleStreamWrapper.java | 22 ++++++++++++++++--- .../hls/SampleQueueMappingException.java | 3 ++- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fd20664692..018da272f6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,8 @@ * HLS: * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags ([#4239](https://github.com/google/ExoPlayer/issues/4239)). + * Fix playback of clipped streams starting from non-keyframe positions + ([#4241](https://github.com/google/ExoPlayer/issues/4241)). * Caption: * TTML: * Fix a styling issue when there are multiple regions displayed at the same diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index 5d4d953372..f43d119018 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; /** @@ -36,6 +37,11 @@ import java.io.IOException; sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING; } + public void bindSampleQueue() { + Assertions.checkArgument(sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING); + sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex); + } + public void unbindSampleQueue() { if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { sampleStreamWrapper.unbindSampleQueue(trackGroupIndex); @@ -48,12 +54,11 @@ import java.io.IOException; @Override public boolean isReady() { return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL - || (maybeMapToSampleQueue() && sampleStreamWrapper.isReady(sampleQueueIndex)); + || (hasValidSampleQueueIndex() && sampleStreamWrapper.isReady(sampleQueueIndex)); } @Override public void maybeThrowError() throws IOException { - maybeMapToSampleQueue(); if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) { throw new SampleQueueMappingException( sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); @@ -63,22 +68,21 @@ import java.io.IOException; @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { - return maybeMapToSampleQueue() + return hasValidSampleQueueIndex() ? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat) : C.RESULT_NOTHING_READ; } @Override public int skipData(long positionUs) { - return maybeMapToSampleQueue() ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs) : 0; + return hasValidSampleQueueIndex() + ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs) + : 0; } // Internal methods. - private boolean maybeMapToSampleQueue() { - if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { - sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex); - } + private boolean hasValidSampleQueueIndex() { return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 0de4faa9c0..705320bdad 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -102,6 +102,7 @@ import java.util.Arrays; private final Runnable maybeFinishPrepareRunnable; private final Runnable onTracksEndedRunnable; private final Handler handler; + private final ArrayList hlsSampleStreams; private SampleQueue[] sampleQueues; private int[] sampleQueueTrackIds; @@ -166,6 +167,7 @@ import java.util.Arrays; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; mediaChunks = new ArrayList<>(); + hlsSampleStreams = new ArrayList<>(); maybeFinishPrepareRunnable = new Runnable() { @Override @@ -219,9 +221,6 @@ import java.util.Arrays; } public int bindSampleQueueToSampleStream(int trackGroupIndex) { - if (trackGroupToSampleQueueIndex == null) { - return SAMPLE_QUEUE_INDEX_PENDING; - } int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; if (sampleQueueIndex == C.INDEX_UNSET) { return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET @@ -295,6 +294,9 @@ import java.util.Arrays; } streams[i] = new HlsSampleStream(this, trackGroupIndex); streamResetFlags[i] = true; + if (trackGroupToSampleQueueIndex != null) { + ((HlsSampleStream) streams[i]).bindSampleQueue(); + } // If there's still a chance of avoiding a seek, try and seek within the sample queue. if (sampleQueuesBuilt && !seekRequired) { SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]]; @@ -360,6 +362,7 @@ import java.util.Arrays; } } + updateSampleStreams(streams); seenFirstTrackSelection = true; return seekRequired; } @@ -411,6 +414,7 @@ import java.util.Arrays; loader.release(this); handler.removeCallbacksAndMessages(null); released = true; + hlsSampleStreams.clear(); } @Override @@ -750,6 +754,15 @@ import java.util.Arrays; // Internal methods. + private void updateSampleStreams(SampleStream[] streams) { + hlsSampleStreams.clear(); + for (SampleStream stream : streams) { + if (stream != null) { + hlsSampleStreams.add((HlsSampleStream) stream); + } + } + } + private boolean finishedReadingChunk(HlsMediaChunk chunk) { int chunkUid = chunk.uid; int sampleQueueCount = sampleQueues.length; @@ -807,6 +820,9 @@ import java.util.Arrays; } } } + for (HlsSampleStream sampleStream : hlsSampleStreams) { + sampleStream.bindSampleQueue(); + } } /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java index 2d430d2c79..9c9cb532a6 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.hls; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.TrackGroup; import java.io.IOException; @@ -23,7 +24,7 @@ import java.io.IOException; public final class SampleQueueMappingException extends IOException { /** @param mimeType The mime type of the track group whose mapping failed. */ - public SampleQueueMappingException(String mimeType) { + public SampleQueueMappingException(@Nullable String mimeType) { super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + "."); } } From 51403410944dd3fd003e9879cd5f17739a43bdf3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 22 May 2018 02:14:56 -0700 Subject: [PATCH 37/40] Make ffmpeg build instructions repeatable ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=197531723 --- extensions/ffmpeg/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index 1f1b93ce53..d5a37db013 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -70,7 +70,8 @@ COMMON_OPTIONS="\ --enable-decoder=flac \ " && \ cd "${FFMPEG_EXT_PATH}/jni" && \ -(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && cd ffmpeg && \ +(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \ +cd ffmpeg && \ ./configure \ --libdir=android-libs/armeabi-v7a \ --arch=arm \ From 1896f2fa7c5c572a68cb9dffd5f21c9e03674a3f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 22 May 2018 02:16:10 -0700 Subject: [PATCH 38/40] Save/restore current cues in SimpleExoPlayer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=197531839 --- .../com/google/android/exoplayer2/SimpleExoPlayer.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 5539337257..0a0df03053 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -92,7 +92,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player private AudioAttributes audioAttributes; private float audioVolume; private MediaSource mediaSource; - private @Nullable List currentCues; + private List currentCues; /** * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. @@ -178,6 +178,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioAttributes = AudioAttributes.DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; + currentCues = Collections.emptyList(); // Build the player and associated objects. player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); @@ -503,7 +504,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void addTextOutput(TextOutput listener) { - if(currentCues != null) { + if (!currentCues.isEmpty()) { listener.onCues(currentCues); } textOutputs.add(listener); @@ -779,7 +780,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player mediaSource = null; analyticsCollector.resetForNewMediaSource(); } - currentCues = null; + currentCues = Collections.emptyList(); } @Override @@ -795,7 +796,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player if (mediaSource != null) { mediaSource.removeEventListener(analyticsCollector); } - currentCues = null; + currentCues = Collections.emptyList(); } @Override From 2a23838116bf941ee023298631a950a1f5fc08f0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 22 May 2018 02:54:14 -0700 Subject: [PATCH 39/40] Bump version to 2.8.1 and update release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=197535443 --- RELEASENOTES.md | 16 ++++++++++------ constants.gradle | 4 ++-- .../android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 018da272f6..0b17cb5251 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,15 @@ ### dev-v2 (not yet released) ### +* Coming soon + +### 2.8.1 ### + +* HLS: + * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags + ([#4239](https://github.com/google/ExoPlayer/issues/4239)). + * Fix playback of clipped streams starting from non-keyframe positions + ([#4241](https://github.com/google/ExoPlayer/issues/4241)). * OkHttp extension: Fix to correctly include response headers in thrown `InvalidResponseCodeException`s. * Add possibility to cancel `PlayerMessage`s. @@ -19,12 +28,7 @@ ([#4228](https://github.com/google/ExoPlayer/issues/4228)). * FLAC: Supports seeking for FLAC files without SEEKTABLE ([#1808](https://github.com/google/ExoPlayer/issues/1808)). -* HLS: - * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags - ([#4239](https://github.com/google/ExoPlayer/issues/4239)). - * Fix playback of clipped streams starting from non-keyframe positions - ([#4241](https://github.com/google/ExoPlayer/issues/4241)). -* Caption: +* Captions: * TTML: * Fix a styling issue when there are multiple regions displayed at the same time that can make text size of each region much smaller than defined. diff --git a/constants.gradle b/constants.gradle index 8871236417..9068fb8b56 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.8.0' - releaseVersionCode = 2800 + releaseVersion = '2.8.1' + releaseVersionCode = 2801 // Important: ExoPlayer specifies a minSdkVersion of 14 because various // components provided by the library may be of use on older devices. // However, please note that the core media playback functionality provided diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 98d5fe91b7..aabb01481b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ 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.8.0"; + public static final String VERSION = "2.8.1"; /** 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.8.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,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 = 2008000; + public static final int VERSION_INT = 2008001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From f6cc43cec7b4fd2933e711a5d33754d804a642b2 Mon Sep 17 00:00:00 2001 From: Andrew Lewis Date: Tue, 22 May 2018 11:58:22 +0100 Subject: [PATCH 40/40] Update release notes --- RELEASENOTES.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0b17cb5251..cb3d654e18 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,9 +1,5 @@ # Release notes # -### dev-v2 (not yet released) ### - -* Coming soon - ### 2.8.1 ### * HLS: