From c79830bd10d7d636db349c73557789579e557ce6 Mon Sep 17 00:00:00 2001 From: Johnny Date: Thu, 14 May 2020 11:58:39 +0800 Subject: [PATCH 01/42] Dash Early Access Period support Parse Early Access Period in DashManifestParser Workaround VOS Early Access Period with extremely large Presentation Time offset --- .../source/dash/manifest/DashManifest.java | 6 ++++++ .../source/dash/manifest/DashManifestParser.java | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 36135e04546..8a773675805 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -92,6 +92,7 @@ public class DashManifest implements FilterableManifest { @Nullable public final ProgramInformation programInformation; private final List periods; + List earlyAccessPeriods; /** * @deprecated Use {@link #DashManifest(long, long, long, boolean, long, long, long, long, @@ -153,12 +154,17 @@ public DashManifest( this.location = location; this.serviceDescription = serviceDescription; this.periods = periods == null ? Collections.emptyList() : periods; + this.earlyAccessPeriods = Collections.emptyList(); } public final int getPeriodCount() { return periods.size(); } + public final List getEarlyAccessPeriods() { + return earlyAccessPeriods; + } + public final Period getPeriod(int index) { return periods.get(index); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 9b5efd49535..22f6521605a 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -129,6 +129,7 @@ protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, long baseUrlAvailabilityTimeOffsetUs = dynamic ? 0 : C.TIME_UNSET; List periods = new ArrayList<>(); + List earlyAccessPeriods = new ArrayList<>(); long nextPeriodStartMs = dynamic ? C.TIME_UNSET : 0; boolean seenEarlyAccessPeriod = false; boolean seenFirstBaseUrl = false; @@ -163,6 +164,7 @@ protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, if (dynamic) { // This is an early access period. Ignore it. All subsequent periods must also be // early access. + earlyAccessPeriods.add(period); seenEarlyAccessPeriod = true; } else { throw new ParserException("Unable to determine start of period " + periods.size()); @@ -190,8 +192,7 @@ protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, if (periods.isEmpty()) { throw new ParserException("No periods found."); } - - return buildMediaPresentationDescription( + DashManifest manifest = buildMediaPresentationDescription( availabilityStartTime, durationMs, minBufferTimeMs, @@ -205,6 +206,8 @@ protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, serviceDescription, location, periods); + manifest.earlyAccessPeriods = earlyAccessPeriods; + return manifest; } protected DashManifest buildMediaPresentationDescription( @@ -1740,7 +1743,14 @@ protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) { String value = xpp.getAttributeValue(null, name); - return value == null ? defaultValue : Long.parseLong(value); + if (value == null) { + return defaultValue; + } + try { + return Long.parseLong(value); + } catch (NumberFormatException ex) { + return defaultValue; + } } protected static float parseFloat(XmlPullParser xpp, String name, float defaultValue) { From 27264836f2f39d5cdaed9c904504db31f625c899 Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Fri, 19 Mar 2021 08:17:21 +0800 Subject: [PATCH 02/42] Manifest patching --- .../source/dash/DashMediaSource.java | 91 ++++++- .../exoplayer2/source/dash/DashUtil.java | 13 + .../source/dash/manifest/DashManifest.java | 10 +- .../dash/manifest/DashManifestParser.java | 34 ++- .../dash/manifest/DashManifestPatch.java | 179 +++++++++++++ .../manifest/DashManifestPatchMerger.java | 75 ++++++ .../manifest/DashManifestPatchParser.java | 197 ++++++++++++++ .../manifest/DocumentToManifestConverter.java | 40 +++ .../source/dash/manifest/PatchLocation.java | 34 +++ .../manifest/DashManifestPatchParserTest.java | 108 ++++++++ .../dash/manifest/DashManifestPatchTest.java | 241 ++++++++++++++++++ .../dash/manifest/DashManifestTest.java | 1 + .../DocumentToManifestConverterTest.java | 54 ++++ .../manifest_patch/mpd_multi_period_scte35 | 63 +++++ .../assets/media/mpd/manifest_patch/mpd_patch | 17 ++ .../manifest_patch/mpd_patch_add_attribute | 4 + .../mpd/manifest_patch/mpd_patch_add_period | 38 +++ .../mpd/manifest_patch/mpd_patch_add_segments | 7 + .../mpd_patch_replace_attribute | 4 + .../mpd/manifest_patch/mpd_patch_replace_node | 6 + .../manifest_patch/mpd_with_patch_location | 50 ++++ .../media/mpd/manifest_patch/xml_add_period | 1 + .../media/mpd/manifest_patch/xml_add_segments | 1 + .../media/mpd/manifest_patch/xml_replace_node | 1 + 24 files changed, 1260 insertions(+), 9 deletions(-) create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatch.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchMerger.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParser.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DocumentToManifestConverter.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/PatchLocation.java create mode 100644 library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParserTest.java create mode 100644 library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchTest.java create mode 100644 library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DocumentToManifestConverterTest.java create mode 100644 testdata/src/test/assets/media/mpd/manifest_patch/mpd_multi_period_scte35 create mode 100644 testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch create mode 100644 testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_add_attribute create mode 100644 testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_add_period create mode 100644 testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_add_segments create mode 100644 testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_replace_attribute create mode 100644 testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_replace_node create mode 100644 testdata/src/test/assets/media/mpd/manifest_patch/mpd_with_patch_location create mode 100644 testdata/src/test/assets/media/mpd/manifest_patch/xml_add_period create mode 100644 testdata/src/test/assets/media/mpd/manifest_patch/xml_add_segments create mode 100644 testdata/src/test/assets/media/mpd/manifest_patch/xml_replace_node diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index d63295adde2..cfa12dc8bdc 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -56,6 +56,7 @@ import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.manifest.DashManifestPatchMerger; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; @@ -104,6 +105,7 @@ public static final class Factory implements MediaSourceFactory { private boolean usingCustomDrmSessionManagerProvider; private DrmSessionManagerProvider drmSessionManagerProvider; + @Nullable private ParsingLoadable.Parser manifestPatchMerger; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long targetLiveOffsetOverrideMs; @@ -340,6 +342,7 @@ public DashMediaSource createMediaSource(DashManifest manifest, MediaItem mediaI manifest, /* manifestDataSourceFactory= */ null, /* manifestParser= */ null, + /* manifestPatchParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, drmSessionManagerProvider.get(mediaItem), @@ -401,11 +404,15 @@ public DashMediaSource createMediaSource(MediaItem mediaItem) { } mediaItem = builder.build(); } + if (manifestPatchMerger == null) { + manifestPatchMerger = new DashManifestPatchMerger(); + } return new DashMediaSource( mediaItem, /* manifest= */ null, manifestDataSourceFactory, manifestParser, + manifestPatchMerger, chunkSourceFactory, compositeSequenceableLoaderFactory, drmSessionManagerProvider.get(mediaItem), @@ -453,6 +460,8 @@ public int[] getSupportedTypes() { private final EventDispatcher manifestEventDispatcher; private final ParsingLoadable.Parser manifestParser; private final ManifestCallback manifestCallback; + private final ParsingLoadable.Parser manifestPatchMerger; + private final ManifestPatchCallback manifestPatchCallback; private final Object manifestUriLock; private final SparseArray periodsById; private final Runnable refreshManifestRunnable; @@ -486,6 +495,7 @@ private DashMediaSource( @Nullable DashManifest manifest, @Nullable DataSource.Factory manifestDataSourceFactory, @Nullable ParsingLoadable.Parser manifestParser, + @Nullable ParsingLoadable.Parser manifestPatchMerger, DashChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, DrmSessionManager drmSessionManager, @@ -498,6 +508,7 @@ private DashMediaSource( this.manifest = manifest; this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestParser = manifestParser; + this.manifestPatchMerger = manifestPatchMerger; this.chunkSourceFactory = chunkSourceFactory; this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; @@ -513,11 +524,13 @@ private DashMediaSource( if (sideloadedManifest) { Assertions.checkState(!manifest.dynamic); manifestCallback = null; + manifestPatchCallback = null; refreshManifestRunnable = null; simulateManifestRefreshRunnable = null; manifestLoadErrorThrower = new LoaderErrorThrower.Dummy(); } else { manifestCallback = new ManifestCallback(); + manifestPatchCallback = new ManifestPatchCallback(); manifestLoadErrorThrower = new ManifestLoadErrorThrower(); refreshManifestRunnable = this::startLoadingManifest; simulateManifestRefreshRunnable = () -> processManifest(false); @@ -662,7 +675,13 @@ protected void releaseSourceInternal() { loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); manifestEventDispatcher.loadCompleted(loadEventInfo, loadable.type); DashManifest newManifest = loadable.getResult(); + onManifestUpdated(newManifest, elapsedRealtimeMs, loadDurationMs, + loadable.type, loadable.dataSpec.uri,loadable.getUri()); + } + /* package */ void onManifestUpdated(DashManifest newManifest, + long elapsedRealtimeMs, long loadDurationMs, + int type, Uri uri,Uri loadableUri) { int oldPeriodCount = manifest == null ? 0 : manifest.getPeriodCount(); int removedPeriodCount = 0; long newFirstPeriodStartTimeMs = newManifest.getPeriod(0).startMs; @@ -697,7 +716,7 @@ protected void releaseSourceInternal() { if (isManifestStale) { if (staleManifestReloadAttempt++ - < loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)) { + < loadErrorHandlingPolicy.getMinimumLoadableRetryCount(type)) { scheduleManifestRefresh(getManifestLoadRetryDelayMillis()); } else { manifestFatalError = new DashManifestStaleException(); @@ -717,12 +736,12 @@ protected void releaseSourceInternal() { // start and end of this load. If it was then isSameUriInstance evaluates to false, and we // prefer the manual replacement to one derived from the previous request. @SuppressWarnings("ReferenceEquality") - boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri; + boolean isSameUriInstance = uri == manifestUri; if (isSameUriInstance) { // Replace the manifest URI with one specified by a manifest Location element (if present), // or with the final (possibly redirected) URI. This follows the recommendation in // DASH-IF-IOP 4.3, section 3.2.15.3. See: https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf. - manifestUri = manifest.location != null ? manifest.location : loadable.getUri(); + manifestUri = manifest.location != null ? manifest.location : loadableUri; } } @@ -773,6 +792,14 @@ protected void releaseSourceInternal() { return loadErrorAction; } + /* package */ LoadErrorAction onManifestPatchLoadError() { + startLoading( + new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser), + manifestCallback, + loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MANIFEST)); + return Loader.DONT_RETRY; + } + /* package */ void onUtcTimestampLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { LoadEventInfo loadEventInfo = @@ -1071,10 +1098,34 @@ private void startLoadingManifest() { manifestUri = this.manifestUri; } manifestLoadPending = false; - startLoading( - new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser), - manifestCallback, - loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MANIFEST)); + + boolean needLoadFullManifest = true; + if (manifest != null && manifest.patchLocation != null) { + if (manifest.publishTimeMs + manifest.patchLocation.ttl * 1000 > System.currentTimeMillis()) { + DashManifestPatchMerger patchMerger = (DashManifestPatchMerger) manifestPatchMerger; + DashManifestParser parser = (DashManifestParser)manifestParser; + try { + patchMerger.setManifestString(parser.getManifestString()); + startLoading( + new ParsingLoadable<>(dataSource, Uri.parse(manifest.patchLocation.url), + C.DATA_TYPE_MANIFEST, manifestPatchMerger), + manifestPatchCallback, + loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MANIFEST)); + needLoadFullManifest = false; + } catch (Exception ex) { + Log.w(TAG, "Failed to load Manifest Patch, fallback to load full manifest", ex); + } + } else { + Log.d(TAG, "Manifest Patch timeout, fallback to load full manifest"); + } + } + + if (needLoadFullManifest) { + startLoading( + new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser), + manifestCallback, + loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MANIFEST)); + } } private long getManifestLoadRetryDelayMillis() { @@ -1397,6 +1448,32 @@ public LoadErrorAction onLoadError( } + private final class ManifestPatchCallback implements Loader.Callback> { + + @Override + public void onLoadCompleted(ParsingLoadable loadable, + long elapsedRealtimeMs, long loadDurationMs) { + onManifestLoadCompleted(loadable, elapsedRealtimeMs, loadDurationMs); + } + + @Override + public void onLoadCanceled(ParsingLoadable loadable, + long elapsedRealtimeMs, long loadDurationMs, boolean released) { + DashMediaSource.this.onLoadCanceled(loadable, elapsedRealtimeMs, loadDurationMs); + } + + @Override + public LoadErrorAction onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error, + int errorCount) { + return onManifestPatchLoadError(); + } + + } + private final class UtcTimestampCallback implements Loader.Callback> { @Override diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java index 1aee832a37f..46a12c06768 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -37,7 +37,10 @@ import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; + +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.util.List; /** @@ -156,6 +159,16 @@ public static ChunkIndex loadChunkIndex( return chunkExtractor.getChunkIndex(); } + public static String inputStreamToString(InputStream inputStream, String charset) throws IOException { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + return result.toString(charset); + } + /** * Loads initialization data for the {@code representation} and optionally index data then returns * a {@link BundledChunkExtractor} which contains the output. diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 8a773675805..de2911af930 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -88,6 +88,9 @@ public class DashManifest implements FilterableManifest { /** The location of this manifest, or null if not present. */ @Nullable public final Uri location; + /** The patch location of this manifest, or null if not present. */ + @Nullable public final PatchLocation patchLocation; + /** The {@link ProgramInformation}, or null if not present. */ @Nullable public final ProgramInformation programInformation; @@ -96,7 +99,7 @@ public class DashManifest implements FilterableManifest { /** * @deprecated Use {@link #DashManifest(long, long, long, boolean, long, long, long, long, - * ProgramInformation, UtcTimingElement, ServiceDescriptionElement, Uri, List)}. + * ProgramInformation, UtcTimingElement, ServiceDescriptionElement, Uri, PatchLocation ,List)}. */ @Deprecated public DashManifest( @@ -110,6 +113,7 @@ public DashManifest( long publishTimeMs, @Nullable UtcTimingElement utcTiming, @Nullable Uri location, + @Nullable PatchLocation patchLocation, List periods) { this( availabilityStartTimeMs, @@ -124,6 +128,7 @@ public DashManifest( utcTiming, /* serviceDescription= */ null, location, + patchLocation, periods); } @@ -140,6 +145,7 @@ public DashManifest( @Nullable UtcTimingElement utcTiming, @Nullable ServiceDescriptionElement serviceDescription, @Nullable Uri location, + @Nullable PatchLocation patchLocation, List periods) { this.availabilityStartTimeMs = availabilityStartTimeMs; this.durationMs = durationMs; @@ -153,6 +159,7 @@ public DashManifest( this.utcTiming = utcTiming; this.location = location; this.serviceDescription = serviceDescription; + this.patchLocation = patchLocation; this.periods = periods == null ? Collections.emptyList() : periods; this.earlyAccessPeriods = Collections.emptyList(); } @@ -217,6 +224,7 @@ public final DashManifest copy(List streamKeys) { utcTiming, serviceDescription, location, + patchLocation, copyPeriods); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 22f6521605a..508cfd4bff6 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.metadata.emsg.EventMessage; +import com.google.android.exoplayer2.source.dash.DashUtil; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentList; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTemplate; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement; @@ -41,6 +42,7 @@ import com.google.android.exoplayer2.util.XmlPullParserUtil; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -81,6 +83,7 @@ public class DashManifestParser extends DefaultHandler }; private final XmlPullParserFactory xmlParserFactory; + private String manifestString; public DashManifestParser() { try { @@ -95,8 +98,11 @@ public DashManifestParser() { @Override public DashManifest parse(Uri uri, InputStream inputStream) throws IOException { try { + this.manifestString = DashUtil.inputStreamToString(inputStream, "UTF-8"); + InputStream textStream = new ByteArrayInputStream(this.manifestString.getBytes()); + XmlPullParser xpp = xmlParserFactory.newPullParser(); - xpp.setInput(inputStream, null); + xpp.setInput(textStream, null); int eventType = xpp.next(); if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) { throw new ParserException( @@ -127,6 +133,7 @@ protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, Uri location = null; ServiceDescriptionElement serviceDescription = null; long baseUrlAvailabilityTimeOffsetUs = dynamic ? 0 : C.TIME_UNSET; + PatchLocation patchLocation = null; List periods = new ArrayList<>(); List earlyAccessPeriods = new ArrayList<>(); @@ -142,6 +149,8 @@ protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, baseUrl = parseBaseUrl(xpp, baseUrl); seenFirstBaseUrl = true; } + } else if (XmlPullParserUtil.isStartTag(xpp, "PatchLocation")) { + patchLocation = parsePatchLocation(xpp, baseUrl); } else if (XmlPullParserUtil.isStartTag(xpp, "ProgramInformation")) { programInformation = parseProgramInformation(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "UTCTiming")) { @@ -205,6 +214,7 @@ protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, utcTiming, serviceDescription, location, + patchLocation, periods); manifest.earlyAccessPeriods = earlyAccessPeriods; return manifest; @@ -223,6 +233,7 @@ protected DashManifest buildMediaPresentationDescription( @Nullable UtcTimingElement utcTiming, @Nullable ServiceDescriptionElement serviceDescription, @Nullable Uri location, + @Nullable PatchLocation patchLocation, List periods) { return new DashManifest( availabilityStartTime, @@ -237,6 +248,7 @@ protected DashManifest buildMediaPresentationDescription( utcTiming, serviceDescription, location, + patchLocation, periods); } @@ -533,6 +545,10 @@ protected int parseContentType(XmlPullParser xpp) { : C.TRACK_TYPE_UNKNOWN; } + public String getManifestString() { + return manifestString; + } + /** * Parses a ContentProtection element. * @@ -1364,6 +1380,22 @@ protected long parseAvailabilityTimeOffsetUs( return (long) (Float.parseFloat(value) * C.MICROS_PER_SECOND); } + /** + * Parses a PatchLocation element. + * + * @param xpp The parser from which to read. + * @param parentBaseUrl A base URL for resolving the parsed URL. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The parsed and resolved URL. + */ + protected PatchLocation parsePatchLocation(XmlPullParser xpp, String parentBaseUrl) + throws XmlPullParserException, IOException { + long ttl = parseLong(xpp, "ttl", 0); + String url = UriUtil.resolve(parentBaseUrl, parseText(xpp, "PatchLocation")); + return new PatchLocation(ttl, url); + } + // AudioChannelConfiguration parsing. protected int parseAudioChannelConfiguration(XmlPullParser xpp) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatch.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatch.java new file mode 100644 index 00000000000..80121251448 --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatch.java @@ -0,0 +1,179 @@ +package com.google.android.exoplayer2.source.dash.manifest; + +import android.util.Log; + +import androidx.annotation.Nullable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import java.util.List; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +// Reference: https://tools.ietf.org/html/rfc5261 +public class DashManifestPatch { + private static final String TAG = "DashManifestPatch"; + public final String mpdId; + public final long originalPublishTimeMs; + public final long publishTimeMs; + public final List operations; + + public DashManifestPatch( + String mpdId, + long originalPublishTimeMs, + long publishTimeMs, + List operations) { + this.mpdId = mpdId; + this.originalPublishTimeMs = originalPublishTimeMs; + this.publishTimeMs = publishTimeMs; + this.operations = operations; + } + + public boolean applyPatch(Document document) { + XPath xPath = XPathFactory.newInstance().newXPath(); + boolean result = true; + @Nullable Operation lastOperation = null; + try { + for (Operation operation : operations) { + lastOperation = operation; + if (!operation.execute(document, xPath)) { + Log.w(TAG, "Failed to execute operation, xpath: " + operation.getXPath()); + result = false; + break; + } + } + } catch (XPathExpressionException ex) { + String xpath = lastOperation != null ? lastOperation.getXPath() : null; + Log.w(TAG, "Failed to apply patch, xpath: " + xpath, ex); + result = false; + } + return result; + } + + public interface Operation { + String getXPath(); + boolean execute(Document document, XPath xPath) throws XPathExpressionException; + } + + public static class AddOperation implements Operation { + public final String path; + @Nullable public final String content; + @Nullable public final String pos; + @Nullable public final String type; + @Nullable public final Element element; + + public AddOperation(String path, + @Nullable String content, + @Nullable String pos, + @Nullable String type, + @Nullable Element element) { + this.path = path; + this.content = content; + this.pos = pos; + this.type = type; + this.element = element; + } + + @Override + public String getXPath() { + return path; + } + + @Override + public boolean execute(Document document, XPath xPath) throws XPathExpressionException { + Element node = (Element)xPath.compile(path).evaluate(document, XPathConstants.NODE); + if (node == null) { + return false; + } + if (type != null) { + // Add Attribute + String name = type.substring(1); // Remove '@' at the start + node.setAttribute(name, content); + } else { + // Add Node + while (element.hasChildNodes()) { + Node childNode = document.adoptNode(element.getFirstChild()); + node.appendChild(childNode); + } + } + return true; + } + } + + public static class ReplaceOperation implements Operation { + public final boolean isAttribute; + public final String path; + @Nullable public final String content; + @Nullable public final Element element; + + public ReplaceOperation(boolean isAttribute, String path, + @Nullable String content, + @Nullable Element element) { + this.isAttribute = isAttribute; + this.path = path; + this.content = content; + this.element = element; + } + + @Override + public String getXPath() { + return path; + } + + @Override + public boolean execute(Document document, XPath xPath) throws XPathExpressionException { + if (isAttribute) { + int index = path.lastIndexOf('/'); + String modifiedPath = path.substring(0, index); + String attrName = path.substring(index + 2); + Element node = (Element) xPath.compile(modifiedPath).evaluate(document, XPathConstants.NODE); + node.setAttribute(attrName, content); + } else { + Element node = (Element)xPath.compile(path).evaluate(document, XPathConstants.NODE); + Node parent = node.getParentNode(); + if (element.getFirstChild() != null) { + Node childNode = document.adoptNode(element.getFirstChild()); + parent.replaceChild(childNode, node); + } + } + return true; + } + } + + public static class RemoveOperation implements Operation { + public final boolean isAttribute; + public final String path; + @Nullable public final String ws; + public RemoveOperation(boolean isAttribute, + String path, + @Nullable String ws) { + this.isAttribute = isAttribute; + this.path = path; + this.ws = ws; + } + + @Override + public String getXPath() { + return path; + } + + @Override + public boolean execute(Document document, XPath xPath) throws XPathExpressionException { + if (isAttribute) { + int index = path.lastIndexOf('/'); + String modifiedPath = path.substring(0, index); + String attrName = path.substring(index + 2); + Element node = (Element) xPath.compile(modifiedPath).evaluate(document, XPathConstants.NODE); + node.removeAttribute(attrName); + } else { + Element node = (Element)xPath.compile(path).evaluate(document, XPathConstants.NODE); + Node parent = node.getParentNode(); + parent.removeChild(node); + } + return true; + } + } +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchMerger.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchMerger.java new file mode 100644 index 00000000000..10d7580d3fe --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchMerger.java @@ -0,0 +1,75 @@ +package com.google.android.exoplayer2.source.dash.manifest; + +import android.net.Uri; + +import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.source.dash.DashUtil; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.Log; + +import org.w3c.dom.Document; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +public class DashManifestPatchMerger extends DefaultHandler + implements ParsingLoadable.Parser { + + private static final String TAG = "MpdPatchMerger"; + private final DashManifestPatchParser parser; + private final DocumentBuilder documentBuilder; + @Nullable private Document document; + @Nullable private String manifestString; + + public DashManifestPatchMerger() { + this(new DashManifestPatchParser()); + } + + public DashManifestPatchMerger(DashManifestPatchParser parser) { + this.parser = parser; + try { + DocumentBuilderFactory builder = DocumentBuilderFactory.newInstance(); + documentBuilder = builder.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new RuntimeException("Couldn't create Document instance", e); + } + } + + public Document getDocument() { + return document; + } + + public void setManifestString(String manifestString) throws IOException, SAXException { + if (this.manifestString != null && this.manifestString.equals(manifestString)) { + return; + } + this.manifestString = manifestString; + InputStream stream = new ByteArrayInputStream(manifestString.getBytes()); + document = documentBuilder.parse(stream); + } + + @Override + public DashManifest parse(Uri uri, InputStream inputStream) throws IOException { + String manifestPatchString = DashUtil.inputStreamToString(inputStream, "UTF-8"); + + DashManifestPatch patch = parser.parse(manifestPatchString); + + if (!patch.applyPatch(document)) { + Log.d(TAG, "Failed to apply manifest patch: " + manifestPatchString); + throw new ParserException("Failed to apply manifest patch"); + } + + Log.d(TAG, "Patch success, operation count: " + patch.operations.size()); + return DocumentToManifestConverter.convert(document, uri.toString()); + } + +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParser.java new file mode 100644 index 00000000000..7fdbfea5c35 --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParser.java @@ -0,0 +1,197 @@ +package com.google.android.exoplayer2.source.dash.manifest; + +import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.XmlPullParserUtil; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + + +public class DashManifestPatchParser { + private static final String TAG = "MpdPatchParser"; + private static final String UTF_8 = "UTF-8"; + private final XmlPullParserFactory xmlParserFactory; + private DocumentBuilder documentBuilder; + @Nullable private Document document; + @Nullable private Element root; + + public DashManifestPatchParser() { + try { + xmlParserFactory = XmlPullParserFactory.newInstance(); + DocumentBuilderFactory builder = DocumentBuilderFactory.newInstance(); + documentBuilder = builder.newDocumentBuilder(); + } catch (XmlPullParserException e) { + throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e); + } catch (ParserConfigurationException e) { + throw new RuntimeException("Couldn't create Document instance", e); + } + } + + public static String docToString(Document doc) { + try { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + StringWriter writer = new StringWriter(); + transformer.transform(new DOMSource(doc), new StreamResult(writer)); + return writer.toString(); + } catch (TransformerException ex) { + return ""; + } + } + + public DashManifestPatch parse(String manifestPatchString) throws IOException { + try { + InputStream textStream = new ByteArrayInputStream(manifestPatchString.getBytes()); + + XmlPullParser xpp = xmlParserFactory.newPullParser(); + xpp.setInput(textStream, null); + int eventType = xpp.next(); + if (eventType != XmlPullParser.START_TAG || !"Patch".equals(xpp.getName())) { + throw new ParserException( + "inputStream does not contain a valid manifest patch"); + } + return parseDashManifestPatch(xpp); + } catch (XmlPullParserException e) { + throw new ParserException(e); + } + } + + protected DashManifestPatch parseDashManifestPatch(XmlPullParser xpp) throws XmlPullParserException, IOException { + String mpdId = DashManifestParser.parseString(xpp, "mpdId", ""); + long originalPublishTime = DashManifestParser.parseDateTime(xpp, "originalPublishTime", C.TIME_UNSET); + long publishTime = DashManifestParser.parseDateTime(xpp, "publishTime", C.TIME_UNSET); + + document = documentBuilder.newDocument(); + root = document.createElement("Patch"); + document.appendChild(root); + + List operationList = new ArrayList<>(); + do { + xpp.next(); + + @Nullable + DashManifestPatch.Operation operation = parseOperation(xpp); + if (operation != null) { + operationList.add(operation); + } else { + DashManifestParser.maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "Patch")); + + return new DashManifestPatch(mpdId, originalPublishTime, publishTime, operationList); + } + + @Nullable + protected DashManifestPatch.Operation parseOperation(XmlPullParser xpp) throws XmlPullParserException { + if (XmlPullParserUtil.isStartTag(xpp, "add")) { + return parseAddOperation(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "replace")) { + return parseReplaceOperation(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "remove")) { + return parseRemoveOperation(xpp); + } + return null; + } + + private DashManifestPatch.Operation parseAddOperation(XmlPullParser xpp) { + String xpath = DashManifestParser.parseString(xpp, "sel", ""); + String pos = DashManifestParser.parseString(xpp, "pos", ""); + String type = DashManifestParser.parseString(xpp, "type", ""); + @Nullable Element element = null; + if (type.length() < 1) { + try { + element = createElements(xpp, xpp.getName(), document); + root.appendChild(element); + } catch (Exception ex) { + Log.w(TAG, "Failed to parse nodes of add operation", ex); + } + } + @Nullable String content = null; + if (element == null) { + try { + content = DashManifestParser.parseText(xpp, "add"); + } catch (Exception ex) { + Log.w(TAG, "Failed to parse content of add operation", ex); + } + } + return new DashManifestPatch.AddOperation(xpath, content, + pos.length() > 0 ? pos : null, + type.length() > 0 ? type : null, + element); + } + + public boolean isAttributeOperation(String path) { + int index = path.lastIndexOf('/'); + return path.indexOf('@', index + 1) > 0; + } + + private DashManifestPatch.Operation parseReplaceOperation(XmlPullParser xpp) { + String xpath = DashManifestParser.parseString(xpp, "sel", ""); + boolean isAttributeOp = isAttributeOperation(xpath); + @Nullable String content = null; + @Nullable Element element = null; + try { + if (!isAttributeOp) { + element = createElements(xpp, xpp.getName(), document); + root.appendChild(element); + } else { + content = DashManifestParser.parseText(xpp, "replace"); + } + } catch (Exception ex) { + Log.w(TAG, "Failed to parse nodes of replace operation", ex); + } + return new DashManifestPatch.ReplaceOperation(isAttributeOp, xpath, content, element); + } + + private DashManifestPatch.Operation parseRemoveOperation(XmlPullParser xpp) { + String xpath = DashManifestParser.parseString(xpp, "sel", ""); + String ws = DashManifestParser.parseString(xpp, "ws", ""); + return new DashManifestPatch.RemoveOperation(isAttributeOperation(xpath), xpath, ws.length() > 0 ? ws : null); + } + + private static Element createElements(XmlPullParser xpp, String startTag, Document document) + throws IOException, XmlPullParserException { + Element element = document.createElement(startTag); + for (int i = 0; i < xpp.getAttributeCount(); i++) { + element.setAttribute(xpp.getAttributeName(i), xpp.getAttributeValue(i)); + } + do { + xpp.next(); + if (xpp.getText() != null) { + String text = xpp.getText(); + text = text.trim(); + if (text.length() > 0) { + element.setTextContent(text); + } + xpp.next(); + } + if (xpp.getName() != null && !xpp.getName().equals(startTag)) { + Element nextElement = createElements(xpp, xpp.getName(), document); + element.appendChild(nextElement); + } + } while (!XmlPullParserUtil.isEndTag(xpp, startTag)); + return element; + } +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DocumentToManifestConverter.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DocumentToManifestConverter.java new file mode 100644 index 00000000000..197d9fb5488 --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DocumentToManifestConverter.java @@ -0,0 +1,40 @@ +package com.google.android.exoplayer2.source.dash.manifest; + +import android.net.Uri; +import com.google.android.exoplayer2.ParserException; +import java.io.ByteArrayInputStream; +import java.io.StringWriter; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import org.w3c.dom.Document; + +public class DocumentToManifestConverter { + + private static final DashManifestParser parser = new DashManifestParser(); + + public static DashManifest convert(Document document, String baseUrl) throws ParserException { + try { + String mpd = getStringFromDoc(document); + return parser.parse(Uri.parse(baseUrl), new ByteArrayInputStream(mpd.getBytes())); + } catch (Exception ex) { + throw new ParserException(); + } + } + + public static String getStringFromDoc(Document doc) throws TransformerException { + DOMSource domSource = new DOMSource(doc); + StringWriter writer = new StringWriter(); + StreamResult result = new StreamResult(writer); + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.transform(domSource, result); + writer.flush(); + return writer.toString(); + } +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/PatchLocation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/PatchLocation.java new file mode 100644 index 00000000000..cd90af9012b --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/PatchLocation.java @@ -0,0 +1,34 @@ +package com.google.android.exoplayer2.source.dash.manifest; + +import androidx.annotation.Nullable; + +/* A parsed PatchLocation element. */ +public class PatchLocation { + public final long ttl; + public final String url; + + public PatchLocation(long ttl, String url) { + this.ttl = ttl; + this.url = url; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PatchLocation other = (PatchLocation) obj; + return this.ttl == other.ttl && this.url.equals(other.url); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int)ttl; + result = 31 * result + url.hashCode(); + return result; + } +} diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParserTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParserTest.java new file mode 100644 index 00000000000..f0b409c311f --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParserTest.java @@ -0,0 +1,108 @@ +package com.google.android.exoplayer2.source.dash.manifest; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.Util; +import org.junit.Test; +import org.junit.runner.RunWith; +import java.io.IOException; +import static com.google.common.truth.Truth.assertThat; + +/** Unit tests for {@link DashManifestPatchParser}. */ +@RunWith(AndroidJUnit4.class) +public class DashManifestPatchParserTest { + private static final String SAMPLE_MPD_PATCH = "manifest_patch/mpd_patch"; + private static final String SAMPLE_MPD_PATCH_ADD_SEGMENTS = "manifest_patch/mpd_patch_add_segments"; + private static final String SAMPLE_MPD_PATCH_ADD_PERIOD = "manifest_patch/mpd_patch_add_period"; + private static final String SAMPLE_MPD_PATCH_ADD_ATTRIBUTE = "manifest_patch/mpd_patch_add_attribute"; + private static final String SAMPLE_MPD_PATCH_REPLACE_ATTRIBUTE = "manifest_patch/mpd_patch_replace_attribute"; + private static final String SAMPLE_MPD_PATCH_REPLACE_NODE = "manifest_patch/mpd_patch_replace_node"; + private static final String XML_ADD_SEGMENTS = "manifest_patch/xml_add_segments"; + private static final String XML_ADD_PERIOD = "manifest_patch/xml_add_period"; + private static final String XML_REPLACE_NODE = "manifest_patch/xml_replace_node"; + + @Test + public void testParseSamplePatch() throws IOException { + DashManifestPatchParser parser = new DashManifestPatchParser(); + DashManifestPatch patch = parser.parse( + TestUtil.getString(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_PATCH)); + + assertThat(patch.mpdId).isEqualTo("mpd-id"); + assertThat(patch.originalPublishTimeMs).isEqualTo(Util.parseXsDateTime("2020-11-09T03:48:41.51468868Z")); + assertThat(patch.publishTimeMs).isEqualTo(Util.parseXsDateTime("2020-11-09T03:48:43.514902582Z")); + assertThat(patch.operations.size()).isEqualTo(6); + assertThat(patch.operations.get(4).getXPath()).isEqualTo( + "/MPD/Period[@id='81']/AdaptationSet/SegmentTemplate/SegmentTimeline/S/@r"); + } + + @Test + public void testParseAddOperationWithSegments() throws IOException { + DashManifestPatchParser parser = new DashManifestPatchParser(); + DashManifestPatch patch = parser.parse( + TestUtil.getString(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_PATCH_ADD_SEGMENTS)); + + assertThat(patch.operations.size()).isEqualTo(1); + DashManifestPatch.AddOperation operation = (DashManifestPatch.AddOperation)patch.operations.get(0); + assertThat(operation.type).isNull(); + + assertThat(DashManifestPatchParser.docToString(operation.element.getOwnerDocument())) + .isEqualTo(TestUtil.getString(ApplicationProvider.getApplicationContext(), XML_ADD_SEGMENTS)); + } + + @Test + public void testParseAddOperationWithPeriod() throws IOException { + DashManifestPatchParser parser = new DashManifestPatchParser(); + DashManifestPatch patch = parser.parse( + TestUtil.getString(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_PATCH_ADD_PERIOD)); + + assertThat(patch.operations.size()).isEqualTo(1); + DashManifestPatch.AddOperation operation = (DashManifestPatch.AddOperation)patch.operations.get(0); + assertThat(operation.type).isNull(); + + assertThat(DashManifestPatchParser.docToString(operation.element.getOwnerDocument())) + .isEqualTo(TestUtil.getString(ApplicationProvider.getApplicationContext(), XML_ADD_PERIOD)); + } + + @Test + public void testParseAddAttribute() throws IOException { + DashManifestPatchParser parser = new DashManifestPatchParser(); + DashManifestPatch patch = parser.parse( + TestUtil.getString(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_PATCH_ADD_ATTRIBUTE)); + + assertThat(patch.operations.size()).isEqualTo(1); + DashManifestPatch.AddOperation operation = (DashManifestPatch.AddOperation)patch.operations.get(0); + assertThat(operation.type).isEqualTo("@r"); + assertThat(operation.content).isEqualTo("2"); + assertThat(operation.path).isEqualTo( + "/MPD/Period[@id='alt-17-101']/AdaptationSet[2]/SegmentTemplate/SegmentTimeline/S"); + } + + @Test + public void testParseReplaceAttribute() throws IOException { + DashManifestPatchParser parser = new DashManifestPatchParser(); + DashManifestPatch patch = parser.parse( + TestUtil.getString(ApplicationProvider.getApplicationContext(), + SAMPLE_MPD_PATCH_REPLACE_ATTRIBUTE)); + + assertThat(patch.operations.size()).isEqualTo(1); + DashManifestPatch.ReplaceOperation operation = (DashManifestPatch.ReplaceOperation)patch.operations.get(0); + if (operation.element == null) assertThat(operation.content).isNotNull(); + if (operation.content == null) assertThat(operation.element).isNotNull(); + } + + @Test + public void testParseReplaceNode() throws IOException { + DashManifestPatchParser parser = new DashManifestPatchParser(); + DashManifestPatch patch = parser.parse( + TestUtil.getString(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_PATCH_REPLACE_NODE)); + + assertThat(patch.operations.size()).isEqualTo(1); + DashManifestPatch.ReplaceOperation operation = (DashManifestPatch.ReplaceOperation)patch.operations.get(0); + if (operation.element == null) assertThat(operation.content).isNotNull(); + if (operation.content == null) assertThat(operation.element).isNotNull(); + + assertThat(DashManifestPatchParser.docToString(operation.element.getOwnerDocument())) + .isEqualTo(TestUtil.getString(ApplicationProvider.getApplicationContext(), XML_REPLACE_NODE)); + } +} diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchTest.java new file mode 100644 index 00000000000..aa68e17b49b --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchTest.java @@ -0,0 +1,241 @@ +package com.google.android.exoplayer2.source.dash.manifest; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.android.exoplayer2.testutil.TestUtil; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import static com.google.common.truth.Truth.assertThat; + +/** Unit tests for {@link DashManifestPatch}. */ +@RunWith(AndroidJUnit4.class) +public class DashManifestPatchTest { + private static final String SAMPLE_MPD_WITH_PATCH_LOCATION = "media/mpd/manifest_patch/mpd_with_patch_location"; + + @Test + public void testAddAttribute() throws IOException, ParserConfigurationException, SAXException, XPathExpressionException { + Document document = createDocument(SAMPLE_MPD_WITH_PATCH_LOCATION); + XPath xPath = XPathFactory.newInstance().newXPath(); + + String path = "/MPD/PatchLocation"; + String attributeName = "id"; + String attributeValue = "123"; + DashManifestPatch.AddOperation operation = new DashManifestPatch.AddOperation( + path, attributeValue, null, "@" + attributeName, null); + operation.execute(document, xPath); + + Node target = null; + NodeList children = document.getDocumentElement().getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + if (children.item(i).getNodeName().equals("PatchLocation")) { + target = children.item(i); + break; + } + } + + assertThat(target.getAttributes().getNamedItem(attributeName).getNodeValue()).isEqualTo(attributeValue); + } + + @Test + public void testAddNodes() throws IOException, ParserConfigurationException, SAXException, XPathExpressionException { + Document document = createDocument(SAMPLE_MPD_WITH_PATCH_LOCATION); + XPath xPath = XPathFactory.newInstance().newXPath(); + + Element rootElement = document.createElement("root"); + Element element = document.createElement("Period"); + element.setAttribute("id", "98"); + rootElement.appendChild(element); + + Element element2 = document.createElement("Period"); + element2.setAttribute("id", "99"); + rootElement.appendChild(element2); + + String path = "/MPD"; + DashManifestPatch.AddOperation operation = new DashManifestPatch.AddOperation( + path, null, null, null, rootElement); + boolean success = operation.execute(document, xPath); + assertThat(success).isTrue(); + + List nodes = new ArrayList<>(); + NodeList children = document.getDocumentElement().getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + if (children.item(i).getNodeName().equals("Period")) { + nodes.add(children.item(i)); + } + } + + assertThat(nodes.get(nodes.size() - 2).getAttributes().getNamedItem("id").getNodeValue()).isEqualTo("98"); + assertThat(nodes.get(nodes.size() - 1).getAttributes().getNamedItem("id").getNodeValue()).isEqualTo("99"); + } + + @Test + public void testReplaceAttribute() throws IOException, ParserConfigurationException, + SAXException, XPathExpressionException { + Document document = createDocument(SAMPLE_MPD_WITH_PATCH_LOCATION); + XPath xPath = XPathFactory.newInstance().newXPath(); + + String path = "/MPD/Period[@id='81']/@start"; + DashManifestPatch.ReplaceOperation operation = new DashManifestPatch.ReplaceOperation( + true, path, "PT1234M5.000000S", null); + + boolean success = operation.execute(document, xPath); + assertThat(success).isTrue(); + + Element targetNode = (Element)xPath.compile("/MPD/Period[@id='81']") + .evaluate(document, XPathConstants.NODE); + assertThat(targetNode.getAttributes().getNamedItem("start").getNodeValue()) + .isEqualTo("PT1234M5.000000S"); + } + + @Test + public void testReplaceSingleNode() throws IOException, ParserConfigurationException, + SAXException, XPathExpressionException { + Document document = createDocument(SAMPLE_MPD_WITH_PATCH_LOCATION); + XPath xPath = XPathFactory.newInstance().newXPath(); + + Element root = document.createElement("root"); + + Element location = document.createElement("PatchLocation"); + root.appendChild(location); + location.setAttribute("ttl", "66"); + location.setTextContent("manifest-patch.mpd?publishTime=2020-11-09T03:48:43.514902582Z"); + + String path = "/MPD/PatchLocation"; + DashManifestPatch.ReplaceOperation operation = new DashManifestPatch.ReplaceOperation( + false, path, null, root); + boolean success = operation.execute(document, xPath); + assertThat(success).isTrue(); + + Element targetNode = (Element)xPath.compile(path).evaluate(document, XPathConstants.NODE); + assertThat(targetNode.getAttributes().getNamedItem("ttl").getNodeValue()) + .isEqualTo("66"); + assertThat(targetNode.getTextContent()) + .isEqualTo("manifest-patch.mpd?publishTime=2020-11-09T03:48:43.514902582Z"); + } + + @Test + public void testReplaceNodeWithMultipleChildren() throws IOException, ParserConfigurationException, + SAXException, XPathExpressionException { + Document document = createDocument(SAMPLE_MPD_WITH_PATCH_LOCATION); + XPath xPath = XPathFactory.newInstance().newXPath(); + + Element root = document.createElement("root"); + Element timeline = document.createElement("SegmentTimeline"); + root.appendChild(timeline); + + Element element = document.createElement("S"); + element.setAttribute("t", "123"); + element.setAttribute("d", "20000000"); + element.setAttribute("r", "6"); + timeline.appendChild(element); + + Element element2 = document.createElement("S"); + element2.setAttribute("t", "120000123"); + element2.setAttribute("d", "30000000"); + element2.setAttribute("r", "3"); + timeline.appendChild(element2); + + String path = "/MPD/Period[@id='81']/AdaptationSet/SegmentTemplate/SegmentTimeline"; + DashManifestPatch.ReplaceOperation operation = new DashManifestPatch.ReplaceOperation( + false, path, null, root); + boolean success = operation.execute(document, xPath); + assertThat(success).isTrue(); + + Element targetNode = (Element)xPath.compile(path) + .evaluate(document, XPathConstants.NODE); + int elementCount = 0; + for (int i = 0; i < targetNode.getChildNodes().getLength(); i++) { + if (targetNode.getChildNodes().item(i) instanceof Element) { + Element child = (Element)targetNode.getChildNodes().item(i); + elementCount++; + } + } + assertThat(elementCount).isEqualTo(2); + + Node segment1 = targetNode.getChildNodes().item(0); + Node segment2 = targetNode.getChildNodes().item(1); + + assertThat(segment1.getAttributes().getNamedItem("t").getNodeValue()) + .isEqualTo("123"); + assertThat(segment1.getAttributes().getNamedItem("d").getNodeValue()) + .isEqualTo("20000000"); + assertThat(segment1.getAttributes().getNamedItem("r").getNodeValue()) + .isEqualTo("6"); + + assertThat(segment2.getAttributes().getNamedItem("t").getNodeValue()) + .isEqualTo("120000123"); + assertThat(segment2.getAttributes().getNamedItem("d").getNodeValue()) + .isEqualTo("30000000"); + assertThat(segment2.getAttributes().getNamedItem("r").getNodeValue()) + .isEqualTo("3"); + } + + @Test + public void testRemoveAttribute() throws IOException, ParserConfigurationException, + SAXException, XPathExpressionException { + Document document = createDocument(SAMPLE_MPD_WITH_PATCH_LOCATION); + XPath xPath = XPathFactory.newInstance().newXPath(); + + String path = "/MPD/Period[@id='81']/@start"; + DashManifestPatch.RemoveOperation operation = new DashManifestPatch.RemoveOperation( + true, path, null); + + boolean success = operation.execute(document, xPath); + assertThat(success).isTrue(); + + Element targetNode = (Element)xPath.compile("/MPD/Period[@id='81']") + .evaluate(document, XPathConstants.NODE); + assertThat(targetNode.getAttributes().getNamedItem("start")).isNull(); + } + + @Test + public void testRemoveNode() throws IOException, ParserConfigurationException, + SAXException, XPathExpressionException { + Document document = createDocument(SAMPLE_MPD_WITH_PATCH_LOCATION); + XPath xPath = XPathFactory.newInstance().newXPath(); + + String path = "/MPD/Period[@id='81']/AdaptationSet/Representation[4]"; + DashManifestPatch.RemoveOperation operation = new DashManifestPatch.RemoveOperation( + false, path, null); + + boolean success = operation.execute(document, xPath); + assertThat(success).isTrue(); + + Element targetNode = (Element)xPath.compile(path) + .evaluate(document, XPathConstants.NODE); + assertThat(targetNode.getAttributes().getNamedItem("id").getNodeValue()) + .isEqualTo("stream_4"); + } + + Document createDocument(String path) throws IOException, ParserConfigurationException, + SAXException { + InputStream stream = TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), path); + DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = builderFactory.newDocumentBuilder(); + return builder.parse(stream); + } +} diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java index 7b3b3cab512..5f98f19be8f 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -247,6 +247,7 @@ private static DashManifest newDashManifest( UTC_TIMING, serviceDescription, Uri.EMPTY, + null, Arrays.asList(periods)); } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DocumentToManifestConverterTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DocumentToManifestConverterTest.java new file mode 100644 index 00000000000..f8520155568 --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DocumentToManifestConverterTest.java @@ -0,0 +1,54 @@ +package com.google.android.exoplayer2.source.dash.manifest; + +import android.net.Uri; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.android.exoplayer2.testutil.TestUtil; + +import java.util.Objects; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xml.sax.SAXException; + +import java.io.IOException; + +import static com.google.common.truth.Truth.assertThat; + +/** Unit tests for {@link DocumentToManifestConverter}. */ +@RunWith(AndroidJUnit4.class) +public class DocumentToManifestConverterTest { + private static final String MPD_WITH_PATCH = "media/mpd/manifest_patch/mpd_with_patch_location"; + private static final String MPD_WITH_MULTI_PERIOD_SCTE35 = "media/mpd/manifest_patch/mpd_multi_period_scte35"; + + @Test + public void testManifestWithPatchLocation() throws IOException, SAXException { + String baseURL = "https://example.com/test.mpd"; + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = parser.parse( + Uri.parse(baseURL), + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), MPD_WITH_PATCH)); + + DashManifestPatchMerger patchMerger = new DashManifestPatchMerger(); + patchMerger.setManifestString(TestUtil.getString(ApplicationProvider.getApplicationContext(), MPD_WITH_PATCH)); + DashManifest convertedManifest = DocumentToManifestConverter.convert(patchMerger.getDocument(), baseURL); + + assertThat(Objects.deepEquals(manifest,convertedManifest)); + } + + @Test + public void testManifestWithMultiPeriodScte35() throws IOException, SAXException { + String baseURL = "https://example.com/test.mpd"; + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = parser.parse( + Uri.parse(baseURL), + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), MPD_WITH_MULTI_PERIOD_SCTE35)); + + DashManifestPatchMerger patchMerger = new DashManifestPatchMerger(); + patchMerger.setManifestString(TestUtil.getString(ApplicationProvider.getApplicationContext(), MPD_WITH_MULTI_PERIOD_SCTE35)); + DashManifest convertedManifest = DocumentToManifestConverter.convert(patchMerger.getDocument(), baseURL); + + assertThat(Objects.deepEquals(manifest,convertedManifest)); + } +} diff --git a/testdata/src/test/assets/media/mpd/manifest_patch/mpd_multi_period_scte35 b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_multi_period_scte35 new file mode 100644 index 00000000000..2965b5a18e0 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_multi_period_scte35 @@ -0,0 +1,63 @@ + + + + + + + /AAiAAAAAAAAAAAABQaAV0NICAAMQkJCX0FEX1NUQVJU5kNVSQ== + + + + + + + + + + + + + + + + + + + /AAgAAAAAAAAAAAABQaAV1EDqAAKQkJCX0FEX0VORF9n1ks= + + + + + + + + + + + + + + + + + + + + /AAiAAAAAAAAAAAABQaAV2x66AAMQkJCX0FEX1NUQVJU2FPjtA== + + + + + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch new file mode 100644 index 00000000000..5a585b5ee1b --- /dev/null +++ b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch @@ -0,0 +1,17 @@ + + + PT0M25.112493S + 2020-11-09T03:48:43.514902582Z + + manifest-patch.mpd?publishTime=2020-11-09T03:48:43.514902582Z + + + + [{"overlayUrl":"https://example.com/graphics/index.html","playTime":24000,"layer":0}] + + + 6 + + + + diff --git a/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_add_attribute b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_add_attribute new file mode 100644 index 00000000000..d5d40eb042c --- /dev/null +++ b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_add_attribute @@ -0,0 +1,4 @@ + + + 2 + diff --git a/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_add_period b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_add_period new file mode 100644 index 00000000000..318fe3574d6 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_add_period @@ -0,0 +1,38 @@ + + + + + /asset=dash_with_patching,version=v1,replaystarttime=1605061242,user=sample-user/ + + + [{"overlayUrl":"https://example.com/graphics/index.html","playTime":59,"layer":0}] + + + [{"overlayUrl":"https://example.com/graphics/index.html","playTime":4059,"layer":0}] + + + [{"overlayUrl":"https://example.com/graphics/index.html","playTime":8059,"layer":0}] + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_add_segments b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_add_segments new file mode 100644 index 00000000000..b8d379de22f --- /dev/null +++ b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_add_segments @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_replace_attribute b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_replace_attribute new file mode 100644 index 00000000000..d27d33d7d4e --- /dev/null +++ b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_replace_attribute @@ -0,0 +1,4 @@ + + + 2020-11-09T03:48:43.514902582Z + diff --git a/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_replace_node b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_replace_node new file mode 100644 index 00000000000..f3f6532771d --- /dev/null +++ b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_patch_replace_node @@ -0,0 +1,6 @@ + + + + manifest-patch.mpd?publishTime=2020-11-09T03:48:43.514902582Z + + diff --git a/testdata/src/test/assets/media/mpd/manifest_patch/mpd_with_patch_location b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_with_patch_location new file mode 100644 index 00000000000..11932ea5da8 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/manifest_patch/mpd_with_patch_location @@ -0,0 +1,50 @@ + + + manifest-patch.mpd?publishTime=2020-11-09T03:48:39.514833095Z + + /asset=dash_with_patching,version=v1,replaystarttime=1604889069,user=sample_user/ + + + [{"overlayUrl":"https://example.com/graphics/index.html","playTime":0,"layer":0}] + + + [{"overlayUrl":"https://example.com/graphics/index.html","playTime":4000,"layer":0}] + + + [{"overlayUrl":"https://example.com/graphics/index.html","playTime":8000,"layer":0}] + + + [{"overlayUrl":"https://example.com/graphics/index.html","playTime":12000,"layer":0}] + + + [{"overlayUrl":"https://example.com/graphics/index.html","playTime":16000,"layer":0}] + + + [{"overlayUrl":"https://example.com/graphics/index.html","playTime":20000,"layer":0}] + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/media/mpd/manifest_patch/xml_add_period b/testdata/src/test/assets/media/mpd/manifest_patch/xml_add_period new file mode 100644 index 00000000000..59e8a6d1997 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/manifest_patch/xml_add_period @@ -0,0 +1 @@ +/asset=dash_with_patching,version=v1,replaystarttime=1605061242,user=sample-user/[{"overlayUrl":"https://example.com/graphics/index.html","playTime":59,"layer":0}][{"overlayUrl":"https://example.com/graphics/index.html","playTime":4059,"layer":0}][{"overlayUrl":"https://example.com/graphics/index.html","playTime":8059,"layer":0}] \ No newline at end of file diff --git a/testdata/src/test/assets/media/mpd/manifest_patch/xml_add_segments b/testdata/src/test/assets/media/mpd/manifest_patch/xml_add_segments new file mode 100644 index 00000000000..f34844ae1b4 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/manifest_patch/xml_add_segments @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/testdata/src/test/assets/media/mpd/manifest_patch/xml_replace_node b/testdata/src/test/assets/media/mpd/manifest_patch/xml_replace_node new file mode 100644 index 00000000000..5da723420bf --- /dev/null +++ b/testdata/src/test/assets/media/mpd/manifest_patch/xml_replace_node @@ -0,0 +1 @@ +manifest-patch.mpd?publishTime=2020-11-09T03:48:43.514902582Z \ No newline at end of file From 50e17456688d7b67bf70b2b6157ff1163d053098 Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Tue, 30 Mar 2021 15:18:12 +0800 Subject: [PATCH 03/42] Workaround playback error on dash stream with eap --- .../android/exoplayer2/source/dash/manifest/SegmentBase.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index 495f288805c..ec00e72b657 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -182,6 +182,9 @@ public long getSegmentNum(long timeUs, long periodDurationUs) { if (segmentTimeline == null) { // All segments are of equal duration (with the possible exception of the last one). long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; + if (durationUs == 0){ + return firstSegmentNum; + } long segmentNum = startNumber + timeUs / durationUs; // Ensure we stay within bounds. return segmentNum < firstSegmentNum From 80ce6e0d3fc11a7e5b8a1e4370cd51960d7740df Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Wed, 7 Apr 2021 16:42:19 +0800 Subject: [PATCH 04/42] expose actual playback speed --- .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 4 ++++ .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 4 ++++ .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 4 ++++ 3 files changed, 12 insertions(+) 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 de8aa48891c..3ae51eee509 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 @@ -666,6 +666,10 @@ public PlaybackParameters getPlaybackParameters() { return playbackInfo.playbackParameters; } + public PlaybackParameters getMediaClockPlaybackParameters() { + return internalPlayer.getMediaClockPlaybackParameters(); + } + @Override public void setSeekParameters(@Nullable SeekParameters seekParameters) { if (seekParameters == null) { 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 f791d05b132..1f57b5af789 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 @@ -896,6 +896,10 @@ && shouldUseLivePlaybackSpeedControl(playbackInfo.timeline, playbackInfo.periodI } } + public PlaybackParameters getMediaClockPlaybackParameters(){ + return mediaClock.getPlaybackParameters(); + } + private void notifyTrackSelectionRebuffer() { MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); while (periodHolder != null) { 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 a98e3475af3..815824197dd 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 @@ -2294,4 +2294,8 @@ public void onExperimentalSleepingForOffloadChanged(boolean sleepingForOffload) updateWakeAndWifiLock(); } } + + public PlaybackParameters getMediaClockPlaybackParameters(){ + return player.getMediaClockPlaybackParameters(); + } } From 28619a9fefeb1f5a9eaff0da9f9d4459e5c7b279 Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Mon, 12 Apr 2021 12:21:09 +0800 Subject: [PATCH 05/42] Basic low latency ABR logic --- .../LowLatencyABRController.java | 201 ++++++++++++++++++ .../LowLatencyTrackSelector.java | 53 +++++ 2 files changed, 254 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyABRController.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyTrackSelector.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyABRController.java b/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyABRController.java new file mode 100644 index 00000000000..afb7fd6b2fe --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyABRController.java @@ -0,0 +1,201 @@ +package com.google.android.exoplayer2.harmoniclowlatency; + +import android.util.Log; + +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.common.primitives.Ints; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicInteger; + +public final class LowLatencyABRController { + private int currentSelectedBitrate; + private final int videoRendererIndex; + private final AtomicInteger stallCount; + private final Timer resetStallCountTimer; + private final long resetStallCountDelayMs; + private final Timer increaseVideoBitrateTimer; + private final long initialIncreaseVideoBitrateDelayMs; + private final AtomicInteger consecutiveFailedIncreaseVideoBitrateCount; + private final long switchingDelayMs; + private boolean isPreviousSwitchIncrease; + private Long previousSwitchTime; + private TimerTask currentResetStallCountTimerTask; + private TimerTask currentIncreaseVideoBitrateTimerTask; + private final Player.EventListener listener; + private final ExoPlayer player; + private final LowLatencyTrackSelector trackSelector; + + public LowLatencyABRController(ExoPlayer player, LowLatencyTrackSelector trackSelector) { + this.player = player; + this.videoRendererIndex = 0; + this.trackSelector = trackSelector; + this.stallCount = new AtomicInteger(0); + this.resetStallCountTimer = new Timer(); + this.resetStallCountDelayMs = 30000L; + this.increaseVideoBitrateTimer = new Timer(); + this.initialIncreaseVideoBitrateDelayMs = 30000L; + this.consecutiveFailedIncreaseVideoBitrateCount = new AtomicInteger(0); + this.switchingDelayMs = 5000L; + this.previousSwitchTime = 0L; + this.listener = new EventListener() { + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == Player.STATE_BUFFERING && (System.currentTimeMillis() - previousSwitchTime < switchingDelayMs)) { + return; + } + stallCount.incrementAndGet(); + scheduleResetStallCountTimerTask(); + scheduleIncreaseVideoBitrateTimerTask(initialIncreaseVideoBitrateDelayMs << consecutiveFailedIncreaseVideoBitrateCount.get()); + if (stallCount.get() >= 3) { + decreaseVideoBitrate(); + } + } + }; + } + + public final int getCurrentSelectedBitrate() { + return currentSelectedBitrate; + } + + private void scheduleResetStallCountTimerTask() { + TimerTask timerTask = currentResetStallCountTimerTask; + if (timerTask != null) { + timerTask.cancel(); + } + + resetStallCountTimer.purge(); + currentResetStallCountTimerTask = new TimerTask() { + public void run() { + Log.d("VosPlayerSdk", "resetStallCount"); + stallCount.set(0); + } + }; + resetStallCountTimer.schedule(currentResetStallCountTimerTask, resetStallCountDelayMs); + } + + private void scheduleIncreaseVideoBitrateTimerTask(long delayMs) { + Log.d("VosPlayerSdk", "Reschedule next up-switching timer task in " + delayMs / (long) 1000 + 's'); + TimerTask timerTask = currentResetStallCountTimerTask; + if (timerTask != null) { + timerTask.cancel(); + } + + increaseVideoBitrateTimer.purge(); + currentIncreaseVideoBitrateTimerTask = new TimerTask() { + public void run() { + increaseVideoBitrate(); + } + }; + increaseVideoBitrateTimer.schedule(currentIncreaseVideoBitrateTimerTask, delayMs); + } + + private void decreaseVideoBitrate() { + adjustVideoBitrate(false); + stallCount.set(0); + } + + private void increaseVideoBitrate() { + adjustVideoBitrate(true); + } + + private void adjustVideoBitrate(boolean increaseBitrate) { + MappedTrackInfo curInfo = this.trackSelector.getCurrentMappedTrackInfo(); + if (curInfo == null) { + return; + } + TrackGroupArray videoTrackGroups = curInfo.getTrackGroups(this.videoRendererIndex); + TrackGroup videoTrackGroup = videoTrackGroups.get(0); + ArrayList videoTrackList = new ArrayList<>(); + for (int x=0; x < videoTrackGroup.length; x++) { + Format videoTrack = videoTrackGroup.getFormat(x); + videoTrackList.add(videoTrack); + } + + ArrayList bitrateList = new ArrayList<>(); + for (int x=0; x < videoTrackList.size(); x++) { + bitrateList.add(videoTrackList.get(x).bitrate); + } + Collections.sort(bitrateList); + + if (currentSelectedBitrate == 0) { + this.currentSelectedBitrate = Collections.max(bitrateList); + } + + if (this.trackSelector.isAuto()) { + int targetBitrate = this.getTargetedBitrate(increaseBitrate, bitrateList); + Log.i("VosPlayerSdk", "Target Video Bitrate: " + targetBitrate); + if (targetBitrate != this.currentSelectedBitrate) { + ArrayList targetIndices = new ArrayList<>(); + for (int x = 0; x < videoTrackGroup.length; x++) { + if (videoTrackGroup.getFormat(x).bitrate == targetBitrate) { + targetIndices.add(x); + } + } + + trackSelector.setAutoBitrateSwitchingParameters( + trackSelector.getParameters().buildUpon() + .setSelectionOverride( + videoRendererIndex, + videoTrackGroups, + new DefaultTrackSelector.SelectionOverride(0, Ints.toArray(targetIndices)) + ) + .build() + ); + + if (isPreviousSwitchIncrease) { + if (increaseBitrate) { + consecutiveFailedIncreaseVideoBitrateCount.set(0); + } else { + consecutiveFailedIncreaseVideoBitrateCount.addAndGet(1); + Log.d("VosPlayerSdk", "increase bitrate failed, consecutiveFailedIncreaseVideoBitrateCount " + consecutiveFailedIncreaseVideoBitrateCount); + } + } else if (!increaseBitrate) { + consecutiveFailedIncreaseVideoBitrateCount.set(0); + } + + scheduleIncreaseVideoBitrateTimerTask(initialIncreaseVideoBitrateDelayMs << consecutiveFailedIncreaseVideoBitrateCount.get()); + previousSwitchTime = System.currentTimeMillis(); + isPreviousSwitchIncrease = increaseBitrate; + currentSelectedBitrate = targetBitrate; + } + } + + } + + private int getTargetedBitrate(boolean increaseBitrate, List bitrateList) { + int rv; + int curIdx; + if (increaseBitrate) { + Log.i("VosPlayerSdk", "Increasing Video Bitrate due to playback stable for " + (initialIncreaseVideoBitrateDelayMs << consecutiveFailedIncreaseVideoBitrateCount.get()) + " seconds"); + curIdx = bitrateList.lastIndexOf(currentSelectedBitrate); + rv = curIdx == bitrateList.size() - 1 ? currentSelectedBitrate : ((Number) bitrateList.get(curIdx + 1)).intValue(); + } else { + Log.i("VosPlayerSdk", "Decreasing Video Bitrate due to multiple stalls"); + curIdx = bitrateList.indexOf(currentSelectedBitrate); + rv = curIdx == 0 ? currentSelectedBitrate : ((Number) bitrateList.get(curIdx - 1)).intValue(); + } + + return rv; + } + + public final void start() { + player.addListener(listener); + } + + public final void stop() { + player.removeListener(listener); + resetStallCountTimer.cancel(); + increaseVideoBitrateTimer.cancel(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyTrackSelector.java new file mode 100644 index 00000000000..6da8f65575c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyTrackSelector.java @@ -0,0 +1,53 @@ +package com.google.android.exoplayer2.harmoniclowlatency; + +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection.Factory; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +public final class LowLatencyTrackSelector extends DefaultTrackSelector { + private final AtomicBoolean isAuto; + + public final boolean isAuto() { + return this.isAuto.get(); + } + + public LowLatencyTrackSelector(Factory aTSF) { + super(aTSF); + this.isAuto = new AtomicBoolean(true); + } + + public void setParameters(Parameters parameters) { + super.setParameters(parameters); + MappedTrackInfo info = super.getCurrentMappedTrackInfo(); + if (info != null) { + this.isAuto.set(!super.getParameters().hasSelectionOverride(0, info.getTrackGroups(0))); + } + } + + public void setParameters(ParametersBuilder parametersBuilder) { + super.setParameters(parametersBuilder); + MappedTrackInfo info = super.getCurrentMappedTrackInfo(); + if (info != null) { + this.isAuto.set(!super.getParameters().hasSelectionOverride(0, info.getTrackGroups(0))); + } + } + + public final void setAutoBitrateSwitchingParameters(Parameters parameters) { + if (this.isAuto.get()) { + super.setParameters(parameters); + } + } + + @Override + @NonNull + public Parameters getParameters() { + Parameters rv = super.getParameters(); + if (this.isAuto.get()) { + rv = rv.buildUpon().clearSelectionOverrides(0).build(); + } + return rv; + } +} From d4c5158290bda39542e5061ee8bce23bab6b69bd Mon Sep 17 00:00:00 2001 From: Joseph Chan Date: Wed, 20 Jan 2021 16:37:10 +0800 Subject: [PATCH 06/42] Add publish gradle tasks for maven central for branch 2.13 --- build.gradle | 2 + constants.gradle | 2 +- extensions/av1/build.gradle | 6 ++ extensions/cast/build.gradle | 1 + extensions/cronet/build.gradle | 1 + extensions/ffmpeg/build.gradle | 6 ++ extensions/flac/build.gradle | 6 ++ extensions/gvr/build.gradle | 1 + extensions/ima/build.gradle | 1 + extensions/jobdispatcher/build.gradle | 1 + extensions/leanback/build.gradle | 1 + extensions/mediasession/build.gradle | 1 + extensions/okhttp/build.gradle | 1 + extensions/opus/build.gradle | 6 ++ extensions/rtmp/build.gradle | 1 + extensions/vp9/build.gradle | 6 ++ extensions/workmanager/build.gradle | 1 + library/all/build.gradle | 1 + library/core/build.gradle | 1 + library/dash/build.gradle | 1 + library/hls/build.gradle | 1 + library/smoothstreaming/build.gradle | 1 + library/ui/build.gradle | 1 + playbacktests/build.gradle | 6 ++ publish-mavencentral.gradle | 101 ++++++++++++++++++++++++++ testutils/build.gradle | 1 + 26 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 publish-mavencentral.gradle diff --git a/build.gradle b/build.gradle index 8c2186522ae..cf5c0a1723e 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ buildscript { classpath 'com.android.tools.build:gradle:4.0.1' classpath 'com.novoda:bintray-release:0.9.1' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1' + classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.22.0" } } allprojects { @@ -40,3 +41,4 @@ allprojects { } apply from: 'javadoc_combined.gradle' +apply plugin: 'io.codearte.nexus-staging' \ No newline at end of file diff --git a/constants.gradle b/constants.gradle index 6c7eefe76f0..b9c24128bcf 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,7 +13,7 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.13.2' + releaseVersion = '2.13' releaseVersionCode = 2013002 minSdkVersion = 16 appTargetSdkVersion = 29 diff --git a/extensions/av1/build.gradle b/extensions/av1/build.gradle index 95a953d1453..a653aa2fcfb 100644 --- a/extensions/av1/build.gradle +++ b/extensions/av1/build.gradle @@ -52,3 +52,9 @@ ext { javadocTitle = 'AV1 extension' } apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-av1' + releaseDescription = 'AV1 extension for ExoPlayer.' +} +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index d0cc501fcb4..09ba9b15dd5 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -35,3 +35,4 @@ ext { releaseDescription = 'Cast extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index f50304fb94a..65b8938c36c 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -48,3 +48,4 @@ ext { releaseDescription = 'Cronet extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index a9edeaff6bd..5cbdfe73a83 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -33,3 +33,9 @@ ext { javadocTitle = 'FFmpeg extension' } apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-ffmpeg' + releaseDescription = 'FFMPEG extension for ExoPlayer.' +} +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 9aeeb83eb3f..02f5ab07488 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -41,3 +41,9 @@ ext { javadocTitle = 'FLAC extension' } apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-flac' + releaseDescription = 'FLAC extension for ExoPlayer.' +} +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 891888a0d25..4fb56a07646 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -34,3 +34,4 @@ ext { releaseDescription = 'Google VR extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 72d40b8f8f7..41d617366fd 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -50,3 +50,4 @@ ext { releaseDescription = 'Interactive Media Ads extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/jobdispatcher/build.gradle b/extensions/jobdispatcher/build.gradle index df50cde8f91..e977b282382 100644 --- a/extensions/jobdispatcher/build.gradle +++ b/extensions/jobdispatcher/build.gradle @@ -31,3 +31,4 @@ ext { releaseDescription = 'Firebase JobDispatcher extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index 14ced09f121..579b51dd983 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -32,3 +32,4 @@ ext { releaseDescription = 'Leanback extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 5c827084da7..9f3b0eee1f9 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -30,3 +30,4 @@ ext { releaseDescription = 'Media session extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 758eb646f61..5cd2decf09a 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -38,3 +38,4 @@ ext { releaseDescription = 'OkHttp extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index ba670037f60..75d66442261 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -37,3 +37,9 @@ ext { javadocTitle = 'Opus extension' } apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-opus' + releaseDescription = 'Opus extension for ExoPlayer.' +} +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index 7a373965688..7718d345949 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -33,3 +33,4 @@ ext { releaseDescription = 'RTMP extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 79d85a6ac52..a4a0e61d555 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -38,3 +38,9 @@ ext { javadocTitle = 'VP9 extension' } apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-vp9' + releaseDescription = 'VP9 extension for ExoPlayer.' +} +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index b3624e75dc0..d72c8f53d83 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -31,3 +31,4 @@ ext { releaseDescription = 'WorkManager extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/all/build.gradle b/library/all/build.gradle index e18d856c838..5599bfbdb20 100644 --- a/library/all/build.gradle +++ b/library/all/build.gradle @@ -27,3 +27,4 @@ ext { releaseDescription = 'The ExoPlayer library (all modules).' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/core/build.gradle b/library/core/build.gradle index ae8e7b773f9..df8502e5036 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -63,3 +63,4 @@ ext { releaseDescription = 'The ExoPlayer library core module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/dash/build.gradle b/library/dash/build.gradle index dd1a939fb71..52fd8531f0e 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -44,3 +44,4 @@ ext { releaseDescription = 'The ExoPlayer library DASH module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/hls/build.gradle b/library/hls/build.gradle index d31f1ce6a23..c5212e9fdd9 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -46,3 +46,4 @@ ext { releaseDescription = 'The ExoPlayer library HLS module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index 34fa62e0962..d979c57826a 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -43,3 +43,4 @@ ext { releaseDescription = 'The ExoPlayer library SmoothStreaming module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 81e1e5e1264..dcbaa487e33 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -36,3 +36,4 @@ ext { releaseDescription = 'The ExoPlayer library UI module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index 7fc5d637cb5..fbf04ae905d 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -28,3 +28,9 @@ dependencies { androidTestImplementation project(modulePrefix + 'library-hls') androidTestImplementation project(modulePrefix + 'testutils') } + +ext { + releaseArtifact = 'exoplayer-playbacktests' + releaseDescription = 'Playback tests for ExoPlayer.' +} +apply from: '../publish-mavencentral.gradle' \ No newline at end of file diff --git a/publish-mavencentral.gradle b/publish-mavencentral.gradle new file mode 100644 index 00000000000..c017f6a0326 --- /dev/null +++ b/publish-mavencentral.gradle @@ -0,0 +1,101 @@ +task androidSourcesJar(type: Jar) { + archiveClassifier.set('sources') + from android.sourceSets.main.java.srcDirs +} + +artifacts { + archives androidSourcesJar +} + +apply plugin: 'maven-publish' +apply plugin: 'signing' + +ext["signing.keyId"] = '' +ext["signing.password"] = '' +ext["signing.secretKeyRingFile"] = '' +ext["ossrhUsername"] = '' +ext["ossrhPassword"] = '' +ext["publishGroupId"] = '' +ext["sonatypeStagingProfileId"] = '' +ext["buildNumber"] = '' + +println "Loading env vars" +ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') +ext["signing.password"] = System.getenv('SIGNING_PASSWORD') +ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE') +ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') +ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') +ext["publishGroupId"] = System.getenv('PUBLISH_GROUP_ID') +ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') +ext["buildNumber"] = System.getenv('BUILD_NUMBER') + +nexusStaging { + packageGroup = publishGroupId + stagingProfileId = sonatypeStagingProfileId + username = ossrhUsername + password = ossrhPassword +} + +publishing { + publications { + release(MavenPublication) { + groupId publishGroupId + artifactId releaseArtifact + version "$releaseVersion.$buildNumber" + artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") + artifact androidSourcesJar + + pom { + name = releaseArtifact + description = releaseDescription + url = 'https://github.com/harmonicinc-com/ExoPlayer' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'harmonicinc-jigsaw' + name = 'Albert Cheung' + email = 'albert.cheung@harmonicinc.com' + } + } + scm { + connection = 'scm:git:github.com/harmonicinc-com/ExoPlayer.git' + developerConnection = 'scm:git:ssh://github.com/harmonicinc-com/ExoPlayer.git' + url = 'https://github.com/harmonicinc-com/ExoPlayer' + } + withXml { + def dependenciesNode = asNode().appendNode('dependencies') + + project.configurations.implementation.allDependencies.each { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + } + } + } + } + } + repositories { + maven { + name = "sonatype" + + def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + + credentials { + username ossrhUsername + password ossrhPassword + } + } + } +} + +signing { + sign publishing.publications +} \ No newline at end of file diff --git a/testutils/build.gradle b/testutils/build.gradle index cc72126ba35..c35f5b5458f 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -38,3 +38,4 @@ ext { releaseDescription = 'Test utils for ExoPlayer.' } apply from: '../publish.gradle' +apply from: '../publish-mavencentral.gradle' From d326fd41591dd52e95bcb5aa58ad9a3785e23550 Mon Sep 17 00:00:00 2001 From: Joseph Chan Date: Tue, 13 Apr 2021 14:25:07 +0800 Subject: [PATCH 07/42] Switch to another sonatype server for publishing library --- publish-mavencentral.gradle | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/publish-mavencentral.gradle b/publish-mavencentral.gradle index c017f6a0326..6e5e31a076b 100644 --- a/publish-mavencentral.gradle +++ b/publish-mavencentral.gradle @@ -30,6 +30,7 @@ ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') ext["buildNumber"] = System.getenv('BUILD_NUMBER') nexusStaging { + serverUrl = 'https://s01.oss.sonatype.org/service/local/' packageGroup = publishGroupId stagingProfileId = sonatypeStagingProfileId username = ossrhUsername @@ -84,8 +85,8 @@ publishing { maven { name = "sonatype" - def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" + def releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + def snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl credentials { From 459436a080f5455456714ecbfc5b60b1cb383c9c Mon Sep 17 00:00:00 2001 From: Joseph Chan Date: Mon, 25 Jan 2021 14:17:16 +0800 Subject: [PATCH 08/42] Fix dependencies group id for publishing public library --- build.gradle | 2 +- publish-mavencentral.gradle | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index cf5c0a1723e..0e356f8676e 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ allprojects { } buildDir = "${externalBuildDir}/${project.name}" } - group = 'com.google.android.exoplayer' + group = 'com.github.harmonicinc-com' } apply from: 'javadoc_combined.gradle' diff --git a/publish-mavencentral.gradle b/publish-mavencentral.gradle index 6e5e31a076b..cccbb8be632 100644 --- a/publish-mavencentral.gradle +++ b/publish-mavencentral.gradle @@ -15,7 +15,6 @@ ext["signing.password"] = '' ext["signing.secretKeyRingFile"] = '' ext["ossrhUsername"] = '' ext["ossrhPassword"] = '' -ext["publishGroupId"] = '' ext["sonatypeStagingProfileId"] = '' ext["buildNumber"] = '' @@ -25,13 +24,12 @@ ext["signing.password"] = System.getenv('SIGNING_PASSWORD') ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE') ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') -ext["publishGroupId"] = System.getenv('PUBLISH_GROUP_ID') ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') ext["buildNumber"] = System.getenv('BUILD_NUMBER') nexusStaging { serverUrl = 'https://s01.oss.sonatype.org/service/local/' - packageGroup = publishGroupId + packageGroup = 'com.github.harmonicinc-com' stagingProfileId = sonatypeStagingProfileId username = ossrhUsername password = ossrhPassword @@ -40,7 +38,7 @@ nexusStaging { publishing { publications { release(MavenPublication) { - groupId publishGroupId + groupId 'com.github.harmonicinc-com' artifactId releaseArtifact version "$releaseVersion.$buildNumber" artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") @@ -73,9 +71,11 @@ publishing { project.configurations.implementation.allDependencies.each { def dependencyNode = dependenciesNode.appendNode('dependency') + def patchedArtifactId = it.name.replace("library", "exoplayer").replace("testutils", "exoplayer-testutils") + dependencyNode.appendNode('groupId', it.group) - dependencyNode.appendNode('artifactId', it.name) - dependencyNode.appendNode('version', it.version) + dependencyNode.appendNode('artifactId', it.group.contentEquals("com.github.harmonicinc-com")?patchedArtifactId:it.name) + dependencyNode.appendNode('version', it.group.contentEquals("com.github.harmonicinc-com")?"$releaseVersion.$buildNumber":it.version) } } } From 930b02cc95c25ca2e44ccc321e2f13bab3900bb9 Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Tue, 13 Apr 2021 17:05:13 +0800 Subject: [PATCH 09/42] Add Usage for LowLatencyABRController --- .../LowLatencyABRController.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyABRController.java b/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyABRController.java index afb7fd6b2fe..33b007e48ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyABRController.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyABRController.java @@ -19,6 +19,25 @@ import java.util.TimerTask; import java.util.concurrent.atomic.AtomicInteger; +/* +Usage: + +AdaptiveTrackSelectionFactory aTSF = new AdaptiveTrackSelection.Factory(0, 3000, 1000, 1f); +LowLatencyTrackSelector trackSelector = new LowLatencyTrackSelector(aTSF); +SimpleExoPlayer player = new SimpleExoPlayer.Builder(context) + .setTrackSelector(trackSelector) + .build(); + +LowLatencyABRController abrController = new LowLatencyABRController( + player, + trackSelector as LowLatencyTrackSelector +); + +abrController.start(); + +player.prepare(curMediaSource!!) + + */ public final class LowLatencyABRController { private int currentSelectedBitrate; private final int videoRendererIndex; From 01fcedb275268cf6452f2936e373b9e3eac56aab Mon Sep 17 00:00:00 2001 From: Joseph Chan Date: Wed, 14 Apr 2021 11:41:03 +0800 Subject: [PATCH 10/42] Disable official publish gradle script --- publish.gradle | 204 ++++++++++++++++++++++++------------------------- 1 file changed, 102 insertions(+), 102 deletions(-) diff --git a/publish.gradle b/publish.gradle index 437704f7b03..f70c73b0e11 100644 --- a/publish.gradle +++ b/publish.gradle @@ -12,105 +12,105 @@ // See the License for the specific language governing permissions and // limitations under the License. -if (project.ext.has("exoplayerPublishEnabled") - && project.ext.exoplayerPublishEnabled) { - // For publishing to Bintray. - apply plugin: 'bintray-release' - publish { - artifactId = releaseArtifact - desc = releaseDescription - publishVersion = releaseVersion - repoName = getBintrayRepo() - userOrg = 'google' - groupId = 'com.google.android.exoplayer' - website = 'https://github.com/google/ExoPlayer' - } - - gradle.taskGraph.whenReady { taskGraph -> - project.tasks - .findAll { task -> task.name.contains("generatePomFileFor") } - .forEach { task -> - task.doLast { - task.outputs.files - .filter { File file -> - file.path.contains("publications") \ - && file.name.matches("^pom-.+\\.xml\$") - } - .forEach { File file -> addLicense(file) } - } - } - } -} else { - // For publishing to a Maven repository. - apply plugin: 'maven-publish' - afterEvaluate { - publishing { - repositories { - maven { - url = findProperty('mavenRepo') ?: "${buildDir}/repo" - } - } - publications { - release(MavenPublication) { - from components.release - artifact androidSourcesJar - groupId = 'com.google.android.exoplayer' - artifactId = releaseArtifact - version releaseVersion - pom { - name = releaseArtifact - description = releaseDescription - licenses { - license { - name = 'The Apache Software License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution = 'repo' - } - } - developers { - developer { - name = 'The Android Open Source Project' - } - } - scm { - connection = 'scm:git:https://github.com/google/ExoPlayer.git' - url = 'https://github.com/google/ExoPlayer' - } - } - } - } - } - } -} - -def getBintrayRepo() { - boolean publicRepo = hasProperty('publicRepo') && - property('publicRepo').toBoolean() - return publicRepo ? 'exoplayer' : 'exoplayer-test' -} - -static void addLicense(File pom) { - def licenseNode = new Node(null, "license") - licenseNode.append( - new Node(null, "name", "The Apache Software License, Version 2.0")) - licenseNode.append( - new Node(null, "url", "http://www.apache.org/licenses/LICENSE-2.0.txt")) - licenseNode.append(new Node(null, "distribution", "repo")) - def licensesNode = new Node(null, "licenses") - licensesNode.append(licenseNode) - - def xml = new XmlParser().parse(pom) - xml.append(licensesNode) - - def writer = new PrintWriter(new FileWriter(pom)) - writer.write("\n") - def printer = new XmlNodePrinter(writer) - printer.preserveWhitespace = true - printer.print(xml) - writer.close() -} - -task androidSourcesJar(type: Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs -} +//if (project.ext.has("exoplayerPublishEnabled") +// && project.ext.exoplayerPublishEnabled) { +// // For publishing to Bintray. +// apply plugin: 'bintray-release' +// publish { +// artifactId = releaseArtifact +// desc = releaseDescription +// publishVersion = releaseVersion +// repoName = getBintrayRepo() +// userOrg = 'google' +// groupId = 'com.google.android.exoplayer' +// website = 'https://github.com/google/ExoPlayer' +// } +// +// gradle.taskGraph.whenReady { taskGraph -> +// project.tasks +// .findAll { task -> task.name.contains("generatePomFileFor") } +// .forEach { task -> +// task.doLast { +// task.outputs.files +// .filter { File file -> +// file.path.contains("publications") \ +// && file.name.matches("^pom-.+\\.xml\$") +// } +// .forEach { File file -> addLicense(file) } +// } +// } +// } +//} else { +// // For publishing to a Maven repository. +// apply plugin: 'maven-publish' +// afterEvaluate { +// publishing { +// repositories { +// maven { +// url = findProperty('mavenRepo') ?: "${buildDir}/repo" +// } +// } +// publications { +// release(MavenPublication) { +// from components.release +// artifact androidSourcesJar +// groupId = 'com.google.android.exoplayer' +// artifactId = releaseArtifact +// version releaseVersion +// pom { +// name = releaseArtifact +// description = releaseDescription +// licenses { +// license { +// name = 'The Apache Software License, Version 2.0' +// url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' +// distribution = 'repo' +// } +// } +// developers { +// developer { +// name = 'The Android Open Source Project' +// } +// } +// scm { +// connection = 'scm:git:https://github.com/google/ExoPlayer.git' +// url = 'https://github.com/google/ExoPlayer' +// } +// } +// } +// } +// } +// } +//} +// +//def getBintrayRepo() { +// boolean publicRepo = hasProperty('publicRepo') && +// property('publicRepo').toBoolean() +// return publicRepo ? 'exoplayer' : 'exoplayer-test' +//} +// +//static void addLicense(File pom) { +// def licenseNode = new Node(null, "license") +// licenseNode.append( +// new Node(null, "name", "The Apache Software License, Version 2.0")) +// licenseNode.append( +// new Node(null, "url", "http://www.apache.org/licenses/LICENSE-2.0.txt")) +// licenseNode.append(new Node(null, "distribution", "repo")) +// def licensesNode = new Node(null, "licenses") +// licensesNode.append(licenseNode) +// +// def xml = new XmlParser().parse(pom) +// xml.append(licensesNode) +// +// def writer = new PrintWriter(new FileWriter(pom)) +// writer.write("\n") +// def printer = new XmlNodePrinter(writer) +// printer.preserveWhitespace = true +// printer.print(xml) +// writer.close() +//} +// +//task androidSourcesJar(type: Jar) { +// archiveClassifier.set('sources') +// from android.sourceSets.main.java.srcDirs +//} From 2c7f948987f806b48050774baa740afe86536583 Mon Sep 17 00:00:00 2001 From: Joseph Chan Date: Wed, 20 Jan 2021 16:37:10 +0800 Subject: [PATCH 11/42] Add publish gradle tasks for maven central for branch 2.13 --- build.gradle | 2 + constants.gradle | 2 +- extensions/av1/build.gradle | 6 ++ extensions/cast/build.gradle | 1 + extensions/cronet/build.gradle | 1 + extensions/ffmpeg/build.gradle | 6 ++ extensions/flac/build.gradle | 6 ++ extensions/gvr/build.gradle | 1 + extensions/ima/build.gradle | 1 + extensions/jobdispatcher/build.gradle | 1 + extensions/leanback/build.gradle | 1 + extensions/mediasession/build.gradle | 1 + extensions/okhttp/build.gradle | 1 + extensions/opus/build.gradle | 6 ++ extensions/rtmp/build.gradle | 1 + extensions/vp9/build.gradle | 6 ++ extensions/workmanager/build.gradle | 1 + library/all/build.gradle | 1 + library/core/build.gradle | 1 + library/dash/build.gradle | 1 + library/hls/build.gradle | 1 + library/smoothstreaming/build.gradle | 1 + library/ui/build.gradle | 1 + playbacktests/build.gradle | 6 ++ publish-mavencentral.gradle | 101 ++++++++++++++++++++++++++ testutils/build.gradle | 1 + 26 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 publish-mavencentral.gradle diff --git a/build.gradle b/build.gradle index 8c2186522ae..cf5c0a1723e 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ buildscript { classpath 'com.android.tools.build:gradle:4.0.1' classpath 'com.novoda:bintray-release:0.9.1' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1' + classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.22.0" } } allprojects { @@ -40,3 +41,4 @@ allprojects { } apply from: 'javadoc_combined.gradle' +apply plugin: 'io.codearte.nexus-staging' \ No newline at end of file diff --git a/constants.gradle b/constants.gradle index 6c7eefe76f0..b9c24128bcf 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,7 +13,7 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.13.2' + releaseVersion = '2.13' releaseVersionCode = 2013002 minSdkVersion = 16 appTargetSdkVersion = 29 diff --git a/extensions/av1/build.gradle b/extensions/av1/build.gradle index 95a953d1453..a653aa2fcfb 100644 --- a/extensions/av1/build.gradle +++ b/extensions/av1/build.gradle @@ -52,3 +52,9 @@ ext { javadocTitle = 'AV1 extension' } apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-av1' + releaseDescription = 'AV1 extension for ExoPlayer.' +} +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index d0cc501fcb4..09ba9b15dd5 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -35,3 +35,4 @@ ext { releaseDescription = 'Cast extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index f50304fb94a..65b8938c36c 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -48,3 +48,4 @@ ext { releaseDescription = 'Cronet extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index a9edeaff6bd..5cbdfe73a83 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -33,3 +33,9 @@ ext { javadocTitle = 'FFmpeg extension' } apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-ffmpeg' + releaseDescription = 'FFMPEG extension for ExoPlayer.' +} +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 9aeeb83eb3f..02f5ab07488 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -41,3 +41,9 @@ ext { javadocTitle = 'FLAC extension' } apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-flac' + releaseDescription = 'FLAC extension for ExoPlayer.' +} +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 891888a0d25..4fb56a07646 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -34,3 +34,4 @@ ext { releaseDescription = 'Google VR extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 72d40b8f8f7..41d617366fd 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -50,3 +50,4 @@ ext { releaseDescription = 'Interactive Media Ads extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/jobdispatcher/build.gradle b/extensions/jobdispatcher/build.gradle index df50cde8f91..e977b282382 100644 --- a/extensions/jobdispatcher/build.gradle +++ b/extensions/jobdispatcher/build.gradle @@ -31,3 +31,4 @@ ext { releaseDescription = 'Firebase JobDispatcher extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index 14ced09f121..579b51dd983 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -32,3 +32,4 @@ ext { releaseDescription = 'Leanback extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 5c827084da7..9f3b0eee1f9 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -30,3 +30,4 @@ ext { releaseDescription = 'Media session extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 758eb646f61..5cd2decf09a 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -38,3 +38,4 @@ ext { releaseDescription = 'OkHttp extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index ba670037f60..75d66442261 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -37,3 +37,9 @@ ext { javadocTitle = 'Opus extension' } apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-opus' + releaseDescription = 'Opus extension for ExoPlayer.' +} +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index 7a373965688..7718d345949 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -33,3 +33,4 @@ ext { releaseDescription = 'RTMP extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 79d85a6ac52..a4a0e61d555 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -38,3 +38,9 @@ ext { javadocTitle = 'VP9 extension' } apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-vp9' + releaseDescription = 'VP9 extension for ExoPlayer.' +} +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index b3624e75dc0..d72c8f53d83 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -31,3 +31,4 @@ ext { releaseDescription = 'WorkManager extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/all/build.gradle b/library/all/build.gradle index e18d856c838..5599bfbdb20 100644 --- a/library/all/build.gradle +++ b/library/all/build.gradle @@ -27,3 +27,4 @@ ext { releaseDescription = 'The ExoPlayer library (all modules).' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/core/build.gradle b/library/core/build.gradle index ae8e7b773f9..df8502e5036 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -63,3 +63,4 @@ ext { releaseDescription = 'The ExoPlayer library core module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/dash/build.gradle b/library/dash/build.gradle index dd1a939fb71..52fd8531f0e 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -44,3 +44,4 @@ ext { releaseDescription = 'The ExoPlayer library DASH module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/hls/build.gradle b/library/hls/build.gradle index d31f1ce6a23..c5212e9fdd9 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -46,3 +46,4 @@ ext { releaseDescription = 'The ExoPlayer library HLS module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index 34fa62e0962..d979c57826a 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -43,3 +43,4 @@ ext { releaseDescription = 'The ExoPlayer library SmoothStreaming module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 81e1e5e1264..dcbaa487e33 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -36,3 +36,4 @@ ext { releaseDescription = 'The ExoPlayer library UI module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index 7fc5d637cb5..fbf04ae905d 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -28,3 +28,9 @@ dependencies { androidTestImplementation project(modulePrefix + 'library-hls') androidTestImplementation project(modulePrefix + 'testutils') } + +ext { + releaseArtifact = 'exoplayer-playbacktests' + releaseDescription = 'Playback tests for ExoPlayer.' +} +apply from: '../publish-mavencentral.gradle' \ No newline at end of file diff --git a/publish-mavencentral.gradle b/publish-mavencentral.gradle new file mode 100644 index 00000000000..c017f6a0326 --- /dev/null +++ b/publish-mavencentral.gradle @@ -0,0 +1,101 @@ +task androidSourcesJar(type: Jar) { + archiveClassifier.set('sources') + from android.sourceSets.main.java.srcDirs +} + +artifacts { + archives androidSourcesJar +} + +apply plugin: 'maven-publish' +apply plugin: 'signing' + +ext["signing.keyId"] = '' +ext["signing.password"] = '' +ext["signing.secretKeyRingFile"] = '' +ext["ossrhUsername"] = '' +ext["ossrhPassword"] = '' +ext["publishGroupId"] = '' +ext["sonatypeStagingProfileId"] = '' +ext["buildNumber"] = '' + +println "Loading env vars" +ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') +ext["signing.password"] = System.getenv('SIGNING_PASSWORD') +ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE') +ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') +ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') +ext["publishGroupId"] = System.getenv('PUBLISH_GROUP_ID') +ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') +ext["buildNumber"] = System.getenv('BUILD_NUMBER') + +nexusStaging { + packageGroup = publishGroupId + stagingProfileId = sonatypeStagingProfileId + username = ossrhUsername + password = ossrhPassword +} + +publishing { + publications { + release(MavenPublication) { + groupId publishGroupId + artifactId releaseArtifact + version "$releaseVersion.$buildNumber" + artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") + artifact androidSourcesJar + + pom { + name = releaseArtifact + description = releaseDescription + url = 'https://github.com/harmonicinc-com/ExoPlayer' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'harmonicinc-jigsaw' + name = 'Albert Cheung' + email = 'albert.cheung@harmonicinc.com' + } + } + scm { + connection = 'scm:git:github.com/harmonicinc-com/ExoPlayer.git' + developerConnection = 'scm:git:ssh://github.com/harmonicinc-com/ExoPlayer.git' + url = 'https://github.com/harmonicinc-com/ExoPlayer' + } + withXml { + def dependenciesNode = asNode().appendNode('dependencies') + + project.configurations.implementation.allDependencies.each { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + } + } + } + } + } + repositories { + maven { + name = "sonatype" + + def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + + credentials { + username ossrhUsername + password ossrhPassword + } + } + } +} + +signing { + sign publishing.publications +} \ No newline at end of file diff --git a/testutils/build.gradle b/testutils/build.gradle index cc72126ba35..c35f5b5458f 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -38,3 +38,4 @@ ext { releaseDescription = 'Test utils for ExoPlayer.' } apply from: '../publish.gradle' +apply from: '../publish-mavencentral.gradle' From 6d591fe0ef5a6a339665837d6b127150fb7da6c1 Mon Sep 17 00:00:00 2001 From: Joseph Chan Date: Tue, 13 Apr 2021 14:25:07 +0800 Subject: [PATCH 12/42] Switch to another sonatype server for publishing library --- publish-mavencentral.gradle | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/publish-mavencentral.gradle b/publish-mavencentral.gradle index c017f6a0326..6e5e31a076b 100644 --- a/publish-mavencentral.gradle +++ b/publish-mavencentral.gradle @@ -30,6 +30,7 @@ ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') ext["buildNumber"] = System.getenv('BUILD_NUMBER') nexusStaging { + serverUrl = 'https://s01.oss.sonatype.org/service/local/' packageGroup = publishGroupId stagingProfileId = sonatypeStagingProfileId username = ossrhUsername @@ -84,8 +85,8 @@ publishing { maven { name = "sonatype" - def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" + def releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + def snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl credentials { From a608e173dd59e6f47f2ca6cca1c376034290242a Mon Sep 17 00:00:00 2001 From: Joseph Chan Date: Mon, 25 Jan 2021 14:17:16 +0800 Subject: [PATCH 13/42] Fix dependencies group id for publishing public library --- build.gradle | 2 +- publish-mavencentral.gradle | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index cf5c0a1723e..0e356f8676e 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ allprojects { } buildDir = "${externalBuildDir}/${project.name}" } - group = 'com.google.android.exoplayer' + group = 'com.github.harmonicinc-com' } apply from: 'javadoc_combined.gradle' diff --git a/publish-mavencentral.gradle b/publish-mavencentral.gradle index 6e5e31a076b..cccbb8be632 100644 --- a/publish-mavencentral.gradle +++ b/publish-mavencentral.gradle @@ -15,7 +15,6 @@ ext["signing.password"] = '' ext["signing.secretKeyRingFile"] = '' ext["ossrhUsername"] = '' ext["ossrhPassword"] = '' -ext["publishGroupId"] = '' ext["sonatypeStagingProfileId"] = '' ext["buildNumber"] = '' @@ -25,13 +24,12 @@ ext["signing.password"] = System.getenv('SIGNING_PASSWORD') ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE') ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') -ext["publishGroupId"] = System.getenv('PUBLISH_GROUP_ID') ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') ext["buildNumber"] = System.getenv('BUILD_NUMBER') nexusStaging { serverUrl = 'https://s01.oss.sonatype.org/service/local/' - packageGroup = publishGroupId + packageGroup = 'com.github.harmonicinc-com' stagingProfileId = sonatypeStagingProfileId username = ossrhUsername password = ossrhPassword @@ -40,7 +38,7 @@ nexusStaging { publishing { publications { release(MavenPublication) { - groupId publishGroupId + groupId 'com.github.harmonicinc-com' artifactId releaseArtifact version "$releaseVersion.$buildNumber" artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") @@ -73,9 +71,11 @@ publishing { project.configurations.implementation.allDependencies.each { def dependencyNode = dependenciesNode.appendNode('dependency') + def patchedArtifactId = it.name.replace("library", "exoplayer").replace("testutils", "exoplayer-testutils") + dependencyNode.appendNode('groupId', it.group) - dependencyNode.appendNode('artifactId', it.name) - dependencyNode.appendNode('version', it.version) + dependencyNode.appendNode('artifactId', it.group.contentEquals("com.github.harmonicinc-com")?patchedArtifactId:it.name) + dependencyNode.appendNode('version', it.group.contentEquals("com.github.harmonicinc-com")?"$releaseVersion.$buildNumber":it.version) } } } From 0d0236c62d94082b578f72a1c7d2bfabfec9a192 Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Tue, 13 Apr 2021 17:05:13 +0800 Subject: [PATCH 14/42] Add Usage for LowLatencyABRController --- .../LowLatencyABRController.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyABRController.java b/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyABRController.java index afb7fd6b2fe..33b007e48ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyABRController.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/harmoniclowlatency/LowLatencyABRController.java @@ -19,6 +19,25 @@ import java.util.TimerTask; import java.util.concurrent.atomic.AtomicInteger; +/* +Usage: + +AdaptiveTrackSelectionFactory aTSF = new AdaptiveTrackSelection.Factory(0, 3000, 1000, 1f); +LowLatencyTrackSelector trackSelector = new LowLatencyTrackSelector(aTSF); +SimpleExoPlayer player = new SimpleExoPlayer.Builder(context) + .setTrackSelector(trackSelector) + .build(); + +LowLatencyABRController abrController = new LowLatencyABRController( + player, + trackSelector as LowLatencyTrackSelector +); + +abrController.start(); + +player.prepare(curMediaSource!!) + + */ public final class LowLatencyABRController { private int currentSelectedBitrate; private final int videoRendererIndex; From 05a81d75bec201229cfd1095d2d7e8780671731e Mon Sep 17 00:00:00 2001 From: Joseph Chan Date: Wed, 14 Apr 2021 11:41:03 +0800 Subject: [PATCH 15/42] Disable official publish gradle script --- publish.gradle | 204 ++++++++++++++++++++++++------------------------- 1 file changed, 102 insertions(+), 102 deletions(-) diff --git a/publish.gradle b/publish.gradle index 437704f7b03..f70c73b0e11 100644 --- a/publish.gradle +++ b/publish.gradle @@ -12,105 +12,105 @@ // See the License for the specific language governing permissions and // limitations under the License. -if (project.ext.has("exoplayerPublishEnabled") - && project.ext.exoplayerPublishEnabled) { - // For publishing to Bintray. - apply plugin: 'bintray-release' - publish { - artifactId = releaseArtifact - desc = releaseDescription - publishVersion = releaseVersion - repoName = getBintrayRepo() - userOrg = 'google' - groupId = 'com.google.android.exoplayer' - website = 'https://github.com/google/ExoPlayer' - } - - gradle.taskGraph.whenReady { taskGraph -> - project.tasks - .findAll { task -> task.name.contains("generatePomFileFor") } - .forEach { task -> - task.doLast { - task.outputs.files - .filter { File file -> - file.path.contains("publications") \ - && file.name.matches("^pom-.+\\.xml\$") - } - .forEach { File file -> addLicense(file) } - } - } - } -} else { - // For publishing to a Maven repository. - apply plugin: 'maven-publish' - afterEvaluate { - publishing { - repositories { - maven { - url = findProperty('mavenRepo') ?: "${buildDir}/repo" - } - } - publications { - release(MavenPublication) { - from components.release - artifact androidSourcesJar - groupId = 'com.google.android.exoplayer' - artifactId = releaseArtifact - version releaseVersion - pom { - name = releaseArtifact - description = releaseDescription - licenses { - license { - name = 'The Apache Software License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution = 'repo' - } - } - developers { - developer { - name = 'The Android Open Source Project' - } - } - scm { - connection = 'scm:git:https://github.com/google/ExoPlayer.git' - url = 'https://github.com/google/ExoPlayer' - } - } - } - } - } - } -} - -def getBintrayRepo() { - boolean publicRepo = hasProperty('publicRepo') && - property('publicRepo').toBoolean() - return publicRepo ? 'exoplayer' : 'exoplayer-test' -} - -static void addLicense(File pom) { - def licenseNode = new Node(null, "license") - licenseNode.append( - new Node(null, "name", "The Apache Software License, Version 2.0")) - licenseNode.append( - new Node(null, "url", "http://www.apache.org/licenses/LICENSE-2.0.txt")) - licenseNode.append(new Node(null, "distribution", "repo")) - def licensesNode = new Node(null, "licenses") - licensesNode.append(licenseNode) - - def xml = new XmlParser().parse(pom) - xml.append(licensesNode) - - def writer = new PrintWriter(new FileWriter(pom)) - writer.write("\n") - def printer = new XmlNodePrinter(writer) - printer.preserveWhitespace = true - printer.print(xml) - writer.close() -} - -task androidSourcesJar(type: Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs -} +//if (project.ext.has("exoplayerPublishEnabled") +// && project.ext.exoplayerPublishEnabled) { +// // For publishing to Bintray. +// apply plugin: 'bintray-release' +// publish { +// artifactId = releaseArtifact +// desc = releaseDescription +// publishVersion = releaseVersion +// repoName = getBintrayRepo() +// userOrg = 'google' +// groupId = 'com.google.android.exoplayer' +// website = 'https://github.com/google/ExoPlayer' +// } +// +// gradle.taskGraph.whenReady { taskGraph -> +// project.tasks +// .findAll { task -> task.name.contains("generatePomFileFor") } +// .forEach { task -> +// task.doLast { +// task.outputs.files +// .filter { File file -> +// file.path.contains("publications") \ +// && file.name.matches("^pom-.+\\.xml\$") +// } +// .forEach { File file -> addLicense(file) } +// } +// } +// } +//} else { +// // For publishing to a Maven repository. +// apply plugin: 'maven-publish' +// afterEvaluate { +// publishing { +// repositories { +// maven { +// url = findProperty('mavenRepo') ?: "${buildDir}/repo" +// } +// } +// publications { +// release(MavenPublication) { +// from components.release +// artifact androidSourcesJar +// groupId = 'com.google.android.exoplayer' +// artifactId = releaseArtifact +// version releaseVersion +// pom { +// name = releaseArtifact +// description = releaseDescription +// licenses { +// license { +// name = 'The Apache Software License, Version 2.0' +// url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' +// distribution = 'repo' +// } +// } +// developers { +// developer { +// name = 'The Android Open Source Project' +// } +// } +// scm { +// connection = 'scm:git:https://github.com/google/ExoPlayer.git' +// url = 'https://github.com/google/ExoPlayer' +// } +// } +// } +// } +// } +// } +//} +// +//def getBintrayRepo() { +// boolean publicRepo = hasProperty('publicRepo') && +// property('publicRepo').toBoolean() +// return publicRepo ? 'exoplayer' : 'exoplayer-test' +//} +// +//static void addLicense(File pom) { +// def licenseNode = new Node(null, "license") +// licenseNode.append( +// new Node(null, "name", "The Apache Software License, Version 2.0")) +// licenseNode.append( +// new Node(null, "url", "http://www.apache.org/licenses/LICENSE-2.0.txt")) +// licenseNode.append(new Node(null, "distribution", "repo")) +// def licensesNode = new Node(null, "licenses") +// licensesNode.append(licenseNode) +// +// def xml = new XmlParser().parse(pom) +// xml.append(licensesNode) +// +// def writer = new PrintWriter(new FileWriter(pom)) +// writer.write("\n") +// def printer = new XmlNodePrinter(writer) +// printer.preserveWhitespace = true +// printer.print(xml) +// writer.close() +//} +// +//task androidSourcesJar(type: Jar) { +// archiveClassifier.set('sources') +// from android.sourceSets.main.java.srcDirs +//} From 2f979371af5d9651c8568a8ad4546fc70bb28554 Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Thu, 15 Apr 2021 16:24:43 +0800 Subject: [PATCH 16/42] Temp disable roboletric tests indicated api 29 --- common_library_config.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common_library_config.gradle b/common_library_config.gradle index 431a7ab14d7..c010b571d9d 100644 --- a/common_library_config.gradle +++ b/common_library_config.gradle @@ -31,4 +31,9 @@ android { } testOptions.unitTests.includeAndroidResources = true + testOptions { + unitTests.all { + systemProperty 'robolectric.enabledSdks', '28' + } + } } From e22f8af2427b3a764cfa405eaa1ffd0c0674d81e Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Fri, 16 Apr 2021 10:44:27 +0800 Subject: [PATCH 17/42] fix test file not found --- .../manifest/DashManifestPatchParserTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParserTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParserTest.java index f0b409c311f..3892af9ad25 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParserTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParserTest.java @@ -12,15 +12,15 @@ /** Unit tests for {@link DashManifestPatchParser}. */ @RunWith(AndroidJUnit4.class) public class DashManifestPatchParserTest { - private static final String SAMPLE_MPD_PATCH = "manifest_patch/mpd_patch"; - private static final String SAMPLE_MPD_PATCH_ADD_SEGMENTS = "manifest_patch/mpd_patch_add_segments"; - private static final String SAMPLE_MPD_PATCH_ADD_PERIOD = "manifest_patch/mpd_patch_add_period"; - private static final String SAMPLE_MPD_PATCH_ADD_ATTRIBUTE = "manifest_patch/mpd_patch_add_attribute"; - private static final String SAMPLE_MPD_PATCH_REPLACE_ATTRIBUTE = "manifest_patch/mpd_patch_replace_attribute"; - private static final String SAMPLE_MPD_PATCH_REPLACE_NODE = "manifest_patch/mpd_patch_replace_node"; - private static final String XML_ADD_SEGMENTS = "manifest_patch/xml_add_segments"; - private static final String XML_ADD_PERIOD = "manifest_patch/xml_add_period"; - private static final String XML_REPLACE_NODE = "manifest_patch/xml_replace_node"; + private static final String SAMPLE_MPD_PATCH = "media/mpd/manifest_patch/mpd_patch"; + private static final String SAMPLE_MPD_PATCH_ADD_SEGMENTS = "media/mpd/manifest_patch/mpd_patch_add_segments"; + private static final String SAMPLE_MPD_PATCH_ADD_PERIOD = "media/mpd/manifest_patch/mpd_patch_add_period"; + private static final String SAMPLE_MPD_PATCH_ADD_ATTRIBUTE = "media/mpd/manifest_patch/mpd_patch_add_attribute"; + private static final String SAMPLE_MPD_PATCH_REPLACE_ATTRIBUTE = "media/mpd/manifest_patch/mpd_patch_replace_attribute"; + private static final String SAMPLE_MPD_PATCH_REPLACE_NODE = "media/mpd/manifest_patch/mpd_patch_replace_node"; + private static final String XML_ADD_SEGMENTS = "media/mpd/manifest_patch/xml_add_segments"; + private static final String XML_ADD_PERIOD = "media/mpd/manifest_patch/xml_add_period"; + private static final String XML_REPLACE_NODE = "media/mpd/manifest_patch/xml_replace_node"; @Test public void testParseSamplePatch() throws IOException { From 20ae95d859617a4c5bf0ad5206e1390111cd1278 Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Thu, 15 Apr 2021 16:24:43 +0800 Subject: [PATCH 18/42] Workaround roboletric test fail --- common_library_config.gradle | 5 +++++ .../manifest/DashManifestPatchParserTest.java | 18 +++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/common_library_config.gradle b/common_library_config.gradle index 431a7ab14d7..c010b571d9d 100644 --- a/common_library_config.gradle +++ b/common_library_config.gradle @@ -31,4 +31,9 @@ android { } testOptions.unitTests.includeAndroidResources = true + testOptions { + unitTests.all { + systemProperty 'robolectric.enabledSdks', '28' + } + } } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParserTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParserTest.java index f0b409c311f..3892af9ad25 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParserTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestPatchParserTest.java @@ -12,15 +12,15 @@ /** Unit tests for {@link DashManifestPatchParser}. */ @RunWith(AndroidJUnit4.class) public class DashManifestPatchParserTest { - private static final String SAMPLE_MPD_PATCH = "manifest_patch/mpd_patch"; - private static final String SAMPLE_MPD_PATCH_ADD_SEGMENTS = "manifest_patch/mpd_patch_add_segments"; - private static final String SAMPLE_MPD_PATCH_ADD_PERIOD = "manifest_patch/mpd_patch_add_period"; - private static final String SAMPLE_MPD_PATCH_ADD_ATTRIBUTE = "manifest_patch/mpd_patch_add_attribute"; - private static final String SAMPLE_MPD_PATCH_REPLACE_ATTRIBUTE = "manifest_patch/mpd_patch_replace_attribute"; - private static final String SAMPLE_MPD_PATCH_REPLACE_NODE = "manifest_patch/mpd_patch_replace_node"; - private static final String XML_ADD_SEGMENTS = "manifest_patch/xml_add_segments"; - private static final String XML_ADD_PERIOD = "manifest_patch/xml_add_period"; - private static final String XML_REPLACE_NODE = "manifest_patch/xml_replace_node"; + private static final String SAMPLE_MPD_PATCH = "media/mpd/manifest_patch/mpd_patch"; + private static final String SAMPLE_MPD_PATCH_ADD_SEGMENTS = "media/mpd/manifest_patch/mpd_patch_add_segments"; + private static final String SAMPLE_MPD_PATCH_ADD_PERIOD = "media/mpd/manifest_patch/mpd_patch_add_period"; + private static final String SAMPLE_MPD_PATCH_ADD_ATTRIBUTE = "media/mpd/manifest_patch/mpd_patch_add_attribute"; + private static final String SAMPLE_MPD_PATCH_REPLACE_ATTRIBUTE = "media/mpd/manifest_patch/mpd_patch_replace_attribute"; + private static final String SAMPLE_MPD_PATCH_REPLACE_NODE = "media/mpd/manifest_patch/mpd_patch_replace_node"; + private static final String XML_ADD_SEGMENTS = "media/mpd/manifest_patch/xml_add_segments"; + private static final String XML_ADD_PERIOD = "media/mpd/manifest_patch/xml_add_period"; + private static final String XML_REPLACE_NODE = "media/mpd/manifest_patch/xml_replace_node"; @Test public void testParseSamplePatch() throws IOException { From caea39894f78602f5f0adcdfa141784304be121b Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Tue, 20 Apr 2021 17:52:46 +0800 Subject: [PATCH 19/42] Pub missing lib --- extensions/media2/build.gradle | 1 + library/common/build.gradle | 1 + library/extractor/build.gradle | 1 + library/transformer/build.gradle | 1 + robolectricutils/build.gradle | 1 + 5 files changed, 5 insertions(+) diff --git a/extensions/media2/build.gradle b/extensions/media2/build.gradle index a89354d7b34..04db3c91257 100644 --- a/extensions/media2/build.gradle +++ b/extensions/media2/build.gradle @@ -40,3 +40,4 @@ ext { releaseDescription = 'Media2 extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/library/common/build.gradle b/library/common/build.gradle index d1d0d86f422..04b8126e288 100644 --- a/library/common/build.gradle +++ b/library/common/build.gradle @@ -50,3 +50,4 @@ ext { releaseDescription = 'The ExoPlayer library common module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/extractor/build.gradle b/library/extractor/build.gradle index e7f20051cd7..e2b50745066 100644 --- a/library/extractor/build.gradle +++ b/library/extractor/build.gradle @@ -45,3 +45,4 @@ ext { releaseDescription = 'The ExoPlayer library extractor module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/transformer/build.gradle b/library/transformer/build.gradle index 6870c9f5774..1fd8488967a 100644 --- a/library/transformer/build.gradle +++ b/library/transformer/build.gradle @@ -45,3 +45,4 @@ ext { releaseDescription = 'The ExoPlayer library transformer module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/robolectricutils/build.gradle b/robolectricutils/build.gradle index f5a86822b72..8762d28a6a9 100644 --- a/robolectricutils/build.gradle +++ b/robolectricutils/build.gradle @@ -33,3 +33,4 @@ ext { releaseDescription = 'Robolectric utils for ExoPlayer.' } apply from: '../publish.gradle' +apply from: '../../publish-mavencentral.gradle' From 0b76b837cfa648ca33ae9936285a9bc6febd5330 Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Tue, 20 Apr 2021 17:52:46 +0800 Subject: [PATCH 20/42] Pub missing lib --- extensions/media2/build.gradle | 1 + library/common/build.gradle | 1 + library/extractor/build.gradle | 1 + library/transformer/build.gradle | 1 + robolectricutils/build.gradle | 1 + 5 files changed, 5 insertions(+) diff --git a/extensions/media2/build.gradle b/extensions/media2/build.gradle index a89354d7b34..04db3c91257 100644 --- a/extensions/media2/build.gradle +++ b/extensions/media2/build.gradle @@ -40,3 +40,4 @@ ext { releaseDescription = 'Media2 extension for ExoPlayer.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/library/common/build.gradle b/library/common/build.gradle index d1d0d86f422..04b8126e288 100644 --- a/library/common/build.gradle +++ b/library/common/build.gradle @@ -50,3 +50,4 @@ ext { releaseDescription = 'The ExoPlayer library common module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/extractor/build.gradle b/library/extractor/build.gradle index e7f20051cd7..e2b50745066 100644 --- a/library/extractor/build.gradle +++ b/library/extractor/build.gradle @@ -45,3 +45,4 @@ ext { releaseDescription = 'The ExoPlayer library extractor module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' diff --git a/library/transformer/build.gradle b/library/transformer/build.gradle index 6870c9f5774..1fd8488967a 100644 --- a/library/transformer/build.gradle +++ b/library/transformer/build.gradle @@ -45,3 +45,4 @@ ext { releaseDescription = 'The ExoPlayer library transformer module.' } apply from: '../../publish.gradle' +apply from: '../../publish-mavencentral.gradle' \ No newline at end of file diff --git a/robolectricutils/build.gradle b/robolectricutils/build.gradle index f5a86822b72..8762d28a6a9 100644 --- a/robolectricutils/build.gradle +++ b/robolectricutils/build.gradle @@ -33,3 +33,4 @@ ext { releaseDescription = 'Robolectric utils for ExoPlayer.' } apply from: '../publish.gradle' +apply from: '../../publish-mavencentral.gradle' From 2c76282b613241c4f3d78b25a8e79662b6a81a9d Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Tue, 20 Apr 2021 18:11:00 +0800 Subject: [PATCH 21/42] typo --- robolectricutils/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robolectricutils/build.gradle b/robolectricutils/build.gradle index 8762d28a6a9..dd2325a3c96 100644 --- a/robolectricutils/build.gradle +++ b/robolectricutils/build.gradle @@ -33,4 +33,4 @@ ext { releaseDescription = 'Robolectric utils for ExoPlayer.' } apply from: '../publish.gradle' -apply from: '../../publish-mavencentral.gradle' +apply from: '../publish-mavencentral.gradle' From 4689a29e5d727293ce88a2ce9f4f26ad228c3ed0 Mon Sep 17 00:00:00 2001 From: Johnny Yiu Date: Tue, 20 Apr 2021 18:11:00 +0800 Subject: [PATCH 22/42] typo --- robolectricutils/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robolectricutils/build.gradle b/robolectricutils/build.gradle index 8762d28a6a9..dd2325a3c96 100644 --- a/robolectricutils/build.gradle +++ b/robolectricutils/build.gradle @@ -33,4 +33,4 @@ ext { releaseDescription = 'Robolectric utils for ExoPlayer.' } apply from: '../publish.gradle' -apply from: '../../publish-mavencentral.gradle' +apply from: '../publish-mavencentral.gradle' From 1ca4abac04d725841a0f31b23b44173769bf35da Mon Sep 17 00:00:00 2001 From: Enson Choy Date: Fri, 17 Sep 2021 17:14:04 +0800 Subject: [PATCH 23/42] Support thumbnail tile track --- .../java/com/google/android/exoplayer2/C.java | 9 ++++++--- .../com/google/android/exoplayer2/Format.java | 19 +++++++++++++++++++ .../android/exoplayer2/util/MimeTypes.java | 5 +++++ .../dash/manifest/DashManifestParser.java | 12 ++++++++++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/C.java b/library/common/src/main/java/com/google/android/exoplayer2/C.java index 1c2cc923624..05ab9221eb0 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/C.java @@ -1009,8 +1009,8 @@ private C() {} * #ROLE_FLAG_DUB}, {@link #ROLE_FLAG_EMERGENCY}, {@link #ROLE_FLAG_CAPTION}, {@link * #ROLE_FLAG_SUBTITLE}, {@link #ROLE_FLAG_SIGN}, {@link #ROLE_FLAG_DESCRIBES_VIDEO}, {@link * #ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND}, {@link #ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY}, - * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG}, {@link #ROLE_FLAG_EASY_TO_READ} and {@link - * #ROLE_FLAG_TRICK_PLAY}. + * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG}, {@link #ROLE_FLAG_EASY_TO_READ}, {@link + * #ROLE_FLAG_TRICK_PLAY} and {@link #ROLE_FLAG_THUMBNAIL_TILE} */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -1031,7 +1031,8 @@ private C() {} ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY, ROLE_FLAG_TRANSCRIBES_DIALOG, ROLE_FLAG_EASY_TO_READ, - ROLE_FLAG_TRICK_PLAY + ROLE_FLAG_TRICK_PLAY, + ROLE_FLAG_THUMBNAIL_TILE }) public @interface RoleFlags {} /** Indicates a main track. */ @@ -1079,6 +1080,8 @@ private C() {} public static final int ROLE_FLAG_EASY_TO_READ = 1 << 13; /** Indicates the track is intended for trick play. */ public static final int ROLE_FLAG_TRICK_PLAY = 1 << 14; + /** Indicates the track contains a thumbnail tile track */ + public static final int ROLE_FLAG_THUMBNAIL_TILE = 1 << 15; /** * Level of renderer support for a format. One of {@link #FORMAT_HANDLED}, {@link diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Format.java b/library/common/src/main/java/com/google/android/exoplayer2/Format.java index 05062727c30..d543d5dbe08 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Format.java @@ -165,6 +165,9 @@ public static final class Builder { private int accessibilityChannel; + // Thumbnail Tile specific + @Nullable private int[] tileLayout; + // Provided by the source. @Nullable private Class exoMediaCryptoType; @@ -230,6 +233,8 @@ private Builder(Format format) { this.encoderPadding = format.encoderPadding; // Text specific. this.accessibilityChannel = format.accessibilityChannel; + // Thumbnail Tile specific + this.tileLayout = format.tileLayout; // Provided by the source. this.exoMediaCryptoType = format.exoMediaCryptoType; } @@ -575,6 +580,11 @@ public Builder setAccessibilityChannel(int accessibilityChannel) { return this; } + // Thumbnail Tile specific + public void setTileLayout(int x, int y) { + this.tileLayout = new int[]{x, y}; + } + // Provided by source. /** @@ -754,6 +764,11 @@ public Format build() { /** The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */ public final int accessibilityChannel; + // Thumbnail Tile specific + + /** Tile layout. Format: [x, y]. Should always be length of 2*/ + @Nullable public final int[] tileLayout; + // Provided by source. /** @@ -1219,6 +1234,8 @@ private Format(Builder builder) { encoderPadding = builder.encoderPadding == NO_VALUE ? 0 : builder.encoderPadding; // Text specific. accessibilityChannel = builder.accessibilityChannel; + // Thumbnail Tile specific + tileLayout = builder.tileLayout; // Provided by source. if (builder.exoMediaCryptoType == null && drmInitData != null) { // Encrypted content must always have a non-null exoMediaCryptoType. @@ -1271,6 +1288,8 @@ private Format(Builder builder) { encoderPadding = in.readInt(); // Text specific. accessibilityChannel = in.readInt(); + // Thumbnail Tile specific + tileLayout = in.createIntArray(); // Provided by source. // Encrypted content must always have a non-null exoMediaCryptoType. exoMediaCryptoType = drmInitData != null ? UnsupportedMediaCrypto.class : null; diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 13cf6b18c3a..7d9e9111029 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -153,6 +153,11 @@ public static boolean isVideo(@Nullable String mimeType) { return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType)); } + /** Returns whether the given string is an image MIME type. */ + public static boolean isImage(@Nullable String mimeType) { + return BASE_TYPE_IMAGE.equals(getTopLevelType(mimeType)); + } + /** * Returns whether the given string is a text MIME type, including known text types that use * "application" as their base type. diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 508cfd4bff6..775eccc5596 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -48,6 +48,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -805,6 +806,15 @@ protected Format buildFormat( accessibilityChannel = parseCea708AccessibilityChannel(accessibilityDescriptors); } formatBuilder.setAccessibilityChannel(accessibilityChannel); + } else if (MimeTypes.isImage(containerMimeType) && roleFlags == C.ROLE_FLAG_THUMBNAIL_TILE) { + formatBuilder.setWidth(width).setHeight(height); + String tileLayoutStr = Objects.requireNonNull(essentialProperties.get(0).value); + String[] tileLayoutArr = tileLayoutStr.split("x"); + if (tileLayoutArr.length != 2) { + Log.e(TAG, "Tile layout malformed: " + tileLayoutStr); + } else { + formatBuilder.setTileLayout(Integer.parseInt(tileLayoutArr[0]), Integer.parseInt(tileLayoutArr[1])); + } } return formatBuilder.build(); @@ -1473,6 +1483,8 @@ protected int parseRoleFlagsFromProperties(List accessibilityDescrip Descriptor descriptor = accessibilityDescriptors.get(i); if ("http://dashif.org/guidelines/trickmode".equalsIgnoreCase(descriptor.schemeIdUri)) { result |= C.ROLE_FLAG_TRICK_PLAY; + } else if ("http://dashif.org/guidelines/thumbnail_tile".equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= C.ROLE_FLAG_THUMBNAIL_TILE; } } return result; From 8c0d1ab0f2e55078b899228ba614f1bf019742cb Mon Sep 17 00:00:00 2001 From: Enson Choy Date: Tue, 21 Sep 2021 13:00:12 +0800 Subject: [PATCH 24/42] Set tile layout to null as placeholder Thumbnail tile will not support parcel as argument in format() --- .../src/main/java/com/google/android/exoplayer2/Format.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Format.java b/library/common/src/main/java/com/google/android/exoplayer2/Format.java index d543d5dbe08..cf0dbb3a1b1 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Format.java @@ -1288,8 +1288,9 @@ private Format(Builder builder) { encoderPadding = in.readInt(); // Text specific. accessibilityChannel = in.readInt(); - // Thumbnail Tile specific - tileLayout = in.createIntArray(); + // @deprecated Thumbnail Tile specific + // act as placeholder + tileLayout = null; // Provided by source. // Encrypted content must always have a non-null exoMediaCryptoType. exoMediaCryptoType = drmInitData != null ? UnsupportedMediaCrypto.class : null; From c626bbc79c45e6c1da377611f6c1ebf79f8e2ce6 Mon Sep 17 00:00:00 2001 From: Enson Choy Date: Fri, 17 Sep 2021 17:14:04 +0800 Subject: [PATCH 25/42] Support thumbnail tile track --- .../java/com/google/android/exoplayer2/C.java | 9 ++++++--- .../com/google/android/exoplayer2/Format.java | 19 +++++++++++++++++++ .../android/exoplayer2/util/MimeTypes.java | 5 +++++ .../dash/manifest/DashManifestParser.java | 12 ++++++++++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/C.java b/library/common/src/main/java/com/google/android/exoplayer2/C.java index 1c2cc923624..05ab9221eb0 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/C.java @@ -1009,8 +1009,8 @@ private C() {} * #ROLE_FLAG_DUB}, {@link #ROLE_FLAG_EMERGENCY}, {@link #ROLE_FLAG_CAPTION}, {@link * #ROLE_FLAG_SUBTITLE}, {@link #ROLE_FLAG_SIGN}, {@link #ROLE_FLAG_DESCRIBES_VIDEO}, {@link * #ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND}, {@link #ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY}, - * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG}, {@link #ROLE_FLAG_EASY_TO_READ} and {@link - * #ROLE_FLAG_TRICK_PLAY}. + * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG}, {@link #ROLE_FLAG_EASY_TO_READ}, {@link + * #ROLE_FLAG_TRICK_PLAY} and {@link #ROLE_FLAG_THUMBNAIL_TILE} */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -1031,7 +1031,8 @@ private C() {} ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY, ROLE_FLAG_TRANSCRIBES_DIALOG, ROLE_FLAG_EASY_TO_READ, - ROLE_FLAG_TRICK_PLAY + ROLE_FLAG_TRICK_PLAY, + ROLE_FLAG_THUMBNAIL_TILE }) public @interface RoleFlags {} /** Indicates a main track. */ @@ -1079,6 +1080,8 @@ private C() {} public static final int ROLE_FLAG_EASY_TO_READ = 1 << 13; /** Indicates the track is intended for trick play. */ public static final int ROLE_FLAG_TRICK_PLAY = 1 << 14; + /** Indicates the track contains a thumbnail tile track */ + public static final int ROLE_FLAG_THUMBNAIL_TILE = 1 << 15; /** * Level of renderer support for a format. One of {@link #FORMAT_HANDLED}, {@link diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Format.java b/library/common/src/main/java/com/google/android/exoplayer2/Format.java index 05062727c30..d543d5dbe08 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Format.java @@ -165,6 +165,9 @@ public static final class Builder { private int accessibilityChannel; + // Thumbnail Tile specific + @Nullable private int[] tileLayout; + // Provided by the source. @Nullable private Class exoMediaCryptoType; @@ -230,6 +233,8 @@ private Builder(Format format) { this.encoderPadding = format.encoderPadding; // Text specific. this.accessibilityChannel = format.accessibilityChannel; + // Thumbnail Tile specific + this.tileLayout = format.tileLayout; // Provided by the source. this.exoMediaCryptoType = format.exoMediaCryptoType; } @@ -575,6 +580,11 @@ public Builder setAccessibilityChannel(int accessibilityChannel) { return this; } + // Thumbnail Tile specific + public void setTileLayout(int x, int y) { + this.tileLayout = new int[]{x, y}; + } + // Provided by source. /** @@ -754,6 +764,11 @@ public Format build() { /** The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */ public final int accessibilityChannel; + // Thumbnail Tile specific + + /** Tile layout. Format: [x, y]. Should always be length of 2*/ + @Nullable public final int[] tileLayout; + // Provided by source. /** @@ -1219,6 +1234,8 @@ private Format(Builder builder) { encoderPadding = builder.encoderPadding == NO_VALUE ? 0 : builder.encoderPadding; // Text specific. accessibilityChannel = builder.accessibilityChannel; + // Thumbnail Tile specific + tileLayout = builder.tileLayout; // Provided by source. if (builder.exoMediaCryptoType == null && drmInitData != null) { // Encrypted content must always have a non-null exoMediaCryptoType. @@ -1271,6 +1288,8 @@ private Format(Builder builder) { encoderPadding = in.readInt(); // Text specific. accessibilityChannel = in.readInt(); + // Thumbnail Tile specific + tileLayout = in.createIntArray(); // Provided by source. // Encrypted content must always have a non-null exoMediaCryptoType. exoMediaCryptoType = drmInitData != null ? UnsupportedMediaCrypto.class : null; diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 13cf6b18c3a..7d9e9111029 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -153,6 +153,11 @@ public static boolean isVideo(@Nullable String mimeType) { return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType)); } + /** Returns whether the given string is an image MIME type. */ + public static boolean isImage(@Nullable String mimeType) { + return BASE_TYPE_IMAGE.equals(getTopLevelType(mimeType)); + } + /** * Returns whether the given string is a text MIME type, including known text types that use * "application" as their base type. diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 508cfd4bff6..775eccc5596 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -48,6 +48,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -805,6 +806,15 @@ protected Format buildFormat( accessibilityChannel = parseCea708AccessibilityChannel(accessibilityDescriptors); } formatBuilder.setAccessibilityChannel(accessibilityChannel); + } else if (MimeTypes.isImage(containerMimeType) && roleFlags == C.ROLE_FLAG_THUMBNAIL_TILE) { + formatBuilder.setWidth(width).setHeight(height); + String tileLayoutStr = Objects.requireNonNull(essentialProperties.get(0).value); + String[] tileLayoutArr = tileLayoutStr.split("x"); + if (tileLayoutArr.length != 2) { + Log.e(TAG, "Tile layout malformed: " + tileLayoutStr); + } else { + formatBuilder.setTileLayout(Integer.parseInt(tileLayoutArr[0]), Integer.parseInt(tileLayoutArr[1])); + } } return formatBuilder.build(); @@ -1473,6 +1483,8 @@ protected int parseRoleFlagsFromProperties(List accessibilityDescrip Descriptor descriptor = accessibilityDescriptors.get(i); if ("http://dashif.org/guidelines/trickmode".equalsIgnoreCase(descriptor.schemeIdUri)) { result |= C.ROLE_FLAG_TRICK_PLAY; + } else if ("http://dashif.org/guidelines/thumbnail_tile".equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= C.ROLE_FLAG_THUMBNAIL_TILE; } } return result; From 82b72a6106a55d21c49a5daa3527bd023ac4ffe4 Mon Sep 17 00:00:00 2001 From: Enson Choy Date: Tue, 21 Sep 2021 13:00:12 +0800 Subject: [PATCH 26/42] Set tile layout to null as placeholder Thumbnail tile will not support parcel as argument in format() --- .../src/main/java/com/google/android/exoplayer2/Format.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Format.java b/library/common/src/main/java/com/google/android/exoplayer2/Format.java index d543d5dbe08..cf0dbb3a1b1 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Format.java @@ -1288,8 +1288,9 @@ private Format(Builder builder) { encoderPadding = in.readInt(); // Text specific. accessibilityChannel = in.readInt(); - // Thumbnail Tile specific - tileLayout = in.createIntArray(); + // @deprecated Thumbnail Tile specific + // act as placeholder + tileLayout = null; // Provided by source. // Encrypted content must always have a non-null exoMediaCryptoType. exoMediaCryptoType = drmInitData != null ? UnsupportedMediaCrypto.class : null; From 71f459b0fe23e46e5f3a1ed207939349d68ba055 Mon Sep 17 00:00:00 2001 From: enson-choy <88302346+enson-choy@users.noreply.github.com> Date: Wed, 8 Dec 2021 15:58:22 +0800 Subject: [PATCH 27/42] Feat: Parse MP4 PRFT box (#5) * Init PRFT info * Parse PRFT box to listener * Add PRFT unit test * Individual listener for PRFT events * Remove dependency * Use int to store PRFT types --- .../exoplayer2/ProducerReferenceTimeBox.java | 93 +++++++++++++ .../ProducerReferenceTimeBoxHandler.java | 8 ++ .../exoplayer2/ProducerReferenceTimeInfo.java | 123 ++++++++++++++++++ .../source/MediaSourceEventListener.java | 17 +++ .../source/dash/DashChunkSource.java | 4 +- .../source/dash/DashMediaPeriod.java | 7 +- .../source/dash/DefaultDashChunkSource.java | 26 ++-- .../source/dash/manifest/AdaptationSet.java | 11 +- .../source/dash/manifest/DashManifest.java | 4 +- .../dash/manifest/DashManifestParser.java | 43 +++++- .../exoplayer2/source/dash/DashUtilTest.java | 4 +- .../dash/DefaultDashChunkSourceTest.java | 6 +- .../dash/manifest/DashManifestParserTest.java | 24 ++++ .../dash/manifest/DashManifestTest.java | 4 +- .../exoplayer2/extractor/mp4/Atom.java | 3 + .../extractor/mp4/FragmentedMp4Extractor.java | 40 +++++- .../FragmentedMp4ExtractorNoSniffingTest.java | 19 ++- .../mp4/FragmentedMp4ExtractorTest.java | 3 +- .../hls/DefaultHlsExtractorFactory.java | 3 +- .../smoothstreaming/DefaultSsChunkSource.java | 3 +- .../mp4/sample_fragmented_prft.mp4.0.dump | 9 ++ .../mp4/sample_fragmented_prft.mp4.1.dump | 9 ++ .../mp4/sample_fragmented_prft.mp4.2.dump | 9 ++ .../mp4/sample_fragmented_prft.mp4.3.dump | 9 ++ ...le_fragmented_prft.mp4.unknown_length.dump | 9 ++ .../media/mp4/sample_fragmented_prft.mp4 | Bin 0 -> 235582 bytes .../src/test/assets/media/mpd/sample_mpd_live | 3 + 27 files changed, 462 insertions(+), 31 deletions(-) create mode 100644 library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeBox.java create mode 100644 library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeBoxHandler.java create mode 100644 library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeInfo.java create mode 100644 testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.0.dump create mode 100644 testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.1.dump create mode 100644 testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.2.dump create mode 100644 testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.3.dump create mode 100644 testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.unknown_length.dump create mode 100644 testdata/src/test/assets/media/mp4/sample_fragmented_prft.mp4 diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeBox.java b/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeBox.java new file mode 100644 index 00000000000..fcbc4247eba --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeBox.java @@ -0,0 +1,93 @@ +package com.google.android.exoplayer2; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Util; + +public final class ProducerReferenceTimeBox { + /** + * PRFT type related + * Use int to align with ExoPlayer practices + */ + public static final int TYPE_ENCODER = 0; + public static final int TYPE_CAPTURED = 1; + + /** + * NTP related + */ + + // Baseline NTP time if bit-0=0 is 7-Feb-2036 @ 06:28:16 UTC + private static final long msb0baseTime = 2085978496000L; + + // Baseline NTP time if bit-0=1 is 1-Jan-1900 @ 01:00:00 UTC + private static final long msb1baseTime = -2208988800000L; + + private final long referenceTrackId; + private final long ntpTimestamp; + private final long mediaTime; + private final int type; + + public ProducerReferenceTimeBox(long referenceTrackId, long ntpTimestamp, long mediaTime, int type) { + this.referenceTrackId = referenceTrackId; + this.ntpTimestamp = ntpTimestamp; + this.mediaTime = mediaTime; + this.type = type; + } + + /** + * Clone from Apache Commons Net NTP TimeStamp getUTCTime() to avoid importing extra dependency. + * + * @return the number of milliseconds since January 1, 1970, 00:00:00 GMT + * represented by this NTP timestamp value. + */ + public long getUTCTime() { + final long seconds = (ntpTimestamp >>> 32) & 0xffffffffL; // high-order 32-bits + long fraction = ntpTimestamp & 0xffffffffL; // low-order 32-bits + + // Use round-off on fractional part to preserve going to lower precision + fraction = Math.round(1000D * fraction / 0x100000000L); + + /* + * If the most significant bit (MSB) on the seconds field is set we use + * a different time base. The following text is a quote from RFC-2030 (SNTP v4): + * + * If bit 0 is set, the UTC time is in the range 1968-2036 and UTC time + * is reckoned from 0h 0m 0s UTC on 1 January 1900. If bit 0 is not set, + * the time is in the range 2036-2104 and UTC time is reckoned from + * 6h 28m 16s UTC on 7 February 2036. + */ + final long msb = seconds & 0x80000000L; + if (msb == 0) { + // use base: 7-Feb-2036 @ 06:28:16 UTC + return msb0baseTime + (seconds * 1000) + fraction; + } + // use base: 1-Jan-1900 @ 01:00:00 UTC + return msb1baseTime + (seconds * 1000) + fraction; + } + + public long getReferenceTrackId() { + return referenceTrackId; + } + + public long getMediaTime() { + return mediaTime; + } + + public int getType() { + return type; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ProducerReferenceTimeBox other = (ProducerReferenceTimeBox) obj; + return Util.areEqual(this.referenceTrackId, other.referenceTrackId) + && Util.areEqual(this.ntpTimestamp, other.ntpTimestamp) + && Util.areEqual(this.mediaTime, other.mediaTime) + && Util.areEqual(this.type, other.type); + } +} diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeBoxHandler.java b/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeBoxHandler.java new file mode 100644 index 00000000000..d366af68f0e --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeBoxHandler.java @@ -0,0 +1,8 @@ +package com.google.android.exoplayer2; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Util; + +public interface ProducerReferenceTimeBoxHandler { + void onPrftParsed(ProducerReferenceTimeBox box); +} diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeInfo.java new file mode 100644 index 00000000000..7fd073a265f --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeInfo.java @@ -0,0 +1,123 @@ +package com.google.android.exoplayer2; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Util; + +import java.util.Date; + +public final class ProducerReferenceTimeInfo { + private int id; + private String type = ""; + private String source = "manifest"; + private Date wallClockAnchor = new Date(); + private Double presentationTimeAnchor = 0.0; + private boolean inband = false; + private UTCTiming utcTiming = new UTCTiming(); + private long timescale; + + public ProducerReferenceTimeInfo() { + } + + public ProducerReferenceTimeInfo(int id, String encoder, Date wallClockAnchor, Double presentationTimeAnchor, boolean inband, UTCTiming utcTiming, long timescale) { + this.id = id; + this.type = encoder; + this.wallClockAnchor = wallClockAnchor; + this.presentationTimeAnchor = presentationTimeAnchor; + this.inband = inband; + this.utcTiming = utcTiming; + this.timescale = timescale; + } + + public int getId() { + return id; + } + + public String getType() { + return type; + } + + public Date getWallClockAnchor() { + return wallClockAnchor; + } + + public Double getPresentationTimeAnchor() { + return presentationTimeAnchor; + } + + public long getTimescale() { + return timescale; + } + + public boolean isInband() { + return inband; + } + + public void setWallClockAnchor(Date wallClockAnchor) { + this.wallClockAnchor = wallClockAnchor; + } + + public void setPresentationTimeAnchor(Double presentationTimeAnchor) { + this.presentationTimeAnchor = presentationTimeAnchor; + } + + public void setTimescale(long timescale) { + this.timescale = timescale; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ProducerReferenceTimeInfo other = (ProducerReferenceTimeInfo) obj; + return Util.areEqual(this.id, other.id) + && Util.areEqual(this.type, other.type) + && Util.areEqual(this.wallClockAnchor, other.wallClockAnchor) + && Util.areEqual(this.presentationTimeAnchor, other.presentationTimeAnchor) + && Util.areEqual(this.inband, other.inband) + && Util.areEqual(this.utcTiming, other.utcTiming) + && Util.areEqual(this.timescale, other.timescale) + && Util.areEqual(this.source, other.source); + } + + public static class UTCTiming { + String schemeIdUri = ""; + String value = ""; + + public UTCTiming() { + } + + public UTCTiming(String schemeIdUri, String value) { + this.schemeIdUri = schemeIdUri; + this.value = value; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ProducerReferenceTimeInfo.UTCTiming other = (ProducerReferenceTimeInfo.UTCTiming) obj; + return Util.areEqual(this.schemeIdUri, other.schemeIdUri) + && Util.areEqual(this.value, other.value); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 39fd6d53a9f..6d8a291d8bf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -23,6 +23,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ProducerReferenceTimeBox; +import com.google.android.exoplayer2.ProducerReferenceTimeBoxHandler; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -139,6 +141,12 @@ default void onUpstreamDiscarded( default void onDownstreamFormatChanged( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {} + /** + * Called when MP4 PRFT box is parsed. + * @param box The {@link ProducerReferenceTimeBox} extracted from MP4 PRFT box + */ + default void onPrftParsed(ProducerReferenceTimeBox box) {} + /** Dispatches events to {@link MediaSourceEventListener MediaSourceEventListeners}. */ class EventDispatcher { @@ -467,6 +475,15 @@ public void downstreamFormatChanged(MediaLoadData mediaLoadData) { } } + public void prftParsed(ProducerReferenceTimeBox box) { + for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { + MediaSourceEventListener listener = listenerAndHandler.listener; + postOrRun( + listenerAndHandler.handler, + () -> listener.onPrftParsed(box)); + } + } + private long adjustMediaTime(long mediaTimeUs) { long mediaTimeMs = C.usToMs(mediaTimeUs); return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index d93915f761c..146d3be4fca 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -18,6 +18,7 @@ import android.os.SystemClock; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ProducerReferenceTimeBoxHandler; import com.google.android.exoplayer2.source.chunk.ChunkSource; import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; @@ -59,7 +60,8 @@ DashChunkSource createDashChunkSource( boolean enableEventMessageTrack, List closedCaptionFormats, @Nullable PlayerTrackEmsgHandler playerEmsgHandler, - @Nullable TransferListener transferListener); + @Nullable TransferListener transferListener, + @Nullable ProducerReferenceTimeBoxHandler prftHandler); } /** diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 6cf10b35787..c80c12c1d1c 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -24,6 +24,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ProducerReferenceTimeBoxHandler; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -762,6 +763,9 @@ private ChunkSampleStream buildSampleStream( manifest.dynamic && enableEventMessageTrack ? playerEmsgHandler.newPlayerTrackEmsgHandler() : null; + + ProducerReferenceTimeBoxHandler prftHandler = mediaSourceEventDispatcher::prftParsed; + DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( manifestLoaderErrorThrower, @@ -774,7 +778,8 @@ private ChunkSampleStream buildSampleStream( enableEventMessageTrack, embeddedClosedCaptionTrackFormats, trackPlayerEmsgHandler, - transferListener); + transferListener, + prftHandler); ChunkSampleStream stream = new ChunkSampleStream<>( trackGroupInfo.trackType, diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 22255899506..1a1e9c1bda0 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -24,6 +24,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ProducerReferenceTimeBoxHandler; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.Extractor; @@ -90,7 +91,8 @@ public DashChunkSource createDashChunkSource( boolean enableEventMessageTrack, List closedCaptionFormats, @Nullable PlayerTrackEmsgHandler playerEmsgHandler, - @Nullable TransferListener transferListener) { + @Nullable TransferListener transferListener, + @Nullable ProducerReferenceTimeBoxHandler prftHandler) { DataSource dataSource = dataSourceFactory.createDataSource(); if (transferListener != null) { dataSource.addTransferListener(transferListener); @@ -107,7 +109,8 @@ public DashChunkSource createDashChunkSource( maxSegmentsPerLoad, enableEventMessageTrack, closedCaptionFormats, - playerEmsgHandler); + playerEmsgHandler, + prftHandler); } } @@ -127,7 +130,6 @@ public DashChunkSource createDashChunkSource( private int periodIndex; @Nullable private IOException fatalError; private boolean missingLastSegment; - /** * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. * @param manifest The initial manifest. @@ -159,7 +161,8 @@ public DefaultDashChunkSource( int maxSegmentsPerLoad, boolean enableEventMessageTrack, List closedCaptionFormats, - @Nullable PlayerTrackEmsgHandler playerTrackEmsgHandler) { + @Nullable PlayerTrackEmsgHandler playerTrackEmsgHandler, + @Nullable ProducerReferenceTimeBoxHandler handler) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; this.adaptationSetIndices = adaptationSetIndices; @@ -184,7 +187,8 @@ public DefaultDashChunkSource( representation, enableEventMessageTrack, closedCaptionFormats, - playerTrackEmsgHandler); + playerTrackEmsgHandler, + handler); } } @@ -670,7 +674,8 @@ protected static final class RepresentationHolder { Representation representation, boolean enableEventMessageTrack, List closedCaptionFormats, - @Nullable TrackOutput playerEmsgTrackOutput) { + @Nullable TrackOutput playerEmsgTrackOutput, + @Nullable ProducerReferenceTimeBoxHandler handler) { this( periodDurationUs, representation, @@ -679,7 +684,8 @@ protected static final class RepresentationHolder { representation, enableEventMessageTrack, closedCaptionFormats, - playerEmsgTrackOutput), + playerEmsgTrackOutput, + handler), /* segmentNumShift= */ 0, representation.getIndex()); } @@ -807,7 +813,8 @@ private static ChunkExtractor createChunkExtractor( Representation representation, boolean enableEventMessageTrack, List closedCaptionFormats, - @Nullable TrackOutput playerEmsgTrackOutput) { + @Nullable TrackOutput playerEmsgTrackOutput, + @Nullable ProducerReferenceTimeBoxHandler handler) { String containerMimeType = representation.format.containerMimeType; Extractor extractor; if (MimeTypes.isText(containerMimeType)) { @@ -831,7 +838,8 @@ private static ChunkExtractor createChunkExtractor( /* timestampAdjuster= */ null, /* sideloadedTrack= */ null, closedCaptionFormats, - playerEmsgTrackOutput); + playerEmsgTrackOutput, + handler); } return new BundledChunkExtractor(extractor, trackType, representation.format); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java index b0689eeb11f..3434c00aa9b 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.source.dash.manifest; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.ProducerReferenceTimeInfo; + import java.util.Collections; import java.util.List; @@ -56,6 +59,9 @@ public class AdaptationSet { /** Supplemental properties in the adaptation set. */ public final List supplementalProperties; + /** Producer Reference Time Info */ + @Nullable public final ProducerReferenceTimeInfo info; + /** * @param id A non-negative identifier for the adaptation set that's unique in the scope of its * containing period, or {@link #ID_UNSET} if not specified. @@ -65,6 +71,7 @@ public class AdaptationSet { * @param accessibilityDescriptors Accessibility descriptors in the adaptation set. * @param essentialProperties Essential properties in the adaptation set. * @param supplementalProperties Supplemental properties in the adaptation set. + * @param producerReferenceTimeInfo Producer Reference Time Info */ public AdaptationSet( int id, @@ -72,12 +79,14 @@ public AdaptationSet( List representations, List accessibilityDescriptors, List essentialProperties, - List supplementalProperties) { + List supplementalProperties, + @Nullable ProducerReferenceTimeInfo producerReferenceTimeInfo) { this.id = id; this.type = type; this.representations = Collections.unmodifiableList(representations); this.accessibilityDescriptors = Collections.unmodifiableList(accessibilityDescriptors); this.essentialProperties = Collections.unmodifiableList(essentialProperties); this.supplementalProperties = Collections.unmodifiableList(supplementalProperties); + this.info = producerReferenceTimeInfo; } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index de2911af930..91df7e2084c 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -18,6 +18,7 @@ import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ProducerReferenceTimeInfo; import com.google.android.exoplayer2.offline.FilterableManifest; import com.google.android.exoplayer2.offline.StreamKey; import java.util.ArrayList; @@ -252,7 +253,8 @@ private static ArrayList copyAdaptationSets( copyRepresentations, adaptationSet.accessibilityDescriptors, adaptationSet.essentialProperties, - adaptationSet.supplementalProperties)); + adaptationSet.supplementalProperties, + adaptationSet.info)); } while(key.periodIndex == periodIndex); // Add back the last key which doesn't belong to the period being processed keys.addFirst(key); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 775eccc5596..a5b7f11d290 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.ProducerReferenceTimeInfo; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; @@ -47,6 +48,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Objects; import java.util.UUID; @@ -405,6 +407,7 @@ protected AdaptationSet parseAdaptationSet( ArrayList essentialProperties = new ArrayList<>(); ArrayList supplementalProperties = new ArrayList<>(); List representationInfos = new ArrayList<>(); + @Nullable ProducerReferenceTimeInfo producerReferenceTimeInfo = null; boolean seenFirstBaseUrl = false; do { @@ -495,11 +498,18 @@ protected AdaptationSet parseAdaptationSet( inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp, "Label")) { label = parseLabel(xpp); + } else if (XmlPullParserUtil.isStartTag(xpp, "ProducerReferenceTime")) { + producerReferenceTimeInfo = parsePrft(xpp); } else if (XmlPullParserUtil.isStartTag(xpp)) { parseAdaptationSetChild(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "AdaptationSet")); + // Update timescale when segment is available + if (producerReferenceTimeInfo != null && segmentBase != null) { + producerReferenceTimeInfo.setTimescale(segmentBase.timescale); + } + // Build the representations. List representations = new ArrayList<>(representationInfos.size()); for (int i = 0; i < representationInfos.size(); i++) { @@ -518,7 +528,8 @@ protected AdaptationSet parseAdaptationSet( representations, accessibilityDescriptors, essentialProperties, - supplementalProperties); + supplementalProperties, + producerReferenceTimeInfo); } protected AdaptationSet buildAdaptationSet( @@ -527,14 +538,16 @@ protected AdaptationSet buildAdaptationSet( List representations, List accessibilityDescriptors, List essentialProperties, - List supplementalProperties) { + List supplementalProperties, + @Nullable ProducerReferenceTimeInfo producerReferenceTimeInfo) { return new AdaptationSet( id, contentType, representations, accessibilityDescriptors, essentialProperties, - supplementalProperties); + supplementalProperties, + producerReferenceTimeInfo); } protected int parseContentType(XmlPullParser xpp) { @@ -1544,6 +1557,30 @@ protected int parseTvaAudioPurposeCsValue(@Nullable String value) { } } + protected ProducerReferenceTimeInfo parsePrft(XmlPullParser xpp) throws IOException, XmlPullParserException { + int id = parseInt(xpp, "id", 0); + String type = parseString(xpp, "type", ""); + Date wallClockTime = new Date(parseDateTime(xpp, "wallClockTime", 0)); + double presentationTime = (double) parseLong(xpp, "presentationTime", 0); + boolean inband = parseString(xpp, "inband", "").equals("true"); + + String schemeIdUri = ""; + String value = ""; + + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "UTCTiming")) { + schemeIdUri = parseString(xpp, "schemeIdUri", ""); + value = parseString(xpp, "value", ""); + } else { + maybeSkipTag(xpp); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "ProducerReferenceTime")); + + // Initiate timescale as 0, and update it later + return new ProducerReferenceTimeInfo(id, type, wallClockTime, presentationTime, inband, new ProducerReferenceTimeInfo.UTCTiming(schemeIdUri, value), 0); + } + // Utility methods. /** diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java index 188d1b2a180..228a5eac9fa 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java @@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ProducerReferenceTimeInfo; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; @@ -76,7 +77,8 @@ private static AdaptationSet newAdaptationSet(Representation... representations) Arrays.asList(representations), /* accessibilityDescriptors= */ Collections.emptyList(), /* essentialProperties= */ Collections.emptyList(), - /* supplementalProperties= */ Collections.emptyList()); + /* supplementalProperties= */ Collections.emptyList(), + new ProducerReferenceTimeInfo()); } private static Representation newRepresentation(DrmInitData drmInitData) { diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSourceTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSourceTest.java index 93700d5ec3e..06cef103609 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSourceTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSourceTest.java @@ -69,7 +69,8 @@ public void getNextChunk_forLowLatencyManifest_setsCorrectMayNotLoadAtFullNetwor /* maxSegmentsPerLoad= */ 1, /* enableEventMessageTrack= */ false, /* closedCaptionFormats */ ImmutableList.of(), - /* playerTrackEmsgHandler= */ null); + /* playerTrackEmsgHandler= */ null, + null); long nowInPeriodUs = C.msToUs(nowMs - manifest.availabilityStartTimeMs); ChunkHolder output = new ChunkHolder(); @@ -115,7 +116,8 @@ public void getNextChunk_forVodManifest_doesNotSetMayNotLoadAtFullNetworkSpeedFl /* maxSegmentsPerLoad= */ 1, /* enableEventMessageTrack= */ false, /* closedCaptionFormats */ ImmutableList.of(), - /* playerTrackEmsgHandler= */ null); + /* playerTrackEmsgHandler= */ null, + null); ChunkHolder output = new ChunkHolder(); chunkSource.getNextChunk( diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index c82f2012678..e9058cc52ed 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ProducerReferenceTimeInfo; import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement; import com.google.android.exoplayer2.testutil.TestUtil; @@ -31,7 +32,9 @@ import com.google.common.base.Charsets; import java.io.IOException; import java.io.StringReader; +import java.time.Instant; import java.util.Collections; +import java.util.Date; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -191,6 +194,27 @@ public void parseMediaPresentationDescription_programInformation() throws IOExce assertThat(manifest.programInformation).isEqualTo(expectedProgramInformation); } + @Test + public void parseProducerReferenceTimeInfo() throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_LIVE)); + + ProducerReferenceTimeInfo info = new ProducerReferenceTimeInfo( + 0, + "encoder", + Date.from(Instant.parse("2011-03-01T00:00:00.000Z")), + 0.0, + true, + new ProducerReferenceTimeInfo.UTCTiming( + "urn:mpeg:dash:utc:http-iso:2014", + "https://time.akamai.com/?iso&ms"), + 1000); + assertThat(manifest.getPeriod(0).adaptationSets.get(0).info).isEqualTo(info); + } + @Test public void parseMediaPresentationDescription_labels() throws IOException { DashManifestParser parser = new DashManifestParser(); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java index 5f98f19be8f..760b7506deb 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ProducerReferenceTimeInfo; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; import java.util.Arrays; @@ -262,6 +263,7 @@ private static AdaptationSet newAdaptationSet(int seed, Representation... repres Arrays.asList(representations), /* accessibilityDescriptors= */ Collections.emptyList(), /* essentialProperties= */ Collections.emptyList(), - /* supplementalProperties= */ Collections.emptyList()); + /* supplementalProperties= */ Collections.emptyList(), + new ProducerReferenceTimeInfo()); } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 95cd1e2c179..d2d885b2d57 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -397,6 +397,9 @@ @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_twos = 0x74776f73; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_prft = 0x70726674; + public final int type; public Atom(int type) { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index e14381c5640..f9e57914f9e 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -29,6 +29,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.ProducerReferenceTimeBox; +import com.google.android.exoplayer2.ProducerReferenceTimeBoxHandler; import com.google.android.exoplayer2.audio.Ac4Util; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; @@ -128,6 +130,7 @@ public class FragmentedMp4Extractor implements Extractor { // Workarounds. @Flags private final int flags; @Nullable private final Track sideloadedTrack; + @Nullable private final ProducerReferenceTimeBoxHandler prftHandler; // Sideloaded data. private final List closedCaptionFormats; @@ -194,7 +197,7 @@ public FragmentedMp4Extractor(@Flags int flags) { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) { - this(flags, timestampAdjuster, /* sideloadedTrack= */ null, Collections.emptyList()); + this(flags, timestampAdjuster, /* sideloadedTrack= */ null, Collections.emptyList(), null); } /** @@ -206,8 +209,9 @@ public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster time public FragmentedMp4Extractor( @Flags int flags, @Nullable TimestampAdjuster timestampAdjuster, - @Nullable Track sideloadedTrack) { - this(flags, timestampAdjuster, sideloadedTrack, Collections.emptyList()); + @Nullable Track sideloadedTrack, + @Nullable ProducerReferenceTimeBoxHandler prftHandler) { + this(flags, timestampAdjuster, sideloadedTrack, Collections.emptyList(), prftHandler); } /** @@ -222,13 +226,15 @@ public FragmentedMp4Extractor( @Flags int flags, @Nullable TimestampAdjuster timestampAdjuster, @Nullable Track sideloadedTrack, - List closedCaptionFormats) { + List closedCaptionFormats, + @Nullable ProducerReferenceTimeBoxHandler prftHandler) { this( flags, timestampAdjuster, sideloadedTrack, closedCaptionFormats, - /* additionalEmsgTrackOutput= */ null); + /* additionalEmsgTrackOutput= */ null, + prftHandler); } /** @@ -247,12 +253,14 @@ public FragmentedMp4Extractor( @Nullable TimestampAdjuster timestampAdjuster, @Nullable Track sideloadedTrack, List closedCaptionFormats, - @Nullable TrackOutput additionalEmsgTrackOutput) { + @Nullable TrackOutput additionalEmsgTrackOutput, + @Nullable ProducerReferenceTimeBoxHandler prftHandler) { this.flags = flags; this.timestampAdjuster = timestampAdjuster; this.sideloadedTrack = sideloadedTrack; this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; + this.prftHandler = prftHandler; eventMessageEncoder = new EventMessageEncoder(); atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); @@ -470,6 +478,23 @@ private void onLeafAtomRead(LeafAtom leaf, long inputPosition) throws ParserExce haveOutputSeekMap = true; } else if (leaf.type == Atom.TYPE_emsg) { onEmsgLeafAtomRead(leaf.data); + } else if (leaf.type == Atom.TYPE_prft) { + parsePrft(leaf.data); + } + } + + private void parsePrft(ParsableByteArray atom) { + atom.setPosition(Atom.HEADER_SIZE); + int fullAtom = atom.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + int flags = Atom.parseFullAtomFlags(fullAtom); + int type = (flags & 24) == 24 ? ProducerReferenceTimeBox.TYPE_CAPTURED : ProducerReferenceTimeBox.TYPE_ENCODER; + long referenceTrackId = atom.readUnsignedInt(); + long ntpTimestamp = atom.readLong(); + long mediaTime = version == 0 ? atom.readUnsignedInt() : atom.readUnsignedLongToLong(); + + if (prftHandler != null) { + prftHandler.onPrftParsed(new ProducerReferenceTimeBox(referenceTrackId, ntpTimestamp, mediaTime, type)); } } @@ -1524,7 +1549,8 @@ private static boolean shouldParseLeafAtom(int atom) { || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst || atom == Atom.TYPE_mehd - || atom == Atom.TYPE_emsg; + || atom == Atom.TYPE_emsg + || atom == Atom.TYPE_prft; } /** Returns whether the extractor should decode a container atom with type {@code atom}. */ diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorNoSniffingTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorNoSniffingTest.java index 969c9b8d5a7..6bfd1f9175f 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorNoSniffingTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorNoSniffingTest.java @@ -17,7 +17,10 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ProducerReferenceTimeBox; +import com.google.android.exoplayer2.ProducerReferenceTimeBoxHandler; import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import java.util.List; import org.junit.Test; @@ -59,8 +62,22 @@ public void sampleWithSideLoadedTrack() throws Exception { ExtractorAsserts.assertBehavior( () -> new FragmentedMp4Extractor( - /* flags= */ 0, /* timestampAdjuster= */ null, sideloadedTrack), + /* flags= */ 0, /* timestampAdjuster= */ null, sideloadedTrack, null), "media/mp4/sample_fragmented_sideloaded_track.mp4", simulationConfig); } + + @Test + public void samplePrftBox() throws Exception { + ProducerReferenceTimeBoxHandler handler = box -> { + Assertions.checkArgument(box.getReferenceTrackId() == 2); + Assertions.checkArgument(box.getMediaTime() == 14168880443813L); + Assertions.checkArgument(box.getUTCTime() == 1638758891266L); + Assertions.checkArgument(box.getType() == ProducerReferenceTimeBox.TYPE_ENCODER); + }; + ExtractorAsserts.assertBehavior( + () -> new FragmentedMp4Extractor(0, null, null, handler), + "media/mp4/sample_fragmented_prft.mp4", + simulationConfig); + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index a9d2397ca71..0b8c097e1c3 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -128,6 +128,7 @@ private static ExtractorFactory getExtractorFactory(final List closedCap /* flags= */ 0, /* timestampAdjuster= */ null, /* sideloadedTrack= */ null, - closedCaptionFormats); + closedCaptionFormats, + null); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index a1251810ad6..8d1d00c4432 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -227,7 +227,8 @@ private static FragmentedMp4Extractor createFragmentedMp4Extractor( /* flags= */ isFmp4Variant(format) ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0, timestampAdjuster, /* sideloadedTrack= */ null, - muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); + muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList(), + null); } /** Returns true if this {@code format} represents a 'variant' track (i.e. the main one). */ diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index be9aed4393e..ae84cfa9a38 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -122,7 +122,8 @@ public DefaultSsChunkSource( FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, /* timestampAdjuster= */ null, - track); + track, + null); chunkExtractors[i] = new BundledChunkExtractor(extractor, streamElement.type, format); } } diff --git a/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.0.dump b/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.0.dump new file mode 100644 index 00000000000..4ef14894f16 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.0.dump @@ -0,0 +1,9 @@ +seekMap: + isSeekable = true + duration = 1416890417114 + getPosition(0) = [[timeUs=1416888111114, position=112]] + getPosition(1) = [[timeUs=1416888111114, position=112]] + getPosition(708445208557) = [[timeUs=1416888111114, position=112]] + getPosition(1416890417114) = [[timeUs=1416888111114, position=112]] +numberOfTracks = 0 +tracksEnded = false diff --git a/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.1.dump b/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.1.dump new file mode 100644 index 00000000000..4ef14894f16 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.1.dump @@ -0,0 +1,9 @@ +seekMap: + isSeekable = true + duration = 1416890417114 + getPosition(0) = [[timeUs=1416888111114, position=112]] + getPosition(1) = [[timeUs=1416888111114, position=112]] + getPosition(708445208557) = [[timeUs=1416888111114, position=112]] + getPosition(1416890417114) = [[timeUs=1416888111114, position=112]] +numberOfTracks = 0 +tracksEnded = false diff --git a/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.2.dump b/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.2.dump new file mode 100644 index 00000000000..4ef14894f16 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.2.dump @@ -0,0 +1,9 @@ +seekMap: + isSeekable = true + duration = 1416890417114 + getPosition(0) = [[timeUs=1416888111114, position=112]] + getPosition(1) = [[timeUs=1416888111114, position=112]] + getPosition(708445208557) = [[timeUs=1416888111114, position=112]] + getPosition(1416890417114) = [[timeUs=1416888111114, position=112]] +numberOfTracks = 0 +tracksEnded = false diff --git a/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.3.dump b/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.3.dump new file mode 100644 index 00000000000..4ef14894f16 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.3.dump @@ -0,0 +1,9 @@ +seekMap: + isSeekable = true + duration = 1416890417114 + getPosition(0) = [[timeUs=1416888111114, position=112]] + getPosition(1) = [[timeUs=1416888111114, position=112]] + getPosition(708445208557) = [[timeUs=1416888111114, position=112]] + getPosition(1416890417114) = [[timeUs=1416888111114, position=112]] +numberOfTracks = 0 +tracksEnded = false diff --git a/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.unknown_length.dump b/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.unknown_length.dump new file mode 100644 index 00000000000..4ef14894f16 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/mp4/sample_fragmented_prft.mp4.unknown_length.dump @@ -0,0 +1,9 @@ +seekMap: + isSeekable = true + duration = 1416890417114 + getPosition(0) = [[timeUs=1416888111114, position=112]] + getPosition(1) = [[timeUs=1416888111114, position=112]] + getPosition(708445208557) = [[timeUs=1416888111114, position=112]] + getPosition(1416890417114) = [[timeUs=1416888111114, position=112]] +numberOfTracks = 0 +tracksEnded = false diff --git a/testdata/src/test/assets/media/mp4/sample_fragmented_prft.mp4 b/testdata/src/test/assets/media/mp4/sample_fragmented_prft.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..4d1ef38671c97d4e2d72e2ecda3f92bbb5497b33 GIT binary patch literal 235582 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%0D}DE0wlO0E@bfU{s0 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

1AYBjyuJYVo>=thoAUnW5VR}?HJ zh{}$L%-;#f%8d`~A=FcO)gk1%BazV`LzIwrQJ%^>3-Qa+BaJT}rzIY-3jc99 zoSbrO1moHrJ!z}ZD=46mQ^pm=7pOvt`@#}!Yp!BA3~=`Wn3%lRZ^iBxw~0`o zq>cc)3dsx5Ask!B_kQHi^9_nHus@1X-Gz~qp7Q^etGzwI)XqXql`;CKFT}|^9W-?z zznj3hwX2h`eA>ZxB=m!&SBSH|{?8To;aRHXn#`Vc+I;HAjj|+jH zGBy4Wb*i53O8$`lZSc!`2{A=qFdIysO&eOqZOYo*%mg&bVL#m&w}?s0cqO0VYXuYQ z=-{w_xK_?n+ zi)?5GOo;D#+ddLzD6x-I4&;`G+$Vv_a)0Lqm>}m3y+lR5K2gtUwCGSkE7g`n95WO8 zXwveuvy6w}8J8IDZOAW=FsPb^7Y8Dzpe5)~BwTi5!NW*z zyflC#HCMJ|Y|k?Ez2?%!o-e)YMzT&Q2zC=^>!9x3#XT)Fn$on#*6@}d6{nwP2))<8 zhY=601;TrUSPA6(qS|Bii4kFB?6EXj@Dv2yJnaB?{AZ>IYin!BghE(A9CR90*m<>d za`H^Jz(+zerLA$bW}MzgM;GOmo~^N2S7Pb9rkpilif^4N{i42roWM~}Z+(*(CY>ng z+4K~_rWTVVtuhde#WGt1q_c1p{=?N;`QD4jeLsqZxqqdn6#@g{ zN8I7il?VRdNjUgVrd(uIRD&niVcmc{a*LT9zurm4ke!uHrQE#2Zxu|F@r}tJ=-|7u z>LtFQsBM?&S}*c{Ap(@xc)|a~uVDUaGa8O!=>g)uw~U0ph>=II;#1mpkX|MD3;nVi zRfVkYd|&2k>{_9w)k8Fl04!zpSONOvMylYUy*Iju-JJJVaIY~CWotP!fDgVZ0q2n9 zOK(c~aSYXR(8&T+c(b}yd756Tl#$53OWea2m7BNISyg=nK6(S;l~1t$mUAx{RvtUx z3^O3Z(J)n)#kn*-e&NrbR#)V@A{_aj!mpBN2O8O9L9$%ZZ_?em^gXL#4C4rE!qK{t zvV6`eCW>mh=mcP4no^ovfW{^k_<0L)r2Skk z7f-nOkWhxjA&f3DCqj3?J>~~dP5~xP*}MVW8wVs5b#98y3gN-P8G6g7KIf)kOg3Qi zEXYx_&v1N(LKF=RSLh+W;|+TNqGFb3zR+;~y1l+f$*RXieoWsVKJXy&?yhq!_7`tr zsBa`2s{ALpnRDk{*}8ZYo&ze39d4@VGtFRp+dD*3#}!yi+n6Be=#E*Aoa6S1MMro+ zKoH*N{VH)9T@1BUwz)jo5a41L!~6UrWovF%!%AX6gl%YoGlfY_);MY>t=NJ7m?}%- zgvC+W1GS7hoh>8h5Z233K=na6BOeOU%-~aYE4t&kK^H>AQ=TFPjgkdcIF$y0+ z)kfpK44n%F*~1l_9X%wRP7}CF<=0IYz>czUO~LmeX$?ViZVM%~PyPr)=5UOkE*(z| zH9cAPfLH5xTIFzX>k*t*Z0tZo>5qkUik!YTJ;4R$LAx0pPTtG8(?!{=0o@iIn$dxs zkq%nBZpxDnpf}A*pfQAAKWnAD&esxG{1fP1$ zam8678P&QdAYJ(>p ze}v^AJHS~~;c7NRo37H1DOpZ=BLt7M_HH!C^Khp%ru)`M>c;U9mU#%w`a2m$n&)DY z5>oy&N3MEG{gEK5>G&Y-j0yLm_b*MfsRAR_zf{fH_|m`ewx{VHd@H%=WhJAe3wr|< zHt<(ptV-Z_jDAn%)VH}nQTkbsk(!1b6$kcn-2UZsO?13+QYG%uqQpa4QGK8{K8R*~ zD{gI*=eAi8<>&(gA~9zh7Q(oNoT=Nd(5yL$N?7!4e(N%w4tI$uFPj+34ns@|u$hQ4 zT)2zjF!vU^ZII!MbM&@~6r?L!Uox-vfBQKJ)2dC2k*kCn!SbEi5x}6eOVmVU*g^UX zMYcbH@UV-(2f2=EO4;b|#+J?QEe(s*?Q~NH&1UkE3I>zH3!f)M^A_KOV`tE`J&r9h ztcVevEh7$N|1%8`&hE(uUSHnt2^_f)f%HBs+opU*8-Sv?>9S}m*Bu+iomtY6O ztLep<_eQe*rPE;oD+*e_3;!+H}16N#{ z*Dh%aSbI2S2f0Q`Cpk|>K3Kl7(DpC{@ak#>k~9OLd-@JBHS+Y3(4$)KET{Ui;KT`6 z3G^mARUHMj}vnBGGWJ*>7Ub54KV9 znzN&i-vQgQErb&#{&N~7Oi0lO*uIx}?37hZ^2_?8x&pPd)O&iK>B>plK1J|x75S~X z>zVE5TIR}yoc5CRE~m2~XS31vv?COF>q;aHn)?EM!E{8y-++L zF>x~+w2XV8%6B8W>RZQPUQcL52?nM)J>q&roI4JvTof9~?C!DpKeQ4VhC3JKQ7v-_ z0o6`62CR0lFCQuUSS@n3%$XU&eAZB#_wUe67hCw1ss*gKoY36bcbQbBcCks+aastC z-Z|AoBdJt_5+iWmaK?g<%Q29Q^p}A4vFA<=8fk3O6F`7U479=mer40^jv3ouON zKe3zxSo&~NSa}*3!C46{vGp!jw>^*15g_Ab!V`PS@sdPP;nFWGI1HO=`!#YSGBfx! z;%$vwNVwsBbKf1b(A?US<8PhH~@ivi2!;) zAZ*F=#CQ0c5(d0*9@H{DglKljslJz$qB}sRl`^ijH$NMdj+(4D~02)fdnT=ux=5UCjeg z4v0{&v2{_pS(}X*`X#7sT=-4~Q(6Y7s)$b)q~K8_`q~8s|HTXgd{xNSZ*w@$&T(aK zA`m376D4Gi#>`w@-VyFAAf*>2z2EdHC+2x3?8$0D!bKE$in5sn{{SUw$X*=%&(iO}LtQZ*3ftna1RMtD6O zgy7KN7RDz1JJrSG#ElwN?Po%ZA1lO#clJE75&Wu*;tXUS2TSeDn2Fc>88^}zHVfst z&?l42jNR8|HUMY{7GCF*|CxgsnwRq^5u6_t;(%R1IWTe`(A@%TZvku2R4e_ZHil*$ zJN$}YeC=yCZN;FKo1t7rV}H|P^$lUy{GbEeumhs;9ZzAv>SP@yoBZ!)X09%!d46@p2lG|rJcE3ScuUGD zlK|Y*2Y-}6Z;&XwFwDQ;_J&ThKF%DE@?~>*QCTWtzAB-jXab^|Tf2@;J5$wE9t%oN z1n)BM-dZV?u!Xr0akB&Cvt>x@Jt3q-YlLq2s7*)4Gun66pCdLn2%aP3f_0eVLp=de zjdWPx!*TQe3^0ir274W6t4D6oT3@Qlw}f=Z$BX=D38qIZn|voh^VhFrH}32TnZpHf zw{WJ6cA^W{=OIuXBav+ZZ?fOsjX+s7oP0X=43i1qzdJw7_sSK!4IiC!>xC4L#}7ag zb4l}#(ugNBPAmJUPgUYLwpdwgnn!7FlzK2Bz)f`hMiN}JHl!IrSWukK92xzkS$3iL zpN~RFDs+y!EA=m|OVkXMa*jQcJd-mJ6`5scy8iEhqdfUA5Zx`G{ zM!%zipF}~ccGrUI&^~t=zjeNf)UX)vJbZvP7zX@lm9i`xk!4f??cxLwv0bx0aIKAY zXkJ_yvexT0MwC&)5g?zDTTO_Lu##;g`azU#IAv{G2B!Y$X5d{5a*sf#EL&TY8nKm= z+urlLYR5FD>8X?g$169?XX$i84S+>ik=noSQ)3t)?wcuUZ^$yCI=1GVJ-LI0#^0_F znK*i$yr2g`7b^rS5H0rkTa%7*RTQI-HJmapjx~;veWCvrgA!(e3tuqu0D;Wn&bJVu z?u8xcL|tgiKGB@E0_SjnZy=u_CSu7!^7EMq zoVOvG=9~~UL!1xJdZt}Kc5Q_g-_{8-Tr~comY7m?E`JAxH;?v$Us3gG_~Lj~+)gn+ z*0o;{qGV)(b5sBEGCJyr!r6=hA{4Ri>B{9F_MXxxWX!a`(f_+hopY*F0@JucJ%Fo(XHG}OMt+K5BBAoMA zDl(2xVjM!Ds(lZo*S4Y1C*;teb|6V~7$!&t;W`UoaA6Le5_OB-T!iVb9zuB;sr7Qz z32>Ze2o3;gI}pqqu$^Xq#)eZ)+@>v|Hm^tXG7MdGy~)Rrhy+@zW=A(}atwdy*nHYS zwOgXip(pQ8~u7>H~|fnSsQndX8qKfYol+x@2Wq2*XK8i*N>d$ z6yI@S(ic17?*+@s=cT>tKQ`#MmpZ2C!wBCi@fS{7sz-C~pYqV^3F~!YAZ{KJ zrtxM0+m;=SmF!2jyO{V~N0pE{6dUWVq*D!gW!YDSU9?l#*3s?e@($_jgvYPe0xlAN zPKACfI~4F{qD*m%Sr%YgCAbW8V0uy#T{H+kie8-e*Ta}F{5^>thU0%Kp;w1uNbB8fmK&Pk{xnyUY)zzS?r>DJgWs}$o#o+N|Cs0EpP>G*lE#&cF-$#{_%1*M(ah z%1fiQY~xW)KP5i<*3|qOM{Y$8mU5zr#s4^-cOd3AsRym;*e+~t4)V@L4vvAas%bP` z`?~9|I?mPEvrA*DG#p2}x2~@V7mG!PbM#}eBPp1uZG47y0S8Qg;2au3xuyNM!fM87 z@9}Q?nA1x~r=S>!*d1JTyX^NUtU)9Z#$VoCof+6sM9^rnrOaKbm&kd;V9+MzA@zoz zTNHRgH*F0rrX5LEWi03(nDic=3pw+R>D8H+L)O)x1;*NU-7Xj!hWE`Fi-W*me9$(e zHl)>{7OCkIWX8^Ns5?Lf)oPMMCMkuhm!|%`*Chnnm18#k0^!U_GrOV@Osrt7N_uZ& zrE8<&$i?3S7ezTWd)wHN*-~1(NeEwcO5r(_G+kv2UyJh2dP6Hzn-n`fBHhu2SPAA89puh z!hidyc8DzfM9TLE0>{G#b2QC)s;GZC-tBI!@oxTJSHepk#$9)cFm8x_iU&UPb72Jx zW~Z_a*A^Fu_B%Z0(nl*)#Aw|ricKun0zVx5ojJLG2rA=?#N`R$&E3``MaV}!L>pK^ zXrjn&sm~`vo4~$lMF5gmhSOQW;7%xR_Wvjb^-^U`5-gA<4kWA83qRxY(^1?c|(?N2K`>jV=SZ#SYQo6D~_6fSM3}{s$y~G(+yG zgWKKH!m6?bXfhg(`=H?wdHZ{F^hvo&f#i;KWrr0ip@hZGciGxg8}nnCLkRc?Q~Un- zLRi_74sVLMTh!Ss#BO}bk%MJ!ih?XuBb4$(?aZ$MBg<}F ztWYv8$|A^Tx9^Z&PiN8`%Th1AB{dFwN)e`fw>fZA)oukLKv=cwko8^9pl?R2ozT^! zhH05B%r-duB72w2S+Lky&{hK#2MFJJXD2RZ#Z`i7dO*gVIKQ5&7pk4nEdqyL~qe48$A(#D{qPkX%aC3C-Q+l&pdx z$oNV5g$j^%R1?KXndWURMR1EmL+Ee1_yz_FN>rH;V?Y1;nJxEf_cP*j@8}ni@S(?c z>8^G&X27ysjx++O$b2ESney6`UdSr3;x`-Ycn=lPf-?MiUFWX5tcQJ$#r&K|Pd88) z$o%n0_PAKvLgSWS`tC$0!o)%^+X&uiY}|D^UAZVBq@mO}2G0fx9cEUMFV94*9E0VP zzt<;I9BOUv_Fq;|f27CRZW&(1Vb--4OFmWxHpy*a`OfCYFFqZ#DOG`@y0f&lIeIW3 z2Gc7{%U2M?wlxYYM_p?Oi4JSP*lc%8^{S(belzf!VfHJ0A$&WpFN(~mq0fT!`2P4; zKipv9A!u+=2*0uEJhPfafyVEPN@W2Pi}su*P7j+;YP(5*#Iouw%xWQU_M`eEW}}Q; z)@3E32QN;Kq@}4mFxZC@%U&KxhG_@_a|aPn3>^b&Zsp(6HnK&fp+scJ5LzG;LcPxO zd8Ppf-OGPc5hJcf$=qt%L7D_PoSagR22&t>h=_JSB)j6qjv*=?>u#y2toQxgiZO9k zWpH42^wkzMk44mo#@rDxAS;ipSN!T$g%LO_MiG%*oW?3qIIfFx7}xu{@*zuzn0To% z>b#BB_qR{2_>9lnglzE}kR_*IZ%XVLkfY0=-_o!3UC1x_4at*q0i;H9Do|_u3!woI zzh^mmp1rOX;kTv@%!9%kmrWWA_I^AC@iAglUWUr&d;Gb zdj9##Ueju@Tgb6DHJ#%E433d2rkhc7Ju9n0o~1>P@MqzYoklCNGb!^v_Hf%?f>Hf2 z2D%8m>YsdDW1)Xhs$Cac9>yJ!rPbIHq<>jBrW)!v=_mfu;AaF{xA2zD>8og~7F=@o z$SWY)5{d2I+;30VOaFO{ZCa7rgl>hj!gDJ4TtR}=%<|5yUWQYUw^JYP^L)AX>X%7# zIplErSIIv;N%u+!SD!1I{^LP9geS+uJysk}qj7(e*1R^|c#7%f3J@wS?>`BG`JHU$ zHl&>7M&=`4CnI6aekv4H$y~F5Qp)yncO{> zL}(+uW;F}xg3~dT)}?f&i`jba9x#no=pD%zB)+KOSum~-d&s!2XR$q0L&9=0lw%4O zPoT<5tANUQxf>YeIJP7GE||Sm$z@HN0Nqdo(~t*G`Or6Z6@@v_mIh#pMpF$5R+UlX zk8a>59{fE#-hgD0(1==YfZ(mGGwL3xVt_>^2EIT3-p4i#`7PXL91s7zB8Git$rjp1 z;u&aq z;YY9qih;>RX9v;Tsk;wM>?(c1A&-N6H=5OcBxj#P;9;JDG3)nlT;4UiJ4HV1d9{m$ zJ`bg{bK_xc?5$CNFQ#=y2@ecRR$%2bR1BcPBj!ZVZA~%p566)m!)J580D0Kzyj#^m zyS8wX$KUv^We{AKD?Hu7o{TP3#KLew8J<(d|MGwGrSvYDKjU~B#$iX5){cFA0O3nr zM|7~{o?+F!wpL%?B%T8mc*or%K6@t(Z}`qK9CUJ-Jsw&UUQ zV*EC@+>~BGP5bmvj;`Jvpfuw6Np3nz9!Ux%U0|u+K?b!Y*@JaU{o{0ch--(#2KL)U zyK%W(Cg?)@7KK1(UQR1oBRNKvL4v~+XYQ4mVS94+4g^nTBtTwfHS{O@yMTRy378c&j*WuW z+Lc{+>I?MRjb?(U(}I}sWkNo(jg_MX0yM_9%G|k|@9@jf@WOjShiB_S&)!I2*k z3W<9$6g{NNueLI;z|J8(%1Zf@(AT5SV;U(L>V zE!C7Vx~Jnc?eGiIV9CTL<-jO+8urX5O%+0>oM680QlB~}PL-%XwUZa@=I3Z?9HZsx zO6Ni9BNk-0y~kc^UgR^weYo(0YO`7X@Djp+n=k z!PyDUiQzLe?SlcdHdU7|%Yo_3C1HhStaj_AFS5d@wS$t4*OGkWpPRv8XFOPpZq&D0 zX`^3xc&qYvn@)V$Tk$Su$2pOyR9T{VxdPQB7T;JHDIREI^5f7YqA5<|Vh4MnLTm*$ zvg8n2zN98}d9>y)Vbe4qhYUQd`Nb(jW?eGz4xQKh%+UYn;}VS3?%qCLuGFpdU&lwV zqQ24!ik6E9SPCGe``w%O!4|EGi9DtYLag1E*K8u%Ni&x4+{GhQ+$aHqaY%wLWN92fUbq+!6SETu*=;mWi7r#rz~Gkx-Anv^iH?I>gT?od!+(;*ykR ze$T^$YLCgX#n+ip50}#vp#06zLs(#@F&n*Yq?O9eJWmnR79aC5UR?^J%z6=A&r^hf z;`?G?M{)4zugeY6Fgta6JH(F{xVfM`UNf18ID7<-_p4NIQR%yJCu_|RMq7f97L|F9 zDbKC~d&|(3B%d}d7NfP$&pNK-v@F^e(xv~h&kj<1UjFUxw|UDK&mYBIpG^S{ZykW* zmtC?89e;h;%jHNh)^KgRO|@_B2%OBVbOqGL&P8zbLp0jua9nv_mB#9e9?Tnw3@xuq2qpCj2w58VvdP zlE3X0dZWxX2Fy{3FKTfmAu0y>=>JkZsOd~hr8|rzISc-c*0erXo4QoSMa#u2A&{rX zfSH%$kD%tVweK#cb^iuOvgBu5`lth#VCt|z9U+{{6vOpUEvHjjC;&@&X6YhVCj-S_O4!`c# ziq(d%2Basw#g2DbPnYQIK>DD09!U9t0${Yb9|}+MclL$O@Is2JLC!-t9)i!RVa79N znWYlDP;h)@CsnAK-Eb@a^3y8;8|q_r3SFc!oc6f-y$DV7=4IPoFT_&zc)n zb_gd(qf(H?Ez5YDVcdU%++o=&hn_crbbSiy$bMF2rYK~+UV|PRp}(g^qXC;16^3-k z8)5Ce!(UpH9U&uhXzH`O%ASML3HI-yo8^a(Ec$kW@WBcb+F%CCgr;x-qi+Eh_PIAm z4>Z1@#qCt$h}+7#S2u_2%iy|fTA~QDZFH%o3mo=KYvJCIx=a6uNID7SNI2H1dE2WL z<8;23hMfY$Q(0{e%#e#~wuYqB(R2SRhy?hMSP87ZgIVn&x499Us}q`SB^g5R4RJ4H z%CzuUa)fTtic{L!73_e#HvURsNm&v){s^gM7AjO~6E^Tb(YJq$VEVXZU}HXZQ_2aW z<+=jf!nCuyw&mNZ*5&BE(scbz%f_iyF@Sf!7Yc zuoYHK$Ds+{r?)ADg9P$6+@8qeK3OVM|JFC#xQ(JD|7#_v?PV_PN|P?N3PuMH&q=m7 zj(`>`F*6Y%hIG{&5e&IvAHxuQCFWU{w4rjLtu;h{s7LCtV{f-$!uEhVOyj{Z!O?g| z_BZqR8ge9N++(!q$115DQm-l0BODAN0V#_KP4=Fi zsIt)}fNnj7kmNW7O7pfPO2VZOy)7qq-Jkp`AOxNbY#I|B)fV7ND2_P}fHC zlK6AjgQMe_TMP#*(bclQwiQo{E3(w(^5){D=gr-Mv^r&fszk@EPn4ZLNvK~%Oy~bv z;+qS^=qPw|cW~It`caxGdj@A1$6Xdb8jVZzDyAcd5-!q~W4+fX>1{&f$DL;cTd2m*~#=Uxffq@3$2K}uUBX2IfjxT zDmi@Edez>xAEzY2L4*DyFFUTGa`x;MMXb&^2vTc#z~V*CM`VN2r<6Ewm4(DS>HHo& zMn|a+l+qLcwKmSGQSEhaRKbwAGlgKbnoA8?U%cl^fkU|D>)E@>%v+quyiW~)-mf~Z zuAsJ6pLyS$q*{fLz4EzB*3uf%>h<(SN_iy2gTiU4<_@98?K;Etchng3YsWy0|SWjWfL7gGhOA27&B#T3E&X;-{R zy$@ntAswNmAIo%Mmg{)^Opp_`qA_$t!JRWHeN+<3bwx+sXtIvl@0YMdo-pki4`}f| zB;otv>=2MXrFZtyethw^uXDzA!1bMyiDyEiF+*BEV|;u=)fGXh=*VBinro9cmo`$z zV4Pws(m#S+a`{1=YDXM58G~15{-$jUkH|33S*^|SO|2Dm z05akon8H?-*4#^MGDZRs=Tj|?FB)0jT84}tw39HWiPTazd9OlFJiRT<&vC!vuAllxyf+_ zufS@{Tw@Rl^Kj^Q(2fXk#C^dmk^uJ5ZNaL});e!FYFUZb-d*f8qg`mG3&l}xIKZ`2 zRY&e=kK^kXnZAYr0zEI%t`aco41^|IU?7-oQyhuwz8Qb_>Z_-=qgzf}IlCyhI@X}b zsk+}28Y`OYDS+K~P<3AiOdgMQEsCG#;1|HI3cl^toZ{;4Ve$W-d+%Vu0%D*^q$qFJ z7W()Aam$DeozrQ#MKy65T;^i# z{{O=IgbwY&l+w_lk-7*-8nN#{&=>s->{9^5e=tUh-qk)%5$nhppxCSntR&t`o+1bP zmz#bHI}Oal$m8Ce&rB94rlG1-8JaQYx|#=WuHR$p&qcFbJpu8tR{A{m@U>ew`sp0v^Xf3%gDPa~g)}t5serYDdx_(x z%pf7VTk@IhE!B1aVjpTd60$Lv_=_?3K~LOQ;Vn7Z^ziyM&{^;oJffj5PxQ;ihz5hG zM2hKA6}UA9!9Je35%6EZ1lr|T@H~KBv9Y04SZX5^8@-&Eg~|MgOa~zdVkF~ zS8;k!;^?C7qRaR6LnhFo8YCIPoS+r}PX2KvrV<{o^_{i~Gk46e9~yn}Y0D~Y3CgNv z!6rl9mc_I1##@H$M(=Mvj8z3oCm;+whp7Bqy|v&3kh3NvaD*yp&J18G+H#Okn+P*5 zW<5~m&pXpaPyNK-73uG4sdgUuz;v%1{jb-^@S^d%0w(i|YuT;y_}(0b-iT-q?*e z<(%DUpo1(d!;K;qP;Al8$y`cdqoPa_G{CM9DlsIwfJ@?0Qhh?RQ+#00wEjG zzgs#x5~n`mDNlR8nLpxRVuD4uSS{VzD{4Cfv$KrYmSX^;GBr6@++HD11%obxd{0R> z0N&Q=JM4w>k|Z1NcGMG^3bW4lgUNNtC~hEf&&_8&IsP<|aKxa29DGXKm7q=^#k`5k zNZz=%IL1jwM-cxBT{l?`9Tx9HbZYsIdCE;x#++!pyR~K1jYsn=aTAn$>&HC6v1ZBS zXua`h)bn~Q#=(P8Mk){nwAg!sqR1)Q8Ajc?_pgOdaG_wtlN_7X*l1FI)O)Nv_~4DYjAYMbU(0Q= zZ#LoyIjkgf6^6815k})J^3?sZ4TIFd9I8mtpG+;^B!+ySQxX(Xw1}Q4H(0TsVc@Pb zflwT6L%9BF!Yx(V%?mKp2%k+GvpdY@b_G=GMe*yLFaNkK6D4?mHR#V!`zT8=%&$%$ zGk|XslT;}>XyDTtb})z>>K6DLNbe)tbL$-r|2?kCyMyhc$Aj1m>4pS>@Dz2sE3yVC zHUD4C>?vD}b>R?2M}sV~7`z^sp5P12yGvcX020#!-!Zsj4Q3}r<3Wm+38HMnu za&#Kms24Ralv81~kK`}#R=SdPZC*^rwIKbfLH6YiPRlEFDx*B%=YcgS@b*J|oe=6Q zboXz*2UO_|GPOxVzip=9pqeaBER3v${J**G|G!`Vqzal64$241b{5GX8Q6CU9n#lXObTDZg zvfbijE^JQNpR}{bPnh^Z8Xs4(OhE0GRK5Dh>+%$gnKC-p>g!@~EorVnDxf_T=5>(X z1d|lMda#Vdb1e&LGJsPl4+K-i)6d@Gao#?bdS>R9F&MIJ08N*;917aico(ES5Jf1P z+_FJ?uzwTFV8`afcacJQax25F@`3n;vSb|U6^h-ry?eu9Nz9nF0t>I1l$tD$JH#a0 zI+L|-vKOgIef({dX=PQQ*Y^tiJG5CcDL%OPH}E#OW} zv9ZaPb%O)b&b)j4+1P{u6?O@Ue33Ms+?B*G=|BI~)2joor-8s%eEn{7D6*Lj?E`$! z!9NSn+PdUFXJr(FDTaG^pzH1ca!rC39RnM7AI8^5jLwd-pA(3EN!CZwiM+|_@7$gi z9~s_a7sRWiKy?^q(=0r;RqG}?C<4c8y7*4#u&_ve30@VaM4EbqOMEVONPt&?+sJVS z?j6+UB8x#xiwC}lAF&QD5%HZ(4p4u2y#OeYIC|MkPT56r!`x{PYCv5tT}T$Q`bQvS zj#R;Q0jNjiBJznzr(tBJs&mCSN_ZQX=mivA4D{>PFj%uStY9;|uiBo#F@V{bj}CQP zd9u`RUDzJxHBzXeIP#D3yx(-LMp0!xIJ6NZp!i(iM|5u8sS8idKCSR?N4D5~?Ik-p zqtC59_Jt&seiC9T$gy5jMAFn6I8`OK#!p;P9pZ?d#@f4+zrgr&ZI|YhAPbtiUeB#s zCgGqT($1h)wWwoKVH#g16z^lKQ3q2%=3z7~8d0P2lB*H4L`4PbTNyN#*i&TYYD{=_ z+dA@^2qA^XpKnq*C_7r}d=V_gz7d_zZYaub4*YyFi8*EY7I$I}c&lQBYz*sLa)7Ir@xpwzR+YV-A#52Tmqa)yG!X z9%eJ@w$hF7jmRl81Z6FcoA@YLA3}+kMJ(miy7O0R17#VA85VwOh+yw$r()feoa&no zo|SnpJ9;O@HM~YGrJ*!7Rff0oLNg#~x92EHEeV>8-L={qE`e~;^MiW?R4G(gIW#z9 z^aw0OdY54cRiioAKH7m|oWGCyr)dVaI~5U!(Z>W8Pv8#!9fie3rm5Po?xwzc0g5c9 zlJeS{gah<*WZ!Jk_dW5;fLUq6y?2snid`K~X!1w13rb;4fR-nz)E>pHQ_2(1eJ#?B9FE zw0~T%4vx;q5Vs|qLoNOnPpPQZKmHH(sHU)=QRQO&A==HPBT6fpQ#+m#NK5+4l(4XkgwJFfcn&NlDk>BCV`^%=mtX6jJF$)Hg-#?eV_GuvVsGjgH2k znG~M-y7M%C9bzPjE-$s2uIG-Hme~0#P}cC3;#~HGfDHufD>$6T(G$*|>2bxp&77}o zWoI`>Qni_bOa%>e{ofjfT1`0vdsJz)c*xY~ff9Oa2Q@YJ!)BC;M&Wc!%J?-mRI7GJ zN`Kk01KEm5!%J>QQf?InXDo6M9pTP{I@y@FKV&ow<0V%nP*#?j1DdGs4}Bv~R^V7! zPw>m0rQew*du`}dKuXZxu!l|`7=18M2ACL1vnSP%JRDVy#K=dz7WOViv^Z8LL60h$ z^O@f%5+T_il@fQKQd5v^Wql*N#(~Q65?&}f{k#2=ERPd*^bmg@go-Gq1!2JIr95q+ z=TC-Np@3Vcd*J_aC!`Z7o*w#*24u7`3UIa3M=Y1IUi08ff_moO-puHvxTOu{pV^a2 zpxMCIMqc}31>ti3;z#{uB8vXjg@Tk3)zS6ZbHv*?s*x9~v!|ul{5Iw13anFWA>W^p z>AyH7A4v}?%%E?d#)W`@phjFPih)jFgL%sS(W(DlL))Ve|+cb^UwbVk;7F zC{Eh+0J4nP9mVMBL6s)V((5YWk#n0GmJEKYD8UfLEo;8;r(DhU)af65RC|b^7=Idq zO$;o|6$|DOui9SekMa2q?^VM4l^M+B^gfSFt_XhyWBmQ?q%e|b8R?{H$t2r$^UAlK z6upu2VBs6OJZaCl%AY8IPhjq_pZ13`0@91EQ5#l!7rMjW$DhJE?i=1>TZmCGpK91$QYHjX~eIVn-<2W__vk_B4kgX)rZ? zUMU=zK+zX}rGi%T`3Mx<1w(;6 zFC2Y?Kvsg$T+w(wJEUpmp1xOdgc8XFCW1wko!ZYe^F)w}9;ZNi7+&4fMRrb?+A8X8 zMOoXwh@GCuCmjR}b+Q-3C=tM@rM`l6ggm+GB0THfiS8lNJCzjH-CJ zh1rM;+L-!gCAPr@G7GV)xDKn>G|DR0%uvHC#Znm-K|e0^9a=Wqr7BK54%t|31%4c( z2-Pmk7&t{UXSWP3aFXIPn%9!2@T=JVb^5q{vIVn-=Ms?#3TXN#vXZcjPXA#!az$3H ze$6{Pk)iQO{X#azeQLn+a%s7JO4H|6rJGBIQB^Z3KRn)Bj(eNcmvSpAi>#CKXQ#}* zB9;Y!YMy0yWu1LQWtUIEQQ@2I)WI#F9X$B>F1r5?$)4T=@w14{*i;J?!sXYTPCSqD zw6F8VuE@s&tdH^WSOt#bfOlbqmwOPfV`{rCsd!vJrukVbwnzP`J=!lnZY5jBm>PZ! zzZ1?EBpP1cmAj$0mXXUA-RD};gQJC{an?O*pDrzRd9sF#Q@v@8%N943X#-jV?^!&G^3^c)coxCXUWClgNK&M2Uo#N{0jK8qg^7h4 z*(*DGDop;5vcbFvv5nT)=x~$*ZuQTG3#;0Itfq0f8$HwYdu3CTcz|d@F*kki^mUzy zU#@qX92ORh4WrR%d8#iX)Er%WYKJ^T=KQGu6Ch5Bjgytqx|*YE#~C)lGbrSQg|8Rh zc~jvbuTm<=4gKdz7sb`sizqRW;?{Y{Jh}WGdkgm7YvaV`o`hhMep5VW{f>CcW%mUm zVn=l!*bw;SMl$uqoiGDF7Du(lSK0gB#MS=h`snGJVeoWuDob(e42Z3I2RZ%9S75B} zHs+>!1{0rdK(7s^BnMe(uF+@g!jB|D>_i&-ntWCzh zsi_Itaj%c{`am{;qjhx*&XrTh1+9SPoS=fAZcF@sV~+#?$6KAHZRwwFMA zS3R{>cE!(+VVM^oQ8qlrZe!*o@p%*AA@a${q$mZyWazMe4^U%#KEh8`Fyxe84=Pwv5CH9_#HfPKAD{Qw;W;;{ z#M4>`=?OFY;+uK`aU6M=i#a{&ZH}xkMW@#}Z5`%o9U(s~Rm-0M|Z{6NUq zvLc9Y0*W@2B}jNQ?GUhx)SmE`+4@3V;W@t6ciwn6Osb?an5I;GB`}6Qadzsfp+{N2 z`!@EXu4ug@LLwkNERprjn>WHX>^=R8(XEc)>{0T?1gXqpm*nrlMLXav)6IfjQ&ry= zlDzz;))Pp$(|-mhM}zeOb)b&>6pGV3Q68V%t-SEg(3S})B7Q{>8iYiWluh)^Yn1gD zbn=;<$M7C;)#yg+<4%03!FVrFhq>(vyj>RK(-83qP9Y}QfeTZ}ISS+eKn{P&Z4px{< z9QX@cjE7C}N}sT7XAlgkBy0FOsOpNc;J&;)^ON?_KKN*sntU8GKKYG_kbIF-6>-VH z8=?K}1`Z$d&$bYrs4b+m@o}}*3nG?aEK)GCn1+LV^COwSgNhrF4Y#RXvND&Y_g~$l zQpvv86)9v8R354u6w2eM`c5%X#1FARu45QuI=P114z;6SwJiCqhigJk&6)1-Y)esL zzPIpU%A?*jd%mji0EVHM?CxMtz>bvth2u;?jA{?G#ms(4yY6T-k9`zb`!BN2J!H>R zfI}!P)e2tc-?r`Du~wT4-k~KvCh4?j)un3?Vu=Hg6N}88gkt$;JBbLbu}P@nSem`? zYhaW*nNEunyUkUDxIykQ18P^dS?Y&s&(b0LQMWAqAC91=CDNybnf zo3G=K(ub$7u8f70?(D$S&?3EX#R~{X)ZaPQ+f6#A5bRt z4~!mjfbfmd!^Y(mx-0v(Uh}wIapNpIiwPQ_GZJWN;j1EewO*WBYn1??DTIb|0kP)gz ze~ETMTrSnSr$?Jy(5SBAqHwMeUiv?2OZC~Z4PQxfv=rj4JnMHG`wy`?_x_}j{z)kT z0s{Ude*SYQC14MM96%sZhzValTHNJO;QGs@wl>on7|Gcp)POUCoZe~n;2 zu4zM?^{6jTdvOM{N=81ZL(LHeDy$mY{S+$Y4w8Jb!VKEj9h&z-9a=KOxWOCFM$2cE z#V-F1z+}}RZOYzq!jCe?XbghI#tWPreO^cV%hghwC;!SXS&`7#66}33*`xXZwV)GD z+MW=!oL4!N-Z^24OE2fjZSp-g!D4c{=RM^wh#+o&;OSV(k**DnwE>~m23G98 zP5Moag<+&Vv+hUxnOJu^rdL^xfTR4Plx8InHL*wt)_~;eN zgrA47j}Cka#RayN*^o&(`^13!^$pX%1C~6NAesjiSl3j_uDze8?KCe{wmZK!e*oz zCkKr=d{QJQu+;Vr%-@%iXf09LU3HTy+>JVgHH!2MZ@on|YaKb*bX{Dny+<2Z;H_ig zB9fYNQr*>#djb>1#fg6NrS(-m;T-Ufo8rk+cr3y?q}yWkXXQDYHX*F1m^Ng)sAAE9 zV+1RL!uw%-V1qB&4iO^ME0DJ3@sYYUm1be4M>1E7xG{K>o99uTa%wNg0+d6(a#wGV7ZX&=V<5Tp#P8G>0f@J zf35)j98sK1jGX>u006-3-IKCS{TRYd4$d}!0Pz2E1ONQu06@_V0RRAJA@zSo|N4NU z8vpTyzU~lRWg>PJ5>k)KGh(_}rxHo-!ae~}k67qN%*q$6l|9TXT zYv|GN70d+>CplEuP$b%ZJqf1SORmGFA)C6#n1GSo7qavW3<tiPeRP0YE)I1oV)80{#uO^4{kbEBC|~Z=_c4 z?!~mArhd)JG4XxQE+q;-7w#ntOe}`vjo?ZQu+i8lp!YxP2?X$;r6mCW{H$l-KkEsq zDKw`LvIXkeohKv>J{V%?XP-H!dY0}X1rOqq{RQYfOQzuS2> z3}qNe1jbEmE{fs~(RaTU`X=(Ue&O>q$0{iVx?3hF52~8nC;};3J_i&|N_K+RUjmaP z2H(`dcAi*#9B=3)qPrQkD;QEknJz=4C-ps`^tm#TZI*-~fLUDu3Rd^Ys$x#EJi&Xj z?AMN3o8*c=>N*ZDMFao2wFt1U+?DI0DSgABmdh%F4SGq^Q| zXnk>*{rBh^`dYc==TSV2xXVWmZf2~c_JcqpeKn=aFG*&uz9;|w+u2d+Ry7EHHlO{> z!TyeU5S2pFy;cvUJC!<~tkyvDpbuo}wQXkv9DuTd-e^1YQ2)ML4ba~TyJu%HGbPMy zk=hVFo3^KMmsC`0zQbzb$#nAaBOym5B-^87=41V(v7&5Ki0)O6;)`X%5I$vkI>=1Y zGul`Wii9Vp%aE#E1Ev2vLjJ?V6;{4L24I*ir`W#o&K;gtHF|_W{XV829`$JXJJsK_lr(JaSc?T$W!xW|0!-qx07Bi|7 ze~`c9=I87`mo6p9mSb@@0 z@^{wBZl{Q)(3&Q=4DVDG)VLVFI2oHiumIi0uN0AgfrL3wV)x#w8g`SgLr6Vg9BubS zS1VgY1$7vQ5oufJ4#auQz(qW(WLmS#f%u1Kz&u9U>Kyh=M!9~ zhTz3yu`P8Etk%yyZ7&Dy zPzlN869ds57vF^RvVUz7T~Vzf#T3b+YE+X*!^H8ZJntq+dh_q*$3R`%=Cd~cM0suB+oq`^VQQY^%>0ol2sr2t@rFmDu+=x=E#b7uI?A{Dr=I5(8BiUlHg+G1#<}X zVvV{r*N&CL49S~2$XJK0&G9CK7L#&H&Fyh5JN-Lor1&KnIq9TLzJqeh)DpF=T)p{1 zy&9N_C;9cmV1yZQOvHnbeXyLw0ftLp**05W^7Y=RAbf}TKp~|(2NY@5>if$Gwp8#g zS)eah)h9|pp0m2CvMslYma!7IlFkGMO2Hf-yxlD7?C63=MTG{zVES~FRuOZE6F9-0 zlo2Oj|0(A=8@5kaGAoypDr>v{U4J8v6%uwGYPyq!k~DDbg7`8kimsXmr*w6Xs&Z%8 zz^Atk+;{vS@kI2z<-J4T!G>Eig_^I?K*xtbi55*yA-XWWEF)|w(xkzR&KSRorGPVu z8b?T*m8iy50g>2g*GKxPx#W<0-3%Fn_$-hS$#o0+a>RVUG}ty zIf}4$g{{Irq67Cdx?O-w0A$FwXW00;LOB7KbFGSs>E44m3Et+tiIt+`Pq{m&`a0G zhBvh2>+)pbq{8ErYnfG6&wDx9CEiBFl<n7vV?bvNi)7ils!rx8~8k;s)FI^Po= z?g`kNl0BBHnX-pX{CmS`eXl467?kbL)7bp04(zb(WPYcVz>+cPiL)%uoS7e0`!5$; zK^mS{KEOY3S%O;S09S#b)5lGX?szmZF+u?Fx!)Imfid@597)HD9qq!HyQrXGRIt4j zI|ZP?`u4=Ft~N*4j;xl>m@i+)_^8s!L2+mria@RgF@Dd{RpaQWxpZPYDm4Lv*OCk$ zedtLIn?+QAjKyFA$DJG#`{Ho3J~%3;3Db+*b$+|~FWZ0#A9Vl|%&ruFs3(NShY7V> zZw$}!)b+mAAADeJ;nx>kn4^@ESHsY#I1M<;Ex+-{A$&ObzUa2;P&&?piNm^(^`szl z2OaPhq)R)M#)8^`B97-23_SRhv+Tv7q~VjFr$1T$fswt`MSyKMvM2k_G0h-1!Xpv^l-;YbE%4kGGdrshShJ}0!99Wuq?+Z0N`xzIZ zS9ORgIa9hJDT5V1%N0HZ9?%)LYTj2H=U>93sO)YRgCa5>`k6d*Wr=AP5qM0U$_8o= z!nLzVZL*{$-6y!3O3@&V^kBh_*B9}qWA`4WYv~$G7#J6il#wT{1PjjxYA;2u(F`y^ zY21F52yVQeGT;B=q((vqeuf_thP7x-q{OmCUm{eDXR+|mE@vXt_3^@N4K{O1{wxJ| z{>A)SqD!UNL`6ve8*yjU!sNz3@gc;+u4z`oM97z_S$p;j0!O*734mB@zpJR1E zpA*v_D?jgKPB<_-vGNjSuQ!NmQm?f*nPgRM60HNBPvW}&5Nmv+x?iQvx7LoiYxa6v zWfXk=$o%gY5RDZVGa@q5PlHT9;{L3=Wp8lA<``PJM*e55Mr8{kP*&OPnKu{A|yTK>~elOQo>ei&v z9jnFFw9f4!u@{>_y>}X+Y*+prtbn3KM!U{sjQ#SxU9X$&q`M$>>U4wYs_{(;E*28S zJhZDgC1%gl!2YT0`vqabbC6UzV&DstmO5;5k8v3D^OEOy4zi>BeMg{Aj~^jo9K@C{)TJ zLoG~f2`;@LQyfV9Mc@W0!Ev(dQ@RBTrC+5i2P?NSqhreiB{&6#8?2O_kLhDrz=4WV zZhZYb?eDi=9>fb*zcnvjnKKGydMDfko;UCcrB4emwrNMk zlS=ennzcjlxH2?BA1BA9c}8ZZ^g_UZ*F+w;$3C(yqAKgZ zxkcw7VocI%k=EiNmhhJ(+#Xpq4KNQmSWR1b*T5C0i}tM%6X#5SMie)gltAWW5lh)4 z%SAp*kZR=4Ci_Hm5f9WBcsECJ{;y{Zc1!vH5vCJL>s;JQ(f1QnO$B^WxCC0dr zS!MMr7Rvr4%?q}(Lzu6ZQW@2^OZ(-ssPTbHrKScLzI+z^VFSH3DACn*wa1p|Gx2T% zS!4HxBLp8&wYoYr0$!j!KaPXt<%QuB~L}@Ai|kzV==W66GP1)mgVlV12d7; zC|gx8(z{g_K{YDl=jYiVB_SmO;eJ4^^@Q%Px2ji8dyQ49C_&rks#`EuX)~@r7%avz zgs0|s&B5i`%&5N7s-~GyX1U4`6Sse)s+=>f(!v&G{rqPy8Y ztqHA}JWbUfSg?@})ogYiM-}8!+#FZz!N%xfdwbTyinV@3ImYvVqsGy=C22mKo-kuZ zh@hx*jf6mlMm-=r=9*+$4j9UbC;F&hKu{^lqw_2iYTrF~IF#c+3SFEyLcG=(gI1ZB z-M{(WA>k)r=Fp*+?t_Dzb(1i1b+&z2YHi}NZgH=6K@}o6wCi$3Dv^QFx}8u|ot`fg zn?FMT`r$vv04+DwI`_~ z*aFkTy6xNv_|4!p?Qig`A}_`76~Vm52vGbogSJ1S^f3Ah*N;yrX!DQ=-_uJEkt!iz z=27~rs(F;JNQxApF5;;nQ``EoqtP)I4VaWI%CtiICFWI4sy9o8QanFxo5!@|K==`L z>(?`-)h!NaJ5_pt2@@%=;Dwhh0MSiQrM6r$H2DyuJ1YfB53B9^uxS%54%Q=52IT+k!~vzWbO-Zdi1R!Rn!s7hLT12k2V+7-H;1 zLcKVcM)ldoXE;IhN6zuUs)8AONr6eY4`jNc6$C!oH03T>~)}_Y%504oGtf75H5_}N% zmAvs25QtWxp0xPcKq$sdGtOZ2(o6w6njgP>dAzTJ4%`vQsj*Y4I#5|;g>XxEEYoYp zd{9ed=ue2AJeyf6LpGu zTiI`Eiffp{%GkdR1A!~*&jHhHh0!u?!|GM<17{U^b)Zcq(vzQ3tRJ8Q&>~gfZL>vg z-pxE&%lT?=8wci2WtKyNAG7uV6g7|V5=NHGMf^%wAl|N-N+z+}&_yxtHKoeY#Q+vM zp2@F-xnkPwlPJ(l(*lG#@q2J@*9`ZaLg|C6Nn6Ph^q7wZxDXK*3SZ)#QMkoak0JWM z@erA`D6Mk){blnp08ny7!+{$ImIVtv=7y%xj8eGc@Q>XqY;PaU;xiRvd}+mJ1%2W- z@(U{jP(kUCo`JdjuQTRGp;Fn<^?HM>WI*5y(|lRs9kVByX0KbmMU;s5=;^(T%yAgE zCFSmkk$eqO^SN{p9S)%x*Y27XS#)J!rjGo_<}8Qj<`~hjgNP0!?jMNw?;+?SJuLn@ ze6+kdRGf4Jd+Vud?e2JRFE#d}6zU;XhShvw5#n`TR@d}VdQ(2rCKqciCp=yBk&Ylk zSg=>`<2~q8<>0q`-AvRt+&$C>cQkBaZIHrY6?A9FlDRbAA5&&mgSIDf>v6K`9qzKn z`Cki1$oGAC!7|Yul9&@gz$*=qC{YMy^2G*PyycFl zx_ou0yC1ejskc1b&60hPH~+?_hY_c;>|fswk&97!Y>sy&M>-tUG!hVGV{H&3TSeLq zomGu>r52pZ3{T4pDq(ZCNhyD&l^1V}bCj?<)tCo7A30TgG{RL~QDOS)&d z7ZlXs4M9oyUm$247$LFk1soyfO5UWPk@Rjt@+Z=?bt#{i^X~lkEX=JwM8cvmFY&jcUrEscz<|=Yl%*0a-LWvZJ%_ zy6~S1i?nGybFYb@ZH^KXIZ2blLTd7(ub>Hx9@xFA2n<=C$%6Mo11JdHK)d8OjmfLy zP|!@L+=_I(({MLG34dI@3N3kag-sx&4*pdi7RbZr-7$Ikvaj*IJ;(b{YrBjfKKAtk z)u@}%il~JgI+k_%=e$_PM*sjxH1NK0l1Fbfse7KVHWhF1ZY(6sO)&eh%k~8gfHkPt zh=JS3!B|&Ml9Hj&N@&9*ZJKBWG-8|Q@9t^=10+l)mczO4 zFnA@8Qtivc(nSTczo5|H+S)OThQM%2x}S6nYZCRg79%3vE1uc09rIt44uNIo>TTm| zpJPbANb94@v>HeNa1}1-Vu(!X$ zD~<}lJ84W*g9xJ|D(gywrg{TNx;+yA20zwP#_{^i>4jHV+9pK_oL~X9=~DgsVn9@P zh63q2qw-ne`7{Y7Q)QHu0<-+mpOpGiu-O=u4*e~%6oBOZr$IHfykE`cwy3uaX{idaYk6K;^ajmNwIxslKvbnC80~qO2GCx&FMY z>P45?+pqx~=D1?JQH}0NtAeRp2dV^?4?)W2Yd4t8dHr&fqRL&rktcnq{9mS>{@S)XlXI)&3nNbgp&TS|iMMoe?qSZ6vB$$7Jw4Rm0af&IUZq%sGf46ruIC-{b#M(KVwL7`R0K3~P%bjxqj$-Wpa-cFTsxOd@w z_1BpwkePk;@7fd?E)~3`(aT0a6T{{$@5VZchFhQVy-Iq$7(_H){Yg#2Z0usE2%;)$4w!aucA z6d+sn%M-3f;El4_MJ}NrM*+@)M_4qMaRwxrro6A_%_DoM`dpTMAR_eEdHX;76gtM5_xA}rBKL|#%j$G#-i`C0ftxa941E1 zHR8tWMZ`3)1Oa6p&LV^d>IAUvLIi0G|JCX_yPW0IX%*^Rv2cmDKQs5^Jv7Xo#m1TBZ z!7EoVgei&slynQ{cuC^!@I3%n(+=A)MOz$&dp+k;w(N~D8k+@k13aJYd5zD#6D1ut z1ze#bM)4yDJEU&t%H7w+>+#^ z1AI+t2qb`gURGmYkc&zrY~LU7A;WDLB6eF+3^Gy&wM=H}`@vA&rSmU-UERK{3;4Jq zUA2;SpxY(Yg=YmE_%faQI`gOXDDWLwcg61gvtv5^@#TP2!GMkikJVzW49+Xg#$7NY z*(yYyU&O{^f*^}(26o66eTg2&IeF%SQ7b-4bSw?kuWIY^cYp9zpaDb@NN^cDH5E=M za9fZ;LR&^70jXEb-`Q9@Y9v4bmKY(zW%Z&1Zcci_WVu^Z|KrbYB^aH5w7=|X#zJNj_=zgZx z%sajyja6g<=Jna_IhbTdlJo%NQHA>sM4Fgb)=|kYUk4NM(xhsrOZk-NI;G)je_O;t zb9`<@*YE7!YN5-)@vV2J<&M2%Gkb$vX)B6VZ985ypa?xt3-&t z8Y^82y747d)!u&SbH`Z^+I<#NpnIP{!i_-JO?pBSLTn)m@@PLMUR9eXd&53YOPxsq zr*)AxS9~yXamH^}&#h>uQBO+1aOT11uV(i8zPLqcQ<3)5VY|ovdZic`u#2coHC}yl znnSL>`(3BmiEpE$Xe_Sp@0>XF_aoN+=?m~^G%U!EJB1Fr?1u{bKaug|KmPwWGM?G{ z@n`P;g^UNW_=jwVq8s{eGM@Y2$oU_#9*FV3$@w1}V&~t^-kWmunMp{zx6PyU&Kl$_{O?78b4V;WPg1U2S+LJHd!?OP1CvLIoIE zsWb8vJz@vVL_25pbIBWj&Z?~;6R`r{r<*j%=#JsUSu0UW<$USHWrq*-KiPjW+wU7v ztAaz!L~n1+Z>K9a4LT4if0eo#j-y9q5#;YA+Efp{QEYiKPk6`=bPG{Z#x2?E zk+tJBBLIqwh_t}k2@iRsIAhP4sWk0)zkq`xPpn3+ME%vCN;<7B=^m-t=#TsfSG*oaY$5yADPGL0AT8MUW6$B}J zI7=p@bzIm|F}1#m@afBWx+%NV?bMbj9MDq2TQgrbfhwm`oIH-3?xU$3h&A>uF-JJ@ zB5r0lxCCQ_Jf;79Cvq=Foft}OO$?~oG7PGfsXad_rjE#zHF;%QIQX>|BZ6E zY)mCI2Dl^90G+~y(;%?KooS_tMMn--~X$shzgWDmZr5pAk7vCaOOIUPX6 zA25yXCE+!kvM&f!t;mwON`1@t;T6r(M^IZ-Vov zH>~Y`qJmu>0iIM?ln;z75-m(xg!FgEK= zml5p6F=PMM;@Jq#g0JHAtBryiSAVsXc$DDHT^SetOz|E}VL@K_1_8oB=~0P`Xh#y&fJqKZs4u2dMrj zM-^;4T^!8yo!4r;P*y4yFHRCLU38Ri<=8VY|Ie=VkL(BdKP~&22h0Hq{weN9bkt3X)Act4S{#q;L@JZsf<9?)X!?Zxk-tqur07*ObLe%xI%Et0p|<PUl-sen%3kf_Ce=88KFlD8j z-`C5AfBA~}G;%?%b>=~-z&D5E#YwI4jPAittqwUSRFlg6rQI<3RYHg&9W&=BxxXa~ z$eliK>j7C-bbO6$9faJI$a)LED1ebHsxz=~$H=o9$Gs6pP6|*7P0f}477Kgv_O-aS z?)JI?ZBZWMHNWBFgbHW;eKd-0*R++Ls@Er~D_>&ZDSBpP}R4oZ+dsq)x z9AAyhPHzP`1=dBW-Ji2==p|{&5HSxxz+>ohfMmIZ)iTVOBNx?N5Dc~Xls9*xil#MQgZNno%pGw-mb%5x{X%1}4p1#|E%q(P%PcrTr=ZYgWX*_;L!IogU^|Y;E9^_B0 z>^s51GBL5ZS!d~Y{p5Be;jVpuLp=3zmkrNxRUxRDV;Te)&Sxl_H1Hq9va`OxhR@38GkS&f+1}g=^g`=^xA2K-WD= z?1p98T03Z84_>&3p6DjOp544e4Ja|$$X!brrN0O zg8{>z9DmEGG+nENYs8T$$bEljpC^5gj77~iyo;8@DAoHDG5;r=Me*bRf5TZqgSbC) z|1UTTm-D|xjhFw0vp}`}4R`$uXO;bL+yy`?`tLXk8n6iuyJ)}Ua=-L0ytbc7m4|%)Yug3W}C`Rt)*FSn&mSUNg_gX zTKtQT@^~p92HI?%k%F1saYO_AZd1(w1Ml^O2^E5HEofRD_L`7^?()5ZRqQ?G8Ak!R z66mv^iZp8+EW9EtCFLpzjk5l|7nN#_$&dQG`|v~Y)>@gp8(-tmYtBWgh=C%e|C8fd z_B2=*8gZ%IC@vZJsT8t6@ao_L`jA<900uFk2s+Pd_>=MRMtxVWsO2^h|2#YX@Kw3r zpXt=i2deMJ7eV2#oz0EW@z$N_H7#=r{(4xwk-1*=96!sF>{Gbit0bK9st^j4IZ{3Q zM%T`+MtfCm;8-+3V(gFdq_5;szb}|-GDGf4mEXMLa!J8dQa3ZLN02#}7%e<%>B_z=D(f<()a%1_W!?4i_GxoeSsM3~vx zIW-*=DP-ebf=+JM?xM*X1@#(I1>aJBDE6THf6;bNL7D|yyQs^yZQC}xY}?jXw(Tz4 zwr$(CyKHw=?cOVP{AXXB6LD_V?TmSunURs>8SlUp+CVc@#-rpxPsO)eH>#D4A4|%~ zJY$(S-nBfkDlX0~3F(0ka(DpYw}pdU3dFp}9S>Yf2vaxSnQ;vFWaO*%$|)zz{h6|H zbD#yp@~+=Rq&gXPVaV^xOQkHgF$R>X-ShaZe#bv@@mnL`8l#2n-ZUa!6-az^!AT`L zip)3ap}@(s-%*%y>Q)^%RwIHi^PCu5=6YZj#inv7&{DOdmuUD)E?a;bz*;))89?6R zu5|$Ja`zqt+)P2M-ekznc%SPLiellxS78?12xSVydjeyFymIaTUgALj*T_5i)+tYI z8c1eR9*>2A>iabvv9}1=9X-XvPp-Y*+lk~|v?0P{tX5As`_&6t?85gf5ppP|y<98Z zdQ_x*%0P+vBW$Wz7YvRwLMdjrJ?%AyEA|w><^>RMG^0YFw6PV<7Cy8N-hVN&jJuXM-_x_9Wk zFbX(eN7Kz@Syw>>@EIaT0FIcTha3)|c6c9T)Lj?sBs7A^VA{m&(a5H^PjrB4q z^Myk-S2k`p`?1se3Zc^?4;8P$2x>5mpLTU(Mu4A~6z5Br{44)N z8;w21A$E)>L$>Og+SKu3a!e0mm|9khYlwzC7Eemm_Ar zR#A6TbXHQIZHHbeOFqZ|M&EZVyJy3aWpcjCwOE2rB?@j$jTTdaY6-5ACBVM>8t7~4 zNNyNjOa>iga`-JFcK8x;7O(kY_jI1e+2MyJ-kTqyG@iQI50L8@dfig63?(JZyVD*T zfXNa{jL7cDvuh)AgGg~x{=T?JdKkjX{PS?dz)mAL@j){Mvfl^UT^u76iWN;^Y!Cm; z1c@^MSWbFVk=!XmK>aqBcaL7zLe@^UW<)eV+i0G+milBEH)I!iw(_P<*-;5_Y8Rl} za`mxWxb;ObCms#CX48#2VHvJEbSCT8`?2i78OsyLIXX3{yQA>=5iIF;(mU%sUK-^gEZKxsLk|6U1y9%}wr7ucK*r3ODl z`!RdIpGb=9t2&wZ`ivDe-fFSXv%uH$af)DLik>=M4l%k%YZG3Ce{s{P3m-s~X`UtY zmU=Q~uJecgf^t*?@}$TYA+E4gni(Zxs3CcO9Tx~=iY3t9>IVj017Ysszq@wdpEQd! zX;J6Dxh2WaPSiIcw0$x3Tg9rS8OLn{B5m!;Wvm7W1Osuq*+L>Gj_RCv*~Jkaqy6v> zotyLneryS?iCz#Ub%qGwwmld_+mq!U&K$Q;Ldm%e?N46mUEKnzOr=8np=&?l7a+2z zDGsWy``T3`0(}>(F2Y%N=|&Fll2v1lOqsxrdt|+2*@yJ81JOFP=@5FvT=)zg&w@k|F+81Q!Dutq|g zknM!&<5!E8WnG`l7nFiO}qdz^lD{!*+0UXq|m0h*jM`EaVTe*bW12TwcPtk zntJFF*<}nOaCtbb_{~D~zQMv|2sSo@&<-k^w2OeOkh~$_6Rx}p!3GRiLBaxdASf^r zezznf%~=@MstJHb*>%Gs8L$!YmaXWL_dv`HNXYVqfN$#2#^c#-P<)(`gaY>Hw*%i_;$I zmA2x&KHhlLrY8oX98ZHI?!p_5+>3kchmuonQqXd6^u-8;$aBDZ@`hH5GF4D&mujGS zop*MjjN6CZ#*gc^@c|J#mAWs1BP$dZf9Qy=y*mspF=KHPExE6j918@ z9*Tycuy5YlTqiZA)Fo3Z-L>2I<7ZQ5WaX4BgVgL2plw3{+0+cTYS>Yyd<3cej!)A7 z5U?&jC8LnG6)CE0v!y1p8*>tK{Yadrx}V4txgJsLvQZJj{z9vV2NWS1L#HyfKJhZZ zuV8Q{qC=Ae&Z8M6dUSO+SE%`!SJ!kw9YHBbN34>C zMG0fR*%*j%lNJCFWjVM1hjcXYP~FrGmk`*U5-B|zeDD;jl#z63lFR+$%a^$fS>rIC zZ(SP+2?de6V&L2@$s2E@Ee6_DhTFsIYdO{BkJJLFweDJg2sIg)SYy6J=1higRKf*( z`?-q!fv$2X+aJn{Y4CGOJ>F(i+b=mw=6i`tqH-XE$HHd{*RGhbRo_JubBu0%{H80g zv_-G2sKy#Mq#(f3(@#Q+YU{?GB1NQro1vN5P)0zrs$+pzNbW z;1#6q2zlC_d)(9Pmf1O{@)j>=RjR*ZqQa_%2J$^MJ7>?&zf5w(oH}7R3Cssl%ip4Z zrSzj>dFb{zmVru@|ENj&^6u(2Q-32%(UZTbiTE~p*J?sHw<#dwAy!mt=G{_1q>lWV z8D|qH)ZIhGK2>IA&AalJzQ<2(!Ih-gQmNCK{5PibMx`I;^qIt6Wx0ix%++9Kg#{mi z;xacLbQFOyNPRYS-4?LeEoJ2({9{G1Lm#llsFrLPFJMy(>h`ey1A8*Kf?20IPuO2E z1F%N#KLkjqu8QkwtE;T?tjnoy0+(P#ST*uPC#Q) z-Khhoaw(rM*Ezv*-=j%kG|puqu>KXb#HYgbWTvx2SUulgr#m}&-quB)tyfuYNSy?4(<0EB3C!tUFS^o_ml>OfumX=vXMA@r$CZWVQ+Q z9xro$@q+4>?(1kg2Z}7J<%^r-`b27``N|jOO}EN#{TIjOk>Fn)aQ?NHNdv&sePudU&ayYP!YHjDZdPs~Ds^!odO!QA zVjZ|UEH(*@qx~Q1N3Z3umbXdY$S9?Yr$}%kynKcYy60VdjKz0RBp{vchh|AP> zQLAegs$IG|>8wH5m4SlMBEMrbHOvk7d?vTB=8LYTv}=ZDdz8Xj?)d*%M%?GyBuzj7 z3jC@a00MM@$au9-4vSKJhxX&6K1ot?kA7OcH(0NHXAAEYT^J3{eqJ{piNM%zy(K>P>z9 zaYvR$6!eF5jKT@EqkD ztl+%0$!b)e@kuU&2jRw6qE;DZx}iSK6qY6lh&aknljsa0i=_`fW?pdL#7{f$(K65U z7#H{yGbyJLM0blv!BWF4{@s)5-we%L{4wmo#p%>hhHKjUcxiux9}06jdIX2ebVn(h z;$nVE`)>!krrrlMC`m*-WK6Fn7mTC*3#)w0`gcK7Sa>=v+4_&L7`4^FeRcuqg*xeQ z`p@{&G}cB0Co*un^iQVhYH+7eI%NfJ$VVC$R20Vkw6i7CoDqptx)4Pimo|>_~iCXaJzu;!I`^B7@ zc7HF2HMejv5jHFpV`GeDTf0BEXDog_NbMC)c%}~|Ve#7@b8VDudmXa6m6FLlz(;&3 zrvA`q^(vl%7BM!8=?Suio+Ma$YOV{W63d)Eg5TaO1dm2cCE=N%=K8&0vr2@&-#$kP z-jR%RA)NSeSJLuZG&JJB%*|>dVM8@YODsh-CA@ociu-=zwp>MIoP;GzWq=BcBOibj zo`|OG8*Co_t6kV!=G4%qp|4+*Ovg1( z)=Jx$%K`hA*y*iIg`m6klbI!5LL@5b=G1RR%xNyRJ(|Sz5nhtBf|4GF34UNMV?0un zhn8mH`xfB~yN0xVMkcwz*moXClNaW)kv5k=5m#kT%=*=)WeBfb{P5Pn?$>V$T!3AD zDrKHA4k|AUO2WB1}T6oDmf5kcgguOTnGdh9JD?}<~JGjH&6A;(o9achE7 z-wQG9TXrvuvcM*ao8e44lIm{O{YUA111(9Bc5UI6dH$fsM#%3%QAsaX_|aFSFI}=4 zroTsdWt4t4DV3KJze)u5Q=x~3{fI#z#%Y55v3Mi8phsT$wW#1gV-S7vu5DKtG%a{i zdTLtIN1DB7xA?(mm*u-w>n9v!ZDgDO*q3vxh=%%Vq(HvO>hu{TO?Xq2U8WLHx%~Ko zzvrJuF4l^9*l)7c%9+LIt_E5(U!HsO!1B9ASeM(CnIA#mKFHT7&((%{+>^-YtH$lh z7xtd=8a7H?YQ2N=OCiUb#bw102c*<@6U*UJ`~-Y!wPGLigEB;0u`Y#@{q6cFn8StB zqDNpW83*s0oyqtq6~5-}f)$S<%FRuM{jJ6-193IznJ=V+ybNcJ<7INVP!F zNEp7An?GbZDM(+;v%eQC(Ys+}KaY=Uq58DEn6*G6^y?0iksJw`^#K7GyHB>B3`0Eg zsEM(svNr?cM%OGw!7&N*fAdL)q&PfVRIR{Nv7GI5b?QHeF?E!#D` zEt0H&DFxNf%fwkPOL=?GbF7PRzX}wsk?^QtMpWb-T)b;AyJrIzqF_!@c^!EiKDs56rcP0{xFSY-?AA=t!3W5$P)3`!(Snv)WgREM;4%U~XZPW?$LYmpa5QCWA&>QN# zbxF5j$c7tB7Xyhuwzr&;r2dRCj4NI6Dw4hEf>c|3*KPZorFR)LSwg|FkHdki`2o>-5euYB8Zk?n03O3@qnwAnzeGex2twn;FX%gO95HD4TB5Y+d zVWdFJU_w~X{=<_=%HshQQqSzH2H940H_mm*fFu&&=nL18n~4pSu@+v? z*D{cqE(}%wF8X6()uy-UAG5i%fYdD;&yvVj+ESM68AbPNa~a!K+FI@76fE!O)8*R z8`=8Bu80qDM15UsghJk#bb-9dH<`8GcNf)JkNJuiok0F9S-10Z%$_Fl8A1eRqy%-u!(ONZ5&3v!i~$oL)M=ldT#Wb~Y*GBDn{-f1 z*Kz``M>37MaFhRW|A1k!W#BMe@?Xv|2+g}b4b%IZQuw41@7igc$lq=@meSC?jOSlJF(4v!Mavf%L0NR{({lh zxusNRHye1K!2Hp;h2=DYPdUJw+8=tZYkw0KyZ@y*b zyBOF}>~s9OlfgTqGRhoQd`X1$R-tQH#Vqa1kh@VQ${BR;+K!?hWMc?WO|(s%Az6N=bC795h20qAE{)Pgahx=UbjicsmdKoG@p{Sn#O_9Sn)6JQmg6O1i$Peb?L? zr0_K|;Q)pEv#kMEW9=&Vl(s6(r$pNxS)4W}6HXcy4IHIjp$wvwUu%Jy%&Sd9Q;K;)$0WiKysCq123{JI819OtOC)e8ee~1& zIovu+0?=y+RE2!20`P*oU&%h_X_Sof&!%}SMeCombvP~tjFyfFMu>alT8KXWvcPvE z=rh=ATIEIJYH)qMHjZARID493@he3{6U>Vw8klEB#1HZTX#s(-c-@~QJ;LkSSKm-e zqu~lLzbfYJ&Ir(q=MoUHepYaFT<(LnM-dV_5L6zL4Juvp^nQw)0kZCso_8 zTv(4Mka7*R^z7 z`Z$U?%Su$2ttWcoJnvA*7>A*4p-eQl zp14)tK^CPd{Q*-Q@~F`0nTf(J3hxb?JQDc@^9p_An2Yez+53G2Ull;=<50PokFY0w zzeSmjNu@}j%U-)Aes7VyFHw%nb6vh!BExemcFJmfcusrhK5#h6_tAw&Vh{F7blPqx za>mUu?+FlR!+!`=V1Lfr5+x8Wa^dh=n{Cf}Jm)y-V)$oodOh#hj_l7(tI|6u%pkpL zudL1b-Ss%q*D8+`WzAz$#7DfLF4_+imcojOh?VH2MuLle)H!<#25`?o;_eH| zJFWG>3bYiNN=s@(-%3$9PDvRX9?ihuN zHJtaA;QZ2V;u zXjXR)Kdvz?^BSN*Ox;)OvunF;F?+s5B#ydBbV|-vLpE@ftj<^;{K=0XXsdtNxEhZ_ zyOtS#Nf>*HZeW3d_#K>)i?R1%rLt{kc^2~G-h)*9<0a0V9~&xXsg;1a!JVEKxfG|m z@RyKkZub{0(d@cKz)641-EKj$F5p- z74^P^4N!)lOE5I9vY?lqJ+TRvW?EFRew8k^$ZR@(r1kQsbqM~f-&j?d2D|TJcwk{} zLO{J9yovf$Pkfd4^dWi<7k+7r)`t88Bi_Qn2W2^w;a3%Id^W{v-US|A@(B|U1HPWZ88gxUIP}+TFJnm z+x(3Kqvr73f^O!gfxl+oiHr~~!J;&9HhV}*c)M$Sq)eE1+z(>HzQeMtClUi}xu1B8 zH`LT);!xkh86rt4sFA||oUQ+5lLsOIoC_K$fs9Pz&$8W$O|hc$zax$$oIj<&a9f{r zog7c`xWC8ET+z+78*iH8*q!4FGf%og-H7EF7NdM@!WWvHOIBVC4rjZP;|lU~c?s)r=t{2@)(c}5%F`)m)agzC)}@EW~0QNp`A|33_n)T zJo;{!2PZk0$9zX?Go@_Zz!es$)YoT}y210Vw8&_G3zfB)oC*wI0{!kK( zS`t`5!x20dbdgM=8{N9!EFHgY!)l-RCi<{0jGw4&sIb`G=0Ic8_Te{_kC(Wqn<9M> zQcB9Y4!QL66l+uX*|!b3lrautEDsVpLDC@fub(MPmZp3!Je_e@*`;_)iSvCY5) z*OKycULS`elmxdpVbIL3~vUj;n{R7|1eiiL&zdi-Lc{Yz(KvpvTcQ6r zjC>>1dm^@YIwJ^$kN`-}ffKw0XQQR&%!y6O2MyO?>{*Ng+R`;5NR0Oo9472cINtsg zJ{UcWufUMk%CF0dj1E3J$a_UfY6=vqX03ng^#4tBQUCk?|4(zNz=;3*_y0q4ftde~ zivLf99qz>crMv!VE^z7pqq|0dfJg)XZ_Neq-}Z=q|LvRT|Lhy)*#-i>DyGtQ2vfqh zJBgXu?l|y8;y`P}=ij(W9tOzbE+4e7(?y3SsM}^Z!l+ToB(dXpFYXJOhWH1Um+RA8 zucf!Yy{u^6Jp_P#A&g3#xjh)$5D`K}s`&GB(Kwrr+V1`4(1~qJ9*?51T`imJB_6sc zURW55Qc*5MhQ~6mjwpYapaM`N_enkL7X0wuq=fk`NI6e?W{vH%8}6DlI2KI{)>-*~ zB?9qI+QHwmYYKB>9l8MuTB+9HA(*{sZh!%-or#jLBG^`vC z;FZ5Zt*-Xu7(NOeJ?1apgvl18s%7w3SVV@u5QreyGF3;vwE;lBkPkyaXZ*)@vMdoX7g6oE z(f(7nBp)$F4G+-g*u7xHv80H6hg}-s38}Mt)|o@l&nrdzfC8`MQHnFs`a zO6X$Y(Sq=DBhRS~{Q^x49%jJ575kr?FT{V@4B{FHXM!gD#|(lri32s;2~y`#2(S75 zW)}ovKcfnU)Dx#4);IH`6_udfHGCh2?WFV+$XK#~$#Zz8KzP-Mg8^n{)?D}-7}(<8&TEE1Uz>sNlwFcZEWCSptO%!U=vD) zS7AovyY38dP`p-G1F*g}c7n1|hWpnznMYGxRw3|6E36m}Rd&VkkiS78=Hef=qqkra zn{?R@>ix5iS#96kq1&p|LeH69ZR82B3~~4{;jp%Wd~cqmp76I)vW^xSMHcU`Wj@Hv zVxV+C{rBOY(H0J$V5+R-=f zrl4eZ&QS_^adLEa`_Y@yJP*XCY_kK@^u|dZ_9iQ@7so^^#2@W8#rhMovr7bPg-^j^ zu#r%@{$hYdwIh?gyj5}3*{J)4t4Kxf(0}iwg(NBW4|X%>|eYM6N>nX?8OS3EsQWw-7qW=$Gz|p0fHhB*d_}rjuGa!CdAy`0xj*~*uOY$s#M@_(mu3oh zqCNG6Tq1c^QAw=cERc(fGuk;CCLgwsxDeBvWE(v;ZmhzAjsm&n3}-$nWWQiU=A3ZfY^W$)x!Ke$bIlHM$Snn=PIkLbYPa9;=j`T*du ztloOmPqA5;x`yn4(32=lt_`V#@pR7gD-;7ffK3Nyipvy2ZxrW#MJ%w5+@2a9S{7Fa z83(DD&OgMcq_WwxFavGz%nRCP>blC#$xYE?>k2Ocec72gx)566KW4g$C2UGKMDvBs z8uW4?vLgJ=Cxqsi0&5t%x~eH$c+jllz&Y;NB@%+9Un6Y^iIB243KXE;(XaXp@=&S# zeL~=2qzHS3N~ndwYJ0n2Zcl7{2!o6&hIu}l-F5jT)-3XI3cvlc$!Idbxy+?P%)4P~ zP|K{C!Ps%3sqh`ffrve3v;$KzBEYyQO%cFh__# z2H!7mzYCta?_USBugB%XxJ%Ar@2p{|6ui|6XKB=7_)LG4tU%595jvF8S(T8m*g^c# z8hqybN7FeVbu|#@Z0wi-j7{TI& zriYp_n~#6JkPi6laYUAF7lt2K1hj3h4<0n3#ew6IOi0*ZjWla# z?71}Q-PbESqyJRqR_U*$o)sXC0D@Z$;UBKz^hCK5Rc9Ozze(ziLE z&vb>8Qg0Gfq)v9(k7(0IY8|IqS3M$~>;W<+Xm?ZldXOc(H54etPUUmliM+)J^_b{4 zeyJ&=lwQ}YMeW!88;KLxaR{i3JE9l~$NK$7hQzZ{gD|YVVoz>qQ=XhcMJwtDTyAW?s!5?vQiV1V*OO3BSKgEm41$F%E^A7mdHc)BCdmD|GFog=t8>J^Re zKFyi0{=r8;_z@d6UyZL`Qu7^9((tI!ny_m0;@rMDIK1=3p5Bfn*iv{LJoK$nwiMKl zl?2{d1%^l#fd3;g9*WsY08VREM~RN^t*TmhC-;o@+C58vK!ccOD2^ZV`!uBhFp2(h zo`60)oB13tc?dQFT#Z@C*Or)#2llnxXUP<)WEjm8W;Mcghuo8)j?&^~<_V)4iY(4akji`{E#unC=@go0Ig6MIk?i23Jv*h}T=*TNfpVxLY-FVo>shGmBeGeoDt;Jz-i2MgM`Ok$+YYJ`q3Qx!;2zb zSO5){SMEM|cq$3m6ctJ}Eh0j3jCfswJ{8MtV9i6+Xc*uBkSfAx=NlulK^+^ovW0m~ ztL1reZiZG!cpjNRy+FH#+^Z@seyAvYZ{tO@uEsH@9NaB14Wj1fb-yEtobJQ3EV@kH zGU}BwByWq2tKcR%yGLiZGp#YL-WQZvZQFVw79HLVKX#fmJ1V$nXq6~rtsBKZd{;O+ zLW<8QC4!Fx5Fy#5%jYAxlV{V|0WR*?WtEi|&NS zi58ZX>z0SQsaZ{$65=JLJx-KBpY7aY8&F_D@dD=bhB2h!K$8mG)p_@XM~7M)Yyh$7 zsHGTM(D}@y^ID@MgL)^e!u_2kF%waP^!Rkm$& z#8Qu@c4FP>tiXV0z7L`|O$4o^aYxQZSXxz;Zez7KUR<%-C^uHGB&uE+_UY zb0+bvl|>focH0WJ&;sVCQy#9Yj~|YIzOfNcRVsnK$o9kgyarg?L zT*zm{S0pY(`#dD7(TUS&3&3LD^2{QX>LnV~K`4fazp`>cNOJj4uA-xL_RraxSAZeA zw*Br8@r9IWX&1{HL1mY3yw=%TneCn$xiCzEp>qs8Iw(<2iSy5hBQ-RJ5_Xh~QyEvQ zjbB1Sa#NSTa}QfFJ#-0z{J82uQhjjcGHtW*IsApbsAV_J$F%-vp^oRF0Cz>NI)NFz zm~YrGS6&MRo7;DDP2*|iuafk`?j=QKV(eq$luz>LvA(xN7>ciJKbc_C%ln+JI0+|>MF^Lg+N{7K? zl#J2Wg@fWuO7Qhr@cjh*h&)~7M<4d&wR9P?_estK8*TAQ3l9<$Ax4D*bc7=Fjy#^C zK6P$n-txQ@>T}#PGKd!<(ynXE$&uBDlgMuoxXece9JW)1FWZ*lY12)L@$O*7t`%Cn zkl|zh5Ia0_k}R{b@jwF;W&oSC3k(a1EMCX15yC1S9eC9$(k{BeGqe4hMs6y5Ey&&Tj1T|eLPGz`h!Q5TIL#31p8>LmuPb(Rw z`ZL?uxErFMl#VY|n>~sWH8~j*$&F?k^WjwnfsmCG8-u8#SuBtGZ}Wgl#jcM`@?JLh zP#avyYbR@!xn_JE+!a!=MT~ueM-b3GZ;|?RSrGr&4v>s%cNugrG9qC|bQ;JUJUT{~ zX6e9X&KRuf;^85bjU%cDuQFZx&NrT?G2JI^*DU$sbm(T~(nZwjZUGj#b)V{5u0TB%$s#bHFd#v(HG_?A!7%n-f*niD9~ zup{4Pd9Q#~L5)A~wV^RXrkIhhVq8CZvv?mwM|uA&ee4RLM*-`M_A;CL$91=pz2n5l z@KW+dIu5-f?!%}SZg=+f?exX$5h-UdOx+l$7Ee^Pza(p)*l3iX-@E4{v+cT~IUH9S z7(-&ITi#^4z*+x|zqACS+SQsw{3sF2oEZE%tI-(oBfRJci;t8uzWNvermPum7)Hi_ zGWSd)J+)YisXOdgWyGH)o-vG8e!_@4_GoZ|3gauO%5mO?myStxt`nwKUr2%&n`3ja9ztwPga z|5IrFJ)-}VFbX;PWgBe3#yoogAiiNwC#0=xkO1o4wcdTMkWYM4sfcx~R2Ub*Ye>}l z7)V>(!TSPE{5Xn}%TR{iRLczeTWNuFlEF$PaqI)e;eN8mfaSb91sxwAp=*DmDX{3Y z^^wypoG)4$grRfH@I>>*h1 zigCOS+jN4nyzqaxW01l$tqW$!|tYVRqY^yK!- zZf;#>a|KRWQ-OLFORnJrnK>8feozGc!f*l#n(R{x^42JgDyyNF>^k6{Cm@uV*`b3i z@L2ro1gO@pB!U@~=C|l`-^QYWcbyrI<6J3C$Qt#tHPTRYsFIBCS217$%MA(E+k9+Z zYMC`H#KlH}8{JjdA8$|qjA#}=CMP zA}fK1RMPdDF#;)xoklqTyLn0wQJe+$!G5cAusYfsCI4jll#5fxEo9Cry2-!2MQ=Hu zswzEb&q-YQa`7Aih@in!NV-9F5pRu;o+Z`;F{J38N>uFElmX%aTIUE?kF>0S{bu^% z&ulb7Dw$YZcp-RzYv?XEYlb#1*E$-TXdzwQnl!%`2%UvK8Aw|4k&&R52e=d}*!eSj z@nRe^#D1T~8#(wIGO`EKnebQ*d6S6Y{f=INukAoG%$~U&S3@b7=zAo;PNS;6$0MkW zr+vL|aUUYXHo#TCEhjn5blp+JxBUX|^s^^nK7x%5(~ei}9teJ9ZGPVOZnDF2Bh5SP z6Fc+Efd(Y?!NSh})#f$s&1+2+!;&dJ1}BNeUdr}2Pj{y zc&D!{oJM-VJLeCuD>?q-xF4M}-s$7pt`yd}kl`G7vW`mjERPwfd0?B6WZZAJAIgXi zmW<%5)Jn5mB7M$OEDQ%bO?H|XU|Mq}Edi;+`9C|t=TifRp4yGXrfTJ71*5W++C;Nd zuAu(&-1|)YXIBK8K)XfLj)#!h#VPJ9#;(vieLjuP_Z9QK=fEi zA#wR(v=MFuC%3EkqWpUUoxC-;LfJvrPe&*dK-c6Tf1PsIeYvg9zqHY5nQ<;B)Sr8W zIjsW+RD#d%1srS1tHor#i>BADoO1hMOOPM3D7n6DlV60%G4mJ@yOe`QC4A;?I|>q{ zrT)U15>ZCi*N~oW4CD8CY@yW#8J#1*H1sAgWVK2iY2jRXE15FBcyP`$Q+FV6lx-p1 z7>3J}1}6LhS1C1z(I4K$;`W)!^kQ-pE8g*-oytQ>le0f60NL*Dnx83TD`CuZ! zyv>pp*q9nc-XM?&+KStq)yoh@rK(lVmpyS2qiezO5DzWn!-@YiTO(yzqbK|ISDi`-#%&zd6}oE1Q&Wtdnk!SKBWcVbix;7KG_fXWeZYyfOi?Y!0I?Ds zynB2R2vGX>a?SV`15LDA!y#*-GMuu{{1CiEY9BI(%PfH=QlZGnl553~!DLct3 zSK?MVD;7jMG+=oar+_s$A^-hqIhV2i*w<89C!F)z_yI`rU_Yil{^AOHyM~P!aeHy2 zE`x$bWw^DbnKf%g|00Eu)Lur$w8ia~*%L}`FYUg6%`=WT*W)22Jcmeu`kptoAWv_- zcs?SB(mE4Jg|Tg|-pMVM!y-u32H!f+So{Z3>;UexA33Gt3{2f$-IHl$^Afd#z88=( zaA;@s&M~m_)N`k>Q>ExyA!XDqic(<)zQ%b)q=Re@giuKT!lOTw{sk&Nc^xUN_3;*E6_&`@&a&3v~x920cYopfdS9-ptQB$v++gu-lTQW;b@YRA$eeU8#31df8GRCuvT zV`fG=JbS~2E^}^c-KuXgwQQEVCHgdy1=7N_<(AoY?st0)7@uE9Owel^svDt?5W>2` zxL1E>DD2m^Wm~P&0ke;dsGdXLY+4F6LHWs{G7oI+IQ3Ngb{PhFT@|hz{LDn^75CYJ z$zOK2xyo8)gvFfh0Q`DEB~A)s7nz(kiQ{p0 zQaWOQ(NmdJ>?03;W9@~T@7An9!0}uacW`;%a63kYAJ?nNc*vNM22xVA+roBNg! z_Wp7k$z%%Qkozd1Kv}9}fb8(Wus*#{1(gTPC9|JBh;5WY?T+jPLpH}RvZS&DGj?oj zqnOe@qArwAx=_#GnpK#WOyw<9(*65who^DHhu?aPKT<11m7ilcvx?g2dLWgGA2hz7 zb0rK1gkj*L@&ojH_S!taSJ# zyaD50ck+D8&6%}zNDqYX>iud~aSt_0Ii1MrJs@Zz8sCQrwTi?qL|d#-Jh&TixiqbuyKJ`e4ZjgMywQ3wonj*7(7PG= z%%s^Ul^l=39Y=FFF&9bSfK!ATTeuUx z<*QFPd&lv!cEq1p8u&>O7Hf>Q{_JPikq~@6^W9@CybL z^<4XUd@%B)z-2*CDsBo8$n(rh94oT-?HM-9eVa31EHKb?8+Cl8NhBEm1XD_=j|yjR z?0h(V&_)w#Z86>EKCdz?_3D#4M^o=QE`q8j(uw`+w1P zPC=qYNxClEwr$(CjZ?O5oU(1ZPT96?+ji9{>sHV0p6G{}h}!6Qh{x zM(Zq8g(TQ4qc?T$@1#@YQu!goxylevKIiW?eX+XAQ(c-) zWV+w2JtEtVonUXjVkK$s(EkKe5;H_i2k4#`P(3be9bOi@^LF6A-0LZ4HuPw2XLw!LC))D<}2NbhOTZu0q57`;nlW(%5bKnCyD9QUA|Zisjk-9$Qyif zjdT%lwi1-)n_=TIcLb!F*Omx2QNrCT8iQkai*insn;_IJL59->G;wy)m+|vtPoYHm z%zWJxF)J(ma(i}r$(nenYK5>E!p&g1SrpZAbj}_*3`R{#D6woE#d>|@#Fl$>{K_8; zX?)FfvCB`X!?cw-$h&umK4^{*e^zgoR)1881cBOnSk*|z!qan_uL`M2k*%3Cb}VWj zp-|(m>lcj_;|%xixmnF${OsA2~bTe zhGV$*l(}r`dM5{an$D`H{vYO{X-jxO)si**yn1>G0! za2{a2Q%au(9_#+8-e;(J>8CK-nw0f#F<{(ad^T?C->c|&h|Zr(K1B@;|1A`NOWukk zZZUG!M*h>W3RC~r*-dm>na%3*8%8=qd%$i!pfKuh-;rdD-v{*Iph2RJg%Gjuy*q8fXAIS9EtIKYi)$ zlyq4EuiEXFg_oS>JRZS`EQ^SA@IR*ac6SNvKJ~_My;r&4og@qmQL>S0?LFBpiRZO= zba$)0HG$=A{1=`TPqlXvFV6qC_%G7J96Wd#FT89kc1DX-V9QkBiljOYd)Z@C=9@J( zxKcw$H{ScSAlqn8KM}2LM)etm=Hb-~SpiJYg=UR}i?4hSGBDb_xDp~&fp`x)9a;EA z{8+%%C#;M0wl7E)Ed~gdUI%|8W1DSW zQ6N|DzO*$vEUsg=jTK_2;<;U=C?IS6LcVB2-T`<{6dKv{oLGj1U)qg(&HC9;b35V5 zV5nUwuF0^83EGg9u{pgi@!y!M!%7ntqL~N!mNTD*Uy_b&ca#c5vRj>2bmN2-IX#_l zkF;GjT^RO_ljyIS&I-+sQ*Akzw2DEjUhp8|b_imGI**NkKs^>tYLd`O6n>&MI5ZiB z*&6}%VRae1YEi*u56==-pJCqBG=a@BMMQn4$o>Zmd*ndt?erqaIt)UWOh5$UapuPt za~!3A)k&Uxy9Z$#ID-XYyM}lB%F%!C}KeN-|>~Dz_;rp zJhDq+TM>oWj{wT)hLjd7$$}v%C%y)bZAtKrrMjoo7h6H8NeQVLo@8+y>{3u_*-9w;`z@h>LR2*DXPWp-%n7*O z9T%s!>C>Xnzm14?tHMRJ2csYmJaxJCvy}-tghSS0xHxA5+(V+;9w5nKJDaw zCu%3wuntE0_A+$v7=P2H6L~M)f~jga3qF3_GyHed{fCJ)EFQrOkR-M6fU0l17K%<8 zevdc27_nbF3Wigzv%_}i*+1G8JQ zz`_f2B=l84(-fs>?kq_%wcb!}OkM%xWpZPR|ih2gtEWbX5VB*&$im)g^woq*^E&ynlr7 z?nBT!?p3o%%3+-7#<2LjWsg$xt8HHRDCQE5T`r0l{_M0KEgbe@Zb%!+TK@${CrAS^adXPa_A1T{t31gea`90_SGC=k=#3pjCAo`9UgGbz82=fwYPm$&qA~)h4MM(Xc z5Bk{q#{_q+6I+!j8h;!*_+n9a%9I6z97ZI%$=a6P=yp8G7d0@_ zbPXmz$z-;f0m_zLZ$PO1Vk73!AhLUUX`D9yr860jWS7dVDCgJne2+#eTeQfy+?zJY>)*g+B?r66nG1|gkNnmDl=J)YyRB% zX!nbk6#{8)l5xRg(&zNh1#I}yW&pD}FH=$E!!FNl^mTW<-ew*4M*wTb3VJz0PW_y0 zTHn$qa;B<($Lr2qsgAA~jty3!%YLi+pA8J;4W^$rfGfvngN`|Ct`c*Mx9l)-ZtAkTf zRzAR}Lq2s8@|uUL#ez=5Alu~1st%LJ=Ql@E?C_-aEu{F6(UL>l9p)0St<@`%RWrDc z%RZLq2SnwZr)*Qj6k{FJ*Pg#$!YZ{zV@m`wL&lgJc~kr@Hs_MCZAYgJ-A>LMb-W0y z6AAX&8x{^Kb%pgqV+d_b;cfe{BU;qe2z~i5j1S^Lq1=0ntVy6>#0UuF_xe5CheyUc zdZdS(+!Z1x_tzbm)koBmW|@g?2S7nmU}(fFj5#FCGH9g)TP5_oA)?JbaIz&~u`KIm zrI7lhSBN}3W4u#I+lKRQVfr_JV2a`QmV-a#aaV^q&Z(HKtcnRo3TJlqJD@WSNfqL* z%ad~3h7$;`{q7U@4x#Y8oOhB5iCBVm)GvH#>QHupdW>7~8OS65I_xYEIlWAh6D!j% zVexkjC4Vm+^7beP-mattX3iP)Z1fb`NosM>k7@IEM_7cQ??eUdFwH*0@dd3t#@^yOaBHRvp^^wC5;lf%YgSSgU#goxhq9KQL#)|1ro~IlOC{(ghZ4`8Oq6`kOsc? z9NS9gwEs4N-lp>{@$TO6ZZYHAWR%8cB$}4553g5q1R&*Pgr>>>{bmG5v*6cY{y0yW zZxongi5wiwF(q%lk+3!HNYHL92NI8;Rb7cBQ&xle)%&Ma7~(AR_D;|b%*7qcZfwAR9+w~n~FW>w%Y$tt{$`S+H){_ z>X$inD>JEL9`mZ#Akfpz0Q9$J&YG?bpeDlhkIMRWetTO`PGeV>$%~^}#7F(Iy~%v< z-~l2hCMMb`^6a}F@(L|zuHH%hIg_!wV_V7)5@|?7N=irY(lB39L=C+V6C6yc!Q3nB z+;eWfU+{-R7mU{KtZw|U&B+oQ#sxZkp55)6q~>y{a>a0@TOYXs(H9jlHgk>Bh?pW( zGWI?(IJ{o8`5N` zsu{Q#WhK~2TaPkEY~q(Qhx+0qrW$m)EWse$f)rG|JC~M7Q^F4^?(F`SZ8-LNJ-01* zn@Xk%K^x{_&W}RoqSjf`At2*DUHlceNc)=$=E7YeMS3@RGFNy;47bKQ_?29Xq1pzkuJ?-#1OU-(O z>wPBtg9Z@2lKY4YeSujHHKTAH`eLw6zUyxDeQ-Ql=4Q_PFq2T;2+a8$Z42V;Os4v& zr#{%|yq(n|WvKuL%nob_{bAo^(|Q{;$h2iFCar~GwZea22Uk7~j0B|&ncTUyU;BP~ z)|fq%Um+z!VB>x}mm`*Qv>2XJSQ|UJSn5Bebfj5Rmo^j!enc7<3ni*|d_JJWU$1-( z@98}EgbKp=#3k}0kAJY zhU_~SVuwePogp2|C*?q~!F%Q-JW+J}r z-zABLb_Y_J9Ggu8!cKDCzB2wzBAwNON2lt?9G9hXFrmpOH`4nHfhSM}Rr))UMIjpv8w61X;JhY+PebD;FetC}_a2o2 z5X(6Fv`GhnkO|IN%ln%+kC(`7y3?p0jdhv_!R8ufBHxDA3 z6li73mPSHRPAptEgF^Dn)q3gwc(>SrOAq0_^`FnhS%@eHL1qO%x1_;F*9?AHQv^>% zX~WZ?WvJHY{D$JGmDsst-f?}tHELtHgH|rKd31U|-LB7GePEEelN64vuGGIm9q6%N zOeONlXtK8TTQ6Sv_JSM)qgm{OGvdMw(g}^<;TF%+0dSUXh%>k}dFA35q45U*P{7du zND9nnOF6}0NacTuYy%45oNRY+ zWlv9}0oLwgKjuKAW87AQukmC4$*T?r4wk=i3KU5JVs0r~o;N_vSCqD(`EGGI;?eY3 z23wI2xV6c$%G>G@U-`*wg&7&3uaeSooWn%JpNKMH^_igzU?RC#pnu>6e^C-DVbJ3l zGcX9`y5AVgjJ)7t=a68p}Rf9Os373Pwr;jOuXHJxINGU@`qnDGt`g678fA z#fXE>&4ON-QUafKx8@dO*zJ!XO@60y>vH!~o8g;{ZQ}xrN;s65L&w1NRND4;1`I02 zCr5;}*PEnhF70U*{*!paSFWYO$X8^tA7Mc;JY_wY9q-2__`f^j*+vE-_m0$0%oJ;Sbs*vHO7Yarr`~@SR z!2dCbJ^!n0KBJSHcqMJ-51@yNQ$ftLDT1%T?l|j%`DG=~m!0qR>P)SepbPdF`;z$2 zsFgW;;44sz69^vr$Wg~y{C#X+;-6O9jQ1YYV`J1Jp_Gjl3$eC%v$N2e-XwRea)pq@ z1!n4=mCEsFs8l}GkA8f!x^=33a`K<0o@Glt?&#M9w^W~Kx6@!BC!L*E zA8}m0H8trnU`XNQC8E_`_sx$tZcdHRM)$&q04ZXGRlKvX9(8eF< zj#NHn0eF*X$Sn)@IVH9nTTZdV^{RL5H1ve)(w{evvP(>}Pmf{?plgVR4%rgzD{M zr-Hm)*XNzK?l@rG2r+v>iUcr8LtIgd8>@hzFs5d#M>S-m<3ir`S+yUsrjgZ{Kkjo@ zJbab*R-C=h%$(jD@YQPf|1oUti3l~7#o8|(%BLT}X?`YEcx=}WX;DD7TVf!)f0~!W zsvl$VbZ)rLP|_$(;#uB5elt#};O}F?32hu+oPEBeJxzjBW6@ZPctZ*hGMw@phRCx! zSB4e&z+>W27I7631r6X8M;^Z9`lG0YDO!3aMF8!Biv2W*Iz$b@W7gB#Az9SjC?jN1 zj6XV>AmXkFNSCFL%WS8u6mC&Jn++oIg&D}FIuhV*8Q>tfTPGqX_Q~# z7FN+W5njSz7vB4zBPfPS%2dK!8*=akL!@u{_ZQ7dE4lkQGsdf>Z;SyZVgiK(U~HKi zxIaZ{S4R_O$&E|lvj5a!NK@P&;qHNwpy3p22>Er&4DiyQoXNK z0m>ugxwjFBcLc2ZHQ|D2K~yrtfmcbXabRBGu-Ymw!?AUbF^32VyupGlbEd?XEP2GZ zW-gKEKFP@Mb8qN;E4d)pZGHi5@o|?~qXoF^;F`k|U0A7>{8Gycv#(aMDnM8|p*oD& zD>6h=-vH?QM3FqI$hEAM5#AxFCx*TcJy7VO9SdpLG~gjHlElG)U)?szvF5Kgtr~x1 z7`yxOzpa)u8jGnXuukX5}- zkMhvIX9B3Mk`#TH#M*OlArxel+TgX)2qm=|K0Fjqlh&z?sHFg;yRMB{bvwW>XQ5rl ze?T~`DHF8=(RYT$Pw4G{hWrK4cT(ohI_~y6cbQwY{}!|tuag*5GHOyeqHga+Psy`o z*m%*Fk;Z4PG0iIkCn_C@)pfNq6^%5xVP<$_&=D5MT*xeOXkH~!k>REF+v>`&&R8Pb+>zl^3)dV+{V=AD9}?nkgc2;?x=F zfDLSj9Jn+xF~`(yPs!CO!rxZP5i6S9;Fq90YX2?%sg!eWh(n^{(~^X=D*T;6aNKcs z^4YicTq>1plEi#cQ0Ced42vuSw&yW(P&8aiyGg3a#x$T(Cmc}!(U!ZRzIqlQ*E~T% z5+OIY$JE_ck}#@nRbe(0yO6bj+9B)8fKgBy@u-^3yH_A0y!TY;?~k;?o4rD%XP<}f zBC7Xa4k2{w2>^Bw?9*7FILISR#}}RLkPGh#;zeL!bZ&i)kfMX|e=_zz*9m_E&h$3h zA4DF!GFnM}ZTuWrc7B~SHRVQ@;tgiTB&u&C!wf9YN(=F#_zrXrO0W*n3K`z+AnwN_ zcSc-QvQkszHguD@QFB92WHSLoE2WE8CFkXd6&2DQ*KOW43mA8-xf;SCZ)n|Hi#@SL zOa?cM*BGP|(*SSp=S4kmTpdMj&treLR;-Hwdw-GJfZLcLnNo_s&yV29yBV+dT{D!L zA&iao$Ym2@bA)u73z>5HGvQwr44MY8b{>v2}lD;?uj&rpOV&XEF6r>S}`KZD$?^rlNDx(mrxfVaq7L+05 zNpMsUz^>t|0=DLi6ldpr@v=(aPc=%u^pRQ;^kF%BRSsOkSoAS8SO!5OJML=gl&n_%|c8VyCYMVOjbA!yK>~y{ua2>Lob@Jd+2!h5HcMs z$YB#$;^fU*L~hZts#C-mSmsXoci3H|g+Itz{1MfPA_v<^CrZD(u~K=H*BXP&Xwk*@3P89n56O{|O2g}WDw z!QzjlIB#0fEv}_sleF`L3NS34(dRt3zB_`_CFf!MssoG^dgA1n^h}dxUFZAAXH=_5 zj*w4`P+h}`4rK5pwYrxLO5jm1nS=*Y$#7E<^!~2KmYOP9jz~YF_W)(fwjo`=%>Gt! zy`(LZ>tA=f(q_WvU>4t<>lzte91^lBRiLruWX*^{$|IYWsj9&q0BSWbfMfsVoa}qj z-usFM9l%28{<;L3gCB=xg=mdd=MDe>U;v<1{b)XtJUq>( z)uD$UxgdzI{gAoQM3eDF3~L32f{F9#CWF)H@}a!_cf0#>QOHEwuhrm=vQX=tI;yby zNB-zt>&|a#r{sT_H)iy{azc@f*q@}v(S4QC_QB9zF16oit&%#(z1kylF|I}zpp}1~?z+LiQmbebcsE z|B48blE+C~cwTG-6>)_-N)s&Zm~`) ztCD*Oi312vg{yBAM`_+pu?%1Su#Xpj`aVu(G28!OHWD6*Q<$$^DRTG)7EaZRS3qJl z^YPQA`kx>I>Yw%h8${rclKivxe}M=MWQ8iX;X}`%TSc=f!7aCX=oiat?E+0t%SR^8$50IHXwHyD(e{VAHK+&tJ}T z8KtoxRBE1o{Q|k36^PVEJkRQcs z0qcgErJ@jCXrBGSY2@NGcwS*u1Nl)T;Ic6y4OV!Q@);+elHirRrE;w&ifzJkK}*K_ z1tHcchPD>7Mqd}MI-^sbkj3vwZ#5IRj;3f)x{GnSCWMF{Km~7Pfl3z;>$ZB((%9~I zvrHMgqkDUWMMGRvj{0&Re<}XUGVl{%^3(}>53m<2JkHWzPOk(MI_{bi)kBVZ zT!$z&tZFE`(10`d5WshIFJRw&tX&K=Eu&@JRCN8z_vMxaq|Vq$4~T&kE1_g*jV9yq z%z~kx-qdRJ)Yx^&qpQL9(FGQ`io|G2d%^HzrkWLh(f6Hu=#1|mO)zq>lK9X@GV6zA zUxg+)L3td5_&hmxbx3Ta}D}oPmvw81OEjW7(f1n*QIeS5_d2del$@PQeZ(+ zcHxk!`T55bU%XCL`|ya}pr_)gnJAu;tTwFL)#_7JHNoOLJIjRIagKCS!jVAN;`*N7COOP)a4&>mFqW+3Gwvl6$SXWWUs`5l zulLcgihaCf#@z5bwXx`)z{UE;jW1|HvC2m-8P9oEy@K?*4t|OZNtm~Q7WNDs&0g@A zh@hJE4%$R-`n9&prErx_HR}b6s6M)UUY5+UiRM9w7Kpt!q7rVH>)X@LpycRx3PQKY-EA-GmS#tJgVqIi zJ*Fihl8Pjc?`iY7PW5AO;S{x)qXvnFDAJ5gq_BtR5ePW_)^hxmm9R$S;AMR4 z9i|dX06oP6Q(=sp4(6pf?*ZcSm!3A^^In1aBfCMvO>!QmghLJ=Q(1#G)M(UW$T)I_ zpp(c7Y~>D)b>-d?Zee38qekYEr;UDUF$uJ+%Z28|;~17^H2Ybp(PUM&O;Ihr2|bOp z1mC7B_WAibR$*y3U@;{&4|IF76klM6PFJ(S0=TXwqTgnjz@rSo5NsZS?M2A(_o{8D za-0-g>Fzxc%P;E@F5WFZhsA0qCEk0BoF~!ohg!9@N0`(X3L0^)Dx1RAut7H9?E~tY z=f&&ijNR90G*7`<{MT^-CqB{p_ra9IcA&$GMBAHTcj&MR*DG6*t?`CL4?DQ{lJV+p zoxNfA;1ntyr=1V+iqtYT<)aNew)UBI-@O1JPy*%p2QNLbbX9m!0j`Jg7Vd-C0tx;< zH^qOc_(1-r5iw!F9ME_%M6_?l=|8RM(+#Lp)Gf4TMvIl()!097-HD!WdKn+t-ge3o-C+|yVf9{PsL|Y{B`Yy92>iw2+X0E1_{y< z;iRl+d9xkW>6txky8=0_hv>=Cp-X>3tN~j9>!=3Gwp#c6N+@78pC~&4lq9#EU#w2? zRs#JN&)-+xX&us(YivsLbyRG4!`c3t4R0>KL=)bC3$zg7r}KA!A6J>sSlknUfjEhi zls}!PL6;E+yKE=|b*sk^&;wi@PvD4TxqhElqK3Z8#G(>u0gXi`M&-AhP2Ly|s26@0 zS-HOCupdAn5;-$>xDFZxD(6)NJgD;I@$xU0`5g54qA*dv(mWI%7u!9en3;k5${?iC zas{%nln;Y$P(v}RLWml`=EnrOlA+cK@4n2NyvIB4rg>cN)=Tx}33%RhVsBN{G90a!FgX z^oDxlbU#2u>s6#-7-;tl618~7es}hn=))*Oe+%;IK7T(yY`0imQnO1CAb8C%Yir%b zii0gXXB^KD5$U^SNs9<7!Bp1D#W6H6u%-&6qhM;(v!$ubOQh*JxPDhGePPV-H_4y7 zQ#nQIs_)bQ(<2B2;^yI+XhsrBP8W#qp1I{_0o{gy`A!mp<=UW8^e>ecuqOT^SefYE@-*UKw+a_7QL>x%6hxv3 zz_+Fh5nAn5_uka<@cYPt(>Z-EnC>Fi#cOZ;49Ir2|bd&>VbkoSs;vtv;w*R+Np16~VvG-PAm z;_rT!x5m`YU3`Wy9(WCr7REk5vEKkr?Ae;bEk59Ozo`<_CMGK5<>OTIM%{lYlVB7G znC|Fp)eRd)UKv+rHwMDSL5B2EPKCB2_fu4ISdicK$kz`plL^4H(M_qG-<@>?na5zM z&3W7&2JiSdRco{hQ?+;=MRdR5X|JBlK%>!6MNaWNW=eq3Sxnra~$pO|x69+dGP z#Y%W!eSzPy|1MjJQFWsa_0E|^Ss+llo0$lr=?TS7_AYD$v_2mqah6w%IucB?+lhxG zi7ki1kAg@barCO4fMhXi`f_+D;-f>ZP4>b3bK0UT_z+_zL=o$g6?Vf-N(KvH08gb= z(re9A&DwQu3FX7dKcN-k?AE4`ff>{_b-xrDXghJjbg8BZ<+F||6k&mDll{SeskIN5 zIzr?wLQFqmrEj})&72XG$BnTImnE>?;lim1d>ex|#T7GLJcarsoYoR}eAD*|fZFKV zTOVW0OP)gGtBD-W74@Du3&Qw~7$v;Kd} z0sJ?+e)j$^IRKf=KSAMt`NJOlTTbwg9N_D}a{>T*_J7X-p#H<;9O=Gb258!Uk#mnN z`xH5tt`DRfpr(cxb*1?Di@Z}-Cb4IR%K~baCsucC29-$qn-m1kC(o-RIgjj9nFqY5 zI8haJqV=qsm7bGOWw8rF8(y_@;EWPpC4O@i_bdk?e-0H|-<_NmFOw%ChwzUx=I{_z zybfhnHyAN4)hO+q(1cAP(le^i|NYc8;o9mS8Zza4&JQPn+4Wl3fZ*09Zjh zW;RY9$&%-y)l&3XN?MWF(u{!#}ZUFFfUUr@Qq z{JN$u+Q8&uaefMsL_Lp4z?2G!Ujrbk7dnFJ_d?&z_ls{RCH^4XZA*ktAL_Ch^7s-y zheqN%@l)^!-SNntA2Ez+NmH=F^o)-dX^wFYramj)Z8l5&K#= ztwwEfMlC01s1izr8uC0hA!9KIYk$bxf(kS-IUe$ugBa!;Mq)on7gkW@>zElQ#xJRvCc%u*bCjgFv5oQW-vTwWs7J*e@fY4aEUf6a;ao2Q@cOoB7wormot)tgMcbTG^%k=ftk#nCk<1 zh$@juaZoi9`iw^jy(_y$NEkxM#5%5su{)?oPj%TTBIv5W*tuq~`H^dI=0eZj6{IJK z89hOLWL%H;v`chfccjT5P{YrD5+@^lSzBeDo2-bhh!HwSjJxiqk=KTiVvWvo|LDBF zhZo)6k2VTMHO}&s97+|M)O28f&}vOBg!3k!ScXgQ6AS|aU*beJP{B*3uz9!OLi~tL zcHv=LE!E1__==F8CS9Nc7uBQ+LcDuW~|&pU3X%6HE3?2BIhYQ=Jf#aLr0(S2r1 zbX(;Al@5y?sU+Za$>zvj6XJ<-JZMLrJPNvMg1HCNcv!WhD^G@~Wd>EeY1IKET~~so z2}(P4^C~p2s4XWRfi=}hSOvbNarT2D^}rMA+cLEWZFLiD&mOJbB9eNKsPEIEQ5+Nn z9vEYiPqgZd;zou*IdW9ks;W$Fk zO{nyQL?5@z@TM*xjJ9tj-e_o&9Z^C}1E+MCU_>v0bu84g_BBF6lPkQE-;OKbv1{(S zBhuU+Q+aR%*Nqeu_fxE3}f@3MV;kth#M=nE{2jaC^dA zMRj^`co_>PWz-*Ww)#$=*F=B~P4qjqi^z+Uh-G38>Q2%ovv$tsDIKh+np^Qd`#sbTm1$+sR2BB5_YjuqI_pHU(xDHPPht zQ&x)l{`r4GPoORH6?CWR{d^c1j|6PH@Y;u9rphb%QN|c*@6oH|bBovwijW}B3Nr^Y3&;9g zZ6urw`{2qe(OS=0gpw4In?bWB9|aC4)kY9_Q`vB-JkG<9aOZ$w5o(ub7*iqCizRIT zP>oCJShQA`Cp|i0&5BFm=25&_i|r`AMf=FC=W{OFdK&(I^6Nj5_rasC8J%`~a_x6* z4aqc203{Rh8;4;+rCG`}c_t4=iCVxb4)N?{$q<~?O<$)Qnj~HP8rT!=(bv`8DHYT& z4|TgG8SZe090IW}VHyLUFL7!arU{BXd1dDh{FA4sM_L`<0KbF?>qvMMN7+J zdZ5vjX!mPCtsR(h&w4M}T{4jq+o$=D^`gV-NGVwXg{>FMq?l7a8oWRotY13GT3{A9 z`M1f31^uQ{Pon|EH)bvBqkCE4a~5G^^fcu#fA1~lKv)~8$Lxb|*^hmJIJg_@?j~so?oCzU$0|??W1ax&^WRfxXiq;gs`}+6 zBMmSzWmL0}Ysj9Q&e0$qkl0&?m;{Q>g8W&S?*%s`f|U?8X%R0t0;{IXfqZR#RTROm zxHe-=S#R$F`{z*AUVIjZO(JKJ4I1Q6$9h1d;r=1)vvZ3(K$+^bR0cV~f4U~aOgK+B zcZYz;maS@9%g?l58)md)_;W=+y@*HUw4P)ox^r59q0dOC`j*b57J{?8o+6K^I`U+2 zE>0vXO>s;a!*@rwuo0r~men3%GuhQFt4C%PE@fDN9)xployd!P_gNOhnA$4{=UO{7 zA!&E)&bbsQ*0rCD7Acv2-zI}V(|4X)R`3;dax5`9si=&prtH8y-QG;P75d`(k%Lk) zyR(RnS!hAJGmzWA%s+}`yMg5kD=OB1~w!BQklhqL&b(g z&GHnuv|I+Kd~CxcBo;p9uk&LML{zmlaQfYz{GKcPh2=9$Iyo;yzUszRzYDUOj4b9P z(=GTsicR4|skwF?N|0mpG(#%Wxv3b9XUcFV92n$XD?vA!R_d7u;O-V=$4UJPah+c< z6SI`s6f{&PFyVHzE7?E}fzI{aiZ+nmN-r7Vhm zkcbSoH)K385E5+LK3D<0q6ptyk*|D|rpet>_S8HekC3hA#;|wJ43MLu?S~PjLVbz6 z&S`OJ;yBbiN!m1VWYdvhw)|(}TeUXYFD$o#fux9j{o3akEf&kBeJcStOhFtn-c!vC6;P z$G^CdIAG&lQhMHR6rKMak(Tn;=k8PRN3I;jA)1)3tVi45)#~mBmi~y^KR3PyKmj)h zAWNYWVZ~gV9?wfli&h;pk_}ue{KW+c)Ddqu3Iwj>_8cW<^Js@(7xA{l&N5c}Fxw@g zls=`JjCeOLFI@2S7Z#ytuRIsmJvz%(bi&@{*6a)tT15cMy3L_cu3&5>vqI(1M|bmpRssV%*$i zV8yb{I=F)bCLx9qR2a`c$qZ?&o|H{~H^;8dMDXHc`@D61o_ zAELc71(kd#u%;GMFERIk;oq#N`z*xwv=Cp`hT*)nq;hkpUD>$s>2s`MdN92SP(cDA zOsJl&uK1T#X}zG=my7t~i)>no_oPPQknp2^`f1E0?w4bab-tEzoWQ5$U|GHxN1j8h zWpd%~K{CPbD+~2 zW2cO4VB`xkyJCP=qgyji1MxL~m@568#hHc=o}0*~E&-AO838`H+;lkG)Z~0`$252@ zjKQqd)hv1Jg*=W!{a+I2FyRVRxOb*)=GDMG7GFj+0pOTr`p@fN!r_ey0_T-tHbg0* zT(csa?_w|XcU_YLO z>9dS8)vJK-7Cx+@T@P;r@JpIWUGZchZ)9L5O9FoT48O0g>T1ygjZBu7P{eFu!)z+wC7 zQ)kkWtKGBJk+>`OZV1CI(RGV|YAsr!e0wjbnFTy8`M}lomtiSco=Ko}L&Y0nr67Z8 zPK9tBAMVYC-%mutL2ZVDhm5qjoHlEc0ri>QWfJGr8N+-EAFK|K<~%Q@$($=ivNWUF0_ij@ z6ONe4t&LHh%ZX7E@WurlCoq@SELc;^J8G@89Rg$S!}ya*}aT(3(pb*6p3HzZ=drLEWS=(4X327)|1f=|8#@PyM?DniVO) ze^{)n9p&Q%$MQ66NssX}c(&!9`qNDT4^Ft$)2bSh>D#>D!PIw?YI5qIenEZTORf8POHVHkB{QCz!-);_l8k0`;ySvSG z*BKT#Z*MZrx696zrjAFs(jXSxUb5VqgT27XV z_Udt93N<{#ZsQ@)o)ovQ5xDgk34z|*ERYCD(i`Gt($+bvtSld^UP6}jNe?JPia15X z5FQ0#t9Hymi!DQ zi!EXz_rRGzBCeOxo{}~=I_wZQeTo-~vg1%-K)WgQ4fM<$SeyK%Tumz2mUN8yE@kT0 z*r)?ruWjZR%v$rjYJWu>;{ncvnNM`Ac%oaMS@hhNX+L)9D!!%i7ai*DsTww>8m+Z- zy)y!^QZ{i%8JZo#Z#-pXL4?BVL(DEz6KRBNm~Y$wnQ`W&TKFEq{UEj=q%(t`M%y9Yzr z8_og9+m)_K3SauB0fuH6>s>1sm?$5xw5-+{PKHfiEg+{xU2EQ*EsuT%j{Q>%(?`<1 zpiHOXu^o6CO9KC3=7JxJ0>RV(M(X;GDH=Ev7WqstVY&g7Y;?T@q<>1EVI%ajtG|K6 z7A3Xn`Z}h-()^mBS#a@TRuudxd(-2ti`C*Pt32z82ieb?5y%RZeTn$c)IKVlw@m0&IsPkkXOitPahuied1()?M(fnTM}=EPc2mui>~f zG}rxvL^*krPW$CHLthaCvGsv(!c=6td9}DRGOWw5e2A6P4}WwS+XOE9+8ZAxlLGfKISea)UXvS*imdSp3Mx~hj{iWM8>41oljC@8 zYPlJGgLrU%T|R)`?cpFst2*R}`Q60+#XN#h^`~!BL#Un=cLCQ_{yMffV;d^)Q;J2U zmdxZQ<~FDY0P^PqUffUhCqVhhlcQyFq~T_|E2fl4rDZ7lC~?lwB&z343&=2Xe@}vuzPh4@e{Y$4Jne+ax)=Q-abnaG1aqT@kg>O!6j!)DYm_xF zMvSYmynWM$>Ajxi<+$!eO_L=kprJF_kL=eBH_4Z45IN-gdw3cTh&T7)a{xu zTMNAbGmprVcfp}4M~pGs#&X{!i-oqmLMY#9HJthx{+)H{YWEPt>g4H*(kNJO_Np*u z<)#fg_b+4>GF^i`PV~dSMqTsB-cr(G=uodStgsie3B*v^UI}}3p{j>T8mavpXywJ6 zmHI}f?fZ@0s}uGggAp5|+ov3jP!6iOC3_B-30jw-ra;^f*qtidB$(Zp^~h-2g!Kp5 z-Hp=}wILLNZH7b7RhLg3x0nY4Zu=s{B>Jqz(T0*mLFSkNRrH`C1=CPuC()|@LBvw17)0dt%QhMsd-q`$7!G*nSlscYmQ%Sxu|uFe zTmUrupc;MIP3}?Ju`c7qKI|f&4vW_9v6xQk669Px3 z&vZEb6L0>u_9Ujzqc#v+N_V#Bq~&)S#7^0Ua*lhgBz!}Jl3G*-xSdwYE6hcr2eNOb zb#QgpM^)c42T!s+>DRQ@@ij)pn)3%1QM@Xyw3r4)Np+;)qj{Nl3n5Fl9?i$28w;B} z+fy1xB@8tjfqn{cFomaAnHXSqb@A+cjvk4FWinX#{w~H&aJRiGsylxgde)~hLi}RN zK`msZy0t-eczM;~$l~bUK;BjY&3!}~GzH;jA9RMq>bOReoy(0o#hkJ%LRvc zqRlhy7gx2{E_RE^7m;It_@R>KsrQlZ#=xbdw@M&g0}#V=$K4h5l{Rno;UP{P7?Zfj z?QyLLJ=><#c9*UdZXUMD8C3eNu?4Y#H%Jdp3 zyL5FCNu+M(JxliS;df*HUq%Xq#Ap~XvGLaOi+CHBvVjT$b;(l!$FGQ`0g^dO$34rV zB%5Ag%-nC;bO7lD(2Ua6i}tksG3X(f?S#*;unB#cpO+S%=?CRve~dUWRN=7mjpSH_ zq(6nNl29^XR@jp?ShDnb_9b{3GAvE&1AnKAOI$`RFiR-O36<4$zC6E+o+5BNMRrHr zo=1I>u5uU?LP)A`O{K05y08(!tjGQ@}Oz2zIg<^*35#1H_3L`Kd&v5&EV7FbNd3L*69)U51%!ja zj_#vnA0)wa8I6(<^P(o$TG1*I=!OBv)0FW2Ap@4M09k$S2Uu?EWA3h*s)qOkw|i)1 zes++r(lkZWl#!k$1QjK&@yZtCgg=^^^-jNiUFHf|l(RxirWF$B2!7_ag1wi?C)5Z@ z?$7d^>hj@)QG0S2M8272?hbNE1FZTMGD_npsng}lQc$bft;RCdOvG!jF)Z2#hEQ}{ zS3%ojyBNQ5zs1MIl{+QAuG@eEeDf}3BAF)Q=eB_+&P0rz<2qDMB~yHxv8eLl7zUvb z{bNr`yurv4ot2tnja?nlL06;d)r3(VgRJ;BwfjT(6Yzqed@Z&FKVoev zIdf$PhWU=lgc-vkjg`Zxu+-7mnXM|sFi@^9mW5v65!NMHb_o6I+$6a)P%AGsZ2r-K z*^JF0(Dv=?Kza!=!b!#Ag#iYh+EQawHD3&tH(@u#Y04NeCon!F7i|-S*_*>;v3+Or9Fof1eyZfH>uf zp6R)3e`{`xrS3@?!6A%Kf=&Ymus&`b)?KSra-ZBi%kYZEu7}OVR%XG)@uh^!TaNWu zNpP7>B*||+XK{W6)7{qlK#L1mA>Jsk6;7()_(wQ&qSD*Fc==F;9o(^cIf_`Ry;nwf z&HJz9VIiDfYJ}{#xx7^om%{fi$U%A`ByR~Kn?C1*Nl;MGSVVMSV5LV5m}P*`NJp-b zFXXpR4YfP-2PB!Z$s|GGVen0e%_2Hd)OIgn@^^YYTHlC))#8;MnbgIRq)Hr$uPp`4$@G@d5-NMSR@)U)dAP^B>^5a&!ZF(cj;mw0*D6 zNOD{OpTua5QS@!+!ULm@kn?$}3M@8Fho1KKKJ&(O>xvv~f1n9oZ$II?r!z zl7AEdJjq{tS>9>Ls!+gzaGkKQLL~fkXHDB=uj;5ZQ$+0-RD2}+fQyAfzvTtWqW8g4 zsQFVGI*M3mZ(moA06Xh^f^Ab9QuEKgdfX$4CLaLHg;O_^RGJv7BF*TV^G>1KLAe99 zG*e{G(lMvU%B;8u00ey_T27UJIXc6k4cEQ2qJFqG{g9bS1Y*kA85ss%=KWaG`@hrW zLAYTKch^b^#@3}PTh|iVg_ojL^iFCnC91Tb6&72M2|xWAMZDiM-Kedw4<8M`8-T9w zJcxFO90qu8tzSCi4Z^4AW zE4TlBb5PD`Ednfow= zl`%s?^1hc#dv*pvzjB`Hu4(+EY9CsOefHL=@CtPk$8J$UlCbw@t68Yl0EKB3{Kk0k zWak`0%1tL3tblpF;5l6Pj9`W{xrw+-_3c%ECRb9-03~kMs^A#YbXlpqp7ZQd1qi6!!RsBcl><{39?1>OrbT<|^uOs=#Pb`K?zDJCuTy<>5{uY1>Id)+=P(2#O2qndwJ}d{ z67+%r?}HG3F)H#VY=SEb5}a1r@|J_gBV+Vr*+DS_awT)~)mLJJlZa5;%W)(`JzUCO zLrCubB%xK?!=gU8@`|g;MEL3pL!4VJ@d>1Mh+LZ8Hste4;(zJF(v3|F+}yG_zMgoVU2Rv{A29o@VF#Hx6w0|3B=C{tO`9}QT0?aP2+3@pRaCO9_7&#F=gkdg%;pBsgss6M=Oa_#!=sDdp>1@!0~n3 zd)SwxaGCWX@S--e@Z)_kq;*lMca5GXGaKK!L0Y=)h5XZH{kv6M3Qfp6=UW+T;hCiZ z9-yqu)paw>t_2(i?BY5B8oT<&ru!PwF%+~9HP|kZmEc>j(E}L{Ok`2B(f)6I48&4B z+D-`r04FFOb{q)+!@UrzREZ7%OyFDv-0uHq$|$6Am~Ive%P|jA2cmsy?$i%yQ8lu8 zl^2y^55pn80ibl+C`wTcpg~Xc7mZKJ=Hz`-fi;ErDd1PpyQdvxaW7P(&Rn9zAJoEo z_zs`X6abiY0(P9)QYy+PqM-#g3{jNS+T_LU7w%|$W%CM z{|$M+NbSburShqUlaXEj_>Xoyq5nTF?LWjZ^#8O^;w6v^hW1}R$+q3o@yuJ`@~g*8 zX|RuPxy%*qj(4)q49`;D$sv{Yzvksmh8+1=wC%Y;h;_x~-^0t8QVn4Bj|{MrS1*M; z>yPjSDUgIht|{C@x^;kprgguB!3p*+l=eVI(`ePB=l-w?ncVHUOKJS=r;n!r4(1>+ zkZi;0o>FIq*04GQHiqCCLP&?2obDxfway0A2$=4T({o6JK5R)&uV>qDV-x>VhMs$`{7oKnlp^Dk0^>Zfmeqn)p=SDln^S$o$5CWAw1g4Sgaif-^OUmuPzK>ZEvI)l|=V)s=bnvL2+ zkvV2~Bww9jl@vuF0<;Lt2#XkAy(*Vo+&5WReivRc^~yEBw~vIUWm{m|M@^PGW7&pD z3nG;;9f@N5RFk`fLAEI|G`H*Ni;s!KLocfZ|J2x0dQe526(wVxcURxu7bJH<^;d}? z?WNKwbaf>3D(1`qK=2i^R%e+uQB|UK?^z+dpW-my==hzZ%flxd{3+=?iR!z)F+s`p zKYypHMK-SzK4xp_>VDr(`HKf7=J+Ma^YFo=*B(X#->}~YUl%={joKJMj2X#Bw*PDq zDsvD>?(l^O?Cs@pu%cnQvQ60X0=jj|GCC)QZ#k*%QPe8L&jp%82P|-0^ zFdn*+h(7vT)2)T|pxxdRopczXz-;o$taLV+@NwCckfa?B-26ISDjtt<5*0qrMAr6i zRJB|UUp~_u=iT$AE@JTQEsb;s*#qn?37VOIir<`zFxo0DKy{F-dbuu>@`^9gl1zOb{kv8n?g~E$lnbo5Q^nj^K}!=OvF-VJc7>uKO5aTurKdaN2lV8)8wHSjCbWM5689kc4(0?++3}zPJl^&T z*2oa0!+=ugHK8Tbb-qzneZVq1mZ4FeTZ09!@+8MWwtJIS!MLek#BUp3wF_a)Rkv3n8l0MK-Gol)Q zxPdx&L4nIByIDt^)!R=sob1e+j z>^A)ARL8%EvG#7tw3m&OaXS>}Cg-l4K1NH=!wjspORX!pH>_iuXevp#Br}xkg*|ho;?Ze|>Q{<*we#Gwejc zz0rHjw09P#sPif~>96WSF_B!kx`u!M!JOOz=Rt^CMf^ahKXk| z-#vqaXdUbxy+1454)iR5`4?D;SeHi2J1x)C%|}=e`iJK>vj8HRn}R0nS#d4T^VPi* zyN#O@$L&>^|wrnL2f0V}lX_tdNPM~GHw08eKqtWek8KpTzY>JJt?N@4W6;oo1g zcl)&5VDv((qV~zBtgKr92nY*W-~`<$YWN^}N1Sa?DyXy9eQSbUUL1ED{p=NMiGfLrIg>aRk}93QaJZ!bw> z;F;D<5mB{k9B&L>`LZ$I2_rT6ojtv-3!2aPklWQLl(bIvb|B4cJNQK4W%+O!f)Uk( zt#F9?B1n4HrfV6Wg6~&&(z{M~LBnB~Wk7B#lxh79*#qq&sXf7dACGOjGf>X6{u`j{ zYFTxp;(3rh?u9f5O=ixBZRf_Pp^b+*(l^BtkY)372kKoIAQ{_S8!-Xq$Rm)1pYJYQ zTF;0tw(I65Mk$kDy$XxZ{QL7fA;3BE)UQ28@^BT@A>~9z8_C`{O{fW2fhz1_TP%` ze{#Sn=Kc?y{~x{&Uj4su{!st`=E?sP--r31_GY&K?Z&kKNL~z|x-n0Ue%7%qO#PM# z;3-I{gUvg$r$6!|`fEp#3x>$3e9SKRqt&{&9y_3qSz&h6nk$6^uleag1#&B+-<9+YNx=+ZItW#xw7mT-H9Z|Pugc~*g zwmiG^TmUjbTUiE121l;4{<|_!R55sK5+Fm`#!l?r@WNm>6wvcd#0QoAuI{GxjB+(o zmW9p!IFGw1hr~r_MFO!T_lFDCl0U@leVCFt6;P=9Z2Lyv3;5$p;yn{D%+@TY>@4l^ zuv*76R&aMjPz$e_coRAA3zq{rH+1?+{f$Lqk`4Z&-$wW50fw)WlFucwd(n4D2AvcB;o-;Z#vht>-T##VnAQkh*6T^MZqqp`w7mw?^X`F6c@D-ZB=4IEjp4t3f%s zXaCoDPwuI|aA%KQnWx66<;Of?XWc^kGYsursx zmI6z^$H@QJe}JVLQz_^;rTQP(2oq=P&}l}GvAt&UR|Jq?6Pu4mp6s}9^y+}X2JUQb z6%+6xl|PN`GCXLLky?>@(;FBV#AMs26vF*0Xn%^xqcRDLWr+$8fd2}?18TX-Nfl&s?ZE#G%sM>vE~jHiLQ6f7)O2HO%BX43)@3RvtZP8>2OkPlRI&WX5j}e%1l5X2MzwO{8Ijmr*O3gJ5D%hK%G2p;T*5I+k z^j2kEn+HI{Rt^)*JzAf8pqo{{^~vyPqD^DCoEu_*qefvlyp#9jUuh}>V>r|_m$(*O z=x$Dx@yktKk-o;Ne|D|`JzuYW)3?vIoWCT#Q>BE?RD1=CN+V1K7iNFWspx*1(iXV6 zT?0suu8`3iU&>BXT8@uZY=2bQv-yDL$x5z#8Az!(PrCoVE%~267nuKH3x>i(AQLS6 zKNd_kp8=gjdUFT+I+B|kF+}*Oyiu83Zt2S6r|yUbVAQ9i4p0n>eG{z{DcGMH-K=G6 zWEC}2aLQg~ZZ}Vui z+iBK!!m7QnWQIC<;5F2cj|9Vs5MizBpth+h-_x~(_~C(zbUtQL!6zKx{+sc5gC~t) zMsRk>YP;btz#6rjTmmIBWd-D>*Gm;px4);qn86#!vl&bBTHmWG=~;tg94w>r*o~bS zxgo=nG77xwmwj=`Sb^oZ{+)+RW&+g{WHA;EA^_V!Z4ffye+%-+PlK)wOy#joB}Ntc zC1x328;8=iaRLZQKqmm3V9V7tOC z-mrWV`CSGBh&-NZi2j_3C(HG#EA`e2%a}Td=gbLe80fUG8sSr2{D}wNDzSbFm_!-u zQdHZDWRUWi!~Qtk-6`_d6a(ZKrx;i=^qVl{5r6Tq4;-e9)?jV>FpngZV6TBEp zLpO7x+n)XbYUX?c^%BN?FX`r8-EXOEeai< zxyp&rdz@yt8qwvP|5jtRU$gH7qs840e_bF>+@$a@Kr#~W z4;$^?hd*_27wqbVAiqcLAPA&!45I()ez>l1k1vg9e@IM!@gbf^!em=z_WW?|Z_op! zwdG@%dUG^<1lT&efr^yA@1z!Rt#oYG2~VxcR*`>Te$0K8ml6AZe#Xu{tX3qUhUKXs zB{Sa1U$>R!1_XlaP{G%L1jv$G_lGJ*RGy|^0Hs#WKHo45HhU2|p{U}?2;i}}X$W~( z_GIvmd`)S<$v(reyxOl0XxIGx&<_g{pzkux)&|luV&ykJSw7}Q39K2Wdqe)lj@Rd!)E%U%~zPyIcu>mTpoC$n2<){H2_yvl}Y;r)DtS)gFMD z6%k&Ahe=jd`Y%0iFVeoNI0ofc!s@b4+^ZHs#IpP?&dN^WDs^(`LKVA<;Ljl?g*gPT zNG{(4LfmW4+r^T3=Q?{o7u(byBL8q!?94C~X_Fd(`=;9Jg1xrsblb%naZ;t}0XCk9 zO31>$G!vD&dz{$*i;1i{=%CJ7T^zNEY)6O@W)cX{$ z_YtGaU3#o7eLFzcs2HtDO>*9qY14e{JAakBH3`X-Ck3!=VyAqi`U9^u^9J?F7Nnf0 zqeIUr-BuF5i?`DmR70w4Wg{k5WhwKztwJ~yHxPEH!v4(mrk5d4P1;n6HiQJEWXkD+ z1F86(!V+xQE5gvIm4(KSq+TSWpd(xaD0PyOnsEf@QoeL{Mwn%=-j*~~HtL2N?U~`HG6W%xX4cmq zY)WF(w)ax4FyTeWjm5^VfjhK9N_vL>!3^l`PDXnvif@KczE_nV}!I@4bxG(q9NO)cgcdsJ^`#T{s?AHl- zn<{>~JnBUZzxYb(s-(nz)!-@H3yq*(>{0;9g5C-ix5m$^Q9d1|WmuWX-k^^97hZvw zGgxK6F{1ARqawP&=$x+q6|1Y-G#q@By#XM>b?DY_6{nmC=P{KcjL}X3U{r{<=tfH-gC5*y zbq_GrVwl3J+Fzvg4X&A37HoFv7fuC_YSaD@TgcX~squ5Ej;*2%#mm@qy7Bz-M@Yeb zb9NcM>U^f*&hk;IRyI>|24Gfx{xN4sGE&>eRiTM5-$tH3OR9_0SBp&nROkc;-Yb{( zvBg&qjjebOySf(gi;2_%POEp1NNCQBg|Bp>@Q-(c>CpBxAoHLmPv))4pnUz+H3jie zR5JkrCem!I8T`fig;U~Z z{|4&zr6*a|$w?LWTA)aR^a>p|Q`(A+DOHdVL_ui0UB{FL=;_}my3*aHm~IU$o>lq) zKyA`g$}k8qX-FyjS9Rm=&GH<`---V1`C2m8Q<}FRzR1``K;OYhOdqy}G z1W3tVuf{#?k=byXls-nH`S+`Mj7>sQ&d{Y`b@njd3hOaZA5|-ZfVKB@{2d^}8fS&L z>+ZrtM2lGGesUHT^py3HC!XY*e)G&@wUBAke5vMwOA7xQwxZTg(o~F69H=r8!y^Pv zF}eqWuGV>odr)#w+8rcaE=@X*?@jQ4(XHDtB)yyk;o#*9PRn9C_H} z9T9)iW79l{8dmn5Ft5#6zcM3o2K8T(H|BO9ZFZFx`jXx0n@fk^0ZUGF>PAfa)ehoH zKRS2XNk;1CPN8M+?^D=SHQDZ>LoyOq7V-;m=%?GXr;5|H)&4HZqR6JNt8pYE#Or$m z2fXh^PUM4$bRPf9g+!v}4q__aE!UKfYolsW3&Qg8b-pysNPX=@N2m_NVh%JZ&&~0o zCA5}Sn#M|Eiyt&z05t0p7osu7hREA0%m{-$M0}sCkgFq*EEw{cQRDh@tR|)7lX68w zuRk7E#5NkR(tt)cea0T|8|IjoT7Hu{#}%!C>o~WAt8URMzVC{oG?Drp?_k>uX~vxX zx>rFNU#7=P!zhr1_mh~F(RW10a@m!PuAw{PJCV#8tC4I>ZRE>bl*2~A-r3}{h&lJo zb<~2UHbO4YbBuq5;%`#Q>Os2%h%UI6f)=5?(GtyjgQz#a34&OjKqCh*%>rCrYJ<63 zk2`p`(5??RtS}?fl;@Uy@5@OHxuta!6EpG{GH~SUF?>BK{>othP6C*(4J(yMf-nLO z=_1hefPycgYMIXpxP8u?u`zmwNhOQ9mHHz7{TXPWOCoa}sb+Pq)t$7a0AlM z+jb04=l|2G_L@I05R>>4U#LWu1PC;fIc+wK0bV5IbYx6Fs$y8%TU^(L*F@HGcJ)lZU^Oc& z;k@S(Ala(%^I0xENuBhw#t!)O#-$ve0xT;D9|{=!vp`F(i|=zKV{j%q-JYgXYDR4| zpBwKl+{s*gyM(j858Rl8rfv6ROlcW(I-|gxF^zf*6yKrAEucxKNa(<1L#mX%JP8rE zAHsZ{9wEFtT!{4h6>vS1;mBfIO21sh%MDa{UB1?L-%zCxHxdpkAi9zqIyl;LEV9E? zDFET*K39*cn|UgKARh!&(7n_q;ZRa;s3i9?pkD@Z=BR^ol?cU6hoD{|?4iMaHfPdb zGp(w~w4E{UAyEL`RcQ=Rg)_JWhhaik)*QOz4#%|xFALL>NtD)rhIA)$@NdjaBOxSB zr_6};*I{2Zs?WGaEg?+E2fcM}mZuCuR#OFKAycYpAFjlHuttIWrAB_*nXtm}LhWpERiUx&^ao;xL* z-s@I${=3Hd1jER5u&D70&qOW4-2Bb(7#VQm4_-3-L*;YRBGL1fGV6a}i!RunYLjf6 z%mN%Ay59X85D~kjgJH0vgRpaZbf1DJN?Ws0G3N{t3S=i zodDV5UXD?2(Uq{S7wuI&4$yT1!2{u5gtv(zY3lp9O^2-Z$9T-m*y*q_8(*W}G#h+& z(}3UAPm+33A48yH(~-X0k2`fETU*+!CY8ioL>E=$gpbn>XvRUwggt_R7KNG3endDj zNMhI77{7Mi&ekJueX_qK(-FGdo`nFnL+V$jao4A;4k zmv1c!$<$11A39dad!v_{3Y(x`9s2t3ufEX9e|;5n!AgP#4~JIX`S6em_|KgypRs$; z;F`dHU zO=&-E;JHkyl`ws%v-a|SVLp)~Zevxwm>e2Q@Auf~zwtN@;IhqHXFt7V>kIPob(+V?HCA(PPLr@U7W zT{@L#mR42|(62QTmT-DZG#jRv(NY6|Vui`93wAbXi#m`%UrlL3{>XC+guU6>jXQVG z`Zl*MAqZ88&}jn8%2rIMK$wn9_+1n~3=z>bQ*b-BL>$qniZf9t$Q-ROyGX0iR*_(Y zXeGiK1I&HqLza?ZNHr1kZTcSn%jIOvzt*t@ZyaM`oh8tz{@gxXQ}lT)6far`D)T~5 z!zaf81F~~$)T8DnWi(tAF!*B@BL7_rcMM@cOhU?wGAACFc@U|PBT9&yT;CD1BJb}i zO6=F5n-|Zq4DOu{ELPd+F65Wl_A*vmj)-p8HLU3B;UQ!6MZ^bxv^g~P0+toF>M2ES zBxh^@N|V_igEc;)$I>0zwl9zmnnCxFB9|Zx#w~=@0RO52lqp*Xr1~*PJnIy%6{xAl ze_-|<@YvY(9ZZ_gHdNI=B67g7iUFac7{mETu0o!nWNiuA!0w&D%+@h(TQN57xB1OC zxw%l7+&rl*3IpM0zkJP-hSxr+mrHbt9lV3Qpn{gJ)(wws+Ifq&RWe0S&>~jO3-^&B zXgv`ELg3qI@pKWCtAZ?Ha$XUlc%*_ZBpzjhK;Q0VhZg^eByq&}D4$dqz8+((nm)M* z%ZoMF+T6r>kmN?m3!~>=nTFz=`^V0;mZ9{3$BHOFnjkJ+YonR=`QJuIjpRn*SM4oyheLH)FHO|b_- zij;F9v!1<{RoaKfjQq$L_&cX5vetl)K!I=(8l@afn^j`NYsRKMC!hpdfJ}sUGHyMG z+JIk>?hjA^4+$2%`}EFyUi4Gm-Cd3=f`?)#BG^rshBGPsifa@LM!=RlJwov%I0r=p%lBqov=urC8+P&W$4!c#T6ol-+4EA#-lhF9MR`FQ;P_ieuxBZfSWyCg)!DiAMiY}$KsV5MvoWBQCqPl<6NVWM z6ok;*r}Sb%q=UZ1D)(gLF;_5c*;%pTNy*R=ch!2g~;);V=8n65Ha55Ev^NKuL#{9 z#Ef*m4X~zY0OG%65XZMfjp_tXIw7dhs%cCr(=8US2HfrYWj8QiE+&nc`{{;e2_Xb5 zeqsrV@!BqCveM!S7@06tTc@%06d$|Yy$2JX26sKW)a@M2 z?DUhR__6BgIFE)GC)+|!Y(st1z8k%Gii7J!&6Fh9ZbV#%b0)+dzZz4sydy8_zEZ4JpXkzqOSssAmVFywb*_d?mDAP3KhcZ0ZUg!XCcSDXSDeF@ z^aLb8GNP&&GEhf72s@(EXCl(Epq!=Ze+d8r<5<f4DmwYHgcuM-OwD)BUE znCP%4k`gGXmFnX$DULRr>Ga#989cIa5NBWCED|)RE8!(T!Jo3Wjy9o=$&n1|beW`} zaqQZ`Ii1~I71O%R*b2I&zn0hc@9 zIf{w893U+}cBpFeN}{-~V$-WB{#x_>r&G`F@HoJxtLX$GfV4s@_crJpVnLX-aRGh zr2;Yo(aRB*)X-i&0VioQ{G;52ON6?JpE}x-j;2?WWDFc9Jv{j$l+K$0EkbUn^dD$% znH_AZ2B>$5q@4ipGTMbZ?9_n-^2$+k><+&*X$LMctcN=9w-jeS3WW}3&|($K)90*| zeAWCFC9>K*b04b9KG#1K=g47%y=wF&iV)@LLix@N*-MHa4^bT%!h)@k;csQJKaPi)tMRiFl6;W(ePRnghHMwhPny=fn6?FFZdCSe?VZ1i|nW*_zX)Kf`+Ym!~(v6 zRILeKVDRs9Bwp%JoU-$4;xL)oJMu;}Leiq|L1@h(ZV=fcILa)S6A9St=$%W0E$n1u zLPOE6XA1Vvy%qsHE;X66=^WVgcurRvY#act+2#HFUU41k6uacdqAzKtM}jIb8yl2a zhC}P!0q>K(oTs4Jo3jsfio`lFpW;cbVwe(qk1Mg;TbAbgVSlwOOYy+_u0zGl*ml`{ z7Z8J*IwX@X<3~1->kzi23r7wvEwDjbAud-irZ_$hh!)l05fd`>(N;N+eqzpS(JGdMj2lSm_hlZlF@rDx98yKS7JyqdA|AcwRUB{Fu5n4&b;64;xTVoEbW(bA zV!KM71-e_YvKSM4nMC9)kvGPA-fPd}Ap~x0sq}j`;tM0U*&#ED9JNx2%I^A)Sg*3; zV*Uinmw1t=cTrknCkBDA_M&A)m<9ZGpdYQu|68^1di|*v=8rS%;;zNUk&PiXwe`~t z0ASr=?NX4e7F+?4x>;4yAm4ii(rwX$t0_gzZNA?^R(_PKy`>UgwTzB;qXhRawlg*Q zP*kCaFp5QeZPj{1VJ}?(bf)>_QN+qzZWooss{n)Mr;gl?Xpmb(>@)8ZB4W>*TP3)< ziPKwMA+V7|PdAi;_3drB!1_AgN#5~v%ld@mSJ8uOkg~a$ChvC z<<>zU=GaE)CI}qF4++WsW1%1EMf-gbxG=XBt$)fVi#5kcszH^Q+w)U}b-&ZYjaNgN zgb^H4a@WK^Y9k@Dt|X@~a7OA)SAz~R^W&g~>`C%*r&h6W*_NG1sGmoq@kFSBV7!R3 z#jOf0rlF@HEQCY}j25M1WzIQ-u?b|d=x#L{hzlL0DcFuim8mH{3jBv3g6HXvBiDm& zz0kKCx7@vdIv2zjo%}auL(AJJ?E8_tvjjxlE!xJL0bn@v4aU`s2r$qa{;3iRVyU#% zZuCT9(oi_d>{|3z7&3Jis?eJ^Xd27y%V@h}Gma$XAmeGL^wDS$18spn* z*2wN>C^X>w8jyM*^NgL!5BV)=R_a2N-if_P2kv80PDrDz+2{Q$a^TBBF;mcA&8&t- zLmNJsV4ncZhXi- zjQYmOT(ZH5O5-T)updrj$mBfi?w7^h+m0OU;2dQbP$Ri#qFTS~Ym-{w7HLqRMGu_|+b2Hi?PGz)L3iSsie$O25lgO2v;9tmwhg_z? zXB_(zbT(4s0|jdVP(1CycZH4LK!pG%6W<}&cjZql7)X4Kju^{1;^Ue%TijV*xeR%| z02A}Ae$b3rGOvI+@l97N@cchk6!(O=#UxKd>2IOW~DbUI8fcZ}bR*JzN|QMw)Bp zna(t-_+%y8)vz-`7 zY%hr;I6z}~7}7>3kr_cv8@@Iou6+bkF}%9E|1e#RFC@sTLdiQyG)%LGHe6K5?c=u@ z-?8^i-WS3Sw%HNO)OY*Y8g-b@x1uSS_mc#4&(NDt^I3n0-}4ZoB(ycDy@q6^iEurt zL(L!N;dAOr)!zz#DBLbib)PXu_JUtO=C)7wK+LQImmoJ^G5_pqAsLfPiRssHI4`V9 z0#S8wxz3S~U;DPdUhD6ug|c^Nn*G!>%f630BqtX#-TB^gA7hA^24~0CGT|NeuEdFc z#%~qBZ(fD_<(l$0Q};8-td~&4HPhE4Q_;b=`75Wja%#S&m)H%OhGtMk;R??%iOrAB1b-tot+e*^&pXdF@@d1F8`m(&Kvf6Jb1asdf_l45S?xwX! zZBG4ji*}~(*zL^SVX~t1A$R znRbKuwI70n!&Vo~UM)fTfX$_WJq;YEbg^ZI;mzhfCI`naPCu^RZX6EIJmL7(b_e%6 zvLJ^e2h&tL!Z$;#6ztv*s;RMM-p?Is3P>Ad?Q$WJ92q({0IOvCzI_7xaeF4)O+AG5 zFQM6roPh`+bA2ev*off#w7{9a?HXE`8U(kEgCN1Z<6XwR6SVV+<1N59#38#)QkX_7 zbP243Wd(I~)>FuG=N+U#yO@PZSU&PccVzN@c5FtLP~ULY72LO>`8Eco5?Zl@B=Y2wRnKWGcKWS z-4NG7!o_Jj)|Cdm?X~PNKYz<7R{phj2rE7cCKhQM*>%>wXMk6A-obi9ceBCM{CKPS zGbX;louLsD4iu@d{gki5-i9*z4ChFc!orQ!B%9p(EIbjUEnK5J1Iilyci&w)zQJeZ z6hZ<301JQ)DQqsMgH-Je*SRWNT$1}E`QNB2=fOFE)6cQ$IIb(_R=#)JLUU$=hPFU< z?_2pU=**C)*9#_qw%lW|)En+39v5_j4m-S_TUOdoe2Udm^XN!MFrsLZd-h8?Uf6WM zB3s+9sCHG*3p1$-7q8}ZKZ6?GF>|n32`Yr&(Lg3-`uxHT{{XY@Y!{9yTuzS?g=xP6 zOs1d475M=Id=u{+)(#V5roE%Js@tK=>nJNea+KSbB)x!{vO9?*p^HAse{v@4}*bO8!8Q1mMhR``K> z{Wr;nCn2Ost*S+7D_ZpF&;u^i8i9~&6qLSPQp{JXD;kc2Do`&wT3mKORizcHq5(8V zz((RD5`L%C>2%>G%lYf8^-%QVETO%;&BAbb)6u7dNUSy~jDd4oP1>0i=O02`)4jCWNsuj11i6oZKOMf^q`ZP&~i=}y`X?Qxx9@?9{h zaYO*gZN+pgRSI2|13fHQR&571PvncNtl*x06SOAqlgB*C2ri?utq1k!X99Ks*z#EO zV6g1686Xyu_jeq+VYXetjC1##SNhU~j?gZfB1ZOASmED^Ko6DYclw)`jP|4_3T&t@ zt7%vm3)%G*ah+nccATyX17!h8I{hX@I1+eoA^XhVKapA|v_tQejEx-FuGt?Z%4%D9BX#F%b zcfA3QM>jA$vwdAoBmvdhW{Qd^c$s&YpIX3LB*J{Yuq(^0hEpw4dv%wKw3;m9dem5p zSPJTZ7x034p6`5oaGe-6aiku`J+-L_Mhc4i_XK22cB2OtH^r^(nFoDre;MF_^_f6};^sEJ z3#fE7CKeAluFet#b7aG8z!m%sD(NQOIi@@+{Eo#@|D(8~TiG{;Y#!PABH3gEn)>iH3CLUA#e* z0U_uOJBV<7Mvh&S$3J1Er!UDOFRia7hBbRddT z(c6}E4Y;|1Qv?J2`y$qEwjy+hul^zJo8(fwpZOrh-4iqqFjyUS>%`@Bjb*rwtV0q9 z*wUts0E?Q-{yX>Qbj1~-G6uo`wmv;I6O|{%#FwzV^d53K*@hSrxaf)Q9&p2_7P6t!0Kprhy5$uePf z2-G6(LmQd)^faSWRlcRab_lUU3G&pKhFI8!Kk=+HbF>a=>6 z(L3b!WCJ<5GCl*P%>^@}!$a8WQ;>x+c8{YRqu%#hN3@z5`Lff~zkL-FYHv{HxA-^g z_pU`Xc*sKJ*xeSMA9L!FD+|g6gIng!@NL0W37C`w*`TeTSEVHP3I;RZ84@@>_$$iV zx}uz`-bhpx8BKHzdR!qzAV|vh9?klLYzPT%g;f~YW&7#Ts&3a0>&znExMCf*n(i4~ zrAuY-FUNg1he=Sr*mzZGgp3dV3$e_nf$h z4!Yd_Vs+2gl0r<>$TsjnwDC_{D~3<1d!#i-Th+J@fxyOX(r-=SnT?GJ5E(sFuc{eY zzf4#IiAVnKJz|G!N)kpi0k!1PA4OtrZm--)V@pDBR?(t?Pt-^O-nCn3ul7T6SHv~5=O{IZTf zf^p94)^Cienf6f+TDNmko1#xG$)x0f(s72c@cIa2@4l%<6HZS;hUlCbIFj?^R;7#b zwngr^5(qWlnXsl%0nUqol`l+&8XLyEwU9|b48hNs*pNedquS!;9phc8Pp;0wfeqy~ zry-6=LgxJ!kDr(Fdgc)k-zN=-C4WC=4YgmH@d+uV`c{}xW%9-*=+ziI zq`d_cH5J2^;_oYa35C7s(i07P%PtW(=l89t*{#Lsev!aM_P@LO|3*~N{oDWlBdYQ& zVE>){e-Tx%{~@OS9RTgMVfu&U7J$|Bn8sR2-x)RQ3I-_Fa3x7KP5*!5LoV)8= z)xxWexO=^C(2I+ZgGbvab;+~q4_0UG*B3nXklr(-Uh*--q$3$wtjei^?1zvs*x+0GQ`1Cx-|c!)U#q`m&qT0kkL?-f#prEToz%f*^iN%n}v%9>68 zZKbb$O>iS%#FhjD-7AnV9wp|ljf?b z%z_Qqu<5lXy5vZ7yI35y(&6x5-^-jJT4w3IZ75~kA7#PS2{8G#w1&r&f>a-JcNss{ zEnHkyv#%#-gKz>K5av*7M_VO;MLn6&)|qh+HsNv#Ew3+}nZuc~c^w(9T!zJeY4aa0 z3huv+7cTruhOGZ2!>_d+LAQh38eo5I(^2M^R8MFrQdQtaxQ5C{kYP3?m66&>E0x8u zp^W>p;PE_vlM2E9ggqNOov3kH6HWvY6rI4C7F&@0C>UX)E zcyvcCG=4ENwTfq4j$PxUYRj1xI{P3;vWM?G7`@T5u+_$?7{dH{N*aHUK0=8O;nK54 zbGyzijr#WQDnkG1Ft;*zb3bJ7Z<>WhssuAhCL#CK89$K3u)7p?vaU#I^h07SN5&f9 zznVb;Hm`37K4XmAq4o(3L$%|G^6wu|2+be^ZQLA%Eae&I?|-UR4gptmz%NJ37D2OY z9OAHg7$~b!d)wAP02sGST!r9yFK`tdGaC|bZeX+Xz~!VINSR^28Z#o)DYJa720Ay} zf+(d-P+YsstP<;YDRV#Zp0?bDy(Cy#1P^#&;_bT%LG^!u8`Z zWf{@N*&`DWS@hPRv(NayC3EwTMAxemAiRl`agQHPhG=zgfyiswGC*ZQSJT>lk1UN; zz$q04o7PpJ%F(H3&Ln!KkZ5xv$<>AsI>38&N2H}V0pPtFxZl-1c^wh-N>}%u?<(bP z{N9d9#AX#&n#u;lOGXj2(vhc}ko#{k38j31-F$>8KEiC_c*z%o$Gn7df;M5+q^3gI z?q7U{{$by6+LL!w{yv24l}(Q}m%U6CQJZWuA3f`iwW_*kZ}b{4V&=Y)fmmIJLH|2- z{>w-qjig{Ec)nUhP+Ns|U^eYC+Rish6gE-ap`#?p`AxqY0AkTF@le+tL;}!y zU!+&EU@R_XB}^Z}4WdH%)EkD*U@zYx$G50*1!L#BDP@#l)qHL~M+A zw$fMMM&)e%nBRGOAVpE9Ub)wnAqdM%`8)EVb+3~MC}uc@>h+6{;Hr2X*~p$Pr(=v> zus6+Yz7F`yq6K_oN{Z>z2LuQlv0++ zr+z=|x^wdvbX^6~&l3i8uL^(BM@wM7sLF*Wsegb7#sjmW0Wm22#cvh)vpe9Ex;^il;8K(x2kkrm11Q zKRz>V%ALz?^vx#TgKh!p^mnCnkKiA;b~~Y8IF7}0s=r*qRqm>wyYy`V^GAu*Zy|gz zOLo8^9tl%*1?-jP`DQeRd4F1g6HHx}Wg0VhcEdLGKmwFqNsIlS3@vpX+F}dN4msIJ#w2L!oU%Lm=Ik-`l@S>Tf$aDPWio!=95O_OZ1b z)ok$Quv^X(QQzQ#c?uw_d3v9*Pdn!U<5ZG`xpmG&1UdMMrp+aEb=sUu>MKus?QXfOY|&LPu9bdgXt%~hB_ zS7q@DWrvJOfMrCK-u>MWU8HY&-3`QDi8wgpKIGunqoVr63Y&{I;#UMWD}VMSf)r6- z$jdgon^SYX)fg8x72m0xG#I{?$kQ=1si1WK{^Sf z44FVDa)Cq0bM!jvj1#tC^n)OmNppYY^kv)h#R3i;Hty ze}p|^=K&~t@B*fO6chQi*F^ClfJFL;`b)F$6#f*M*6gi!_4crHt)a7zEW6NG9q-fL zr)pDhLb3|V>3eb54G(?S@ZFmgzouXyMfE~R-yb4sGUF6R4kDB zs*Tj0ceJ~|z2Ky{cC_t?mliBS)emGNVo88>R-9l-PEwHsWV2iYiQu*}SeXB^Bh33& z08*w~UljxCGM2R8!`?gL@VcF^BRg3~fgEm!T^lZ5!hxZZA)!9Ams)1tC8@DkF3aLCo9zlU?8 z$Pf^%pGCDGb<7^^nlJYfkc6}MmMrM{ zMuR|}kP*b*Qwf{1;l1kDt_W~^zO5$jwNd5)E54U(SO#B4F(tmYS~~Bn{mTA(RfE~; z_~o*W(xrQaWuJI-*ro@S4)nqiA+kaK72AO|(z63~d$?npB`qI-ZNU=|@>~qWY zdkzaYnu3UY;Z@sl-j1f?eS~pxznd6BwBCMO`25%SBjn6)S19RuogP9MZat7Z|RFqj=KBaU(8i zcY-m8EUD+(uR4Z~-BkKR*Z2L(uqU@)*c71?oChH`;hXi8tDTOUw#r?aXO)eaisK0g z^X7Yw@|9YyWA%GEg9Uw*fO-h_2(R@h*^9^j>6--e^mU>|^PJxtgSqngnVNl}N)#j4 zR3!;nN+cMl_@S4_RwM^BQ{{GmdAthAi|9pll_5Dr`UbdvWV*5g(*bu42jr1QinNrl zImt!o=Jdja+l z8y`Z$Qr&6iB_y4r`313R^be-Ri5JUD{>1Vv%g8)cs6ZcA|=iA43_Oe7zjHeb5#SFAi87^m$Dq3@` z?H@Dx*vgi6%k+Yu6L)VWK}5hTJI^1s<%kiW?% z9Hcc0%nb%Yj+~fvQ}FOI3|;$DKL8%;R;SKm(qZAbXTbE7mbvdFWSi)nE|X9FB4aeD z?Q2W7>Y)uWH9h4OX6{LQ`ag0wHnM+kK68xb2a=Di?YEhfB{9;=0{t<7VqhU+{)yBC zi48x~O(xEuLOgPs*26&|YHKSLUc${h>aZ$a^vcf)QTe5}tWzzY3cI&Gw>K<3m}dyQ zxF~I9)xn!!_N`qNbdk>t7dua;HDD{$@mdlg?z_53{n(pq>QR5d(}wo0I#jbkxzLhy zz5PV0&Wrd9?XxC&Wj;cBFi&Kar!1B_hZ_rZ<$4k85T|q`bK*1Y;oJDCIy$My;qs5! zWwMqPBw2bGjeBzYFO_!$uTRK#KO6RuiQ%sKRb(A%AHt7)p7emI-#8z`Gu$Hbe-*zi z?oAh+(|=QmzsLa92Y*mI{nG_!IoV<|iV*oWTrtL>BoknMn#u-=3d!kv;ZY(4$Id+n+U9r|11Qk+Yl_X^_H$Cv|4K!+nj4P zs<$opb|ZVPOVX#FRtc_KtD@3e%AJz+71>GpgRPA3LmgRT%)A$``WO3Ill-4(V>9_^ z-bT;)B5|Ri1Q<&vM_!BDh{HtobLT0j_{iRKcYoyff|Tou_(_yC4bpMWSC~*xkhG_c z&07K%l3K%xcD4*tnln|7tq;6h;*$Nas;9?g&yYl^L?)z(?$aqAvvg$5jTI!!;^kDx zR{&j`+`ha5Uk6J0!?$75N6T?8A@w;cdG(8I*Bc}xT*DI0suC_`bH0oKC3usQA$6p&$3<}>#%!^*N;TMjU1!G#K zZ@z`~mv_V^)6MzVrgl)Ba#>(P3cATQGLE!aT6EB!Lhbu~UOra;mRX@A2~K^#E}E}u zu{Re91n+AljaZld=&@z@vztA1xkgTrH*1guN~Nfoo$u3>zI z=`4ansG=)u5m{w&5V01P4C9nT5#d40i$SSA+@ju{%RL!f9 z#+3mdY248 z3iXSDUvTili|KW_@uxnSQ{5d2Eb6_l6C3?(TsG{2y0_rpn>UFJ8FnV(1A*w8(-BZA zw%e=d^ccKN{A*-Vja_C6WiQ8;AlsQ=z^~8YN>ErytyV3i1`TeL*Hoj(7igm{Tq@M9 zh{CG&;}Khk%DR1tlsr+26?sBxcDxnz+pNGp%-d)TRmv(D9Jr>27j4#v9V*QF6?nxL zB!N&^P#vIIv#cJNItkbF*9)wjEJvnH1WalQh+eOOKDh!^(R-nfWTv`F%0C*F{0T{xb6Bc@`!3UU+UZBOFcWwi%`?upwBq}+8SdRGXQzh^^y<-ftg;}vC-ejxQkeZ@ zl@w*-@mpqPtDFj9!>k0tY!Wh^7xxpyB4+>%;b3FVxxr!ilnQN{e2-8!a!gZ!K}Q?cd-8MR@<}Kjb%ZJUGf+uh zf>g7kv>MO%+op&p8T%z3dQA&_YcWkuBmfpNIbM!MSVQ5tC?#&KT56U?sqtZkcUENYw0mXah;(#g0Gv~!=n6$aRBaNo zBp1!Noww9le7{#Ic#x)s4{ozP&4djlnK+0~_XX_cw@F0ug~M=UY2?|4xU)3m|ts&*lotGu-9%&T=*5*xQWn6Ktj~sN$Jr zz5q2cTru=gpM2fbE|Dvrke7u=r)ErH_TJ)PHTWD>v-1T5<~?oTnWs%8I&K=FzjLE@ z$QJuIdTW(+i&$!-ayT_UOQc;M{9FHW9M|y5?p(du%seJiMNe~=ehZC6r~#X#kWvpi z=G+Q3g~@samDl;B!3L^lRnel3)+$C0g75UeJ}(4eG; z={Nco6R&amKUe4~pPIzSbWsQQMr390=(bRKs8jfl+!HXp0hnZso>O#crkG_aRua|` z$3fLekE;w2@r8 zZU!%`(!A31x%kfj^ALlNFUr7kW{-L<6KX{$TW4lv0NyjOve0|() zi&reHY@qN8ZbkmNbUKktCFwBfC`ifp`1J-gtxd&U@+qzoo|FNw8uZI~;Z^ts5pBho z^+Vc3wNikXf}IZ+`i}xyAVJOJPI8ZH3^freqa}BY#e3Y@+sWq?jYiEC56G(Tfjat3 zdmJ^_`7J*Wq5pvM6vlDHGrhO@LRj%G7!`8aAS4k8KPM~$^Jlf`~hs!3twtGWjt2koF2FTT^)ug=m*_|MSF7bTy!oz3({5%e+}BTs1v|}arjgCR-5GwHg%5W6mRBd`D^4E&-Kk|S$%NuxFqHe=_w#mC` zZ9!viquZ_k=D>HWF8IbCBIw2}LMnl^^m1srEb+Irf>2r^x$993YM!E?9)(NPmv%j*mH>X#-Gp0&gSbd<(x8c%63!D{Q@_d9uy=1nkMI4(AWpKW==L>lPqAcmoGNN7k|J)ebD@Tn z*tRUEgd}!%4Moe311MviKP3 z9nf+`H5ubSd}_~8j-=@CVTq=pgfpTP_S|Ayw3fw*hxtT5BwOCs|qlqSU%h` zIWvL979O}pE~$`_e_oRS3CH0T;nCQ|k))ZPan(8`qZ>`2p0e$g{Z09+D^M&^^i`u#XqGRY4;XFg9AEG98C_G({O z#OYJd!?LNF+urnR^}8r<&*NY&E{W}xscY6|j&tP_6+ICok47Yx6N=mK7j26zwVE~5 z*AF0D3V?WeJ_)3kIMi1#A*Kb#A={Am1(kfrie}2F}=b7WvjRL@V!`pEf zQKjoSz318b0RF}j7pFNAIKnf~%Yxjx@Nl&5EPQ} z3h%1gCmd&gZ?sIZLTH3cA9Zt#h$&}2JyYGYvCqhler)F~*{SiSrT$iY!gvZJY#P=iUG?D{>EfJRtU-aUOm>xwZ0F*t0A_{O#CaE++;$s^cO+{HiC1qu z_Qr(Ow#(k*-vEhoL%Kxv8`}F!|1#-q2llAs`a>K*^srfBqI+Y2|{3rv64M z$2Qhj?N!ErRl}n!-tRA6+a;ZD-WF}nhROlm4~8%U(8L+ga`?s^9UTHVXOev1k?IPo zycvkv+VewsVh+I!JCiAu8$-9uWVHt{mmMCrrEH<-C2fCqnx+PNbO5$`cqF@u`om@$ zpD<7pG#TSMmQxP1I_4nm+|ZRS0gDdjV|$J9B?k|Sr~XjVu?+bgd&OmeWlfH-*R;6sMj%pGc@`|Yw+ zbV;<|X&u&cDTb4C7Rzs!5w3X@`Fk%RpNWn*Y$^{&o|;qA=z=dvLM_}G2n)cE_1nu5 zw#ka+I$8DCfl;au^Wwun=o#0_KJe&e^4HL)Ea3Yo?RUPH!7N7>y4>O#KNeI+74=hV zI^$|mdj$tORbu==7U{b5O5)L@5hxmQfA?@6Y+AA6FiY?T!;|O1{mQi0S(6G~KM@w0 z8SIfCtLBD-qPpYWsiUB|x`fT5p}l)PZDc3My^$02R2(%v6sf0_YtsN~e0J#}C z&p76A1Fbm-1f91TS`ign*bnUZ*7;gemJCpvORY8R!F#d{w<$hM@a~F$)GiaKqTDp8 zU9;_ut<}N0B_eybU;?0j#~%`HGP3xbegS-F}((^LNmHQbe_^@AnU9udZU_Q zOVF7meulGl`N=a8`4iC@13lOb=AvB9WT|1(x8t45eFa5z-@;DBO&>1i#uKm(*bMp4 zzR8z2sj|^+ST%eOB$J0)STFiFg{dzWr zeRAE5b#3b>hc`gKMKNs*IkN@oOMuwX0tq;kiw9 zc)ZZ>52G1%bhfWhlF3U(;)XPf9)&;*Bf$0$xL@{Ws)zJxwfV_K6KWArr48|g#uFh| zqXiOLMhCwx!Aw{+*}5_BI;&ayggF!6*X`>GZYPX=B-b_!#Rj=zM8FVZR(sUgqACb2 z2!d@tK>hgw1W;47vcW8>(o05`ObsahqaS$-RBgXtDbmMETSo3dV6Fm}WCfqueO?v4x=j;$t+Y zTPG(fqI9i6@}iz~0YxJeFB(&GdVMK}#R@(1Q0knfA|08eEtnj;&mLNS z1R#*GjhH6gfE{flQ%0OXpM;*z@r<^)&V;Zlu%})0&h! zW}JSPXyV;q?MV_C{iIOX1ZN_&t6TeG;oD|!L^HCW%>v-9M@a?>Hgm7TSYQMvARF|= zuW|k02B%DSEIau3gTCZQ<{u-;i0NTrGmW;*~q!h+1a|XOg87IJ+^Y&$qJ%*Kf9K6f;Kjp{(hC z{L&fm$Q%0^rzy9Y$xA&BC$%4_PTIVWqacX?UTg9{7&m<6V-mPm+68!bvJ1 zo`o5uL~@I0lXrMKInU^9eM<2vCKH8sIruY9UhQ=0`({cBt-Q1zyezxw#_VsRR2BkP ziBV2~TU5zZ=g(9SY%58ZhD)>@HNj+lZ%()$8QKZJCcN>HeNBj&wh--rW)68OB>cVp z)N8(0Ic3yZ0 zi89Y5wC(n+{86u>)1%MlDWuGoE;8Or7&Q_;G4rH4JHS7%y1jKU(&~!6+pkA>9dkKy zyN7w_plyUn40AQe(aJOGlMPe&`cPtxHzX6O+h7<6-qp{TGrVKDAWSP9=t2bneSl^$ z$bR#nPPkf($w>Z=+APziGr=opiZNH!1crK0?s2%gnmXNt0Qqt*p&A|SR#xPx_gj;> z#34@#5?qzj=E=3;a&wsYex&F+&ly(tQXblxM@(g3TQeWV)FhoBm!gUNVtoz~>btw) zT|vNUQ!hyqU1{9PZ=Hp3GbDQ4sKfw6+$t36BDHrlB2N2RQeGQAeIGPGA|xxNhaWA< zE^ufpMuC3TfN;LOSyiSu+~^aA!GM;9@IZ6A44&woZ3tO z3Oj18!;8=`<7w;ys+bX|cn?bV8dF(hFn+ z{-ag;?6BbDJsb-}AQw;r{LhsBj@kBjGQUaIj625cJ9xJ%Zp4Nto!~VdlUwzf z*lW#TUZzj7P3!vq%&Y?HdT#;7h>XWZs@Pvw{|FT?WR&&`dg={^LVkkx>crX!5}2*t zfX;`%EpXMtxHa>_vfe9V9qRxjPcM!<%FzABKfyDv;QkQ+oftmERlLAW;vZf<9OTS< zDRZR{xpk4#u%IosDMC3*Adp(hTpNApW&s;$q`x#yKVZ!?`i2KOC&*VcExb2ak?=#g zkD;3EKO1AE9>LUsBz*vy$j|q>j(4uEYE$5?Uths?t4ZmC+@#6oRsW zRl7Jk=N;jL2sG&&e$X+}OMr|tHHK(!^i~q6WlOJIJ3(IAp4a9lMTUS~=!OJmvrePC zJx#?H@1kJVwUrewrQX94t-3$e7?~+->y}d7f{x2WgWUwL$HFu(i4Og3)O4bRM@{a9 zo+=W;pN^DY0R()}C{0Gampv9bH$m&BPr1+u zMVU0q1VHHWWHgH!p@l`pw!TU)_nTx}$?ToYu4SExVlUd5Z{%Jw0jW83?j~28fxZ+0 zFzza(`8cFFt2tOB_TZgpIX`zw)5Ir^50oQ3JiJs~dn%3-0TbH9Exa(UV3xnY!nSj9 z9LN9sl%i*vLdbRH>0lpCaNK<2tLqWeo-NmwR#ag#r`^D|mL+d2{sm_BSW zT*ta4C62xGa4>Zy4&EWX&zI@_)r`7_EXVO95*Vi7b@iBWwK=dPHK{h)6)h9#o-jLz z!X_YhZJaxZw4N|*qo(9a*-AI`fbtFCH4hKtW%rF5sNpZpb_eU}S~2KyUrK%puz3MN z<%jHV2!@(?K^XHU1|vIYFhoAZ$y75yq0h5{8Z}zzL32BnHpy80%-l6d|M_n|ToUPGW!#e|vt?PQJ0*3rv;V6zKFS=m$Am&7j;HlVx^L{eyuoKkT6c ztCeZMLISn^5l{JS2$B8SfR|X#I*uL4M0tU#PwrvWk>@DN+On7$Z7d;Ar=8 zh>HqZiLk%9Mk7%Apxh(&P=65Jh`XsMcda$69tGXS40`jzqRwn1*U_{%x*+bZ6S%c{ zqBh9}dC<~^A9065=~TO&BtyV;VP_Shc!A)1f9i$AwB-f%Q&19iErb|RDiedeWL=b# zS@^phr~$wJib}KX0s10{3VLYLdorH;1Hh$LsP>N7+1q_dkvk$$OcId#^CiUfMTYV7 zFMR^d-c&zO2!x25mt%V%xKiWpIb(9ZT3QJi3`wFvEO{MQmFdJ)L6N362%UKe z<^fm+zRtxeKGGg$6*2c}5ZZbs0q3d^v(JBN8twzBqz||dU_SVFZ!r)l-oH9u>L16u zxQC3*QYa0$P-1XHl50V{zsC%CzXw zeVQ?dTa|g()d~UzG%G;aB#x;h8%&(DEBTxhz2Azsp@WDVlj@me<<^|wuZj->UPZA7 z@yPDEfBme$h3*0QY%U&rV0M}r+7e(vz97YbROYSNw23Sj)GlSjHpq11M2FttaEm185Z7sgoVnys4HLsCo zhwt>DT9piHAJ_?gt9Nre3EpHPo0j%K%i=){)ek*r@W@NcR)fbKt<_E;CWPcr7DHL- z*N)o!@%{py!ZKI3M!+29$#mdLiZ_g(yGIDXbmsz!Dpqqc8^;^MFmUH`hnR6zCjri# zUxB#lQf(;A>9_{*cgJrj5v*H-{$%tAwidlcUT1nD>C|I&n+&V;e!o=K_G7f%2DMdjU0B3~6-Ffc-6Cdm2rnm-A_&vNCEHl~vK3^-tLq zuDW9!G9OiV10KRe@m1|P92t?ZUFYvEAp9$rPy8dDDn|3~tzC8vMEUB|FW~j80j|k4 zns6Y=G9V#Cm{@x?6ahRni)8JpLn@t@*jmjKhHv2B_?*nS_T{~)j18ZNpr7y4J-4kO zHpO-hxCgeOPhL-Nd)jVRI}8Exf<3@)6m8``**8-sO3PI5*Bi04CqfhXXY%8?UHLql z%_IXuwq5)~ws&%+${>DFW8^{E47puQ^oq!g67xhe=P!YFRIF6s-wF1v%0FcVOXc;Y z0lqu6?C+A<(FCK=wftIQW#^xp6u|)aE6nt=gpMM=LenZ^I}g7D4IkFPvJb_ScWS8I zkbCSr?%W!`w{QbfatH)PppT+|Q_5Cy+C{tjqhsd*2{%i5eH>T0wtYL--vQUkSgoe? z!vCqKG(uY4&q1?<3pOIN*!%$yp`(rKbo%oWqT~x)oC%Wj5#82ZwCx*g7HeHo1KbAE zIME4#8tr9kkb&h59HBb&#xx|dVpD%-HDr!#g7n2P~%_8zw+DKEYUpg?9K3*gn6PE~&#<*-IcHvhd`j?v}&Z#|Gho|L%vs z8J8MWw@I+s6_g~wQ2j%BH`*ancH+`v(C~p5lcBFRNICt(^8oNaXnUt1(Sof@w`|+4 zUAAr8wr$(CZF85+UAAr8)~z^w?~VVVJEC9CV`k)fTXU@(ne!W?RyDghR{jD^NkI8a zY?c3WXJYindn{zZ(m5P@DN{Uq%UY!3s&`@!i;0A%E?DnFJ;IW?1c6PAoO4NId2<2} zP-C~T986w{@n58fpg)m+_w8@X^8XISq5GZxze90q>sWr*{vS{rFv$O)Hc+%f|1XMT z@SpMT1OE%f0q*+G_&-<U7%ym^KrRHH=x?4R6=!NZqldY!*U1j6r#Nxh z69*aWR7u`Qbd+iJ{XI};om*4|Y~$SBwNJnC<&3oEdh%>5Srd8Q_S_ntM-0#4-S)cl zW`0W`2S8AX^FrL-VdOFnk{>#>q$mb97+QpR_;gP}^NwbSyT5m3|mMF}n&+3uIK=fQz)Kt>1MGB0K;vw{jq|QgIpQ?&qiI9f)n$*OS%& z;&;#`Vx604_KihFrECxXxr}d%#J_i%$l9okCens{suLIEv7BscZ0um9#7dRFif@5z z#3Xn)NUkrV*g@MB_NIM~!Q{{B-2K3V?ttHRT_W3|ZIs!Vav5~G8ekBOm3><1=Tv8H zBcAd$2r{BPKPrCjz-Bew`BPm+=fT{ct9iWGavX^HlwzW?V^qVHG?>pzn)ZDjJ8Yh8 z#Xc21z&7}zePkxy|Z5WHh=jR^H*7pv&dH8dD81DIswW^W>NDVC(eE@#|P zDr8}}HHa(3^Ikj%r`f}@V49eTsrI^XZK80IhXD50WAC)-ZxX2b-T-{L+N)H+ggu4k z7iwgs=XP8JOrEhopO@Jmb~o?YsQ3J;1uX$z+fV!|`#`f-8YPrGUpC%o}eKJn?+PN;C;myFyUw{pHT{}$D)w+}% zF$98$qx*4~9Ek5X$=egzQ(O_@f51yG(s5^g?U6Bic)p8@qBj5<9JgJiOFw8-@J3#n z3jEa4l?r=yJL>(nkVP&5N(OTudw z2*XSWs`V(EuZ#xQuFI=2DjJDV*N?{a7&+6LrEV%jFgjJsltGBBGTpwF4+FCd_>-uaG3fU#&Wk|#B(FOTRZTKENvOlr0vMD9n*ROnEUeC~4x!sIS4_4xAc zJ;{cj@BHS~vA7+I)2D1QG}vmqm-G9^Jq=GP;wIaiLK}%9t=Va})!psrahs38MGD)9 zI8U0hodu(k`{+~PGW23(`Jz>An7K`#i0VmF*KRk_1B>(Fg>y_(q|b~I=yLO88lBL6 zfd~sa%C<|J{OE9t7j`M5Oqh-EK7Lgm8!gYDr8I1=l`ygOAKjm#F^;feRG0pECc`T< zdU6h_yPNy|(Rs33v|Nlaf591dY0yHiE28{pWlG!&_t`;*xiNqdOYre5UIsXVS0mD9 z83!R+q&{p{Qp)?6)PhIi2^O^w@jepY=G|y75|8wa1v*oIq$24&O7BvT6tw<)1`(z& z9F+Q?vd4@j?LT|n2PO}a1YV9c1v}f~1?NC!n?al!r#spKD12--f?M~c-SzqUZhZsG zZ^iF_%tS=dGsqLS>v)F~TFds>r>ycym;8OA$hgQNF{&6tB%GHZWJO(PuF-YO(28D4 zJAQ)CpJ-5#Y?7Y!e}57G!O-CU+f3#QU@k+G=xFhe6HQ%3&g>R)XN=K6UkZE=@@^Hl}KMnKNa z&bn^ix;e^B`0dGFv9lc_#X~R%2g#qvH8w$axe4wW>20tuPPth_Yv&u7$E6*x?zveG zMaj5wpiRDImZd>`R1p!=d-5}%f}&Om<^HM^E(J<$o55%%-6ag5M3CMqVFsQJs0g`+ z87e{$q0Gdv%>oX#j_Uz9hb4~iH0*-~nrKJ>SR(aBoW5k0zr<5M3u7i7Bp*M-O9qgJ zI&KIyT0!#mH~RZPpd>-yG{d)3Yi53(3_7o}7qbW&joU(szq?c}gaWz7kMO6rYuF3e zVl>l>aTU_BVdx1(sYB95KlT@pWV<21e=5#1t#PJ2J(`4< z=ssXX#8E6B1YnX^e{Bv6QyD4~21c#qrf=7F0aWP_d!&1Vvq-C!(s1sX*vf~)->WQ^ zdk|cgzeAKYEr+txqq>y`8r&jBPlS?_X+1iPP_#H$Vu+f5c!`Nx$(zYPixTNeYBur4 zy8N81K>;GwosE(eL!Wh&q-b&FM@l2Tre6bu%?J(BQIxQeEU}eI?U_m6M`ukd8J#VL zu9?8HkbahR_Y-bS?1RZp1*#uFuG0_6vdYOjx8cUO64e#4;SoLLV%zO$y}pwfk(s zjhIsYe+7o{U*DC(e#I(&bQUQp~o?hw{)xSM3_+;Kv z@7lO=se}Qty$RN`a_5X(c1vNa0Zq`Ba}J6`5nS(YIeF=zCvp9BLl_q=Jc}P5GiX$P zPv>H7q7+kml$S`&ZYxMSaLWch!QI)B97-?N6Yd*08RgDK_e&fld#YxQv1H;dUb^b) z>fRBb3k*5rbtUD)0>Nx1AJlG;BEvi)C8JyD0qu{;35hHedU`dGMpJbr6U?B-WCdnl zbd)~)wMLd1;F0UTX^=bsvGaCww#r?Yd;gx6)9+>G_30kOJ)z68>}xvTtON~0+$y}* z?cuKq)AAT}HpI?Q>(zx3NO?7L6R%BZWPHt@50a0{n0Z91U<=ZKA9TgfKQ>B0JI^{v z-VEmub{`{Ij7Hd@d+*ov3=?9Y*_Y_OArk?hPmDNl6z(;9EpT5zaD20bgLJ@5JSGwy zJ|RP~T@+@sxS;+i$7h+r4CI>-BhKTfxFtVoV6FLA$U(%GJT1MbH3WK%GMCCuHWC7( z>eZ8!891>TbRE=5M_YMUuLph7M^q>dI_t$qK|$@gnD$u<(Z`*5%r@Jm@t6e#)ctqA zhr@33Z=_{tj8&zZpjfIdZe7zc>pwe)KNt;KLDI>p>%xhSd#KgcyJ&1wGipIO>p6oB zEZ~Loyc*A#;b;~xSlM2*u0Nmx6UCO4R}Y+=eF+^AyQu8hX>^P&qV)x@Dee2T?~moC z45LKWG+Tw3ncW8iqYCvc`;$I4KX-t2mib@$i5OrOCOl4I@g19$M8ShGYl{aJyKiP% z1wZ0Aj3^1h5R2W5>89rNCYCYtPxG3?Hjtli6hwp9poD%O%J*D6lEntBzV+|?7+F;X z6D|J+4Z3KQbcrIai5ZaRoi&8pAgaJWvc3%r(~vkcfmGP-Hn!aP5?D#LWX`d_LIy#9ZvxL*SW;Ozfj#R2g2|4-G1@LvXB8G!{dA&P&qG>_;WDf`xg z=R^W;5Pzc~1W7F6cv=!hTC{ZA-FN_oRo}vUavJ`Dfe7TN63TCebxe-$+39-kG83#$ z?9e`zah#Dv}Xg zoIv8U#Lf#_Rf<1##tT|ge{hWnl{P>qZb=@EA!na0R{%iYl~sGHlg6{hvv2ZzTo^)_lC z@RM{#G*NWum@p+3Pl3!!0cWqqJh-*DX5>NVBo6m7_iOqA#zZ&W_@^(W(zIO0+mTwt z3hrKtmn(+AN~duyDf0^WI60Jxe4gKddb~_Vf3ZPn1^Wh^Bt&)CtZCqEg6-KEAJtb?%8_49a0DT~*L{~} zcC@D3->gH4H;loGhWLHR&)ur-eeGmEI?C6)9ZIG3hfxzk)a9Q;R)1gV5j-;`^4(|2 zH03Bg)v0hppzA3A2vIyVVUG}K7eQlpxYTudn%4KlpuP?=2-}0UJgL7{p(_p?V_Mt# z9Og`e?EN6fppO%ZH-%k==+x2cbYYje1q?S{#2zK=)X6mBo&DleQ0uP%M8SES>>(StQ%r~ zPx5;gWy8D<{uyCPW~b^*e1!Pbui^r$IN12;9eQ6@McAJKAIhD)!=UD-6rzo~s77iZZoV%YpId9a{A zDa6+O04vfjE9Giv#&*hR^A^%(pnk-U&7DP&+t&{e&yV>#7s18cHXGLSTI zwt83v)U&s3`IM#;2cq}ov)ucDHu-zp-;~8xq@S)@&7M|a#N2Q?yz}`iNeVbs&_B?l z4<;=3Lcgcnn;yTHEMBB4j6D<6I^^$&cZE7rBYt{?HO@pW(TkQ#QbMrG14*?PvY@jkc^4(Xka%uEWG>CFT zm(O%MNj+vHmgO-gvwK1ZY5}sGY-1qgrCN1U_++9foD8`FsG=CZU%Rt`j2h;xr>#}}-a@il4IEt#tpVyx+NXV`@m`%VVqQ2I76@}E=*1^zz-@h4T;1xjvRUnTqO%H`E7jUhe9qBs;64uYQzp^ z4CUxr#`%#>7A#1#aGwNUty7v~60~OuD@!?&92lsMHwBG2#Zw0d_dCk(TxhvPa1NiB z_hOI_P|TduQbiZyo&6kXNT>+t4YR{%%_r<(%u=G|D+$>dXA#`>hR>bp!EZG5ns5DR zt{&n!K={KC;k-<-ghSG`c;;0#7Jy_<^L=uKk?=agJJhKe9!6V4Hg&h)1N4168e(#v zIbVy{EXpUQA^f|lmoy7a8@(M6xS3@k;#Ly7K_GU5L_s0&EU+riYdY*P$_TR|J09n5 zc3PSs7x)Hp-P?zq0qBN9+0h9uT&nQ9KMpN>a(=>8XWF(gD9N^aCsO7C8}-8ROWTX> zL{S#DA7f7yhON4dtLS;vd98~!0O;rD#DH@3lwaFOH&Z2+NI;dxr_`?DbdG1ah}bhv z#z9AlcL@2QT|wK8COfD*ij<&)FigsF*+d^?qiVnTO8JVSYU8-+8+w!BYVw)2-nedP0BD^&lNW_kB_uR+(t~%y133zbcZaX3lI0 z3^*V4){nQDpmssMhHZQkj)wD;>XId_Bgsgal)y(a~#an(<9=(94} zDv0kQYMJT;a#$!t6*9O)Qh!X>hH+qj<4Ml$kVcn|v1fO%UQ80eTt`1QyVZG-&gQcZ z&QmWM`DUv#2LXL0A&u5~F*VhcxHdc0FOyE<=|3QB z*|+1u)Meb_i`CT9w5q@Zq9L(+n>6eU1!Zn-F&h6T-A!g`P#QX}{05I}P>1*mjV1;A zV|ku;;^ea;u)c)LS|)llW+v-+5xt3Fd6CRk_KlqCI|eWl*}Gf4jERQ%BKfelW>6so z`myWZ8PJ5msW#x0KFKAzU+BmUlKCg9)tk_{|36duCFXNHNG66~hODd%(l*P|YY`M_ z?@@gBA@g@X{9cJF?h>_&4np(wY-1hQVzV4Ofi4T{R=f)%0Ct+)`9O;6C4m5-b;uis zkqsfy9(xU>W{<6^MCNNBrdvRV4`Y)`L2j?3&QI?g&?`<7g}zy;JQ;`AjtL%TU`Ch? z6AUV~w08m}H?Hbd6VC-fr|d&Eu_CFoct81rfuR%5%%F7*M25Oj*hp-p!8(NtGcvpQUId`^x}!-!M)~Q; zNjG^G4oj778!j+X6J$OLKb?3nrJj)Fnk3{Uxl-i0)Di zlEl5xJBb85v{e3h378htz+8NU4IF!~D_OQzg(NH43tf>Bv@I1MRbCB}*$(o>_k2u_ z(G5?UHH#>^OQ3foAZkMG-~}`|!W%&vJ*ajR%Fg&%ZQiRJYNt(b} z|0-HR2q*;CUp<1Th)9kZ0l8%39$j+-4H*w;=2U1{yS*4gMyuD6brd$81)N&+Qxiio z$x!Eit}=kF_))Hq5a6s@>*Iq9k-e&v4F)1n0E2+&64`AA6(aJ7@BUe|YoP_$c3!579;Ikw3Nxa98u674%G zC~j3&Rylpd!jc*o`aMUcL~DKL3}$?N3V@lVO<}@!M?835&Wa8gZ<@&%cCJkKzyljY z=MDBVD`6Ga*~ig#?wob`UVLnDQd=jQx@KB3LKK7UWAGbWtsjoh5i7n?a;wV{_|@RG zT?;mRet>bhrHTut?m&~sJme0U$6m9B&q?DqfZuKJU>rbhxJ^D1a@?pw+yi0hXJ{>! z5Jp?R1yS~d&QJ=NBfYoPODSOgJ26f6D{u2DTrW3D0IU?O%JHyR`Rw7&etp`r<$Fep z9D!5E-x`!Z^-49`eVoT2PbTktpM0*}RCZ#SjEUyCPEppWls*P(yA4eBlo_vlcT{F` z;o#YC)QH%giO6-ZMCNVnxH{c+0zBs}Zg7t+&d9Qv6Cy)O`}8o!rh+!G5+f&92XoDW zzyOEYo_9MCe-}KMzxmeF-n6g)swj+4s8A?Chyvndbex;K2mK0fZI8>pr7Os$}f#9ZW{3HTg& z(F{b-qX=u=C5Q`eYMO*J_NImu1q!fyi(E@bAyZ;l^vp>TJYuDqOxtn~%OM9kwLcdS zkD|!Q=#_^)A&vk5Hy;V}^o8l7vqK=Mu$=5-)xR2n#eOh#a!pJW&(#va3vq!Tyiij( z#=M4k7!wrTBXrC5TmaMobuy)3qwgs)k1WJlnEo%(%pD6ftrKE>i>jS@%7H55v zSF#F77={E(iQ)JZUy7Gerbd06g+bXm#FYE;67WH^zN0UlhDmf2XnB12;HEIu?b1(M z>y+*c9w*MF^+pN7_`euo)c%4Ur4Lk6N+laJbB9CEpbj#;KRvV`>TC zp_pfTdPwiYI03yfbc^o5oG-UuM^ea}X{eFwz4#JHiuCdhJ%rEeDqsm#jq|-=XN@#l zP+sT#!uv)1nq5!(gO<50=BOw2V2tItohTx^+Fw;dvppLyRoM2+gJ%gmU{85WbNigH z62@9ve{l5%adU`37K%47TOecB>lToOWHCJVXT3BX^@77Tt~Ps{?LXfB5e9@e6{j=T zTF_00GM_^J41WA*MGhd(>(r`0JT(&dO`T2n3ZTc}cih9fw{Mr`Clx{zIeP*g#$JWm zpCk2E?y8uDcX&8x4k6?thtyFC!<7KLn}d6<`-es@{UJ*)2jCuhfY25t8BUH8!==*A z>^)PIs}hH**N3sO@wT8x$ExwUI(duX`__0mBkArU?x3otJ*$SLuAUqA9-1=8%TcPi z%@8+Ow?A%B%@5cp#bQ%w1;76TQCkCkXvTe539Y z8cP?L`O~v8NhdREWZ#~&OxnG@ew0shLdx8FyVPRz^?AY@U;>t`^GAcfuO34BZ@Mt& zKXm@OkG2ht%S`+bnm+Ic;wA`kgr|P=Pp-&%*jpF%o^_ADfg(bx3YKfa{Gj&KVAajC z0`&QiKLx;Xe!|V@m0c2066iwU)Ekw?cm^PYy}u1f3U0OJWJFZzwPC&wlB^5>@s0jg zB$W~|1CMVqgetICx87q>=3OIp;dV#8cs^Jv%VCv+*!XGF>64R_g*S?YKPl;Z>Z@cc zkjUpss00v1SRxWtMo?;1^CwabF*C4$2^^=|F&3%QQ+wlA1tB~9Aq+tdN)MU%Scy3N zNL~^J>97M_B#p=qbZz6BTIf?5ec4FS8_1l>+59W00O_;zibc>l%sFPlwl6Iue}%e+ z*pZapQi)x3mh?%m9vxp1kTLnN4R%W(-G=KQNxVn?ZGo`Ma&v%nL{zPv-XIzNqi9HQ z3&n2b2Xrrkqmn?8?r2ZJjE%w8%@}BMqf6^ZKUD}thNtUmvJ_%vS28gCw@l`sLWqt# zHq9j>YF-wckC$?Y)0C7P2iE=#{u9B}GQUvghjx!}8wtt31>){m;C_DF68Bj45hD2a zJnyg^HV;JQ>INyQyG~=sWe^7fyc*NJ)(p-^S}8u!0h>fCTD`eo($fLK^IDLhc$vv7}8wAN_{B)m)N( zCtw(OG>@3QR6qTg_jm$Y%9hj!;`0?_=s;1QXdoKN^^ieIt;lrD%=|2$rM&9DZ+=fB ziYTjNvpnH>y;ZWQrXO^CEsOAMQ64AFRqb;PY}t8dNOjarEUqr-SH3JOLUtn<^AVAO zaza{de_sAzR8&-?>*bc6EJ}C0;TIIv)ctfZ!HY4y(E#KbV zkdRltt6^g3`mG+Eav;fQ-aVVWy3{J+f&`B$?Z?D$z4zbm?{~;MEEV8PxoQYQn9Lhm z9Q|&Anucm6Cw1;k)*WIq_z2o&ipQ7r5rpMTI#Umxf*Jv(nEIS#dyNOY7TV!_WZ0c1 zomYuvQLgC+adA=Vh9eB#+fy@MpG1Wsu3Nqd3a6S;@zpDL8k?e%q_{UgMA7Z!TIPG} zFv$8!47LRZ|B1^m)ZcmxOh`(hE|6{FcoTyAV-8LIc15Fq1R2-dbjT52lPDLXo)@l%+irhHUz6)-zFGiOo2drdxMIot6?IJ0T1 z&MXqr<`=iGuybaIc~k?fy3rBsCb)iv-`VXaMk+}~kG_~O#eHaBl|C6KE36M3v8cQ4 z$v4?FMq2x1bHq8~^Xsy(ChbZ136ivN9TWo1Fk!okbn6lKBWH>wU_2V_m`^1#fzCxA zSB)c(H&JJfm&)kcmx0Ee2#`e2m|v?F1MD*^#U}h1CFA_;-cCiixVXe<`1>S(MoHGJt8QXWQVY zf#HdODuBYJLp`}16z|t7&c|iy5dl6dXE4>}=ITy_*uCf89;9u_GGP4}SiNO7Smh89 zLQ9c)=s*XR5%KjvynW&$j>=oDiI7c+g-o4ZktQE;8?H@xYe+?Zja{@Ai9nm}n*pim zPM)BCM=I%3STji$rnZA6^hq0-!YZYp<24f-%@+wZ@Gcm$Otz$s<)8Glyr}_sOR`5` zzBoI>npgV67W5Hh&kT7**SMw=ue#^`$T*rj=Yz|XYK7HpAjK^6K$N+;b>+Y>?b^Y&Ipkk z+i^LtQ)ONkD8^B*CSuycG97&&?vs!-%?Xlv(X zqp8bO9#=sUFUowW%wynsO4PB3#~fugS~0QpB?IdB znteqNgXau`Pwh?a^>8QfU9|Kgn{FXDAKro1Tw$Xj|Dq7$m$PbyRJ@@Eu~;aBZBz65 z01CDEgsW+J0H=Fp(@}k19$;s%Vfk~r%Zv@;0EyEm3pH4*r|3W^cWSpu9p`hrDflD# z<+nelA9QxtSuw!qEJuXS(dth^aDdKfHWK0_`0+PzGt)eEJAK??QZvkH#k=6?DoOH- z*|n@weD&nE!_XMF%wyRiFa|Z!B4p0frdlsy-YX8`=`7@Y73D{EJ5{^BTpxkVNP(~| z9MVv#ITVy|x~(z$`Hm7wQz>6k_Qfk6XS28A532Hg5nA{NQVu1j#1S{@PD-^AE**LN zrz?%iP>t|t@6U|%bFDB=Apt$RhQfVA zGcCT9aV$gsZD~$Hu<7zRv9 zhJHGkznhu_StMfJL*+HKu~0Z-B$A*0>^{d8Gs_`j4yQD$=6S%22$W!~k{7e*|ydR(}yZo(_9qyp7d66|n1*zoDo9e=F zE8~lHogaZH;PXF@s*2JEP%WFpZ$z9p@{vj#Wx=D=9r+gA} zL4pA(WPBre0sv+z_6Nb|VBoJoQGR#%V$H@>xl0=l1U4fBoG1dBV9cTtfy3lZOd{S^ z|Eu`SWK7qT{)W9w3G6_%m}sfUFerK9 z(j#Q1uvDFqMVJQ z`S|*rI4Xt#=ASHwlFf zB5KVFU3%gY^Mfsc=|Xidf%C=#cc0Va+PZ#30=AL@Vul0((H$%0o_oZf(VwsaXXr|o z+F-^Nu1WZ>ws7`<)Ww~8UlJiTRk{y5+?{tW`(xVJ5#WnBp2d;7y#64tsS4>%d5wxo z3PGOx9Rjq5`=P=M(=H~yJS3WDuHSB(!Z{lJra`v0i9*i0pXkh1pjl3SNdz4rGI=HM zA3Z3>5wmHX{iG;;wptp(o|}{V(2rO>zN-ScCHZY`fHMj6IdoiAH%r~r$c-tEgb+v_ z6u_I;l{}u1lS5{9r&CYGQ2(?DN$JO1V+TYpwGS%4OuHR;S-1 ze(+WDEafaa>T4(4Gvpi?Il^I5uu>HZgwN|m0wQM?Nu>bccva$`0empC!#W#*uXv+i zaP_aM>|2JSX7~)5%V4^=9z;rgQyO}hlsPGq@MuCZY8l9aeIm&2Z*GWOMeFPj+RXcw zE}9v5q>W2u_%u6nN`aEqRukuEl%ys*O9gpy83l=88sPKUG`?5s3clig5Ct=7$22@H_^sbiOI!KN5tTZCp0ovp8@A`^aQr-#PHFm_rU2m?}eRArYn0oNQB}fDJ$?MBS zLG{>dDQ%Ak+XS0^xV;zKNwp;Kf1BUSc&F}Mon<&FmWtlK36lvIies*y@BK>y%PIA* zVD>bFf+4*bupl?dPV9n1Vo>XJ(-SRmWH(c6jGu`M|9e=8;2xrgRIsrnNU#SE|b+Go-o zJjtbp#8$dc`rssiT+$bGW0LM|>5+ejj_kT3UXMZ=V^2;*Q=J1(_W~Om7=WFYuI;(I zVidXM9~;`sViQXmpJ~B7VO}pUP#y<^_MWqI?6l_)9X6P3_K7Ekn{kk%f4ttSI)q1@ za@g}tBN9P#4}tRn4bQ%NXFA4ROK0;I}0)z+N_gmSC}J z(dy%XUaKz=V+*O@AOy1OE6BZ>tEJ%9iiqBn@dIJ8VIo*ED0PltE45Ie)|&2o6LxXv zeTh~hNPSCh+EVVs@#LM5*t0J|HbS1K4@uw51g1JSS;b$Y56MzITqx)?gO+?-uaM8= z|Ix&tu{BPE7Xr>0N!?oRP75t|v@t5x>Z1b&_YW85z>bD3+c|b$(q$u-*{(W|Y6N0_ zcl8}Vi6hoU)V+$^RUF*sdx-j#egPRFdxRIPNtkD$))c`Q-DvUWS;6ImVmauiwu zzVgmKoBpUThmq<>1o*7Y$yCiHXWQ4GM+kyA685p2`A$`K5z#I@6p{^yLc^%#3gBw77W`wx*&-Xo!CKh0mniv5sgKF}-R6ETXl{b7PXx*L<Qzz=nqDEz zr(4_LhVHVGvYo z&C+!Hz1qhR^5?!{euXeUm!a?#(r~Hffl|M09Vs-VZP|ML&zh>IHGU&{{OaM3ff)~= zTIvIXs@T2JC1TFK5k?2t?d&Rr;m|cnWxPivN@kzMREO5$$dar!D$FrQ@8v`LLy*Wh zf0EhK#i_Op&h@mPz$c79%2~cDPCq$kS3>J;CJFIp!rB5#fx?BIsLB9>1Oc#^R5Bxb zaz``u0qG0Tt5HM`lyy0f{#3}=v&o4wIYs}rMAplgxC1J-6HjW#r%$Rqrf^S7(nQEC zu#v4g5*>>#o&;*;twZOUZ$zoYYudlccr$!DbrHoDZ*zl|PaGrY{#n`O^K3#ZtW#fH zbb`zL7N9So#Gp)07seA8hP)F7cc6&9zG^3IX8 zOTnA2jv=rQO*IM-&lz8P`X=f*TU#Zs#L@$$mJ26K`IC&rWl0Yl$ZFWH3{3uxM>S#R zB2if(2%ey^Dh4C21H-?b40YI6Dj+q&^xON4UrKZwCi=DZ;BGokjH06R)GzredBk%_ zzLTLaOV(6BhIK<)*Fipw4dP(~n~ITsJ<&lHu zb3|t=S^pYsL0`!Yv$ku9MAxw_MR4VGrGZWUj=^Znb=Em8zOBxx;y#1;P`P!;3ZqXGOnK?Bj5QmA2?-0h?>Jh3@Hn$PJm6|8k=wUd=_s~U`vc{`vnGz@N`b2eS z=CJ^AmoQFo#ov6imlKI?3fWf$)^swJ+Y?-~celVxpXQv<9xbOst`m#p!ReyXJ@?dg zC`ZfPos=(&gOZWM7^OudhrR}GumKOHDU04z)cd5vaP5|+;9ukKn39q| zV^nVGP-Y4>*`9c1?BU`x`wPqo68nT@?&qOQ>$D-D+MmXrT$BK$Sg`k?s@Z1ZzP?7% z617KTt!J2yZpW_NM}1iyC=5`H6)N0CPWDydjt1tU@7E&TjO~!I*AXKW>mB#0H)fbf zJgMpxa~_8ArO6~hj{)LR;Di|*dv@Pn{ruxDEtSad2zC%|kfMs5Qo}fDHAb7Su<`*&6t>Ul z2|8JpJS=T(VwGW;d%Xa8=c*w7_^Kul=o4&=x)@tr!WGX<59WG`Xh$@#L;imQ<_%i_ zkbi7Lqocp;7_UB^AML8IDlA$$|n3n?NF<%goPF~#4? z92l@{M}wOM9j$BC+5%m$Dkz$<-g#%<&;lC9CP}`wjE`o21eX+w9V!B1I zfyv3JR9QhDmNef&Q)uQvFGGtDX<5Mt2j~sap6D8FcXn=6tH>~j9p}?mn31>6^Q5I# zEOUfAO_dC4EKzhFdEL*2Lv|R7nkZ631}@RJ1-r1>8Y;>fasKx0p?Dwm%$$d+nm%+? z4pe-h0Y)!Is-|2@bOW8O8 z_-qHIX5a?TE|&RtD0monno~nTc9KZq6On$gssN4H65tm93j}4gr9PW$SZTS}o~=dx zhVY(?u@8%WLvWIER^6ZljV6n|FCpMd5UJz_QKHt}JJRH1#%wew@pvh5bFx38MAIn% z!+!^G1>*No8@$unj#J~npUqUrEoB{yqt4JeVvfvsWbuW#!Q9a_KKvPY{4o(hJSNC z3`a6ESGwH?86aQar+@G8;rX4q=d-Bp8VEw%453a$PkMXBGw2EVPt{3?)7~xv%XX}c zk?zUhzDMQ((l~4>JoM|(nxS8+M|K%mm{g4HwhBf>3>@|PcviD`ropqNC|sL-roB_d z=<9h?I?TsO$7KNo4Gf8iyIOJcvtI@h2*s4}&zxkaX~0 zuPyf?I@V_<_T`f-y`#CFI(n)R4)ADC5zRR6Ia>bAHuno(_AeinBH%^H*w$IlbHV^a zAPrHSk+PgO^wM&1e0-CkCB|SQacvJ4Fi8Jf4W<(6 zvwpO9lMNB?PJo;%Ki!K6LIhi8@HzY})0;m5=mCq0qWs@#hs{8o531tW?mugN7RSW} zF~!-z!(3)aJGKLHoEvi!uni5Q9x(bT51KwSma6du#&oH%%5_7Ph!5mvh=;xuG*fFS zGS#EPn1``S@Si=|U*Qf_O(6J6c2GF1!sW#4PMu*s`%B@Qf+s8`^TcmkY1%NcpHhVej zCyeoHEi2hv8QU~T5G;XgM-a?6p8YxYVh^eojrg;^&L2jO3t!KJGn9p6ypAG3&s?dn zw2^|PJMvh*50t(j_xD&rL~#*I&srUrug`+vq>Ar}@%ya6>aDKF^RqCYa*ZQh?WtYL zPE2DRXf)K9;T7zoTFTL(hr8>24vVaJ>$;_zsPhs8O$_Ttv%3?C-5W+&gHld?!Ge#x z<5WH-fpxm@Y8C?^O?%N%n;8I}q; z(LU{a0Va{9e5Vo6u>8_BuA5jfY|Z5q5rRNb#+hk;-0K3 z{$54t@iAgCy{s>qf$SR`TkEK(i2Py48vZ?esIT8;FoL*E!10x3|7Bklh?&<}+CPv2 zS00J{n9Yk8Tdo+Z{s2l%%x-zP>_|ucu&45%p)6AXVtSu6%%{TN6a)&t#NkzXQfuN4 z%s%dtod$UU(UIZ?dXc~-3!b&|VI@*c@C{;0b-Y6XPG3$yMr71h6=Pp4L zX?_-{T9)_BxCfvINR^$v4VAN{{Jl^`=iSBUP`=67y7{^Po;y${7rN*7a7k~h;7MAc zTN?W~L7S2xlk>~8;4{ItT7>cturzsI1NRa*2hWp61Cga>lv_>~I5iB3Kw9a9<4Ye7 zTZZRapAZ5}Hp5UA+_?u%$<|f80Pys$O&7qTW~4{pLkYi$1GTpJWc=~P2NXhj@cAx2 zN;bP5%C{sOuuj_%|G9R>$R}42odaZl^x2Gk?P}pXb);k@CGcP#dZ~deB*XlMjuoil zAu~r}c^pZn9P#(2SZtmx+};>E9^eG6hM0WHn~>GJEr0v$Od6*rIT45d zU5OMLd@jfK%j?$9;cm1)Vjy0hg zq>BF&N^ZqK%b<)+!1~bTOxzU94bikW$$Jjvq1s3b+DRZq!-27k^0IaOxE3aKf$qGy z!<(m+BzU-Lb6R8;49XHIJVz+sX|e1C=0fG)o(*#v zUj=DZbrIOU(GlSW|II_dD<%s6U1#jj8nHShX`wb&u?r^vob>ADG;GF%5+E$1b9%Uq! zT`x?3=i8p#@;2CEm-Q2)#Xju8TEDZ6RKlc?*+d1f77XqA1CqV9Xi};L*Mjhm(KYQm zg`?E&@L$*!R&@S8ge4n1^<&4)T-d0SVr38SfggN#fudfbbqletBAh3Je*f+pJ-0zq z+ib-TX*jA-Pg8tC0-x^zY06tjW{iOpXC5=0iQEj4Cm|F0tL{a_?(j4*t*D{6LH;>J zMG}Iz=w*X57QU&goOJdJSj_O7fc<0?Hhz5H;YM)J1H*B;v~+R=nl;r3JfkqPef*L@ zGsXFD0n`yL6=)VuDG+=JnI9*)MHV00bTmFJ17ToTRi?X>&eZw?K2@t1BJpuMGV+Pj zVFKo%plj3L3-n;;VYr94+1gw`OLt^2+Ov+bXSy&e6~9nml6KF*B!xMpUq$nlM#;5OPoZmE+B z9$zZdYWw$oEf!yw*+R^K0)eIaedIN#&qUZ46j5bbQm_wLR0~G;C{DZVI`K0ZCiVDP z7O%mS6>Hx(ma{W{Qz7hhFYshuRm?MQ%ywEpN+)kbMa>14o9!1w4}+_S)`Ywi(wxsw z9WR%OH#j)vGng)xht;YS1{Bj<vviGup+T{K7muQ)nE5QJZ6m%BQ zO`Lk3KQhmbm$N@208+HtE044O+pTES5hGY&$&OCR9w_&t0qYxAATcB;pQ9s%t(kR{ zxVz?2LkV@_`Aa)#w(2ObPAog{+xFxPFHcQ2?;xsx?dDHqk^$fN8rqt^nQU26N}Zz- zX*l|zT`d$U2sP83HVZ_0%%|XyX6ke zAvlZg5M81^W&)=ukJrnOKq?Of6@a_nVttm_S!A}5&g(&xz|ube{UYTv^^%v3c#Zfn z>$9n)oNZpcu1b_68optor*Kt8Rx6=~US~-KO8v!$$T%osu&j@IWdBI+}~~~E+;vm zR8}!@#4j4<#D`!JOF-a5z4!}}Kv#>(j!jx!7_%^M$50A`LS3?5PcQIDC{j6&yy6Q09t%E2@dO1)AX%!e`A;?SCo!g^e-JCv>KQqlDOv3$Cq3Kq2-UB4 z6iv%Gbn(Tecm*SJDwgjD*TI(yXNsgTvZV@&*h}d7tOKuiB(l1fgn(5D!ftje40_Nz->xO~jlvn(1qH=g^x5A9{tN_h0C?M?yZK0B-#;wizYITT>j^Yc1^ z!{jvU@rr0tV|aCj=Z&ver`~j#jasy3=TS=CJdFY?)e?d*cwcHz2wa+eUSC}W)ZB6v zltWt7@!@MWLS@8MOXF;GVRz`bn*Cz7ibv11Bz5I|!@=O+jc=|rq zyC-38r;?}S4ypEL=h*RO8EPPvjpAQ{r<2ZA?5|x8Np~T9b#j*3 zi}{TcaZnXjw_^G;(EU^TE^xq+^46J%MD@XbmhTLS;8jaessPtnJoKVad)(ql8=OoZ zPWr~MYKES77HHR({^xo9-|XZ7%mkPNnJ-QzHq=q96Sx4?qwX-eMX zT3HA>+E!SQ3K0KI$$I?FAj8&rJeD09YzG{q5D`B9=Y8A{M{ODV7__6k4xG01hWwVE zsP;6A)hvmQLC(teA7=BD7HGRFR(lOnxNAdDSoNY5v}`02>olZOVccD%D*MIE82a&C z3TBL_j@54*bbB+})3UG&dVu{?31(nF4E(bLzxma} zYFv|67-aQ{1YgclF;6uI<1X`umX|hv{{-7~_vC)l0%#YrAG`0Ax3(2p^;vbSz(_b} zW$B8E-J@276I+AC7xZt{Z{p9^w7%^MF}Fy2PUmg=1@bkp$~rhh;n>$!J(R;Me1iGb z2p2OYA-B%9ok_G!uDWJ8RMkh8-OS36ssG>4rrw$L0s; z)d+9oh-tHbeAkA)lLa-vsk$D{uw?YM~a)KI}R%r-!R$6tZZ?ILl(nb zG2u-_dY4=5Cs%xo*+ByH&vuvQ<9M$#FQSJ+$cWvXo%r77AK?l-SN6WCJDOjB<;ZZq{nk1R?917%3Bugx)zj4Kt0Pr_!A z1PJ`o+-m*0^MU_h3{{s|V93*4BweL&U#+j%m9?ZiJ9_KE1{O~?whxU-IdUp;iH;zg zHHX-qWO4INly=%M;{mv;sgGR*;C%n%F9zCl%y>GQCi?DO+0)_t;7wxFv?&X!Lk=cW zq4Eme9B?g9gSLFW7RxrBO-R{{2~D#ZX8fvewGc(d3>IEGJ8=bAcdM<$%hH+QxAqFt} zkroZI2S`#`ybVQ*qo|oPE@vu}rmfSSz@<3(6LcnL~TSiZ5cmin|`gwQ3 z=i_n^9WZidsQAz|e$A%^I+s+P>Yr)w+E2Cg`~sw!X8}d$rBXsOSaabu8)tMJ&LxU` zg>OL&X-RqYo6|(Xo_bdecg6PuQ7R_zo99lon;WCPC2dcZ<)ZR@fV(2<9#8N$uqQOp zmQT=Ws*fgu|)rPo_n8 z#R-x7rj+Ft)dU;Dix&B3EhL@uQcq3Oit^IY2w$QH-m8j!C+=mx?^odMn~hs*rD^)9}`iC^;~ z-awC37mk`pW=Xt(9svaK}P?lP-Z8d^kUz)c4K zu!fDffwnTiM!&kJP)ji;q;+*2OdIub{)++BEyPwU)MURvg=#kMlDAId5%&Vo7`mxGUF80E0W5I&e~o zQ7ogTo-K;XlDXwL|BEZA``^-XN=SnZN4K2X5E8ZFy~yp^cR(8E{YY@sVb&}T1S1Cf zMAF+f*3zVbepqy&un8Aq_b{g|%b%4Bk^B}m;#yQxGhFPUArx50Wisjr%wVn+j|y5X z*F?#QA=P`QEG3u)WJqvsGC0GsOe;fiKd{As7i!3bTDoQR)3}c$u;*3-Raf3iy3xLs zYlV?_`NNB)$HjILHBu1n4w_V-jGXgDn`@3N?P6-Ze`EQd-(;l!W={sPTK){kvj2pU zbBcA7Q7A7$+`l+Aem~8F$OKOcRM_WPjWpy_zN0yz2j%dNTHLzMY5bAi$7L5VTs#jz z_4PBe6#`*ZZB3cv!<&=FBrL{YjW8TVI@I`xMQJs3whCUj3v`R^tC$OxRm##>-3TeO zK1IrdY~X8Urz9k&s^%wI)mvx+%3kYyLqYlg7~zc~?k?lEt{ue?}DyI{3qJKQyjBJfT zC3|YUz=v9;;F5}1-wXs!RmN*(?Lkn=oSS)nt|e&7KVOk!XWp|CM`z?kea%;YAkn03 z3Y4bAPZ$@({uEYvjxQqOeY9_mC?3BJ9lBkBp(2N{;zlcj)FLH3btRl$>uCzqde)7dVxO7)j`u=S(QM89YV$L4)<9@xprTj1V0MLw zc$|>CdbY^OJ+8K^JbRH!z6*GAk@mA?HMAHU1Qlpfwy$xE#AM{n=tj0mZSY}bp)qur zXf%qOVyxfB$^ti^m@wAY=x2)CK-?MHHgOjMc61NY%HAh+8CNm|<2>U|N-Hrpl;13m zRp8rg3vd>iExKFncC93FM#~Z`>fPAUJ$H;tmr=N8M$_5f(e&U80$sOkDGS+*B|lNU ze{pr)hhq9CuNoD~)^a$5v%f&b1#UO~PmwbLM#{w=ehgb}ohL0h<<^(1+n|zXGx})h zsgS;_QXbL(eA5bw_wsDkHOBMWx|g74^*6)8VT>TEn>B{>(?%$mYa`sd2mjQ;{Mcrm zYG^Be+KO{7PJFPG-cUFmEWJYvt3jWG?1ERjPQUoQsnT4L#2R~A_ZW65gK)4Ui+g?q z7I=DB?-P(BreYN{$uFZ^SR9Uzx#Jr|$`##~?Ion5H`jU2a$4%C8K;Y8uYZr6C#a_5 zH`nw=6zFf4Vqks=W>c0~-_v8cA3rX)rZfe_EqLMo+=^OqxEZ9Yk019nZV-wn2rW*w zbJ09zcitbd3F~OTaLlF1_dptOAp0Yh3={c9h zTuPYEcBKDOc1A}S@M#ILm3+hxi|-9`2aYm#l@vHOr z=CQ6EBIqP@AWf)c0(q0!xM2Jgwm8>xvA&9q_;eV1uc4|%?WT3t5R4^>-{CVC9?6`O zRbc|-rrk=Cqum|+_kVqErRiI>!c_rbWO^eIc>|Y*?F_F z8T&H`W$V%+&5g6gBYc53Q?!k}IKhK`%W~M+o72w+1%JoC|GLroj+{NPfowyK99+Ip zFtnqFr>Wg$e(EvCSZ$;~p*jSH;jUpP)ut{RW%yB3^}&g&q+be(VyhY9o|^CzG7__F z?9@j-(64~&uJNm}gAyLhNOpD-5+$!L6x`?~@z%u?&t0OR!ll&UadyPcwi+C0Zx~$8 zHGl}D?ah;Cg{hZ16qAWfcW)jVa3utQFU$V7B)*xH7AMk)rhv}itq08o)&YQVLeOE; zK`7g;PCUFwbe-OlO4ccn3^`&1i_mguxK!?Vq8Gi3IS#fal{SNMNapZjg|894Kyd_N z=??1H&iiEknMp-2B~7;VC#yhm6ob)V-D6F}DiUsnXngUFo5XqiE$KPw~IZRsGp&(LLm9dwD?GKBhU?38O zK!kUcogx=A98(t$F>%eazRt95tJSP{pMn%6^QzjU4Zx_uO(=HAd2SmthaMQ8IaJ%D ze236Zj6UG@ry|$`5ZMV0b=T2bkz?jzU1d7jhPa@&x-xX$&FVdzEWc?mCi71nliKCz z0`Yl*ai!%3)=dUn+CitnTK|Dw7fIxwRymiGYWg|eiR1B95uv}W)y5~DIHI79%QB~q zcPhF74yfP&$OXR2BCt^v0rJZZ#S!i}4ufKG#9G$H!fWdNq5~1910~8Xq1KZlm7q&^ zA91yPRgkvnU~-kR0jI@}1pEScEB(CjSNok9dEDGeEiq%y9K>!z?E70O4!f%4`vH4V zz)cwGe+4?0wAkOxzzgiem%{65Kl>^tzZv8XoJ8xzQ0-myOmOc3Ys6Z`_YNwiMp^S?o^soRb&c{WKSJb>FvA;N_Cx9$Zw`dWb&*cgg%{ z3^#5wXWnU|sah>fs{_CaRjYT=l~xgLwjK0{}qaOCz{ z+DN8PIEyrQZi8Qn5O|7@K0<1N#w(hr*45Humd675yiJtzTTMPoGNW`1`B|*^7K7m_ zy`x7QO*q^gWnIjXj5bjryy{rtOyM5D4n4wM+;!l?-3&SGPo^6 zRR53)BbpD1EPuZXu=T=A#mxLcG^1f`xi==$@D7%ODJDo)03*k~ z-a@#f9LwSbEcerh#*EuKSe%Z}J~tQdHAm+O8G%OUS%pF-B?sURS)(gZ?YeiE4;?Ut ziK%O9*joC@5?Lns9-i=1s*;+(r+CS{n|^e$J(E#i08zAGu#oWNW2RZk7bWi4)Fn{@ zpiiCd`~o!1KIv?um9*KmqCD#tq%!+f{9ra{lJq@dqx!+iIy&9cy1u;@plOGZBW6qh zzW105Q)V)#lCPC}?effWA7|6)*YynvEdgEPaGafB+cYFIAf=PCc%DDK+dGs0oAWU{ zFLSy^#JVeNGPv8W8(H4A!kcDFU1QRr!nyipBo z`zj7>`Gbk3=iMOOp37nq%JdP*$l&H?)dP0Hg(+b$&Jg8KJSRzjeXZEG>tf}uRC;Y0 zVX3E$dRK;+5WGLNo-QK>Y>flewNMa?N)*P#NPc?=j=dTM1*~lMdbhkQu@f{~J@SG8 z((|x`O3|GKW{xjPRTRA$z^AnXFBTS}9KxIPiq_Y9G(-(SMW=LEOqCqpQ$gq&f+gt5 z8rXwU=cUXrgRBHQgypoUmnseNFtX3EiKguj&m0#Z89Wi@=YUol^|kf3Ljzh~rEV$< z^>_SmHaA$B%43GYkcu;T)%Xex^}p|`X|vW!;_xbn4Q(CuM+=JB&P*M96AJ)E{IB;I zCoX8H*7nP zxEboqkN>LAn6*RPTqxfRQ}SYYrf%+FUF3CTug=pwk+9I)i$o|~SR2bbg_rmyhxxvH;-I@(Mq&f}*<08nkns~I z(?;(nJd}^fG3aDq3kJPIu(~;lcS;|nOI$J;MI>DibsXzK7sR*mOx!F)N=MqJ{k8u7 zS#uC8?h=Hi29M+xGYiBxCK3q&VYxWEanBLPuHYxD`%AKG-8QFq%EfcJJ>r>UB#lHU zQh7ofpMj*EyyY7(Ps!ReC7va@OJC<;v*}|^31av5(cqhlZj&yuPOOAHJ(&}IuJm*F zyl$>IhcfJ^2=}_V-zr!hYWH)Il!O)=ErE;l+hiZ%Q&bpLCJnuElM`Yqm2kZ#9h=wP z(_`z+Pw*j2g#Mq=ov#v&i#x<;Lw{Q-d3w58pLM_BhT1{igC`o`BS?DkXg^Fc69~5H z-^C;&f)OcxMC**fRql#}eV&BBH=Et_+Mi`sdV&&WSY73RR_7a0%KQ8g1%T`e1s0Rt zeZS1Te>9({c9z4(evP~fjeO)Z;mFuC?{JQKyN>8KxIisaP`HtIDnktfpVroGCS!cZ zk@ox`7>s#!A3oB-Ebg-!eQ}?}3q*CvbnblX;gOWjfSCLk`u2s0q>>5gg&0e6N~-W=E?B zWjU~BVfc3*6%&S?j)pF;!No{81WLAZA zY2TInqf-s*Cdk0*@o$dw^9n!|{=RW=*B6c-Y_~`oBtO5~*_9#(JkI%Djv(eXu2R

p7WdS?#{jL(7_kqXzzeB4T%Pr`=YQiMPtt8QcMcr0AKcx#%rTPVn%YTd48q6yvc#ycl;`2#S zVgb~}n{g8HAImr=+wcOh+D$VXyzO{n8}m$xMYPWtfAz=Vf;V-fV}&Cmtla%1-YFA* zD3wwZLCD)wHkiNrBL>jdU+nRJH&vJiBAefn_6JeOnxfgv1jj;MYb#e-2wdh}Rg9e# zL)rk|w0xmIY8-;Z!*lp)b0-=pY!m*Vpn@(Ofi)-yf8{oj(u2v|1zMo9f%5Ucjy&~Q z>dfBP)6F_7u%y%s{T_KS%CFf8PDvh^9C;by5ouDjHou{AIGpK7VIdM(E+m!~t@O@f zUZ4KOo*}0~e*#dU0xR2KFbieIQ}~{ht|-w6PhcCNjOxIGmuO6)iq+a!h-CR9aJ-xe z*jAJUsLl6-+T|xEPVe0(x)FecBK8a!4;u>&HNbmdPsQnN}P*k+bjNFx~KPKYj6=eL-W{fCkhTX@BvZB*F z@U0=hHV_8_KCf zPt5e+B$NTmk94<6)5-WJlm#mYhBzoDtBRg93foq0uKLRllZ!D4YbY@WksYes5Og-_ zYGnqvl!E_M1o2ONIR9Fr%r!?e(x{p6(Q4TFcKd_C-z|~O0L-n(85Zw!J;Xs9%tUNU zH~k8kIrL8|AF^6LszgU?X>1W5duU~%UX9w}rWC|C+j4<-pyFq~XQY~@x#s#4m=J}F z`lr|jcfEzE*Z2^v_M7Y8IOg!9`}v=lp3j&&iGk%fKA9jqmxX1wa>LsiT&;(xnDq?Q z)SwQC)M*8eTvkziCPnh18K6t{A~%7Kx-H4!D;mLA@d0>7;2U5E6(5ix`PGLxM8>k^ zLUNlUuOtS%U@e??>Sw1&vh}5;j-1q=sX*nlLvp}O=Cuh@N%PSHi6F(7^{&innwFCa zZjL`?Q5s_V$MfA$%{FwDNCWH{BYAnBJ*ZpV5l{V5!oc&HBn7km!l<@|5e}%?F5w^l zEXvNNhiBd+H)FeU?Pz!M%*&#SFZz+ z)7tq-hbX~-1WJxlAb?TgD8BJGQtW7ORSiX}2gRgy#QB2M9m6wqBeO6STz4te7wkys z>@rowq<53QgjJJYn}dH!r7})+e~R2diHSxry z4(LNg7nWql{bFYJsJNoG_p3X8YrDlAkxNcKlYD8=Eese7vyzy9uM$upVmR2NO@aG~ zZ@Hm4oX}k#ngmC_48x@sGdI1g!lbQW4vwOkjY^UQIeUn8GOh~$angfn*2DzSFw6MlQ9{e5XS8x7?_pAmG@Y}{R`+?V1hW+154l2eKb34K== z09rXcz^UQv0_)4ToBy&wh8-#b0D7bhhbhhg<6O(zgsFvhLV+6ZNCwSOh>M@eqnr}r zaj&OuH_+b$+y9PF<>Jg#t}l;z5Ob=U$VfGR%@_Ai_6C5QfLSxuBkleU&I**Kl_rsJ zy%LD@Dnl$fAK$vXy`TJ-SJwmNzryA6zWAPl{Q#LS=5~|#UO-DfUSS}pj``3W3K%RMjITSXn~%*e)+}gRcaQ15jVHslz9oG6W>&Z%!n6QeHmW*6YGNIfwEsH4IwihwM52s9E8zyuJ8)7IzX~7b=m6$FY^0DhzmYF{p&^4=Q8E zDp2?`2SsIJXQ0lHBe_j5SOW6=03)?hw%3J^O$n=gvDmo{HWzo5$LWY8%y#9lS6RG1 z#K8@T(+_J?BQ(#Z#?R}a6MJW^aBh-&+kL13+DVH*2)^q-zr-11G z9;9ppWha0RPv$0xowqn`9Z*qHHI8D#oT!_cDTAhd^m4Pf#t6{WKkRD9OYW)49z)~r zUv7dMq^~OZ%LLADuCi$*Rmt)|V}o}3s>~583?kzm8v7u3_tTqo3%9iXjHkvIaUDE) z1+a5;kRfUy%Vs7bdXIFB#JVB9$B}s)^TWoFp!J=QYJq6H{PmV65ngcCCib%1c;|8f z^t!Lmc%CM<@j%*O`ZVNG9a^AM{Pu_##mtD<^3rW~an-S^zkyoezgN+A20jEgLsEI4 zT}`uyXc0-3CNlvYfQPVWmh!x6K|b621A*dWsSS*7EvCY6MJ6H0#=lr7%OO`yucF(Q z6CnsJEUo#VL3Bx5bsp#qrv`JzSy5#%xK6oVhOC32Loxx~ zoOE^{);S_O&(JmzEdKe3C&k}$ECrZIuHHnMN&HG)@gB?)ZIxe)!8fCDnC*6PDzJ^W z<6o;?woIUVVxMZv%9m{S88w%>=Z7)Yv4$G*;$=-F_n7TK9pOO5tA6Xd@?q3sC*tfug()i+{4TrOZfE(8>?Qfj$Y#3{zu*@ zRpmL_F{$2=a3M}71m@dgoxBX!H4ToZv;X@({d{UGHngHzJw?;ID4-&3#(l$Ee;pZ7 z*Nka^j{2g^FOzqE88U;_wLvt{u0hB){Q+N+_p)Q`#dGXu^CX@@MPKOLfpEBR*~_q zexIOlS{eB*h4;kj0-13rSk@$-AvvdJSj{nNR@{PtOQ5@wR{u2 zs{kzO4m2)Rc&iODGU}e8H`XKm&Haa|`rjfw3t7`JMfhf@1(&77oWM)85cw@pJ%@yO zSf$dO=ycJ+4QY6wCF|p7syD`Q;!B8x5Mproma+*Q{=3G&zjm?PkkIR{x5dH&H>tdGRGSufATtJ^%j8+%mvc{a*ZN%X6H@SSck(y1Wrc8z(@aqWoio4=jrD9>@(Wbj*S3!|rW8%)|k9ZvjJ5=AA$!zkC)l^$MWiTN>x! zRJM}Uv<g(F^qc?wq>f?3})#^NZD(cF8&r z!BRAf@M_BO@n;-6=Be^?mdcrP$xM7`{WbdBcWPHt6ik$S?5EFq6Gy8d&2A!!&A?d> zsg^6`;;+SVfAr>5w%>b_IrW>#gc>h)lSizW$+zL|Xw=8^L$&cCr^5Rm1n8STUJ#HgR=y6FB=XeAALm zi)PNnn)IpnlbkPz<&j_y?tX1C5nYA;o_ABm3G&=WGjNFB0_9^GU*GX;&9quNjI;-O zLck2ylGAjY@5hoYtVz^>mqQnh@u5x#x_G0xIkM4?Ct4H)crP^yUmUf>x;b*FLzFa* zy0?6EkTZT5snPBemcGHtuySO0$}75+>zM1Y0=|0L;9KPcs3{xE%>w$_V@0rPMjlm* zdQyppqG740J3JR2DibQb`DnH+F$5O5UYe@Df8tPF7&tU>Uo<8Kk%2 z`NSIKK_vK}ytrj}U3t}|+6{bY`%{|Fa+!EqZqcI0aTi#=yxGoxba{L;4K5kbH)#FL z1dV;Qkx_Fu>#Hj_)Ik=*^>3=9RgGfMQ=w*nE(_A=EntM7D$f^``1YSh>w_0(lPv`L z_;&5=Urpn)s|O#=^YzV8-bPYvLN7C|Z295{;xlCHgRtAgBJqAezi?U&U&ep|B15k` zP9a@-Ui_8`#VSfCmoQ{R+g`IasTgT?Ib$VLI?mUlQ^bO1=EjVP<9AHg=8( z2-$3JwX7itfX=`>zr*QrvU6#toKBb7DD;>R~p^v~&>`kIkt9EH~iSa&c)~Kt;Op zDiTa;q3&B(9 zR}a%`?)`g95{T#ljBQhzTFY$;IW5p{0rz7LH>fYNj>R&wG-*+qrI>jtscXVTsrT-! zZ9wczC$YKUa8>1}TFVkl%M0sRwd-kQl1zW`g&R5}lVTq~fa5@AdB zAl!c$f#R848-@WiIEX4BsIvH|=l>Muh0tjhKK{Ko89T_1igF%@omKq^7YGdiv`E4J zh*zRml8vt!au)E2d|w+`B~#dfYh^VGi}2xoqq%3@r(I7?TH!E9?kt?}RWa(45LBSj z9z;5h3yZZkyT6Nca;8Yj5bIh)03{tBHnI1bW~-rg*cJK2d99 z#(sifd5fV^Yf4FYe|eP71Ka6u5p;pJH_DfGC^CxMX?a!Os*0Q+GbUskU)O_)0P=?( zbByz-cW_=Lj>PwGDi7zr>V4qhQKCo4x$jESg;r_lC2Zjjf9sCnK$k!xg1qURV6zdN z)qpc4JpN^-v!J9{I?u|%zagkupf!dCsLegElKUHG1H;3DQ0H{fZ! zgOE!9p<0J)9xVx-!E-H2!u&@!TzEIEWj8~bh!PLi>_N~fO^9~X9-rQl8#%^zIKRAj z)gRX!=K0BjyS>8R@Di5g@qpuSPp}~xeKj$q3Uu+?e{V%0#0Hv8k1svRzLfYS9u1NkL>7sB-XC7Wz0L}ZqjE)vxq#0l9iZ3zO=Vt+&Et5KG`lBeOQq6 zjHx9ch-*-tM%~K0_`co0NRLIo5(&a8|Ii_W*rpOE6YgA|rJ>z*{~)K(hkfs(8&eT3 zYT>chCv{C4moCeV0fH$gPRO1{H+d{Vlj}!*(&877!TrIB5sF2l$=qDs;?mm0TVx|g zQVa_a=$K$J8~554G1VT}mCI1}Ny~mw)Erou~hOuiqb&~Fdk1q650en5yTT3|2`z#A^d=M~{&C z&?gZT6)RYeNb5eN(Koe(WVk%@=(1Vje{)T-#nDwx>FY) zU_B{4%~o%9zFk20s!ZBvq&KSDMTC{{4t2Q4tK6G`Wj?jw?a-kQx{_k0yBd!9PQl13 zr7!(?80V<655gBt*_I2gffV!RvjYL1SWvN7M>(ktck=Ky*Ax;bWZk*|Ts7EFbOb8W z+LDR7WK^+|A!>!638f~pzp{+E6P}KbcQpj%j<3$iO+S@!?Sq$0nxTyYdE=F0Y`bWb z0%u{o(*WxO9=77a2K&Eq^V2&LXexOsxAw=Ip$NR8!6z-cnwu`ebKmEjXWIR#w6UyZ zyhbEfkae!MnJu{`8B0+BYg%CGgMgX32NQ)X!*r`jqBhuXy5Ci()c=&Ugso*W` zb#pZ#KBhg!_5{XF;)RJDtN4Gx z{i2}2QLGGV0@S-N*#8CRQKE8Hk^vWG^;->(EPa#OJ^ja?4>f?`_X%H(DGQ|*H%d_H8do;O-F;FM@69MZkkMnacq$B+JjfGcBqei5@0 zY@y`~cYJs;YD_Z=;(%6_E8pMCDN?>S-KIU@@0j;B#x*I-IQ^kjb!p72ike!aD0s-H z^8U$=T5lzaf&*C~mRvw`N#A|6+m9`~MyW98f&GC_8wYuWd)Iy1;4o8JLOTDtVzoWI z$-9x6x=+{pCTxMf{Q9I>qeKZRzCK?VZu<9QaaYiMxEAB1dFxt4+clyFu+4FJ42R{w z$n(j-t`EJVu|^Bb?XJ`2CNMiu?woL@!vggq>rq61XmMmr^fQ0gP%&i1sYRMFc777x zFec-GUw+kMVT6P~D1b~)uQ&ObtL*<1#v%WA^J7i?IZ$LGP_0+V{c{?AN|`TrfVC=@ zAxU;!G(}XD*@S@gk#ykFU?-Ar-4BP*&|=o<1=M}a9R4NlW^S~YytE9dZ6pah_I`BQ zviU+H{a~`&AN@<{phEPWo1BKp?y!&)nljy-ez~V)DyCUde~GFT`Fe^MQXqD)PT{|_ z1!7@$`gHld_TGU@M2dk*J%kRneo+;D?n}0N*Mx5V_8Wu0qJ<;?k(5>t!QBw0##%cc zH`|B48pMqPa&NmorPzKK!~@}FDYb!a-}Oat+!+xI0Vww1T$2x3@=TH+hcA}nq~KTJ zfZA{xR)DF@KS;wbda4pRAD-Mtysk`XRxsfOJ0%Xj9Cfx`K>3<3wp;yzf$y}v^zO(K z%9r$v9{*7o>E`y8j}=|Fq=<6tN#RC_d|-KC&u0iJTjpT_|K1$h2j^Pdcl<@`^JA{* z9frtVgjP+ErYZyGGJq+g3H-J%A%G912_*x_Kvs&ulT(eqh!$<_Ob%_FP z^pk)+L#wKsaQkOWBs?Bige{7AG3er~lUVzww0|B(c3Mr)re%evbobsw&wP~Gnx}pUGVcs?XmnXL3aG`mt zH>H4;<~RvrT~W8`7h3?A408Jip$HZm!jFnJ5s0gzjtPMXF!AAsP^}1Vx>uTKW*(oz zWvb~k@HTFP8U18d<_u4!Wg|d>9#n2Puq27%i39k~8Pb}#8cJwBr1n$C=z)+iu(|5qMw2Kg=bf_$ON8uo%IYe^Ajv$+V<8Ir z2hZbD(M0p}ZVAlcx~ckfxw=}#T5O-%FXjZ8mn*|zM)%Q5t_IwkXAN?T3%u+xQ~p_= zL)IbM%68~Ok@gTbJ{|$&H0h z0Yfq>Lh_a85C7lG>DoYWZD*hS30A}E zAfXS5vTk~kU;bY=llA2*BrK$+d+OAoz+A#*msK;*2<#q%o?iZ0)z z5wWXVq@#Twqog!=jQdVdEa@$J{Jcf6EBz5QR2wdt1WAAoh%OixF_!zWdlDb5m<6)P zrb>>nh6myML>@(nC8m$KdXdUh%`(m@eR*(c(4#y4c6OUTL9k!jn=iPb0svx?$obv zKdprh2+-+%gY5Gf!td4ZABu-R4zdMgAs$Olvx#DqfmLvf0Kqn)qeRdov0U&J8@>02 z4V4NI5@DK`(01@A!^vA+D)k8&KE}<<*x0BU0h#!&>x(E<0shM*RPUP{QS1kb5n<3= z_S0Uj-{`J7Zkr@ry5r}Ze`n{odU^!rU5SoUPXfN6dz^D_LU_YU8kLAuQ#=hwV|PHK z&7WDB@rc_**adge>;Myy`Z%1E&B(=YY1CtF?jwXv(ndzIuPAivuFlCqV;Yjs#l1Kj zTxay=-L`^BY8gV7kZh8yls5lbJfCBcyBx=qCUs32IpeO_Ghzm_pr+A(u6oMdJC+Zj zg^D)=x^!o2`bO*-tzr>dn?wj#GkcCt#^Un*%9U~a-g&YHIJTa_|ANmUBS^aJ10`oy zZ!@=@LM5*A2?DJaa6FhF$707FYyZP7S6tIU?qS5_ybTouGg%$I`+ zT%%#CXY-c_FvW6h0t5IsYmY5H{JWX^&l~S1qcQwS;`&z2HCmdohXF~K_tbowEn@^{ z3fO01CEtfX+x~4MPN-n=Y2Q09P3c@iLfuCgvL^9)%BN$(K9`c`d2( zh3n>wOtQrd6h)e>10^s`0Yig%a=z%{_#}U-pG&QXOcrOK%l$42m|!t4KU+k99pDIm z3N;S-oe;-TJ##8+wcC;f3>9rMT0K^_rZaCe0e6T|)c7qUDvRC@$ETM|zJ z6kPRMS%F#ulZZT20VT9N)1+H@0Wh;iP81+l) z)?lsD;^PrWGeP{SU=#qLkz()m((vXtjP-XWXAl>GoyG00N{+)W+1GWDlAD@!v*kaI zo1bXE&&f9y63rIDV~CD`nfSbR-yV;JumMI0fz3Tr5gE z7@xP+#8T612Mq&&{Yde#NDsU}_Yr&JedlY?;>p{{uaujI>UpZ(>jm zcu;Z0l=_tlknf%0tiWh%(ha~e^?ZyZn3pe0WVr5SK1?{<$09M@?-y%`|MlHkA|2#7 zP$nXfOnLH`C;rDtJv;@}Hi;Yu_3JH3VLW1n*K=xtmQ8@1qkWzFeJ={87zMF+Sdex_ zx>~!*oqt3aA%!sDWWw=Abk{dPL=hZ?V`GfHyRaA{DGnza{u`Pt|J1p?uqNM3rMAtA z{czO891ciiOFTqH>e?oSeTK zr3Z(f)wOrw)YVs>w7PBpsV{nsFAN1)8UU`!;mI&&OpNC+w1*VHG2t}0hsAy3Rq)wH zWh^S0ob+fRpd3f879dwYqPYn>!DR0qv+(_DqiWp;>m{*cQh;C5J`cbk~O@e7$3g@6Y1fF9*&GFvdBX!c0fQM*rkuu-LvZ=p=W2Eo%1gfvBEK z;gNPibXgIm5Wf=&F~^OCG3WSNd;XMeb)jjGjyPTWA}BrqVeOX*qPf&Ep^;E`RSY7c zL(v7|7B--rBLiCR=@axdqiDr%dH;^3|1e`H|IN%8qc?vBRMo#6UvauNDmu=fNyQ==_dk1kJW+LY0KIRws zdLlD-p0)N8-Qpknx$T(;)O9e$2*cv4Bp6(#mDSDRDObR*S)N4FinR0g_}Pl&&T&Ib>r6F?iM-w&~PoE8#kB?HEZ4(#UbOI<<==b!Qcx(VInij zjHaqXC>9AQB{_d`hYju`F+B#9_>bOsLX+bRI~{NOyb&*dVkXmwW9ShF8y~YG=Q0nv zuU6L&9-#H6Z~J14F)Lz&82zk8{luH<4eGHG_H{xg2-wKj7>USOkq43Jv%3g%jmRTX zr{5>>$u-f!j;{dadjgI=mEQ>DBKpoSIQ#){f>++f4yrx=rrM0J_YsH6Tlx@_lz8c z4YRIa*oWpb=tXU_^K?3#I!b^~aqz__Y85KFd?6WFHE#^2T?c3mFW$+ic{tucn6Jf@hNo(wS>mEv5LsZ1RB{o5;12O(SzL!$TN6I&o& z=W9Szq1r3Fpuh1{2)uP_pgGzigpkW6?*eZZnVpd#x7KdDa`!a2RyV_(TFF&R?)&@m z1rEA0|5Lws>;4o+KNzmogTOd-p1{0?>nOKiFrLR-sT61BCfj44gfc8|WDgEd)hZ~) zu|i|@*mDl%z%jD`i@GFkLixi8{SrhTkSaCGXH`n{=4hCt%TiYT)N`rR!lR zf@ls(!+KWJ=@SpU!!&AnauY95$OA8I9a{Sb0t{C4(v`r(qIpI+{d2f?kO7FTC6gVI zLEZJ2htlJ8!~8+1ANtvycYzOl;W)>wj(*2`$stu&NFZ_ULg z6$v%TXL}5-H8hT(Z0X7PvLZp#A!3e;Y+7y&I=qVQn|Ih2LeFFaxYh{qO4Novixc_u zNPJdkF$=dTZ{EEBKD;kr#F!Q3vh=StP#1Oot0;0U5*yDbp-b3+_5 zl3&)rG?+72?79%ah%wF;` zr!|rkMv)Pw#UoR?t>xTfz$|v$a>FvXhap7qrSD0WhgIKT`#w}2bq8N8R-jr!(qw*E z{qmA}2^KVj?bAFB1m=O98+>X!oU8B$qCMp(y-)*BN(7nHHaaw-dPxO8t%BvJ z*iKyR)j-LW!Xa(}lzINi)g2asqCB!ZgD{7Fq-9|ICt>7d?x=1JAQgUc)@kxGSTgqr zf2Pwi;+zhpaIzRu%whuu!Ia!Ds)|k){#a?%p6Yvw2pgqXd96i6A2x1*mav-quhV7< zmuU>QUy%Wj{OAydWky%xZNx40OaMTzCMI4rcY4+Q{iu8=P-EW@6z!Sh??-@#$fXM7 zOC>z~31$~cfUY10ZE;Z%+w#@bp3=&L7-FROlOeku6ZaY*tT2Z`dhIy#)spldH3vKD zkXS+yAy&&_Y7LhmBv-@*v?%3tu-1^xVMFme|1e1sNx6xh{@1+7+rj*ufA~GR;I6Xi za=Mno6Yo$y_5xwmTSFWD9vVXMQ%mQBv}BmfB>GRiW+Aa#xAlUl{zkOPd3hx|1wZi{ z6gr<&M02h&KHA-k+9$CwY2cM9h^O!a&_xu~r()S>xiSS+Nps5u{@T1D ztdA(=*B9>f`HWFFIyFV_l3~YCIeNQl;vWW7VjvKGQMJ|j;%2Vx+cjTWIzkoC*xzkPCsA%YT7o+i;s0O}xkkb4e-Uugh|RNCM3UuDhnZv?~T4>>7=-2fnrP4%Pc^3Jfa z-+vdj^!oVa^ZFfsiRTHNQ4q#LrJ$~+VA*~hJ|y~|MGbCWMZuU(pH~z(!Fd2lO$Fmc zf&#}$JPG8D62e2fSZ<9IsF(ZR)Uy^Q=CI|xJf9@Dq1#-H&PLASPC~LO)p;fNb2b_z z+4@m^>Ywbh`TM(rP(d$z+PhuAiGTUiO&vic<|=Xi=@4mILI_A7TD;;KlBZ-vcV3#( z^6Y%RPC*%Ny7?d-vK!*ZI#kuc5qQ*VAGs)3ZDn0A7myX%wLVI+KCvCtq3_Yv3e&-} zf%niLO9P(uCCnXcRl!nl9zp2(zU`i>&FtUYu~=n^yQ)nX(6Ln)J|px&>5YbfpA*S@ zv?qoED)Xq3{edfSexTa{wLZ-z^`e3qb&AyMPsr4)TKA({-+CaUF5o@*D`2bP2u>M` z-9LYWCBZh-QM7vxUvjDG^+)ub;rWj_$%oJqw|_t~buDSm#UJvD#1N;D0s(%_MRirY zDugk)?@2Jqo+TakV*$b z*wC>LjoNeTd>FXWCzSIfnamtGg7-}6Jc;I;@K=_g=HZ1NBNb;(Y;9Z4k>cm4BqWN(2FgIUaEA$}{gH>><)@E05^cYmkh5bJ!oBQ!5av>7 z5-WLaXzO0ie8dXddPBOtNdSj;k^YRKu&dYWK;Xm|c7#qoS!bq%LX`FajDYvwF|f=J zveFY0Wsgve@)&Z<6mrx;WOcTKjx~2 zjfti0O5?-eGBhmP6k#B8Snky#ER_vson<1jVe*#;03>IP^PWm9BkZy2WRdu|NzGuj zKc$!#XiO%smdjI+bL#z}(frsXbJflBi@gs)Y=sMWYM3K-7~t&lN7p{m0sL(t^jn6a zMKo$f7SM}K?zPmqY#8d3JhNB_hB$my3Q=eyzQ0H~!aFT+0;o|3#TtR?p5~!gyAZ-X zGyU*Hl~_?dIJI3lv;JyZfM((0n4PFxL@L}k#@6?EUhosE*&5M3Qbj!c=eC8_&%BF`N^WiWFBGwIF}1{kc`POaZp31}_vJDvY)+C)Z-tF;xNGhsSr z{H4T8S*dbW*f=boX-NQ_4|M(s8Mi$65RoILe64;F5v+JZj{dr0ALLrZ@x%x$;J6!7 z2z>IFFOBPrT1ndD&Gcf;Cd#q%m( zVbl2u1M*MtKqgZ7T6z2F@hJ|aa@Y%Ftk_He7P?WsE6E(b*i3Hb8uX_T=K zAiwVAso8}A3E@CI&}#Q>6I%R=;qBZbZaW;fSoO1mJ-|?*4O)Ly&lYk*c%clkeFtVt zSdS^qS1?kQtf%~{A%F|@x1ENle$Y%#jlaQNOQqx$6-T+3Ku_)U&vKznkusQ)a?;a`eB{ML&?7P0B`YPNptrgb8fkcmx|VFER9Oq#y%|bm-l{g z^BQVH3hsp*j5N^UJj7k$B>TE4w6a-o;pp#!lEj6Uus?NJ38vt;_P}1cJG8&`8rn?v z(Lh4Vm(VL3nTS1d=cF0=zVf|HAYFJ(nqQqSWl)-?LM)e9*%_cXQcUsxqFn{Gz1{%1 z=6TumcFKY3Up5&1Lze2dC-q!1sQc~9EN$&77pnV6=vZ4>!gLYMhZ&P9cCmFP8V0k|?$ zC$l&w#AH1{vK-hU}B%yf0}BNLp!T@D@*9=;%Fp^khjR#cN=$(SsAS zay8#9iD&Z?St@-C3Rv2Vs=?kq5piSE4D=L>msvDz^N<*XYJ%Pxu9<9yCz2Mrna8qM z$0;kD{rSDq7I|5M;33sI*4M)VUSJ`+5l~Rbrv?G0?6Dm_$sUfUYbT0u;7-B2rEn3! z0XrQ_h`lbm$^oYHWN|R$j}?<~#C(5O=VBF>sA9i%^?w^UpKjVsnBKSn#^Q?L1P`%P?#kU2)qY@GX4G>$oF*$~N50aN(Yv!)@%)qfg0 zvX16#dDg+BiTRwQYd(5n`kqBRm<~b9yTE)QeI7AK6iLDINz=OV+0I?zTE{2Pn3ygG0y;B=v z3id1vq^H9Eq-$=AvkZg^0{?{TT)R73vQ8G7;k$OP?jz$#8m#hcQpgs2NKn|P6gu_v zH}n{jT4yOpZ%PmkUpNHxTApBY2vlKzfoOcQoTfT9qvU zCdV(Uq^&2vLD>Jq5yh!io*R3g77?N4bIB5;tX?S%GJKfop36qO=c_s)F534&pnaiS zJ7mn2K`-u)CUe(yP2-enM+Y4$pco0Bv&;j8G*Ly2_z^uKj)?qL=kDI7>sQ_%o#u^xTJ89sBSxOgXOTD3QS_A^d|q@C zM_E-ZygT{1>SN@8&CQwvh#Qu)Ov-=1xLownJ(Q!$lG8qr?vp0 zUbihuX=T2iUm%g?ZlQq_mXLB(16D(Nr_V^$_2of(-asJkakW+a3x&e6S=Bj>pf!5A zErk}EL%`VvU6!x%?e`~FKhP+;a-CF(KR`Cm#jw9+Lf0Pna}_7-jl?q{7SR_r9ym8E za151p=f&M9fhi^VOURGIik3@FMhsK|%iLzqgZ6Kr`2Y*dNAWSTK^lMUtCWd!dilz^ zJz#N17TYP)#Ebb78z{fK$@2~FpFmP)u~Vg#3rk-fk`txUA5OXOZ;=t`AkNjme{b~s zvb%BB(3*zQRucV5Kq2y1CW@cQIwq~|+VlyT@n+=jI_g!$ z(S46HA5>y+%e3WIYU5<|YuQaJqog=VjI+}WgzN>0E*sOxz60bR*f{aZ7h=kI<$f1T z1tF%Y{@a+JFia)HAatJA`h5%lz%)7kpB54*CS1V47;6--Pqs^6~97 zgWCNT=^A2~5Wc!f!Wn1JY-iZy+#n{B6SbuESOdBenHm9JcNG(39Vb>Jcp8_Va2B}4 zeWu@%?=TV30ZQ4FU@s$BvY=m>CMDUhV!m|<55!`2CK)K?qu^Qt5V%mGg#tm-JGLb5 z*oMxfs0&}>q0k2sb@$*|Nv3f?9;d2CtH0H0=ihB%flcJ87^$Nh^dGx-K&Em4fix_@k*Yl}N= z7T^QN8y%Z4yCi`>aI3xT!c+NdphiWMwhQGqG&P`i(m(b5t9r^zp$yE4 z(%4Nyj$$W@JVV;adXR1<0bIkNEQ}M@BgmPAT7OEX*H^9#qU-CZQL$t)bUr^!9^WIA z$5DZ${xtyV4-^hIZ)Q{zz0zU|SZ4$+Xk0u7#~Gxw&;9xbrGP2G_Qv+EN zqCd^%<+~*nb`kPtMb;Ozs4mk&gnJJJpfTVL^(C{eFTV1onEhjxeW`i36k|dzE25k} zTaWr_pVt0z95N|%l3$X5w|9>`_9>@lbdfpC4$}4xv_=B)k8j=1b5G(#(;7?MPF}{r z`%2aT8{)88FsdWh??`&1HLl%}ohIlw`BxXy-?IA=RTnEr8+gBjvMZZi3?Er*!C;DU zH}(~DX61LDE(1Kr&^s|?x8Zp2z@bPBDX%Zyi0M<2MjsE(P+W~iety?*&Fd`T)p6S6 zr-Cso@XU*o3Oc zHlVt}V*Pzdz#?KZzr}b&wzRMZe#wC(-luMJMTydFufP$0@^2{v&7!_d`MqeMd}#+j zPweZ-TzvO5wJ7cSL^T0EO94Ud*S@gl8}T&M+X)j;&VC5D^HDsn2`v0&d6hZu5+2UW zuDB>nHb1yikRs&shkJ2)(X(99GC=(ZM0ddSj{c9Qe`!?c>$pPE*t9Nfu=Al|Y+WH3 zLL|_-W`RJ6(HD>qhitDD6ezFEbN2BKH{AUFv=z$xOyjI+EiQn0vn*rx5p|YR8PI0fqqo00afMg@55Ga{({RVga=*IgaO}rkbQBY*iKDwiG#}#M*`W;pK^KdvvimJ{N1}a&SZrv_TMb0Nkx*i`U7(&V< z`H%dwbmD+nmA)0oG=JHB|03x?UQ;ZCzIT)B&#!&G{BUxGBCdP-U~P^jtXw7M zS>}G6`%loibG;yp{@4j@oqg_tv3+i15WIT9Fa(@AA!tpo1?LKQ|7(qd6d3}PNVC1^ zfO{luGCC5Jy#3uRCg(`^SKGBM;cj57O?E;uD8cetkl)c@0aU8~H&tq_pQSkzRg};* z+PhbuZ>-B((FvrtP~@|q`t&->eeZJ%bX6$>iOrr=G`}IxbSCIRgf$LMsWhE?Rx^vQ z&2WUOen-0>`Qd}JkA^DSC!ZPiEbv!556II;>0^wPc`N7>*;{}z#klC)p%fi-1^q!j zqz0+tY0IJhH`?ly&*l))H1nDiL3ch#sk34O*rTj(%}(AlcUw~x2&5q=ojgkzGN;yaV!&|xaexo)|g|?s&sXj3K#J%rSu?(wGMPuwM%m%^Wv0S@B%Yo)>Pdd&f8+i$J|B36L<$#6c`b^jWmg(?&H*0`A z4wx5crRkX_dD>UbdbtnW!9zlVao*AvAob9dIKkzpzn&y*>$UN3!nAC!nP}(&+%=jf!Zl6#wZcsLCN%SEXXWz>8+;6BG~@@z5Jh$M%wV7~ zn`V!$#e-86a`H5|_=x$dhSP*1fCu(5Dnfm)xepYeBQ50LqPS*5tpIYIdiihrG|h>! zg8nWAqTow`kf{fZ`@jpSwBGc(4y!-=!OW|qlXX^P6wa4L*ZDrmp1!}fCGn-IU_e=o zJc=-aS92ojmR+qju}7m?C}w_)&+a}NO%>V&qnmGbstL`^)uTK7el*!&E2Ct2#)klw z2qyF$iSh4IwNis_?wj)Jo5XS$lp``&p8*SWtN4J2$KX7=c`5-4`^mposWZG$KXM7= zl8KZ`CmR#MfnW5#?QyurFiW?7XD6@{`hyc=C;A7MgV!*NBUyTz+-P{4{j6lrnPsob zES3;Rq0@L{;k$+f+pV9+jaGD>@HsORNN+L4=e*G(lW9W!lEUz0LcqT1pc~#8*`~gV zJ8o?s&UM1Kh*gf9YCTU*)=&0L3NvWmE*BB|2l17I0$o}dJ0E7iuW#4a!YFiV-Ol@3 za8GO!Yb{-K6CO=Ugl~65;DLe4`I}GCDGi@HcYscETftsF)bfTKFI;l%qQ0ZKCCZML z5v(>W+GW`x4<~ACWjNcA%*s2w5jTKgzt%mq(NFx{^#nu(3d3bl6mnekW<9nsabvP3 zG%_08|HWN7MmC*`C3yYQ+_*u4Db3{1WkU?b)VhB zYdV-V$s*YE5Qq2HN*d#{#<{LW`@k?`RoKvFxObM6`%o~l&CM~8=RAw1^0K+BwM%1V zlxh^-?_YgI2-{dBYYGQ|d+?U(`*4BB=8>8j~A39$20w~GlIkg*87KV+wOT;KHsEghbikyxs0ap*_`vX{R|Sj`*SD7 z-KtIrYx8%ubNN~VxM~}Kw&GJb9#QI~z94hwNa5A<6Aga7pCxR%_vH{3Il0Q;Wv zn`w!fArfQq;hRIi#*bMEUSQNx_TWU6F|el|noYSiKlwE@5u7CTn=$(qnMH7~9Ve}F zai*iA!Vb0m{bFKf_hD>`z7s9qsg%xqP^-d2syV_~NjM*FHMXgjQHs_;=DQurKoUOW zG${0jC@ARqO}hREv|G!FPB#h+*P8#}QN2Dx+IIQ28{i*RA_yQ?C?3s#m?GXAMwZGu`eOWh)@M?Y7#fT7(qOj+UV1Me2cAL{q zF5z|kmGl@)e7K-Gv!rF~>kfKZ_tyBc)?bR_tCIODDcJSM63C#b2in+R_TbC9&d~U2 zheIz=;Ed>?Swtp!e*+|~-V@-AaMyUk177YxzE&-Hvo1YOl#SCY7G9xVfYEp8^o1(K zn4c?f2SH{1S0(K?@U;!+^YS&^4*S|1GcF+{L}we|_H^#=mtv4)jxF46JC!r0NO8mZ ze%tdzJ%lXpXv;aly)hX*$)+It;c*ilcl#fKXmVHFo;OJb=lxVc zLbTx{agRH;Nw)C8;_y#Uv(FExE}y}LtiSHUffKezt&f%OA`E(+uQ8?)Rgm7M5PCgT~A4$%(Km}}})FN#ijb_W+!AfbU}Le73F zOjBu1={e;}LSh|%EfXA!J8^I463!bEK1E9SyI=oHjpcZ_deCC}QEvE?^9ub6Vxc?; z`t)%kMNdm4z7c8?*~!>@vzKV;Jtd>9?DF?B6mJ=tsY5_bGZR!JYYR(7zrRFXRBvzH zD(y2smIc0$tKvAKKD;2m6o%Zkvi^D(F?kahnV?gdc=hq-DVjC{yp&VW1Q5UiGAKe_Z+@EG*emyQG^|ZW znAe@32QHeQI5e~@B>d!7lH$Rv;L0((|5?`@Z8Z{Z=J;DIc+_($pnM8oDQmUyEf z-qpl8ah*MTenBpO?Khv?_obQ!3fEL4`+E!=C7rr;e%Na2yO9xXMCFVQW~pcZe+l1x z{*y&B?i{ga2pa;Di1RT0e3Ui_2b3E;DEeDW?E&+o;okLshF7V#Mi>jLKaDCTm;f~1 z*adcKK$jmqtMZ`HzRCM*2RQ*TR1xSw^z0spo(oVQz1@iG96-1JE*`dbXXT%a_(aoE zj-NC)(AqDHuC6M?XOZ(rd^2o$=;rS}KaItUDGHz+gYt%+vV_nJ%l@+>pA_SXXEoi; z2HR3e5h8v3Z8fh2p4LsKozrSrb2yw}Bn=1mWxoqTJzh1dA6on^xLcbGJH4d&>aFmUD;C6`Zngz|(sDl^ViF^gN5+&Zw zx1(H1xBdFet}*o7L1W0%{uuw~V0s27g_w<%&krH`$#NaKKSq|p`{q`EelD)+R76V< z5o)P%{miL04kL z_jqnS`Dj?xGq{7;gM#+N*o*@{D>$?6)B4K7<8u){CB!SIAe4BV$A_GjL5H<9spbD2 zkv8C6D>ro`j_1EZgu{BDGuUrVM7$NRdKU!*pWCHy9L4Bu(s0TNSMtk<2IX~%540t` ze#ambwBdoMCX|x(bSwFYvFZaX%B4I42biRMWdfe~NtIqZ3>6D~=JRiHWQyrr4ttC6 zzXyx|We1`BwgVb)ipI)GD}Np^oV`x*=Ws>SY*t^CN0c=qAX+5F#aC%V?4ec0*Nj^H ziZ1wf<*+d`G}~h`!Rb+UZP;;3xI+r%YQM8m?)UTx3wc5SmPW}zo~>2t@m%X2lP3;s zMSQmBPz<`jAZGu%U75}McoU^P#EEF&Y}0DAV)DN&i|JBAa6J5$2_$FDW|aL!7@E+p zT`&3y$H#FEoM$OVLtFb*t+xFm>FOB!IRrZ`?%N8euDqX&8YCd0OQTJM=P7Sr-E9Fx zl1Kp1S)?XKgZcimhFE@=MwyJWj9iQrsdsFhE6*B8yHzCOH{et2V%AcMmiZ z_(P>8arGvJY6b%<7sw7B%lnrNcZ@(P(IoumZaH7WpX)P(>E`stT@)G!N-SRH3^*P_ z<81DTI9Qp&BxV5GIAnq*?9xo$a>3pNDV{=G4PX=`QmP_nzLZmmT-oxB$#2GA2b49x z!dKLpfHqF-}Ovlme_06K4=g9chD< zG%nL5l%Y}@{vJBvH7huK&s_X&-}mUCSo&ldsJwQjNnIN_KZD*Be>wTo{?j`}BeOWl z4un<3a>iDVmllmmYbo931bisMKcwXZ_LyC*?NBS|7P+6#9a5f1Owfq?-9PcmLZ=j> zD*m=vY$IoHa!m5RUphtgL$kDJ!q8697hV#XjrF?xqH4`LIEODIk z80NtN`x7PTFA0lAAp!-xZL;CMQM~v@1PMeak!;RC}Lg2UHa;u?%cJfGIS+IRy^Y-SrbY zYBNr_(KJ<;1xfWzV(RIm86R6%UGxsOlHjFed%$?MveYBmf>+EIieI> zkuai@$gRYeo@AdzXB%l>;;K8329?D>V692*MPC+g^lyg zc-Vj6D8`a(+%oG8G`}Pq^a|aaEOxYeGNIbs##HOv1kBp?48oa^3o)01he?}!m(<=w z{F;~YWFW3cwv+$PD<9GRl(`$G`OH6wEvfIyQ*wnY;@yAomBOk+8o`A+9^y`l6^CU6 zwcPLb=+{2`Xc<8T!qqtCjPN|^HyY+Lv{{GlCerg2=W=*~x7E@KxF^5E-{CvSkmwQ)DaAXLeC!}%(GhX%nD+C(MAnDW#ksUBUzt^NGZ@K!0KR# zx0e(wufvWpX3paG3s79=CG6^4q6Kla^8&dDXfr1E`9-=>0xUhfhr$7YY(o^MUzRp{ zgbKf8N8A)$&eL7|92-irK36IuoCm_>@iB|QPLO!=eb^V}vYkeeq{Wo` zQEvFP82Dq`jw`)U6zM?r{-G(qdBdsn5A*oHF^;VN&j0@y$E&fGfA{`B7)L15|JCX> z^1m3z$^XJU{$m_LeHpzB|}4Oiwu=7j;kUY`G8vmIWTBQfn;Uu zUAznKSN;`eb?@WHP7>(f?25;H!axb980sn!0U~SCAQdQ;v9GMzQ{c`cBArJ%?PE>e zsl3)2z@nXZ5Yh!8{~Gb6V7u2hE((mN2Sf9|{CHGwVnG$h!pF1Gv>MQ4jC68He7Tcl zV2%3f599+Ja*5hL51GX7PE1ryWb8qTiY( zwe7{VEpvzEu^}~osa=F7C;stJsg{76gqjR#!TG|I6vU^R)|fBy;T^f=z@HBsSVJcU zW0@&PAB;v(vRnyxS6-k^u{PN{t1io+4uHm*^P3D2&{(d5RHM?)APB2*W+h%zysALW zFgm$H9&_l$;^x|w<2#8N@eoe0AV?STPqH`e0F28fA)r(cbwWK_Cyk$lj9K(P_l|{2 zFfIRSQq!d`>PK-6C!6E=9fmp2>qnGg2?N}K2`L{2Bl_4%>77+(3i}~Z1L<9`tL?&0 z7riSy+4Kjf??ZU)oy~YoYo$~ETGe?)<8b?WP`nPc+(p`)>`y6rw1BN<3-8UWOm#aQ z!%#Uz|2gp>9}u9H-nOL)YIWra%-ZTz-PT-W#4_WDR+A^5(lpP4=?=O)RakMX%w#oc z@~(-fcHr1j+pDYJHuFHc)Wc*Nj>I?5`w_#w?J}&MQ4n|Q^(|5ZLTMDV_kAds%uei~ zM&xZ>+2}4m9@82yYayENrO=B3JMw}MCg$l>mopCNUF|Q-6ea%IPb?G}amcV$8B~)K z-En;Rjr$JoF}3SL-$?mh?*TW6zh%6VqC@$qrFTz0?CblMpnX%~DaoT={I9trd5yy? zu2J<5#m29pN|EIblP3 znb>4#$xb!1lF!W4;0zR+$(((XGj*1GrC#K(e5Zi1^y{g~*EH;DygENe0N|fRq6_NY zGKW|nsz>70usB}ryW^quSEHo0HAB02=%KvVI*Y8-2Nwv+1o;p%uGjVpIe&=%E9WWM(CGoGOXP}lY;C{exCjsNYY%Fn^jdnPiD`}rXylGxjTyr}d}Cv`YuezW zLY%zbNZ-_G(>JZ=BpP!2X7$9~NQ#J+poA;?&7ek^tNPnuFbE*r?R%DR0z~5eEzdI( zfcd*x4n{IIT>NShFsn3y+x0FN`!YMBHZ|v=dY>HQY#W8bZ)9#tM4ZU0*8WS^8&v`81g{XU33#(Gfr*kjBZENuF9x zSXKBI&-I_UaQ}({j)!CVAW$kWS-OjO!n=bGl{PP#`~D!|93C+%)KsTaFZBX%E&5`Y zri^?$v3)B)L!Hc5!MI}YXn%m#PLf@oEaq*E6OHgRg|8Idt{D#Q< zZSaB%b^*UEmq+c*c&ki3D;lg>W+(q|>A28q(j1LHHX?nPTtiyDKcAql#Ho^|e-jM~dSRe~-4Cv#4B;~azq9Y@bK*Vj zVRw<=7-mB~|g9|n8sP{ojCw=ZD zOyGR`eF$ipuG^5m2;j_jj5;^l&GH4VC7;iSI?n6Q>~Q|l2kVQD=j>lTa`*!72$^>S zCP9!oFLf<{-*fQlzjwvZvFRPeahyJ)PINby8oB7|!w~gx_B*_F-(toTqQrf*`b=cK zVggb_4)35?Z+-7G{;ers6oK#p+v5~FJbJ8|0D*1df{+lOyd-$@Ri(pM0Y%k^d+L^S z2YH_lx{5?TX>0wH&nv>~OaFqgqPlAqvtICtRoZK3m`_t1k*C(x7rX6#}P#f0We< zGK^`2|9Q`5f%v^roUM@a9XZLg^4ejkS=1g2|X(@~;#on*$xTFIa9V zV=^QtMR>@Mds+0E%4d35Oc+QCn+$NI~_e6T<5;hpuix7 zJ0+R9({e>6A0LPwW&orA)tgKzf8>Z%%F!q44Ao{OEhhkxnOoOporgoZSaD? z;t4~BFTnK@5;kMpw#aw30q}Rca)0g_;bIY;3^sid+!HN;?FE)^uTPRr{eDHOd|EY+bb-S6Y#*i0d4xS9^(ijN}(bnM-y&WG?2x|Tldp~`O~Ulji(#5 zZ!}H8UsH&4G2=de`Kah1AG+tRzO|B)(IQ28oG}m$6I*oB=wDK{XI(PMu;VZN*iz8m z;4we&K=^wy-s90wtxUzgeZ#^+hv%_LDIAZ}J|v5p(qPF4ptaxvn$Q;+PE$i@7rV5| zU&|hjuWciG39Vlva;Ebz67Og%cLHUXHtpy|F}`RuA0KnK7KPZTt&<0@nUepRu2rAE z=bTU+22u8EKpDX%TT0<*? zyz5{9wlOqD8lW+cCdY;1;`)i4ZaST3u|V{`Vwf{5R_wKuP~ia`k^q^2NCw!Zl-ya=uAGoPiG2D-t#Yy`39tRjyCCA8#flYn*~PK z`oC@4+hmUe6AFFd@18pbNV0=rZGBnYbb=^*mUYu%nP7oY)Q!wIyu}`WbahjvwfOV6=4g!fW}&xC z?*z(c6;m>P3Yg zWGOiYKy!Vixsd`Fu-Qd1i>ri;OViHHZ2<(&2LJ>`K2P^-;j=Mt#G}Vm*8iRwsC1b@hz`LvI5oA*Ldv-n5taBG@!-Hde-Gri@ zr|jy>$;aP)0%vLmPVo%*3;z^I`yegaro8FO$;@U{ANje9QMy*^8a+TTm7VGnsX<#A zNF6GYrQg?BoH|(T{56(op3S^cU`di5b<~28=}tjH5su@juz$aoyTDssJ7utE%<3Z& zfK-~uL5kvE$s7FS7D>bpkF>zDg%RMf{9FBrWlv&6ADxogPNGKOV9we8&UFoVxkwRT zwH=6)ZTt3TJQU;i;_snx)AoF53J>T+vnm(;X+ck&NZ;)xD8+*HUF|iPvP*CHGf-pV zDT)c?30&Ih?jfq97-ay+>)CghhNOTnEJ^U=_drN(}UK_@4YNj8#ON zi_#yw+E;k08_%f`1gRK+uh7iNPY2R&qrs;!0Q1Ph>(1pq6|6tY9Fhi| zJuP)B^lupdZ%F{#zw`gUB;YUn_rH7pA4vdh^?!cp|IfbffBoO!M*k%l_>Uw&_5UOT zKx`KOH3`7@Zx&F{3JYh$HvAJ%{F0p)p^6Wv>Rvf+8501BJbFFZCjWUmcnGbXnho&_ zGCMn$O7J_?J&@NWXw86iH19lE2#V+ZSeb;1uvu0hSe+JpekK@;Z&JxVvd)Pt;x~8k z;}+nY;oY&&x4er-ncwxo@>R%YqvG(?YN+rDMnwr$(CZQHhOyQ=Sd z(ea`O-4Q+crx`i@PR8DO*7~BXfF)0Z0qsb&Qw;oD?Ce5p;~7siI6OA(fs`5vFG}|* zgFt>qDx;Nz6Nc%`NYV_iOs;Y1;oZk5XU**ghK$2kB@P<5+Y<&NfyA~03e3JpKcj@-7 z%QU7c=le}i*Xau8k-CTC8@xvCbJH;h8b*UtL@ZG{44DIopyhD*uf8ohTy6l>eour+ z%ZJ3b@+P{LTpjXUw`YG!`Z$S;R<8yR2t`qyI5!xfJTv=NR|H!y!qzFE7=K+unc*K* z=x_)r7vR_GCkHlg z2JjAVdFnkjWg%NI4RO$Ra`t*gGmGsi7fwq;T+Or;Cpg$nUT8@g02)VDs$;%m5uPco zbMULZ##s-94fv4*ND*vmWt(Xu@c&b}qMlaePwxoUyDN{oVV=cZB0kc!4;K4+0^d|g zDIAzGJFK`SL(d;|#vUCRvc`m=Fo`s9)G~ot&hnbgg_V6rVx=Tp{1ItdlL2;9On!>B zP9cWCi#9cWff*F8q`4w3J7dvYw(_SpBf;kX;KM`$zGP$U8d?V#B{)r z(kX`gwZo)YPE5Yj;a=Vi<0FbWq)N6m88)D0TM~M(v|SOmLKi&8liN8>wZ0wsDbYC3xs8zF9_BIiPSw2J{)Tq~ZK#0%(obM;tG(3`VY$6)U*>h(!EEc7c{*WgUsEh_J7_Dlb|~dpId-M6v47FB zgRz`evf!lskMlvdZkCLE*4~t=SX{98YS3vKKr#xKzz%0aDQtSM)us*DO@QA$J_k$A zv>$5#xTQ`JW5R~l!k%TndyN``X_LEX5_3Cf8fiXU!ZPJX($!wM#I)VZYK)}WqA7lB ziLdGSSAg5%k1v>F|KeCV*H_Dg$CQ}D%oyZh{c+CC#M zLtOW7@)J>cULBa6@-u%E;X@L?G1TV}cdS9iggRZQc#vYO&vrWo{*%#+%y<{i!uXDT z##FCbj{%h-tT1;~xQO73ch5=L?rczi6^B0+{L$GlYi8tJ-3%1BYHYKby{qpCg5%i$ zoONiZ*nww|7Sb#%si0iWvA4lp6@@lZCEr^1M~swi+`ODCq8=laMj^@=zbJ`tF5sdA z_ZrW3r+i#B1J8I}6VBX9u-N8;ucD&5d7)EObk6(+N3V+25hE6zZUjf z+ijkatQyYq;9l~S?^?1WKd{VL9jDh1s{i|Lu`bHK)dZTK*HnMfE(?jru2OXZCRrFU ziKBS-%*`y0JKTK5f;^Dy&m_X0`v8U*Q1;|LV;jsVmf#)Q=+x8f@+u@jQb%H9yOX40 z#%6kLW{f?qwU|19BsLmJ{Sz0e7_idN&n#<&Jj!b|q`j~>Ox(?CzA%oXaal`0Do-pw zFBrkI0k>&l1b(+Xdx_{Rv}K^s21FzK!gg&EU$W*?&EKilrt54R`%GxF zxfH7*B4cEMp7ZDK>>LD*E8}(?EpO)eL5K9HPuahI$XvcbDA<6KH_|lqea#g=7|iby zO7wiz%Oiq$zlIyLG`dw(g~WFEp3WzZ&*ViitDy)a^oUae&!V>yeflGRT{?KFho8w8 zUjE9PQC|wC!?JI1OBjHHcjmI+fcO zP_(h?OSUhyqh_n4Pn5!za@2;a%wU#WQ0)rp+y%8Ueh0`L(ikGh4^`r5rrN#Xgm}Np zP#75KN-gcbDK9@?8z-bSXlw-ELd)SfdAODfzEiFA%xaF7bCzl^1_ogb z6R`@#M`5F$QI$dUeY|V#>V!RH?C1ibp1HL0;%o0>u^ex^2r9*Za+rGe7`lzGm2_&y zQ|NG)OO9ZXtPK8WJmXIpc|qnqQl};Hc#+GgL(!7#)5PJ0n5ARgqj-WM{}N%Zd0;KmYYX0rrH-=F;4vaQfyxc3loZuo+w*WsqYmI#VW#kt7 zNByw$?&~O0S^gMVjic1wh~jn?s^3Vta`ylu<2e_z3xsr(UDYLEhjY2d%!)Fj`M>%} zDn@A^$wgmVvRqh-a{_13DIL7m>555Cp1C_cW^z}|a3nCXzfHMJZ}IGXh|_DY><jXupo4UzW`cn~h+)@+o@Ez4%#*tC>cxy6yT?rnE)DtpinqV$B}BJs zRO^cA+~8qEA2sG8APgZ4EBOl?u=z|`^(AuoBl{8>)aooEPrVw^?Bueyd`tF*_H|S| z(9{q4+bW^vX}Kj22CO?%+qZ{kM*6oKT)NJ41ZQ}$S9K2G$$EDl#7o1lhOe%CiSroFniiOly zp#9w2x0XhB`Rg^j?#Tb@OUCR@?JqXYg{1kGOCruu2D+g6jM9;ft8slwyR<&yE#`m(^z)Xffd?8#=AL<6;9SO zG^pw-L)vguVe!sO@W2LlkL-Y)3l+xal^DgCKM$vVDJ_ysynoWJ5^zev4W2W10gPo$Sl|Ks{o8eN~wisR}ZoE_$x-KKp7w3^F|~ea=eR zC|;jy44n~&MUGhL{+D!sPm5oyRHxv z@T$?|;?|oO?%}iaBJE966a+6VmII(}_u9RMB=5`e9)L{|?#P~D!Gz;ry71p@5#ovF z_RxVP{}nLLdRJ4|isswwgbsh=WdI5^*`yQ*(NiSY=1Q4$aj7Ay5RYafheAJ%;4Fr~uA>K>j#N&4sq!!8fD4iIUv6Rg_;a`BgG6iQ-b6BUynitAq}ILa3XL@th^ARL1Lm@gtBz~pa58>h zrO-O5fAX%*sju%11$GXezsg_R)->@bp37g&h5E?-j?8=-a{NHd0*w7_n;tv#dzXk) zvZ4KF(VqNTVEd($i-G*^NdAU3(mL0+n)08IDH|qy=7+#shEY=!Q91&Nea+c8KE?iG z1N`iSZw=P&W!Z3uHq|sMcA7U6QhnZaudGRg(wP^kvR!W^7T!2-Ky>^Sp@TA}{?XId z(e^n=*Sa5vPlMUzm`Yx-7NqmNdns|gDP5*`F|MP?&lwQSl^&PHcBm?vNf0g_~Ahm!RI?%}Ma1|e_ufNb3wdYrE>FG3h_{oCBB=YBjU7o&PG zESIY;xXJG3Og zlAR?x&CiS1vXmgq+Gkfw?|%?=ct4enP@tjIbiqcKCIU-Wbp4n9+u_Zp7zIrQ&toNN;bjB_ip{jUNR(pxE}KLVk=8L zJmxS5RrJfsi<4uCp4>&3h%@$2m+k2Vr3i;uKPtD)WV4nvCCch~c&5tCOJd40w`)?VZqT zk~F1=UdWt2ux8lxJzvuV(m0np8-RfJY3mFRbjP9ua@u49%i|EY=bnv;u^pCE=L6s3 zRlTn!gANPgI|#tm<;Rt{=;$@OzVJf|nq94F`+$VI-Qrx+6Enu!me|@fH+bA)pKpb} zivr|>2=j7d$lrHp1ab^a{8wt_mi7*aYwS}=wR{nYu<5ktDvv5u7^S3gUgrxPj z$xg7%#l4r39rK=n+DXqxQoJxa5_dovy6g6X&6i}MqY4S1K8A6yrtC36Qp7<&sEbCs z*lMCqnlOQnp=Z*G@Qp=EVOeDbP8l><0Ki#Ki=WO2n54o8doYsWCwo^=xrQ#p^WVSF z$B4Okv}iHEca48V3Mry6=7hDLX4``L?D4Cb@D6i^JQ|9PZSm~*jc;}jnEASR$m<95 zHbU98Cu!I{9p=$fe*+4x7cy_l;hM_KICqmj>qY3wIDNA4iDjn)#H17Vt9{0;%A2^B z$tTn^PpW{kHHm)czGGmkY5QEV`o~2lI8~$Uxsiz(h1m~tU=pocQ3exVQgr2LdOI`J zdJ8+Vf5>`{?|Ct>G8@WCGrFMuNO&Y;4a+>tYazX6nKUd7xR$0q9CQ0Z#96ygKdgzj z>2;E~G^YwJsqkKVd1$|0@}+PwIl(n|{FAX!(Ni}zV8%TYjaV>lA?9A~B%ZFRk(Yox z!QUOC_6K^p2Wb|!y*&65=g^Q24s%Sx7Nr@!9i|I1kg<)fSe(4qn zUx1;(sndY7KA7>ZbFO5ow1H>af-QZvo|Y7_M>t(SDs04;GaD*UOjsx-$t3u0BEV{>d_!6wZV zz**JoqgURm=p7=PJ{#1%nKWQgAHvv9EE+y|HXhaAI`o2ajf-%s4>EG0MBAgn zyJj7FkS&#*+k87|9!{gbb|y-obrKmjwD1EMd^(b~?x_UU{3G+ua2NE4`#lM`a*6L# z|M&By(OIwB;ScY-_Q%4}t33Px>ewbH4hWod+0dJt;~I`rdd(bnQGu=TeJXm`B;(xh zY?qIy2|Vs2Pkz$sSnRQ24z20+<$Xam}P&?H0xR_PJjxgtI5dW;>7E`TWn=`X2fAX5zcD zd<4zoLo#A{CVq1*-aZ(OwN|p;8ci@^WdmfETZmJe1Funm1nFMdPU#Y_S`9%lQlR&; zwAP^G?L){E$a&49gmvkrzfU8RmLx7*9Bcc*aDX3}0TRiAEwons%HsMmJ*f%9H8#N0|f#lyn794qw)S$uLNjj=E^f zx|!@XB1f1Z_F@L(D6>s6sFyq&7l{jKW7Ea#;d(1L5>$l|b~>q~3BX3`Z@jJ*R9FKo z`jbTH%8*}qu-gPiFaQi`69ynHNTS}Yc0d!9_lg_(``CAR5qf$@74PjSISah+p>yFq zI}Bd3qW3?*%(Ez%!_^$2@^;w{w{tiY1hEs@Qf+8N03EX~5^4Irkv}B%HY3@~A8(xR z`@Csmb@tlmbSEuNm`NsjyUA-4cD`ongFYmM2}XLO$lVG_M~z6T->J6@LB#FlpEzjo zc$(t7QI89un|LsF>)-zSNPtqOo7RI3+p$meMVygvhYxHpVSAh(2$1R|K)slbkopft zFGq-mMYSDMx&Vi5Td664F=He$KqcjYq)v3Ggji9Fg+jx2vGZsuv8MGb$b@$pfMSf| zv<7modO-5ZKAG{Vy;~Z#-K~QHtRNhNp2S7!M%gqTt9d4Bl?Cx5B|~)zB3{p^|1ent zohdhXZxh4Bt7BEnP40UF{$XcXgk2?HxvhHYMuZUSZ8Zw8XjG}(_e`DFigR|ho)Pl? zJ26xk_sX6OI6w~`x0G7kcm)Koy)d%XIvhyXZz@@>T>tH?1frsR7^#@I&ZxP8IH=|N zCet)`$y!_%C|mHD@1hXlyO*|sM28Bs&ly7O!x(Kw52<*bLO3Me+H^Uk^M?4xGvIO% zGg+yb?T+*o3EnXQu2~#X-s`Dll>Y+A^T`F}{sK6+;tLMe$QetBz;7P%Xca>MouQ2G ziL=?SkRq(IDTG*gu%+?vHTj1x1c6)V<~`#y+c~oE1wM6*90ajZaz=RQ?Q2^x$l(M~ zNw-wsv}Ruq)9R}XK7UsfOf46UDgmCIKW`!-s44Luj_c4jjzc&XVB6+`mZMxQMK;E6GsArTjTmw%5{)hou|q~Zt! zl1NJi*DnSQQ#%oGJwBE1HucH{={`(v3{Ke809a;s8`VnzU8hr12F{(LtD2_-Yu5#d z(<)i7YcF1`tK>IbAx;7cjAv5(k;}8#?90RkXsnJg@iR0`ers&v+ZkE2n-q}VD%)V( zJQuTq0A`Z$X%uH=iyS0s_sLo0Ra-5Gy-zC3Kxb8EKgJXZAd7M<7A(jZZ$3)+yyR*+ z+Tw?(_Wji^w^ViTSyl(5S4ioE5d7%vu-Vcn&*&Dh#DM|OC*n>zbBWE^H~dGYLHov6 zAg468;^~h(=#v@T4Gv?$)5~5ThdoE6GhUWl~}Bf0yK+k1{mQP$R_T%_YA~V z$VDVjjA@SxOk!zO*+kh$cj+XLI|GmE%Ef6377g`}P+4lsF0GJzAzZt#l5hu?Er z9MHBz*^1PGUDo>?R`eZ?D>ZcS7@1(w?(*03(}H{Az(LJ?jQ^mCDq;=b>c-ks#jP;n zcHi-~N&hr}Qb7c90p$aVgf}Mx#~u-im3sm(l8$--6c>O?09JC8f~V`Ndm!%bY!x)Z zr|CGQcR-Y_NI}rcY!W;c(%;~*W-uw*kp4bD+$D~;L!H9adyqyMDQqj&7&kTA4zXsQt$g@9>ft)aF~+X;busjdLut5RJ5dS37m zx$YB-@LB_1W*c{e11A>LL&E}tBE1&4j71@ei@R)4%j2}NK_O_kXvXSXIwV?RW)e2>-)?-fKhT4}_>aDH0f=Anpv@shlwd@1b5 zb{!u@kp}5&*Vqbt3d9hlv(dv z^-9v5cD90P$6+E^Q%K2iL3+m#_58mc$4lWT-pf)bvucT`XSczBzM~I&yRo!`V9$vM zm}(?JtK@Fs5M>Bx6>lP!g+IYbJ&KBGjTj}gH`S{U+b(e`M2BX?AFN50#u}9OdSX0t zB`=G`_~Dz2yE@`oKR>B%Ee@V&St=Ad?(DwrV&hQqAMiiJh&xaSk{t;iw; zb`4~gNe#j9c)yUJ8c}g~Q0fa7i#M1nH8^GP=6&%9CT;T2R&N8a0kr#m8YfhdCt`Bo zqgzt!czUCF$qw~uMjTNo43-KR1v4XQq=mwIXsCM^^0J*4t~H+B?A5j!183repO$Pb z6Yp3OikklUa1X}2;|5vyA8H$Qu^H&JG8(k?$b+OAIW?xpY5Vd>TAMvcx6>!9DooB% z5*!xM>VSW#{1pmK?n`Ow+goyI9?AD8(z3rGkOh;iWBED|IvkCs1cm|V)Su6TQqkTO zQu$~ncJ(t)ou0)0Ifgkq5|Ub9q~ZlVQE2*Nl33UV@)s@T3A-`A>pmVsYx7_yStLXm zu=K5jwV;Wi8IiWLU!>1a+&xqyHxxoyVvUw*ICl=4>K9c+zt&^OT0QGEK2y-6`CRu@ zW(_df&9Ye<=}Nz45<&}3phPavk4oKIjFinh(^w!Iekp8LlZDNA5{x*uMa(bWbHDno zLc!wx3XS0AU6KO$jK%JfC|LMwd!j4p97nzD-o>u2DPAK~Re&+jWluXOES*Lmc;swK zCV93J&3ui(EYI?KHFw@M51i`{hpFSd9e2@P1G(Nn8Jw6`Zpu26=qE1H#nd-J-^y!X z)AR-Fw$7An>{R(b@H356&%kul*F)Y#(*L4lv|B!a=m#@v=(MJOkEz)g{s zR%Ks8=kcW8d!yAEV^VUpFSuud{#s8e@C#uoCu4ekk^_!2q?zG4h?xh>-?G)kq=gD+ zDA$-v;_yP2rR3JmAM<6tKSpwpn!U`qn(A^cn^Nd-p@0%1PT(44B{8}ZQKGZ4nr(5$WUSi6Tkl~GS%J&G;@%ad(k%D+99Owh($ia!?J*cA^sCJ@0Uv6qo5 zawL45TOLT(cXW%2mF&d}v;T70@Jf;2u@?dypI#N46;Lj!H`MipEtA7(nCpX?5GWT~ zzG<3Ss{caM#%dhUE8@G!b7WUv2s0#WdZ92Oxjxkag#h2#m1alVUFEUM-wG-u8GCNq zOT*G{IL;Oe*Ua1X-}{TUn3!tsh_em`*#*jJ$B&!S?K^7+dxXdvD6eGHGpj&F;U^9| zGk!YSv5BMLH-OG1l~+yQAIvB6IvMLsQHCOahPsh55Br7mB^FCF^=BAv0k&)7{XX}2 z*4H)$uO=mbUMEJx#qUpce?;c4urs>vRpc93ibE&7DlMw<=D+}q>i z?;O`N9^+mMQUILeXBUjgbJ3(Csia5uqta!ktxP;lTt;|f4@H^Pdi)=pZ^}5E?H9;* za4dko#SlCIR&p!yRHoP6Wg=v|OB*FTs0o?@C+s#7B>n(?e5*A)Y6{(Y%@}~#zW_w@ z_u;_}Dm+C`Er)_}W<97SSOgbgWYZ^rr0jK{tDLbbkC{8ai2X{84%b~4!S}3Vs}|n< znAyYkyJf(7E(XP2%3O}B)j5BvAd|m*+ra{V^0|wB(~&*_)!VYs%Al-R3^*fh>0$E? zO*M02Kb@jy>p6Xd-<34eMgD?-!rh1oG_KoJrm;chTUWNNz5v1tw^g4di6$av34$Ro zVwc7Gi>I3nd$n|f1570>UZ%dYb7{VAQb&lJ)&o9bTV>j+O;%Ap;7fUK-Pf+qQ~D)@ zg*sC%rBAT{2RnG=UAAWFXNXoek{sTK5p7Ds$Q6In#eQt_A$RnR_{4|d@8(>*z7_AIBD)~vsTM^yyqeBJ`b{$W}c zsZA4uE3lQJ&xP1Emo!K8i0xM{12Eej_TMTS5Xcl?|I-kSboY~Hd4Y8#NU3bXT*D;eSQUkWm;(L+kuWJm z6Bipk{PI@SzqD{V!ms|-=+3`nQQie=D9?`(w~4(%VdSfZz|DFy{BVcHhdKmj7Dvn_ z&%&oLrx05f3T3_4c2;hWpEp}A&o{U$+M-~{2f%?hc4Eq(n(%(g@N`pVZQ7noLa>?Q zFb7j59tVlhT(ecn&LJ4ptkn5d_*HAYpcRbuFznm%0L!WUy*ibGwXAh9Cp0Zxj`A@J zr6cw}rbQ4}&FK>4!YR=QnSMq1bxyr!?!PcYLMD(_P805*Db8-%g`|Rk&$#nvxAN3G{ z1h#LS+A^m}+G$C{q0Y7c&R)jHlLrtHwOr^(qjZf0M)wE)tb4-|@eW?Af>Uhy$0*a|L%+OE3E`c7M0FXY$Uc+ih4mzSX^??2CH!Kf~GlimV?uU5X7=v)s+4Y?yu8fd3UkWgAw{|EqJ5r}Hg!A0 zVusFa__DeZfarONc^#tAgFK0)8Af&Yl=t#uw7nKe%?cRRJA$t{8b7gHp@Yj?s_i7$ zQ(y1)HOwA+r7O~m8SONgk+FT$kBLvVc#jkJ`q9#r|7t{)8V@L*Tq(V+7Fj)bf%TN5 zYG-`hI$YkyJw5RaJXY!s;KPdDTMC!0_Hlc6lrgGupXqE1hg5BwqiP_$sk^d=H~-=- z_xH0Yw{>gub_W`xC;;+nzA8O*l$I_Kw}==isG7VV9?7P|hCK#~E};+m>W?QM>)I+aqpu^4F&o4fn zKEYz`Aoo$Fu;6b*9YKntIfm+srhp)dD(GN zz3b*NSy{LW>Cy5R6ElI{~!KhNPL-)nkMnDDIw* zzJl|<@O|&ZqsV|*qT800eh+_@H4{v5?I862>pdo6YJ8j{vD;DVCJOmNHi~uIzL+89 zOOZovs+oiutAN)xo-(9_DS-D6doUoL_O6`|Y<9~m3Nt>t9mwnaCCaX7Hv5(mpn{pf z+HMA5sN_41+pRb;^DpB!&mdK*X~n%D*D*$iP62K>7RVrN`eGQv#l~G(QvlUxK|`+E9!`HVhW{ zH$B|g$mRsbzG&`UL1;TK!?~}JE%}?)-J$hd;zMcw_|^G=4u%u&Aq(!b+~J!Vg%TagOR}mter$SWn{=II^y@)G{u-;L;7Xzli4;hpFZ?^A7*OmE z^Rq1BDx6>6`$UP z_4e2_g@V6HFbGkw{jE|c{$2b1@2)ts|1h-s=S3g~n)bh--4Gcn!$cng62vu}eF@xG zMREQ|x>Y8sq^&r&VuOzaFg2hNwb^0^ABiAu_0u4GclA+G+iM`fEuBVI^dv}C*aov6S1UoO~fL|Ux6vh)toNP#kQsdz5A8kixt z)4_jlTXTtvqTs1pqXAFVMX=R+VzWGGyXiaWnS@|x%yT`qG%rBR;$q0?^;A5U5PADw zwg`Ewbhn%Pmi*OIf>-84vdRLB{L~^5=iW;d! zctbJXiDEz0DJQ*BbAt2u$*)Y1Z4;vn^<@*$a-E|`>_{w{Y1^UUQy=kf7htJD8FJ>B zyjW|qQ)fS~=gCHl1`K^2In{Kd7XNH_8Xa0r_dXTzzzW@1GFHHerDyybok@bbY>JDt z=}|FeTw3ttMUU%88Fyc=s%-7$X;lPHcr@>pY$VUW$AbnplG&@jZztuQ5>>(dC1~WY zSD_HL2W8C5p-Y}hBBEm~*ja~(paS=)X7QhNhu5bie12&NAYLE7Z+YzU@TBXtfL-(i z3cs1DuiVXmehy&kzLGpPqk~JJ>STP4Gm)ks;L&5773T%=73M^q@R{vI$<=lMlx9JY zME|Uf&tjKl&CP@OWxXl$0k~?%gIiqxGVDc=yeS2p?6`C>OA=(%4suOFBNyT8>gyf` za0~`LX98M@yMg_Yc+xB#%FV?LGdZCQg!(OcM0oy zqE>Pj`iUI}^DC<^-p!-1l>AH*Jl-R_S~8l*rZaCO_O0VxfL9m9m)o<}@YYzxsm%~k z=~2A3;NQecFno7uY~7d3E}8}V5?HnFCvAOO&d51j}>w zKj-Uzn0N#_2h4%47Db`(J=?cnkpT>IU`Zy$9}E?v&w6V7laqxwjCdE+sBJ2X7C?w4 zJuL$?57g?*!a?cVe!dRrB5;joBANql6L{+R*Y~ZQYI-K&WpiXl(4Fa^W9-|%;J0ya zr%OfeOJ`AsI58<&OiXsaYsLq#L{Bbn^Y;j4gRTKil)jtBch*{a4m>&VGlu+4#$|2H z1xdzmYhWgNY18}lj>y)7m(`-kPyW^jA`$bI3!&+^5N3q@Zfv(| zV^Qt(n3oxAYZ^@S0~J?vPJl6##otJ+pXNCe%UNB#n&)zbal&KmiP7P#t*-z3kuti< zyZe;U4(s-b8^uD(e9kPB$8ZQU?h)SF6W2!nOj$9;D?VKQ45Sj0SE#(a!N6?-2l^Qg8E6t`e zRvL%s)nIXB(#Mzy43uqLTFAfcGL06q0sNy{j)|`{pBJ{8S&I2uy*WLpAWmQGD80#8 z-;>XGg4G(E{hvSvy^0!jYk)iG95CxH^=suLbfudW)1XO-vIZI1x4z;no5&X^)I&m> z;~rpLuITz%L9c7{C>)|_Z?VYom40gSr&v=Su_EMHP#nsG7heP~FB!ArJr5ADF1McO zGN%u z30A~q4kj<$%Rm1@P5Udu_goMj9t{YM3{(>t)bj0NTKWU_*3a;%gklUtBZ}=4h#`XP z-uDJ~w14whf>DyNC91fKzklBZHge(tqVHd%{%^Ds%fILUU$oN`q%z6BcmE%>6VSo` zcuhdi{?Sgv4*v)J^pAG>WB0%5rx5@Es;U2zc0&ITqmV#u{~})dUlG^O*PV-!5Xm}D zmGgAJPi8UR;jQ)}S(SaZuG{`wV-D5Az0+UuT)cf)@;RPqO-<8Ak#XxL%~$p9IkCBiVM-HOgSp2CrW6jbFWYX+l#J-g%Qk&zV>mN&#n^stcOqde6*jv z4FU-5XGhNK_KzhodaFFx8UTs)C%_pZ58uW&F#I6ogWplB>NL~%NicUBKlq+1Y@vjL zMCq$zjMc_hK^K+>wR@H)WNrz&U~Ho=VIXfRz`#B(K#c~Qa?Y$}<~@pzkBgjc)Cw1N zOxI*QwhiwMYti0?vdZ$HnvAfg*D_U>HgR7Y+F!AIuhwYEK8&uJmM>fHwLE>~xpZpD zOBvG%wYI-e(724nN*s7yON-nHC*U&(2vJdu;o6@0K1eaU1CjWnWuZ}h^CBwM6J=ve zl&E%5`@uasND_2NfanP?9W6aep%1cOYNMB%W#SYy+op0cGu zDc_r8N*|Zr4fG+(Gdj=e*2Nb$42m@}_q=jv+b`cSeDUd5-|Co?fQ8sIRs%TxtonTX z(Exx&{ZG#yFIJJ0y>sg{D-An3rx_k~YG}C;R4l7U+e~ZOh+x{A=^bSi9B6e)aNEDU za#?@CF!8|q15u?uK3v%zhO`Q&Ih6uoR>1q^dtC{f6dpAsZhhXGG9jpZxG{K=VO5%p zb1s#|gNGC&_+W;pBDoWr8t2#Eib{WQ@V>y*F$26*$>3p^^s5Tti> zb<28ytr8PzT3Nmr1<#Wu>u_*QCdHCbUd`1DRSQ7*?SZH z9Z9(KJHP{ zeNK>K=R?BKNlh_OWVxL~>k0#t1pQ_);}HGzlHv;h&4@pHf$9=k=6~`DhGII-^GDP) zRAS=IQKx8v3EmKtUfFxn;V?WwqWu-jI#zr=;!Ip78i=6{YC5RwcnIpZG33=2#|L& zsX-1@Dt>12%{|7LiX?keP*TyC1!{2pI|C6R$bXmL5fvF5@^oK0i zWtk-0e5d#*93{H3yp5AM{M(qBC`2~`R08#Q4Jy|h@L`?=CU&7c1xL1!_zsY`d34Zt znf^@RUamuPrBu}9LYSOLktkukK!X4CpT&Q1)~wODA`$T$5gHsjvAmaQ5A&# z@20vZRJ5x|GZ1A(E;yHB>ruMa9LiYHSA2a_hs5u9);#M@1+y=RgQg>`>L;y`!2S|I zn$h;S@b^~i#t)`nYeid+S8{Qy-Is--DUr;*0XjARiV}K$MYR$2JP3xv#-X<2(gMQ7W&1U>j_yF~8 zHcpmV*HxNCvBL7-^%Rw@eRk^wQdItsiTx85GQZWHF9^B@` zUX2qxy(Y2Xzif4GsF)+lI)%Qx@Ek)at2C5PP`moRT@6p^S{dj^4R^%Os-9q1{A65| z*<3hB^#&R+t#O_!ozG=V9)8c?>PxKCXyVY?ukX(>^LnCWuB=0lF}p7hf8zo|BSlrK zh@Kz`r(Xgo6{kj(lAv_2TJhX(yqI0>N>DT-4(rM&bCBREE|n?{PCd%qPN!r2L`iGfpDUGx8FJ1%NO;p2Zb zDoWvkxip9O=a8WXB`$=hSi#8n_WW`zOGx5NhAQ^2(+)41W^pnKeI<+OtV?}Qhraoy ze`{>K6iukiASI*a{<}89$}csA+VR#MVz97+nB%}scR!DVUK6PK$WXaNU8}gPOG@tT ztO4G2yam6ks#Y);9RXMX{=!V<249oBw-&{g7(qD=L(keP0LGAam4v6oNfJn(J|P;n zij$I~9=v|>)16a49lXPzT#>_f_Iwt%!cz_^uV0dzLv%&SZvk!&EDa;7Lo}vw(V;WJ zXOhPmi#Ov8TPA8k``Gs==~~>FR9x{Lr51EBp*qq?B2~3miT$(^wo-*4&ORtit2D$6 zk!p;9b^G4iJDd7Xs(b7uI|dOLg4|ac9yLr8hOP?&JH{CfGvtj@X^(FWk`r3FH^y$l zeq^HYXYky++`mliW5lQHx8{QPp67R=ppex8DgE#GS7y2BK|_W$f%1i~lMbEUgDZ-5 zRE+Dw;ZGB|M!3@ju*1^td%)*gg8w1^IEfgPq1 z^{|MB1)>GdPo|A>2GZeW73i-Yjrcgk+I~Kbq7ZrXU>gGr4B;glG7mtL{`uJM60@z2 zaTfcl56-&AA$T7|klG%c?s-Z*KgKtTHpvAB<*51Qv`Q`I1-t2(b32aZ`;4qh(EQYX z6vLspgT%iEZn4Z7IUrwWT<8K?Xjh&+=Q0Kb@Qj!OSxjHq*%Q;MYn(wY8eSe_FXC5- z!*=teHwQaZdx{I{hH>NIZ|9cR3@u&@?bWZKof3H~kI_kS+Gi0}pBpR;w};Lj95OW1 zlbxMESz=v|^xBYjQj5L;DS<;y1JK_|HfCu#>FDm~+SO0zfBgVmNLbbs;7MD#3H}#t z=MXFk5T)m9+qP}IYn$)dwr$(CZQHhO+cxqhnWScsRAn=}uC895>S}!F``a4(Yvlme zr)LM@-hVmnsJ|KHL9KlQX*P%&kPie+LRg#rxt59UZR!c0PjZs`bWh{{Y1nSLHNIEL zYG=xKpVcxRRd4HOr)=Qe7C(g&NUnki#E2Pue|^_o;>c6#6S5y9On8{G#&Ez!FmlexkDL z%{4koyV&7MtA~ki+PK?kdmxSZ9k>Pwm%(KVcpbNE{RIZL0${5QlZOrI>Kw zcU1UIz&u6)+3U;d(-&az$vQLhKu@9Y6L!s7y+(R%kL*A}yf(LB5X79<(A>cWYliCK za;#j#uH57xs}r}+`!q_$PY(x!*ktaepW{wiBQ5geh_2-L z7eO?zQ&_tR(6B$;Yo^lyQ<4V=)klzJo7qe6cq8;)mtZT64~UIzD?;t!z2i%r!UlGr z*GwfCxrxUkKmtjl45z+g#za83jE{L@6R}EW>ni#mP&qi1tx7 z2ek0uOOV}_+YSX8vwhOY&t@FN7XP7U!51f@Kz1;sno;-9t<+3KXTEdd67FXV*!LH3 z$BLiV+}EiC#pk>@@Pc~hHS?rllxed~?Zb0rj{0W>yg*&`Cgb%H1jf1hKd)>EvA!4U)>L{d?DUP(rZj%jiNG)SY~kmIu0#^HoNhCkreD-oVnDDkYfV?VWPs5$d!5_m ze|F?+2{?%RsxwG9K5i>NIwImJWv3S%?|d9bQDLBK|G2ilPtybGo(iX8K~YG-5Nu&4 zijIc501tTaBkjTv>J@pn`>1N3(h_V;f_L|{YcHd>JwhaJ2vWta!poX{_ zdZdU4%2~BM@j~y+D%RG!f~kNZBJrjh)s_^jtl~dbvI2LWEt1j?Wznv$cq*4p5I@bZM=VzuK95^WWP>WfI(U$*AyfCvliD<8~Jdszqf+M!F3%( zYY<{rO&*cu7!hv@8t_yslQgR-T{fJ^PsiMPd~YWnG_} z<`FyxoGB%fwyp(3QMdE^*}(xw2>6UIWrEj@wj4rs;7ar&K>nr$s$c3=wO-(z`I5G0 zA}`J6;gxGcCDj)MR&il#w+fzJb=4aFgfsTS?5j76_3Ovr2#G(@EB#%RdsB<8oCt(6 z%j~@A(zxB-nd%kTVe!AwakotU5R{yiaW(ML{hczBu(#c*`npMU$hquF@4p9c8!J4Y zqx;YVKqu)~Aw<>}@+7y)UTWPK)-}7SnjMn*$v4nWo9V0jW@dzn2B#9S^Uz#YR7L_+ zWP!|r&8b(L{(z6r^;jj*f-6MvR~uaQ+eF*2-Z87Rh?d9K|5XnE3)gU*A{{&9+mI1> z4N!#6K6XDbR@RmIO(iuXo!i?ynEH8?DRAr~Z$9;kOXk%@m2=ff|2r$KnU*zRfOvb22J{m{wW0m_%-?kUL zueh^P#=e)zmVNcXW_L`asd(%wf<_EWG~P~=3f;f6kxa-})r{?z5BObzqVW667CfvE zVnaK<-8sVy+tdpBu2TkzGqG{b14#yr2M_1!;G<_N0=4vXOqFEVC z#_dEhOFtpgVV=*x-LVnJ_kpTevXoS@t&QmJDya1cO;2kfRvtD8Z9b>=D*$O>oNeL+ zN={e}&oSjFg*mI8QicPj=gfg^z9tQ&s=Ah@BT0jutld_z$>_Ri5P5b_a-M!YMjLHQ ze+h(MX)1m4Vxcufe!9AMX94@*%sWPDDLy46m_e@xgV0&WgVi{vH_$$h2g`?Lx$ms2 zXcILe-i*o&nNP8Kz~}}v(yQ0wEF%2Db4LGEzhNkUx~a0^1lB~5EU=e$FKJ;D{#s`S zOQt5Q(vW9@v zK9I8#%~9KO2Y>=EN69ZWbwW~Nkj(6?oqho=p^}Qlwil|d_TSq}h#W-+gB<(1Oau?9 z$%+?OfRNm9glq2`;Cc&mea8^0-El*0dpUuHKzn)&iY`Yl5QTt+(C44k0#nLy5xw%d zKkRF$%fa*6x)`Zgq{s}421A$*1{916{%uk#q<WMYhHYTP)+58CMNgRy79Qu*kVJ=QstkT>*#hCF-p29`R}CwcHg$Y9n}< zy*(*$PNyJFZ*5{J6BiTMlk--bzKgXF2{m#~B$}NmE2R^U-ax3`)dpIj%#WrJ>#;hc zI04m4Y{QGYzjU);m@7l;uyXg=oONusoHwTEPd(atj7cotRxwK+o202w+# z1Mm+uN3#nh?L0Tb_5C*6l+!RrUdXsIyZJ#TGp6FprJ;v$<#3V}7zP>`MJ1}pgQyAc zLBbYt@+Lgh45_m8*LPhWp(zz8wv~pubTxYzjKNmfb{?{B!!_kdOHP~iDnK_I;qfmF zGQ1`8`sxEN>3<0bCg?QeJbAHB`+ z`&raYA{?Z9VX!2g=E-oh@?NV@`2&o@Q?L%!jRh_as=T3&=F>#-w2%4|H`%c z;H{j)wUnFKD*AJy=4$$UQ0jqT=!TMfDMPn%@~8DMa*q7K>-76) zoqT%a3E5!4-6tepH5yb#Ev+&V3TK@8{cx+VXrDZ2vb@{-KafsGTb8V2_{lZDxZX9F z9*3I~kEi{GGj~bN9^GHpMHrv8u&c^T4NcKTmV~BcR3t&6b$io2ykb5O`{j?419faU z6ggyih0GCKA?4;=dJxwHFllMb5VD`wNYwjPjA`-)Uxj>|pHgNo>raaz*M0-1w`jpQyBvav#c&?c`pP3= zUo-!i5yiE9y{XUVsi_}8G>T{z0sR2XcSDs}Md2E~`8TAnvJ~V6$9Pd!MrV5okqV3r zEeviExjcRZBMy(U8cy66Nco5Q&EgJLEC3-Gb3UfygjI3*ob6w5Ew)h=WNhp>3*JK9 zQk>Ws4}N-?czTV`8EaRB_2`D-;CudHg!UWnk8_tIQ;xGorTtAoAlei#KbPuRf+Igu z>I^n`8d#TTm$3rQaknU+8&J>e1M?w9U>;krQ%(Vdukr7*EZK`XX6&b&@UzkS@f0Iv zbmoOKJ$!Do7+4=WY19XR#}1x-4@-wp9=$A`*a&&t&uK4&{I^m+&A64vYsBPku)=y- zO5<^)g#>)j=89 zm}6lQZ9;X<(FH>F7rsc@S{Wf<614d;2+?QIPGQ+OD^QWj3~?u7cyy(!Ul=l5bosYd zrZ5omoYK&u(J|sPet`8H=cXrqGb!_Tu{X}*cvkfma!oimX@x%VcR1euL0RcgCi)<) zSsM~uvp;>i>ZDbD)GYBVt~bb-#-I>4IArU^JNaiZf`KFyFBn=Cn}T1DJIja za5u2MIVs#^oPS^|nw~WQ80F~gPs6>ul9TgaN&WV%y5`&8F81zSMN^&{U>=(Fd+g6H zMqV1(FQ~f9=Z{syU3i=*>h-hasY%UMxdQII@K2%;6yQ~0$itgl{;qGizn_|M$2n+G zQxs75LsiV|Sy)4?X17Y7&sa|KM`@>GX zGnbvav3(^Zx=5%`(M|UC9rW5)=)78xt(_XpGG-Qc5p%#GY+02a%TpBQj}TQ?zS!_s zyPT#<)N8+mh!Qq`ZQHSA@};W*nUIwNY?auSqh0HdJGqkpfkbt1ULFN^efy%jpmlyh zu}e>F@PsM3&l9lQf#P_OY5w~e5dYQFW_lLub&l<;E+8F~5yTJkpa6BCWrcX5Hx!f{!}&pV}WM5RFP8i_q; zyjqRpBd%`w{DRf&&4KH>*bmbkx^6+#uiZ`f6civ^2f7hWWnc+;`RFb>%Ox|lt_C4Ruso&D*Signv<+@Aawu@hqfgtxqlKg z2!xkNKm13eFMf*7ZEa zdcn_cR89gJ?z2Q<^~NXcqCjSQ@A?$)zhKn6-)6okFY}_V1ukQ@$8ML-d`bwmz}-~Z z8{&=^L?JFBqFn4SJ$=l6zGlRXr!p9aIm!avc7}H++VLjALY-+Ba{Ii5?Cr#uU49!* zt{ytKC0u6`3c`qI;u-El?4RIwfqKAWU>uM*K&6I8SLO;R_#ZKEtqRAdW!+A`Ib@}e zKfOMM4E$B!O=Tvv2PpyAs%Br*XHXTuF z+zv~hSk1V-dN=okQ+PXGb&;DZjS2sz#DkR;x<{y#x2B@*?Er_GNNp z4by}w1gidh3lF29zTYRwI_;p$;qg)lWCgH>nU;j09hr{#p?>Kt5!Ia>1cTs(V~%&v z$-C%MG@dJvp=J~>qCzvhs3!)?dua8~+VQBN3`__M1n;IL zlnUnGnKQNCh?K7&TStfwFfwtDk6v5!sYMJY73qA4g4Sm;KvDJMULz?AxMf-C`U|Yo z7ki_NOt9e2Ez0KW5*;aW!wo^F6u?xIX9T>RGyIqEdSXH8Vxr@%G>11k6yuj{$atql z;IYCYmdjg%r7tyMM|}jpJzq38b(iIj4D$SC*KhkoOl^xC7SVL!eAA^XCVN* zeJ17*UoF6A!#{N)9iHoFApxh8F|tOYXKs*)+)aMg z^wMQ;gsj7d8o2CUeXIr}9fj%{lsE}=va-=vv-G_>{qJg6@Hk*VQaXv(Yp?E6azz`x z4}H5Q)_cj&fsB$Cazq;N`RtBl zsDFyn?jL5Nr-?bCj+v4q6S7Xr1l0V%Xr;}HLIHe;j1Z4Thv-JVHqLk>LhAjq z(oWM$>y$>eqsd(E`N~*H7*maQ-@>duRvKn)b9hEY_MZlA z>umd4q`F>SNxcXrM`hlcl9*0BjvHCN!KBAj6Y=@C5YY>LN+8qa>Om3_*1>8I9vtIg zcCUxHZ)Uw8QDd6$bgWmry$YI4^0v77ANOk9%;0Wkqi-5qa^8QC zdhBLL{@A-r$gSuhf7dtfM`w?DDdQthI`i-0DS-j6PY>dSzpa* zF3%ZN#*cZ#nK-CzRlt)`9`smzcSp5+`H9k4lWP%B{4rKPwq(ui`Dq+CZQ^R7(u4U6 zW7b)Ag8*DVhAlW!)c+uPF6W2H=Zp9+FTwV(PxXEe7lzVp!lzhjaxnnfAJZvtrbSkA z?&rTW7iV#5b#*nsiRMC#Qly=X3tCvFdCgpvf zA;n0+(zar&K(Iav^Fu8tBg6wEzUc+PHpL;7SVD&Ts=+^$VhJdsoGFnd?%v3*b1pfGD<2A3t}0Xp|GwFUXTu8r_xZ^9 zj~EwCeHb@qg{RD~bfhmKF99_v9Y`+XwqNNb?m^N(au03weW3+u*y`T*JS5-Gzt{=q z^8aXUpbX&#z4GqvRZ%z8U|sGQV9sraP+O}dZNEo}P)wx#vcEyFD#O;0;(BdjTCF_3 z3>-oyDH?vp1MenSNu!NQ=F|R+Xa_N@1>YU8c_g&BBCD9wE8{6~>T`l&+EEW}WE3;^ zQ?9qZ2VJe$6vnGoK5Yi_4>w>-k`M1M_2t~nBV32l=735CR`7`U(L@L4BhhKvP56fp zvP{5%(7=y79R+)jpd{0zVc7g~j&=-mi-F0Is!oh}h{~66F*qhczSyL^I+IP6uL8#@ z>crXfo=BU8d#*#;9j}6DNh)xC&4&~+s^K?;irsN5MJ)>)5q-je;9R{hHL1|z4PkO z6b%H&5^RfDlo!R~twc6o2F@QiQ^dB1V-J{Oe;$)|9Bp`i5K9%EpUIr#ccsFq(dv)^ zNC?t9Nu*Tat6IRUt#~NPz#>&9+o7T8$V^q@!tQD}QY3XM`iktJ0K0fYAHe{teP%x_ z7OZt#6cw{>TV`tKj}de4Qwy|KL+`UeQXeu)@x9U%o(K3A1VyKU?CAA7(m+D8m#CQO zD84EFQ%z6GR=@_<=N*E)@8){Moy^D#f+_VxVaelvDk4U9?}6!XF&?M=taJl$cmrp- z^(oelF9%nYuh)D_8ggf>3B+d^AqqNZP?#-mI-;;{PpY}#&f9=bk&3gO2gaixZ}_3U z39(-?LGcHSbwn37@~z~j{uy?Q&^Ew5fNkq<6&&_3V#T%!4B!p^(MZ8dIAiEY7X5NC zZAY+0PqICki)e$kF1U;sYOg@LcE-zDJy-kR#1ZCycUuCOKn^UED5Qb#uW`8-gGeZ- z`B%eTvDm(zggeF+0}5ya-sb8?34d%)$w&YrJvX-*8LY9X(lqE0r?_}jCwf2kujarj zDHfI3kDK?eb-wk(LsZ$|c3AstfaCaFzSP9w`(wJ8*1Ua-K}qmzBuS>vE5w(Kf8Bk7 z0rpL0ACHwV`73pfwa@EQ^dC5&{UkP@n|B*g29>i3G&bdX>OqU?xMJmUS5cjBZUu&E z`Ec!w<|H3&zZp{MYv@u=qll2 z1Q?>81Iy!Vb2^`|jqF%abqI2J*P>hQ46x0dKd?LQ4n2NWmogfxyD7dA?troSe=H(? z0pneZv=uWkxryyNMJFmOcTA4e#@$T5N%P_9{-*kZG#8+=*64o_eblUjn)4^=zuuy# zwE$Wxzlu$*m^nH`_;6G2F_Gg`>M$fk5}Fhw75urELe<<6^;F)UW`L#FY?ARZ&&bIP zi$pVyvRDvwlq&NL9n@oUwybezb|AWTfUW9WW}WY_uv4#3pXzz3yvuGng;GKczIptH zQ$YR&F2Udj>eVEc)D%i@ed_~`xq)Ln^!1>A;bkh|l@|@#D`*Z-9Te$Wlf9^z;+e)!mmNBdpnV&oJQM&>l>)I05^0;jU*o# z8Q*>TCX7^^s*sTjpYaSG3HMV3o1t5B$&t!`*;-2U-pcmavqNm`z4a3|Lp=!q$rx~L zfVZ$}84x2Jc!yl`@uU3_OYiUD8AhioNzzc^h@oLYwYn(5)K|TokJal)(vbNnwRzE9 zP=d0XzV;m(#>VFQY-_x*R*K$f2jPKw?GI@k)_k}8HRa1$e;uF?1N9|yA*}Sl$#!rrLk_wGbUqa0U!AQq0KctUsD(>b zb+a-+7bNX}oXZkpKRLwBYQ!VeSDid^(Qy!~^Z4EM-LtpMoqNSKJ~eQ08bK;Hg9Rq{#DC8&=Z91Xmp-CKl}x_>X$k zPQS}b(a0|&A>NTjXi``1+24$H7SFpEAZ&&4iT7RztVzF`y0+#V2raU{Yl)*!Ut#%@ z(|8j?xAQEFipp#OOwQObNS0CJx_@)=Qqy`{v!i!5Yft5a>Nj|CkGeYLDht%%)V0*P zTPrtj6xpwIbOcGnNK+5)4jSSIb`FSnLF@Dv2e912l>j}M>}xwQ&deTSA@~ZsVT9t) zPR{;9(;Go}-E(XeOb@K!?WIay8BAitz5D&!Z~kvgG26fS|39V}GehRz+W!|*%>927 z68}F|NjcGR8Lm0EEZ?l_|#jZ*Cg^Tma00trx`+&n1cTKfWLE zE8*%f1C$AgP@VZxO4>^?S8MfM!v8Jx>kDi#cbEZXN?qSj(tw`Ppa!xL-1 zy@0$0H*^vkJZEYyF8f#8@BlnTJBpynep^4ttCzDog33LOq}Bn9f8Ru)!;{KJZ_USf ze#0j{Fb@$`pT2Vq7B6!V8!@a739cvc4y5~1Qq5g8duEG>GO!-P`M3Bsl%gw5w@v)p zKsh69&?>!o_gR!`LFnr~-6f%4ahCrme2j#lMaQbT&qT|5UO0p>00yXE+>+_bUZB#b z8Yy(PbqE_^1}BkTP-1%5zruSiE^sbn`K*$fNpRqq9unFvY`Vw2U|EfuiXlK51jO1< zoD6mg-21ZS#%3Quz!C!1ACzVSGatvI25KV@pIu~huvGC8u8RFjC&ooFrNQrD~Ak={xY*~#iAeXzlFaKq8A z5lKl(Gm*oToJ!p^qG96=rLX!wYWaWCx>*0sJUZa^zuIa1 zkJ_Pj<(sQ7sKiSYDbD#rZ8KmD0kM8c*fAMNu!h@(9byjLGK>v6R7refLJIU%{kE!3 zJPblZ|5k|J)|dzy-_q}Mo7*px;0Vphb66SM{GneC8PFLG_1NU1rkN-Q%j3hDbiHv@ zS8#q3!Q-HaFFE6kKW`XRWl&KcL_8LqO%Hpa>@Ban)*TlL1x>`UGy|k3XUl~MmG!;% zmqc6VGUj=)E*sFyfH`%V_ClKy9|ua=0*+|rhw8-_*Qb(jN#47Pg^Yt9wTZ15mgy*QL-K; zaRXUnHe| zlRs1NZw+h+xs(XSc8pseQ4T)YBjX-brx|8!3g%9(%v?jop1cN}+e0Pum)-2Y`-15^ zpt~-Wb_^`5KvN(t)^UhneVq^x$4m*@MqJ(Z9Y%jB0BPS0tpxmK>htWq7-~~Q(V9pF zx46vD|0$#YX6PH|P9Oue?SDbv9B>C0|IYK159+I({Vdgp(agT<;+fivyN0YGe}Y#@ zjGCldg;4f5`?&7sW9sTk3Y+9`+;u82;o9RdCKC;IFu*Csc^QCrB z8Y<+8H$W4LwBxE6bVB?+^>o)HGKU|D)4E&6Qa8*2vpHef>V-FtgV}LZ`jK3rL*9#j z>kQf|V`dPY6=ew8q&3123*pfi$w0@snBq$xiPu&=s?m8;l=eH_kY9VxVu8B0I9T^$ zzG{tiTzji(!1G_1uX1)IbkX#aw}GK*l?e4zXs-80dYmmZ_tVL&9MR$(w9=rrZ!i3r zkYy&<$bG~GniIdbLnaWTXTxQpym2}T@f&MsG&8`Ch7sW{hpA?>-_nbS2pitYkftMsiG72j#e8kOkU1z%3IKD+W3= z@4Gzlx_L6Z1er`tzp|c(%7Y6^W2CCbgZ<96pYZq6BS*4Sin?w4XV8!zdZLFGl1dvs zn5P(U-Pms=t_Go%qm855kG!=wy@n?VeQeEMviy5`yd4J2uS~${O#K70`^p?3x|1Ju zy4M1?G@iwVWn)QKBQ!V(Kq(gHp-h|CU0(gg{mnpB8F%)5v0)-NQ8iQ&&E!V(>+o;gx!PS`vQ{VZ40yKIPJVZli28Ox^fbt!^^z#f~aOx zS9S`i?e1Dj>Pg~(OKDzhSDu;LihtIc+gw-pC7?xOS@O<7D@lxZTVeD2QzjPll__D> zPDs&~pB&+MX8^@b&9kH5jatkfbScI61WvG~Wo*wOooKVETl{4k7>jQ*PV1`r1*s1L zGb~zapDSK1m01L#-3QPkM!LE1{Lv!Re!myKN4wz1D|fQ zry4eU%sgdB?j9xeKFOt>VwG*#2(PfIh1pU4O3fLe9F@d9Ne%y{a>)YDl|Jnc+hrqn zmmTBek!5V1OJ~sVwem!WZa70IR&n~4$;l2*p*SJPMIHuO19!Y}DBJn@l{5i_FJ1N{ zeY2>m0Cl)VGzyD`HoW8ToE8d)&zoI2p}m|C{+3&A9gp`XiKje}xg?u@xhk}7|E4Bk zWF`m~Mt;h&27(0T?ywKA2@$EkTV)>0fNxFVQmmG!zcw4lle)`{DHZIS0 z8l17T0zEB_1S&x(l>U}v`snURSzIcIlvXP<>{Q=HpJ|-!Yq!iFXwSE%d;w~ZHE8~| zpZIzKd&DUns?1Vpc2*O19x+|G%VInX>cu=V>P%o)x2bnD7lT;rA5kwT*=vY!4ps-L zeUrw3Uy{+&cTrcE-#K~s>N~CU84a%ZM~S7eJ8fs5mVNix1zRH>t#UTee7rrTigZ-9Fs4Oy?;*;<7MnG!r?jvP*TY?HD9c_uM5)D0-a1M^VxePQ8J{}I}l!qy%K^bS=bGT=*OB_DH1C<#zv9CC0L?9RgpB~D-h0c?d z&Aoot=D97e z6cxeXPG9xTB(U|V?o@)mmq67n@vW&a>Uoe;)15s{XOo9ZH7Z-=Y8^Tx(;I-qwCR zc;j;QXa&Gu%j{R+%2wm*$vB+D=I7fzGGcQ}a20);dk!c_Y-UsIar1fu$h;=-jO0Rp zy}=WFA~zXp%3n2d*o|5vAmyG$Z_gS9dX7ERCpL>d91;+b3?;mxjjmJNPjWM{O?hs( z%y?8jgcd)0#m?0V3Vd&wk7oK~&v7@_4?Z4<#z2KODxp|oAKp?Kx^pMa+T`QkzCHR0`UbcTCKu(%Vs=TjPmZ(ljNma*$g|>kD7fEy73dif9bzQU4dh9N~BkE4SDAI)Ny)6?SWoXiki6CKz z|179!*TE=E-cP;#r25P%d7m8dX}8b7s|yS9=FQtw@0VL_5h5qPn8ATR33;jjCOMDIw13cXiQAh;j7Dp+X1l+Jgcl`118@)mY4toX$Pv$N>XkyX%o zU33hyz?_Q&Q9ukDWnu}5PL;6AzoH1i_daDssHpdEV8u~H&Pw#uP{cY!-g?5dNfg;J z8)A2V6dkmaiAQZJ{gzdr(7B5tzATMgJ0RIjyK&WHT01#ZCtmp0S469*LzYloU@;PU z-0Rtutp#j&{Z{Vw8v^T1pfvcoQh-04nub;tz*(Y;cHYDjMf)+U{mO9hn!QJgGZ%JO z?Fr?J0~mz2^;UAKvuR=z zZ0rya?FnR!NdZKVe)2qpzgtMC(`BSxbXXA3jAVJf*3niX(0lAVa1(>`(9R4SiJCcu zI+&&)=hPzEqplIz5q5COSQ#E8oEk9+pxVr*_ks#g2ZRq6Izum#f1kGy0SGOfBl=J( z&V14m;HXh>+N@4wbaMeY!;bFmOBpEtc*XPN(Wagfs1|uRr#t(jD#P#w&{HZm6i_n& zav^Ks(9LKysv^cYE-oi-T`=2E!9ANXwOsb4$}t=KbnoU>e@|#B<3Jb}Kw^0iilld~ zAk%!J<4Bk|v7vzb+=#o(D)72r`^qnJK?z$?3%rzoOR}8Nf7ZmXl8CUO3r9{u&24^l zb)?%2|8p#IX{m3eYe()&FU3=dV_0=gf?%@&Mu{Eq%T%DFazmm%u7Dk9j&6O5ifOJG0vKSAFbqoVSy=tkJGWRw6ePz=@D`b7cjGU4^0D+4v^pvjjljEdxDB_z-VQxW>Kul1t zr>~$?mShv@IS)^0%wEY4&~nqQ+rMGAuCkQqenA<)$&(euh3GOT0JP}oFj)gOaxKY{ zzM#iZbSUcKcH3o9vAxZh7Haf;ViPi>P*rDs&r`PU#SwDBzmjS1n~}X@MN&ItG!L7; zF_a_ZXi^Wl|2c=}Sjr#Y_$!wSJhI_Dd&4OPUYk=8#JYQ*fIxR|r-aL7MHxOauH@Nn$&#H%=sa5BhX(TWUq=7!y<*~8>7KS?Tdu)B8CpQ=A5 z(Oj2dmix(%Zd5gLr#NPRnP+`xcz4GU<@lxQkB?+VPFsGgg87mDd@Z;^^pi!5As*Cy zga|ekNeRuCG~5sBVOf=C3^Zpm5(^;;aa?K9{I7`!3z zKaRfh)T4N?ob57TY^`j`?H(SUmdMxPImHiN{=NrBsZ0ixMDBU#s2X!5%7~Xd_<>)2 z4u;`sbFG!l3~!wzE+D*Z@A^LW)efjyHeo5kRz^GI#=Pn5o!{1=UXb? zsxW#3l=YwbX&hpMS{zZp&LxIc(TkZXLe|5i&zUUuZ-)0>jB3=c)$ZX( zBr6X#W^n&z$*!4e3YVRTz>B1bdn8hQ2drKeYaIjpzW9Oe!b;qZhs8Q9?3=s*_HL3m zqAJ8tLFgq=Wz$4;hoV*zZrI}!6ng8Vs5WKwjC&^mJuP;2c3Z=I`S=iqDPq422x#|C zzK;ukDUErsWn~=x7EN+r;ZnC)&Evbx?gQ_0kb7blpXI;A7T}{K>;=y$D{@|<(e2H% zXhhQ^$ba+}A{x^jQ=hIn$?$=XZ989foK>gYmIqVjDCkZM=pwX{tH`HVdFN>vlobV71o~~-Q}^O*)@42c z#xH5%upq(_X6Jo}4S_*iB%P4{#2g!~?xx*OV9ryq+qFPIqg1cP3N5E+MOO zsQ0zE<{1vyCDSUyhWUfGY+v35pS!qs5P-Im5&*YScR|PLBm6i+0GtNa*Xf*s)!M$2 zIZ9XzE_^X6KbT`d0pWzspJf}#=DkG^Dc@_;+al@i6CiPk(jV*@+hOnJgN=bo$kc)n zGate!Fktkew78S2V|5d}Ec5ttgTTBHAQR*SNDM9R)J*6Y(rSH`5QL%;|4O5RKK)aX zcK*ttTeFZzT=RApUQ5^ks#KtTGR8(3u z8zFY~!kJP}trO$3+cK!l>sA&U;|OHJ?x z^Xm~Qe^V(@uU1*bK;hcrd@2zevV3mL1(rfn-9!>YM}dt{$oPDBb)fQvE0o^U^yn%U+|=LO-zW6cdSk=sWIM!S>U3G z@fw(j$zM9uI*{VM-#l4^i?it=iGe(5SqI?(HW{cJxPO=a9-Lce|IGMm)Vk6a)Aqf; zNUm#@)oBd10N(osXiOOGZ5*w&w6W^7HTdnu$dwql%RqvTFz4qu;^u&rK$ zlWHr5)P12*4u>|`iJ>j8$^wdbOz)ol3EBgpQnq@Y&27dX=0%6+E-?qA7tfi8Y6~c^nH=_&wW!S{5z;`K=tB>Fb>Y zU5|T`P;~MXI;p(TKM!8yn(Qj&e!FpB3*E=~guq)q<=y7lm@F8R&Mp7#0o1 zB!ltc0~&~o=HuX@^pLT0sd~>5SXC=Tv@wdxexz2$E_lb>Rr=;~P_6fCG@qmS@rz@% z``7R;f4Kr8TW02iMR(@rvuRIrlqss=$9d5AI{{-ya8dOQFuWNU8 z)vKjJQe~-TSx5Uq=9w93xcF3>o>i-HX0%V=NJzxZYEUU&W(oiXgUi)46Kim|0xu}; z)%NPmLc7C@qn@44e9+N6&+zjiSg&SMSZ8p=abY13Ap;gE4!0JH3r~h4OjNq^KX-|$ z2|@?V)1*nacc9m%V135XnkR(R>~xc>@;e52gG(Jrjm8NWEH=%MT661AbQVysRL}5H+ zDqeomN+pU5>OKg5C)~9ephi}tLb2oB>ZzAGGcRGgo6WI}R zHkdggN|0__3|bnJfA*(NiYlD)Yy$S*_bchF08D1j8(X?+@~!HoOne)Hkp(Q91&^!z3`-9N|=YFAfR4GT@eq zd{SEI=~3Vm^51XoWaA7>kXmlGkPR3R4{xGb0KRP;TV^(U7it`QKm9pRX7KH~p*#d5 zWx_zW{(BCwSo5trmOwZ^n(Zcf5uLBwaq5)~wAYHi=aHmAkDYn#<1YJ642~PbfA_5h z7wjx#iJYb;Kv%wabpH;ZmRkv)CiBTY^eU6`1mAkIs~Wfo+_`E0)t^QA`qEU05gM-j zGzM5IGDGmlJA6vovW};On0yH!*0g`!cGWCr|Fg4`8g?w2+uj(odHbdha4A1babB8) zl1W|_;Sk50(v#_agi?_>GTIR9+LXk_@5aYPC>`Zy=^|RCCA}6?9~(s=u~k<1dV-|m z@Xo!`YRUD&H8}G8%MV{*MsfCJ+3YJ2b`>eW3@a##rsfv$c8CyD+oL&572z)=KUrSj zS%@;zK*Gr>HD6s(QtVMSEBN8EY@^cLzeZEk$^7ssN!hN6VXZKGZD8?^`llJ&vTD2n zWzG@P>MQZ9WSMrhYR|5WY8#A(ar61W4`0PJ2x(kM!CE6bqaqz4?)~%5DMc2AEMQa^ zKBtj&mJKIPp}!UHtMlxIQtl->!I28JOaK@P-*D}lL-F%oAqkaD8ZMUy1?Lkn7Tn2o zNZ(zexvDoJ0LCO~jaH?J$#I>>Y#PL+gbO0AmPt*$W2YHFtq5J5l=UnC&ZTdgNR=#K zdP@V8&lWXU@5opNfb1opeEr&;j78i$7`+N}^U&50Y2~|WDb-64&%1J!XQr;2FD^AxaAIfOoTQ8(NPCR>33`-_4Z5$R)T+X zWksirI6L|>Bp5eT7m+xdIlk>FDLP0;(OZmVOhRrf+OGNSKy9MS6}#_2yeid(N1;hh z)zfX&S~o@d!0F>`t)X`=N;z|CaRz?|nB6_NvXr~E>-ZNhVZY75GT$(|+{Fju!e(BW zE8ykPmlFN9P$ocabU$UIU$m|ulf-<9C4Fsp?JH8ZRZwMptC1c1>rF`9?ZKz8-UGMb zYS}{8M;5^RTHuk{fN8nru8nBiVZygpSM(GZkLb5m^QE&$^1iumZcLIi|O?Zp?@8kzdZ* zi60Keus9d;=imU5^ zu~adVh%Wc52ew>(v4QY|Ha5TH?WX1t7RlSBAta9lQ45=17CM#b4k0V+^&{t*mv&`j zF+uH<4=oD!v$=U0O|{C%ej8CV)?pCQ%U%9i>Jjn-xccDQPsY?X?ArmPB?t2VbBkfb zitqoEvnnMDb_1^x>aqWz^SN4r#h>U#t8Z>$HDoZJV;Y%{p5&eajCS4mEyUQYSX6!1 z>zs3N>fh$Vulqc#KDrVR^}3H$QUp^0j zPw`SqrmFADqYRd#`%?n^9K)wB;D_H2enJUaqdX3ZQ|`x9nza7x9QgZc{kqyy+~ueh zh{zz!Sc_<`17gXVHm&Hmjf%sS=5rjlqu+;{KbIUn!pD4_sKiR+ zeWeCee~-L^1^q*}JdnuBjx|&VKy~p}T~+Yi_M)Z|{_%2M3cI!_m(t6FeK?YScI@rT z;VE+AJ)xwaP)k>dO0rs}fN8pd9wYCh3zbe2;(>njhcw1+$-_bwW8r2X5?#nzOlfLB z>&}|hRy518FZ;2{?dp$Gsi$4}gE7YD{X$XLvF%ixjAEGcXutlYLa(A%eb(!YL!!L*j&%Ju%=wSpk<@mB;OnAzn|6r8*IYaB_#a&IPhbaR^#(_Nq|p zkzF~Seao*=1d@AvPuIMC_+OX&&_dowF~WgX3(q*zF_;y4Z9%}k+0>g<$va0HPIxG= zEO{WUMh{q(gq@4AK90hpPkZw1jky)@W;~vFswLu9Uht18A;1^R!mVmwZxYR>jhOM+ z+vI~4+QwdS*5GR^U6VCyd5@}*bYZk)*Sy(odEReZ@S|t$nZE`&HkdG6aWw3F8u^tV z3^9yOD9cd;JO)MBaqGl)dw!qKKq6PL!7pFdeqjp50Ywxk-m`6gbM>z6FG-&%!n-H6 zP}#ih<=&0bT@0Z>O$hu;|8=Z}*|RI$^>J(&(0IGT)WA+73ybuqjg)9mD(1bVJ;Rdl z89^q7Kk9sPbm$E*eQoeuY6K9P3oL!^g( zO#3eo8*?)*qiUNqKLT8`tW1y8V-=6kj$3|tPEE9kH7b;$w`8W_wrw_Vyl;KRLNwFv zv$1~?3Vj-e_0JiiAB!>VxH#aj0Wz`&Dj#@)aut8CzWLV}P1&WPuTS&AQN2J=y836o z08a@7&@Om8$N(p6(kJLYUl2+a$Y(2>salF9(TEpN?A-jUriH&TEp8fX`UP>RQMd^* zwsZ$vDfnIcoP;nwncI>r7N{XxAytY20Y$vDu&Xd%bL%5~uq9|MNhI2;k!q3mW8@14 z%!qsLpb#84R1&5o^W`l^OA|X~CH6HfR9JZ`+W36HV<0?t0NC2rGF`UZ+Y+7s58BQt zIFu;d(y?vZws~UPc1~>Dwr$(CZQD*x>`d+*)P0z#nwR_9)lXgj-o1ABx7G@IW0oT6 zDs~ZTi8_aA-29^+cP)XAm4oI0UE1MSX-6bzJk)Dm={w`bHr?&6yXxm%hw9+OIVYD4gDlhm?``EiP5Dp8-{i?NsnwPq8TT=` zKl~L(<)}xfCPj88|VG)+}DT7Llh&8JnvZeTV5f0SNyVl@MrB< z12iOikq$RUcnlAw5;Sz!hIoDq8&i4>V|$BWy_ip$)HjO$uGMh_1*xAtE4A>*D$l}* zJv;E1k%p_ZIjY3THKfpTNA|s{oDc+tGwhr}x1AqpDXXk&Ia|53@D9-c3Z+#PG za9G#=T!Xv&5p)t`%dFv9$7s#Ez;d$!8Ymd0Me)%>1bQfKALM*Dac4Lp@2lH<#n~-Q z02$05+k@8?5ITfAyc831vidLOYyOg^ye*c0zB4fp>7{Zg?N-QlMinRnVr z+&w9#ws3n&LdthPxR&<7@wG+fLr@gn0CMdu=Ug*{pQNYWu@YEYTms`k3_JN>_!=NB zoU!miNk(xU9cNVc4`E`=F(a)PDeKf-hx!mEia#!dB!YeOae1ktwD$u{;Mx%^0nh72 z8*k%^x2o;obvfqtTK2)3pRZJ7MStjxk(W-gmH$#|%9*gzYmn1pW#B4AlHgL+)ZTN= znK3T|_q|0sU=R3)FXGk{xKfUq82S{}DfqkMwj7eed_+^yt31K~)0N$JQ#idyg>GLl zq}>0;Zp}kLBVVc0GFap}ZF7A&CtEF5qI9LCE5W`T@FHSNL`^Jf-5YjvtiFYb@K27__wX-oPS3WoMwqiZMsvjl>}~X5WIm3QYv5mh)#35gfx4G4Q+#EotC$Nxc^?{!PgDoj@fBE5Sf0~K5 zGSo%dCQ$RYDirygZCwN*x)0=Fc=I3a*(KI=zSrHnG@E!`BRT(gTB8+h#;PmUi(D+G zt7~^kc@d_NEwM$2kH1raz#4Z2R>oPcv~yhA9DwsPx2zsdPzS!ctcX+VN)%o00g24- zv7|WuD3~g~GE&S$#VSDx=^+k{t(HGfQ_ykGmM!&=>5q|{#!15VSLsewy??F+2 zsc1lp-UjH<%ek;3%0SjsF@YoNH>*xez7p8mBqRIm`-&WaI+?&ya(f^IA4bJi*h>XU zZd3tS-n+V}Pj9XnD_GPPZ>TrBXKMuQ$dC;f0sG|B20p1)3f~X{ocsOnS;P77W&tOD zXRY-=W{nK2uXd56S-c*qc2ErpPlvIY-~^;kX*o4sA&`VLPyH|ZRU`f^qkDstsxO3! zP7`Qi7$J#3Q>BrrBQkPi1Xuj-?>Y4`4@qsw-)BkwzQ(&592O&>L(t(VQuOMp9A=YG znsiM3Sb=UsnCgfo$be6`5Q)I<&$I5XuZ4t`0Uj!`+3+OSa`FO7Lv`1gz39`7*=-IrYN5Zuv4@{hra9J7p$)ibV{y=DIdzomB7K^ zXIQijdGbzd+1X#BOMV`&ETxibkr^MTu*L2XPw7_0@4icvqm@+RzccfHlcyZN*Z=?I zDF<2h@4f#Ic}mptzgXw%|0z$wV*ZO<{SSHC{(s~u01fPam8UrW%^)I?vS0>W*Z%|& zf2&>FmgsmN{Edfik%;A_F2^;RWEh`z4si#c@6%G6#hY~Rz<*HjZ&@Mygf$4t^Dpo8 zI+Y^)3%RG1gLQ4YSA3T?xaVFYQh@lq!$JOzH7kJ~jgRz6b+@d>K^i5W@B{v)SfiR|Es?O)c{$ z#aM0Zi;fx6Ij-Khyoh$7ng<%XAW+?mHy6bre}~BLQslvwEQ^p6ja5*82T{S9@1a|X zI0421H=Uo{&xAVugSyu>AI4bo7Ju4PZi5q9mTW5sER{r6zpqE|qC-#XYHht!MAW>l z3Amd_Qb^;jZkQ9()T@TAWezuYKo}zS&;z=F+_H+OfXyg51P!sEDG=$~&b)7jz&2EJ zPC(0QDBbnJxR0j~j0}%+-0R$JHjnwD_SjrQW+q;s>DaJD?9XcBkz{jJV-!oC-8RN0 z)P{uU4ur&kXu2`Jb$lzfth2WKeh2a+owzb+cD-q{ftlAlp>U7Zt%zbfUPLug@b0>iB%f`t$#CnK}H}%cq6#S zR$nnk?~xR9#0=lO^lBSK354^0*7Y&la5^}}D`(}h1X#fd(nQ`gFJd}RA-fb3@iJzK zj-A|7wns>4Cw5;cZFE}fMscrED2v8-(pqH@ZQ6UO#sQ^C7b}_5_VSO#;9*F@g zO%d51jiwRUzT2H{iT}83i<$|^QK)E&<-qkHJOH_iLMc@v@3_RvAO9GdDGR6g5~k$$ zVVr1Y*Psk|QDrc5Q-#Cz!v?)n4gV>-7D&4TWmckqqhnoU+tfU=MC*m;gXZe>9`uff z(ufqp*rM*@zp=lewR6}gVh#pccQ$=O`ev8bHDF1ORB8fRvv{Z9R;y5|(KhxQE2=u9 zd2fb0Ea)bGmYNH~CZL%JWkY71bP13-n!a*{XPxLewSWXvf`q}vzvq7hC9U)yTlB;R zR<~O&pL6VTU-k;0L_Ny!&{A;->8)azP=O0lRP;2uF4JifkjFFnD^)xw3ltuPm@5-= zr`gS`=MU;1#M1qo9$KBX5JxWs7hC!Emop^r6lC0Sx7-W6)vzGGnKt!3H`_w%1aL3| zZ`AIce#1a6qvgS#9?_lzm%e|;sAdk+rXEl!4t=I!Vt~Z`5Cxtec3)MCnE5wMP4ufi z--herA6y%q&|&3;lbH|w{lLVAQkn&MV+-65+Zs-{~U?!S$$UL3E9PLXNR}v0&qe0tAk_WQ{-V6n7%W z{5I(fbx2|cvG7|r*3n_f=~e{fm31mbb)^psK6hSWys_jCSyBJ5v+Z9Y4Gd4WA z9IwU`5plnqheYl?-k#Q}{cJ`qQ0k@b@RJL3f7-Vc9!PaNdAvl>RLe8fkbrV-|5z4YHcuQRSOD7c- z)RqxU1j;D%b{(mDcQ6c;{TRwy!Fof_2_jchG2}sXJLa+Azb<#{^!7T69T;b? z34XWi;ys6Ca)H={Yp2+8rmQOFeG`}h_yn7TqZPhi+{XcU_J7g&$1i2Of!--EYQHkcag2g{Nb)e1-FM zYeo%m5(XSyq~j|T&22gLFO{~ZcRxQ2`aNj}`b;fx^RnIwBm|Y(Gdryi`hNq+Z^qBx^*eq`4rvZ5680UR)tE19~m zZB5A!B>Yrwl+_KVJkcX$F6fWhj=HX3YY)1=AtF28K09wF{2#F3d75OHipqPmu)c>Z zfbF4cb-w0WoDM(H0g~clGXizioODQ ztX=TRRq~Q=ZZO*hR($|&M6KS^1h}pvc`JkA?Gu-rBiELL@$$(x*+mrHtMtT6#j0U2 zeNg=;W9bSL=vmfw@eOpHVv)X(|JHI>EM_p`HGD0ZGceh-#3ku#@mDr`I#7D*#z3II zj1HXwb%p#<%sI3nky%xx)OMZ9b0=x)kpxAYjz%Jl@v{4hN){o!2jGbc(ZAx)!JM1udhXv$#IxHf>G)hgb+DkD(=FUf4(Po)X zyRDq``1MTfn5F(wa6BO*NrmBEp?pK!pshax&z%>b*@@xH-)TrEW77+xl;dK#Ok&h& z8Ga<1#-kBw8F#S7Ec|^81Bs2JfvKYEvolEHCK18k@wNwgcj~e5!y+ zhS$f(5l_{2n*#_wpAVkn=nDAZJ>_Qrhw(_6V(FLk{X9&0Y}yPS`@f%Ai5S-J?(Hj` zVTaFm%rIFCq@S^;@*7$1i1GLrElfOwJh&H4oDQlXQuw??bcZbAn7=-U{SfXC;TQBi zzf~JLb@11>>nB{`0d)f*BCYZC%|6J)A8FgR;{k@mw_Ir1l+*`Y9M{Jg+f} z2+>WM&PKV)Y;8q&R^LYT$wW|RQ4!b}+_-H<{hvhCChr3WcW~>+5)$sNKt|X~nNTZ& zsjuZGtnHeF5k$`f{)FCbiy^G6s+Rm@;>feUZc444wr!p60M6o%2EPYOm$RifA&BJj z28{YMUMBWNdpl-exF5gs|HiOw1Vl8L9u3Z-2E%DvqY8c8B2i=wQ+n^ke~)QW>5FGx zE|6}JFHx*rbRqM#T8ba=-cbxN?Y`?9L|~Gxk*Z!~%}@hsOC&5&O&Ct~qaBvu7SvNJ zg5BPT?pKkt$kOu`)_p*pvj=d^F||Kc-D%90ym8?*<+-0?fIe+dE)fx|s$zSlePUNr zzM{^jt%KGMZy;*h2)xw*mS4AEE%ApM_#@0RCvJ+6zkm+=sW~3ux6QCvv`W!ZPHhHC zeE0K&TJ~3s8)0BHQTSl%QK()=(P`c!M8{h0y-ztX`C@g2IXt7iyEMexP~qE(Dkeea zjwyXCBCO1eaWj2FA_@Rye3=9J$iIl%jYzgDEFf9r4w5ENy&U#ZPPg1f5QH`z3M8Q= za^!pD!OAY!>i1HR1<{-=CggP2g1rxIsEyx&C$jz;jo{y%PW&XIx*^k9`hN{X*FTJonGbeCK8yo3ZJ-1YXvM)u_pVXaUKo&O%eDVw)8{6oN zhCxGVwLvAghzf%=Ln@U16C0?OKOi0lcb+4ztBMO&0xpWgps$SNbzll)y#oHmTey&@ zA5=f$aDosuVC=-7S=?$Q(b?sd&k9sdH7t3@;XDOVJ8Jinvt1VIOx^Wn#bzYBSWQ>h zKdZ#gFk$qjlgD2o-|Ch=!w(ym10=%t@OEWPF66#JB$nR0#AqFFUQFuHQEc z<`CWPbZ2DX;lKspOwg{Oi>=sh8dPwk1xwRBbN9$SLFTy0Jj;J?hk?SbJ`rl54W4g&0(vzAEV?seg;Tua1M!5}P9@T<5ay}?XrXe_oRt#ZH8z)R zw_eE&Lo`oe(O{j;Eh8f~8-;yZ)nEsEKq>rVXkLy_qLdZbG)I;g|J1WNgnYXA++?{U z*hMNo9o!#JQH_|JrN?A%F`NeH*6&02-7d@mhU*@UghJM_`jp&kKL|duOnfLP>YqvBErky(Zj&%W3nszH~=kV(+!OBTt(iVO+STwCQuu z-W5vuQ8Etm&0rxcR03vl=pT!gR9An-mLw-YjQ_>5xE}+S48Z97m6F}2AW~S`Ot*hsO=h&w5=CG&sC7L z_ei4#4PQ3rAFjqSP_ij}GQP>6`vR6NQJT;{j-W6^$5YxGGGYjzisBtU{BM*d2{@|{ za1{2X-gSwqxqc&`=Vp!54Ys@a7J)QRsWBm7a&Ek)iM3h*sr9YJMAyZqM-qmJ& zl}IosdcINggl;x1`R)kMd>iY!FlLcoR);~-H)1G8bA6U2xuN%T)x5FEF3+|}I+1>A z6hnSy+SUF-O6>*yKOsb|_h#Aq-JK zF7!Ip70?-o8_BX9W<>2|KP)=aUmzasIn)Ov zsNXBJKtYXQq1%6Exl&|sFGTVCtGiV@eQ$6M7M0^Np)+YUs1W52(4Uh#4!Qb@ihPHaH@$nUbjiI+^{luF5|^Zk=kZ zno8S!?ZJPdaPN3R1KDOJFpm_+S3~YWUP{_}FB7n*(|$T9JMiJHNQbkAM+RN6Xjb}f z5lTMl1qb&*t+I-{O?Rb5OTrVTMLXX2@bjPn190t^Jns&toL~trj^#NQ-<~TR>{RD# z{^6NSv+5!kVMOLCjn139`NtSc7&U}ev882Ml)GcmZ@7D@pJ8!uS?(P^c@Z`74Q^NQ zd~5Iu+Ix|9%U`{KQgO*nE(%7x#AfsiOpCcRH z0%{d+Qpv)AkHZtH031*`)69)>E&yCRA=TD0%GW6+d)BCO2d4mRU6Oq$jzMCW>|ZdL zcc|nf%sQ{m7nBILLUB_Hx26sMwtbIL3{dLVB7)^1D+!x|jC@erin6~w=!H|rcDyyqy(54AD;I(YBs3YO6r9+gy#?&Yf088P+q0!m}oKk?gv;E2^my7$8!2i4%DwV}5}D+ezxcp{Sc5!k%_S_o<@LMhVM zy^cD>W%?IY04E7!Kv9^nl)9tQs9CY28V;m?DsPRzgRz&iC?TSd_IVFpGyZ!{W1^9a zfz0LHoa#+m+Jw+5#`B9HNZzKpac?18)i4ITa*jS?YGM0!cA&Kdk~zO9@K}Iy%PrN^HReYA{LAB4lu)#p-SVdadeZ z9g~sU8qOpHm#PKB&MSS zLEjfIQ`^znSI4F)21OIw%T<~)K-=epa~eNW3TC>S;8c1UFuQ-zaGGi4%*x@xUORXS z#ik&vtjGUT$f8B-EQ%FZUVyS*_|^rP!?@D4Fe}*qwEC=b_EPTWaT;iNZ6NC|^GDuy=Mxn=26=1WrSanm= z?x@}3I>gd$kTt#8P5`N!AmlI$8~p%EernepFAuzIISxaT_(X>>UMYNsb^5b}T|EBc zFFk}BgYYXMFd%ler8VBigJ;zZ?G5PY9p(h}1odG3*gMCQL(M}L%k9VMP^Q`Ew*2?9qrvFcu*}kjMnuI$iIO{~`BL5JV>RJuUUT z!aYa2FW<${AZ6lvfY5Fnk;sU>+7^1&U=DDRxKWwcV{bMp-6drC2IQws_|gRcmQhru zVlBko6hakz@ar{fm0#aBtznVXViAqX!SzFp_Y(NO4nNrxrh8*`{WEyfpn~_~m1V*# z`L~%?1om5;;F1C?ypQ}Xo3wKEh!5fhkmgDdINn5)(e z1Y<`ewR`U5F1`TBG|3oL0=}q8gsYH*5_Pij9nUkjNRzZ-a0JrP(wobMVPpXZ9^#kt z?yFdgNR}~Uc9645!h^0plwKw8d$b$RlNP^-GI2m_x=TRm5wgsRK}J_i$&fL{?V*Ajxt{!F38U= zc1?PzPgL%}uVrqNmH1bVKScI}h<`*mi@|)*d@LLSp}tH8iJoBbgL?>h559{tm2-qS z!A}lh%vrI>2R%viup4)|?4~RCs;pNWb`@7xi6$Gh6& zFhCbZjP#qWIJTBsS}4BIu9%ni7LT?~OemI4#^+FLbB347eS1+#-96^~h5 zgwqE$S==xTMi{Nj-3O%1us1fwtX9Rura@OMrW0J|@17!E zo}vc7${?~hH4GbJ>~^AULNT_hFVT|a2H^9;d=nhJcaUwYD$)_*C$9*4buL{pSxI<& zJAV_%7Y3N@y4cTh893V+TrvAHXKU#lm_j%`=eu|Y{-25IErwv=jK*F$R1SIl`zdg!3)hEPO}>9_lph6y(;|sTg%hh zXVyAD-*Is|%RLrAZdGX4G~5ZBK5zOFWSEE25X@Vg@=e6k7NYw$+J%}c3J80i{Ec}- zoHYEF^5Oa_!?9=!S#VGq@XSM@r88A9f@QYY=jjHH4eMcGhO$%o@ODZ4UB9YpoCfjM zpLmSu`AYiKB;%S>DYq^`Xp&Lk74!1;_LXD_xV$?K_MOdX{%y?A5;b4FvsEf0LEB^Mq za@54LEHOHIl21`F`ejgZz9kU0QiH$o$4%W_^^EM56wraJcb@{)HTv)BM)FIN5W8 zRG$mLZrEVlC7b!iAk=o>YP$k- z+1ScM-gy*?wF8*=!nceS?in+4{Zxx#qHz;==v8l=;lNI_`Vw(oPPkE0n75-E^(@P= z3C@351$RlwP|3Ir6(zYR&(C_+_-dOAtrM;1@s9V$;s7ITML`x)-MN)W`X2?xM#d?L@)BAD47rh{IF z8ySi-%xc(5lDWcsGpsFM>}9W{hfn_QA>g+G ! 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 + + + From 3d297eb731d80225c21ff4ff4d95448015a02a78 Mon Sep 17 00:00:00 2001 From: enson-choy <88302346+enson-choy@users.noreply.github.com> Date: Mon, 24 Jan 2022 10:19:58 +0800 Subject: [PATCH 28/42] Feat: Synchronized playback (#6) * Enable synchronised playback control * Rename PRFT variable --- .../exoplayer2/ext/cast/CastTimeline.java | 3 +- .../exoplayer2/PresentationLatencyInfo.java | 55 ++++++++++++++ .../ProducerReferenceTimeBoxHandler.java | 3 - .../exoplayer2/ProducerReferenceTimeInfo.java | 9 +++ .../google/android/exoplayer2/Timeline.java | 5 +- .../DefaultLivePlaybackSpeedControl.java | 17 +++++ .../google/android/exoplayer2/ExoPlayer.java | 14 ++++ .../android/exoplayer2/ExoPlayerImpl.java | 41 ++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 76 ++++++++++++++++++- .../exoplayer2/LivePlaybackSpeedControl.java | 19 +++++ .../android/exoplayer2/SimpleExoPlayer.java | 28 +++++++ .../exoplayer2/source/MaskingMediaSource.java | 3 +- .../source/SinglePeriodTimeline.java | 3 +- .../android/exoplayer2/ExoPlayerTest.java | 22 ++++++ .../android/exoplayer2/TimelineTest.java | 3 +- .../source/dash/DashMediaSource.java | 17 ++++- .../source/dash/manifest/AdaptationSet.java | 4 +- .../source/dash/manifest/DashManifest.java | 3 +- .../dash/manifest/DashManifestParserTest.java | 2 +- .../exoplayer2/testutil/FakeTimeline.java | 4 +- 20 files changed, 313 insertions(+), 18 deletions(-) create mode 100644 library/common/src/main/java/com/google/android/exoplayer2/PresentationLatencyInfo.java 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 1a03b779f60..f0ed4214d48 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 @@ -142,7 +142,8 @@ public Window getWindow(int windowIndex, Window window, long defaultPositionProj durationUs, /* firstPeriodIndex= */ windowIndex, /* lastPeriodIndex= */ windowIndex, - /* positionInFirstPeriodUs= */ 0); + /* positionInFirstPeriodUs= */ 0, + null); } @Override diff --git a/library/common/src/main/java/com/google/android/exoplayer2/PresentationLatencyInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/PresentationLatencyInfo.java new file mode 100644 index 00000000000..b68ed2013c3 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/PresentationLatencyInfo.java @@ -0,0 +1,55 @@ +package com.google.android.exoplayer2; + +import java.util.Date; + +public final class PresentationLatencyInfo { + private final Date wallClock; + private final String type; + private final Double latency; + private final Date currentTime; + + public PresentationLatencyInfo(Date wallClock, String type, Double latency, Date currentTime) { + this.wallClock = wallClock; + this.type = type; + this.latency = latency; + this.currentTime = currentTime; + } + + public Date getWallClock() { + return wallClock; + } + + public String getType() { + return type; + } + + public Double getLatency() { + return latency; + } + + public Date getCurrentTime() { + return currentTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (getClass() != o.getClass()) return false; + + PresentationLatencyInfo that = (PresentationLatencyInfo) o; + + if (!wallClock.equals(that.wallClock)) return false; + if (!type.equals(that.type)) return false; + if (!latency.equals(that.latency)) return false; + return currentTime.equals(that.currentTime); + } + + @Override + public int hashCode() { + int result = wallClock.hashCode(); + result = 31 * result + type.hashCode(); + result = 31 * result + latency.hashCode(); + result = 31 * result + currentTime.hashCode(); + return result; + } +} diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeBoxHandler.java b/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeBoxHandler.java index d366af68f0e..ab5d3db2b1d 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeBoxHandler.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeBoxHandler.java @@ -1,8 +1,5 @@ package com.google.android.exoplayer2; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.util.Util; - public interface ProducerReferenceTimeBoxHandler { void onPrftParsed(ProducerReferenceTimeBox box); } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeInfo.java index 7fd073a265f..3d587ea015d 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeInfo.java @@ -14,6 +14,7 @@ public final class ProducerReferenceTimeInfo { private boolean inband = false; private UTCTiming utcTiming = new UTCTiming(); private long timescale; + private long periodStartTimeMs; public ProducerReferenceTimeInfo() { } @@ -48,6 +49,10 @@ public long getTimescale() { return timescale; } + public long getPeriodStartTimeMs() { + return periodStartTimeMs; + } + public boolean isInband() { return inband; } @@ -76,6 +81,10 @@ public void setType(String type) { this.type = type; } + public void setPeriodStartTimeMs(long periodStartTimeMs) { + this.periodStartTimeMs = periodStartTimeMs; + } + @Override public boolean equals(@Nullable Object obj) { if (this == obj) { diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java index d7e1e955dbf..26ae509e75d 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -132,6 +132,7 @@ public abstract class Timeline { * timeline window"> */ public static final class Window { + @Nullable public ProducerReferenceTimeInfo prftInfo; /** * A {@link #uid} for a window that must be used for single-window {@link Timeline Timelines}. @@ -257,7 +258,8 @@ public Window set( long durationUs, int firstPeriodIndex, int lastPeriodIndex, - long positionInFirstPeriodUs) { + long positionInFirstPeriodUs, + @Nullable ProducerReferenceTimeInfo prftInfo) { this.uid = uid; this.mediaItem = mediaItem != null ? mediaItem : EMPTY_MEDIA_ITEM; this.tag = @@ -278,6 +280,7 @@ public Window set( this.lastPeriodIndex = lastPeriodIndex; this.positionInFirstPeriodUs = positionInFirstPeriodUs; this.isPlaceholder = false; + this.prftInfo = prftInfo; return this; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java index e4fa65c4362..5d64a0dcf02 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java @@ -263,6 +263,7 @@ public DefaultLivePlaybackSpeedControl build() { private long smoothedMinPossibleLiveOffsetUs; private long smoothedMinPossibleLiveOffsetDeviationUs; + private boolean useLivePlaybackSpeedControl = true; private DefaultLivePlaybackSpeedControl( float fallbackMinPlaybackSpeed, @@ -315,6 +316,12 @@ public void setTargetLiveOffsetOverrideUs(long liveOffsetUs) { maybeResetTargetLiveOffsetUs(); } + @Override + public long getTargetLiveOffsetOverrideUs() { + if (targetLiveOffsetOverrideUs == C.TIME_UNSET) return 0; + return targetLiveOffsetOverrideUs; + } + @Override public void notifyRebuffer() { if (currentTargetLiveOffsetUs == C.TIME_UNSET) { @@ -359,6 +366,16 @@ public long getTargetLiveOffsetUs() { return currentTargetLiveOffsetUs; } + @Override + public void setEnableLivePlaybackSpeedControl(boolean state) { + this.useLivePlaybackSpeedControl = state; + } + + @Override + public boolean getEnableLivePlaybackSpeedControl() { + return this.useLivePlaybackSpeedControl; + } + private void maybeResetTargetLiveOffsetUs() { long idealOffsetUs = C.TIME_UNSET; if (mediaConfigurationTargetLiveOffsetUs != C.TIME_UNSET) { 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 9169271d126..73472b7d658 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 @@ -684,4 +684,18 @@ public ExoPlayer build() { * @see #experimentalSetOffloadSchedulingEnabled(boolean) */ boolean experimentalIsSleepingForOffload(); + + /** + * Get the low latency live configuration. + * @return The live configuration. + */ + @Nullable default MediaItem.LiveConfiguration getLiveConfiguration() { + return null; + } + + @Nullable default LivePlaybackSpeedControl getLivePlaybackSpeedControl() {return null;} + + @Nullable default PresentationLatencyInfo getPresentationLatencyInfo() {return null;} + @Nullable default PresentationLatencyInfo getPresentationLatencyInfo(ProducerReferenceTimeInfo prftInfo) {return null;} + @Nullable default ProducerReferenceTimeInfo getProducerReferenceTimeInfo() {return null;} } 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 3ae51eee509..98a210c8fe3 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 @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -81,6 +82,9 @@ private final BandwidthMeter bandwidthMeter; private final Clock clock; + // PRFT related + private final MediaSourceEventListener prftBoxListener; + @RepeatMode private int repeatMode; private boolean shuffleModeEnabled; private int pendingOperationAcks; @@ -180,11 +184,19 @@ public ExoPlayerImpl( playbackInfoUpdate -> playbackInfoUpdateHandler.post(() -> handlePlaybackInfo(playbackInfoUpdate)); playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult); + prftBoxListener = new MediaSourceEventListener() { + @Override + public void onPrftParsed(ProducerReferenceTimeBox box) { + internalPlayer.updateProducerReferenceTimeInfo(box); + } + }; + if (analyticsCollector != null) { analyticsCollector.setPlayer(playerForListeners, applicationLooper); addListener(analyticsCollector); bandwidthMeter.addEventListener(new Handler(applicationLooper), analyticsCollector); } + internalPlayer = new ExoPlayerImplInternal( renderers, @@ -228,6 +240,16 @@ public boolean experimentalIsSleepingForOffload() { return playbackInfo.sleepingForOffload; } + @Override + public MediaItem.LiveConfiguration getLiveConfiguration() { + return internalPlayer.getLiveConfiguration(); + } + + @Override + public LivePlaybackSpeedControl getLivePlaybackSpeedControl() { + return internalPlayer.getLivePlaybackSpeedControl(); + } + @Override @Nullable public AudioComponent getAudioComponent() { @@ -373,6 +395,7 @@ public void setMediaItems( @Override public void setMediaSource(MediaSource mediaSource) { + mediaSource.addEventListener(new Handler(applicationLooper), prftBoxListener); setMediaSources(Collections.singletonList(mediaSource)); } @@ -1453,4 +1476,22 @@ public Timeline getTimeline() { return timeline; } } + + @Nullable + @Override + public PresentationLatencyInfo getPresentationLatencyInfo() { + return internalPlayer.getPresentationLatencyInfo(); + } + + @Nullable + @Override + public PresentationLatencyInfo getPresentationLatencyInfo(ProducerReferenceTimeInfo prftInfo) { + return internalPlayer.getPresentationLatencyInfo(prftInfo); + } + + @Nullable + @Override + public ProducerReferenceTimeInfo getProducerReferenceTimeInfo() { + return internalPlayer.getProducerReferenceTimeInfo(); + } } 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 1f57b5af789..736777ac2b1 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 @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -55,6 +56,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -181,6 +183,9 @@ public interface PlaybackInfoUpdateListener { private final LivePlaybackSpeedControl livePlaybackSpeedControl; private final long releaseTimeoutMs; + // PRFT related + @Nullable private ProducerReferenceTimeInfo prftInfo; + @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -257,6 +262,7 @@ public ExoPlayerImplInternal( Handler eventHandler = new Handler(applicationLooper); queue = new MediaPeriodQueue(analyticsCollector, eventHandler); + mediaSourceList = new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler); // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can @@ -365,6 +371,14 @@ public void setShuffleOrder(ShuffleOrder shuffleOrder) { handler.obtainMessage(MSG_SET_SHUFFLE_ORDER, shuffleOrder).sendToTarget(); } + public LivePlaybackSpeedControl getLivePlaybackSpeedControl() { + return livePlaybackSpeedControl; + } + + @Nullable public MediaItem.LiveConfiguration getLiveConfiguration() { + return window.liveConfiguration; + } + @Override public synchronized void sendMessage(PlayerMessage message) { if (released || !internalPlaybackThread.isAlive()) { @@ -592,6 +606,60 @@ public boolean handleMessage(Message msg) { return true; } + public long getCurrentPosition() { + if (playbackInfo.timeline.isEmpty()) { + return 0; + } else if (playbackInfo.periodId.isAd()) { + return C.usToMs(playbackInfo.positionUs); + } else { + return periodPositionUsToWindowPositionMs(playbackInfo.periodId, playbackInfo.positionUs); + } + } + + private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long positionUs) { + long positionMs = C.usToMs(positionUs); + playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period); + positionMs += period.getPositionInWindowMs(); + return positionMs; + } + + // PRFT related methods + @Nullable public PresentationLatencyInfo getPresentationLatencyInfo() { + if (prftInfo == null) { + // Init the PRFT info. It can still be null if PRFT is not available + prftInfo = window.prftInfo; + } else if (window.prftInfo != null){ + // Update period start time only. Don't override values set by PRFT box. + prftInfo.setPeriodStartTimeMs(window.prftInfo.getPeriodStartTimeMs()); + } + return this.getPresentationLatencyInfo(prftInfo); + } + + @Nullable public PresentationLatencyInfo getPresentationLatencyInfo(@Nullable ProducerReferenceTimeInfo producerReferenceTimeInfo) { + if (producerReferenceTimeInfo == null) return null; + + long presentationTimeMs = producerReferenceTimeInfo.getPeriodStartTimeMs() + window.getPositionInFirstPeriodMs() + getCurrentPosition(); + long currentTime = window.windowStartTimeMs >= 0 ? window.getCurrentUnixTimeMs() : new Date().getTime(); + + double wallClock = producerReferenceTimeInfo.getWallClockAnchor().getTime() + (presentationTimeMs - producerReferenceTimeInfo.getPresentationTimeAnchor()); + long latencyMs = (long) (currentTime - wallClock); + + return new PresentationLatencyInfo(new Date((long) wallClock), producerReferenceTimeInfo.getType(), (latencyMs / 1000.0), new Date(currentTime)); + } + + @Nullable public ProducerReferenceTimeInfo getProducerReferenceTimeInfo() { + return prftInfo; + } + + void updateProducerReferenceTimeInfo(ProducerReferenceTimeBox prftBox) { + if (prftInfo != null) { + prftInfo.setWallClockAnchor(new Date(prftBox.getUTCTime())); + prftInfo.setPresentationTimeAnchor(((double) prftBox.getMediaTime()) / prftInfo.getTimescale() * 1000); + prftInfo.setType(prftBox.getType() == ProducerReferenceTimeBox.TYPE_ENCODER ? "Encoder" : "Captured"); + prftInfo.setSource("prft"); + } + } + // Private methods. private void attemptErrorRecovery(ExoPlaybackException exceptionToRecoverFrom) @@ -1041,8 +1109,10 @@ && isLoadingPossible()) { } private long getCurrentLiveOffsetUs() { - return getLiveOffsetUs( - playbackInfo.timeline, playbackInfo.periodId.periodUid, playbackInfo.positionUs); + PresentationLatencyInfo presentationLatencyInfo = this.getPresentationLatencyInfo(); + return presentationLatencyInfo == null ? getLiveOffsetUs( + playbackInfo.timeline, playbackInfo.periodId.periodUid, playbackInfo.positionUs) : + C.msToUs((long) (presentationLatencyInfo.getLatency() * 1000)); } private long getLiveOffsetUs(Timeline timeline, Object periodUid, long periodPositionUs) { @@ -1057,7 +1127,7 @@ private long getLiveOffsetUs(Timeline timeline, Object periodUid, long periodPos private boolean shouldUseLivePlaybackSpeedControl( Timeline timeline, MediaPeriodId mediaPeriodId) { - if (mediaPeriodId.isAd() || timeline.isEmpty()) { + if (mediaPeriodId.isAd() || timeline.isEmpty() || !livePlaybackSpeedControl.getEnableLivePlaybackSpeedControl()) { return false; } int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java b/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java index 57f85486ab7..91ce9e7a595 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java @@ -40,6 +40,13 @@ public interface LivePlaybackSpeedControl { */ void setTargetLiveOffsetOverrideUs(long liveOffsetUs); + /** + * Gets the target live offset override in microseconds. + * 0us means no override. + * @return the target live offset override in microseconds. + */ + long getTargetLiveOffsetOverrideUs(); + /** * Notifies the live playback speed control that a rebuffer occurred. * @@ -64,4 +71,16 @@ public interface LivePlaybackSpeedControl { * live offset is defined for the current media. */ long getTargetLiveOffsetUs(); + + /** + * Sets whether the live playback speed control should be enabled. + * @param state true if the live playback speed control should be enabled, false otherwise. + */ + void setEnableLivePlaybackSpeedControl(boolean state); + + /** + * Returns whether the live playback speed control is enabled. + * @return true if the live playback speed control is enabled, false otherwise. + */ + boolean getEnableLivePlaybackSpeedControl(); } 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 815824197dd..7997b3af084 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 @@ -719,6 +719,34 @@ public boolean experimentalIsSleepingForOffload() { return player.experimentalIsSleepingForOffload(); } + @Override + public MediaItem.LiveConfiguration getLiveConfiguration() { + return player.getLiveConfiguration(); + } + + @Override + public LivePlaybackSpeedControl getLivePlaybackSpeedControl() { + return player.getLivePlaybackSpeedControl(); + } + + @Nullable + @Override + public PresentationLatencyInfo getPresentationLatencyInfo(ProducerReferenceTimeInfo prftInfo) { + return player.getPresentationLatencyInfo(prftInfo); + } + + @Nullable + @Override + public PresentationLatencyInfo getPresentationLatencyInfo() { + return player.getPresentationLatencyInfo(); + } + + @Nullable + @Override + public ProducerReferenceTimeInfo getProducerReferenceTimeInfo() { + return player.getProducerReferenceTimeInfo(); + } + @Override @Nullable public AudioComponent getAudioComponent() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index f9cf5af50db..5d4d35a035c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -379,7 +379,8 @@ public Window getWindow(int windowIndex, Window window, long defaultPositionProj /* durationUs= */ C.TIME_UNSET, /* firstPeriodIndex= */ 0, /* lastPeriodIndex= */ 0, - /* positionInFirstPeriodUs= */ 0); + /* positionInFirstPeriodUs= */ 0, + null); window.isPlaceholder = true; return window; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 9c9f2265adc..c6624c3e559 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -296,7 +296,8 @@ public Window getWindow(int windowIndex, Window window, long defaultPositionProj windowDurationUs, /* firstPeriodIndex= */ 0, /* lastPeriodIndex= */ 0, - windowPositionInPeriodUs); + windowPositionInPeriodUs, + null); } @Override 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 08d20541d19..6f9c35dcab6 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 @@ -25,6 +25,7 @@ import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; @@ -38,6 +39,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.content.Context; @@ -115,6 +117,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -9001,6 +9004,25 @@ public void onRepeatModeChanged(int repeatMode) { assertThat(containsEvent(allEvents, Player.EVENT_PLAYER_ERROR)).isTrue(); } + /** + * Test the functionality of parsing presentation latency info from Manifest + */ + @Test + public void testGetPresentationLatencyInfo() { + Date wca = new Date(); + Double pta = (double) wca.getTime(); + ProducerReferenceTimeInfo prftInfo = new ProducerReferenceTimeInfo(0, "", wca, pta, false, new ProducerReferenceTimeInfo.UTCTiming(), 0); + prftInfo.setPeriodStartTimeMs(wca.getTime()); + PresentationLatencyInfo latencyInfo = new PresentationLatencyInfo(wca, "", 0.0, wca); + Clock clock = new AutoAdvancingFakeClock(wca.getTime()); + + ExoPlayer player = new TestExoPlayerBuilder(context) + .setClock(clock) + .build(); + + assertEquals(latencyInfo, player.getPresentationLatencyInfo(prftInfo)); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java index fe3edf41772..2b63fc418ba 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -137,7 +137,8 @@ public void windowEquals() { window.durationUs, window.firstPeriodIndex, window.lastPeriodIndex, - window.positionInFirstPeriodUs); + window.positionInFirstPeriodUs, + window.prftInfo); assertThat(window).isEqualTo(otherWindow); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index cfa12dc8bdc..016a4e7a105 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.ProducerReferenceTimeInfo; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider; import com.google.android.exoplayer2.drm.DrmSessionEventListener; @@ -1298,6 +1299,18 @@ public DashTimeline( this.liveConfiguration = liveConfiguration; } + @Nullable private ProducerReferenceTimeInfo extractPrftInfo(DashManifest manifest, int firstPeriodId) { + com.google.android.exoplayer2.source.dash.manifest.Period period = manifest.getPeriod(firstPeriodId); + for (AdaptationSet adaptationSet : period.adaptationSets) { + if (adaptationSet.producerReferenceTimeInfo != null) { + ProducerReferenceTimeInfo prftInfo = adaptationSet.producerReferenceTimeInfo; + prftInfo.setPeriodStartTimeMs(period.startMs); + return prftInfo; + } + } + return null; + } + @Override public int getPeriodCount() { return manifest.getPeriodCount(); @@ -1323,6 +1336,7 @@ public Window getWindow(int windowIndex, Window window, long defaultPositionProj Assertions.checkIndex(windowIndex, 0, 1); long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs( defaultPositionProjectionUs); + return window.set( Window.SINGLE_WINDOW_UID, mediaItem, @@ -1337,7 +1351,8 @@ public Window getWindow(int windowIndex, Window window, long defaultPositionProj windowDurationUs, /* firstPeriodIndex= */ 0, /* lastPeriodIndex= */ getPeriodCount() - 1, - offsetInFirstPeriodUs); + offsetInFirstPeriodUs, + extractPrftInfo(manifest, 0)); } @Override diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java index 3434c00aa9b..88956a9f018 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java @@ -60,7 +60,7 @@ public class AdaptationSet { public final List supplementalProperties; /** Producer Reference Time Info */ - @Nullable public final ProducerReferenceTimeInfo info; + @Nullable public final ProducerReferenceTimeInfo producerReferenceTimeInfo; /** * @param id A non-negative identifier for the adaptation set that's unique in the scope of its @@ -87,6 +87,6 @@ public AdaptationSet( this.accessibilityDescriptors = Collections.unmodifiableList(accessibilityDescriptors); this.essentialProperties = Collections.unmodifiableList(essentialProperties); this.supplementalProperties = Collections.unmodifiableList(supplementalProperties); - this.info = producerReferenceTimeInfo; + this.producerReferenceTimeInfo = producerReferenceTimeInfo; } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 91df7e2084c..6a9f44303f5 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -18,7 +18,6 @@ import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ProducerReferenceTimeInfo; import com.google.android.exoplayer2.offline.FilterableManifest; import com.google.android.exoplayer2.offline.StreamKey; import java.util.ArrayList; @@ -254,7 +253,7 @@ private static ArrayList copyAdaptationSets( adaptationSet.accessibilityDescriptors, adaptationSet.essentialProperties, adaptationSet.supplementalProperties, - adaptationSet.info)); + adaptationSet.producerReferenceTimeInfo)); } while(key.periodIndex == periodIndex); // Add back the last key which doesn't belong to the period being processed keys.addFirst(key); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index e9058cc52ed..a3db625db9e 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -212,7 +212,7 @@ public void parseProducerReferenceTimeInfo() throws IOException { "urn:mpeg:dash:utc:http-iso:2014", "https://time.akamai.com/?iso&ms"), 1000); - assertThat(manifest.getPeriod(0).adaptationSets.get(0).info).isEqualTo(info); + assertThat(manifest.getPeriod(0).adaptationSets.get(0).producerReferenceTimeInfo).isEqualTo(info); } @Test diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index f01cc8d2ca9..303e0263573 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -21,6 +21,7 @@ import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.ProducerReferenceTimeInfo; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.util.Assertions; @@ -334,7 +335,8 @@ public Window getWindow(int windowIndex, Window window, long defaultPositionProj windowDefinition.durationUs, periodOffsets[windowIndex], periodOffsets[windowIndex + 1] - 1, - windowDefinition.windowOffsetInFirstPeriodUs); + windowDefinition.windowOffsetInFirstPeriodUs, + null); window.isPlaceholder = windowDefinition.isPlaceholder; return window; } From da6e5f7e55ca8fed2d692c7c2c718e4af981f883 Mon Sep 17 00:00:00 2001 From: Enson Choy Date: Fri, 28 Jan 2022 15:29:27 +0800 Subject: [PATCH 29/42] Remove upper bound restriction if overidden Remove maximum latency restriction if target latency is manually overridden --- .../android/exoplayer2/DefaultLivePlaybackSpeedControl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java index 5d64a0dcf02..993e86a3d63 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java @@ -386,7 +386,9 @@ private void maybeResetTargetLiveOffsetUs() { if (minTargetLiveOffsetUs != C.TIME_UNSET && idealOffsetUs < minTargetLiveOffsetUs) { idealOffsetUs = minTargetLiveOffsetUs; } - if (maxTargetLiveOffsetUs != C.TIME_UNSET && idealOffsetUs > maxTargetLiveOffsetUs) { + if (maxTargetLiveOffsetUs != C.TIME_UNSET && + targetLiveOffsetOverrideUs == C.TIME_UNSET && + idealOffsetUs > maxTargetLiveOffsetUs) { idealOffsetUs = maxTargetLiveOffsetUs; } } From 959604b6245e2030aa6332f3686f5e33af4b271d Mon Sep 17 00:00:00 2001 From: Enson Choy Date: Fri, 11 Feb 2022 11:21:32 +0800 Subject: [PATCH 30/42] Fix: Set to max latency only if latency is not overridden --- .../android/exoplayer2/DefaultLivePlaybackSpeedControl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java index 993e86a3d63..a12281f8c75 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java @@ -451,6 +451,7 @@ private void adjustTargetLiveOffsetUs(long liveOffsetUs) { currentTargetLiveOffsetUs = Util.constrainValue(offsetWhenSlowingDownNowUs, currentTargetLiveOffsetUs, safeOffsetUs); if (maxTargetLiveOffsetUs != C.TIME_UNSET + && targetLiveOffsetOverrideUs == C.TIME_UNSET && currentTargetLiveOffsetUs > maxTargetLiveOffsetUs) { currentTargetLiveOffsetUs = maxTargetLiveOffsetUs; } From c0adc58491683009fe27aa8585cbaf35dba1e059 Mon Sep 17 00:00:00 2001 From: enson-choy <88302346+enson-choy@users.noreply.github.com> Date: Mon, 14 Feb 2022 16:24:56 +0800 Subject: [PATCH 31/42] Fix: Skip safety checks for overridden latency (#7) Skip all safety features (prevent buffering) if the target latency has been overridden --- .../exoplayer2/DefaultLivePlaybackSpeedControl.java | 7 ++++++- .../android/exoplayer2/LivePlaybackSpeedControl.java | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java index a12281f8c75..895defd69c8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java @@ -318,7 +318,6 @@ public void setTargetLiveOffsetOverrideUs(long liveOffsetUs) { @Override public long getTargetLiveOffsetOverrideUs() { - if (targetLiveOffsetOverrideUs == C.TIME_UNSET) return 0; return targetLiveOffsetOverrideUs; } @@ -428,6 +427,12 @@ private void updateSmoothedMinPossibleLiveOffsetUs(long liveOffsetUs, long buffe } private void adjustTargetLiveOffsetUs(long liveOffsetUs) { + // Ignore all safety checks if we are overriding the target live offset. + if (targetLiveOffsetOverrideUs != C.TIME_UNSET) { + currentTargetLiveOffsetUs = targetLiveOffsetOverrideUs; + return; + } + // Stay in a safe distance (3 standard deviations = >99%) to the minimum possible live offset. long safeOffsetUs = smoothedMinPossibleLiveOffsetUs + 3 * smoothedMinPossibleLiveOffsetDeviationUs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java b/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java index 91ce9e7a595..646eb56f5bc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/LivePlaybackSpeedControl.java @@ -42,7 +42,7 @@ public interface LivePlaybackSpeedControl { /** * Gets the target live offset override in microseconds. - * 0us means no override. + * C.TIME_UNSET means no override. * @return the target live offset override in microseconds. */ long getTargetLiveOffsetOverrideUs(); From 1950f080904357c4126616535ed212d46531da19 Mon Sep 17 00:00:00 2001 From: echoy-harmonicinc <88302346+echoy-harmonicinc@users.noreply.github.com> Date: Fri, 18 Mar 2022 16:05:07 +0800 Subject: [PATCH 32/42] Fix: Incorrect latency for multi-period stream (#8) * Fix: Incorrect latency for multi-period stream * Fix: Remove unnecessary API --- .../exoplayer2/ProducerReferenceTimeInfo.java | 47 ++++++++++--- .../google/android/exoplayer2/Timeline.java | 6 +- .../google/android/exoplayer2/ExoPlayer.java | 2 +- .../android/exoplayer2/ExoPlayerImpl.java | 6 -- .../exoplayer2/ExoPlayerImplInternal.java | 68 +++++++++++++------ .../android/exoplayer2/SimpleExoPlayer.java | 6 -- .../android/exoplayer2/ExoPlayerTest.java | 55 +++++++++++++-- .../source/dash/DashMediaSource.java | 24 ++++--- .../dash/manifest/DashManifestParser.java | 3 +- .../exoplayer2/testutil/FakeTimeline.java | 41 ++++++++++- 10 files changed, 195 insertions(+), 63 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeInfo.java index 3d587ea015d..10f3f920cf6 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ProducerReferenceTimeInfo.java @@ -6,15 +6,16 @@ import java.util.Date; public final class ProducerReferenceTimeInfo { - private int id; + private int id = 0; private String type = ""; private String source = "manifest"; private Date wallClockAnchor = new Date(); - private Double presentationTimeAnchor = 0.0; + private Double presentationTimeAnchorMs = 0.0; private boolean inband = false; private UTCTiming utcTiming = new UTCTiming(); private long timescale; private long periodStartTimeMs; + private long presentationTimeOffsetMs; public ProducerReferenceTimeInfo() { } @@ -23,7 +24,7 @@ public ProducerReferenceTimeInfo(int id, String encoder, Date wallClockAnchor, D this.id = id; this.type = encoder; this.wallClockAnchor = wallClockAnchor; - this.presentationTimeAnchor = presentationTimeAnchor; + this.presentationTimeAnchorMs = presentationTimeAnchor; this.inband = inband; this.utcTiming = utcTiming; this.timescale = timescale; @@ -41,8 +42,8 @@ public Date getWallClockAnchor() { return wallClockAnchor; } - public Double getPresentationTimeAnchor() { - return presentationTimeAnchor; + public Double getPresentationTimeAnchorMs() { + return presentationTimeAnchorMs; } public long getTimescale() { @@ -61,8 +62,8 @@ public void setWallClockAnchor(Date wallClockAnchor) { this.wallClockAnchor = wallClockAnchor; } - public void setPresentationTimeAnchor(Double presentationTimeAnchor) { - this.presentationTimeAnchor = presentationTimeAnchor; + public void setPresentationTimeAnchorMs(Double presentationTimeAnchorMs) { + this.presentationTimeAnchorMs = presentationTimeAnchorMs; } public void setTimescale(long timescale) { @@ -85,6 +86,33 @@ public void setPeriodStartTimeMs(long periodStartTimeMs) { this.periodStartTimeMs = periodStartTimeMs; } + public long getPresentationTimeOffsetMs() { + return presentationTimeOffsetMs; + } + + public void setPresentationTimeOffsetMs(long presentationTimeOffsetMs) { + this.presentationTimeOffsetMs = presentationTimeOffsetMs; + } + + /** + * Merge but skip properties set in updateProducerReferenceTimeInfo() + * @param target ProducerReferenceTimeInfo + */ + public ProducerReferenceTimeInfo mergePrftInfo(ProducerReferenceTimeInfo target) { + this.id = target.id; + this.inband = target.inband; + this.utcTiming = target.utcTiming; + this.timescale = target.timescale; + this.periodStartTimeMs = target.periodStartTimeMs; + this.presentationTimeOffsetMs = target.presentationTimeOffsetMs; + if (!target.inband) { + this.wallClockAnchor = target.wallClockAnchor; + this.presentationTimeAnchorMs = target.presentationTimeAnchorMs; + this.source = "manifest"; + } + return this; + } + @Override public boolean equals(@Nullable Object obj) { if (this == obj) { @@ -97,11 +125,12 @@ public boolean equals(@Nullable Object obj) { return Util.areEqual(this.id, other.id) && Util.areEqual(this.type, other.type) && Util.areEqual(this.wallClockAnchor, other.wallClockAnchor) - && Util.areEqual(this.presentationTimeAnchor, other.presentationTimeAnchor) + && Util.areEqual(this.presentationTimeAnchorMs, other.presentationTimeAnchorMs) && Util.areEqual(this.inband, other.inband) && Util.areEqual(this.utcTiming, other.utcTiming) && Util.areEqual(this.timescale, other.timescale) - && Util.areEqual(this.source, other.source); + && Util.areEqual(this.source, other.source) + && Util.areEqual(this.presentationTimeOffsetMs, other.presentationTimeOffsetMs); } public static class UTCTiming { diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java index 26ae509e75d..aa42d228a7f 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -25,6 +25,8 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.util.Map; + /** * A flexible representation of the structure of media. A timeline is able to represent the * structure of a wide variety of media, from simple cases like a single media file through to @@ -132,7 +134,7 @@ public abstract class Timeline { * timeline window"> */ public static final class Window { - @Nullable public ProducerReferenceTimeInfo prftInfo; + @Nullable public Map prftInfo; /** * A {@link #uid} for a window that must be used for single-window {@link Timeline Timelines}. @@ -259,7 +261,7 @@ public Window set( int firstPeriodIndex, int lastPeriodIndex, long positionInFirstPeriodUs, - @Nullable ProducerReferenceTimeInfo prftInfo) { + @Nullable Map prftInfo) { this.uid = uid; this.mediaItem = mediaItem != null ? mediaItem : EMPTY_MEDIA_ITEM; this.tag = 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 73472b7d658..07da0478a74 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 @@ -696,6 +696,6 @@ public ExoPlayer build() { @Nullable default LivePlaybackSpeedControl getLivePlaybackSpeedControl() {return null;} @Nullable default PresentationLatencyInfo getPresentationLatencyInfo() {return null;} - @Nullable default PresentationLatencyInfo getPresentationLatencyInfo(ProducerReferenceTimeInfo prftInfo) {return null;} + @Nullable default ProducerReferenceTimeInfo getProducerReferenceTimeInfo() {return null;} } 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 98a210c8fe3..9bcf9a3f62e 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 @@ -1483,12 +1483,6 @@ public PresentationLatencyInfo getPresentationLatencyInfo() { return internalPlayer.getPresentationLatencyInfo(); } - @Nullable - @Override - public PresentationLatencyInfo getPresentationLatencyInfo(ProducerReferenceTimeInfo prftInfo) { - return internalPlayer.getPresentationLatencyInfo(prftInfo); - } - @Nullable @Override public ProducerReferenceTimeInfo getProducerReferenceTimeInfo() { 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 736777ac2b1..c4a6a5412a2 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 @@ -37,7 +37,6 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -625,26 +624,56 @@ private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long pos // PRFT related methods @Nullable public PresentationLatencyInfo getPresentationLatencyInfo() { - if (prftInfo == null) { + if (period.id == null) return null; + return getPresentationLatencyInfo(prftInfo, period, playbackInfo.timeline, getCurrentPosition()); + } + + @Nullable private static PresentationLatencyInfo getPresentationLatencyInfo( + @Nullable ProducerReferenceTimeInfo producerReferenceTimeInfo, + Timeline.Period period, + Timeline timeline, + long currentPositionMs) { + if (timeline.getWindowCount() == 0 || timeline.getPeriodCount() == 0) return null; + + ProducerReferenceTimeInfo prft = producerReferenceTimeInfo; + long firstPeriodStartTimeMs = 0; + Timeline.Window window = timeline.getWindow(period.windowIndex, new Timeline.Window()); + + /* Update PRFT with latest info */ + + if (window.prftInfo != null && period.id != null) { // Init the PRFT info. It can still be null if PRFT is not available - prftInfo = window.prftInfo; - } else if (window.prftInfo != null){ - // Update period start time only. Don't override values set by PRFT box. - prftInfo.setPeriodStartTimeMs(window.prftInfo.getPeriodStartTimeMs()); + ProducerReferenceTimeInfo newPrft = window.prftInfo.get(period.id.toString()); + if (newPrft != null) { + prft = prft == null ? newPrft : prft.mergePrftInfo(newPrft); + } + // Update first period start time + Timeline.Period firstPeriod = timeline.getPeriod(window.firstPeriodIndex, new Timeline.Period(), true); + String firstPeriodId = firstPeriod.id == null ? "" : firstPeriod.id.toString(); + ProducerReferenceTimeInfo firstPeriodPrft = window.prftInfo.get(firstPeriodId); + if (firstPeriodPrft != null) { + firstPeriodStartTimeMs = firstPeriodPrft.getPeriodStartTimeMs(); + } } - return this.getPresentationLatencyInfo(prftInfo); - } + if (prft == null) return null; - @Nullable public PresentationLatencyInfo getPresentationLatencyInfo(@Nullable ProducerReferenceTimeInfo producerReferenceTimeInfo) { - if (producerReferenceTimeInfo == null) return null; + /* Calculate latency with updated PRFT */ - long presentationTimeMs = producerReferenceTimeInfo.getPeriodStartTimeMs() + window.getPositionInFirstPeriodMs() + getCurrentPosition(); long currentTime = window.windowStartTimeMs >= 0 ? window.getCurrentUnixTimeMs() : new Date().getTime(); + long timescale = prft.getTimescale(); + double pta = prft.getPresentationTimeAnchorMs(); + double pto = prft.getPresentationTimeOffsetMs(); + long periodStartTimeMs = prft.getPeriodStartTimeMs(); - double wallClock = producerReferenceTimeInfo.getWallClockAnchor().getTime() + (presentationTimeMs - producerReferenceTimeInfo.getPresentationTimeAnchor()); - long latencyMs = (long) (currentTime - wallClock); + double ptaMs = pta / timescale; + double ptoMs = pto / timescale; + double ptMs = window.getPositionInFirstPeriodMs() + currentPositionMs + firstPeriodStartTimeMs; + double ptaMpdMs = periodStartTimeMs + (ptaMs - ptoMs); - return new PresentationLatencyInfo(new Date((long) wallClock), producerReferenceTimeInfo.getType(), (latencyMs / 1000.0), new Date(currentTime)); + double wallClock = prft.getWallClockAnchor().getTime() + (ptMs - ptaMpdMs); + double latency = (currentTime - wallClock) / C.MILLIS_PER_SECOND; + + return new PresentationLatencyInfo(new Date((long) wallClock), prft.getType(), latency, new Date(currentTime)); } @Nullable public ProducerReferenceTimeInfo getProducerReferenceTimeInfo() { @@ -652,12 +681,13 @@ private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long pos } void updateProducerReferenceTimeInfo(ProducerReferenceTimeBox prftBox) { - if (prftInfo != null) { - prftInfo.setWallClockAnchor(new Date(prftBox.getUTCTime())); - prftInfo.setPresentationTimeAnchor(((double) prftBox.getMediaTime()) / prftInfo.getTimescale() * 1000); - prftInfo.setType(prftBox.getType() == ProducerReferenceTimeBox.TYPE_ENCODER ? "Encoder" : "Captured"); - prftInfo.setSource("prft"); + if (prftInfo == null) { + prftInfo = new ProducerReferenceTimeInfo(); } + prftInfo.setWallClockAnchor(new Date(prftBox.getUTCTime())); + prftInfo.setPresentationTimeAnchorMs((double) prftBox.getMediaTime() * C.MILLIS_PER_SECOND); + prftInfo.setType(prftBox.getType() == ProducerReferenceTimeBox.TYPE_ENCODER ? "Encoder" : "Captured"); + prftInfo.setSource("prft"); } // Private methods. 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 7997b3af084..686f2accb80 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 @@ -729,12 +729,6 @@ public LivePlaybackSpeedControl getLivePlaybackSpeedControl() { return player.getLivePlaybackSpeedControl(); } - @Nullable - @Override - public PresentationLatencyInfo getPresentationLatencyInfo(ProducerReferenceTimeInfo prftInfo) { - return player.getPresentationLatencyInfo(prftInfo); - } - @Nullable @Override public PresentationLatencyInfo getPresentationLatencyInfo() { 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 6f9c35dcab6..3dc0d8de498 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 @@ -114,13 +114,17 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Range; import java.io.IOException; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -9008,19 +9012,56 @@ public void onRepeatModeChanged(int repeatMode) { * Test the functionality of parsing presentation latency info from Manifest */ @Test - public void testGetPresentationLatencyInfo() { - Date wca = new Date(); - Double pta = (double) wca.getTime(); - ProducerReferenceTimeInfo prftInfo = new ProducerReferenceTimeInfo(0, "", wca, pta, false, new ProducerReferenceTimeInfo.UTCTiming(), 0); + public void testGetPresentationLatencyInfo() throws TimeoutException { + SecureRandom random = new SecureRandom(); + int randomLatency = (random.nextInt(60) & Integer.MAX_VALUE); + long currentTime = new Date().getTime(); + + // Testing data + Date wca = new Date(currentTime); + wca.setTime(wca.getTime() + randomLatency * C.MILLIS_PER_SECOND); + Double pta = (double) currentTime; + long pto = pta.longValue(); + long timescale = 1; + + ProducerReferenceTimeInfo firstPrftInfo = new ProducerReferenceTimeInfo(); + firstPrftInfo.setPeriodStartTimeMs(wca.getTime() - randomLatency * C.MILLIS_PER_SECOND); + ProducerReferenceTimeInfo prftInfo = new ProducerReferenceTimeInfo(1, "", wca, pta, false, new ProducerReferenceTimeInfo.UTCTiming(), timescale); prftInfo.setPeriodStartTimeMs(wca.getTime()); - PresentationLatencyInfo latencyInfo = new PresentationLatencyInfo(wca, "", 0.0, wca); - Clock clock = new AutoAdvancingFakeClock(wca.getTime()); + prftInfo.setPresentationTimeOffsetMs(pto); + + Map prftInfoMap = new HashMap<>(); + prftInfoMap.put("0", firstPrftInfo); + prftInfoMap.put("1", prftInfo); + // Setup stubs + Clock clock = new AutoAdvancingFakeClock(wca.getTime()); ExoPlayer player = new TestExoPlayerBuilder(context) .setClock(clock) .build(); + Timeline timeline = new FakeTimeline(new TimelineWindowDefinition( + 2, + 0, + true, + true, + true, + false, + 0, + randomLatency * C.MICROS_PER_SECOND, + 0, + AdPlaybackState.NONE, + new MediaItem.Builder().setUri(Uri.EMPTY).build(), + prftInfoMap)); + player.setMediaSource(new FakeMediaSource(timeline)); + player.prepare(); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY); + + // Expected data + // Compensate for the latency caused by runUntilPlaybackState() + Double latency = (double) (clock.currentTimeMillis() - currentTime) / C.MILLIS_PER_SECOND; + PresentationLatencyInfo latencyInfo = new PresentationLatencyInfo(new Date(pta.longValue()), "", latency, new Date(clock.currentTimeMillis())); - assertEquals(latencyInfo, player.getPresentationLatencyInfo(prftInfo)); + assertEquals(latencyInfo, player.getPresentationLatencyInfo()); } // Internal methods. diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 016a4e7a105..5101db2e048 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -85,8 +85,10 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -1299,16 +1301,20 @@ public DashTimeline( this.liveConfiguration = liveConfiguration; } - @Nullable private ProducerReferenceTimeInfo extractPrftInfo(DashManifest manifest, int firstPeriodId) { - com.google.android.exoplayer2.source.dash.manifest.Period period = manifest.getPeriod(firstPeriodId); - for (AdaptationSet adaptationSet : period.adaptationSets) { - if (adaptationSet.producerReferenceTimeInfo != null) { - ProducerReferenceTimeInfo prftInfo = adaptationSet.producerReferenceTimeInfo; - prftInfo.setPeriodStartTimeMs(period.startMs); - return prftInfo; + private Map extractPrftInfo(DashManifest manifest) { + Map prftInfoMap = new HashMap<>(); + for (int i = 0; i < manifest.getPeriodCount(); i++) { + com.google.android.exoplayer2.source.dash.manifest.Period period = manifest.getPeriod(i); + for (AdaptationSet adaptationSet : period.adaptationSets) { + if (adaptationSet.producerReferenceTimeInfo != null) { + ProducerReferenceTimeInfo prftInfo = adaptationSet.producerReferenceTimeInfo; + prftInfo.setPeriodStartTimeMs(period.startMs); + String periodId = (period.id == null || period.id.isEmpty()) ? String.valueOf(i) : period.id; + prftInfoMap.put(periodId, prftInfo); + } } } - return null; + return prftInfoMap; } @Override @@ -1352,7 +1358,7 @@ public Window getWindow(int windowIndex, Window window, long defaultPositionProj /* firstPeriodIndex= */ 0, /* lastPeriodIndex= */ getPeriodCount() - 1, offsetInFirstPeriodUs, - extractPrftInfo(manifest, 0)); + extractPrftInfo(manifest)); } @Override diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index a5b7f11d290..06b1ec8751b 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -508,6 +508,7 @@ protected AdaptationSet parseAdaptationSet( // Update timescale when segment is available if (producerReferenceTimeInfo != null && segmentBase != null) { producerReferenceTimeInfo.setTimescale(segmentBase.timescale); + producerReferenceTimeInfo.setPresentationTimeOffsetMs(segmentBase.presentationTimeOffset * 1000); } // Build the representations. @@ -1578,7 +1579,7 @@ protected ProducerReferenceTimeInfo parsePrft(XmlPullParser xpp) throws IOExcept } while (!XmlPullParserUtil.isEndTag(xpp, "ProducerReferenceTime")); // Initiate timescale as 0, and update it later - return new ProducerReferenceTimeInfo(id, type, wallClockTime, presentationTime, inband, new ProducerReferenceTimeInfo.UTCTiming(schemeIdUri, value), 0); + return new ProducerReferenceTimeInfo(id, type, wallClockTime, presentationTime * 1000, inband, new ProducerReferenceTimeInfo.UTCTiming(schemeIdUri, value), 0); } // Utility methods. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 303e0263573..0cd224f587e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -19,6 +19,9 @@ import android.net.Uri; import android.util.Pair; + +import androidx.annotation.Nullable; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.ProducerReferenceTimeInfo; @@ -27,6 +30,7 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; +import java.util.Map; /** Fake {@link Timeline} which can be setup to return custom {@link TimelineWindowDefinition}s. */ public final class FakeTimeline extends Timeline { @@ -53,6 +57,7 @@ public static final class TimelineWindowDefinition { public final long defaultPositionUs; public final long windowOffsetInFirstPeriodUs; public final AdPlaybackState adPlaybackState; + @Nullable public final Map prftInfo; /** * Creates a window definition that corresponds to a placeholder timeline using the given tag. @@ -180,7 +185,35 @@ public TimelineWindowDefinition( defaultPositionUs, windowOffsetInFirstPeriodUs, adPlaybackState, - FAKE_MEDIA_ITEM.buildUpon().setTag(id).build()); + FAKE_MEDIA_ITEM.buildUpon().setTag(id).build(), + null); + } + + public TimelineWindowDefinition( + int periodCount, + Object id, + boolean isSeekable, + boolean isDynamic, + boolean isLive, + boolean isPlaceholder, + long durationUs, + long defaultPositionUs, + long windowOffsetInFirstPeriodUs, + AdPlaybackState adPlaybackState, + MediaItem mediaItem) { + this( + periodCount, + id, + isSeekable, + isDynamic, + isLive, + isPlaceholder, + durationUs, + defaultPositionUs, + windowOffsetInFirstPeriodUs, + adPlaybackState, + mediaItem, + null); } /** @@ -211,7 +244,8 @@ public TimelineWindowDefinition( long defaultPositionUs, long windowOffsetInFirstPeriodUs, AdPlaybackState adPlaybackState, - MediaItem mediaItem) { + MediaItem mediaItem, + @Nullable Map prftInfo) { Assertions.checkArgument(durationUs != C.TIME_UNSET || periodCount == 1); this.periodCount = periodCount; this.id = id; @@ -224,6 +258,7 @@ public TimelineWindowDefinition( this.defaultPositionUs = defaultPositionUs; this.windowOffsetInFirstPeriodUs = windowOffsetInFirstPeriodUs; this.adPlaybackState = adPlaybackState; + this.prftInfo = prftInfo; } } @@ -336,7 +371,7 @@ public Window getWindow(int windowIndex, Window window, long defaultPositionProj periodOffsets[windowIndex], periodOffsets[windowIndex + 1] - 1, windowDefinition.windowOffsetInFirstPeriodUs, - null); + windowDefinition.prftInfo); window.isPlaceholder = windowDefinition.isPlaceholder; return window; } From 1252217697e957c720c23e7a26758bfc85114f06 Mon Sep 17 00:00:00 2001 From: Enson Choy Date: Tue, 10 May 2022 12:57:40 +0800 Subject: [PATCH 33/42] Deprecate JCenter --- build.gradle | 6 +++++- constants.gradle | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 0e356f8676e..66d4955a99b 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,8 @@ buildscript { repositories { google() + mavenCentral() + // Keep JCenter for downward compatibility jcenter() } dependencies { @@ -26,6 +28,8 @@ buildscript { allprojects { repositories { google() + mavenCentral() + // Keep JCenter for downward compatibility jcenter() } project.ext { @@ -41,4 +45,4 @@ allprojects { } apply from: 'javadoc_combined.gradle' -apply plugin: 'io.codearte.nexus-staging' \ No newline at end of file +apply plugin: 'io.codearte.nexus-staging' diff --git a/constants.gradle b/constants.gradle index b9c24128bcf..5bb8095bd3d 100644 --- a/constants.gradle +++ b/constants.gradle @@ -19,7 +19,7 @@ project.ext { appTargetSdkVersion = 29 targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest. compileSdkVersion = 30 - dexmakerVersion = '2.21.0' + dexmakerVersion = '2.28.1' junitVersion = '4.13-rc-2' guavaVersion = '27.1-android' mockitoVersion = '2.28.2' From 3aaa8a01c920e6bea5e369f32b5588a1d26626ee Mon Sep 17 00:00:00 2001 From: echoy-harmonicinc <88302346+echoy-harmonicinc@users.noreply.github.com> Date: Thu, 12 May 2022 15:49:21 +0800 Subject: [PATCH 34/42] Remove deprecated packages (#9) Remove deprecated packages that used libraries that are not available now --- build.gradle | 4 - extensions/gvr/README.md | 44 ---- extensions/gvr/build.gradle | 37 ---- extensions/gvr/src/main/AndroidManifest.xml | 16 -- .../exoplayer2/ext/gvr/GvrAudioProcessor.java | 196 ------------------ .../exoplayer2/ext/gvr/package-info.java | 19 -- .../gvr/src/main/res/layout/exo_vr_ui.xml | 20 -- extensions/gvr/src/main/res/values/styles.xml | 18 -- extensions/jobdispatcher/README.md | 25 --- extensions/jobdispatcher/build.gradle | 34 --- .../src/main/AndroidManifest.xml | 18 -- .../jobdispatcher/JobDispatcherScheduler.java | 171 --------------- .../ext/jobdispatcher/package-info.java | 19 -- extensions/rtmp/build.gradle | 2 +- .../exoplayer2/ext/rtmp/RtmpDataSource.java | 4 +- 15 files changed, 3 insertions(+), 624 deletions(-) delete mode 100644 extensions/gvr/README.md delete mode 100644 extensions/gvr/build.gradle delete mode 100644 extensions/gvr/src/main/AndroidManifest.xml delete mode 100644 extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java delete mode 100644 extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/package-info.java delete mode 100644 extensions/gvr/src/main/res/layout/exo_vr_ui.xml delete mode 100644 extensions/gvr/src/main/res/values/styles.xml delete mode 100644 extensions/jobdispatcher/README.md delete mode 100644 extensions/jobdispatcher/build.gradle delete mode 100644 extensions/jobdispatcher/src/main/AndroidManifest.xml delete mode 100644 extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java delete mode 100644 extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java diff --git a/build.gradle b/build.gradle index 66d4955a99b..99c71553289 100644 --- a/build.gradle +++ b/build.gradle @@ -15,8 +15,6 @@ buildscript { repositories { google() mavenCentral() - // Keep JCenter for downward compatibility - jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.0.1' @@ -29,8 +27,6 @@ allprojects { repositories { google() mavenCentral() - // Keep JCenter for downward compatibility - jcenter() } project.ext { exoplayerPublishEnabled = false diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md deleted file mode 100644 index 43a9e2cb627..00000000000 --- a/extensions/gvr/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# ExoPlayer GVR extension # - -**DEPRECATED - If you still need this extension, please contact us by filing an -issue on our [issue tracker][].** - -The GVR extension wraps the [Google VR SDK for Android][]. It provides a -GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering -of surround sound and ambisonic soundfields. - -[Google VR SDK for Android]: https://developers.google.com/vr/android/ -[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround -[issue tracker]: https://github.com/google/ExoPlayer/issues - -## Getting the extension ## - -The easiest way to use the extension is to add it as a gradle dependency: - -```gradle -implementation 'com.google.android.exoplayer:extension-gvr:2.X.X' -``` - -where `2.X.X` is the version, which must match the version of the ExoPlayer -library being used. - -Alternatively, you can clone the ExoPlayer repository and depend on the module -locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. - -## Using the extension ## - -* If using `DefaultRenderersFactory`, override - `DefaultRenderersFactory.buildAudioProcessors` to return a - `GvrAudioProcessor`. -* If constructing renderers directly, pass a `GvrAudioProcessor` to - `MediaCodecAudioRenderer`'s constructor. - -[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md - -## Links ## - -* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.gvr.*` - belong to this module. - -[Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle deleted file mode 100644 index 4fb56a07646..00000000000 --- a/extensions/gvr/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (C) 2017 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" - -android.defaultConfig.minSdkVersion 19 - -dependencies { - implementation project(modulePrefix + 'library-core') - implementation project(modulePrefix + 'library-ui') - implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion - api 'com.google.vr:sdk-base:1.190.0' - compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion -} - -ext { - javadocTitle = 'GVR extension' -} -apply from: '../../javadoc_library.gradle' - -ext { - releaseArtifact = 'extension-gvr' - releaseDescription = 'Google VR extension for ExoPlayer.' -} -apply from: '../../publish.gradle' -apply from: '../../publish-mavencentral.gradle' diff --git a/extensions/gvr/src/main/AndroidManifest.xml b/extensions/gvr/src/main/AndroidManifest.xml deleted file mode 100644 index 6706b2507ea..00000000000 --- a/extensions/gvr/src/main/AndroidManifest.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java deleted file mode 100644 index 041ca09db4e..00000000000 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.gvr; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.audio.AudioProcessor; -import com.google.android.exoplayer2.util.Assertions; -import com.google.vr.sdk.audio.GvrAudioSurround; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * An {@link AudioProcessor} that uses {@code GvrAudioSurround} to provide binaural rendering of - * surround sound and ambisonic soundfields. - * - * @deprecated If you still need this component, please contact us by filing an issue on our issue tracker. - */ -@Deprecated -public class GvrAudioProcessor implements AudioProcessor { - - static { - ExoPlayerLibraryInfo.registerModule("goog.exo.gvr"); - } - - private static final int FRAMES_PER_OUTPUT_BUFFER = 1024; - private static final int OUTPUT_CHANNEL_COUNT = 2; - private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output. - private static final int NO_SURROUND_FORMAT = GvrAudioSurround.SurroundFormat.INVALID; - - private AudioFormat pendingInputAudioFormat; - private int pendingGvrAudioSurroundFormat; - @Nullable private GvrAudioSurround gvrAudioSurround; - private ByteBuffer buffer; - private boolean inputEnded; - - private float w; - private float x; - private float y; - private float z; - - /** Creates a new GVR audio processor. */ - public GvrAudioProcessor() { - // Use the identity for the initial orientation. - w = 1f; - pendingInputAudioFormat = AudioFormat.NOT_SET; - buffer = EMPTY_BUFFER; - pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT; - } - - /** - * Updates the listener head orientation. May be called on any thread. See - * {@code GvrAudioSurround.updateNativeOrientation}. - * - * @param w The w component of the quaternion. - * @param x The x component of the quaternion. - * @param y The y component of the quaternion. - * @param z The z component of the quaternion. - */ - public synchronized void updateOrientation(float w, float x, float y, float z) { - this.w = w; - this.x = x; - this.y = y; - this.z = z; - if (gvrAudioSurround != null) { - gvrAudioSurround.updateNativeOrientation(w, x, y, z); - } - } - - @SuppressWarnings("ReferenceEquality") - @Override - public synchronized AudioFormat configure(AudioFormat inputAudioFormat) - throws UnhandledAudioFormatException { - if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { - maybeReleaseGvrAudioSurround(); - throw new UnhandledAudioFormatException(inputAudioFormat); - } - switch (inputAudioFormat.channelCount) { - case 1: - pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO; - break; - case 2: - pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO; - break; - case 4: - pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS; - break; - case 6: - pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE; - break; - case 9: - pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS; - break; - case 16: - pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS; - break; - default: - throw new UnhandledAudioFormatException(inputAudioFormat); - } - if (buffer == EMPTY_BUFFER) { - buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE) - .order(ByteOrder.nativeOrder()); - } - pendingInputAudioFormat = inputAudioFormat; - return new AudioFormat(inputAudioFormat.sampleRate, OUTPUT_CHANNEL_COUNT, C.ENCODING_PCM_16BIT); - } - - @Override - public boolean isActive() { - return pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT || gvrAudioSurround != null; - } - - @Override - public void queueInput(ByteBuffer input) { - int position = input.position(); - Assertions.checkNotNull(gvrAudioSurround); - int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position); - input.position(position + readBytes); - } - - @Override - public void queueEndOfStream() { - // TODO(internal b/174554082): assert gvrAudioSurround is non-null here and in getOutput. - if (gvrAudioSurround != null) { - gvrAudioSurround.triggerProcessing(); - } - inputEnded = true; - } - - @Override - public ByteBuffer getOutput() { - if (gvrAudioSurround == null) { - return EMPTY_BUFFER; - } - int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity()); - buffer.position(0).limit(writtenBytes); - return buffer; - } - - @Override - public boolean isEnded() { - return inputEnded - && (gvrAudioSurround == null || gvrAudioSurround.getAvailableOutputSize() == 0); - } - - @Override - public void flush() { - if (pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT) { - maybeReleaseGvrAudioSurround(); - gvrAudioSurround = - new GvrAudioSurround( - pendingGvrAudioSurroundFormat, - pendingInputAudioFormat.sampleRate, - pendingInputAudioFormat.channelCount, - FRAMES_PER_OUTPUT_BUFFER); - gvrAudioSurround.updateNativeOrientation(w, x, y, z); - pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT; - } else if (gvrAudioSurround != null) { - gvrAudioSurround.flush(); - } - inputEnded = false; - } - - @Override - public synchronized void reset() { - maybeReleaseGvrAudioSurround(); - updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f); - inputEnded = false; - pendingInputAudioFormat = AudioFormat.NOT_SET; - buffer = EMPTY_BUFFER; - pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT; - } - - private void maybeReleaseGvrAudioSurround() { - if (gvrAudioSurround != null) { - gvrAudioSurround.release(); - gvrAudioSurround = null; - } - } - -} diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/package-info.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/package-info.java deleted file mode 100644 index 155317fc29d..00000000000 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/package-info.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (C) 2020 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. - */ -@NonNullApi -package com.google.android.exoplayer2.ext.gvr; - -import com.google.android.exoplayer2.util.NonNullApi; diff --git a/extensions/gvr/src/main/res/layout/exo_vr_ui.xml b/extensions/gvr/src/main/res/layout/exo_vr_ui.xml deleted file mode 100644 index 6863da95780..00000000000 --- a/extensions/gvr/src/main/res/layout/exo_vr_ui.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/extensions/gvr/src/main/res/values/styles.xml b/extensions/gvr/src/main/res/values/styles.xml deleted file mode 100644 index 2affbb2f053..00000000000 --- a/extensions/gvr/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - -