From a626bcdb7cc73fd5b39d8cea5b49e3f2f4f97eab Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Mon, 30 May 2022 17:58:14 +0000 Subject: [PATCH 01/45] Remove FfmpegVideoRenderer from 2.18.0 release --- .../media3/decoder/ffmpeg/FfmpegLibrary.java | 6 +- .../decoder/ffmpeg/FfmpegVideoRenderer.java | 136 ------------------ .../ffmpeg/DefaultRenderersFactoryTest.java | 11 +- libraries/exoplayer/proguard-rules.txt | 4 - .../exoplayer/DefaultRenderersFactory.java | 25 ---- 5 files changed, 2 insertions(+), 180 deletions(-) delete mode 100644 libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegVideoRenderer.java diff --git a/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegLibrary.java b/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegLibrary.java index bc86776cac..19155d8b76 100644 --- a/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegLibrary.java +++ b/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegLibrary.java @@ -50,7 +50,7 @@ public final class FfmpegLibrary { /** * Override the names of the FFmpeg native libraries. If an application wishes to call this * method, it must do so before calling any other method defined by this class, and before - * instantiating a {@link FfmpegAudioRenderer} or {@link FfmpegVideoRenderer} instance. + * instantiating a {@link FfmpegAudioRenderer} instance. * * @param libraries The names of the FFmpeg native libraries. */ @@ -148,10 +148,6 @@ public final class FfmpegLibrary { return "pcm_mulaw"; case MimeTypes.AUDIO_ALAW: return "pcm_alaw"; - case MimeTypes.VIDEO_H264: - return "h264"; - case MimeTypes.VIDEO_H265: - return "hevc"; default: return null; } diff --git a/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegVideoRenderer.java b/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegVideoRenderer.java deleted file mode 100644 index 395b21b33f..0000000000 --- a/libraries/decoder_ffmpeg/src/main/java/androidx/media3/decoder/ffmpeg/FfmpegVideoRenderer.java +++ /dev/null @@ -1,136 +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. - */ -package androidx.media3.decoder.ffmpeg; - -import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_MIME_TYPE_CHANGED; -import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_NO; -import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION; - -import android.os.Handler; -import android.view.Surface; -import androidx.annotation.Nullable; -import androidx.media3.common.C; -import androidx.media3.common.Format; -import androidx.media3.common.util.TraceUtil; -import androidx.media3.common.util.UnstableApi; -import androidx.media3.common.util.Util; -import androidx.media3.decoder.CryptoConfig; -import androidx.media3.decoder.Decoder; -import androidx.media3.decoder.DecoderInputBuffer; -import androidx.media3.decoder.VideoDecoderOutputBuffer; -import androidx.media3.exoplayer.DecoderReuseEvaluation; -import androidx.media3.exoplayer.RendererCapabilities; -import androidx.media3.exoplayer.video.DecoderVideoRenderer; -import androidx.media3.exoplayer.video.VideoRendererEventListener; - -// TODO: Remove the NOTE below. -/** - * NOTE: This class if under development and is not yet functional. - * - *

Decodes and renders video using FFmpeg. - */ -@UnstableApi -public final class FfmpegVideoRenderer extends DecoderVideoRenderer { - - private static final String TAG = "FfmpegVideoRenderer"; - - /** - * Creates a new instance. - * - * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer - * can attempt to seamlessly join an ongoing playback. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between - * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - */ - public FfmpegVideoRenderer( - long allowedJoiningTimeMs, - @Nullable Handler eventHandler, - @Nullable VideoRendererEventListener eventListener, - int maxDroppedFramesToNotify) { - super(allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify); - // TODO: Implement. - } - - @Override - public String getName() { - return TAG; - } - - @Override - public final @RendererCapabilities.Capabilities int supportsFormat(Format format) { - // TODO: Remove this line and uncomment the implementation below. - return C.FORMAT_UNSUPPORTED_TYPE; - /* - String mimeType = Assertions.checkNotNull(format.sampleMimeType); - if (!FfmpegLibrary.isAvailable() || !MimeTypes.isVideo(mimeType)) { - return FORMAT_UNSUPPORTED_TYPE; - } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType)) { - return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); - } else if (format.exoMediaCryptoType != null) { - return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); - } else { - return RendererCapabilities.create( - FORMAT_HANDLED, - ADAPTIVE_SEAMLESS, - TUNNELING_NOT_SUPPORTED); - } - */ - } - - @SuppressWarnings("nullness:return") - @Override - protected Decoder - createDecoder(Format format, @Nullable CryptoConfig cryptoConfig) - throws FfmpegDecoderException { - TraceUtil.beginSection("createFfmpegVideoDecoder"); - // TODO: Implement, remove the SuppressWarnings annotation, and update the return type to use - // the concrete type of the decoder (probably FfmepgVideoDecoder). - TraceUtil.endSection(); - return null; - } - - @Override - protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface) - throws FfmpegDecoderException { - // TODO: Implement. - } - - @Override - protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) { - // TODO: Uncomment the implementation below. - /* - if (decoder != null) { - decoder.setOutputMode(outputMode); - } - */ - } - - @Override - protected DecoderReuseEvaluation canReuseDecoder( - String decoderName, Format oldFormat, Format newFormat) { - boolean sameMimeType = Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType); - // TODO: Ability to reuse the decoder may be MIME type dependent. - return new DecoderReuseEvaluation( - decoderName, - oldFormat, - newFormat, - sameMimeType ? REUSE_RESULT_YES_WITHOUT_RECONFIGURATION : REUSE_RESULT_NO, - sameMimeType ? 0 : DISCARD_REASON_MIME_TYPE_CHANGED); - } -} diff --git a/libraries/decoder_ffmpeg/src/test/java/androidx/media3/decoder/ffmpeg/DefaultRenderersFactoryTest.java b/libraries/decoder_ffmpeg/src/test/java/androidx/media3/decoder/ffmpeg/DefaultRenderersFactoryTest.java index a393d7d826..60fe068004 100644 --- a/libraries/decoder_ffmpeg/src/test/java/androidx/media3/decoder/ffmpeg/DefaultRenderersFactoryTest.java +++ b/libraries/decoder_ffmpeg/src/test/java/androidx/media3/decoder/ffmpeg/DefaultRenderersFactoryTest.java @@ -21,10 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; -/** - * Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer} and {@link - * FfmpegVideoRenderer}. - */ +/** Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}. */ @RunWith(AndroidJUnit4.class) public final class DefaultRenderersFactoryTest { @@ -33,10 +30,4 @@ public final class DefaultRenderersFactoryTest { DefaultRenderersFactoryAsserts.assertExtensionRendererCreated( FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO); } - - @Test - public void createRenderers_instantiatesFfmpegVideoRenderer() { - DefaultRenderersFactoryAsserts.assertExtensionRendererCreated( - FfmpegVideoRenderer.class, C.TRACK_TYPE_VIDEO); - } } diff --git a/libraries/exoplayer/proguard-rules.txt b/libraries/exoplayer/proguard-rules.txt index 2a4d945bca..8a9222e52e 100644 --- a/libraries/exoplayer/proguard-rules.txt +++ b/libraries/exoplayer/proguard-rules.txt @@ -9,10 +9,6 @@ -keepclassmembers class androidx.media3.decoder.av1.Libgav1VideoRenderer { (long, android.os.Handler, androidx.media3.exoplayer.video.VideoRendererEventListener, int); } --dontnote androidx.media3.decoder.ffmpeg.FfmpegVideoRenderer --keepclassmembers class androidx.media3.decoder.ffmpeg.FfmpegVideoRenderer { - (long, android.os.Handler, androidx.media3.exoplayer.video.VideoRendererEventListener, int); -} -dontnote androidx.media3.decoder.opus.LibopusAudioRenderer -keepclassmembers class androidx.media3.decoder.opus.LibopusAudioRenderer { (android.os.Handler, androidx.media3.exoplayer.audio.AudioRendererEventListener, androidx.media3.exoplayer.audio.AudioSink); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java index e78a2f1b03..46e80d2e9f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java @@ -415,31 +415,6 @@ public class DefaultRenderersFactory implements RenderersFactory { // The extension is present, but instantiation failed. throw new RuntimeException("Error instantiating AV1 extension", e); } - - try { - // Full class names used for constructor args so the LINT rule triggers if any of them move. - Class clazz = Class.forName("androidx.media3.decoder.ffmpeg.FfmpegVideoRenderer"); - Constructor constructor = - clazz.getConstructor( - long.class, - android.os.Handler.class, - androidx.media3.exoplayer.video.VideoRendererEventListener.class, - int.class); - Renderer renderer = - (Renderer) - constructor.newInstance( - allowedVideoJoiningTimeMs, - eventHandler, - eventListener, - MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); - out.add(extensionRendererIndex++, renderer); - Log.i(TAG, "Loaded FfmpegVideoRenderer."); - } catch (ClassNotFoundException e) { - // Expected if the app was built without the extension. - } catch (Exception e) { - // The extension is present, but instantiation failed. - throw new RuntimeException("Error instantiating FFmpeg extension", e); - } } /** From 711409ab5128109d262413fe4e4cd5d716cfac0d Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 30 May 2022 17:16:59 +0000 Subject: [PATCH 02/45] Update the Gradle Wrapper and Android Gradle Plugin The current verion of AGP warns it doesn't support Android API 32 [1]. The wrapper was upgraded with ([instructions](https://docs.gradle.org/current/userguide/gradle_wrapper.html#sec:upgrading_wrapper)): ```shell $ ./gradlew wrapper --gradle-version 7.4.2 --distribution-type all ``` [1] ``` WARNING:We recommend using a newer Android Gradle plugin to use compileSdk = 32 This Android Gradle plugin (7.0.3) was tested up to compileSdk = 31 This warning can be suppressed by adding android.suppressUnsupportedCompileSdk=32 to this project's gradle.properties The build will continue, but you are strongly encouraged to update your project to use a newer Android Gradle Plugin that has been tested with compileSdk = 32 ``` #minor-release PiperOrigin-RevId: 451893214 (cherry picked from commit 3ba9d7e125482aae236f2e9e60de7f8535d9f162) --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index aafc0e790c..0c15bce9e5 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.2.1' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.2' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b1159fc54f..92f06b50fd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 00ae09f4599ee11860d30daee96f4fdeb35ab5f8 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 31 May 2022 09:19:29 +0000 Subject: [PATCH 03/45] Add explicit cast to ByteBuffer for Java 8 compatibility PiperOrigin-RevId: 451994696 (cherry picked from commit 5cdac6575e662bf575b4890f17b600e7b17bffac) --- .../java/androidx/media3/decoder/opus/OpusDecoderTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/decoder_opus/src/test/java/androidx/media3/decoder/opus/OpusDecoderTest.java b/libraries/decoder_opus/src/test/java/androidx/media3/decoder/opus/OpusDecoderTest.java index 59581dc6a6..f166a511c8 100644 --- a/libraries/decoder_opus/src/test/java/androidx/media3/decoder/opus/OpusDecoderTest.java +++ b/libraries/decoder_opus/src/test/java/androidx/media3/decoder/opus/OpusDecoderTest.java @@ -200,8 +200,12 @@ public final class OpusDecoderTest { return ImmutableList.of(HEADER, preSkip, CUSTOM_SEEK_PRE_ROLL_BYTES); } + // The cast to ByteBuffer is required for Java 8 compatibility. See + // https://issues.apache.org/jira/browse/MRESOLVER-85 + @SuppressWarnings("UnnecessaryCast") private static ByteBuffer createSupplementalData(long value) { - return ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).rewind(); + return (ByteBuffer) + ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).rewind(); } private static DecoderInputBuffer createInputBuffer( From af91fdbf54b0f73b53db8e9f042a308017e5d7cc Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 31 May 2022 10:09:10 +0000 Subject: [PATCH 04/45] Add `@deprecated` javadoc to all `@Deprecated` `@Override` methods This ensures the 'use X instead' message is easily visible in the generated HTML for the overriding method. Currently it's not, e.g.: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/BasePlayer.html#getCurrentWindowIndex() #minor-release PiperOrigin-RevId: 452002224 (cherry picked from commit b8ca5b8951325ed89063b2e30e1c7fd3b78f5e97) --- .../java/androidx/media3/cast/CastPlayer.java | 5 + .../androidx/media3/common/BasePlayer.java | 42 +++++++++ .../media3/common/ForwardingPlayer.java | 92 ++++++++++++++++--- .../datasource/cronet/CronetDataSource.java | 6 +- .../media3/exoplayer/SimpleExoPlayer.java | 37 ++++++++ .../trackselection/DefaultTrackSelector.java | 3 + .../media3/session/MediaController.java | 47 ++++++++++ .../androidx/media3/session/MockPlayer.java | 47 ++++++++++ .../media3/test/utils/StubExoPlayer.java | 37 ++++++++ .../media3/test/utils/StubPlayer.java | 5 + 10 files changed, 301 insertions(+), 20 deletions(-) diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java index 3ab3878864..ad21e461e7 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java @@ -458,6 +458,11 @@ public final class CastPlayer extends BasePlayer { stop(/* reset= */ false); } + /** + * @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or + * just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when + * {@link #prepare() re-preparing} the player. + */ @Deprecated @Override public void stop(boolean reset) { diff --git a/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java index f45f131812..fc5a96c96e 100644 --- a/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java @@ -143,12 +143,18 @@ public abstract class BasePlayer implements Player { seekToOffset(getSeekForwardIncrement()); } + /** + * @deprecated Use {@link #hasPreviousMediaItem()} instead. + */ @Deprecated @Override public final boolean hasPrevious() { return hasPreviousMediaItem(); } + /** + * @deprecated Use {@link #hasPreviousMediaItem()} instead. + */ @Deprecated @Override public final boolean hasPreviousWindow() { @@ -160,12 +166,18 @@ public abstract class BasePlayer implements Player { return getPreviousMediaItemIndex() != C.INDEX_UNSET; } + /** + * @deprecated Use {@link #seekToPreviousMediaItem()} instead. + */ @Deprecated @Override public final void previous() { seekToPreviousMediaItem(); } + /** + * @deprecated Use {@link #seekToPreviousMediaItem()} instead. + */ @Deprecated @Override public final void seekToPreviousWindow() { @@ -198,12 +210,18 @@ public abstract class BasePlayer implements Player { } } + /** + * @deprecated Use {@link #hasNextMediaItem()} instead. + */ @Deprecated @Override public final boolean hasNext() { return hasNextMediaItem(); } + /** + * @deprecated Use {@link #hasNextMediaItem()} instead. + */ @Deprecated @Override public final boolean hasNextWindow() { @@ -215,12 +233,18 @@ public abstract class BasePlayer implements Player { return getNextMediaItemIndex() != C.INDEX_UNSET; } + /** + * @deprecated Use {@link #seekToNextMediaItem()} instead. + */ @Deprecated @Override public final void next() { seekToNextMediaItem(); } + /** + * @deprecated Use {@link #seekToNextMediaItem()} instead. + */ @Deprecated @Override public final void seekToNextWindow() { @@ -253,12 +277,18 @@ public abstract class BasePlayer implements Player { setPlaybackParameters(getPlaybackParameters().withSpeed(speed)); } + /** + * @deprecated Use {@link #getCurrentMediaItemIndex()} instead. + */ @Deprecated @Override public final int getCurrentWindowIndex() { return getCurrentMediaItemIndex(); } + /** + * @deprecated Use {@link #getNextMediaItemIndex()} instead. + */ @Deprecated @Override public final int getNextWindowIndex() { @@ -274,6 +304,9 @@ public abstract class BasePlayer implements Player { getCurrentMediaItemIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled()); } + /** + * @deprecated Use {@link #getPreviousMediaItemIndex()} instead. + */ @Deprecated @Override public final int getPreviousWindowIndex() { @@ -326,6 +359,9 @@ public abstract class BasePlayer implements Player { : duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100); } + /** + * @deprecated Use {@link #isCurrentMediaItemDynamic()} instead. + */ @Deprecated @Override public final boolean isCurrentWindowDynamic() { @@ -338,6 +374,9 @@ public abstract class BasePlayer implements Player { return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isDynamic; } + /** + * @deprecated Use {@link #isCurrentMediaItemLive()} instead. + */ @Deprecated @Override public final boolean isCurrentWindowLive() { @@ -364,6 +403,9 @@ public abstract class BasePlayer implements Player { return window.getCurrentUnixTimeMs() - window.windowStartTimeMs - getContentPosition(); } + /** + * @deprecated Use {@link #isCurrentMediaItemSeekable()} instead. + */ @Deprecated @Override public final boolean isCurrentWindowSeekable() { diff --git a/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java b/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java index b6fb7111df..5975897b40 100644 --- a/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java @@ -299,7 +299,11 @@ public class ForwardingPlayer implements Player { player.seekForward(); } - /** Calls {@link Player#hasPrevious()} on the delegate and returns the result. */ + /** + * Calls {@link Player#hasPrevious()} on the delegate and returns the result. + * + * @deprecated Use {@link #hasPreviousMediaItem()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -307,7 +311,11 @@ public class ForwardingPlayer implements Player { return player.hasPrevious(); } - /** Calls {@link Player#hasPreviousWindow()} on the delegate and returns the result. */ + /** + * Calls {@link Player#hasPreviousWindow()} on the delegate and returns the result. + * + * @deprecated Use {@link #hasPreviousMediaItem()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -321,7 +329,11 @@ public class ForwardingPlayer implements Player { return player.hasPreviousMediaItem(); } - /** Calls {@link Player#previous()} on the delegate. */ + /** + * Calls {@link Player#previous()} on the delegate. + * + * @deprecated Use {@link #seekToPreviousMediaItem()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -329,7 +341,11 @@ public class ForwardingPlayer implements Player { player.previous(); } - /** Calls {@link Player#seekToPreviousWindow()} on the delegate. */ + /** + * Calls {@link Player#seekToPreviousWindow()} on the delegate. + * + * @deprecated Use {@link #seekToPreviousMediaItem()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -355,7 +371,11 @@ public class ForwardingPlayer implements Player { return player.getMaxSeekToPreviousPosition(); } - /** Calls {@link Player#hasNext()} on the delegate and returns the result. */ + /** + * Calls {@link Player#hasNext()} on the delegate and returns the result. + * + * @deprecated Use {@link #hasNextMediaItem()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -363,7 +383,11 @@ public class ForwardingPlayer implements Player { return player.hasNext(); } - /** Calls {@link Player#hasNextWindow()} on the delegate and returns the result. */ + /** + * Calls {@link Player#hasNextWindow()} on the delegate and returns the result. + * + * @deprecated Use {@link #hasNextMediaItem()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -377,7 +401,11 @@ public class ForwardingPlayer implements Player { return player.hasNextMediaItem(); } - /** Calls {@link Player#next()} on the delegate. */ + /** + * Calls {@link Player#next()} on the delegate. + * + * @deprecated Use {@link #seekToNextMediaItem()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -385,7 +413,11 @@ public class ForwardingPlayer implements Player { player.next(); } - /** Calls {@link Player#seekToNextWindow()} on the delegate. */ + /** + * Calls {@link Player#seekToNextWindow()} on the delegate. + * + * @deprecated Use {@link #seekToNextMediaItem()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -429,7 +461,13 @@ public class ForwardingPlayer implements Player { player.stop(); } - /** Calls {@link Player#stop(boolean)} on the delegate. */ + /** + * Calls {@link Player#stop(boolean)} on the delegate. + * + * @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or + * just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when + * {@link #prepare() re-preparing} the player. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -498,7 +536,11 @@ public class ForwardingPlayer implements Player { return player.getCurrentPeriodIndex(); } - /** Calls {@link Player#getCurrentWindowIndex()} on the delegate and returns the result. */ + /** + * Calls {@link Player#getCurrentWindowIndex()} on the delegate and returns the result. + * + * @deprecated Use {@link #getCurrentMediaItemIndex()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -512,7 +554,11 @@ public class ForwardingPlayer implements Player { return player.getCurrentMediaItemIndex(); } - /** Calls {@link Player#getNextWindowIndex()} on the delegate and returns the result. */ + /** + * Calls {@link Player#getNextWindowIndex()} on the delegate and returns the result. + * + * @deprecated Use {@link #getNextMediaItemIndex()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -526,7 +572,11 @@ public class ForwardingPlayer implements Player { return player.getNextMediaItemIndex(); } - /** Calls {@link Player#getPreviousWindowIndex()} on the delegate and returns the result. */ + /** + * Calls {@link Player#getPreviousWindowIndex()} on the delegate and returns the result. + * + * @deprecated Use {@link #getPreviousMediaItemIndex()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -589,7 +639,11 @@ public class ForwardingPlayer implements Player { return player.getTotalBufferedDuration(); } - /** Calls {@link Player#isCurrentWindowDynamic()} on the delegate and returns the result. */ + /** + * Calls {@link Player#isCurrentWindowDynamic()} on the delegate and returns the result. + * + * @deprecated Use {@link #isCurrentMediaItemDynamic()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -603,7 +657,11 @@ public class ForwardingPlayer implements Player { return player.isCurrentMediaItemDynamic(); } - /** Calls {@link Player#isCurrentWindowLive()} on the delegate and returns the result. */ + /** + * Calls {@link Player#isCurrentWindowLive()} on the delegate and returns the result. + * + * @deprecated Use {@link #isCurrentMediaItemLive()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override @@ -623,7 +681,11 @@ public class ForwardingPlayer implements Player { return player.getCurrentLiveOffset(); } - /** Calls {@link Player#isCurrentWindowSeekable()} on the delegate and returns the result. */ + /** + * Calls {@link Player#isCurrentWindowSeekable()} on the delegate and returns the result. + * + * @deprecated Use {@link #isCurrentMediaItemSeekable()} instead. + */ @SuppressWarnings("deprecation") // Forwarding to deprecated method @Deprecated @Override diff --git a/libraries/datasource_cronet/src/main/java/androidx/media3/datasource/cronet/CronetDataSource.java b/libraries/datasource_cronet/src/main/java/androidx/media3/datasource/cronet/CronetDataSource.java index 505a65033c..056ece7591 100644 --- a/libraries/datasource_cronet/src/main/java/androidx/media3/datasource/cronet/CronetDataSource.java +++ b/libraries/datasource_cronet/src/main/java/androidx/media3/datasource/cronet/CronetDataSource.java @@ -481,11 +481,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } /** - * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a - * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. - * - * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a - * predicate that was previously set. + * @deprecated Use {@link CronetDataSource.Factory#setContentTypePredicate(Predicate)} instead. */ @UnstableApi @Deprecated diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java index 3d4a315ab3..3dfd26d19b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java @@ -33,6 +33,7 @@ import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.PlaybackParameters; +import androidx.media3.common.Player; import androidx.media3.common.PriorityTaskManager; import androidx.media3.common.Timeline; import androidx.media3.common.TrackSelectionParameters; @@ -426,24 +427,44 @@ public class SimpleExoPlayer extends BasePlayer return player.experimentalIsSleepingForOffload(); } + /** + * @deprecated Use {@link ExoPlayer}, as the {@link AudioComponent} methods are defined by that + * interface. + */ + @Deprecated @Override @Nullable public AudioComponent getAudioComponent() { return this; } + /** + * @deprecated Use {@link ExoPlayer}, as the {@link VideoComponent} methods are defined by that + * interface. + */ + @Deprecated @Override @Nullable public VideoComponent getVideoComponent() { return this; } + /** + * @deprecated Use {@link Player}, as the {@link TextComponent} methods are defined by that + * interface. + */ + @Deprecated @Override @Nullable public TextComponent getTextComponent() { return this; } + /** + * @deprecated Use {@link Player}, as the {@link DeviceComponent} methods are defined by that + * interface. + */ + @Deprecated @Override @Nullable public DeviceComponent getDeviceComponent() { @@ -1003,6 +1024,11 @@ public class SimpleExoPlayer extends BasePlayer player.stop(); } + /** + * @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or + * just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when + * {@link #prepare() re-preparing} the player. + */ @Deprecated @Override public void stop(boolean reset) { @@ -1046,12 +1072,20 @@ public class SimpleExoPlayer extends BasePlayer return player.getTrackSelector(); } + /** + * @deprecated Use {@link #getCurrentTracks()}. + */ + @Deprecated @Override public TrackGroupArray getCurrentTrackGroups() { blockUntilConstructorFinished(); return player.getCurrentTrackGroups(); } + /** + * @deprecated Use {@link #getCurrentTracks()}. + */ + @Deprecated @Override public TrackSelectionArray getCurrentTrackSelections() { blockUntilConstructorFinished(); @@ -1166,6 +1200,9 @@ public class SimpleExoPlayer extends BasePlayer return player.getContentBufferedPosition(); } + /** + * @deprecated Use {@link #setWakeMode(int)} instead. + */ @Deprecated @Override public void setHandleWakeLock(boolean handleWakeLock) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index 35b886927e..8e45ec3135 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -616,6 +616,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + /** + * @deprecated Use {@link #setTrackTypeDisabled(int, boolean)}. + */ @Override @Deprecated @SuppressWarnings("deprecation") diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 8f5a0424b7..348750b813 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -401,6 +401,11 @@ public class MediaController implements Player { impl.stop(); } + /** + * @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or + * just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when + * {@link #prepare() re-preparing} the player. + */ @UnstableApi @Deprecated @Override @@ -1185,6 +1190,9 @@ public class MediaController implements Player { impl.moveMediaItems(fromIndex, toIndex, newIndex); } + /** + * @deprecated Use {@link #isCurrentMediaItemDynamic()} instead. + */ @UnstableApi @Deprecated @Override @@ -1199,6 +1207,9 @@ public class MediaController implements Player { return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isDynamic; } + /** + * @deprecated Use {@link #isCurrentMediaItemLive()} instead. + */ @UnstableApi @Deprecated @Override @@ -1213,6 +1224,9 @@ public class MediaController implements Player { return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isLive(); } + /** + * @deprecated Use {@link #isCurrentMediaItemSeekable()} instead. + */ @UnstableApi @Deprecated @Override @@ -1262,6 +1276,9 @@ public class MediaController implements Player { return isConnected() ? impl.getCurrentPeriodIndex() : C.INDEX_UNSET; } + /** + * @deprecated Use {@link #getCurrentMediaItemIndex()} instead. + */ @UnstableApi @Deprecated @Override @@ -1275,6 +1292,9 @@ public class MediaController implements Player { return isConnected() ? impl.getCurrentMediaItemIndex() : C.INDEX_UNSET; } + /** + * @deprecated Use {@link #getPreviousMediaItemIndex()} instead. + */ @UnstableApi @Deprecated @Override @@ -1295,6 +1315,9 @@ public class MediaController implements Player { return isConnected() ? impl.getPreviousMediaItemIndex() : C.INDEX_UNSET; } + /** + * @deprecated Use {@link #getNextMediaItemIndex()} instead. + */ @UnstableApi @Deprecated @Override @@ -1315,6 +1338,9 @@ public class MediaController implements Player { return isConnected() ? impl.getNextMediaItemIndex() : C.INDEX_UNSET; } + /** + * @deprecated Use {@link #hasPreviousMediaItem()} instead. + */ @UnstableApi @Deprecated @Override @@ -1322,6 +1348,9 @@ public class MediaController implements Player { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #hasNextMediaItem()} instead. + */ @UnstableApi @Deprecated @Override @@ -1329,6 +1358,9 @@ public class MediaController implements Player { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #hasPreviousMediaItem()} instead. + */ @UnstableApi @Deprecated @Override @@ -1336,6 +1368,9 @@ public class MediaController implements Player { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #hasNextMediaItem()} instead. + */ @UnstableApi @Deprecated @Override @@ -1355,6 +1390,9 @@ public class MediaController implements Player { return isConnected() && impl.hasNextMediaItem(); } + /** + * @deprecated Use {@link #seekToPreviousMediaItem()} instead. + */ @UnstableApi @Deprecated @Override @@ -1362,6 +1400,9 @@ public class MediaController implements Player { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #seekToNextMediaItem()} instead. + */ @UnstableApi @Deprecated @Override @@ -1369,6 +1410,9 @@ public class MediaController implements Player { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #seekToPreviousMediaItem()} instead. + */ @UnstableApi @Deprecated @Override @@ -1392,6 +1436,9 @@ public class MediaController implements Player { impl.seekToPreviousMediaItem(); } + /** + * @deprecated Use {@link #seekToNextMediaItem()} instead. + */ @UnstableApi @Deprecated @Override diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java index 693a380119..d9a9258b65 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java @@ -310,6 +310,11 @@ public class MockPlayer implements Player { checkNotNull(conditionVariables.get(METHOD_STOP)).open(); } + /** + * @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or + * just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when + * {@link #prepare() re-preparing} the player. + */ @Deprecated @Override public void stop(boolean reset) { @@ -758,6 +763,9 @@ public class MockPlayer implements Player { checkNotNull(conditionVariables.get(METHOD_SET_PLAYLIST_METADATA)).open(); } + /** + * @deprecated Use {@link #isCurrentMediaItemDynamic()} instead. + */ @Deprecated @Override public boolean isCurrentWindowDynamic() { @@ -769,6 +777,9 @@ public class MockPlayer implements Player { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #isCurrentMediaItemLive()} instead. + */ @Deprecated @Override public boolean isCurrentWindowLive() { @@ -780,6 +791,9 @@ public class MockPlayer implements Player { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #isCurrentMediaItemSeekable()} instead. + */ @Deprecated @Override public boolean isCurrentWindowSeekable() { @@ -815,6 +829,9 @@ public class MockPlayer implements Player { return currentPeriodIndex; } + /** + * @deprecated Use {@link #getCurrentMediaItemIndex()} instead. + */ @Deprecated @Override public int getCurrentWindowIndex() { @@ -826,6 +843,9 @@ public class MockPlayer implements Player { return currentMediaItemIndex; } + /** + * @deprecated Use {@link #getPreviousMediaItemIndex()} instead. + */ @Deprecated @Override public int getPreviousWindowIndex() { @@ -837,6 +857,9 @@ public class MockPlayer implements Player { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #getNextMediaItemIndex()} instead. + */ @Deprecated @Override public int getNextWindowIndex() { @@ -912,24 +935,36 @@ public class MockPlayer implements Player { checkNotNull(conditionVariables.get(METHOD_MOVE_MEDIA_ITEMS)).open(); } + /** + * @deprecated Use {@link #hasPreviousMediaItem()} instead. + */ @Deprecated @Override public boolean hasPrevious() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #hasNextMediaItem()} instead. + */ @Deprecated @Override public boolean hasNext() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #hasPreviousMediaItem()} instead. + */ @Deprecated @Override public boolean hasPreviousWindow() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #hasNextMediaItem()} instead. + */ @Deprecated @Override public boolean hasNextWindow() { @@ -946,24 +981,36 @@ public class MockPlayer implements Player { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #seekToPreviousMediaItem()} instead. + */ @Deprecated @Override public void previous() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #seekToNextMediaItem()} instead. + */ @Deprecated @Override public void next() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #seekToPreviousMediaItem()} instead. + */ @Deprecated @Override public void seekToPreviousWindow() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #seekToNextMediaItem()} instead. + */ @Deprecated @Override public void seekToNextWindow() { diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java index ee75dc9d73..28840d6ae0 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java @@ -20,6 +20,7 @@ import androidx.annotation.Nullable; import androidx.media3.common.AudioAttributes; import androidx.media3.common.AuxEffectInfo; import androidx.media3.common.Format; +import androidx.media3.common.Player; import androidx.media3.common.PriorityTaskManager; import androidx.media3.common.util.Clock; import androidx.media3.common.util.UnstableApi; @@ -47,24 +48,40 @@ import java.util.List; @UnstableApi public class StubExoPlayer extends StubPlayer implements ExoPlayer { + /** + * @deprecated Use {@link ExoPlayer}, as the {@link AudioComponent} methods are defined by that + * interface. + */ @Override @Deprecated public AudioComponent getAudioComponent() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link ExoPlayer}, as the {@link VideoComponent} methods are defined by that + * interface. + */ @Override @Deprecated public VideoComponent getVideoComponent() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link Player}, as the {@link TextComponent} methods are defined by that + * interface. + */ @Override @Deprecated public TextComponent getTextComponent() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link Player}, as the {@link DeviceComponent} methods are defined by that + * interface. + */ @Override @Deprecated public DeviceComponent getDeviceComponent() { @@ -111,18 +128,27 @@ public class StubExoPlayer extends StubPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #prepare()} instead. + */ @Deprecated @Override public void retry() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #setMediaSource(MediaSource)} and {@link #prepare()} instead. + */ @Deprecated @Override public void prepare(MediaSource mediaSource) { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link #prepare()} instead. + */ @Deprecated @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { @@ -296,11 +322,19 @@ public class StubExoPlayer extends StubPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #getCurrentTracks()}. + */ + @Deprecated @Override public TrackGroupArray getCurrentTrackGroups() { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #getCurrentTracks()}. + */ + @Deprecated @Override public TrackSelectionArray getCurrentTrackSelections() { throw new UnsupportedOperationException(); @@ -350,6 +384,9 @@ public class StubExoPlayer extends StubPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #setWakeMode(int)} instead. + */ @Deprecated @Override public void setHandleWakeLock(boolean handleWakeLock) { diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java index 5787019136..85569df265 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java @@ -179,6 +179,11 @@ public class StubPlayer extends BasePlayer { throw new UnsupportedOperationException(); } + /** + * @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or + * just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when + * {@link #prepare() re-preparing} the player. + */ @Deprecated @Override public void stop(boolean reset) { From ad5788ef3724042229cbc72fee235b2a0e10e18e Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 31 May 2022 10:26:11 +0000 Subject: [PATCH 05/45] Use resource ID from drawable.xml as the notification icon Issue: androidx/media#66 Issue: androidx/media#65 #minor-release PiperOrigin-RevId: 452004492 (cherry picked from commit 839d4f43906f99002e3be71cdb7f90efb231754a) --- .../DefaultMediaNotificationProvider.java | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index 710070b2ef..dc88c45510 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -79,6 +79,8 @@ import java.util.concurrent.ExecutionException; *

  • {@code media3_notification_pause} - The pause icon. *
  • {@code media3_notification_seek_to_previous} - The previous icon. *
  • {@code media3_notification_seek_to_next} - The next icon. + *
  • {@code media3_notification_small_icon} - The {@link + * NotificationCompat.Builder#setSmallIcon(int) small icon}. * */ @UnstableApi @@ -134,10 +136,18 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi Player player = mediaSession.getPlayer(); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID); + + MediaStyle mediaStyle = new MediaStyle(); + int[] compactViewIndices = + addNotificationActions( + getMediaButtons(player.getAvailableCommands(), customLayout, player.getPlayWhenReady()), + builder, + actionFactory); + mediaStyle.setShowActionsInCompactView(compactViewIndices); + // Set metadata info in the notification. MediaMetadata metadata = player.getMediaMetadata(); builder.setContentTitle(metadata.title).setContentText(metadata.artist); - @Nullable ListenableFuture bitmapFuture = loadArtworkBitmap(metadata); if (bitmapFuture != null) { if (bitmapFuture.isDone()) { @@ -161,13 +171,6 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi } } - MediaStyle mediaStyle = new MediaStyle(); - int[] compactViewIndices = - addNotificationActions( - getMediaButtons(player.getAvailableCommands(), customLayout, player.getPlayWhenReady()), - builder, - actionFactory); - mediaStyle.setShowActionsInCompactView(compactViewIndices); if (player.isCommandAvailable(COMMAND_STOP) || Util.SDK_INT < 21) { // We must include a cancel intent for pre-L devices. mediaStyle.setCancelButtonIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP)); @@ -185,7 +188,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi .setContentIntent(mediaSession.getSessionActivity()) .setDeleteIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP)) .setOnlyAlertOnce(true) - .setSmallIcon(getSmallIconResId(context)) + .setSmallIcon(R.drawable.media3_notification_small_icon) .setStyle(mediaStyle) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setOngoing(false) @@ -385,15 +388,6 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi return future; } - private static int getSmallIconResId(Context context) { - int appIcon = context.getApplicationInfo().icon; - if (appIcon != 0) { - return appIcon; - } else { - return Util.SDK_INT >= 21 ? R.drawable.media_session_service_notification_ic_music_note : 0; - } - } - private static long getPlaybackStartTimeEpochMs(Player player) { // Changing "showWhen" causes notification flicker if SDK_INT < 21. if (Util.SDK_INT >= 21 From 7671e50d71ab9219a4ab7d2241810d1bcc0bbb87 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 31 May 2022 10:39:34 +0000 Subject: [PATCH 06/45] Remove deprecated calls #minor-release PiperOrigin-RevId: 452006137 (cherry picked from commit acb48a249564c5793cfadbee262e2eed29dea528) --- .../main/java/androidx/media3/demo/cast/MainActivity.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/demos/cast/src/main/java/androidx/media3/demo/cast/MainActivity.java b/demos/cast/src/main/java/androidx/media3/demo/cast/MainActivity.java index d6aec8c4a1..aa9bd9f08e 100644 --- a/demos/cast/src/main/java/androidx/media3/demo/cast/MainActivity.java +++ b/demos/cast/src/main/java/androidx/media3/demo/cast/MainActivity.java @@ -230,8 +230,8 @@ public class MainActivity extends AppCompatActivity @Override public boolean onMove( RecyclerView list, RecyclerView.ViewHolder origin, RecyclerView.ViewHolder target) { - int fromPosition = origin.getAdapterPosition(); - int toPosition = target.getAdapterPosition(); + int fromPosition = origin.getBindingAdapterPosition(); + int toPosition = target.getBindingAdapterPosition(); if (draggingFromPosition == C.INDEX_UNSET) { // A drag has started, but changes to the media queue will be reflected in clearView(). draggingFromPosition = fromPosition; @@ -243,7 +243,7 @@ public class MainActivity extends AppCompatActivity @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { - int position = viewHolder.getAdapterPosition(); + int position = viewHolder.getBindingAdapterPosition(); QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder; if (playerManager.removeItem(queueItemHolder.item)) { mediaQueueListAdapter.notifyItemRemoved(position); @@ -282,7 +282,7 @@ public class MainActivity extends AppCompatActivity @Override public void onClick(View v) { - playerManager.selectQueueItem(getAdapterPosition()); + playerManager.selectQueueItem(getBindingAdapterPosition()); } } From f6f4bf5e6bbf7b1a6875ead4152c60e5e17979d4 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 31 May 2022 11:52:14 +0000 Subject: [PATCH 07/45] Permit duplicate Opus headers This reinstates the permissive behaviour removed by https://github.com/androidx/media/commit/fe7e5b8181333683fda7869c3d9cffc0bdefcccb Test file created by opening bear.opus in a hex editor and naively duplicating the two header packets, starting at (and including) the first `OggS` in the file and ending just before the third `OggS`. #minor-release Issue: google/ExoPlayer#10038 PiperOrigin-RevId: 452015662 (cherry picked from commit 1282175808210f0496a4b18ae4e02312dbdf4553) --- RELEASENOTES.md | 4 +- .../media3/extractor/ogg/OpusReader.java | 32 +- .../ogg/OggExtractorParameterizedTest.java | 7 + .../ogg/bear_duplicate_header.opus.0.dump | 1121 +++++++++++++++++ .../ogg/bear_duplicate_header.opus.1.dump | 757 +++++++++++ .../ogg/bear_duplicate_header.opus.2.dump | 389 ++++++ .../ogg/bear_duplicate_header.opus.3.dump | 25 + ..._duplicate_header.opus.unknown_length.dump | 1118 ++++++++++++++++ .../media/ogg/bear_duplicate_header.opus | Bin 0 -> 26120 bytes 9 files changed, 3448 insertions(+), 5 deletions(-) create mode 100644 libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.0.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.1.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.2.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.3.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.unknown_length.dump create mode 100644 libraries/test_data/src/test/assets/media/ogg/bear_duplicate_header.opus diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ce5439d244..eb53d3d6d1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -56,7 +56,9 @@ subtitle format. * Extractors: * Matroska: Parse `DiscardPadding` for Opus tracks. - * Parse bitrates from `esds` boxes. + * MP4: Parse bitrates from `esds` boxes. + * Ogg: Allow duplicate Opus ID and comment headers + ([#10038](https://github.com/google/ExoPlayer/issues/10038)). * UI: * Fix delivery of events to `OnClickListener`s set on `PlayerView` and `LegacyPlayerView`, in the case that `useController=false` diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ogg/OpusReader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ogg/OpusReader.java index d9975bba3a..95996f6a80 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ogg/OpusReader.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ogg/OpusReader.java @@ -15,7 +15,6 @@ */ package androidx.media3.extractor.ogg; -import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; import androidx.annotation.Nullable; @@ -39,10 +38,20 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; 'O', 'p', 'u', 's', 'T', 'a', 'g', 's' }; + private boolean firstCommentHeaderSeen; + public static boolean verifyBitstreamType(ParsableByteArray data) { return peekPacketStartsWith(data, OPUS_ID_HEADER_SIGNATURE); } + @Override + protected void reset(boolean headerData) { + super.reset(headerData); + if (headerData) { + firstCommentHeaderSeen = false; + } + } + @Override protected long preparePayload(ParsableByteArray packet) { return convertTimeToGranule(getPacketDurationUs(packet.getData())); @@ -57,9 +66,15 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; int channelCount = OpusUtil.getChannelCount(headerBytes); List initializationData = OpusUtil.buildInitializationData(headerBytes); - // The ID header must come at the start of the file: - // https://datatracker.ietf.org/doc/html/rfc7845#section-3 - checkState(setupData.format == null); + if (setupData.format != null) { + // setupData.format being non-null indicates we've already seen an ID header. Multiple ID + // headers are not permitted by the Opus spec [1], but have been observed in real files [2], + // so we just ignore all subsequent ones. + // [1] https://datatracker.ietf.org/doc/html/rfc7845#section-3 and + // https://datatracker.ietf.org/doc/html/rfc7845#section-5 + // [2] https://github.com/google/ExoPlayer/issues/10038 + return true; + } setupData.format = new Format.Builder() .setSampleMimeType(MimeTypes.AUDIO_OPUS) @@ -72,6 +87,15 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; // The comment header must come immediately after the ID header, so the format will already // be populated: https://datatracker.ietf.org/doc/html/rfc7845#section-3 checkStateNotNull(setupData.format); + if (firstCommentHeaderSeen) { + // Multiple comment headers are not permitted by the Opus spec [1], but have been observed + // in real files [2], so we just ignore all subsequent ones. + // [1] https://datatracker.ietf.org/doc/html/rfc7845#section-3 and + // https://datatracker.ietf.org/doc/html/rfc7845#section-5 + // [2] https://github.com/google/ExoPlayer/issues/10038 + return true; + } + firstCommentHeaderSeen = true; packet.skipBytes(OPUS_COMMENT_HEADER_SIGNATURE.length); VorbisUtil.CommentHeader commentHeader = VorbisUtil.readVorbisCommentHeader( diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/ogg/OggExtractorParameterizedTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/ogg/OggExtractorParameterizedTest.java index 2b8f329ef5..0e0f76b6db 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/ogg/OggExtractorParameterizedTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/ogg/OggExtractorParameterizedTest.java @@ -43,6 +43,13 @@ public final class OggExtractorParameterizedTest { ExtractorAsserts.assertBehavior(OggExtractor::new, "media/ogg/bear.opus", simulationConfig); } + // https://github.com/google/ExoPlayer/issues/10038 + @Test + public void opus_duplicateHeader() throws Exception { + ExtractorAsserts.assertBehavior( + OggExtractor::new, "media/ogg/bear_duplicate_header.opus", simulationConfig); + } + @Test public void flac() throws Exception { ExtractorAsserts.assertBehavior(OggExtractor::new, "media/ogg/bear_flac.ogg", simulationConfig); diff --git a/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.0.dump b/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.0.dump new file mode 100644 index 0000000000..69acdcf9e9 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.0.dump @@ -0,0 +1,1121 @@ +seekMap: + isSeekable = true + duration = 2747500 + getPosition(0) = [[timeUs=0, position=250]] + getPosition(1) = [[timeUs=1, position=250]] + getPosition(1373750) = [[timeUs=1373750, position=250]] + getPosition(2747500) = [[timeUs=2747500, position=250]] +numberOfTracks = 1 +track 0: + total output bytes = 25541 + sample count = 275 + format 0: + sampleMimeType = audio/opus + channelCount = 2 + sampleRate = 48000 + metadata = entries=[VC: encoder=Lavf54.20.4] + initializationData: + data = length 19, hash BFE794DB + data = length 8, hash CA22068C + data = length 8, hash 79C07075 + sample 0: + time = 0 + flags = 1 + data = length 234, hash B77BFFDA + sample 1: + time = 10000 + flags = 1 + data = length 165, hash F7B07C35 + sample 2: + time = 20000 + flags = 1 + data = length 100, hash 21AFA81F + sample 3: + time = 30000 + flags = 1 + data = length 85, hash 9180DC2F + sample 4: + time = 40000 + flags = 1 + data = length 85, hash 6AE958C + sample 5: + time = 50000 + flags = 1 + data = length 86, hash C1C5AE60 + sample 6: + time = 60000 + flags = 1 + data = length 87, hash B9BD2620 + sample 7: + time = 70000 + flags = 1 + data = length 86, hash 5E69E6F9 + sample 8: + time = 80000 + flags = 1 + data = length 90, hash C44C7DD9 + sample 9: + time = 90000 + flags = 1 + data = length 86, hash C3FCDC6F + sample 10: + time = 100000 + flags = 1 + data = length 86, hash 44EA03BA + sample 11: + time = 110000 + flags = 1 + data = length 160, hash 9F4E1AE8 + sample 12: + time = 120000 + flags = 1 + data = length 89, hash 31234460 + sample 13: + time = 130000 + flags = 1 + data = length 91, hash 45012D79 + sample 14: + time = 140000 + flags = 1 + data = length 90, hash B3E3AC75 + sample 15: + time = 150000 + flags = 1 + data = length 87, hash B83B756B + sample 16: + time = 160000 + flags = 1 + data = length 86, hash 383921EB + sample 17: + time = 170000 + flags = 1 + data = length 97, hash 959AD270 + sample 18: + time = 180000 + flags = 1 + data = length 92, hash 46C74FA8 + sample 19: + time = 190000 + flags = 1 + data = length 91, hash CEA401DD + sample 20: + time = 200000 + flags = 1 + data = length 89, hash 48C41853 + sample 21: + time = 210000 + flags = 1 + data = length 90, hash F23245BD + sample 22: + time = 220000 + flags = 1 + data = length 96, hash 95E8985D + sample 23: + time = 230000 + flags = 1 + data = length 96, hash 34295492 + sample 24: + time = 240000 + flags = 1 + data = length 94, hash 4E3C9C0F + sample 25: + time = 250000 + flags = 1 + data = length 89, hash 28B74A29 + sample 26: + time = 260000 + flags = 1 + data = length 87, hash BAC119A7 + sample 27: + time = 270000 + flags = 1 + data = length 88, hash 7139FF38 + sample 28: + time = 280000 + flags = 1 + data = length 85, hash 246E1D2A + sample 29: + time = 290000 + flags = 1 + data = length 86, hash 488A0900 + sample 30: + time = 300000 + flags = 1 + data = length 90, hash 16FD17B1 + sample 31: + time = 310000 + flags = 1 + data = length 87, hash 20E849D9 + sample 32: + time = 320000 + flags = 1 + data = length 86, hash 23A0E9BA + sample 33: + time = 330000 + flags = 1 + data = length 87, hash EC935537 + sample 34: + time = 340000 + flags = 1 + data = length 92, hash 4D9935AD + sample 35: + time = 350000 + flags = 1 + data = length 87, hash DEDE3FA + sample 36: + time = 360000 + flags = 1 + data = length 87, hash ADC25A6E + sample 37: + time = 370000 + flags = 1 + data = length 88, hash A1C828C5 + sample 38: + time = 380000 + flags = 1 + data = length 89, hash 735C087A + sample 39: + time = 390000 + flags = 1 + data = length 89, hash 19AF5D10 + sample 40: + time = 400000 + flags = 1 + data = length 90, hash BCCEA2BB + sample 41: + time = 410000 + flags = 1 + data = length 86, hash A7C934A0 + sample 42: + time = 420000 + flags = 1 + data = length 86, hash 28BBC0A8 + sample 43: + time = 430000 + flags = 1 + data = length 85, hash E60BB12D + sample 44: + time = 440000 + flags = 1 + data = length 141, hash 1D2B8920 + sample 45: + time = 450000 + flags = 1 + data = length 121, hash 8AA3E19C + sample 46: + time = 460000 + flags = 1 + data = length 86, hash 24DF0F37 + sample 47: + time = 470000 + flags = 1 + data = length 86, hash 1D1775FF + sample 48: + time = 480000 + flags = 1 + data = length 87, hash 5230399E + sample 49: + time = 490000 + flags = 1 + data = length 91, hash 6CD98305 + sample 50: + time = 500000 + flags = 1 + data = length 88, hash 4069FBB + sample 51: + time = 510000 + flags = 1 + data = length 89, hash 76824ABF + sample 52: + time = 520000 + flags = 1 + data = length 87, hash BC1B1322 + sample 53: + time = 530000 + flags = 1 + data = length 102, hash E01BA053 + sample 54: + time = 540000 + flags = 1 + data = length 85, hash C09B626D + sample 55: + time = 550000 + flags = 1 + data = length 88, hash 6B7B404A + sample 56: + time = 560000 + flags = 1 + data = length 85, hash 74A15DC7 + sample 57: + time = 570000 + flags = 1 + data = length 88, hash 38DB82E5 + sample 58: + time = 580000 + flags = 1 + data = length 86, hash 1A39C081 + sample 59: + time = 590000 + flags = 1 + data = length 87, hash 39FEC92 + sample 60: + time = 600000 + flags = 1 + data = length 92, hash 278EA09 + sample 61: + time = 610000 + flags = 1 + data = length 87, hash 28265F2D + sample 62: + time = 620000 + flags = 1 + data = length 86, hash CC2040C6 + sample 63: + time = 630000 + flags = 1 + data = length 138, hash 9E07BC1F + sample 64: + time = 640000 + flags = 1 + data = length 85, hash 4F299670 + sample 65: + time = 650000 + flags = 1 + data = length 125, hash B61123C3 + sample 66: + time = 660000 + flags = 1 + data = length 89, hash 5CC688ED + sample 67: + time = 670000 + flags = 1 + data = length 88, hash 84AF64A6 + sample 68: + time = 680000 + flags = 1 + data = length 89, hash A9BFC8DC + sample 69: + time = 690000 + flags = 1 + data = length 90, hash 2FF77060 + sample 70: + time = 700000 + flags = 1 + data = length 96, hash E11AFD61 + sample 71: + time = 710000 + flags = 1 + data = length 87, hash 85D14EDA + sample 72: + time = 720000 + flags = 1 + data = length 88, hash 5FC71D53 + sample 73: + time = 730000 + flags = 1 + data = length 89, hash 957187B6 + sample 74: + time = 740000 + flags = 1 + data = length 89, hash 5A776082 + sample 75: + time = 750000 + flags = 1 + data = length 87, hash E8A83AFE + sample 76: + time = 760000 + flags = 1 + data = length 87, hash F1989133 + sample 77: + time = 770000 + flags = 1 + data = length 87, hash FA06BCCA + sample 78: + time = 780000 + flags = 1 + data = length 86, hash BD97E0C0 + sample 79: + time = 790000 + flags = 1 + data = length 88, hash E6AE022C + sample 80: + time = 800000 + flags = 1 + data = length 87, hash FB6C6169 + sample 81: + time = 810000 + flags = 1 + data = length 87, hash DFFCD2CF + sample 82: + time = 820000 + flags = 1 + data = length 88, hash A4B5EB52 + sample 83: + time = 830000 + flags = 1 + data = length 85, hash A63CF4EA + sample 84: + time = 840000 + flags = 1 + data = length 86, hash F126E7C7 + sample 85: + time = 850000 + flags = 1 + data = length 86, hash 21A8B22F + sample 86: + time = 860000 + flags = 1 + data = length 87, hash 6520E7C1 + sample 87: + time = 870000 + flags = 1 + data = length 88, hash 825B7423 + sample 88: + time = 880000 + flags = 1 + data = length 88, hash DF3DBD48 + sample 89: + time = 890000 + flags = 1 + data = length 87, hash B32C68D0 + sample 90: + time = 900000 + flags = 1 + data = length 89, hash B99DFFCA + sample 91: + time = 910000 + flags = 1 + data = length 88, hash 9C8D5178 + sample 92: + time = 920000 + flags = 1 + data = length 88, hash 48A0B19A + sample 93: + time = 930000 + flags = 1 + data = length 88, hash B62C94DD + sample 94: + time = 940000 + flags = 1 + data = length 92, hash 96DBDD46 + sample 95: + time = 950000 + flags = 1 + data = length 87, hash 7B80E6F + sample 96: + time = 960000 + flags = 1 + data = length 86, hash 9C60225B + sample 97: + time = 970000 + flags = 1 + data = length 87, hash 45F7E6E8 + sample 98: + time = 980000 + flags = 1 + data = length 87, hash DDC2D592 + sample 99: + time = 990000 + flags = 1 + data = length 91, hash 173D3B26 + sample 100: + time = 1000000 + flags = 1 + data = length 87, hash CF3629DF + sample 101: + time = 1010000 + flags = 1 + data = length 87, hash BBE2E7B3 + sample 102: + time = 1020000 + flags = 1 + data = length 89, hash 89AFFB10 + sample 103: + time = 1030000 + flags = 1 + data = length 88, hash 510DCC90 + sample 104: + time = 1040000 + flags = 1 + data = length 88, hash CBA56E5F + sample 105: + time = 1050000 + flags = 1 + data = length 87, hash B4B1B3FF + sample 106: + time = 1060000 + flags = 1 + data = length 89, hash B976A537 + sample 107: + time = 1070000 + flags = 1 + data = length 96, hash 43ECF2C1 + sample 108: + time = 1080000 + flags = 1 + data = length 90, hash BB7ECB44 + sample 109: + time = 1090000 + flags = 1 + data = length 89, hash B8E221A5 + sample 110: + time = 1100000 + flags = 1 + data = length 86, hash B35BEF5B + sample 111: + time = 1110000 + flags = 1 + data = length 89, hash 9002F0EC + sample 112: + time = 1120000 + flags = 1 + data = length 85, hash F694B20 + sample 113: + time = 1130000 + flags = 1 + data = length 87, hash D7CC386E + sample 114: + time = 1140000 + flags = 1 + data = length 89, hash EE9E0E79 + sample 115: + time = 1150000 + flags = 1 + data = length 90, hash CA72C96B + sample 116: + time = 1160000 + flags = 1 + data = length 112, hash 4AD3D1B1 + sample 117: + time = 1170000 + flags = 1 + data = length 87, hash FA568FAB + sample 118: + time = 1180000 + flags = 1 + data = length 90, hash 6E6948D2 + sample 119: + time = 1190000 + flags = 1 + data = length 89, hash 5465A762 + sample 120: + time = 1200000 + flags = 1 + data = length 87, hash BED19B40 + sample 121: + time = 1210000 + flags = 1 + data = length 89, hash 5D05836A + sample 122: + time = 1220000 + flags = 1 + data = length 87, hash A8A3EF5A + sample 123: + time = 1230000 + flags = 1 + data = length 90, hash 6425A77A + sample 124: + time = 1240000 + flags = 1 + data = length 92, hash 7F320FA + sample 125: + time = 1250000 + flags = 1 + data = length 89, hash 2C7837D6 + sample 126: + time = 1260000 + flags = 1 + data = length 86, hash 58D56685 + sample 127: + time = 1270000 + flags = 1 + data = length 91, hash ADC5072F + sample 128: + time = 1280000 + flags = 1 + data = length 85, hash 53EFD93 + sample 129: + time = 1290000 + flags = 1 + data = length 87, hash D006B535 + sample 130: + time = 1300000 + flags = 1 + data = length 86, hash AE944625 + sample 131: + time = 1310000 + flags = 1 + data = length 89, hash B5D3C81D + sample 132: + time = 1320000 + flags = 1 + data = length 86, hash 3BB1D0E7 + sample 133: + time = 1330000 + flags = 1 + data = length 102, hash 16EEC441 + sample 134: + time = 1340000 + flags = 1 + data = length 90, hash 1005B936 + sample 135: + time = 1350000 + flags = 1 + data = length 85, hash 15EEBF9A + sample 136: + time = 1360000 + flags = 1 + data = length 87, hash 37C83AC2 + sample 137: + time = 1370000 + flags = 1 + data = length 85, hash 2D27855D + sample 138: + time = 1380000 + flags = 1 + data = length 85, hash 753EB7C6 + sample 139: + time = 1390000 + flags = 1 + data = length 91, hash C0813318 + sample 140: + time = 1400000 + flags = 1 + data = length 89, hash 3A6468AC + sample 141: + time = 1410000 + flags = 1 + data = length 88, hash 3D220ABC + sample 142: + time = 1420000 + flags = 1 + data = length 140, hash 7949ABC7 + sample 143: + time = 1430000 + flags = 1 + data = length 92, hash F19AFA45 + sample 144: + time = 1440000 + flags = 1 + data = length 90, hash 3D21587C + sample 145: + time = 1450000 + flags = 1 + data = length 89, hash 5C12226C + sample 146: + time = 1460000 + flags = 1 + data = length 90, hash 22BA14FC + sample 147: + time = 1470000 + flags = 1 + data = length 88, hash F064B21C + sample 148: + time = 1480000 + flags = 1 + data = length 87, hash 6D7906B9 + sample 149: + time = 1490000 + flags = 1 + data = length 88, hash 6756A484 + sample 150: + time = 1500000 + flags = 1 + data = length 91, hash C95C00B6 + sample 151: + time = 1510000 + flags = 1 + data = length 87, hash 728D8119 + sample 152: + time = 1520000 + flags = 1 + data = length 90, hash C43DA1B4 + sample 153: + time = 1530000 + flags = 1 + data = length 88, hash C181BB57 + sample 154: + time = 1540000 + flags = 1 + data = length 84, hash F75B1639 + sample 155: + time = 1550000 + flags = 1 + data = length 87, hash B6F32978 + sample 156: + time = 1560000 + flags = 1 + data = length 90, hash 36D6E2D7 + sample 157: + time = 1570000 + flags = 1 + data = length 87, hash 4C9657A7 + sample 158: + time = 1580000 + flags = 1 + data = length 89, hash C3BDB9B7 + sample 159: + time = 1590000 + flags = 1 + data = length 88, hash DB51087E + sample 160: + time = 1600000 + flags = 1 + data = length 86, hash 1550F998 + sample 161: + time = 1610000 + flags = 1 + data = length 86, hash A445FAD4 + sample 162: + time = 1620000 + flags = 1 + data = length 85, hash 60D3362C + sample 163: + time = 1630000 + flags = 1 + data = length 172, hash 945D63E4 + sample 164: + time = 1640000 + flags = 1 + data = length 107, hash 585B7C04 + sample 165: + time = 1650000 + flags = 1 + data = length 110, hash 74BECF69 + sample 166: + time = 1660000 + flags = 1 + data = length 87, hash 63DE1D24 + sample 167: + time = 1670000 + flags = 1 + data = length 90, hash 1C1D28DB + sample 168: + time = 1680000 + flags = 1 + data = length 87, hash CB382A67 + sample 169: + time = 1690000 + flags = 1 + data = length 85, hash A227BA13 + sample 170: + time = 1700000 + flags = 1 + data = length 86, hash EFD8B10B + sample 171: + time = 1710000 + flags = 1 + data = length 87, hash 47FF364A + sample 172: + time = 1720000 + flags = 1 + data = length 91, hash 31D4B48A + sample 173: + time = 1730000 + flags = 1 + data = length 91, hash DD69BD85 + sample 174: + time = 1740000 + flags = 1 + data = length 88, hash AF1A95C6 + sample 175: + time = 1750000 + flags = 1 + data = length 87, hash 2FB8AF74 + sample 176: + time = 1760000 + flags = 1 + data = length 92, hash 173C707A + sample 177: + time = 1770000 + flags = 1 + data = length 88, hash 5F58F5E8 + sample 178: + time = 1780000 + flags = 1 + data = length 91, hash D449785F + sample 179: + time = 1790000 + flags = 1 + data = length 91, hash CE2CB465 + sample 180: + time = 1800000 + flags = 1 + data = length 93, hash ABC1C62E + sample 181: + time = 1810000 + flags = 1 + data = length 87, hash 83B4B9A0 + sample 182: + time = 1820000 + flags = 1 + data = length 87, hash 3220D562 + sample 183: + time = 1830000 + flags = 1 + data = length 86, hash 64D21AA1 + sample 184: + time = 1840000 + flags = 1 + data = length 86, hash A1FAAF2C + sample 185: + time = 1850000 + flags = 1 + data = length 86, hash ECA80F7E + sample 186: + time = 1860000 + flags = 1 + data = length 86, hash FEB03B2C + sample 187: + time = 1870000 + flags = 1 + data = length 85, hash 2C2E6B2F + sample 188: + time = 1880000 + flags = 1 + data = length 89, hash A0D7AC3 + sample 189: + time = 1890000 + flags = 1 + data = length 87, hash 83739547 + sample 190: + time = 1900000 + flags = 1 + data = length 86, hash 991E531E + sample 191: + time = 1910000 + flags = 1 + data = length 88, hash 16B287A3 + sample 192: + time = 1920000 + flags = 1 + data = length 86, hash FC86EED + sample 193: + time = 1930000 + flags = 1 + data = length 86, hash 96AF0248 + sample 194: + time = 1940000 + flags = 1 + data = length 86, hash 288402C8 + sample 195: + time = 1950000 + flags = 1 + data = length 87, hash 4BBA7912 + sample 196: + time = 1960000 + flags = 1 + data = length 86, hash 4A59C719 + sample 197: + time = 1970000 + flags = 1 + data = length 85, hash 906E8187 + sample 198: + time = 1980000 + flags = 1 + data = length 90, hash CB8B755D + sample 199: + time = 1990000 + flags = 1 + data = length 87, hash C8E02C + sample 200: + time = 2000000 + flags = 1 + data = length 88, hash ACF4D89A + sample 201: + time = 2010000 + flags = 1 + data = length 86, hash 510FE048 + sample 202: + time = 2020000 + flags = 1 + data = length 86, hash 64983E46 + sample 203: + time = 2030000 + flags = 1 + data = length 86, hash CEA76A1E + sample 204: + time = 2040000 + flags = 1 + data = length 87, hash AADE498E + sample 205: + time = 2050000 + flags = 1 + data = length 127, hash 353A6D8C + sample 206: + time = 2060000 + flags = 1 + data = length 87, hash 29E18E62 + sample 207: + time = 2070000 + flags = 1 + data = length 87, hash 2CF7B30F + sample 208: + time = 2080000 + flags = 1 + data = length 94, hash 758704C3 + sample 209: + time = 2090000 + flags = 1 + data = length 88, hash C2153A4C + sample 210: + time = 2100000 + flags = 1 + data = length 86, hash A0A83DA5 + sample 211: + time = 2110000 + flags = 1 + data = length 86, hash 41017D7F + sample 212: + time = 2120000 + flags = 1 + data = length 93, hash 686B0CA2 + sample 213: + time = 2130000 + flags = 1 + data = length 86, hash 554D16CC + sample 214: + time = 2140000 + flags = 1 + data = length 88, hash 99D72771 + sample 215: + time = 2150000 + flags = 1 + data = length 88, hash 7176DFBF + sample 216: + time = 2160000 + flags = 1 + data = length 86, hash BAA22669 + sample 217: + time = 2170000 + flags = 1 + data = length 88, hash B00B0D3C + sample 218: + time = 2180000 + flags = 1 + data = length 89, hash 73FED83A + sample 219: + time = 2190000 + flags = 1 + data = length 86, hash 4A4138D3 + sample 220: + time = 2200000 + flags = 1 + data = length 89, hash E0A860FF + sample 221: + time = 2210000 + flags = 1 + data = length 95, hash EE5A8AED + sample 222: + time = 2220000 + flags = 1 + data = length 92, hash 36DBD7FD + sample 223: + time = 2230000 + flags = 1 + data = length 88, hash EE47A7E4 + sample 224: + time = 2240000 + flags = 1 + data = length 100, hash 2E1A603F + sample 225: + time = 2250000 + flags = 1 + data = length 89, hash 657ED4A3 + sample 226: + time = 2260000 + flags = 1 + data = length 86, hash A833DC7B + sample 227: + time = 2270000 + flags = 1 + data = length 88, hash 81E80732 + sample 228: + time = 2280000 + flags = 1 + data = length 91, hash FA256A0F + sample 229: + time = 2290000 + flags = 1 + data = length 88, hash A63A4DBA + sample 230: + time = 2300000 + flags = 1 + data = length 88, hash 67910A9F + sample 231: + time = 2310000 + flags = 1 + data = length 86, hash EB387DB6 + sample 232: + time = 2320000 + flags = 1 + data = length 88, hash 5ACAAC2A + sample 233: + time = 2330000 + flags = 1 + data = length 86, hash 6ADF2E1F + sample 234: + time = 2340000 + flags = 1 + data = length 85, hash 9D064471 + sample 235: + time = 2350000 + flags = 1 + data = length 87, hash F176C59 + sample 236: + time = 2360000 + flags = 1 + data = length 89, hash 5CA40CE4 + sample 237: + time = 2370000 + flags = 1 + data = length 88, hash 67B944FC + sample 238: + time = 2380000 + flags = 1 + data = length 86, hash B3A84EC8 + sample 239: + time = 2390000 + flags = 1 + data = length 92, hash A6ACF94B + sample 240: + time = 2400000 + flags = 1 + data = length 88, hash CB0C9730 + sample 241: + time = 2410000 + flags = 1 + data = length 88, hash C79FE804 + sample 242: + time = 2420000 + flags = 1 + data = length 88, hash A74C7F0A + sample 243: + time = 2430000 + flags = 1 + data = length 91, hash 55F6F0A5 + sample 244: + time = 2440000 + flags = 1 + data = length 93, hash 330F33E7 + sample 245: + time = 2450000 + flags = 1 + data = length 89, hash 614AFBA0 + sample 246: + time = 2460000 + flags = 1 + data = length 87, hash 3CE4652D + sample 247: + time = 2470000 + flags = 1 + data = length 87, hash 4EFD5467 + sample 248: + time = 2480000 + flags = 1 + data = length 86, hash D81B3EB8 + sample 249: + time = 2490000 + flags = 1 + data = length 88, hash 96CB6871 + sample 250: + time = 2500000 + flags = 1 + data = length 88, hash E9DF2786 + sample 251: + time = 2510000 + flags = 1 + data = length 89, hash 2CA33D96 + sample 252: + time = 2520000 + flags = 1 + data = length 90, hash 96BDE594 + sample 253: + time = 2530000 + flags = 1 + data = length 87, hash C261493C + sample 254: + time = 2540000 + flags = 1 + data = length 86, hash D037318E + sample 255: + time = 2550000 + flags = 1 + data = length 88, hash BC15BC88 + sample 256: + time = 2560000 + flags = 1 + data = length 91, hash A8361A51 + sample 257: + time = 2570000 + flags = 1 + data = length 87, hash 4AFDB5F2 + sample 258: + time = 2580000 + flags = 1 + data = length 87, hash 6447F8CB + sample 259: + time = 2590000 + flags = 1 + data = length 89, hash 48305229 + sample 260: + time = 2600000 + flags = 1 + data = length 87, hash 8741D9E7 + sample 261: + time = 2610000 + flags = 1 + data = length 120, hash 761F020C + sample 262: + time = 2620000 + flags = 1 + data = length 139, hash AECE2E57 + sample 263: + time = 2630000 + flags = 1 + data = length 166, hash 6288797A + sample 264: + time = 2640000 + flags = 1 + data = length 144, hash 437821A0 + sample 265: + time = 2650000 + flags = 1 + data = length 113, hash FCCBEDF1 + sample 266: + time = 2660000 + flags = 1 + data = length 108, hash C4040614 + sample 267: + time = 2670000 + flags = 1 + data = length 125, hash E29064C2 + sample 268: + time = 2680000 + flags = 1 + data = length 126, hash D42D24FF + sample 269: + time = 2690000 + flags = 1 + data = length 122, hash 30AF267D + sample 270: + time = 2700000 + flags = 1 + data = length 122, hash 45CEC1FB + sample 271: + time = 2710000 + flags = 1 + data = length 134, hash 59143FE2 + sample 272: + time = 2720000 + flags = 1 + data = length 134, hash BD52A84 + sample 273: + time = 2730000 + flags = 1 + data = length 120, hash 745C3714 + sample 274: + time = 2740000 + flags = 1 + data = length 126, hash 505E117B +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.1.dump b/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.1.dump new file mode 100644 index 0000000000..9bab0eadb1 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.1.dump @@ -0,0 +1,757 @@ +seekMap: + isSeekable = true + duration = 2747500 + getPosition(0) = [[timeUs=0, position=250]] + getPosition(1) = [[timeUs=1, position=250]] + getPosition(1373750) = [[timeUs=1373750, position=250]] + getPosition(2747500) = [[timeUs=2747500, position=250]] +numberOfTracks = 1 +track 0: + total output bytes = 17031 + sample count = 184 + format 0: + sampleMimeType = audio/opus + channelCount = 2 + sampleRate = 48000 + metadata = entries=[VC: encoder=Lavf54.20.4] + initializationData: + data = length 19, hash BFE794DB + data = length 8, hash CA22068C + data = length 8, hash 79C07075 + sample 0: + time = 910000 + flags = 1 + data = length 88, hash 9C8D5178 + sample 1: + time = 920000 + flags = 1 + data = length 88, hash 48A0B19A + sample 2: + time = 930000 + flags = 1 + data = length 88, hash B62C94DD + sample 3: + time = 940000 + flags = 1 + data = length 92, hash 96DBDD46 + sample 4: + time = 950000 + flags = 1 + data = length 87, hash 7B80E6F + sample 5: + time = 960000 + flags = 1 + data = length 86, hash 9C60225B + sample 6: + time = 970000 + flags = 1 + data = length 87, hash 45F7E6E8 + sample 7: + time = 980000 + flags = 1 + data = length 87, hash DDC2D592 + sample 8: + time = 990000 + flags = 1 + data = length 91, hash 173D3B26 + sample 9: + time = 1000000 + flags = 1 + data = length 87, hash CF3629DF + sample 10: + time = 1010000 + flags = 1 + data = length 87, hash BBE2E7B3 + sample 11: + time = 1020000 + flags = 1 + data = length 89, hash 89AFFB10 + sample 12: + time = 1030000 + flags = 1 + data = length 88, hash 510DCC90 + sample 13: + time = 1040000 + flags = 1 + data = length 88, hash CBA56E5F + sample 14: + time = 1050000 + flags = 1 + data = length 87, hash B4B1B3FF + sample 15: + time = 1060000 + flags = 1 + data = length 89, hash B976A537 + sample 16: + time = 1070000 + flags = 1 + data = length 96, hash 43ECF2C1 + sample 17: + time = 1080000 + flags = 1 + data = length 90, hash BB7ECB44 + sample 18: + time = 1090000 + flags = 1 + data = length 89, hash B8E221A5 + sample 19: + time = 1100000 + flags = 1 + data = length 86, hash B35BEF5B + sample 20: + time = 1110000 + flags = 1 + data = length 89, hash 9002F0EC + sample 21: + time = 1120000 + flags = 1 + data = length 85, hash F694B20 + sample 22: + time = 1130000 + flags = 1 + data = length 87, hash D7CC386E + sample 23: + time = 1140000 + flags = 1 + data = length 89, hash EE9E0E79 + sample 24: + time = 1150000 + flags = 1 + data = length 90, hash CA72C96B + sample 25: + time = 1160000 + flags = 1 + data = length 112, hash 4AD3D1B1 + sample 26: + time = 1170000 + flags = 1 + data = length 87, hash FA568FAB + sample 27: + time = 1180000 + flags = 1 + data = length 90, hash 6E6948D2 + sample 28: + time = 1190000 + flags = 1 + data = length 89, hash 5465A762 + sample 29: + time = 1200000 + flags = 1 + data = length 87, hash BED19B40 + sample 30: + time = 1210000 + flags = 1 + data = length 89, hash 5D05836A + sample 31: + time = 1220000 + flags = 1 + data = length 87, hash A8A3EF5A + sample 32: + time = 1230000 + flags = 1 + data = length 90, hash 6425A77A + sample 33: + time = 1240000 + flags = 1 + data = length 92, hash 7F320FA + sample 34: + time = 1250000 + flags = 1 + data = length 89, hash 2C7837D6 + sample 35: + time = 1260000 + flags = 1 + data = length 86, hash 58D56685 + sample 36: + time = 1270000 + flags = 1 + data = length 91, hash ADC5072F + sample 37: + time = 1280000 + flags = 1 + data = length 85, hash 53EFD93 + sample 38: + time = 1290000 + flags = 1 + data = length 87, hash D006B535 + sample 39: + time = 1300000 + flags = 1 + data = length 86, hash AE944625 + sample 40: + time = 1310000 + flags = 1 + data = length 89, hash B5D3C81D + sample 41: + time = 1320000 + flags = 1 + data = length 86, hash 3BB1D0E7 + sample 42: + time = 1330000 + flags = 1 + data = length 102, hash 16EEC441 + sample 43: + time = 1340000 + flags = 1 + data = length 90, hash 1005B936 + sample 44: + time = 1350000 + flags = 1 + data = length 85, hash 15EEBF9A + sample 45: + time = 1360000 + flags = 1 + data = length 87, hash 37C83AC2 + sample 46: + time = 1370000 + flags = 1 + data = length 85, hash 2D27855D + sample 47: + time = 1380000 + flags = 1 + data = length 85, hash 753EB7C6 + sample 48: + time = 1390000 + flags = 1 + data = length 91, hash C0813318 + sample 49: + time = 1400000 + flags = 1 + data = length 89, hash 3A6468AC + sample 50: + time = 1410000 + flags = 1 + data = length 88, hash 3D220ABC + sample 51: + time = 1420000 + flags = 1 + data = length 140, hash 7949ABC7 + sample 52: + time = 1430000 + flags = 1 + data = length 92, hash F19AFA45 + sample 53: + time = 1440000 + flags = 1 + data = length 90, hash 3D21587C + sample 54: + time = 1450000 + flags = 1 + data = length 89, hash 5C12226C + sample 55: + time = 1460000 + flags = 1 + data = length 90, hash 22BA14FC + sample 56: + time = 1470000 + flags = 1 + data = length 88, hash F064B21C + sample 57: + time = 1480000 + flags = 1 + data = length 87, hash 6D7906B9 + sample 58: + time = 1490000 + flags = 1 + data = length 88, hash 6756A484 + sample 59: + time = 1500000 + flags = 1 + data = length 91, hash C95C00B6 + sample 60: + time = 1510000 + flags = 1 + data = length 87, hash 728D8119 + sample 61: + time = 1520000 + flags = 1 + data = length 90, hash C43DA1B4 + sample 62: + time = 1530000 + flags = 1 + data = length 88, hash C181BB57 + sample 63: + time = 1540000 + flags = 1 + data = length 84, hash F75B1639 + sample 64: + time = 1550000 + flags = 1 + data = length 87, hash B6F32978 + sample 65: + time = 1560000 + flags = 1 + data = length 90, hash 36D6E2D7 + sample 66: + time = 1570000 + flags = 1 + data = length 87, hash 4C9657A7 + sample 67: + time = 1580000 + flags = 1 + data = length 89, hash C3BDB9B7 + sample 68: + time = 1590000 + flags = 1 + data = length 88, hash DB51087E + sample 69: + time = 1600000 + flags = 1 + data = length 86, hash 1550F998 + sample 70: + time = 1610000 + flags = 1 + data = length 86, hash A445FAD4 + sample 71: + time = 1620000 + flags = 1 + data = length 85, hash 60D3362C + sample 72: + time = 1630000 + flags = 1 + data = length 172, hash 945D63E4 + sample 73: + time = 1640000 + flags = 1 + data = length 107, hash 585B7C04 + sample 74: + time = 1650000 + flags = 1 + data = length 110, hash 74BECF69 + sample 75: + time = 1660000 + flags = 1 + data = length 87, hash 63DE1D24 + sample 76: + time = 1670000 + flags = 1 + data = length 90, hash 1C1D28DB + sample 77: + time = 1680000 + flags = 1 + data = length 87, hash CB382A67 + sample 78: + time = 1690000 + flags = 1 + data = length 85, hash A227BA13 + sample 79: + time = 1700000 + flags = 1 + data = length 86, hash EFD8B10B + sample 80: + time = 1710000 + flags = 1 + data = length 87, hash 47FF364A + sample 81: + time = 1720000 + flags = 1 + data = length 91, hash 31D4B48A + sample 82: + time = 1730000 + flags = 1 + data = length 91, hash DD69BD85 + sample 83: + time = 1740000 + flags = 1 + data = length 88, hash AF1A95C6 + sample 84: + time = 1750000 + flags = 1 + data = length 87, hash 2FB8AF74 + sample 85: + time = 1760000 + flags = 1 + data = length 92, hash 173C707A + sample 86: + time = 1770000 + flags = 1 + data = length 88, hash 5F58F5E8 + sample 87: + time = 1780000 + flags = 1 + data = length 91, hash D449785F + sample 88: + time = 1790000 + flags = 1 + data = length 91, hash CE2CB465 + sample 89: + time = 1800000 + flags = 1 + data = length 93, hash ABC1C62E + sample 90: + time = 1810000 + flags = 1 + data = length 87, hash 83B4B9A0 + sample 91: + time = 1820000 + flags = 1 + data = length 87, hash 3220D562 + sample 92: + time = 1830000 + flags = 1 + data = length 86, hash 64D21AA1 + sample 93: + time = 1840000 + flags = 1 + data = length 86, hash A1FAAF2C + sample 94: + time = 1850000 + flags = 1 + data = length 86, hash ECA80F7E + sample 95: + time = 1860000 + flags = 1 + data = length 86, hash FEB03B2C + sample 96: + time = 1870000 + flags = 1 + data = length 85, hash 2C2E6B2F + sample 97: + time = 1880000 + flags = 1 + data = length 89, hash A0D7AC3 + sample 98: + time = 1890000 + flags = 1 + data = length 87, hash 83739547 + sample 99: + time = 1900000 + flags = 1 + data = length 86, hash 991E531E + sample 100: + time = 1910000 + flags = 1 + data = length 88, hash 16B287A3 + sample 101: + time = 1920000 + flags = 1 + data = length 86, hash FC86EED + sample 102: + time = 1930000 + flags = 1 + data = length 86, hash 96AF0248 + sample 103: + time = 1940000 + flags = 1 + data = length 86, hash 288402C8 + sample 104: + time = 1950000 + flags = 1 + data = length 87, hash 4BBA7912 + sample 105: + time = 1960000 + flags = 1 + data = length 86, hash 4A59C719 + sample 106: + time = 1970000 + flags = 1 + data = length 85, hash 906E8187 + sample 107: + time = 1980000 + flags = 1 + data = length 90, hash CB8B755D + sample 108: + time = 1990000 + flags = 1 + data = length 87, hash C8E02C + sample 109: + time = 2000000 + flags = 1 + data = length 88, hash ACF4D89A + sample 110: + time = 2010000 + flags = 1 + data = length 86, hash 510FE048 + sample 111: + time = 2020000 + flags = 1 + data = length 86, hash 64983E46 + sample 112: + time = 2030000 + flags = 1 + data = length 86, hash CEA76A1E + sample 113: + time = 2040000 + flags = 1 + data = length 87, hash AADE498E + sample 114: + time = 2050000 + flags = 1 + data = length 127, hash 353A6D8C + sample 115: + time = 2060000 + flags = 1 + data = length 87, hash 29E18E62 + sample 116: + time = 2070000 + flags = 1 + data = length 87, hash 2CF7B30F + sample 117: + time = 2080000 + flags = 1 + data = length 94, hash 758704C3 + sample 118: + time = 2090000 + flags = 1 + data = length 88, hash C2153A4C + sample 119: + time = 2100000 + flags = 1 + data = length 86, hash A0A83DA5 + sample 120: + time = 2110000 + flags = 1 + data = length 86, hash 41017D7F + sample 121: + time = 2120000 + flags = 1 + data = length 93, hash 686B0CA2 + sample 122: + time = 2130000 + flags = 1 + data = length 86, hash 554D16CC + sample 123: + time = 2140000 + flags = 1 + data = length 88, hash 99D72771 + sample 124: + time = 2150000 + flags = 1 + data = length 88, hash 7176DFBF + sample 125: + time = 2160000 + flags = 1 + data = length 86, hash BAA22669 + sample 126: + time = 2170000 + flags = 1 + data = length 88, hash B00B0D3C + sample 127: + time = 2180000 + flags = 1 + data = length 89, hash 73FED83A + sample 128: + time = 2190000 + flags = 1 + data = length 86, hash 4A4138D3 + sample 129: + time = 2200000 + flags = 1 + data = length 89, hash E0A860FF + sample 130: + time = 2210000 + flags = 1 + data = length 95, hash EE5A8AED + sample 131: + time = 2220000 + flags = 1 + data = length 92, hash 36DBD7FD + sample 132: + time = 2230000 + flags = 1 + data = length 88, hash EE47A7E4 + sample 133: + time = 2240000 + flags = 1 + data = length 100, hash 2E1A603F + sample 134: + time = 2250000 + flags = 1 + data = length 89, hash 657ED4A3 + sample 135: + time = 2260000 + flags = 1 + data = length 86, hash A833DC7B + sample 136: + time = 2270000 + flags = 1 + data = length 88, hash 81E80732 + sample 137: + time = 2280000 + flags = 1 + data = length 91, hash FA256A0F + sample 138: + time = 2290000 + flags = 1 + data = length 88, hash A63A4DBA + sample 139: + time = 2300000 + flags = 1 + data = length 88, hash 67910A9F + sample 140: + time = 2310000 + flags = 1 + data = length 86, hash EB387DB6 + sample 141: + time = 2320000 + flags = 1 + data = length 88, hash 5ACAAC2A + sample 142: + time = 2330000 + flags = 1 + data = length 86, hash 6ADF2E1F + sample 143: + time = 2340000 + flags = 1 + data = length 85, hash 9D064471 + sample 144: + time = 2350000 + flags = 1 + data = length 87, hash F176C59 + sample 145: + time = 2360000 + flags = 1 + data = length 89, hash 5CA40CE4 + sample 146: + time = 2370000 + flags = 1 + data = length 88, hash 67B944FC + sample 147: + time = 2380000 + flags = 1 + data = length 86, hash B3A84EC8 + sample 148: + time = 2390000 + flags = 1 + data = length 92, hash A6ACF94B + sample 149: + time = 2400000 + flags = 1 + data = length 88, hash CB0C9730 + sample 150: + time = 2410000 + flags = 1 + data = length 88, hash C79FE804 + sample 151: + time = 2420000 + flags = 1 + data = length 88, hash A74C7F0A + sample 152: + time = 2430000 + flags = 1 + data = length 91, hash 55F6F0A5 + sample 153: + time = 2440000 + flags = 1 + data = length 93, hash 330F33E7 + sample 154: + time = 2450000 + flags = 1 + data = length 89, hash 614AFBA0 + sample 155: + time = 2460000 + flags = 1 + data = length 87, hash 3CE4652D + sample 156: + time = 2470000 + flags = 1 + data = length 87, hash 4EFD5467 + sample 157: + time = 2480000 + flags = 1 + data = length 86, hash D81B3EB8 + sample 158: + time = 2490000 + flags = 1 + data = length 88, hash 96CB6871 + sample 159: + time = 2500000 + flags = 1 + data = length 88, hash E9DF2786 + sample 160: + time = 2510000 + flags = 1 + data = length 89, hash 2CA33D96 + sample 161: + time = 2520000 + flags = 1 + data = length 90, hash 96BDE594 + sample 162: + time = 2530000 + flags = 1 + data = length 87, hash C261493C + sample 163: + time = 2540000 + flags = 1 + data = length 86, hash D037318E + sample 164: + time = 2550000 + flags = 1 + data = length 88, hash BC15BC88 + sample 165: + time = 2560000 + flags = 1 + data = length 91, hash A8361A51 + sample 166: + time = 2570000 + flags = 1 + data = length 87, hash 4AFDB5F2 + sample 167: + time = 2580000 + flags = 1 + data = length 87, hash 6447F8CB + sample 168: + time = 2590000 + flags = 1 + data = length 89, hash 48305229 + sample 169: + time = 2600000 + flags = 1 + data = length 87, hash 8741D9E7 + sample 170: + time = 2610000 + flags = 1 + data = length 120, hash 761F020C + sample 171: + time = 2620000 + flags = 1 + data = length 139, hash AECE2E57 + sample 172: + time = 2630000 + flags = 1 + data = length 166, hash 6288797A + sample 173: + time = 2640000 + flags = 1 + data = length 144, hash 437821A0 + sample 174: + time = 2650000 + flags = 1 + data = length 113, hash FCCBEDF1 + sample 175: + time = 2660000 + flags = 1 + data = length 108, hash C4040614 + sample 176: + time = 2670000 + flags = 1 + data = length 125, hash E29064C2 + sample 177: + time = 2680000 + flags = 1 + data = length 126, hash D42D24FF + sample 178: + time = 2690000 + flags = 1 + data = length 122, hash 30AF267D + sample 179: + time = 2700000 + flags = 1 + data = length 122, hash 45CEC1FB + sample 180: + time = 2710000 + flags = 1 + data = length 134, hash 59143FE2 + sample 181: + time = 2720000 + flags = 1 + data = length 134, hash BD52A84 + sample 182: + time = 2730000 + flags = 1 + data = length 120, hash 745C3714 + sample 183: + time = 2740000 + flags = 1 + data = length 126, hash 505E117B +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.2.dump b/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.2.dump new file mode 100644 index 0000000000..d1ddcab42c --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.2.dump @@ -0,0 +1,389 @@ +seekMap: + isSeekable = true + duration = 2747500 + getPosition(0) = [[timeUs=0, position=250]] + getPosition(1) = [[timeUs=1, position=250]] + getPosition(1373750) = [[timeUs=1373750, position=250]] + getPosition(2747500) = [[timeUs=2747500, position=250]] +numberOfTracks = 1 +track 0: + total output bytes = 8698 + sample count = 92 + format 0: + sampleMimeType = audio/opus + channelCount = 2 + sampleRate = 48000 + metadata = entries=[VC: encoder=Lavf54.20.4] + initializationData: + data = length 19, hash BFE794DB + data = length 8, hash CA22068C + data = length 8, hash 79C07075 + sample 0: + time = 1830000 + flags = 1 + data = length 86, hash 64D21AA1 + sample 1: + time = 1840000 + flags = 1 + data = length 86, hash A1FAAF2C + sample 2: + time = 1850000 + flags = 1 + data = length 86, hash ECA80F7E + sample 3: + time = 1860000 + flags = 1 + data = length 86, hash FEB03B2C + sample 4: + time = 1870000 + flags = 1 + data = length 85, hash 2C2E6B2F + sample 5: + time = 1880000 + flags = 1 + data = length 89, hash A0D7AC3 + sample 6: + time = 1890000 + flags = 1 + data = length 87, hash 83739547 + sample 7: + time = 1900000 + flags = 1 + data = length 86, hash 991E531E + sample 8: + time = 1910000 + flags = 1 + data = length 88, hash 16B287A3 + sample 9: + time = 1920000 + flags = 1 + data = length 86, hash FC86EED + sample 10: + time = 1930000 + flags = 1 + data = length 86, hash 96AF0248 + sample 11: + time = 1940000 + flags = 1 + data = length 86, hash 288402C8 + sample 12: + time = 1950000 + flags = 1 + data = length 87, hash 4BBA7912 + sample 13: + time = 1960000 + flags = 1 + data = length 86, hash 4A59C719 + sample 14: + time = 1970000 + flags = 1 + data = length 85, hash 906E8187 + sample 15: + time = 1980000 + flags = 1 + data = length 90, hash CB8B755D + sample 16: + time = 1990000 + flags = 1 + data = length 87, hash C8E02C + sample 17: + time = 2000000 + flags = 1 + data = length 88, hash ACF4D89A + sample 18: + time = 2010000 + flags = 1 + data = length 86, hash 510FE048 + sample 19: + time = 2020000 + flags = 1 + data = length 86, hash 64983E46 + sample 20: + time = 2030000 + flags = 1 + data = length 86, hash CEA76A1E + sample 21: + time = 2040000 + flags = 1 + data = length 87, hash AADE498E + sample 22: + time = 2050000 + flags = 1 + data = length 127, hash 353A6D8C + sample 23: + time = 2060000 + flags = 1 + data = length 87, hash 29E18E62 + sample 24: + time = 2070000 + flags = 1 + data = length 87, hash 2CF7B30F + sample 25: + time = 2080000 + flags = 1 + data = length 94, hash 758704C3 + sample 26: + time = 2090000 + flags = 1 + data = length 88, hash C2153A4C + sample 27: + time = 2100000 + flags = 1 + data = length 86, hash A0A83DA5 + sample 28: + time = 2110000 + flags = 1 + data = length 86, hash 41017D7F + sample 29: + time = 2120000 + flags = 1 + data = length 93, hash 686B0CA2 + sample 30: + time = 2130000 + flags = 1 + data = length 86, hash 554D16CC + sample 31: + time = 2140000 + flags = 1 + data = length 88, hash 99D72771 + sample 32: + time = 2150000 + flags = 1 + data = length 88, hash 7176DFBF + sample 33: + time = 2160000 + flags = 1 + data = length 86, hash BAA22669 + sample 34: + time = 2170000 + flags = 1 + data = length 88, hash B00B0D3C + sample 35: + time = 2180000 + flags = 1 + data = length 89, hash 73FED83A + sample 36: + time = 2190000 + flags = 1 + data = length 86, hash 4A4138D3 + sample 37: + time = 2200000 + flags = 1 + data = length 89, hash E0A860FF + sample 38: + time = 2210000 + flags = 1 + data = length 95, hash EE5A8AED + sample 39: + time = 2220000 + flags = 1 + data = length 92, hash 36DBD7FD + sample 40: + time = 2230000 + flags = 1 + data = length 88, hash EE47A7E4 + sample 41: + time = 2240000 + flags = 1 + data = length 100, hash 2E1A603F + sample 42: + time = 2250000 + flags = 1 + data = length 89, hash 657ED4A3 + sample 43: + time = 2260000 + flags = 1 + data = length 86, hash A833DC7B + sample 44: + time = 2270000 + flags = 1 + data = length 88, hash 81E80732 + sample 45: + time = 2280000 + flags = 1 + data = length 91, hash FA256A0F + sample 46: + time = 2290000 + flags = 1 + data = length 88, hash A63A4DBA + sample 47: + time = 2300000 + flags = 1 + data = length 88, hash 67910A9F + sample 48: + time = 2310000 + flags = 1 + data = length 86, hash EB387DB6 + sample 49: + time = 2320000 + flags = 1 + data = length 88, hash 5ACAAC2A + sample 50: + time = 2330000 + flags = 1 + data = length 86, hash 6ADF2E1F + sample 51: + time = 2340000 + flags = 1 + data = length 85, hash 9D064471 + sample 52: + time = 2350000 + flags = 1 + data = length 87, hash F176C59 + sample 53: + time = 2360000 + flags = 1 + data = length 89, hash 5CA40CE4 + sample 54: + time = 2370000 + flags = 1 + data = length 88, hash 67B944FC + sample 55: + time = 2380000 + flags = 1 + data = length 86, hash B3A84EC8 + sample 56: + time = 2390000 + flags = 1 + data = length 92, hash A6ACF94B + sample 57: + time = 2400000 + flags = 1 + data = length 88, hash CB0C9730 + sample 58: + time = 2410000 + flags = 1 + data = length 88, hash C79FE804 + sample 59: + time = 2420000 + flags = 1 + data = length 88, hash A74C7F0A + sample 60: + time = 2430000 + flags = 1 + data = length 91, hash 55F6F0A5 + sample 61: + time = 2440000 + flags = 1 + data = length 93, hash 330F33E7 + sample 62: + time = 2450000 + flags = 1 + data = length 89, hash 614AFBA0 + sample 63: + time = 2460000 + flags = 1 + data = length 87, hash 3CE4652D + sample 64: + time = 2470000 + flags = 1 + data = length 87, hash 4EFD5467 + sample 65: + time = 2480000 + flags = 1 + data = length 86, hash D81B3EB8 + sample 66: + time = 2490000 + flags = 1 + data = length 88, hash 96CB6871 + sample 67: + time = 2500000 + flags = 1 + data = length 88, hash E9DF2786 + sample 68: + time = 2510000 + flags = 1 + data = length 89, hash 2CA33D96 + sample 69: + time = 2520000 + flags = 1 + data = length 90, hash 96BDE594 + sample 70: + time = 2530000 + flags = 1 + data = length 87, hash C261493C + sample 71: + time = 2540000 + flags = 1 + data = length 86, hash D037318E + sample 72: + time = 2550000 + flags = 1 + data = length 88, hash BC15BC88 + sample 73: + time = 2560000 + flags = 1 + data = length 91, hash A8361A51 + sample 74: + time = 2570000 + flags = 1 + data = length 87, hash 4AFDB5F2 + sample 75: + time = 2580000 + flags = 1 + data = length 87, hash 6447F8CB + sample 76: + time = 2590000 + flags = 1 + data = length 89, hash 48305229 + sample 77: + time = 2600000 + flags = 1 + data = length 87, hash 8741D9E7 + sample 78: + time = 2610000 + flags = 1 + data = length 120, hash 761F020C + sample 79: + time = 2620000 + flags = 1 + data = length 139, hash AECE2E57 + sample 80: + time = 2630000 + flags = 1 + data = length 166, hash 6288797A + sample 81: + time = 2640000 + flags = 1 + data = length 144, hash 437821A0 + sample 82: + time = 2650000 + flags = 1 + data = length 113, hash FCCBEDF1 + sample 83: + time = 2660000 + flags = 1 + data = length 108, hash C4040614 + sample 84: + time = 2670000 + flags = 1 + data = length 125, hash E29064C2 + sample 85: + time = 2680000 + flags = 1 + data = length 126, hash D42D24FF + sample 86: + time = 2690000 + flags = 1 + data = length 122, hash 30AF267D + sample 87: + time = 2700000 + flags = 1 + data = length 122, hash 45CEC1FB + sample 88: + time = 2710000 + flags = 1 + data = length 134, hash 59143FE2 + sample 89: + time = 2720000 + flags = 1 + data = length 134, hash BD52A84 + sample 90: + time = 2730000 + flags = 1 + data = length 120, hash 745C3714 + sample 91: + time = 2740000 + flags = 1 + data = length 126, hash 505E117B +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.3.dump b/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.3.dump new file mode 100644 index 0000000000..cf5a6cba2a --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.3.dump @@ -0,0 +1,25 @@ +seekMap: + isSeekable = true + duration = 2747500 + getPosition(0) = [[timeUs=0, position=250]] + getPosition(1) = [[timeUs=1, position=250]] + getPosition(1373750) = [[timeUs=1373750, position=250]] + getPosition(2747500) = [[timeUs=2747500, position=250]] +numberOfTracks = 1 +track 0: + total output bytes = 126 + sample count = 1 + format 0: + sampleMimeType = audio/opus + channelCount = 2 + sampleRate = 48000 + metadata = entries=[VC: encoder=Lavf54.20.4] + initializationData: + data = length 19, hash BFE794DB + data = length 8, hash CA22068C + data = length 8, hash 79C07075 + sample 0: + time = 2741000 + flags = 1 + data = length 126, hash 505E117B +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.unknown_length.dump new file mode 100644 index 0000000000..b5f4b1b400 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ogg/bear_duplicate_header.opus.unknown_length.dump @@ -0,0 +1,1118 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 25541 + sample count = 275 + format 0: + sampleMimeType = audio/opus + channelCount = 2 + sampleRate = 48000 + metadata = entries=[VC: encoder=Lavf54.20.4] + initializationData: + data = length 19, hash BFE794DB + data = length 8, hash CA22068C + data = length 8, hash 79C07075 + sample 0: + time = 0 + flags = 1 + data = length 234, hash B77BFFDA + sample 1: + time = 10000 + flags = 1 + data = length 165, hash F7B07C35 + sample 2: + time = 20000 + flags = 1 + data = length 100, hash 21AFA81F + sample 3: + time = 30000 + flags = 1 + data = length 85, hash 9180DC2F + sample 4: + time = 40000 + flags = 1 + data = length 85, hash 6AE958C + sample 5: + time = 50000 + flags = 1 + data = length 86, hash C1C5AE60 + sample 6: + time = 60000 + flags = 1 + data = length 87, hash B9BD2620 + sample 7: + time = 70000 + flags = 1 + data = length 86, hash 5E69E6F9 + sample 8: + time = 80000 + flags = 1 + data = length 90, hash C44C7DD9 + sample 9: + time = 90000 + flags = 1 + data = length 86, hash C3FCDC6F + sample 10: + time = 100000 + flags = 1 + data = length 86, hash 44EA03BA + sample 11: + time = 110000 + flags = 1 + data = length 160, hash 9F4E1AE8 + sample 12: + time = 120000 + flags = 1 + data = length 89, hash 31234460 + sample 13: + time = 130000 + flags = 1 + data = length 91, hash 45012D79 + sample 14: + time = 140000 + flags = 1 + data = length 90, hash B3E3AC75 + sample 15: + time = 150000 + flags = 1 + data = length 87, hash B83B756B + sample 16: + time = 160000 + flags = 1 + data = length 86, hash 383921EB + sample 17: + time = 170000 + flags = 1 + data = length 97, hash 959AD270 + sample 18: + time = 180000 + flags = 1 + data = length 92, hash 46C74FA8 + sample 19: + time = 190000 + flags = 1 + data = length 91, hash CEA401DD + sample 20: + time = 200000 + flags = 1 + data = length 89, hash 48C41853 + sample 21: + time = 210000 + flags = 1 + data = length 90, hash F23245BD + sample 22: + time = 220000 + flags = 1 + data = length 96, hash 95E8985D + sample 23: + time = 230000 + flags = 1 + data = length 96, hash 34295492 + sample 24: + time = 240000 + flags = 1 + data = length 94, hash 4E3C9C0F + sample 25: + time = 250000 + flags = 1 + data = length 89, hash 28B74A29 + sample 26: + time = 260000 + flags = 1 + data = length 87, hash BAC119A7 + sample 27: + time = 270000 + flags = 1 + data = length 88, hash 7139FF38 + sample 28: + time = 280000 + flags = 1 + data = length 85, hash 246E1D2A + sample 29: + time = 290000 + flags = 1 + data = length 86, hash 488A0900 + sample 30: + time = 300000 + flags = 1 + data = length 90, hash 16FD17B1 + sample 31: + time = 310000 + flags = 1 + data = length 87, hash 20E849D9 + sample 32: + time = 320000 + flags = 1 + data = length 86, hash 23A0E9BA + sample 33: + time = 330000 + flags = 1 + data = length 87, hash EC935537 + sample 34: + time = 340000 + flags = 1 + data = length 92, hash 4D9935AD + sample 35: + time = 350000 + flags = 1 + data = length 87, hash DEDE3FA + sample 36: + time = 360000 + flags = 1 + data = length 87, hash ADC25A6E + sample 37: + time = 370000 + flags = 1 + data = length 88, hash A1C828C5 + sample 38: + time = 380000 + flags = 1 + data = length 89, hash 735C087A + sample 39: + time = 390000 + flags = 1 + data = length 89, hash 19AF5D10 + sample 40: + time = 400000 + flags = 1 + data = length 90, hash BCCEA2BB + sample 41: + time = 410000 + flags = 1 + data = length 86, hash A7C934A0 + sample 42: + time = 420000 + flags = 1 + data = length 86, hash 28BBC0A8 + sample 43: + time = 430000 + flags = 1 + data = length 85, hash E60BB12D + sample 44: + time = 440000 + flags = 1 + data = length 141, hash 1D2B8920 + sample 45: + time = 450000 + flags = 1 + data = length 121, hash 8AA3E19C + sample 46: + time = 460000 + flags = 1 + data = length 86, hash 24DF0F37 + sample 47: + time = 470000 + flags = 1 + data = length 86, hash 1D1775FF + sample 48: + time = 480000 + flags = 1 + data = length 87, hash 5230399E + sample 49: + time = 490000 + flags = 1 + data = length 91, hash 6CD98305 + sample 50: + time = 500000 + flags = 1 + data = length 88, hash 4069FBB + sample 51: + time = 510000 + flags = 1 + data = length 89, hash 76824ABF + sample 52: + time = 520000 + flags = 1 + data = length 87, hash BC1B1322 + sample 53: + time = 530000 + flags = 1 + data = length 102, hash E01BA053 + sample 54: + time = 540000 + flags = 1 + data = length 85, hash C09B626D + sample 55: + time = 550000 + flags = 1 + data = length 88, hash 6B7B404A + sample 56: + time = 560000 + flags = 1 + data = length 85, hash 74A15DC7 + sample 57: + time = 570000 + flags = 1 + data = length 88, hash 38DB82E5 + sample 58: + time = 580000 + flags = 1 + data = length 86, hash 1A39C081 + sample 59: + time = 590000 + flags = 1 + data = length 87, hash 39FEC92 + sample 60: + time = 600000 + flags = 1 + data = length 92, hash 278EA09 + sample 61: + time = 610000 + flags = 1 + data = length 87, hash 28265F2D + sample 62: + time = 620000 + flags = 1 + data = length 86, hash CC2040C6 + sample 63: + time = 630000 + flags = 1 + data = length 138, hash 9E07BC1F + sample 64: + time = 640000 + flags = 1 + data = length 85, hash 4F299670 + sample 65: + time = 650000 + flags = 1 + data = length 125, hash B61123C3 + sample 66: + time = 660000 + flags = 1 + data = length 89, hash 5CC688ED + sample 67: + time = 670000 + flags = 1 + data = length 88, hash 84AF64A6 + sample 68: + time = 680000 + flags = 1 + data = length 89, hash A9BFC8DC + sample 69: + time = 690000 + flags = 1 + data = length 90, hash 2FF77060 + sample 70: + time = 700000 + flags = 1 + data = length 96, hash E11AFD61 + sample 71: + time = 710000 + flags = 1 + data = length 87, hash 85D14EDA + sample 72: + time = 720000 + flags = 1 + data = length 88, hash 5FC71D53 + sample 73: + time = 730000 + flags = 1 + data = length 89, hash 957187B6 + sample 74: + time = 740000 + flags = 1 + data = length 89, hash 5A776082 + sample 75: + time = 750000 + flags = 1 + data = length 87, hash E8A83AFE + sample 76: + time = 760000 + flags = 1 + data = length 87, hash F1989133 + sample 77: + time = 770000 + flags = 1 + data = length 87, hash FA06BCCA + sample 78: + time = 780000 + flags = 1 + data = length 86, hash BD97E0C0 + sample 79: + time = 790000 + flags = 1 + data = length 88, hash E6AE022C + sample 80: + time = 800000 + flags = 1 + data = length 87, hash FB6C6169 + sample 81: + time = 810000 + flags = 1 + data = length 87, hash DFFCD2CF + sample 82: + time = 820000 + flags = 1 + data = length 88, hash A4B5EB52 + sample 83: + time = 830000 + flags = 1 + data = length 85, hash A63CF4EA + sample 84: + time = 840000 + flags = 1 + data = length 86, hash F126E7C7 + sample 85: + time = 850000 + flags = 1 + data = length 86, hash 21A8B22F + sample 86: + time = 860000 + flags = 1 + data = length 87, hash 6520E7C1 + sample 87: + time = 870000 + flags = 1 + data = length 88, hash 825B7423 + sample 88: + time = 880000 + flags = 1 + data = length 88, hash DF3DBD48 + sample 89: + time = 890000 + flags = 1 + data = length 87, hash B32C68D0 + sample 90: + time = 900000 + flags = 1 + data = length 89, hash B99DFFCA + sample 91: + time = 910000 + flags = 1 + data = length 88, hash 9C8D5178 + sample 92: + time = 920000 + flags = 1 + data = length 88, hash 48A0B19A + sample 93: + time = 930000 + flags = 1 + data = length 88, hash B62C94DD + sample 94: + time = 940000 + flags = 1 + data = length 92, hash 96DBDD46 + sample 95: + time = 950000 + flags = 1 + data = length 87, hash 7B80E6F + sample 96: + time = 960000 + flags = 1 + data = length 86, hash 9C60225B + sample 97: + time = 970000 + flags = 1 + data = length 87, hash 45F7E6E8 + sample 98: + time = 980000 + flags = 1 + data = length 87, hash DDC2D592 + sample 99: + time = 990000 + flags = 1 + data = length 91, hash 173D3B26 + sample 100: + time = 1000000 + flags = 1 + data = length 87, hash CF3629DF + sample 101: + time = 1010000 + flags = 1 + data = length 87, hash BBE2E7B3 + sample 102: + time = 1020000 + flags = 1 + data = length 89, hash 89AFFB10 + sample 103: + time = 1030000 + flags = 1 + data = length 88, hash 510DCC90 + sample 104: + time = 1040000 + flags = 1 + data = length 88, hash CBA56E5F + sample 105: + time = 1050000 + flags = 1 + data = length 87, hash B4B1B3FF + sample 106: + time = 1060000 + flags = 1 + data = length 89, hash B976A537 + sample 107: + time = 1070000 + flags = 1 + data = length 96, hash 43ECF2C1 + sample 108: + time = 1080000 + flags = 1 + data = length 90, hash BB7ECB44 + sample 109: + time = 1090000 + flags = 1 + data = length 89, hash B8E221A5 + sample 110: + time = 1100000 + flags = 1 + data = length 86, hash B35BEF5B + sample 111: + time = 1110000 + flags = 1 + data = length 89, hash 9002F0EC + sample 112: + time = 1120000 + flags = 1 + data = length 85, hash F694B20 + sample 113: + time = 1130000 + flags = 1 + data = length 87, hash D7CC386E + sample 114: + time = 1140000 + flags = 1 + data = length 89, hash EE9E0E79 + sample 115: + time = 1150000 + flags = 1 + data = length 90, hash CA72C96B + sample 116: + time = 1160000 + flags = 1 + data = length 112, hash 4AD3D1B1 + sample 117: + time = 1170000 + flags = 1 + data = length 87, hash FA568FAB + sample 118: + time = 1180000 + flags = 1 + data = length 90, hash 6E6948D2 + sample 119: + time = 1190000 + flags = 1 + data = length 89, hash 5465A762 + sample 120: + time = 1200000 + flags = 1 + data = length 87, hash BED19B40 + sample 121: + time = 1210000 + flags = 1 + data = length 89, hash 5D05836A + sample 122: + time = 1220000 + flags = 1 + data = length 87, hash A8A3EF5A + sample 123: + time = 1230000 + flags = 1 + data = length 90, hash 6425A77A + sample 124: + time = 1240000 + flags = 1 + data = length 92, hash 7F320FA + sample 125: + time = 1250000 + flags = 1 + data = length 89, hash 2C7837D6 + sample 126: + time = 1260000 + flags = 1 + data = length 86, hash 58D56685 + sample 127: + time = 1270000 + flags = 1 + data = length 91, hash ADC5072F + sample 128: + time = 1280000 + flags = 1 + data = length 85, hash 53EFD93 + sample 129: + time = 1290000 + flags = 1 + data = length 87, hash D006B535 + sample 130: + time = 1300000 + flags = 1 + data = length 86, hash AE944625 + sample 131: + time = 1310000 + flags = 1 + data = length 89, hash B5D3C81D + sample 132: + time = 1320000 + flags = 1 + data = length 86, hash 3BB1D0E7 + sample 133: + time = 1330000 + flags = 1 + data = length 102, hash 16EEC441 + sample 134: + time = 1340000 + flags = 1 + data = length 90, hash 1005B936 + sample 135: + time = 1350000 + flags = 1 + data = length 85, hash 15EEBF9A + sample 136: + time = 1360000 + flags = 1 + data = length 87, hash 37C83AC2 + sample 137: + time = 1370000 + flags = 1 + data = length 85, hash 2D27855D + sample 138: + time = 1380000 + flags = 1 + data = length 85, hash 753EB7C6 + sample 139: + time = 1390000 + flags = 1 + data = length 91, hash C0813318 + sample 140: + time = 1400000 + flags = 1 + data = length 89, hash 3A6468AC + sample 141: + time = 1410000 + flags = 1 + data = length 88, hash 3D220ABC + sample 142: + time = 1420000 + flags = 1 + data = length 140, hash 7949ABC7 + sample 143: + time = 1430000 + flags = 1 + data = length 92, hash F19AFA45 + sample 144: + time = 1440000 + flags = 1 + data = length 90, hash 3D21587C + sample 145: + time = 1450000 + flags = 1 + data = length 89, hash 5C12226C + sample 146: + time = 1460000 + flags = 1 + data = length 90, hash 22BA14FC + sample 147: + time = 1470000 + flags = 1 + data = length 88, hash F064B21C + sample 148: + time = 1480000 + flags = 1 + data = length 87, hash 6D7906B9 + sample 149: + time = 1490000 + flags = 1 + data = length 88, hash 6756A484 + sample 150: + time = 1500000 + flags = 1 + data = length 91, hash C95C00B6 + sample 151: + time = 1510000 + flags = 1 + data = length 87, hash 728D8119 + sample 152: + time = 1520000 + flags = 1 + data = length 90, hash C43DA1B4 + sample 153: + time = 1530000 + flags = 1 + data = length 88, hash C181BB57 + sample 154: + time = 1540000 + flags = 1 + data = length 84, hash F75B1639 + sample 155: + time = 1550000 + flags = 1 + data = length 87, hash B6F32978 + sample 156: + time = 1560000 + flags = 1 + data = length 90, hash 36D6E2D7 + sample 157: + time = 1570000 + flags = 1 + data = length 87, hash 4C9657A7 + sample 158: + time = 1580000 + flags = 1 + data = length 89, hash C3BDB9B7 + sample 159: + time = 1590000 + flags = 1 + data = length 88, hash DB51087E + sample 160: + time = 1600000 + flags = 1 + data = length 86, hash 1550F998 + sample 161: + time = 1610000 + flags = 1 + data = length 86, hash A445FAD4 + sample 162: + time = 1620000 + flags = 1 + data = length 85, hash 60D3362C + sample 163: + time = 1630000 + flags = 1 + data = length 172, hash 945D63E4 + sample 164: + time = 1640000 + flags = 1 + data = length 107, hash 585B7C04 + sample 165: + time = 1650000 + flags = 1 + data = length 110, hash 74BECF69 + sample 166: + time = 1660000 + flags = 1 + data = length 87, hash 63DE1D24 + sample 167: + time = 1670000 + flags = 1 + data = length 90, hash 1C1D28DB + sample 168: + time = 1680000 + flags = 1 + data = length 87, hash CB382A67 + sample 169: + time = 1690000 + flags = 1 + data = length 85, hash A227BA13 + sample 170: + time = 1700000 + flags = 1 + data = length 86, hash EFD8B10B + sample 171: + time = 1710000 + flags = 1 + data = length 87, hash 47FF364A + sample 172: + time = 1720000 + flags = 1 + data = length 91, hash 31D4B48A + sample 173: + time = 1730000 + flags = 1 + data = length 91, hash DD69BD85 + sample 174: + time = 1740000 + flags = 1 + data = length 88, hash AF1A95C6 + sample 175: + time = 1750000 + flags = 1 + data = length 87, hash 2FB8AF74 + sample 176: + time = 1760000 + flags = 1 + data = length 92, hash 173C707A + sample 177: + time = 1770000 + flags = 1 + data = length 88, hash 5F58F5E8 + sample 178: + time = 1780000 + flags = 1 + data = length 91, hash D449785F + sample 179: + time = 1790000 + flags = 1 + data = length 91, hash CE2CB465 + sample 180: + time = 1800000 + flags = 1 + data = length 93, hash ABC1C62E + sample 181: + time = 1810000 + flags = 1 + data = length 87, hash 83B4B9A0 + sample 182: + time = 1820000 + flags = 1 + data = length 87, hash 3220D562 + sample 183: + time = 1830000 + flags = 1 + data = length 86, hash 64D21AA1 + sample 184: + time = 1840000 + flags = 1 + data = length 86, hash A1FAAF2C + sample 185: + time = 1850000 + flags = 1 + data = length 86, hash ECA80F7E + sample 186: + time = 1860000 + flags = 1 + data = length 86, hash FEB03B2C + sample 187: + time = 1870000 + flags = 1 + data = length 85, hash 2C2E6B2F + sample 188: + time = 1880000 + flags = 1 + data = length 89, hash A0D7AC3 + sample 189: + time = 1890000 + flags = 1 + data = length 87, hash 83739547 + sample 190: + time = 1900000 + flags = 1 + data = length 86, hash 991E531E + sample 191: + time = 1910000 + flags = 1 + data = length 88, hash 16B287A3 + sample 192: + time = 1920000 + flags = 1 + data = length 86, hash FC86EED + sample 193: + time = 1930000 + flags = 1 + data = length 86, hash 96AF0248 + sample 194: + time = 1940000 + flags = 1 + data = length 86, hash 288402C8 + sample 195: + time = 1950000 + flags = 1 + data = length 87, hash 4BBA7912 + sample 196: + time = 1960000 + flags = 1 + data = length 86, hash 4A59C719 + sample 197: + time = 1970000 + flags = 1 + data = length 85, hash 906E8187 + sample 198: + time = 1980000 + flags = 1 + data = length 90, hash CB8B755D + sample 199: + time = 1990000 + flags = 1 + data = length 87, hash C8E02C + sample 200: + time = 2000000 + flags = 1 + data = length 88, hash ACF4D89A + sample 201: + time = 2010000 + flags = 1 + data = length 86, hash 510FE048 + sample 202: + time = 2020000 + flags = 1 + data = length 86, hash 64983E46 + sample 203: + time = 2030000 + flags = 1 + data = length 86, hash CEA76A1E + sample 204: + time = 2040000 + flags = 1 + data = length 87, hash AADE498E + sample 205: + time = 2050000 + flags = 1 + data = length 127, hash 353A6D8C + sample 206: + time = 2060000 + flags = 1 + data = length 87, hash 29E18E62 + sample 207: + time = 2070000 + flags = 1 + data = length 87, hash 2CF7B30F + sample 208: + time = 2080000 + flags = 1 + data = length 94, hash 758704C3 + sample 209: + time = 2090000 + flags = 1 + data = length 88, hash C2153A4C + sample 210: + time = 2100000 + flags = 1 + data = length 86, hash A0A83DA5 + sample 211: + time = 2110000 + flags = 1 + data = length 86, hash 41017D7F + sample 212: + time = 2120000 + flags = 1 + data = length 93, hash 686B0CA2 + sample 213: + time = 2130000 + flags = 1 + data = length 86, hash 554D16CC + sample 214: + time = 2140000 + flags = 1 + data = length 88, hash 99D72771 + sample 215: + time = 2150000 + flags = 1 + data = length 88, hash 7176DFBF + sample 216: + time = 2160000 + flags = 1 + data = length 86, hash BAA22669 + sample 217: + time = 2170000 + flags = 1 + data = length 88, hash B00B0D3C + sample 218: + time = 2180000 + flags = 1 + data = length 89, hash 73FED83A + sample 219: + time = 2190000 + flags = 1 + data = length 86, hash 4A4138D3 + sample 220: + time = 2200000 + flags = 1 + data = length 89, hash E0A860FF + sample 221: + time = 2210000 + flags = 1 + data = length 95, hash EE5A8AED + sample 222: + time = 2220000 + flags = 1 + data = length 92, hash 36DBD7FD + sample 223: + time = 2230000 + flags = 1 + data = length 88, hash EE47A7E4 + sample 224: + time = 2240000 + flags = 1 + data = length 100, hash 2E1A603F + sample 225: + time = 2250000 + flags = 1 + data = length 89, hash 657ED4A3 + sample 226: + time = 2260000 + flags = 1 + data = length 86, hash A833DC7B + sample 227: + time = 2270000 + flags = 1 + data = length 88, hash 81E80732 + sample 228: + time = 2280000 + flags = 1 + data = length 91, hash FA256A0F + sample 229: + time = 2290000 + flags = 1 + data = length 88, hash A63A4DBA + sample 230: + time = 2300000 + flags = 1 + data = length 88, hash 67910A9F + sample 231: + time = 2310000 + flags = 1 + data = length 86, hash EB387DB6 + sample 232: + time = 2320000 + flags = 1 + data = length 88, hash 5ACAAC2A + sample 233: + time = 2330000 + flags = 1 + data = length 86, hash 6ADF2E1F + sample 234: + time = 2340000 + flags = 1 + data = length 85, hash 9D064471 + sample 235: + time = 2350000 + flags = 1 + data = length 87, hash F176C59 + sample 236: + time = 2360000 + flags = 1 + data = length 89, hash 5CA40CE4 + sample 237: + time = 2370000 + flags = 1 + data = length 88, hash 67B944FC + sample 238: + time = 2380000 + flags = 1 + data = length 86, hash B3A84EC8 + sample 239: + time = 2390000 + flags = 1 + data = length 92, hash A6ACF94B + sample 240: + time = 2400000 + flags = 1 + data = length 88, hash CB0C9730 + sample 241: + time = 2410000 + flags = 1 + data = length 88, hash C79FE804 + sample 242: + time = 2420000 + flags = 1 + data = length 88, hash A74C7F0A + sample 243: + time = 2430000 + flags = 1 + data = length 91, hash 55F6F0A5 + sample 244: + time = 2440000 + flags = 1 + data = length 93, hash 330F33E7 + sample 245: + time = 2450000 + flags = 1 + data = length 89, hash 614AFBA0 + sample 246: + time = 2460000 + flags = 1 + data = length 87, hash 3CE4652D + sample 247: + time = 2470000 + flags = 1 + data = length 87, hash 4EFD5467 + sample 248: + time = 2480000 + flags = 1 + data = length 86, hash D81B3EB8 + sample 249: + time = 2490000 + flags = 1 + data = length 88, hash 96CB6871 + sample 250: + time = 2500000 + flags = 1 + data = length 88, hash E9DF2786 + sample 251: + time = 2510000 + flags = 1 + data = length 89, hash 2CA33D96 + sample 252: + time = 2520000 + flags = 1 + data = length 90, hash 96BDE594 + sample 253: + time = 2530000 + flags = 1 + data = length 87, hash C261493C + sample 254: + time = 2540000 + flags = 1 + data = length 86, hash D037318E + sample 255: + time = 2550000 + flags = 1 + data = length 88, hash BC15BC88 + sample 256: + time = 2560000 + flags = 1 + data = length 91, hash A8361A51 + sample 257: + time = 2570000 + flags = 1 + data = length 87, hash 4AFDB5F2 + sample 258: + time = 2580000 + flags = 1 + data = length 87, hash 6447F8CB + sample 259: + time = 2590000 + flags = 1 + data = length 89, hash 48305229 + sample 260: + time = 2600000 + flags = 1 + data = length 87, hash 8741D9E7 + sample 261: + time = 2610000 + flags = 1 + data = length 120, hash 761F020C + sample 262: + time = 2620000 + flags = 1 + data = length 139, hash AECE2E57 + sample 263: + time = 2630000 + flags = 1 + data = length 166, hash 6288797A + sample 264: + time = 2640000 + flags = 1 + data = length 144, hash 437821A0 + sample 265: + time = 2650000 + flags = 1 + data = length 113, hash FCCBEDF1 + sample 266: + time = 2660000 + flags = 1 + data = length 108, hash C4040614 + sample 267: + time = 2670000 + flags = 1 + data = length 125, hash E29064C2 + sample 268: + time = 2680000 + flags = 1 + data = length 126, hash D42D24FF + sample 269: + time = 2690000 + flags = 1 + data = length 122, hash 30AF267D + sample 270: + time = 2700000 + flags = 1 + data = length 122, hash 45CEC1FB + sample 271: + time = 2710000 + flags = 1 + data = length 134, hash 59143FE2 + sample 272: + time = 2720000 + flags = 1 + data = length 134, hash BD52A84 + sample 273: + time = 2730000 + flags = 1 + data = length 120, hash 745C3714 + sample 274: + time = 2740000 + flags = 1 + data = length 126, hash 505E117B +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/media/ogg/bear_duplicate_header.opus b/libraries/test_data/src/test/assets/media/ogg/bear_duplicate_header.opus new file mode 100644 index 0000000000000000000000000000000000000000..aa4e9d6064b2fe0587bb54d76b47095891bf80fd GIT binary patch literal 26120 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 literal 0 HcmV?d00001 From 9cd84696c712425a27eb2b202828ec853fef6a06 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 31 May 2022 13:47:08 +0000 Subject: [PATCH 08/45] Create `withMediaPipe` variant only if AAR is present This should fix running `./gradlew clean test` if MediaPipe hasn't been built, for example. PiperOrigin-RevId: 452033698 (cherry picked from commit 208a9114a9f516f7034b75b4f21f9ded34de8501) --- demos/transformer/README.md | 5 +++-- demos/transformer/build.gradle | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/demos/transformer/README.md b/demos/transformer/README.md index 3a53f71dc7..fd767ba6c8 100644 --- a/demos/transformer/README.md +++ b/demos/transformer/README.md @@ -57,8 +57,9 @@ manual steps. ${TRANSFORMER_DEMO_ROOT}/src/withMediaPipe/assets ``` -1. Select the `withMediaPipe` build variant in Android Studio, then build and - run the demo app and select a MediaPipe-based effect. +1. In Android Studio, gradle sync and select the `withMediaPipe` build variant + (this will only appear if the AAR is present), then build and run the demo + app and select a MediaPipe-based effect. [Transformer]: https://exoplayer.dev/transforming-media.html [MediaPipe]: https://google.github.io/mediapipe/ diff --git a/demos/transformer/build.gradle b/demos/transformer/build.gradle index 6f6ff50908..a745fcea1f 100644 --- a/demos/transformer/build.gradle +++ b/demos/transformer/build.gradle @@ -56,6 +56,16 @@ android { dimension "mediaPipe" } } + + // Ignore the withMediaPipe variant if the MediaPipe AAR is not present. + if (!project.file("libs/edge_detector_mediapipe_aar.aar").exists()) { + variantFilter { variant -> + def names = variant.flavors*.name + if (names.contains("withMediaPipe")) { + setIgnore(true) + } + } + } } dependencies { From 706e60346df7a41055908eac547eff5f1328cbaf Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 31 May 2022 14:41:24 +0000 Subject: [PATCH 09/45] Ignore decoding test cases when library not available #minor-release PiperOrigin-RevId: 452043577 (cherry picked from commit 55a194c575ba40dc0e156815f07ec854dc85bd3d) --- .../androidx/media3/decoder/opus/OpusDecoderTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/libraries/decoder_opus/src/test/java/androidx/media3/decoder/opus/OpusDecoderTest.java b/libraries/decoder_opus/src/test/java/androidx/media3/decoder/opus/OpusDecoderTest.java index f166a511c8..9aa17cffcf 100644 --- a/libraries/decoder_opus/src/test/java/androidx/media3/decoder/opus/OpusDecoderTest.java +++ b/libraries/decoder_opus/src/test/java/androidx/media3/decoder/opus/OpusDecoderTest.java @@ -16,6 +16,7 @@ package androidx.media3.decoder.opus; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import androidx.annotation.Nullable; import androidx.media3.common.C; @@ -26,7 +27,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -69,11 +69,6 @@ public final class OpusDecoderTest { private static final ImmutableList FULL_INITIALIZATION_DATA = ImmutableList.of(HEADER, CUSTOM_PRE_SKIP_BYTES, CUSTOM_SEEK_PRE_ROLL_BYTES); - @Before - public void setUp() { - assertThat(LOADER.isAvailable()).isTrue(); - } - @Test public void getChannelCount() { int channelCount = OpusDecoder.getChannelCount(HEADER); @@ -120,6 +115,7 @@ public final class OpusDecoderTest { @Test public void decode_removesPreSkipFromOutput() throws OpusDecoderException { + assumeTrue(LOADER.isAvailable()); OpusDecoder decoder = new OpusDecoder( /* numInputBuffers= */ 0, @@ -139,6 +135,7 @@ public final class OpusDecoderTest { @Test public void decode_whenDiscardPaddingDisabled_returnsDiscardPadding() throws OpusDecoderException { + assumeTrue(LOADER.isAvailable()); OpusDecoder decoder = new OpusDecoder( /* numInputBuffers= */ 0, @@ -159,6 +156,7 @@ public final class OpusDecoderTest { @Test public void decode_whenDiscardPaddingEnabled_removesDiscardPadding() throws OpusDecoderException { + assumeTrue(LOADER.isAvailable()); OpusDecoder decoder = new OpusDecoder( /* numInputBuffers= */ 0, From 501ea8c5639b6bb42518360b1774d997e2ead8ec Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 31 May 2022 19:12:32 +0000 Subject: [PATCH 10/45] Fix `HiddenTypedefConstant` Metalava error on `PlaybackException` This is done by removing the `@FieldNumber` IntDef completely. It's not really adding much value anyway, because it's `open` so there's no real enforcement to prevent passing 'incorrect' values. #minor-release PiperOrigin-RevId: 452108972 (cherry picked from commit 39674bec78d45baa550df6807e4d49faf42dc25a) --- .../media3/common/PlaybackException.java | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java b/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java index 4e7850de45..2bcc674631 100644 --- a/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java +++ b/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java @@ -401,28 +401,6 @@ public class PlaybackException extends Exception implements Bundleable { // Bundleable implementation. - /** - * Identifiers for fields in a {@link Bundle} which represents a playback exception. Subclasses - * may use {@link #FIELD_CUSTOM_ID_BASE} to generate more keys using {@link #keyForField(int)}. - * - *

    Note: Changes to the Bundleable implementation must be backwards compatible, so as to avoid - * breaking communication across different Bundleable implementation versions. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef( - open = true, - value = { - FIELD_INT_ERROR_CODE, - FIELD_LONG_TIMESTAMP_MS, - FIELD_STRING_MESSAGE, - FIELD_STRING_CAUSE_CLASS_NAME, - FIELD_STRING_CAUSE_MESSAGE, - }) - @UnstableApi - protected @interface FieldNumber {} - private static final int FIELD_INT_ERROR_CODE = 0; private static final int FIELD_LONG_TIMESTAMP_MS = 1; private static final int FIELD_STRING_MESSAGE = 2; @@ -430,7 +408,7 @@ public class PlaybackException extends Exception implements Bundleable { private static final int FIELD_STRING_CAUSE_MESSAGE = 4; /** - * Defines a minimum field id value for subclasses to use when implementing {@link #toBundle()} + * Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()} * and {@link Bundleable.Creator}. * *

    Subclasses should obtain their {@link Bundle Bundle's} field keys by applying a non-negative @@ -458,11 +436,14 @@ public class PlaybackException extends Exception implements Bundleable { } /** - * Converts the given {@link FieldNumber} to a string which can be used as a field key when - * implementing {@link #toBundle()} and {@link Bundleable.Creator}. + * Converts the given field number to a string which can be used as a field key when implementing + * {@link #toBundle()} and {@link Bundleable.Creator}. + * + *

    Subclasses should use {@code field} values greater than or equal to {@link + * #FIELD_CUSTOM_ID_BASE}. */ @UnstableApi - protected static String keyForField(@FieldNumber int field) { + protected static String keyForField(int field) { return Integer.toString(field, Character.MAX_RADIX); } From 216d9715185a4798cbc3650f6dc9736fa6c23dc9 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 1 Jun 2022 13:30:18 +0000 Subject: [PATCH 11/45] Filter bogus AndroidX Media jar file when creating javadoc #minor-release PiperOrigin-RevId: 452282128 (cherry picked from commit bd9bc0f6b7349cf2c1477a6d7e3146555c3ead86) --- javadoc_combined.gradle | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/javadoc_combined.gradle b/javadoc_combined.gradle index a121169339..0b37687b41 100644 --- a/javadoc_combined.gradle +++ b/javadoc_combined.gradle @@ -48,9 +48,21 @@ class CombinedJavadocPlugin implements Plugin { libraryModule.android.libraryVariants.all { variant -> def name = variant.buildType.name if (name == "release") { + // Works around b/234569640 that causes different versions of the androidx.media + // jar to be on the classpath. + def allJarFiles = [] + allJarFiles.addAll(variant.javaCompileProvider.get().classpath.files) + def filteredJarFiles = allJarFiles.findAll { file -> + if (file ==~ /.*media-.\..\..-api.jar$/ + && !file.path.endsWith( + "media-" + project.ext.androidxMediaVersion + "-api.jar")) { + return false; + } + return true; + } classpath += libraryModule.project.files( - variant.javaCompileProvider.get().classpath.files, + filteredJarFiles, libraryModule.project.android.getBootClasspath()) } } From f3574f2354eba9456db8d84eb992832986b6a625 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 6 Jun 2022 16:26:18 +0000 Subject: [PATCH 12/45] Rename `DefaultTrackSelector.ParametersBuilder` to `Parameters.Builder` We generally nest the `Builder` for `Foo` inside `Foo`. In this case, there's already a `DefaultTrackSelector.Parameters.Builder` type visible to a developer, it just happens to be the 'common' `TrackSelectorParameters.Builder`, so using it is a bit weird. For example this code snippet doesn't compile because `DefaultTrackSelector.Parameters.Builder#build()` returns `TrackSelectionParameters`. This CL fixes that problem and the code snippet now compiles. ```java DefaultTrackSelector.Parameters params = new DefaultTrackSelector.Parameters.Builder(context).build() ``` #minor-release PiperOrigin-RevId: 453215702 (cherry picked from commit 247c2d845dbe0887f556fd3d44cdbc4d97f7451a) --- RELEASENOTES.md | 4 + .../media3/common/ForwardingPlayerTest.java | 37 +- .../exoplayer/offline/DownloadHelper.java | 2 +- .../trackselection/DefaultTrackSelector.java | 1185 ++++++++++++----- .../trackselection/TrackSelectionUtil.java | 2 +- .../DefaultTrackSelectorTest.java | 35 +- .../androidx/media3/test/utils/TestUtil.java | 40 + 7 files changed, 954 insertions(+), 351 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index eb53d3d6d1..e59eb25ddb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -33,6 +33,10 @@ `Tracks.Group`. `Player.getCurrentTracksInfo` and `Player.Listener.onTracksInfoChanged` have also been renamed to `Player.getCurrentTracks` and `Player.Listener.onTracksChanged`. + * Change `DefaultTrackSelector.buildUponParameters` and + `DefaultTrackSelector.Parameters.buildUpon` to return + `DefaultTrackSelector.Parameters.Builder` instead of the deprecated + `DefaultTrackSelector.ParametersBuilder`. * Video: * Rename `DummySurface` to `PlaceholderSurface`. * Add AV1 support to the `MediaCodecVideoRenderer.getCodecMaxInputSize`. diff --git a/libraries/common/src/test/java/androidx/media3/common/ForwardingPlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/ForwardingPlayerTest.java index 02b6a3e023..b3c2ccbe3b 100644 --- a/libraries/common/src/test/java/androidx/media3/common/ForwardingPlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/ForwardingPlayerTest.java @@ -18,22 +18,17 @@ package androidx.media3.common; import static androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED; import static androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION; import static androidx.media3.common.Player.EVENT_TIMELINE_CHANGED; -import static androidx.media3.common.util.Assertions.checkArgument; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import androidx.media3.test.utils.StubPlayer; +import androidx.media3.test.utils.TestUtil; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Queue; import java.util.Set; import org.junit.Test; import org.junit.runner.RunWith; @@ -105,7 +100,7 @@ public class ForwardingPlayerTest { @Test public void forwardingPlayer_overridesAllPlayerMethods() throws Exception { // Check with reflection that ForwardingPlayer overrides all Player methods. - List methods = getPublicMethods(Player.class); + List methods = TestUtil.getPublicMethods(Player.class); for (Method method : methods) { assertThat( ForwardingPlayer.class @@ -119,7 +114,7 @@ public class ForwardingPlayerTest { public void forwardingListener_overridesAllListenerMethods() throws Exception { // Check with reflection that ForwardingListener overrides all Listener methods. Class forwardingListenerClass = getInnerClass("ForwardingListener"); - List methods = getPublicMethods(Player.Listener.class); + List methods = TestUtil.getPublicMethods(Player.Listener.class); for (Method method : methods) { assertThat( forwardingListenerClass @@ -129,32 +124,6 @@ public class ForwardingPlayerTest { } } - /** Returns all the public methods of a Java interface. */ - private static List getPublicMethods(Class anInterface) { - checkArgument(anInterface.isInterface()); - // Run a BFS over all extended interfaces to inspect them all. - Queue> interfacesQueue = new ArrayDeque<>(); - interfacesQueue.add(anInterface); - Set> interfaces = new HashSet<>(); - while (!interfacesQueue.isEmpty()) { - Class currentInterface = interfacesQueue.remove(); - if (interfaces.add(currentInterface)) { - Collections.addAll(interfacesQueue, currentInterface.getInterfaces()); - } - } - - List list = new ArrayList<>(); - for (Class currentInterface : interfaces) { - for (Method method : currentInterface.getDeclaredMethods()) { - if (Modifier.isPublic(method.getModifiers())) { - list.add(method); - } - } - } - - return list; - } - private static Class getInnerClass(String className) { for (Class innerClass : ForwardingPlayer.class.getDeclaredClasses()) { if (innerClass.getSimpleName().equals(className)) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java index df11e0e589..25f6c98114 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java @@ -746,7 +746,7 @@ public final class DownloadHelper { List overrides) { try { assertPreparedWithMedia(); - DefaultTrackSelector.ParametersBuilder builder = trackSelectorParameters.buildUpon(); + DefaultTrackSelector.Parameters.Builder builder = trackSelectorParameters.buildUpon(); for (int i = 0; i < mappedTrackInfos[periodIndex].getRendererCount(); i++) { builder.setRendererDisabled(/* rendererIndex= */ i, /* disabled= */ i != rendererIndex); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index 8e45ec3135..dbed6b4ffb 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -102,30 +102,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public class DefaultTrackSelector extends MappingTrackSelector { /** - * A builder for {@link Parameters}. See the {@link Parameters} documentation for explanations of - * the parameters that can be configured using this builder. + * @deprecated Use {@link Parameters.Builder} instead. */ + @Deprecated public static final class ParametersBuilder extends TrackSelectionParameters.Builder { - // Video - private boolean exceedVideoConstraintsIfNecessary; - private boolean allowVideoMixedMimeTypeAdaptiveness; - private boolean allowVideoNonSeamlessAdaptiveness; - private boolean allowVideoMixedDecoderSupportAdaptiveness; - // Audio - private boolean exceedAudioConstraintsIfNecessary; - private boolean allowAudioMixedMimeTypeAdaptiveness; - private boolean allowAudioMixedSampleRateAdaptiveness; - private boolean allowAudioMixedChannelCountAdaptiveness; - private boolean allowAudioMixedDecoderSupportAdaptiveness; - // General - private boolean exceedRendererCapabilitiesIfNecessary; - private boolean tunnelingEnabled; - private boolean allowMultipleAdaptiveSelections; - // Overrides - private final SparseArray> - selectionOverrides; - private final SparseBooleanArray rendererDisabledFlags; + private final Parameters.Builder delegate; /** * @deprecated {@link Context} constraints will not be set using this constructor. Use {@link @@ -134,10 +116,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Deprecated @SuppressWarnings({"deprecation"}) public ParametersBuilder() { - super(); - selectionOverrides = new SparseArray<>(); - rendererDisabledFlags = new SparseBooleanArray(); - init(); + delegate = new Parameters.Builder(); } /** @@ -146,110 +125,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param context Any context. */ public ParametersBuilder(Context context) { - super(context); - selectionOverrides = new SparseArray<>(); - rendererDisabledFlags = new SparseBooleanArray(); - init(); - } - - /** - * @param initialValues The {@link Parameters} from which the initial values of the builder are - * obtained. - */ - private ParametersBuilder(Parameters initialValues) { - super(initialValues); - // Video - exceedVideoConstraintsIfNecessary = initialValues.exceedVideoConstraintsIfNecessary; - allowVideoMixedMimeTypeAdaptiveness = initialValues.allowVideoMixedMimeTypeAdaptiveness; - allowVideoNonSeamlessAdaptiveness = initialValues.allowVideoNonSeamlessAdaptiveness; - allowVideoMixedDecoderSupportAdaptiveness = - initialValues.allowVideoMixedDecoderSupportAdaptiveness; - // Audio - exceedAudioConstraintsIfNecessary = initialValues.exceedAudioConstraintsIfNecessary; - allowAudioMixedMimeTypeAdaptiveness = initialValues.allowAudioMixedMimeTypeAdaptiveness; - allowAudioMixedSampleRateAdaptiveness = initialValues.allowAudioMixedSampleRateAdaptiveness; - allowAudioMixedChannelCountAdaptiveness = - initialValues.allowAudioMixedChannelCountAdaptiveness; - allowAudioMixedDecoderSupportAdaptiveness = - initialValues.allowAudioMixedDecoderSupportAdaptiveness; - // General - exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary; - tunnelingEnabled = initialValues.tunnelingEnabled; - allowMultipleAdaptiveSelections = initialValues.allowMultipleAdaptiveSelections; - // Overrides - selectionOverrides = cloneSelectionOverrides(initialValues.selectionOverrides); - rendererDisabledFlags = initialValues.rendererDisabledFlags.clone(); - } - - @SuppressWarnings("method.invocation") // Only setter are invoked. - private ParametersBuilder(Bundle bundle) { - super(bundle); - Parameters defaultValue = Parameters.DEFAULT_WITHOUT_CONTEXT; - // Video - setExceedVideoConstraintsIfNecessary( - bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY), - defaultValue.exceedVideoConstraintsIfNecessary)); - setAllowVideoMixedMimeTypeAdaptiveness( - bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS), - defaultValue.allowVideoMixedMimeTypeAdaptiveness)); - setAllowVideoNonSeamlessAdaptiveness( - bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS), - defaultValue.allowVideoNonSeamlessAdaptiveness)); - setAllowVideoMixedDecoderSupportAdaptiveness( - bundle.getBoolean( - Parameters.keyForField( - Parameters.FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), - defaultValue.allowVideoMixedDecoderSupportAdaptiveness)); - // Audio - setExceedAudioConstraintsIfNecessary( - bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY), - defaultValue.exceedAudioConstraintsIfNecessary)); - setAllowAudioMixedMimeTypeAdaptiveness( - bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS), - defaultValue.allowAudioMixedMimeTypeAdaptiveness)); - setAllowAudioMixedSampleRateAdaptiveness( - bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS), - defaultValue.allowAudioMixedSampleRateAdaptiveness)); - setAllowAudioMixedChannelCountAdaptiveness( - bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS), - defaultValue.allowAudioMixedChannelCountAdaptiveness)); - setAllowAudioMixedDecoderSupportAdaptiveness( - bundle.getBoolean( - Parameters.keyForField( - Parameters.FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), - defaultValue.allowAudioMixedDecoderSupportAdaptiveness)); - // General - setExceedRendererCapabilitiesIfNecessary( - bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY), - defaultValue.exceedRendererCapabilitiesIfNecessary)); - setTunnelingEnabled( - bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_TUNNELING_ENABLED), - defaultValue.tunnelingEnabled)); - setAllowMultipleAdaptiveSelections( - bundle.getBoolean( - Parameters.keyForField(Parameters.FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS), - defaultValue.allowMultipleAdaptiveSelections)); - // Overrides - selectionOverrides = new SparseArray<>(); - setSelectionOverridesFromBundle(bundle); - rendererDisabledFlags = - makeSparseBooleanArrayFromTrueKeys( - bundle.getIntArray( - Parameters.keyForField(Parameters.FIELD_RENDERER_DISABLED_INDICES))); + delegate = new Parameters.Builder(context); } @Override protected ParametersBuilder set(TrackSelectionParameters parameters) { - super.set(parameters); + delegate.set(parameters); return this; } @@ -257,51 +138,51 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Override public DefaultTrackSelector.ParametersBuilder setMaxVideoSizeSd() { - super.setMaxVideoSizeSd(); + delegate.setMaxVideoSizeSd(); return this; } @Override public DefaultTrackSelector.ParametersBuilder clearVideoSizeConstraints() { - super.clearVideoSizeConstraints(); + delegate.clearVideoSizeConstraints(); return this; } @Override public DefaultTrackSelector.ParametersBuilder setMaxVideoSize( int maxVideoWidth, int maxVideoHeight) { - super.setMaxVideoSize(maxVideoWidth, maxVideoHeight); + delegate.setMaxVideoSize(maxVideoWidth, maxVideoHeight); return this; } @Override public DefaultTrackSelector.ParametersBuilder setMaxVideoFrameRate(int maxVideoFrameRate) { - super.setMaxVideoFrameRate(maxVideoFrameRate); + delegate.setMaxVideoFrameRate(maxVideoFrameRate); return this; } @Override public DefaultTrackSelector.ParametersBuilder setMaxVideoBitrate(int maxVideoBitrate) { - super.setMaxVideoBitrate(maxVideoBitrate); + delegate.setMaxVideoBitrate(maxVideoBitrate); return this; } @Override public DefaultTrackSelector.ParametersBuilder setMinVideoSize( int minVideoWidth, int minVideoHeight) { - super.setMinVideoSize(minVideoWidth, minVideoHeight); + delegate.setMinVideoSize(minVideoWidth, minVideoHeight); return this; } @Override public DefaultTrackSelector.ParametersBuilder setMinVideoFrameRate(int minVideoFrameRate) { - super.setMinVideoFrameRate(minVideoFrameRate); + delegate.setMinVideoFrameRate(minVideoFrameRate); return this; } @Override public DefaultTrackSelector.ParametersBuilder setMinVideoBitrate(int minVideoBitrate) { - super.setMinVideoBitrate(minVideoBitrate); + delegate.setMinVideoBitrate(minVideoBitrate); return this; } @@ -315,7 +196,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public ParametersBuilder setExceedVideoConstraintsIfNecessary( boolean exceedVideoConstraintsIfNecessary) { - this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary; + delegate.setExceedVideoConstraintsIfNecessary(exceedVideoConstraintsIfNecessary); return this; } @@ -332,7 +213,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public ParametersBuilder setAllowVideoMixedMimeTypeAdaptiveness( boolean allowVideoMixedMimeTypeAdaptiveness) { - this.allowVideoMixedMimeTypeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness; + delegate.setAllowVideoMixedMimeTypeAdaptiveness(allowVideoMixedMimeTypeAdaptiveness); return this; } @@ -346,7 +227,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public ParametersBuilder setAllowVideoNonSeamlessAdaptiveness( boolean allowVideoNonSeamlessAdaptiveness) { - this.allowVideoNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness; + delegate.setAllowVideoNonSeamlessAdaptiveness(allowVideoNonSeamlessAdaptiveness); return this; } @@ -361,46 +242,47 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public ParametersBuilder setAllowVideoMixedDecoderSupportAdaptiveness( boolean allowVideoMixedDecoderSupportAdaptiveness) { - this.allowVideoMixedDecoderSupportAdaptiveness = allowVideoMixedDecoderSupportAdaptiveness; + delegate.setAllowVideoMixedDecoderSupportAdaptiveness( + allowVideoMixedDecoderSupportAdaptiveness); return this; } @Override public ParametersBuilder setViewportSizeToPhysicalDisplaySize( Context context, boolean viewportOrientationMayChange) { - super.setViewportSizeToPhysicalDisplaySize(context, viewportOrientationMayChange); + delegate.setViewportSizeToPhysicalDisplaySize(context, viewportOrientationMayChange); return this; } @Override public ParametersBuilder clearViewportSizeConstraints() { - super.clearViewportSizeConstraints(); + delegate.clearViewportSizeConstraints(); return this; } @Override public ParametersBuilder setViewportSize( int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { - super.setViewportSize(viewportWidth, viewportHeight, viewportOrientationMayChange); + delegate.setViewportSize(viewportWidth, viewportHeight, viewportOrientationMayChange); return this; } @Override public ParametersBuilder setPreferredVideoMimeType(@Nullable String mimeType) { - super.setPreferredVideoMimeType(mimeType); + delegate.setPreferredVideoMimeType(mimeType); return this; } @Override public ParametersBuilder setPreferredVideoMimeTypes(String... mimeTypes) { - super.setPreferredVideoMimeTypes(mimeTypes); + delegate.setPreferredVideoMimeTypes(mimeTypes); return this; } @Override public DefaultTrackSelector.ParametersBuilder setPreferredVideoRoleFlags( @RoleFlags int preferredVideoRoleFlags) { - super.setPreferredVideoRoleFlags(preferredVideoRoleFlags); + delegate.setPreferredVideoRoleFlags(preferredVideoRoleFlags); return this; } @@ -408,31 +290,31 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Override public ParametersBuilder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) { - super.setPreferredAudioLanguage(preferredAudioLanguage); + delegate.setPreferredAudioLanguage(preferredAudioLanguage); return this; } @Override public ParametersBuilder setPreferredAudioLanguages(String... preferredAudioLanguages) { - super.setPreferredAudioLanguages(preferredAudioLanguages); + delegate.setPreferredAudioLanguages(preferredAudioLanguages); return this; } @Override public ParametersBuilder setPreferredAudioRoleFlags(@C.RoleFlags int preferredAudioRoleFlags) { - super.setPreferredAudioRoleFlags(preferredAudioRoleFlags); + delegate.setPreferredAudioRoleFlags(preferredAudioRoleFlags); return this; } @Override public ParametersBuilder setMaxAudioChannelCount(int maxAudioChannelCount) { - super.setMaxAudioChannelCount(maxAudioChannelCount); + delegate.setMaxAudioChannelCount(maxAudioChannelCount); return this; } @Override public ParametersBuilder setMaxAudioBitrate(int maxAudioBitrate) { - super.setMaxAudioBitrate(maxAudioBitrate); + delegate.setMaxAudioBitrate(maxAudioBitrate); return this; } @@ -446,7 +328,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public ParametersBuilder setExceedAudioConstraintsIfNecessary( boolean exceedAudioConstraintsIfNecessary) { - this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary; + delegate.setExceedAudioConstraintsIfNecessary(exceedAudioConstraintsIfNecessary); return this; } @@ -461,7 +343,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public ParametersBuilder setAllowAudioMixedMimeTypeAdaptiveness( boolean allowAudioMixedMimeTypeAdaptiveness) { - this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness; + delegate.setAllowAudioMixedMimeTypeAdaptiveness(allowAudioMixedMimeTypeAdaptiveness); return this; } @@ -476,7 +358,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public ParametersBuilder setAllowAudioMixedSampleRateAdaptiveness( boolean allowAudioMixedSampleRateAdaptiveness) { - this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness; + delegate.setAllowAudioMixedSampleRateAdaptiveness(allowAudioMixedSampleRateAdaptiveness); return this; } @@ -491,7 +373,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public ParametersBuilder setAllowAudioMixedChannelCountAdaptiveness( boolean allowAudioMixedChannelCountAdaptiveness) { - this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness; + delegate.setAllowAudioMixedChannelCountAdaptiveness(allowAudioMixedChannelCountAdaptiveness); return this; } @@ -506,19 +388,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public ParametersBuilder setAllowAudioMixedDecoderSupportAdaptiveness( boolean allowAudioMixedDecoderSupportAdaptiveness) { - this.allowAudioMixedDecoderSupportAdaptiveness = allowAudioMixedDecoderSupportAdaptiveness; + delegate.setAllowAudioMixedDecoderSupportAdaptiveness( + allowAudioMixedDecoderSupportAdaptiveness); return this; } @Override public ParametersBuilder setPreferredAudioMimeType(@Nullable String mimeType) { - super.setPreferredAudioMimeType(mimeType); + delegate.setPreferredAudioMimeType(mimeType); return this; } @Override public ParametersBuilder setPreferredAudioMimeTypes(String... mimeTypes) { - super.setPreferredAudioMimeTypes(mimeTypes); + delegate.setPreferredAudioMimeTypes(mimeTypes); return this; } @@ -527,39 +410,39 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Override public ParametersBuilder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings( Context context) { - super.setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context); + delegate.setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context); return this; } @Override public ParametersBuilder setPreferredTextLanguage(@Nullable String preferredTextLanguage) { - super.setPreferredTextLanguage(preferredTextLanguage); + delegate.setPreferredTextLanguage(preferredTextLanguage); return this; } @Override public ParametersBuilder setPreferredTextLanguages(String... preferredTextLanguages) { - super.setPreferredTextLanguages(preferredTextLanguages); + delegate.setPreferredTextLanguages(preferredTextLanguages); return this; } @Override public ParametersBuilder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) { - super.setPreferredTextRoleFlags(preferredTextRoleFlags); + delegate.setPreferredTextRoleFlags(preferredTextRoleFlags); return this; } @Override public ParametersBuilder setIgnoredTextSelectionFlags( @C.SelectionFlags int ignoredTextSelectionFlags) { - super.setIgnoredTextSelectionFlags(ignoredTextSelectionFlags); + delegate.setIgnoredTextSelectionFlags(ignoredTextSelectionFlags); return this; } @Override public ParametersBuilder setSelectUndeterminedTextLanguage( boolean selectUndeterminedTextLanguage) { - super.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage); + delegate.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage); return this; } @@ -569,50 +452,51 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Deprecated public ParametersBuilder setDisabledTextTrackSelectionFlags( @C.SelectionFlags int disabledTextTrackSelectionFlags) { - return setIgnoredTextSelectionFlags(disabledTextTrackSelectionFlags); + delegate.setDisabledTextTrackSelectionFlags(disabledTextTrackSelectionFlags); + return this; } // General @Override public ParametersBuilder setForceLowestBitrate(boolean forceLowestBitrate) { - super.setForceLowestBitrate(forceLowestBitrate); + delegate.setForceLowestBitrate(forceLowestBitrate); return this; } @Override public ParametersBuilder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) { - super.setForceHighestSupportedBitrate(forceHighestSupportedBitrate); + delegate.setForceHighestSupportedBitrate(forceHighestSupportedBitrate); return this; } @Override public ParametersBuilder addOverride(TrackSelectionOverride override) { - super.addOverride(override); + delegate.addOverride(override); return this; } @Override public ParametersBuilder clearOverride(TrackGroup trackGroup) { - super.clearOverride(trackGroup); + delegate.clearOverride(trackGroup); return this; } @Override public ParametersBuilder setOverrideForType(TrackSelectionOverride override) { - super.setOverrideForType(override); + delegate.setOverrideForType(override); return this; } @Override public ParametersBuilder clearOverridesOfType(@C.TrackType int trackType) { - super.clearOverridesOfType(trackType); + delegate.clearOverridesOfType(trackType); return this; } @Override public ParametersBuilder clearOverrides() { - super.clearOverrides(); + delegate.clearOverrides(); return this; } @@ -623,13 +507,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Deprecated @SuppressWarnings("deprecation") public ParametersBuilder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) { - super.setDisabledTrackTypes(disabledTrackTypes); + delegate.setDisabledTrackTypes(disabledTrackTypes); return this; } @Override public ParametersBuilder setTrackTypeDisabled(@C.TrackType int trackType, boolean disabled) { - super.setTrackTypeDisabled(trackType, disabled); + delegate.setTrackTypeDisabled(trackType, disabled); return this; } @@ -647,7 +531,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public ParametersBuilder setExceedRendererCapabilitiesIfNecessary( boolean exceedRendererCapabilitiesIfNecessary) { - this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; + delegate.setExceedRendererCapabilitiesIfNecessary(exceedRendererCapabilitiesIfNecessary); return this; } @@ -666,7 +550,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return This builder. */ public ParametersBuilder setTunnelingEnabled(boolean tunnelingEnabled) { - this.tunnelingEnabled = tunnelingEnabled; + delegate.setTunnelingEnabled(tunnelingEnabled); return this; } @@ -678,7 +562,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public ParametersBuilder setAllowMultipleAdaptiveSelections( boolean allowMultipleAdaptiveSelections) { - this.allowMultipleAdaptiveSelections = allowMultipleAdaptiveSelections; + delegate.setAllowMultipleAdaptiveSelections(allowMultipleAdaptiveSelections); return this; } @@ -693,16 +577,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return This builder. */ public final ParametersBuilder setRendererDisabled(int rendererIndex, boolean disabled) { - if (rendererDisabledFlags.get(rendererIndex) == disabled) { - // The disabled flag is unchanged. - return this; - } - // Only true values are placed in the array to make it easier to check for equality. - if (disabled) { - rendererDisabledFlags.put(rendererIndex, true); - } else { - rendererDisabledFlags.delete(rendererIndex); - } + delegate.setRendererDisabled(rendererIndex, disabled); return this; } @@ -733,17 +608,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Deprecated public final ParametersBuilder setSelectionOverride( int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { - Map overrides = - selectionOverrides.get(rendererIndex); - if (overrides == null) { - overrides = new HashMap<>(); - selectionOverrides.put(rendererIndex, overrides); - } - if (overrides.containsKey(groups) && Util.areEqual(overrides.get(groups), override)) { - // The override is unchanged. - return this; - } - overrides.put(groups, override); + delegate.setSelectionOverride(rendererIndex, groups, override); return this; } @@ -758,16 +623,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Deprecated public final ParametersBuilder clearSelectionOverride( int rendererIndex, TrackGroupArray groups) { - Map overrides = - selectionOverrides.get(rendererIndex); - if (overrides == null || !overrides.containsKey(groups)) { - // Nothing to clear. - return this; - } - overrides.remove(groups); - if (overrides.isEmpty()) { - selectionOverrides.remove(rendererIndex); - } + delegate.clearSelectionOverride(rendererIndex, groups); return this; } @@ -780,13 +636,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Deprecated public final ParametersBuilder clearSelectionOverrides(int rendererIndex) { - Map overrides = - selectionOverrides.get(rendererIndex); - if (overrides == null || overrides.isEmpty()) { - // Nothing to clear. - return this; - } - selectionOverrides.remove(rendererIndex); + delegate.clearSelectionOverrides(rendererIndex); return this; } @@ -798,92 +648,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Deprecated public final ParametersBuilder clearSelectionOverrides() { - if (selectionOverrides.size() == 0) { - // Nothing to clear. - return this; - } - selectionOverrides.clear(); + delegate.clearSelectionOverrides(); return this; } /** Builds a {@link Parameters} instance with the selected values. */ @Override public Parameters build() { - return new Parameters(this); - } - - private void init(ParametersBuilder this) { - // Video - exceedVideoConstraintsIfNecessary = true; - allowVideoMixedMimeTypeAdaptiveness = false; - allowVideoNonSeamlessAdaptiveness = true; - allowVideoMixedDecoderSupportAdaptiveness = false; - // Audio - exceedAudioConstraintsIfNecessary = true; - allowAudioMixedMimeTypeAdaptiveness = false; - allowAudioMixedSampleRateAdaptiveness = false; - allowAudioMixedChannelCountAdaptiveness = false; - allowAudioMixedDecoderSupportAdaptiveness = false; - // General - exceedRendererCapabilitiesIfNecessary = true; - tunnelingEnabled = false; - allowMultipleAdaptiveSelections = true; - } - - private static SparseArray> - cloneSelectionOverrides( - SparseArray> selectionOverrides) { - SparseArray> clone = - new SparseArray<>(); - for (int i = 0; i < selectionOverrides.size(); i++) { - clone.put(selectionOverrides.keyAt(i), new HashMap<>(selectionOverrides.valueAt(i))); - } - return clone; - } - - private void setSelectionOverridesFromBundle(Bundle bundle) { - @Nullable - int[] rendererIndices = - bundle.getIntArray( - Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_RENDERER_INDICES)); - @Nullable - ArrayList trackGroupArrayBundles = - bundle.getParcelableArrayList( - Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS)); - List trackGroupArrays = - trackGroupArrayBundles == null - ? ImmutableList.of() - : BundleableUtil.fromBundleList(TrackGroupArray.CREATOR, trackGroupArrayBundles); - @Nullable - SparseArray selectionOverrideBundles = - bundle.getSparseParcelableArray( - Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES)); - SparseArray selectionOverrides = - selectionOverrideBundles == null - ? new SparseArray<>() - : BundleableUtil.fromBundleSparseArray( - SelectionOverride.CREATOR, selectionOverrideBundles); - - if (rendererIndices == null || rendererIndices.length != trackGroupArrays.size()) { - return; // Incorrect format, ignore all overrides. - } - for (int i = 0; i < rendererIndices.length; i++) { - int rendererIndex = rendererIndices[i]; - TrackGroupArray groups = trackGroupArrays.get(i); - @Nullable SelectionOverride selectionOverride = selectionOverrides.get(i); - setSelectionOverride(rendererIndex, groups, selectionOverride); - } - } - - private SparseBooleanArray makeSparseBooleanArrayFromTrueKeys(@Nullable int[] trueKeys) { - if (trueKeys == null) { - return new SparseBooleanArray(); - } - SparseBooleanArray sparseBooleanArray = new SparseBooleanArray(trueKeys.length); - for (int trueKey : trueKeys) { - sparseBooleanArray.append(trueKey, true); - } - return sparseBooleanArray; + return delegate.build(); } } @@ -892,6 +664,788 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public static final class Parameters extends TrackSelectionParameters implements Bundleable { + /** + * A builder for {@link Parameters}. See the {@link Parameters} documentation for explanations + * of the parameters that can be configured using this builder. + */ + public static final class Builder extends TrackSelectionParameters.Builder { + + // Video + private boolean exceedVideoConstraintsIfNecessary; + private boolean allowVideoMixedMimeTypeAdaptiveness; + private boolean allowVideoNonSeamlessAdaptiveness; + private boolean allowVideoMixedDecoderSupportAdaptiveness; + // Audio + private boolean exceedAudioConstraintsIfNecessary; + private boolean allowAudioMixedMimeTypeAdaptiveness; + private boolean allowAudioMixedSampleRateAdaptiveness; + private boolean allowAudioMixedChannelCountAdaptiveness; + private boolean allowAudioMixedDecoderSupportAdaptiveness; + // General + private boolean exceedRendererCapabilitiesIfNecessary; + private boolean tunnelingEnabled; + private boolean allowMultipleAdaptiveSelections; + // Overrides + private final SparseArray> + selectionOverrides; + private final SparseBooleanArray rendererDisabledFlags; + + /** + * @deprecated {@link Context} constraints will not be set using this constructor. Use {@link + * #Builder(Context)} instead. + */ + @Deprecated + @SuppressWarnings({"deprecation"}) + public Builder() { + super(); + selectionOverrides = new SparseArray<>(); + rendererDisabledFlags = new SparseBooleanArray(); + init(); + } + + /** + * Creates a builder with default initial values. + * + * @param context Any context. + */ + public Builder(Context context) { + super(context); + selectionOverrides = new SparseArray<>(); + rendererDisabledFlags = new SparseBooleanArray(); + init(); + } + + /** + * @param initialValues The {@link Parameters} from which the initial values of the builder + * are obtained. + */ + private Builder(Parameters initialValues) { + super(initialValues); + // Video + exceedVideoConstraintsIfNecessary = initialValues.exceedVideoConstraintsIfNecessary; + allowVideoMixedMimeTypeAdaptiveness = initialValues.allowVideoMixedMimeTypeAdaptiveness; + allowVideoNonSeamlessAdaptiveness = initialValues.allowVideoNonSeamlessAdaptiveness; + allowVideoMixedDecoderSupportAdaptiveness = + initialValues.allowVideoMixedDecoderSupportAdaptiveness; + // Audio + exceedAudioConstraintsIfNecessary = initialValues.exceedAudioConstraintsIfNecessary; + allowAudioMixedMimeTypeAdaptiveness = initialValues.allowAudioMixedMimeTypeAdaptiveness; + allowAudioMixedSampleRateAdaptiveness = initialValues.allowAudioMixedSampleRateAdaptiveness; + allowAudioMixedChannelCountAdaptiveness = + initialValues.allowAudioMixedChannelCountAdaptiveness; + allowAudioMixedDecoderSupportAdaptiveness = + initialValues.allowAudioMixedDecoderSupportAdaptiveness; + // General + exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary; + tunnelingEnabled = initialValues.tunnelingEnabled; + allowMultipleAdaptiveSelections = initialValues.allowMultipleAdaptiveSelections; + // Overrides + selectionOverrides = cloneSelectionOverrides(initialValues.selectionOverrides); + rendererDisabledFlags = initialValues.rendererDisabledFlags.clone(); + } + + @SuppressWarnings("method.invocation") // Only setter are invoked. + private Builder(Bundle bundle) { + super(bundle); + Parameters defaultValue = Parameters.DEFAULT_WITHOUT_CONTEXT; + // Video + setExceedVideoConstraintsIfNecessary( + bundle.getBoolean( + Parameters.keyForField(Parameters.FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY), + defaultValue.exceedVideoConstraintsIfNecessary)); + setAllowVideoMixedMimeTypeAdaptiveness( + bundle.getBoolean( + Parameters.keyForField(Parameters.FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS), + defaultValue.allowVideoMixedMimeTypeAdaptiveness)); + setAllowVideoNonSeamlessAdaptiveness( + bundle.getBoolean( + Parameters.keyForField(Parameters.FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS), + defaultValue.allowVideoNonSeamlessAdaptiveness)); + setAllowVideoMixedDecoderSupportAdaptiveness( + bundle.getBoolean( + Parameters.keyForField( + Parameters.FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), + defaultValue.allowVideoMixedDecoderSupportAdaptiveness)); + // Audio + setExceedAudioConstraintsIfNecessary( + bundle.getBoolean( + Parameters.keyForField(Parameters.FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY), + defaultValue.exceedAudioConstraintsIfNecessary)); + setAllowAudioMixedMimeTypeAdaptiveness( + bundle.getBoolean( + Parameters.keyForField(Parameters.FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS), + defaultValue.allowAudioMixedMimeTypeAdaptiveness)); + setAllowAudioMixedSampleRateAdaptiveness( + bundle.getBoolean( + Parameters.keyForField(Parameters.FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS), + defaultValue.allowAudioMixedSampleRateAdaptiveness)); + setAllowAudioMixedChannelCountAdaptiveness( + bundle.getBoolean( + Parameters.keyForField( + Parameters.FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS), + defaultValue.allowAudioMixedChannelCountAdaptiveness)); + setAllowAudioMixedDecoderSupportAdaptiveness( + bundle.getBoolean( + Parameters.keyForField( + Parameters.FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), + defaultValue.allowAudioMixedDecoderSupportAdaptiveness)); + // General + setExceedRendererCapabilitiesIfNecessary( + bundle.getBoolean( + Parameters.keyForField(Parameters.FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY), + defaultValue.exceedRendererCapabilitiesIfNecessary)); + setTunnelingEnabled( + bundle.getBoolean( + Parameters.keyForField(Parameters.FIELD_TUNNELING_ENABLED), + defaultValue.tunnelingEnabled)); + setAllowMultipleAdaptiveSelections( + bundle.getBoolean( + Parameters.keyForField(Parameters.FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS), + defaultValue.allowMultipleAdaptiveSelections)); + // Overrides + selectionOverrides = new SparseArray<>(); + setSelectionOverridesFromBundle(bundle); + rendererDisabledFlags = + makeSparseBooleanArrayFromTrueKeys( + bundle.getIntArray( + Parameters.keyForField(Parameters.FIELD_RENDERER_DISABLED_INDICES))); + } + + @Override + protected Builder set(TrackSelectionParameters parameters) { + super.set(parameters); + return this; + } + + // Video + + @Override + public Builder setMaxVideoSizeSd() { + super.setMaxVideoSizeSd(); + return this; + } + + @Override + public Builder clearVideoSizeConstraints() { + super.clearVideoSizeConstraints(); + return this; + } + + @Override + public Builder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { + super.setMaxVideoSize(maxVideoWidth, maxVideoHeight); + return this; + } + + @Override + public Builder setMaxVideoFrameRate(int maxVideoFrameRate) { + super.setMaxVideoFrameRate(maxVideoFrameRate); + return this; + } + + @Override + public Builder setMaxVideoBitrate(int maxVideoBitrate) { + super.setMaxVideoBitrate(maxVideoBitrate); + return this; + } + + @Override + public Builder setMinVideoSize(int minVideoWidth, int minVideoHeight) { + super.setMinVideoSize(minVideoWidth, minVideoHeight); + return this; + } + + @Override + public Builder setMinVideoFrameRate(int minVideoFrameRate) { + super.setMinVideoFrameRate(minVideoFrameRate); + return this; + } + + @Override + public Builder setMinVideoBitrate(int minVideoBitrate) { + super.setMinVideoBitrate(minVideoBitrate); + return this; + } + + /** + * Sets whether to exceed the {@link #setMaxVideoBitrate}, {@link #setMaxVideoSize(int, int)} + * and {@link #setMaxVideoFrameRate} constraints when no selection can be made otherwise. + * + * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no + * selection can be made otherwise. + * @return This builder. + */ + public Builder setExceedVideoConstraintsIfNecessary( + boolean exceedVideoConstraintsIfNecessary) { + this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary; + return this; + } + + /** + * Sets whether to allow adaptive video selections containing mixed MIME types. + * + *

    Adaptations between different MIME types may not be completely seamless, in which case + * {@link #setAllowVideoNonSeamlessAdaptiveness(boolean)} also needs to be {@code true} for + * mixed MIME type selections to be made. + * + * @param allowVideoMixedMimeTypeAdaptiveness Whether to allow adaptive video selections + * containing mixed MIME types. + * @return This builder. + */ + public Builder setAllowVideoMixedMimeTypeAdaptiveness( + boolean allowVideoMixedMimeTypeAdaptiveness) { + this.allowVideoMixedMimeTypeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness; + return this; + } + + /** + * Sets whether to allow adaptive video selections where adaptation may not be completely + * seamless. + * + * @param allowVideoNonSeamlessAdaptiveness Whether to allow adaptive video selections where + * adaptation may not be completely seamless. + * @return This builder. + */ + public Builder setAllowVideoNonSeamlessAdaptiveness( + boolean allowVideoNonSeamlessAdaptiveness) { + this.allowVideoNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness; + return this; + } + + /** + * Sets whether to allow adaptive video selections with mixed levels of {@link + * RendererCapabilities.DecoderSupport} and {@link + * RendererCapabilities.HardwareAccelerationSupport}. + * + * @param allowVideoMixedDecoderSupportAdaptiveness Whether to allow adaptive video selections + * with mixed levels of decoder and hardware acceleration support. + * @return This builder. + */ + public Builder setAllowVideoMixedDecoderSupportAdaptiveness( + boolean allowVideoMixedDecoderSupportAdaptiveness) { + this.allowVideoMixedDecoderSupportAdaptiveness = allowVideoMixedDecoderSupportAdaptiveness; + return this; + } + + @Override + public Builder setViewportSizeToPhysicalDisplaySize( + Context context, boolean viewportOrientationMayChange) { + super.setViewportSizeToPhysicalDisplaySize(context, viewportOrientationMayChange); + return this; + } + + @Override + public Builder clearViewportSizeConstraints() { + super.clearViewportSizeConstraints(); + return this; + } + + @Override + public Builder setViewportSize( + int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { + super.setViewportSize(viewportWidth, viewportHeight, viewportOrientationMayChange); + return this; + } + + @Override + public Builder setPreferredVideoMimeType(@Nullable String mimeType) { + super.setPreferredVideoMimeType(mimeType); + return this; + } + + @Override + public Builder setPreferredVideoMimeTypes(String... mimeTypes) { + super.setPreferredVideoMimeTypes(mimeTypes); + return this; + } + + @Override + public Builder setPreferredVideoRoleFlags(@RoleFlags int preferredVideoRoleFlags) { + super.setPreferredVideoRoleFlags(preferredVideoRoleFlags); + return this; + } + + // Audio + + @Override + public Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) { + super.setPreferredAudioLanguage(preferredAudioLanguage); + return this; + } + + @Override + public Builder setPreferredAudioLanguages(String... preferredAudioLanguages) { + super.setPreferredAudioLanguages(preferredAudioLanguages); + return this; + } + + @Override + public Builder setPreferredAudioRoleFlags(@C.RoleFlags int preferredAudioRoleFlags) { + super.setPreferredAudioRoleFlags(preferredAudioRoleFlags); + return this; + } + + @Override + public Builder setMaxAudioChannelCount(int maxAudioChannelCount) { + super.setMaxAudioChannelCount(maxAudioChannelCount); + return this; + } + + @Override + public Builder setMaxAudioBitrate(int maxAudioBitrate) { + super.setMaxAudioBitrate(maxAudioBitrate); + return this; + } + + /** + * Sets whether to exceed the {@link #setMaxAudioChannelCount(int)} and {@link + * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise. + * + * @param exceedAudioConstraintsIfNecessary Whether to exceed audio constraints when no + * selection can be made otherwise. + * @return This builder. + */ + public Builder setExceedAudioConstraintsIfNecessary( + boolean exceedAudioConstraintsIfNecessary) { + this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary; + return this; + } + + /** + * Sets whether to allow adaptive audio selections containing mixed MIME types. + * + *

    Adaptations between different MIME types may not be completely seamless. + * + * @param allowAudioMixedMimeTypeAdaptiveness Whether to allow adaptive audio selections + * containing mixed MIME types. + * @return This builder. + */ + public Builder setAllowAudioMixedMimeTypeAdaptiveness( + boolean allowAudioMixedMimeTypeAdaptiveness) { + this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness; + return this; + } + + /** + * Sets whether to allow adaptive audio selections containing mixed sample rates. + * + *

    Adaptations between different sample rates may not be completely seamless. + * + * @param allowAudioMixedSampleRateAdaptiveness Whether to allow adaptive audio selections + * containing mixed sample rates. + * @return This builder. + */ + public Builder setAllowAudioMixedSampleRateAdaptiveness( + boolean allowAudioMixedSampleRateAdaptiveness) { + this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness; + return this; + } + + /** + * Sets whether to allow adaptive audio selections containing mixed channel counts. + * + *

    Adaptations between different channel counts may not be completely seamless. + * + * @param allowAudioMixedChannelCountAdaptiveness Whether to allow adaptive audio selections + * containing mixed channel counts. + * @return This builder. + */ + public Builder setAllowAudioMixedChannelCountAdaptiveness( + boolean allowAudioMixedChannelCountAdaptiveness) { + this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness; + return this; + } + + /** + * Sets whether to allow adaptive audio selections with mixed levels of {@link + * RendererCapabilities.DecoderSupport} and {@link + * RendererCapabilities.HardwareAccelerationSupport}. + * + * @param allowAudioMixedDecoderSupportAdaptiveness Whether to allow adaptive audio selections + * with mixed levels of decoder and hardware acceleration support. + * @return This builder. + */ + public Builder setAllowAudioMixedDecoderSupportAdaptiveness( + boolean allowAudioMixedDecoderSupportAdaptiveness) { + this.allowAudioMixedDecoderSupportAdaptiveness = allowAudioMixedDecoderSupportAdaptiveness; + return this; + } + + @Override + public Builder setPreferredAudioMimeType(@Nullable String mimeType) { + super.setPreferredAudioMimeType(mimeType); + return this; + } + + @Override + public Builder setPreferredAudioMimeTypes(String... mimeTypes) { + super.setPreferredAudioMimeTypes(mimeTypes); + return this; + } + + // Text + + @Override + public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings( + Context context) { + super.setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context); + return this; + } + + @Override + public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) { + super.setPreferredTextLanguage(preferredTextLanguage); + return this; + } + + @Override + public Builder setPreferredTextLanguages(String... preferredTextLanguages) { + super.setPreferredTextLanguages(preferredTextLanguages); + return this; + } + + @Override + public Builder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) { + super.setPreferredTextRoleFlags(preferredTextRoleFlags); + return this; + } + + @Override + public Builder setIgnoredTextSelectionFlags(@C.SelectionFlags int ignoredTextSelectionFlags) { + super.setIgnoredTextSelectionFlags(ignoredTextSelectionFlags); + return this; + } + + @Override + public Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) { + super.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage); + return this; + } + + /** + * @deprecated Use {@link #setIgnoredTextSelectionFlags}. + */ + @Deprecated + public Builder setDisabledTextTrackSelectionFlags( + @C.SelectionFlags int disabledTextTrackSelectionFlags) { + return setIgnoredTextSelectionFlags(disabledTextTrackSelectionFlags); + } + + // General + + @Override + public Builder setForceLowestBitrate(boolean forceLowestBitrate) { + super.setForceLowestBitrate(forceLowestBitrate); + return this; + } + + @Override + public Builder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) { + super.setForceHighestSupportedBitrate(forceHighestSupportedBitrate); + return this; + } + + @Override + public Builder addOverride(TrackSelectionOverride override) { + super.addOverride(override); + return this; + } + + @Override + public Builder clearOverride(TrackGroup trackGroup) { + super.clearOverride(trackGroup); + return this; + } + + @Override + public Builder setOverrideForType(TrackSelectionOverride override) { + super.setOverrideForType(override); + return this; + } + + @Override + public Builder clearOverridesOfType(@C.TrackType int trackType) { + super.clearOverridesOfType(trackType); + return this; + } + + @Override + public Builder clearOverrides() { + super.clearOverrides(); + return this; + } + + /** + * @deprecated Use {@link #setTrackTypeDisabled(int, boolean)}. + */ + @Override + @Deprecated + @SuppressWarnings("deprecation") + public Builder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) { + super.setDisabledTrackTypes(disabledTrackTypes); + return this; + } + + @Override + public Builder setTrackTypeDisabled(@C.TrackType int trackType, boolean disabled) { + super.setTrackTypeDisabled(trackType, disabled); + return this; + } + + /** + * Sets whether to exceed renderer capabilities when no selection can be made otherwise. + * + *

    This parameter applies when all of the tracks available for a renderer exceed the + * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality + * track will still be selected. Playback may succeed if the renderer has under-reported its + * true capabilities. If {@code false} then no track will be selected. + * + * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when + * no selection can be made otherwise. + * @return This builder. + */ + public Builder setExceedRendererCapabilitiesIfNecessary( + boolean exceedRendererCapabilitiesIfNecessary) { + this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; + return this; + } + + /** + * Sets whether to enable tunneling if possible. Tunneling will only be enabled if it's + * supported by the audio and video renderers for the selected tracks. + * + *

    Tunneling is known to have many device specific issues and limitations. Manual testing + * is strongly recommended to check that the media plays correctly when this option is + * enabled. See [#9661](https://github.com/google/ExoPlayer/issues/9661), + * [#9133](https://github.com/google/ExoPlayer/issues/9133), + * [#9317](https://github.com/google/ExoPlayer/issues/9317), + * [#9502](https://github.com/google/ExoPlayer/issues/9502). + * + * @param tunnelingEnabled Whether to enable tunneling if possible. + * @return This builder. + */ + public Builder setTunnelingEnabled(boolean tunnelingEnabled) { + this.tunnelingEnabled = tunnelingEnabled; + return this; + } + + /** + * Sets whether multiple adaptive selections with more than one track are allowed. + * + * @param allowMultipleAdaptiveSelections Whether multiple adaptive selections are allowed. + * @return This builder. + */ + public Builder setAllowMultipleAdaptiveSelections(boolean allowMultipleAdaptiveSelections) { + this.allowMultipleAdaptiveSelections = allowMultipleAdaptiveSelections; + return this; + } + + // Overrides + + /** + * Sets whether the renderer at the specified index is disabled. Disabling a renderer prevents + * the selector from selecting any tracks for it. + * + * @param rendererIndex The renderer index. + * @param disabled Whether the renderer is disabled. + * @return This builder. + */ + public final Builder setRendererDisabled(int rendererIndex, boolean disabled) { + if (rendererDisabledFlags.get(rendererIndex) == disabled) { + // The disabled flag is unchanged. + return this; + } + // Only true values are placed in the array to make it easier to check for equality. + if (disabled) { + rendererDisabledFlags.put(rendererIndex, true); + } else { + rendererDisabledFlags.delete(rendererIndex); + } + return this; + } + + /** + * Overrides the track selection for the renderer at the specified index. + * + *

    When the {@link TrackGroupArray} mapped to the renderer matches the one provided, the + * override is applied. When the {@link TrackGroupArray} does not match, the override has no + * effect. The override replaces any previous override for the specified {@link + * TrackGroupArray} for the specified {@link Renderer}. + * + *

    Passing a {@code null} override will cause the renderer to be disabled when the {@link + * TrackGroupArray} mapped to it matches the one provided. When the {@link TrackGroupArray} + * does not match a {@code null} override has no effect. Hence a {@code null} override differs + * from disabling the renderer using {@link #setRendererDisabled(int, boolean)} because the + * renderer is disabled conditionally on the {@link TrackGroupArray} mapped to it, where-as + * {@link #setRendererDisabled(int, boolean)} disables the renderer unconditionally. + * + *

    To remove overrides use {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link + * #clearSelectionOverrides(int)} or {@link #clearSelectionOverrides()}. + * + * @param rendererIndex The renderer index. + * @param groups The {@link TrackGroupArray} for which the override should be applied. + * @param override The override. + * @return This builder. + * @deprecated Use {@link + * TrackSelectionParameters.Builder#addOverride(TrackSelectionOverride)}. + */ + @Deprecated + public final Builder setSelectionOverride( + int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { + Map overrides = + selectionOverrides.get(rendererIndex); + if (overrides == null) { + overrides = new HashMap<>(); + selectionOverrides.put(rendererIndex, overrides); + } + if (overrides.containsKey(groups) && Util.areEqual(overrides.get(groups), override)) { + // The override is unchanged. + return this; + } + overrides.put(groups, override); + return this; + } + + /** + * Clears a track selection override for the specified renderer and {@link TrackGroupArray}. + * + * @param rendererIndex The renderer index. + * @param groups The {@link TrackGroupArray} for which the override should be cleared. + * @return This builder. + * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverride(TrackGroup)}. + */ + @Deprecated + public final Builder clearSelectionOverride(int rendererIndex, TrackGroupArray groups) { + Map overrides = + selectionOverrides.get(rendererIndex); + if (overrides == null || !overrides.containsKey(groups)) { + // Nothing to clear. + return this; + } + overrides.remove(groups); + if (overrides.isEmpty()) { + selectionOverrides.remove(rendererIndex); + } + return this; + } + + /** + * Clears all track selection overrides for the specified renderer. + * + * @param rendererIndex The renderer index. + * @return This builder. + * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverridesOfType(int)}. + */ + @Deprecated + public final Builder clearSelectionOverrides(int rendererIndex) { + Map overrides = + selectionOverrides.get(rendererIndex); + if (overrides == null || overrides.isEmpty()) { + // Nothing to clear. + return this; + } + selectionOverrides.remove(rendererIndex); + return this; + } + + /** + * Clears all track selection overrides for all renderers. + * + * @return This builder. + * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverrides()}. + */ + @Deprecated + public final Builder clearSelectionOverrides() { + if (selectionOverrides.size() == 0) { + // Nothing to clear. + return this; + } + selectionOverrides.clear(); + return this; + } + + /** Builds a {@link Parameters} instance with the selected values. */ + @Override + public Parameters build() { + return new Parameters(this); + } + + private void init(Builder this) { + // Video + exceedVideoConstraintsIfNecessary = true; + allowVideoMixedMimeTypeAdaptiveness = false; + allowVideoNonSeamlessAdaptiveness = true; + allowVideoMixedDecoderSupportAdaptiveness = false; + // Audio + exceedAudioConstraintsIfNecessary = true; + allowAudioMixedMimeTypeAdaptiveness = false; + allowAudioMixedSampleRateAdaptiveness = false; + allowAudioMixedChannelCountAdaptiveness = false; + allowAudioMixedDecoderSupportAdaptiveness = false; + // General + exceedRendererCapabilitiesIfNecessary = true; + tunnelingEnabled = false; + allowMultipleAdaptiveSelections = true; + } + + private static SparseArray> + cloneSelectionOverrides( + SparseArray> + selectionOverrides) { + SparseArray> clone = + new SparseArray<>(); + for (int i = 0; i < selectionOverrides.size(); i++) { + clone.put(selectionOverrides.keyAt(i), new HashMap<>(selectionOverrides.valueAt(i))); + } + return clone; + } + + private void setSelectionOverridesFromBundle(Bundle bundle) { + @Nullable + int[] rendererIndices = + bundle.getIntArray( + Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_RENDERER_INDICES)); + @Nullable + ArrayList trackGroupArrayBundles = + bundle.getParcelableArrayList( + Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS)); + List trackGroupArrays = + trackGroupArrayBundles == null + ? ImmutableList.of() + : BundleableUtil.fromBundleList(TrackGroupArray.CREATOR, trackGroupArrayBundles); + @Nullable + SparseArray selectionOverrideBundles = + bundle.getSparseParcelableArray( + Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES)); + SparseArray selectionOverrides = + selectionOverrideBundles == null + ? new SparseArray<>() + : BundleableUtil.fromBundleSparseArray( + SelectionOverride.CREATOR, selectionOverrideBundles); + + if (rendererIndices == null || rendererIndices.length != trackGroupArrays.size()) { + return; // Incorrect format, ignore all overrides. + } + for (int i = 0; i < rendererIndices.length; i++) { + int rendererIndex = rendererIndices[i]; + TrackGroupArray groups = trackGroupArrays.get(i); + @Nullable SelectionOverride selectionOverride = selectionOverrides.get(i); + setSelectionOverride(rendererIndex, groups, selectionOverride); + } + } + + private SparseBooleanArray makeSparseBooleanArrayFromTrueKeys(@Nullable int[] trueKeys) { + if (trueKeys == null) { + return new SparseBooleanArray(); + } + SparseBooleanArray sparseBooleanArray = new SparseBooleanArray(trueKeys.length); + for (int trueKey : trueKeys) { + sparseBooleanArray.append(trueKey, true); + } + return sparseBooleanArray; + } + } + /** * An instance with default values, except those obtained from the {@link Context}. * @@ -900,16 +1454,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { *

    This instance will not have the following settings: * *

      - *
    • {@link ParametersBuilder#setViewportSizeToPhysicalDisplaySize(Context, boolean) - * Viewport constraints} configured for the primary display. - *
    • {@link - * ParametersBuilder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context) + *
    • {@linkplain Builder#setViewportSizeToPhysicalDisplaySize(Context, boolean) Viewport + * constraints} configured for the primary display. + *
    • {@linkplain + * Builder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context) * Preferred text language and role flags} configured to the accessibility settings of * {@link android.view.accessibility.CaptioningManager}. *
    */ @SuppressWarnings("deprecation") - public static final Parameters DEFAULT_WITHOUT_CONTEXT = new ParametersBuilder().build(); + public static final Parameters DEFAULT_WITHOUT_CONTEXT = new Builder().build(); /** * @deprecated This instance is not configured using {@link Context} constraints. Use {@link * #getDefaults(Context)} instead. @@ -918,7 +1472,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** Returns an instance configured with default values. */ public static Parameters getDefaults(Context context) { - return new ParametersBuilder(context).build(); + return new Parameters.Builder(context).build(); } // Video @@ -1000,7 +1554,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; - private Parameters(ParametersBuilder builder) { + private Parameters(Builder builder) { super(builder); // Video exceedVideoConstraintsIfNecessary = builder.exceedVideoConstraintsIfNecessary; @@ -1039,8 +1593,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param groups The {@link TrackGroupArray}. * @return Whether there is an override. * @deprecated Only works to retrieve the overrides set with the deprecated {@link - * ParametersBuilder#setSelectionOverride(int, TrackGroupArray, SelectionOverride)}. Use - * {@link TrackSelectionParameters#overrides} instead. + * Builder#setSelectionOverride(int, TrackGroupArray, SelectionOverride)}. Use {@link + * TrackSelectionParameters#overrides} instead. */ @Deprecated public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { @@ -1057,8 +1611,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param groups The {@link TrackGroupArray}. * @return The override, or null if no override exists. * @deprecated Only works to retrieve the overrides set with the deprecated {@link - * ParametersBuilder#setSelectionOverride(int, TrackGroupArray, SelectionOverride)}. Use - * {@link TrackSelectionParameters#overrides} instead. + * Builder#setSelectionOverride(int, TrackGroupArray, SelectionOverride)}. Use {@link + * TrackSelectionParameters#overrides} instead. */ @Deprecated @Nullable @@ -1069,10 +1623,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { return overrides != null ? overrides.get(groups) : null; } - /** Creates a new {@link ParametersBuilder}, copying the initial values from this instance. */ + /** Creates a new {@link Parameters.Builder}, copying the initial values from this instance. */ @Override - public ParametersBuilder buildUpon() { - return new ParametersBuilder(this); + public Parameters.Builder buildUpon() { + return new Parameters.Builder(this); } @SuppressWarnings("EqualsGetClass") // Class is not final for backward-compatibility reason. @@ -1231,7 +1785,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** Object that can restore {@code Parameters} from a {@link Bundle}. */ public static final Creator CREATOR = - bundle -> new ParametersBuilder(bundle).build(); + bundle -> new Parameters.Builder(bundle).build(); private static String keyForField(@FieldNumber int field) { return Integer.toString(field, Character.MAX_RADIX); @@ -1539,21 +2093,30 @@ public class DefaultTrackSelector extends MappingTrackSelector { } // Only add the fields of `TrackSelectionParameters` to `parameters`. Parameters mergedParameters = - new ParametersBuilder(parametersReference.get()).set(parameters).build(); + new Parameters.Builder(parametersReference.get()).set(parameters).build(); setParametersInternal(mergedParameters); } + /** + * @deprecated Use {@link #setParameters(Parameters.Builder)} instead. + */ + @Deprecated + @SuppressWarnings("deprecation") // Allow setting the deprecated builder + public void setParameters(ParametersBuilder parametersBuilder) { + setParametersInternal(parametersBuilder.build()); + } + /** * Atomically sets the provided parameters for track selection. * * @param parametersBuilder A builder from which to obtain the parameters for track selection. */ - public void setParameters(ParametersBuilder parametersBuilder) { + public void setParameters(Parameters.Builder parametersBuilder) { setParametersInternal(parametersBuilder.build()); } - /** Returns a new {@link ParametersBuilder} initialized with the current selection parameters. */ - public ParametersBuilder buildUponParameters() { + /** Returns a new {@link Parameters.Builder} initialized with the current selection parameters. */ + public Parameters.Builder buildUponParameters() { return getParameters().buildUpon(); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectionUtil.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectionUtil.java index f5581cc863..6d8e3b4942 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectionUtil.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelectionUtil.java @@ -98,7 +98,7 @@ public final class TrackSelectionUtil { TrackGroupArray trackGroupArray, boolean isDisabled, @Nullable SelectionOverride override) { - DefaultTrackSelector.ParametersBuilder builder = + DefaultTrackSelector.Parameters.Builder builder = parameters .buildUpon() .clearSelectionOverrides(rendererIndex) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java index a9576fe88d..60d69cdb99 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java @@ -41,6 +41,7 @@ import androidx.media3.common.MimeTypes; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackSelectionOverride; +import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.Tracks; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.ExoPlaybackException; @@ -50,17 +51,19 @@ import androidx.media3.exoplayer.RendererConfiguration; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.Parameters; -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.ParametersBuilder; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride; import androidx.media3.exoplayer.trackselection.TrackSelector.InvalidationListener; import androidx.media3.exoplayer.upstream.BandwidthMeter; import androidx.media3.test.utils.FakeTimeline; +import androidx.media3.test.utils.TestUtil; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import java.lang.reflect.Method; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.After; import org.junit.Before; @@ -516,7 +519,7 @@ public final class DefaultTrackSelectorTest { */ @Test public void setParameterWithNonDefaultParameterNotifyInvalidationListener() { - ParametersBuilder builder = defaultParameters.buildUpon().setPreferredAudioLanguage("eng"); + Parameters.Builder builder = defaultParameters.buildUpon().setPreferredAudioLanguage("eng"); trackSelector.setParameters(builder); verify(invalidationListener).onTrackSelectionsInvalidated(); } @@ -528,7 +531,7 @@ public final class DefaultTrackSelectorTest { */ @Test public void setParameterWithSameParametersDoesNotNotifyInvalidationListenerAgain() { - ParametersBuilder builder = defaultParameters.buildUpon().setPreferredAudioLanguage("eng"); + Parameters.Builder builder = defaultParameters.buildUpon().setPreferredAudioLanguage("eng"); trackSelector.setParameters(builder); trackSelector.setParameters(builder); verify(invalidationListener, times(1)).onTrackSelectionsInvalidated(); @@ -1268,7 +1271,7 @@ public final class DefaultTrackSelectorTest { result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections[0], trackGroups, undeterminedUnd); - ParametersBuilder builder = defaultParameters.buildUpon().setPreferredTextLanguage("spa"); + Parameters.Builder builder = defaultParameters.buildUpon().setPreferredTextLanguage("spa"); trackSelector.setParameters(builder); result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections[0], trackGroups, spanish); @@ -2264,6 +2267,30 @@ public final class DefaultTrackSelectorTest { assertThat(selectionOverrideFromBundle).isEqualTo(selectionOverrideToBundle); } + /** + * The deprecated {@link DefaultTrackSelector.ParametersBuilder} is implemented by delegating to + * an instance of {@link DefaultTrackSelector.Parameters.Builder}. However, it also extends + * {@link TrackSelectionParameters.Builder}, and for the delegation-pattern to work correctly it + * needs to override every setter method from the superclass (otherwise the setter won't be + * propagated to the delegate). This test ensures that invariant. + * + *

    The test can be removed when the deprecated {@link DefaultTrackSelector.ParametersBuilder} + * is removed. + */ + @SuppressWarnings("deprecation") // Testing deprecated builder + @Test + public void deprecatedParametersBuilderOverridesAllTrackSelectionParametersBuilderMethods() + throws Exception { + List methods = TestUtil.getPublicMethods(TrackSelectionParameters.Builder.class); + for (Method method : methods) { + assertThat( + DefaultTrackSelector.ParametersBuilder.class + .getDeclaredMethod(method.getName(), method.getParameterTypes()) + .getDeclaringClass()) + .isEqualTo(DefaultTrackSelector.ParametersBuilder.class); + } + } + private static void assertSelections(TrackSelectorResult result, TrackSelection[] expected) { assertThat(result.length).isEqualTo(expected.length); for (int i = 0; i < expected.length; i++) { diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java index 11a6bf5a8c..a59377c27d 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java @@ -25,6 +25,7 @@ import android.graphics.BitmapFactory; import android.graphics.Color; import android.media.MediaCodec; import android.net.Uri; +import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Timeline; import androidx.media3.common.util.Assertions; @@ -47,9 +48,17 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Queue; import java.util.Random; +import java.util.Set; /** Utility methods for tests. */ @UnstableApi @@ -454,4 +463,35 @@ public class TestUtil { buffer.data.flip(); return buffer; } + + /** Returns all the public methods of a Java class (except those defined by {@link Object}). */ + public static List getPublicMethods(Class clazz) { + // Run a BFS over all extended types to inspect them all. + Queue> supertypeQueue = new ArrayDeque<>(); + supertypeQueue.add(clazz); + Set> supertypes = new HashSet<>(); + Object object = new Object(); + while (!supertypeQueue.isEmpty()) { + Class currentSupertype = supertypeQueue.remove(); + if (supertypes.add(currentSupertype)) { + @Nullable Class superclass = currentSupertype.getSuperclass(); + if (superclass != null && !superclass.isInstance(object)) { + supertypeQueue.add(superclass); + } + + Collections.addAll(supertypeQueue, currentSupertype.getInterfaces()); + } + } + + List list = new ArrayList<>(); + for (Class supertype : supertypes) { + for (Method method : supertype.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + list.add(method); + } + } + } + + return list; + } } From c22cd449007b5ea3ed79e3fcdaea45672f51988c Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 7 Jun 2022 09:18:36 +0000 Subject: [PATCH 13/45] Fix release notes formatting #minor-release PiperOrigin-RevId: 453384451 (cherry picked from commit 14ea2e4c5146ed57d51fe4a69f6b29a9f3954115) --- RELEASENOTES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e59eb25ddb..4678b2a8fd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -144,6 +144,10 @@ * Data sources: * Rename `DummyDataSource` to `PlaceholderDataSource`. * Workaround OkHttp interrupt handling. +* FFmpeg extension: + * Update CMake version to `3.21.0+` to avoid a CMake bug causing + AndroidStudio's gradle sync to fail + ([#9933](https://github.com/google/ExoPlayer/issues/9933)). * Remove deprecated symbols: * Remove `Player.Listener.onTracksChanged`. Use `Player.Listener.onTracksInfoChanged` instead. @@ -157,10 +161,6 @@ `DEFAULT_TRACK_SELECTOR_PARAMETERS` constants. Use `getDefaultTrackSelectorParameters(Context)` instead when possible, and `DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT` otherwise. - * FFmpeg extension: - * Update CMake version to `3.21.0+` to avoid a CMake bug causing - AndroidStudio's gradle sync to fail - ([#9933](https://github.com/google/ExoPlayer/issues/9933)). ### 1.0.0-alpha03 (2022-03-14) From 240379cc0245bc3688f9ce0fd7119da764bd0bc7 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 7 Jun 2022 09:27:04 +0000 Subject: [PATCH 14/45] Use a shared `keyForField` implementation in track selection parameters The current setup with distinct, private `keyForField` implementations, leaves open the (theoretical) possibility of a clash in the `Bundle` keys used by the superclass and subclass. This change brings consistency with our only other extensible `Bundleable` type (`PlaybackException`). #minor-release PiperOrigin-RevId: 453385875 (cherry picked from commit 814e43dbb9432e3adf6c4c278df50433260a461b) --- .../common/TrackSelectionParameters.java | 59 +++++---------- .../trackselection/DefaultTrackSelector.java | 72 +++++++------------ 2 files changed, 42 insertions(+), 89 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java index e7c4cbbc50..b7fe8176e3 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java @@ -24,7 +24,6 @@ import android.graphics.Point; import android.os.Bundle; import android.os.Looper; import android.view.accessibility.CaptioningManager; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.media3.common.util.BundleableUtil; @@ -34,9 +33,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Ints; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -1073,42 +1069,6 @@ public class TrackSelectionParameters implements Bundleable { // Bundleable implementation - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - // Video - FIELD_MAX_VIDEO_WIDTH, - FIELD_MAX_VIDEO_HEIGHT, - FIELD_MAX_VIDEO_FRAMERATE, - FIELD_MAX_VIDEO_BITRATE, - FIELD_MIN_VIDEO_WIDTH, - FIELD_MIN_VIDEO_HEIGHT, - FIELD_MIN_VIDEO_FRAMERATE, - FIELD_MIN_VIDEO_BITRATE, - FIELD_VIEWPORT_WIDTH, - FIELD_VIEWPORT_HEIGHT, - FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE, - FIELD_PREFERRED_VIDEO_MIMETYPES, - FIELD_PREFERRED_VIDEO_ROLE_FLAGS, - // Audio - FIELD_PREFERRED_AUDIO_LANGUAGES, - FIELD_PREFERRED_AUDIO_ROLE_FLAGS, - FIELD_MAX_AUDIO_CHANNEL_COUNT, - FIELD_MAX_AUDIO_BITRATE, - FIELD_PREFERRED_AUDIO_MIME_TYPES, - // Text - FIELD_PREFERRED_TEXT_LANGUAGES, - FIELD_PREFERRED_TEXT_ROLE_FLAGS, - FIELD_IGNORED_TEXT_SELECTION_FLAGS, - FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE, - // General - FIELD_FORCE_LOWEST_BITRATE, - FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE, - FIELD_SELECTION_OVERRIDES, - FIELD_DISABLED_TRACK_TYPE, - }) - private @interface FieldNumber {} - private static final int FIELD_PREFERRED_AUDIO_LANGUAGES = 1; private static final int FIELD_PREFERRED_AUDIO_ROLE_FLAGS = 2; private static final int FIELD_PREFERRED_TEXT_LANGUAGES = 3; @@ -1136,6 +1096,15 @@ public class TrackSelectionParameters implements Bundleable { private static final int FIELD_PREFERRED_VIDEO_ROLE_FLAGS = 25; private static final int FIELD_IGNORED_TEXT_SELECTION_FLAGS = 26; + /** + * Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()} + * and {@link Bundleable.Creator}. + * + *

    Subclasses should obtain keys for their {@link Bundle} representation by applying a + * non-negative offset on this constant and passing the result to {@link #keyForField(int)}. + */ + @UnstableApi protected static final int FIELD_CUSTOM_ID_BASE = 1000; + @Override public Bundle toBundle() { Bundle bundle = new Bundle(); @@ -1197,7 +1166,15 @@ public class TrackSelectionParameters implements Bundleable { public static final Creator CREATOR = TrackSelectionParameters::fromBundle; - private static String keyForField(@FieldNumber int field) { + /** + * Converts the given field number to a string which can be used as a field key when implementing + * {@link #toBundle()} and {@link Bundleable.Creator}. + * + *

    Subclasses should use {@code field} values greater than or equal to {@link + * #FIELD_CUSTOM_ID_BASE}. + */ + @UnstableApi + protected static String keyForField(int field) { return Integer.toString(field, Character.MAX_RADIX); } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index dbed6b4ffb..c85e40b24a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -1688,50 +1688,30 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Bundleable implementation. - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - // Video - FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY, - FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS, - FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS, - FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS, - // Audio - FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY, - FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS, - FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS, - FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS, - FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS, - // General - FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY, - FIELD_TUNNELING_ENABLED, - FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS, - // Overrides - FIELD_SELECTION_OVERRIDES_RENDERER_INDICES, - FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS, - FIELD_SELECTION_OVERRIDES, - FIELD_RENDERER_DISABLED_INDICES, - }) - private @interface FieldNumber {} - - // Start at 1000 to avoid conflict with the base class fields. - private static final int FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY = 1000; - private static final int FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS = 1001; - private static final int FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS = 1002; - private static final int FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY = 1003; - private static final int FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS = 1004; - private static final int FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS = 1005; - private static final int FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS = 1006; - private static final int FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY = 1007; - private static final int FIELD_TUNNELING_ENABLED = 1008; - private static final int FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS = 1009; - private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDICES = 1010; - private static final int FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS = 1011; - private static final int FIELD_SELECTION_OVERRIDES = 1012; - private static final int FIELD_RENDERER_DISABLED_INDICES = 1013; - private static final int FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = 1014; - private static final int FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = 1015; + private static final int FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY = FIELD_CUSTOM_ID_BASE; + private static final int FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS = + FIELD_CUSTOM_ID_BASE + 1; + private static final int FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS = FIELD_CUSTOM_ID_BASE + 2; + private static final int FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY = FIELD_CUSTOM_ID_BASE + 3; + private static final int FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS = + FIELD_CUSTOM_ID_BASE + 4; + private static final int FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS = + FIELD_CUSTOM_ID_BASE + 5; + private static final int FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS = + FIELD_CUSTOM_ID_BASE + 6; + private static final int FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY = + FIELD_CUSTOM_ID_BASE + 7; + private static final int FIELD_TUNNELING_ENABLED = FIELD_CUSTOM_ID_BASE + 8; + private static final int FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS = FIELD_CUSTOM_ID_BASE + 9; + private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDICES = FIELD_CUSTOM_ID_BASE + 10; + private static final int FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS = + FIELD_CUSTOM_ID_BASE + 11; + private static final int FIELD_SELECTION_OVERRIDES = FIELD_CUSTOM_ID_BASE + 12; + private static final int FIELD_RENDERER_DISABLED_INDICES = FIELD_CUSTOM_ID_BASE + 13; + private static final int FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = + FIELD_CUSTOM_ID_BASE + 14; + private static final int FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = + FIELD_CUSTOM_ID_BASE + 15; @Override public Bundle toBundle() { @@ -1787,10 +1767,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { public static final Creator CREATOR = bundle -> new Parameters.Builder(bundle).build(); - private static String keyForField(@FieldNumber int field) { - return Integer.toString(field, Character.MAX_RADIX); - } - /** * Bundles selection overrides in 3 arrays of equal length. Each triplet of matching indices is: * the selection override (stored in a sparse array as they can be null), the trackGroupArray of From 2ed7efb1c31bcd84d5ca4f1c0ea351b09ddf3f5d Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 7 Jun 2022 11:50:47 +0000 Subject: [PATCH 15/45] Minor fix: remove final from methods of final class #minor-release PiperOrigin-RevId: 453408087 (cherry picked from commit 62497f23fd8f92de304e7a87ffa839e221bd78c6) --- .../trackselection/DefaultTrackSelector.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index c85e40b24a..f680f58e19 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -576,7 +576,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param disabled Whether the renderer is disabled. * @return This builder. */ - public final ParametersBuilder setRendererDisabled(int rendererIndex, boolean disabled) { + public ParametersBuilder setRendererDisabled(int rendererIndex, boolean disabled) { delegate.setRendererDisabled(rendererIndex, disabled); return this; } @@ -606,7 +606,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @deprecated Use {@link TrackSelectionParameters.Builder#addOverride(TrackSelectionOverride)}. */ @Deprecated - public final ParametersBuilder setSelectionOverride( + public ParametersBuilder setSelectionOverride( int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { delegate.setSelectionOverride(rendererIndex, groups, override); return this; @@ -621,8 +621,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverride(TrackGroup)}. */ @Deprecated - public final ParametersBuilder clearSelectionOverride( - int rendererIndex, TrackGroupArray groups) { + public ParametersBuilder clearSelectionOverride(int rendererIndex, TrackGroupArray groups) { delegate.clearSelectionOverride(rendererIndex, groups); return this; } @@ -635,7 +634,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverridesOfType(int)}. */ @Deprecated - public final ParametersBuilder clearSelectionOverrides(int rendererIndex) { + public ParametersBuilder clearSelectionOverrides(int rendererIndex) { delegate.clearSelectionOverrides(rendererIndex); return this; } @@ -647,7 +646,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverrides()}. */ @Deprecated - public final ParametersBuilder clearSelectionOverrides() { + public ParametersBuilder clearSelectionOverrides() { delegate.clearSelectionOverrides(); return this; } @@ -1250,7 +1249,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param disabled Whether the renderer is disabled. * @return This builder. */ - public final Builder setRendererDisabled(int rendererIndex, boolean disabled) { + public Builder setRendererDisabled(int rendererIndex, boolean disabled) { if (rendererDisabledFlags.get(rendererIndex) == disabled) { // The disabled flag is unchanged. return this; @@ -1290,7 +1289,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * TrackSelectionParameters.Builder#addOverride(TrackSelectionOverride)}. */ @Deprecated - public final Builder setSelectionOverride( + public Builder setSelectionOverride( int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { Map overrides = selectionOverrides.get(rendererIndex); @@ -1315,7 +1314,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverride(TrackGroup)}. */ @Deprecated - public final Builder clearSelectionOverride(int rendererIndex, TrackGroupArray groups) { + public Builder clearSelectionOverride(int rendererIndex, TrackGroupArray groups) { Map overrides = selectionOverrides.get(rendererIndex); if (overrides == null || !overrides.containsKey(groups)) { @@ -1337,7 +1336,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverridesOfType(int)}. */ @Deprecated - public final Builder clearSelectionOverrides(int rendererIndex) { + public Builder clearSelectionOverrides(int rendererIndex) { Map overrides = selectionOverrides.get(rendererIndex); if (overrides == null || overrides.isEmpty()) { @@ -1355,7 +1354,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverrides()}. */ @Deprecated - public final Builder clearSelectionOverrides() { + public Builder clearSelectionOverrides() { if (selectionOverrides.size() == 0) { // Nothing to clear. return this; @@ -1582,7 +1581,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param rendererIndex The renderer index. * @return Whether the renderer is disabled. */ - public final boolean getRendererDisabled(int rendererIndex) { + public boolean getRendererDisabled(int rendererIndex) { return rendererDisabledFlags.get(rendererIndex); } @@ -1597,7 +1596,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * TrackSelectionParameters#overrides} instead. */ @Deprecated - public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { + public boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { @Nullable Map overrides = selectionOverrides.get(rendererIndex); @@ -1616,7 +1615,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Deprecated @Nullable - public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { + public SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { @Nullable Map overrides = selectionOverrides.get(rendererIndex); @@ -1629,7 +1628,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters.Builder(this); } - @SuppressWarnings("EqualsGetClass") // Class is not final for backward-compatibility reason. + @SuppressWarnings( + "EqualsGetClass") // Class extends TrackSelectionParameters for backwards compatibility. @Override public boolean equals(@Nullable Object obj) { if (this == obj) { From fd1eb4b46663dea3d00c2600ea737180758a86b3 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Thu, 9 Jun 2022 17:37:09 +0000 Subject: [PATCH 16/45] Merge pull request #53 from ittiam-systems:rtp_opus PiperOrigin-RevId: 453490088 (cherry picked from commit a2a450432904c3d8ecc25e00c7148e73f1923d6e) --- RELEASENOTES.md | 2 + .../exoplayer/rtsp/RtpPayloadFormat.java | 4 + .../media3/exoplayer/rtsp/RtspMediaTrack.java | 9 + .../DefaultRtpPayloadReaderFactory.java | 2 + .../exoplayer/rtsp/reader/RtpOpusReader.java | 157 ++++++++++++++ .../rtsp/reader/RtpOpusReaderTest.java | 200 ++++++++++++++++++ 6 files changed, 374 insertions(+) create mode 100644 libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReader.java create mode 100644 libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReaderTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4678b2a8fd..1c4dbbc843 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -127,6 +127,8 @@ ([#10165](https://github.com/google/ExoPlayer/issues/10165)). * Add RTP reader for VP9 ([#47](https://github.com/androidx/media/pull/64)). + * Add RTP reader for OPUS + ([#53](https://github.com/androidx/media/pull/53)). * Session: * Fix NPE in MediaControllerImplLegacy ([#59](https://github.com/androidx/media/pull/59)). diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java index 5f3e382a4b..39b7d6f0eb 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java @@ -46,6 +46,7 @@ public final class RtpPayloadFormat { private static final String RTP_MEDIA_MPEG4_VIDEO = "MP4V-ES"; private static final String RTP_MEDIA_H264 = "H264"; private static final String RTP_MEDIA_H265 = "H265"; + private static final String RTP_MEDIA_OPUS = "OPUS"; private static final String RTP_MEDIA_PCM_L8 = "L8"; private static final String RTP_MEDIA_PCM_L16 = "L16"; private static final String RTP_MEDIA_PCMA = "PCMA"; @@ -63,6 +64,7 @@ public final class RtpPayloadFormat { case RTP_MEDIA_H265: case RTP_MEDIA_MPEG4_VIDEO: case RTP_MEDIA_MPEG4_GENERIC: + case RTP_MEDIA_OPUS: case RTP_MEDIA_PCM_L8: case RTP_MEDIA_PCM_L16: case RTP_MEDIA_PCMA: @@ -92,6 +94,8 @@ public final class RtpPayloadFormat { return MimeTypes.AUDIO_AMR_WB; case RTP_MEDIA_MPEG4_GENERIC: return MimeTypes.AUDIO_AAC; + case RTP_MEDIA_OPUS: + return MimeTypes.AUDIO_OPUS; case RTP_MEDIA_PCM_L8: case RTP_MEDIA_PCM_L16: return MimeTypes.AUDIO_RAW; diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java index ad4b3e26e7..2a7310c470 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java @@ -101,6 +101,9 @@ import com.google.common.collect.ImmutableMap; */ private static final int DEFAULT_VP8_HEIGHT = 240; + /** RFC7587 Section 6.1 Sampling rate for OPUS is fixed at 48KHz. */ + private static final int OPUS_CLOCK_RATE = 48_000; + /** * Default width for VP9. * @@ -201,6 +204,12 @@ import com.google.common.collect.ImmutableMap; !fmtpParameters.containsKey(PARAMETER_AMR_INTERLEAVING), "Interleaving mode is not currently supported."); break; + case MimeTypes.AUDIO_OPUS: + checkArgument(channelCount != C.INDEX_UNSET); + // RFC7587 Section 6.1: the RTP timestamp is incremented with a 48000 Hz clock rate + // for all modes of Opus and all sampling rates. + checkArgument(clockRate == OPUS_CLOCK_RATE, "Invalid OPUS clock rate."); + break; case MimeTypes.VIDEO_MP4V: checkArgument(!fmtpParameters.isEmpty()); processMPEG4FmtpAttribute(formatBuilder, fmtpParameters); diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java index 3e3b8c0beb..7c09884475 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java @@ -39,6 +39,8 @@ import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; case MimeTypes.AUDIO_AMR_NB: case MimeTypes.AUDIO_AMR_WB: return new RtpAmrReader(payloadFormat); + case MimeTypes.AUDIO_OPUS: + return new RtpOpusReader(payloadFormat); case MimeTypes.AUDIO_RAW: case MimeTypes.AUDIO_ALAW: case MimeTypes.AUDIO_MLAW: diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReader.java new file mode 100644 index 0000000000..9b8994ae6d --- /dev/null +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReader.java @@ -0,0 +1,157 @@ +/* + * Copyright 2022 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 androidx.media3.exoplayer.rtsp.reader; + +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkStateNotNull; + +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.rtsp.RtpPacket; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.OpusUtil; +import androidx.media3.extractor.TrackOutput; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Parses an OPUS byte stream carried on RTP packets and extracts individual samples. Refer to + * RFC7845 for more details. + */ +/* package */ final class RtpOpusReader implements RtpPayloadReader { + private static final String TAG = "RtpOpusReader"; + /* Opus uses a fixed 48KHz media clock RFC7845 Section 4. */ + private static final long MEDIA_CLOCK_FREQUENCY = 48_000; + + private final RtpPayloadFormat payloadFormat; + + private @MonotonicNonNull TrackOutput trackOutput; + + /** + * First received RTP timestamp. All RTP timestamps are dimension-less, the time base is defined + * by {@link #MEDIA_CLOCK_FREQUENCY}. + */ + private long firstReceivedTimestamp; + + private long startTimeOffsetUs; + private int previousSequenceNumber; + private boolean foundOpusIDHeader; + private boolean foundOpusCommentHeader; + + /** Creates an instance. */ + public RtpOpusReader(RtpPayloadFormat payloadFormat) { + this.payloadFormat = payloadFormat; + this.firstReceivedTimestamp = C.INDEX_UNSET; + this.previousSequenceNumber = C.INDEX_UNSET; + } + + // RtpPayloadReader implementation. + + @Override + public void createTracks(ExtractorOutput extractorOutput, int trackId) { + trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_AUDIO); + trackOutput.format(payloadFormat.format); + } + + @Override + public void onReceivingFirstPacket(long timestamp, int sequenceNumber) { + this.firstReceivedTimestamp = timestamp; + } + + @Override + public void consume( + ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) { + checkStateNotNull(trackOutput); + + /* RFC7845 Section 3. + * +---------+ +----------------+ +--------------------+ +----- + * |ID Header| | Comment Header | |Audio Data Packet 1 | | ... + * +---------+ +----------------+ +--------------------+ +----- + */ + if (!foundOpusIDHeader) { + validateOpusIdHeader(data); + List initializationData = OpusUtil.buildInitializationData(data.getData()); + Format.Builder formatBuilder = payloadFormat.format.buildUpon(); + formatBuilder.setInitializationData(initializationData); + trackOutput.format(formatBuilder.build()); + foundOpusIDHeader = true; + } else if (!foundOpusCommentHeader) { + // Comment Header RFC7845 Section 5.2. + int sampleSize = data.limit(); + checkArgument(sampleSize >= 8, "Comment Header has insufficient data"); + String header = data.readString(8); + checkArgument(header.equals("OpusTags"), "Comment Header should follow ID Header"); + foundOpusCommentHeader = true; + } else { + // Check that this packet is in the sequence of the previous packet. + int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); + if (sequenceNumber != expectedSequenceNumber) { + Log.w( + TAG, + Util.formatInvariant( + "Received RTP packet with unexpected sequence number. Expected: %d; received: %d.", + expectedSequenceNumber, sequenceNumber)); + } + + // sending opus data. + int size = data.bytesLeft(); + trackOutput.sampleData(data, size); + long timeUs = toSampleTimeUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp); + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset*/ 0, /* cryptoData*/ null); + } + previousSequenceNumber = sequenceNumber; + } + + @Override + public void seek(long nextRtpTimestamp, long timeUs) { + firstReceivedTimestamp = nextRtpTimestamp; + startTimeOffsetUs = timeUs; + } + + // Internal methods. + + /** + * Validates the OPUS ID Header at {@code data}'s current position, throws {@link + * IllegalArgumentException} if the header is invalid. + * + *

    {@code data}'s position does not change after returning. + */ + private static void validateOpusIdHeader(ParsableByteArray data) { + int currPosition = data.getPosition(); + int sampleSize = data.limit(); + checkArgument(sampleSize > 18, "ID Header has insufficient data"); + String header = data.readString(8); + // Identification header RFC7845 Section 5.1. + checkArgument(header.equals("OpusHead"), "ID Header missing"); + checkArgument(data.readUnsignedByte() == 1, "version number must always be 1"); + data.setPosition(currPosition); + } + + /** Returns the correct sample time from RTP timestamp, accounting for the OPUS sampling rate. */ + private static long toSampleTimeUs( + long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) { + return startTimeOffsetUs + + Util.scaleLargeTimestamp( + rtpTimestamp - firstReceivedRtpTimestamp, + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ MEDIA_CLOCK_FREQUENCY); + } +} diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReaderTest.java new file mode 100644 index 0000000000..1b2ed3a50b --- /dev/null +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReaderTest.java @@ -0,0 +1,200 @@ +/* + * Copyright 2022 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 androidx.media3.exoplayer.rtsp.reader; + +import static androidx.media3.common.util.Util.getBytesFromHexString; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.exoplayer.rtsp.RtpPacket; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.test.utils.FakeTrackOutput; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableMap; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit test for {@link RtpOpusReader}. */ +@RunWith(AndroidJUnit4.class) +public final class RtpOpusReaderTest { + + private static final RtpPayloadFormat OPUS_FORMAT = + new RtpPayloadFormat( + new Format.Builder() + .setChannelCount(6) + .setSampleMimeType(MimeTypes.AUDIO_OPUS) + .setSampleRate(48_000) + .build(), + /* rtpPayloadType= */ 97, + /* clockRate= */ 48_000, + /* fmtpParameters= */ ImmutableMap.of()); + + private static final RtpPacket OPUS_HEADER = + createRtpPacket( + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40289, + /* payloadData= */ getBytesFromHexString("4F707573486561640102000000000000000000")); + private static final RtpPacket OPUS_TAGS = + createRtpPacket( + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40290, + /* payloadData= */ getBytesFromHexString("4F707573546167730000000000000000000000")); + private static final RtpPacket OPUS_FRAME_1 = + createRtpPacket( + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40292, + /* payloadData= */ getBytesFromHexString("010203")); + private static final RtpPacket OPUS_FRAME_2 = + createRtpPacket( + /* timestamp= */ 2599169592L, + /* sequenceNumber= */ 40293, + /* payloadData= */ getBytesFromHexString("04050607")); + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private ParsableByteArray packetData; + private RtpOpusReader opusReader; + private FakeTrackOutput trackOutput; + @Mock private ExtractorOutput extractorOutput; + + @Before + public void setUp() { + packetData = new ParsableByteArray(); + trackOutput = new FakeTrackOutput(/* deduplicateConsecutiveFormats= */ true); + when(extractorOutput.track(anyInt(), anyInt())).thenReturn(trackOutput); + opusReader = new RtpOpusReader(OPUS_FORMAT); + opusReader.createTracks(extractorOutput, /* trackId= */ 0); + } + + @Test + public void consume_validPackets() { + opusReader.onReceivingFirstPacket(OPUS_HEADER.timestamp, OPUS_HEADER.sequenceNumber); + consume(OPUS_HEADER); + consume(OPUS_TAGS); + consume(OPUS_FRAME_1); + consume(OPUS_FRAME_2); + + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)).isEqualTo(getBytesFromHexString("010203")); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(getBytesFromHexString("04050607")); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(32000); + } + + @Test + public void consume_opusHeaderWithInvalidHeader_throwsIllegalArgumentException() { + opusReader.onReceivingFirstPacket(OPUS_HEADER.timestamp, OPUS_HEADER.sequenceNumber); + assertThrows( + IllegalArgumentException.class, + () -> + consume( + createRtpPacket( + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40289, + // Modify "OpusHead" -> "OrusHead" (First 8 bytes). + /* payloadData= */ getBytesFromHexString( + "4F727573486561640102000000000000000000")))); + } + + @Test + public void consume_opusHeaderWithInvalidSampleSize_throwsIllegalArgumentException() { + opusReader.onReceivingFirstPacket(OPUS_HEADER.timestamp, OPUS_HEADER.sequenceNumber); + + assertThrows( + IllegalArgumentException.class, + () -> + consume( + createRtpPacket( + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40289, + // Truncate the opusHeader payload data. + /* payloadData= */ getBytesFromHexString("4F707573486561640102")))); + } + + @Test + public void consume_opusHeaderWithInvalidVersionNumber_throwsIllegalArgumentException() { + opusReader.onReceivingFirstPacket(OPUS_HEADER.timestamp, OPUS_HEADER.sequenceNumber); + assertThrows( + IllegalArgumentException.class, + () -> + consume( + createRtpPacket( + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40289, + // Modify version 1 -> 2 (9th byte) + /* payloadData= */ getBytesFromHexString( + "4f707573486561640202000000000000000000")))); + } + + @Test + public void consume_invalidOpusTags_throwsIllegalArgumentException() { + opusReader.onReceivingFirstPacket(OPUS_HEADER.timestamp, OPUS_HEADER.sequenceNumber); + consume(OPUS_HEADER); + assertThrows( + IllegalArgumentException.class, + () -> + consume( + createRtpPacket( + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40290, + // Modify "OpusTags" -> "OpusTggs" (First 8 bytes) + /* payloadData= */ getBytesFromHexString("4F70757354676773")))); + } + + @Test + public void consume_skipOpusTags_throwsIllegalArgumentException() { + opusReader.onReceivingFirstPacket(OPUS_HEADER.timestamp, OPUS_HEADER.sequenceNumber); + consume(OPUS_HEADER); + assertThrows(IllegalArgumentException.class, () -> consume(OPUS_FRAME_1)); + } + + @Test + public void consume_skipOpusHeader_throwsIllegalArgumentException() { + opusReader.onReceivingFirstPacket(OPUS_HEADER.timestamp, OPUS_HEADER.sequenceNumber); + assertThrows(IllegalArgumentException.class, () -> consume(OPUS_TAGS)); + } + + @Test + public void consume_skipOpusHeaderAndOpusTags_throwsIllegalArgumentException() { + opusReader.onReceivingFirstPacket(OPUS_HEADER.timestamp, OPUS_HEADER.sequenceNumber); + assertThrows(IllegalArgumentException.class, () -> consume(OPUS_FRAME_1)); + } + + private static RtpPacket createRtpPacket(long timestamp, int sequenceNumber, byte[] payloadData) { + return new RtpPacket.Builder() + .setTimestamp(timestamp) + .setSequenceNumber(sequenceNumber) + .setMarker(false) + .setPayloadData(payloadData) + .build(); + } + + private void consume(RtpPacket rtpPacket) { + packetData.reset(rtpPacket.payloadData); + opusReader.consume(packetData, rtpPacket.timestamp, rtpPacket.sequenceNumber, rtpPacket.marker); + } +} From 926327ef4f2214b37044122dc1c710bd40877264 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 8 Jun 2022 08:31:29 +0000 Subject: [PATCH 17/45] Clarify that `ShuffleOrder` must be consistent in both directions #minor-release PiperOrigin-RevId: 453622964 (cherry picked from commit 4a6f431f01bd4cdb6754d5a897cad307eebe4d34) --- .../java/androidx/media3/exoplayer/source/ShuffleOrder.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ShuffleOrder.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ShuffleOrder.java index 6d75f13b53..e6e2e1b2dc 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ShuffleOrder.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ShuffleOrder.java @@ -24,6 +24,9 @@ import java.util.Random; * Shuffled order of indices. * *

    The shuffle order must be immutable to ensure thread safety. + * + *

    The order must be consistent when traversed both {@linkplain #getNextIndex(int) forwards} and + * {@linkplain #getPreviousIndex(int) backwards}. */ @UnstableApi public interface ShuffleOrder { From f5dc99f5965c17485328d29d3a697e69d04ff5b2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 8 Jun 2022 08:49:40 +0000 Subject: [PATCH 18/45] Forward legacy controller onPlay/PrepareFromXY calls to onAddMediaItems These legacy callbacks are currently forwarded to onSetMediaUri which will be removed in the future. Also make sure to only call player.prepare/play after the items have been set. The calls to onAddQueueItem are also forwarded to onAddMediaItems to actually allow a session to resolve these items to playable media, which wasn't possible so far. PiperOrigin-RevId: 453625204 (cherry picked from commit bd126ec5c5497615572bcdf259278a899b4574f8) --- RELEASENOTES.md | 2 + .../media3/demo/session/PlaybackService.kt | 32 +-- .../androidx/media3/session/MediaSession.java | 68 ++---- .../session/MediaSessionLegacyStub.java | 210 ++++++++++-------- ...CallbackWithMediaControllerCompatTest.java | 210 ++++++++++++------ 5 files changed, 295 insertions(+), 227 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1c4dbbc843..290a9d4bca 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -143,6 +143,8 @@ * Replace `MediaSession.MediaItemFiler` with `MediaSession.Callback.onAddMediaItems` to allow asynchronous resolution of requests. + * Forward legacy `MediaController` calls to play media to + `MediaSession.Callback.onAddMediaItems` instead of `onSetMediaUri`. * Data sources: * Rename `DummyDataSource` to `PlaceholderDataSource`. * Workaround OkHttp interrupt handling. diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt index 25c012275a..cc8291c27d 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt @@ -19,7 +19,6 @@ import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.TaskStackBuilder import android.content.Intent -import android.net.Uri import android.os.Build import android.os.Bundle import androidx.media3.common.AudioAttributes @@ -182,37 +181,21 @@ class PlaybackService : MediaLibraryService() { return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) } - override fun onSetMediaUri( - session: MediaSession, - controller: ControllerInfo, - uri: Uri, - extras: Bundle - ): Int { - - if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) || - uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT) - ) { - val searchQuery = - uri.getQueryParameter("query") ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED - setMediaItemFromSearchQuery(searchQuery) - - return SessionResult.RESULT_SUCCESS - } else { - return SessionResult.RESULT_ERROR_NOT_SUPPORTED - } - } - override fun onAddMediaItems( mediaSession: MediaSession, controller: MediaSession.ControllerInfo, mediaItems: List ): ListenableFuture> { val updatedMediaItems: List = - mediaItems.map { mediaItem -> MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem } + mediaItems.map { mediaItem -> + if (mediaItem.requestMetadata.searchQuery != null) + getMediaItemFromSearchQuery(mediaItem.requestMetadata.searchQuery!!) + else MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem + } return Futures.immediateFuture(updatedMediaItems) } - private fun setMediaItemFromSearchQuery(query: String) { + private fun getMediaItemFromSearchQuery(query: String): MediaItem { // Only accept query with pattern "play [Title]" or "[Title]" // Where [Title]: must be exactly matched // If no media with exact name found, play a random media instead @@ -223,8 +206,7 @@ class PlaybackService : MediaLibraryService() { query } - val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem() - player.setMediaItem(item) + return MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem() } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 23db7affef..93816b33fd 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -973,48 +973,6 @@ public class MediaSession { *

    The implementation should create proper {@link MediaItem media item(s)} for the given * {@code uri} and call {@link Player#setMediaItems}. * - *

    When {@link MediaControllerCompat} is connected and sends commands with following methods, - * the {@code uri} will have the following patterns: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    Uri patterns corresponding to MediaControllerCompat command methods
    MethodUri pattern
    {@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}The {@code uri} passed as argument
    - * {@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId} - * {@code androidx://media3-session/prepareFromMediaId?id=[mediaId]}
    - * {@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch} - * {@code androidx://media3-session/prepareFromSearch?query=[query]}
    {@link MediaControllerCompat.TransportControls#playFromUri playFromUri}The {@code uri} passed as argument
    {@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId}{@code androidx://media3-session/playFromMediaId?id=[mediaId]}
    {@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch}{@code androidx://media3-session/playFromSearch?query=[query]}
    - * - *

    {@link Player#prepare()} or {@link Player#play()} should follow if this is called by above - * methods. - * * @param session The session for this event. * @param controller The controller information. * @param uri The uri. @@ -1057,7 +1015,8 @@ public class MediaSession { /** * Called when a controller requested to add new {@linkplain MediaItem media items} to the - * playlist. + * playlist via one of the {@code Player.addMediaItem(s)} or {@code Player.setMediaItem(s)} + * methods. * *

    Note that the requested {@linkplain MediaItem media items} don't have a {@link * MediaItem.LocalConfiguration} (for example, a URI) and need to be updated to make them @@ -1066,7 +1025,28 @@ public class MediaSession { * MediaItem#requestMetadata}. * *

    Return a {@link ListenableFuture} with the resolved {@link MediaItem media items}. You can - * also return the items directly by using Guava's {@link Futures#immediateFuture(Object)}. + * also return the items directly by using Guava's {@link Futures#immediateFuture(Object)}. Once + * the {@link MediaItem media items} have been resolved, the session will call {@link + * Player#setMediaItems} or {@link Player#addMediaItems} as requested. + * + *

    Interoperability: This method will be called in response to the following {@link + * MediaControllerCompat} methods: + * + *

      + *
    • {@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri} + *
    • {@link MediaControllerCompat.TransportControls#playFromUri playFromUri} + *
    • {@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId} + *
    • {@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId} + *
    • {@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch} + *
    • {@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch} + *
    • {@link MediaControllerCompat.TransportControls#addQueueItem addQueueItem} + *
    + * + * The values of {@link MediaItem#mediaId}, {@link MediaItem.RequestMetadata#mediaUri}, {@link + * MediaItem.RequestMetadata#searchQuery} and {@link MediaItem.RequestMetadata#extras} will be + * set to match the legacy method call. The session will call {@link Player#setMediaItems} or + * {@link Player#addMediaItems}, followed by {@link Player#prepare()} and {@link Player#play()} + * as appropriate once the {@link MediaItem} has been resolved. * * @param mediaSession The session for this event. * @param controller The controller information. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 088aed877b..9dcd3bcfa9 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -82,6 +82,9 @@ import androidx.media3.common.util.Util; import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.SessionCommand.CommandCode; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.util.List; @@ -270,39 +273,25 @@ import org.checkerframework.checker.initialization.qual.Initialized; @Override public void onPrepareFromMediaId(String mediaId, @Nullable Bundle extras) { - Uri mediaUri = - new Uri.Builder() - .scheme(MediaConstants.MEDIA_URI_SCHEME) - .authority(MediaConstants.MEDIA_URI_AUTHORITY) - .path(MediaConstants.MEDIA_URI_PATH_PREPARE_FROM_MEDIA_ID) - .appendQueryParameter(MediaConstants.MEDIA_URI_QUERY_ID, mediaId) - .build(); - onPrepareFromUri(mediaUri, extras); + handleMediaRequest( + createMediaItemForMediaRequest( + mediaId, /* mediaUri= */ null, /* searchQuery= */ null, extras), + /* play= */ false); } @Override public void onPrepareFromSearch(String query, @Nullable Bundle extras) { - Uri mediaUri = - new Uri.Builder() - .scheme(MediaConstants.MEDIA_URI_SCHEME) - .authority(MediaConstants.MEDIA_URI_AUTHORITY) - .path(MediaConstants.MEDIA_URI_PATH_PREPARE_FROM_SEARCH) - .appendQueryParameter(MediaConstants.MEDIA_URI_QUERY_QUERY, query) - .build(); - onPrepareFromUri(mediaUri, extras); + handleMediaRequest( + createMediaItemForMediaRequest(/* mediaId= */ null, /* mediaUri= */ null, query, extras), + /* play= */ false); } @Override public void onPrepareFromUri(Uri mediaUri, @Nullable Bundle extras) { - dispatchSessionTaskWithSessionCommand( - SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI, - controller -> { - if (sessionImpl.onSetMediaUriOnHandler( - controller, mediaUri, extras == null ? Bundle.EMPTY : extras) - == RESULT_SUCCESS) { - sessionImpl.getPlayerWrapper().prepare(); - } - }); + handleMediaRequest( + createMediaItemForMediaRequest( + /* mediaId= */ null, mediaUri, /* searchQuery= */ null, extras), + /* play= */ false); } @Override @@ -325,47 +314,25 @@ import org.checkerframework.checker.initialization.qual.Initialized; @Override public void onPlayFromMediaId(String mediaId, @Nullable Bundle extras) { - Uri mediaUri = - new Uri.Builder() - .scheme(MediaConstants.MEDIA_URI_SCHEME) - .authority(MediaConstants.MEDIA_URI_AUTHORITY) - .path(MediaConstants.MEDIA_URI_PATH_PLAY_FROM_MEDIA_ID) - .appendQueryParameter(MediaConstants.MEDIA_URI_QUERY_ID, mediaId) - .build(); - onPlayFromUri(mediaUri, extras); + handleMediaRequest( + createMediaItemForMediaRequest( + mediaId, /* mediaUri= */ null, /* searchQuery= */ null, extras), + /* play= */ true); } @Override public void onPlayFromSearch(String query, @Nullable Bundle extras) { - Uri mediaUri = - new Uri.Builder() - .scheme(MediaConstants.MEDIA_URI_SCHEME) - .authority(MediaConstants.MEDIA_URI_AUTHORITY) - .path(MediaConstants.MEDIA_URI_PATH_PLAY_FROM_SEARCH) - .appendQueryParameter(MediaConstants.MEDIA_URI_QUERY_QUERY, query) - .build(); - onPlayFromUri(mediaUri, extras); + handleMediaRequest( + createMediaItemForMediaRequest(/* mediaId= */ null, /* mediaUri= */ null, query, extras), + /* play= */ true); } @Override public void onPlayFromUri(Uri mediaUri, @Nullable Bundle extras) { - dispatchSessionTaskWithSessionCommand( - SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI, - controller -> { - if (sessionImpl.onSetMediaUriOnHandler( - controller, mediaUri, extras == null ? Bundle.EMPTY : extras) - == RESULT_SUCCESS) { - PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); - @Player.State int playbackState = playerWrapper.getPlaybackState(); - if (playbackState == Player.STATE_IDLE) { - playerWrapper.prepare(); - } else if (playbackState == STATE_ENDED) { - playerWrapper.seekTo( - playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET); - } - playerWrapper.play(); - } - }); + handleMediaRequest( + createMediaItemForMediaRequest( + /* mediaId= */ null, mediaUri, /* searchQuery= */ null, extras), + /* play= */ true); } @Override @@ -498,40 +465,12 @@ import org.checkerframework.checker.initialization.qual.Initialized; @Override public void onAddQueueItem(@Nullable MediaDescriptionCompat description) { - if (description == null) { - return; - } - dispatchSessionTaskWithPlayerCommand( - COMMAND_CHANGE_MEDIA_ITEMS, - controller -> { - @Nullable String mediaId = description.getMediaId(); - if (TextUtils.isEmpty(mediaId)) { - Log.w(TAG, "onAddQueueItem(): Media ID shouldn't be empty"); - return; - } - MediaItem mediaItem = MediaUtils.convertToMediaItem(description); - sessionImpl.getPlayerWrapper().addMediaItem(mediaItem); - }, - sessionCompat.getCurrentControllerInfo()); + handleOnAddQueueItem(description, /* index= */ C.INDEX_UNSET); } @Override public void onAddQueueItem(@Nullable MediaDescriptionCompat description, int index) { - if (description == null) { - return; - } - dispatchSessionTaskWithPlayerCommand( - COMMAND_CHANGE_MEDIA_ITEMS, - controller -> { - @Nullable String mediaId = description.getMediaId(); - if (TextUtils.isEmpty(mediaId)) { - Log.w(TAG, "onAddQueueItem(): Media ID shouldn't be empty"); - return; - } - MediaItem mediaItem = MediaUtils.convertToMediaItem(description); - sessionImpl.getPlayerWrapper().addMediaItem(index, mediaItem); - }, - sessionCompat.getCurrentControllerInfo()); + handleOnAddQueueItem(description, index); } @Override @@ -738,6 +677,85 @@ import org.checkerframework.checker.initialization.qual.Initialized; connectionTimeoutMs = timeoutMs; } + private void handleMediaRequest(MediaItem mediaItem, boolean play) { + dispatchSessionTaskWithPlayerCommand( + COMMAND_CHANGE_MEDIA_ITEMS, + controller -> { + ListenableFuture> mediaItemsFuture = + sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)); + Futures.addCallback( + mediaItemsFuture, + new FutureCallback>() { + @Override + public void onSuccess(List mediaItems) { + postOrRun( + sessionImpl.getApplicationHandler(), + () -> { + Player player = sessionImpl.getPlayerWrapper(); + player.setMediaItems(mediaItems); + @Player.State int playbackState = player.getPlaybackState(); + if (playbackState == Player.STATE_IDLE) { + player.prepare(); + } else if (playbackState == Player.STATE_ENDED) { + player.seekTo(/* positionMs= */ C.TIME_UNSET); + } + if (play) { + player.play(); + } + }); + } + + @Override + public void onFailure(Throwable t) { + // Do nothing, the session is free to ignore these requests. + } + }, + MoreExecutors.directExecutor()); + }, + sessionCompat.getCurrentControllerInfo()); + } + + private void handleOnAddQueueItem(@Nullable MediaDescriptionCompat description, int index) { + if (description == null) { + return; + } + dispatchSessionTaskWithPlayerCommand( + COMMAND_CHANGE_MEDIA_ITEMS, + controller -> { + @Nullable String mediaId = description.getMediaId(); + if (TextUtils.isEmpty(mediaId)) { + Log.w(TAG, "onAddQueueItem(): Media ID shouldn't be empty"); + return; + } + MediaItem mediaItem = MediaUtils.convertToMediaItem(description); + ListenableFuture> mediaItemsFuture = + sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)); + Futures.addCallback( + mediaItemsFuture, + new FutureCallback>() { + @Override + public void onSuccess(List mediaItems) { + postOrRun( + sessionImpl.getApplicationHandler(), + () -> { + if (index == C.INDEX_UNSET) { + sessionImpl.getPlayerWrapper().addMediaItems(mediaItems); + } else { + sessionImpl.getPlayerWrapper().addMediaItems(index, mediaItems); + } + }); + } + + @Override + public void onFailure(Throwable t) { + // Do nothing, the session is free to ignore these requests. + } + }, + MoreExecutors.directExecutor()); + }, + sessionCompat.getCurrentControllerInfo()); + } + private static void sendCustomCommandResultWhenReady( ResultReceiver receiver, ListenableFuture future) { future.addListener( @@ -776,6 +794,22 @@ import org.checkerframework.checker.initialization.qual.Initialized; sessionCompat.setQueueTitle(title); } + private static MediaItem createMediaItemForMediaRequest( + @Nullable String mediaId, + @Nullable Uri mediaUri, + @Nullable String searchQuery, + @Nullable Bundle extras) { + return new MediaItem.Builder() + .setMediaId(mediaId == null ? MediaItem.DEFAULT_MEDIA_ID : mediaId) + .setRequestMetadata( + new MediaItem.RequestMetadata.Builder() + .setMediaUri(mediaUri) + .setSearchQuery(searchQuery) + .setExtras(extras) + .build()) + .build(); + } + /* @FunctionalInterface */ private interface SessionTask { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java index fd7bd48720..6fd12bc37e 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java @@ -56,11 +56,16 @@ import androidx.media3.test.session.common.TestUtils; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; @@ -75,6 +80,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { private static final String TAG = "MSCallbackWithMCCTest"; + private static final String TEST_URI = "http://test.test"; private static final String EXPECTED_CONTROLLER_PACKAGE_NAME = (Util.SDK_INT < 21 || Util.SDK_INT >= 24) ? SUPPORT_APP_PACKAGE_NAME : LEGACY_CONTROLLER; @@ -88,6 +94,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { private RemoteMediaControllerCompat controller; private MockPlayer player; private AudioManager audioManager; + private ListeningExecutorService executorService; @Before public void setUp() { @@ -95,6 +102,9 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { handler = threadTestRule.getHandler(); player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + // Intentionally use an Executor with another thread to test asynchronous workflows involving + // background tasks. + executorService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); } @After @@ -107,6 +117,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.cleanUp(); controller = null; } + executorService.shutdownNow(); } @Test @@ -294,10 +305,22 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { @Test public void addQueueItem() throws Exception { + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); + } + }; session = new MediaSession.Builder(context, player) .setId("addQueueItem") - .setCallback(new TestSessionCallback()) + .setCallback(callback) .build(); controller = new RemoteMediaControllerCompat( @@ -305,49 +328,73 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { handler.postAndSync( () -> { - player.timeline = MediaTestUtils.createTimeline(/* windowCount= */ 10); + List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 10); + player.setMediaItems(mediaItems); + player.timeline = MediaTestUtils.createTimeline(mediaItems); player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); }); // Prepare an item to add. String mediaId = "newMediaItemId"; - MediaDescriptionCompat desc = new MediaDescriptionCompat.Builder().setMediaId(mediaId).build(); + Uri mediaUri = Uri.parse("https://test.test"); + MediaDescriptionCompat desc = + new MediaDescriptionCompat.Builder().setMediaId(mediaId).setMediaUri(mediaUri).build(); controller.addQueueItem(desc); - player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM, TIMEOUT_MS); - assertThat(player.mediaItems).hasSize(1); - assertThat(player.mediaItems.get(0).mediaId).isEqualTo(mediaId); + player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).mediaId).isEqualTo(mediaId); + assertThat(requestedMediaItems.get().get(0).requestMetadata.mediaUri).isEqualTo(mediaUri); + assertThat(player.mediaItems).hasSize(11); + assertThat(player.mediaItems.get(10)).isEqualTo(resolvedMediaItem); } @Test public void addQueueItemWithIndex() throws Exception { + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); + } + }; session = new MediaSession.Builder(context, player) .setId("addQueueItemWithIndex") - .setCallback(new TestSessionCallback()) + .setCallback(callback) .build(); controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); - List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 10); handler.postAndSync( () -> { + List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 10); player.setMediaItems(mediaItems); - player.timeline = new PlaylistTimeline(mediaItems); + player.timeline = MediaTestUtils.createTimeline(mediaItems); player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); }); // Prepare an item to add. int testIndex = 1; String mediaId = "media_id"; - MediaDescriptionCompat desc = new MediaDescriptionCompat.Builder().setMediaId(mediaId).build(); + Uri mediaUri = Uri.parse("https://test.test"); + MediaDescriptionCompat desc = + new MediaDescriptionCompat.Builder().setMediaId(mediaId).setMediaUri(mediaUri).build(); controller.addQueueItem(desc, testIndex); - player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM_WITH_INDEX, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).mediaId).isEqualTo(mediaId); + assertThat(requestedMediaItems.get().get(0).requestMetadata.mediaUri).isEqualTo(mediaUri); assertThat(player.index).isEqualTo(testIndex); assertThat(player.mediaItems).hasSize(11); - assertThat(player.mediaItems.get(1).mediaId).isEqualTo(mediaId); + assertThat(player.mediaItems.get(1)).isEqualTo(resolvedMediaItem); } @Test @@ -788,16 +835,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { Uri mediaUri = Uri.parse("foo://bar"); Bundle bundle = new Bundle(); bundle.putString("key", "value"); - CountDownLatch latch = new CountDownLatch(1); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); MediaSession.Callback callback = - new TestSessionCallback() { + new MediaSession.Callback() { @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(uri).isEqualTo(mediaUri); - assertThat(TestUtils.equals(bundle, extras)).isTrue(); - latch.countDown(); - return RESULT_SUCCESS; + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); } }; session = @@ -811,8 +858,12 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().prepareFromUri(mediaUri, bundle); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).requestMetadata.mediaUri).isEqualTo(mediaUri); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @Test @@ -820,16 +871,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { Uri request = Uri.parse("foo://bar"); Bundle bundle = new Bundle(); bundle.putString("key", "value"); - CountDownLatch latch = new CountDownLatch(1); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); MediaSession.Callback callback = - new TestSessionCallback() { + new MediaSession.Callback() { @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(uri).isEqualTo(request); - assertThat(TestUtils.equals(bundle, extras)).isTrue(); - latch.countDown(); - return RESULT_SUCCESS; + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); } }; session = @@ -843,8 +894,13 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().playFromUri(request, bundle); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).requestMetadata.mediaUri).isEqualTo(request); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @Test @@ -852,17 +908,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { String request = "media_id"; Bundle bundle = new Bundle(); bundle.putString("key", "value"); - CountDownLatch latch = new CountDownLatch(1); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); MediaSession.Callback callback = - new TestSessionCallback() { + new MediaSession.Callback() { @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(uri.toString()) - .isEqualTo("androidx://media3-session/prepareFromMediaId?id=" + request); - assertThat(TestUtils.equals(bundle, extras)).isTrue(); - latch.countDown(); - return RESULT_SUCCESS; + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); } }; session = @@ -876,8 +931,12 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().prepareFromMediaId(request, bundle); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).mediaId).isEqualTo(request); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @Test @@ -885,17 +944,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { String mediaId = "media_id"; Bundle bundle = new Bundle(); bundle.putString("key", "value"); - CountDownLatch latch = new CountDownLatch(1); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); MediaSession.Callback callback = - new TestSessionCallback() { + new MediaSession.Callback() { @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(uri.toString()) - .isEqualTo("androidx://media3-session/playFromMediaId?id=" + mediaId); - assertThat(TestUtils.equals(bundle, extras)).isTrue(); - latch.countDown(); - return RESULT_SUCCESS; + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); } }; session = @@ -909,8 +967,13 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().playFromMediaId(mediaId, bundle); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).mediaId).isEqualTo(mediaId); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @Test @@ -918,17 +981,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { String query = "test_query"; Bundle bundle = new Bundle(); bundle.putString("key", "value"); - CountDownLatch latch = new CountDownLatch(1); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); MediaSession.Callback callback = - new TestSessionCallback() { + new MediaSession.Callback() { @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(uri.toString()) - .isEqualTo("androidx://media3-session/prepareFromSearch?query=" + query); - assertThat(TestUtils.equals(bundle, extras)).isTrue(); - latch.countDown(); - return RESULT_SUCCESS; + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); } }; session = @@ -942,8 +1004,12 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().prepareFromSearch(query, bundle); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).requestMetadata.searchQuery).isEqualTo(query); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @Test @@ -951,17 +1017,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { String query = "test_query"; Bundle bundle = new Bundle(); bundle.putString("key", "value"); - CountDownLatch latch = new CountDownLatch(1); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); MediaSession.Callback callback = - new TestSessionCallback() { + new MediaSession.Callback() { @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(uri.toString()) - .isEqualTo("androidx://media3-session/playFromSearch?query=" + query); - assertThat(TestUtils.equals(bundle, extras)).isTrue(); - latch.countDown(); - return RESULT_SUCCESS; + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); } }; session = @@ -975,8 +1040,13 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().playFromSearch(query, bundle); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).requestMetadata.searchQuery).isEqualTo(query); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @Test From 31c7ccbc49634b10fce64e3bb837f06795467d60 Mon Sep 17 00:00:00 2001 From: christosts Date: Wed, 8 Jun 2022 10:44:24 +0000 Subject: [PATCH 19/45] Audio passthrough: handle unset audio format channel count With HLS chunkless preparation, audio formats may have no value for channel count. In this case, the DefaultAudioSink will either query the platform for a supported channel count (API 29+) or assume a max channel count based on the encoding spec in order to decide whether the audio format can be played with audio passthrough. Issue: google/ExoPlayer#10204 #minor-release PiperOrigin-RevId: 453644548 (cherry picked from commit 86973382335156abaa76770c6897d28460fdde36) --- RELEASENOTES.md | 4 + .../exoplayer/audio/AudioCapabilities.java | 166 ++++++++++++++++-- .../exoplayer/audio/DefaultAudioSink.java | 132 +------------- 3 files changed, 154 insertions(+), 148 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 290a9d4bca..933615fcf0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -45,6 +45,10 @@ * Change the return type of `AudioAttributes.getAudioAttributesV21()` from `android.media.AudioAttributes` to a new `AudioAttributesV21` wrapper class, to prevent slow ART verification on API < 21. + * Query the platform (API 29+) or assume the audio encoding channel count + for audio passthrough when the format audio channel count is unset, + which occurs with HLS chunkless preparation + ([10204](https://github.com/google/ExoPlayer/issues/10204)). * Ad playback / IMA: * Decrease ad polling rate from every 100ms to every 200ms, to line up with Media Rating Council (MRC) recommendations. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java index b6d4f02aa0..9888db45a6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java @@ -15,22 +15,29 @@ */ package androidx.media3.exoplayer.audio; +import static androidx.media3.common.util.Assertions.checkNotNull; + import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.net.Uri; import android.provider.Settings.Global; +import android.util.Pair; import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.primitives.Ints; import java.util.Arrays; @@ -54,18 +61,20 @@ public final class AudioCapabilities { }, DEFAULT_MAX_CHANNEL_COUNT); - /** Array of all surround sound encodings that a device may be capable of playing. */ - @SuppressWarnings("InlinedApi") - private static final int[] ALL_SURROUND_ENCODINGS = - new int[] { - AudioFormat.ENCODING_AC3, - AudioFormat.ENCODING_E_AC3, - AudioFormat.ENCODING_E_AC3_JOC, - AudioFormat.ENCODING_AC4, - AudioFormat.ENCODING_DOLBY_TRUEHD, - AudioFormat.ENCODING_DTS, - AudioFormat.ENCODING_DTS_HD, - }; + /** + * All surround sound encodings that a device may be capable of playing mapped to a maximum + * channel count. + */ + private static final ImmutableMap ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS = + new ImmutableMap.Builder() + .put(C.ENCODING_AC3, 6) + .put(C.ENCODING_AC4, 6) + .put(C.ENCODING_DTS, 6) + .put(C.ENCODING_E_AC3_JOC, 6) + .put(C.ENCODING_E_AC3, 8) + .put(C.ENCODING_DTS_HD, 8) + .put(C.ENCODING_DOLBY_TRUEHD, 8) + .buildOrThrow(); /** Global settings key for devices that can specify external surround sound. */ private static final String EXTERNAL_SURROUND_SOUND_KEY = "external_surround_sound_enabled"; @@ -158,6 +167,62 @@ public final class AudioCapabilities { return maxChannelCount; } + /** Returns whether the device can do passthrough playback for {@code format}. */ + public boolean isPassthroughPlaybackSupported(Format format) { + return getEncodingAndChannelConfigForPassthrough(format) != null; + } + + /** + * Returns the encoding and channel config to use when configuring an {@link AudioTrack} in + * passthrough mode for the specified {@link Format}. Returns {@code null} if passthrough of the + * format is unsupported. + * + * @param format The {@link Format}. + * @return The encoding and channel config to use, or {@code null} if passthrough of the format is + * unsupported. + */ + @Nullable + public Pair getEncodingAndChannelConfigForPassthrough(Format format) { + @C.Encoding + int encoding = MimeTypes.getEncoding(checkNotNull(format.sampleMimeType), format.codecs); + // Check that this is an encoding known to work for passthrough. This avoids trying to use + // passthrough with an encoding where the device/app reports it's capable but it is untested or + // known to be broken (for example AAC-LC). + if (!ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.containsKey(encoding)) { + return null; + } + + if (encoding == C.ENCODING_E_AC3_JOC && !supportsEncoding(C.ENCODING_E_AC3_JOC)) { + // E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer). + encoding = C.ENCODING_E_AC3; + } else if (encoding == C.ENCODING_DTS_HD && !supportsEncoding(C.ENCODING_DTS_HD)) { + // DTS receivers support DTS-HD streams (but decode only the core layer). + encoding = C.ENCODING_DTS; + } + if (!supportsEncoding(encoding)) { + return null; + } + int channelCount; + if (format.channelCount == Format.NO_VALUE || encoding == C.ENCODING_E_AC3_JOC) { + // In HLS chunkless preparation, the format channel count and sample rate may be unset. See + // https://github.com/google/ExoPlayer/issues/10204 and b/222127949 for more details. + // For E-AC3 JOC, the format is object based so the format channel count is arbitrary. + int sampleRate = + format.sampleRate != Format.NO_VALUE ? format.sampleRate : DEFAULT_SAMPLE_RATE_HZ; + channelCount = getMaxSupportedChannelCountForPassthrough(encoding, sampleRate); + } else { + channelCount = format.channelCount; + if (channelCount > maxChannelCount) { + return null; + } + } + int channelConfig = getChannelConfigForPassthrough(channelCount); + if (channelConfig == AudioFormat.CHANNEL_INVALID) { + return null; + } + return Pair.create(encoding, channelConfig); + } + @Override public boolean equals(@Nullable Object other) { if (this == other) { @@ -190,28 +255,93 @@ public final class AudioCapabilities { && ("Amazon".equals(Util.MANUFACTURER) || "Xiaomi".equals(Util.MANUFACTURER)); } + /** + * Returns the maximum number of channels supported for passthrough playback of audio in the given + * encoding, or {@code 0} if the format is unsupported. + */ + private static int getMaxSupportedChannelCountForPassthrough( + @C.Encoding int encoding, int sampleRate) { + // From API 29 we can get the channel count from the platform, but before then there is no way + // to query the platform so we assume the channel count matches the maximum channel count per + // audio encoding spec. + if (Util.SDK_INT >= 29) { + return Api29.getMaxSupportedChannelCountForPassthrough(encoding, sampleRate); + } + return checkNotNull(ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.getOrDefault(encoding, 0)); + } + + private static int getChannelConfigForPassthrough(int channelCount) { + if (Util.SDK_INT <= 28) { + // In passthrough mode the channel count used to configure the audio track doesn't affect how + // the stream is handled, except that some devices do overly-strict channel configuration + // checks. Therefore we override the channel count so that a known-working channel + // configuration is chosen in all cases. See [Internal: b/29116190]. + if (channelCount == 7) { + channelCount = 8; + } else if (channelCount == 3 || channelCount == 4 || channelCount == 5) { + channelCount = 6; + } + } + + // Workaround for Nexus Player not reporting support for mono passthrough. See + // [Internal: b/34268671]. + if (Util.SDK_INT <= 26 && "fugu".equals(Util.DEVICE) && channelCount == 1) { + channelCount = 2; + } + + return Util.getAudioTrackChannelConfig(channelCount); + } + @RequiresApi(29) private static final class Api29 { + private static final AudioAttributes DEFAULT_AUDIO_ATTRIBUTES = + new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) + .setFlags(0) + .build(); + + private Api29() {} + @DoNotInline public static int[] getDirectPlaybackSupportedEncodings() { ImmutableList.Builder supportedEncodingsListBuilder = ImmutableList.builder(); - for (int encoding : ALL_SURROUND_ENCODINGS) { + for (int encoding : ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.keySet()) { if (AudioTrack.isDirectPlaybackSupported( new AudioFormat.Builder() .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) .setEncoding(encoding) .setSampleRate(DEFAULT_SAMPLE_RATE_HZ) .build(), - new android.media.AudioAttributes.Builder() - .setUsage(android.media.AudioAttributes.USAGE_MEDIA) - .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE) - .setFlags(0) - .build())) { + DEFAULT_AUDIO_ATTRIBUTES)) { supportedEncodingsListBuilder.add(encoding); } } supportedEncodingsListBuilder.add(AudioFormat.ENCODING_PCM_16BIT); return Ints.toArray(supportedEncodingsListBuilder.build()); } + + /** + * Returns the maximum number of channels supported for passthrough playback of audio in the + * given format, or {@code 0} if the format is unsupported. + */ + @DoNotInline + public static int getMaxSupportedChannelCountForPassthrough( + @C.Encoding int encoding, int sampleRate) { + // TODO(internal b/234351617): Query supported channel masks directly once it's supported, + // see also b/25994457. + for (int channelCount = DEFAULT_MAX_CHANNEL_COUNT; channelCount > 0; channelCount--) { + AudioFormat audioFormat = + new AudioFormat.Builder() + .setEncoding(encoding) + .setSampleRate(sampleRate) + .setChannelMask(Util.getAudioTrackChannelConfig(channelCount)) + .build(); + if (AudioTrack.isDirectPlaybackSupported(audioFormat, DEFAULT_AUDIO_ATTRIBUTES)) { + return channelCount; + } + } + return 0; + } } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index 13c80a1228..c1a34adb68 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -684,7 +684,7 @@ public final class DefaultAudioSink implements AudioSink { if (!offloadDisabledUntilNextConfiguration && useOffloadedPlayback(format, audioAttributes)) { return SINK_FORMAT_SUPPORTED_DIRECTLY; } - if (isPassthroughPlaybackSupported(format, audioCapabilities)) { + if (audioCapabilities.isPassthroughPlaybackSupported(format)) { return SINK_FORMAT_SUPPORTED_DIRECTLY; } return SINK_FORMAT_UNSUPPORTED; @@ -767,7 +767,7 @@ public final class DefaultAudioSink implements AudioSink { outputMode = OUTPUT_MODE_PASSTHROUGH; @Nullable Pair encodingAndChannelConfig = - getEncodingAndChannelConfigForPassthrough(inputFormat, audioCapabilities); + audioCapabilities.getEncodingAndChannelConfigForPassthrough(inputFormat); if (encodingAndChannelConfig == null) { throw new ConfigurationException( "Unable to configure passthrough for: " + inputFormat, inputFormat); @@ -1693,134 +1693,6 @@ public final class DefaultAudioSink implements AudioSink { : writtenEncodedFrames; } - private static boolean isPassthroughPlaybackSupported( - Format format, AudioCapabilities audioCapabilities) { - return getEncodingAndChannelConfigForPassthrough(format, audioCapabilities) != null; - } - - /** - * Returns the encoding and channel config to use when configuring an {@link AudioTrack} in - * passthrough mode for the specified {@link Format}. Returns {@code null} if passthrough of the - * format is unsupported. - * - * @param format The {@link Format}. - * @param audioCapabilities The device audio capabilities. - * @return The encoding and channel config to use, or {@code null} if passthrough of the format is - * unsupported. - */ - @Nullable - private static Pair getEncodingAndChannelConfigForPassthrough( - Format format, AudioCapabilities audioCapabilities) { - @C.Encoding - int encoding = MimeTypes.getEncoding(checkNotNull(format.sampleMimeType), format.codecs); - // Check for encodings that are known to work for passthrough with the implementation in this - // class. This avoids trying to use passthrough with an encoding where the device/app reports - // it's capable but it is untested or known to be broken (for example AAC-LC). - boolean supportedEncoding = - encoding == C.ENCODING_AC3 - || encoding == C.ENCODING_E_AC3 - || encoding == C.ENCODING_E_AC3_JOC - || encoding == C.ENCODING_AC4 - || encoding == C.ENCODING_DTS - || encoding == C.ENCODING_DTS_HD - || encoding == C.ENCODING_DOLBY_TRUEHD; - if (!supportedEncoding) { - return null; - } - if (encoding == C.ENCODING_E_AC3_JOC - && !audioCapabilities.supportsEncoding(C.ENCODING_E_AC3_JOC)) { - // E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer). - encoding = C.ENCODING_E_AC3; - } else if (encoding == C.ENCODING_DTS_HD - && !audioCapabilities.supportsEncoding(C.ENCODING_DTS_HD)) { - // DTS receivers support DTS-HD streams (but decode only the core layer). - encoding = C.ENCODING_DTS; - } - if (!audioCapabilities.supportsEncoding(encoding)) { - return null; - } - - int channelCount; - if (encoding == C.ENCODING_E_AC3_JOC) { - // E-AC3 JOC is object based so the format channel count is arbitrary. From API 29 we can get - // the channel count for this encoding, but before then there is no way to query it so we - // assume 6 channel audio is supported. - if (Util.SDK_INT >= 29) { - // Default to 48 kHz if the format doesn't have a sample rate (for example, for chunkless - // HLS preparation). See [Internal: b/222127949]. - int sampleRate = format.sampleRate != Format.NO_VALUE ? format.sampleRate : 48000; - channelCount = - getMaxSupportedChannelCountForPassthroughV29(C.ENCODING_E_AC3_JOC, sampleRate); - if (channelCount == 0) { - Log.w(TAG, "E-AC3 JOC encoding supported but no channel count supported"); - return null; - } - } else { - channelCount = 6; - } - } else { - channelCount = format.channelCount; - if (channelCount > audioCapabilities.getMaxChannelCount()) { - return null; - } - } - int channelConfig = getChannelConfigForPassthrough(channelCount); - if (channelConfig == AudioFormat.CHANNEL_INVALID) { - return null; - } - - return Pair.create(encoding, channelConfig); - } - - /** - * Returns the maximum number of channels supported for passthrough playback of audio in the given - * format, or 0 if the format is unsupported. - */ - @RequiresApi(29) - private static int getMaxSupportedChannelCountForPassthroughV29( - @C.Encoding int encoding, int sampleRate) { - android.media.AudioAttributes audioAttributes = - new android.media.AudioAttributes.Builder() - .setUsage(android.media.AudioAttributes.USAGE_MEDIA) - .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE) - .build(); - // TODO(internal b/25994457): Query supported channel masks directly once it's supported. - for (int channelCount = 8; channelCount > 0; channelCount--) { - AudioFormat audioFormat = - new AudioFormat.Builder() - .setEncoding(encoding) - .setSampleRate(sampleRate) - .setChannelMask(Util.getAudioTrackChannelConfig(channelCount)) - .build(); - if (AudioTrack.isDirectPlaybackSupported(audioFormat, audioAttributes)) { - return channelCount; - } - } - return 0; - } - - private static int getChannelConfigForPassthrough(int channelCount) { - if (Util.SDK_INT <= 28) { - // In passthrough mode the channel count used to configure the audio track doesn't affect how - // the stream is handled, except that some devices do overly-strict channel configuration - // checks. Therefore we override the channel count so that a known-working channel - // configuration is chosen in all cases. See [Internal: b/29116190]. - if (channelCount == 7) { - channelCount = 8; - } else if (channelCount == 3 || channelCount == 4 || channelCount == 5) { - channelCount = 6; - } - } - - // Workaround for Nexus Player not reporting support for mono passthrough. See - // [Internal: b/34268671]. - if (Util.SDK_INT <= 26 && "fugu".equals(Util.DEVICE) && channelCount == 1) { - channelCount = 2; - } - - return Util.getAudioTrackChannelConfig(channelCount); - } - private boolean useOffloadedPlayback(Format format, AudioAttributes audioAttributes) { if (Util.SDK_INT < 29 || offloadMode == OFFLOAD_MODE_DISABLED) { return false; From 35c9585f5fa30ab18da8b55492e1f97cc44c4d44 Mon Sep 17 00:00:00 2001 From: christosts Date: Wed, 8 Jun 2022 16:23:18 +0000 Subject: [PATCH 20/45] Avoid using ConcurrentHashMap Use Collections.synchronizedSet() instead of creating a set from a ConcurrentHashMap because ConcurrentHashMap has a bug in APIs 21/22 that can result in lost updates. PiperOrigin-RevId: 453696565 (cherry picked from commit d506c709c9a793d751908aac879f2f85b74f7b58) --- .../java/androidx/media3/session/MediaSessionService.java | 5 +++-- .../main/java/androidx/media3/session/MediaSessionStub.java | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java index 0cb00a915f..d44c3d0da1 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -43,10 +43,10 @@ import androidx.media3.session.MediaSession.ControllerInfo; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -472,7 +472,8 @@ public abstract class MediaSessionService extends Service { Context context = serviceReference.getApplicationContext(); handler = new Handler(context.getMainLooper()); mediaSessionManager = MediaSessionManager.getSessionManager(context); - pendingControllers = Collections.newSetFromMap(new ConcurrentHashMap<>()); + // ConcurrentHashMap has a bug in APIs 21-22 that can result in lost updates. + pendingControllers = Collections.synchronizedSet(new HashSet<>()); } @Override diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 672edf3a30..2a8bbb8b59 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -83,10 +83,10 @@ import com.google.common.util.concurrent.MoreExecutors; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.Deque; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CancellationException; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; /** @@ -109,7 +109,8 @@ import java.util.concurrent.ExecutionException; this.sessionImpl = new WeakReference<>(sessionImpl); sessionManager = MediaSessionManager.getSessionManager(sessionImpl.getContext()); connectedControllersManager = new ConnectedControllersManager<>(sessionImpl); - pendingControllers = Collections.newSetFromMap(new ConcurrentHashMap<>()); + // ConcurrentHashMap has a bug in APIs 21-22 that can result in lost updates. + pendingControllers = Collections.synchronizedSet(new HashSet<>()); } public ConnectedControllersManager getConnectedControllersManager() { From 35691bce9837a3c0dbfabb8877fbbd2d94f802ac Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 9 Jun 2022 10:16:29 +0000 Subject: [PATCH 21/45] Add COMMAND_SET_MEDIA_ITEM to Player.Commands Some Player implementations have no playlist capability but can still set a MediaItem for playback. Examples are a MediaController connected to a legacy MediaSession, ExoPlayer up to 2.12 or MediaPlayer. To indicate this capability, we need an allowed command in addition to COMMAND_CHANGE_MEDIA_ITEMS that just allows to set a single item that replaces everything that is currently played. #minor-release PiperOrigin-RevId: 453879626 (cherry picked from commit 5333c67d08ec22a1b5153af152b456d36099f401) --- RELEASENOTES.md | 2 ++ .../java/androidx/media3/cast/CastPlayer.java | 3 ++- .../androidx/media3/cast/CastPlayerTest.java | 2 ++ .../java/androidx/media3/common/Player.java | 7 ++++- .../media3/exoplayer/ExoPlayerImpl.java | 3 ++- .../media3/exoplayer/ExoPlayerTest.java | 3 +++ .../session/MediaControllerImplBase.java | 13 +++++----- .../androidx/media3/session/MediaSession.java | 26 ------------------- .../session/MediaSessionLegacyStub.java | 3 ++- .../media3/session/MediaSessionStub.java | 7 ++--- .../session/MediaSessionPermissionTest.java | 7 +++++ 11 files changed, 37 insertions(+), 39 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 933615fcf0..52d85073d3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,6 +26,8 @@ * Add `MediaItem.RequestMetadata` to represent metadata needed to play media when the exact `LocalConfiguration` is not known. Also remove `MediaMetadata.mediaUrl` as this is now included in `RequestMetadata`. + * Add `Player.Command.COMMAND_SET_MEDIA_ITEM` to enable players to allow + setting a single item. * Track selection: * Flatten `TrackSelectionOverrides` class into `TrackSelectionParameters`, and promote `TrackSelectionOverride` to a top level class. diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java index ad21e461e7..b01ff7345f 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java @@ -102,7 +102,8 @@ public final class CastPlayer extends BasePlayer { COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_SET_MEDIA_ITEMS_METADATA, COMMAND_CHANGE_MEDIA_ITEMS, - COMMAND_GET_TRACKS) + COMMAND_GET_TRACKS, + COMMAND_SET_MEDIA_ITEM) .build(); public static final float MIN_SPEED_SUPPORTED = 0.5f; diff --git a/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java b/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java index bdc324e553..31b7afd87a 100644 --- a/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java +++ b/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java @@ -36,6 +36,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME; +import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA; import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; @@ -1359,6 +1360,7 @@ public class CastPlayerTest { assertThat(castPlayer.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEMS_METADATA)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)).isTrue(); + assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isFalse(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_VOLUME)).isFalse(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isFalse(); diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index 4a414f6b37..8cd90d2da1 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -384,6 +384,7 @@ public interface Player { COMMAND_GET_TEXT, COMMAND_SET_TRACK_SELECTION_PARAMETERS, COMMAND_GET_TRACKS, + COMMAND_SET_MEDIA_ITEM, }; private final FlagSet.Builder flagsBuilder; @@ -1402,7 +1403,8 @@ public interface Player { * #COMMAND_GET_VOLUME}, {@link #COMMAND_GET_DEVICE_VOLUME}, {@link #COMMAND_SET_VOLUME}, {@link * #COMMAND_SET_DEVICE_VOLUME}, {@link #COMMAND_ADJUST_DEVICE_VOLUME}, {@link * #COMMAND_SET_VIDEO_SURFACE}, {@link #COMMAND_GET_TEXT}, {@link - * #COMMAND_SET_TRACK_SELECTION_PARAMETERS} or {@link #COMMAND_GET_TRACKS}. + * #COMMAND_SET_TRACK_SELECTION_PARAMETERS}, {@link #COMMAND_GET_TRACKS} or {@link + * #COMMAND_SET_MEDIA_ITEM}. */ // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility // with Kotlin usages from before TYPE_USE was added. @@ -1441,6 +1443,7 @@ public interface Player { COMMAND_GET_TEXT, COMMAND_SET_TRACK_SELECTION_PARAMETERS, COMMAND_GET_TRACKS, + COMMAND_SET_MEDIA_ITEM, }) @interface Command {} /** Command to start, pause or resume playback. */ @@ -1520,6 +1523,8 @@ public interface Player { int COMMAND_SET_TRACK_SELECTION_PARAMETERS = 29; /** Command to get details of the current track selection. */ int COMMAND_GET_TRACKS = 30; + /** Command to set a {@link MediaItem MediaItem}. */ + int COMMAND_SET_MEDIA_ITEM = 31; /** Represents an invalid {@link Command}. */ int COMMAND_INVALID = -1; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 8902e5d76f..9f8cd262e5 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -303,7 +303,8 @@ import java.util.concurrent.TimeoutException; COMMAND_SET_DEVICE_VOLUME, COMMAND_ADJUST_DEVICE_VOLUME, COMMAND_SET_VIDEO_SURFACE, - COMMAND_GET_TEXT) + COMMAND_GET_TEXT, + COMMAND_SET_MEDIA_ITEM) .addIf( COMMAND_SET_TRACK_SELECTION_PARAMETERS, trackSelector.isSetParametersSupported()) .build(); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 4b6ce27fc9..784f6c23df 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -37,6 +37,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME; +import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA; import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; @@ -8979,6 +8980,7 @@ public final class ExoPlayerTest { assertThat(player.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)).isTrue(); assertThat(player.isCommandAvailable(COMMAND_SET_MEDIA_ITEMS_METADATA)).isTrue(); assertThat(player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)).isTrue(); + assertThat(player.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)).isTrue(); assertThat(player.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isTrue(); assertThat(player.isCommandAvailable(COMMAND_GET_VOLUME)).isTrue(); assertThat(player.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isTrue(); @@ -12128,6 +12130,7 @@ public final class ExoPlayerTest { COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_SET_MEDIA_ITEMS_METADATA, COMMAND_CHANGE_MEDIA_ITEMS, + COMMAND_SET_MEDIA_ITEM, COMMAND_GET_AUDIO_ATTRIBUTES, COMMAND_GET_VOLUME, COMMAND_GET_DEVICE_VOLUME, diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index 9fe1017622..6eb5406951 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -29,6 +29,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME; +import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA; import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; @@ -817,12 +818,12 @@ import org.checkerframework.checker.nullness.qual.NonNull; @Override public void setMediaItem(MediaItem mediaItem) { - if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { + if (!isPlayerCommandAvailable(COMMAND_SET_MEDIA_ITEM)) { return; } dispatchRemoteSessionTaskWithPlayerCommand( - COMMAND_CHANGE_MEDIA_ITEMS, + COMMAND_SET_MEDIA_ITEM, (iSession, seq) -> iSession.setMediaItem(controllerStub, seq, mediaItem.toBundle())); setMediaItemsInternal( @@ -834,12 +835,12 @@ import org.checkerframework.checker.nullness.qual.NonNull; @Override public void setMediaItem(MediaItem mediaItem, long startPositionMs) { - if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { + if (!isPlayerCommandAvailable(COMMAND_SET_MEDIA_ITEM)) { return; } dispatchRemoteSessionTaskWithPlayerCommand( - COMMAND_CHANGE_MEDIA_ITEMS, + COMMAND_SET_MEDIA_ITEM, (iSession, seq) -> iSession.setMediaItemWithStartPosition( controllerStub, seq, mediaItem.toBundle(), startPositionMs)); @@ -853,12 +854,12 @@ import org.checkerframework.checker.nullness.qual.NonNull; @Override public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { - if (!isPlayerCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { + if (!isPlayerCommandAvailable(COMMAND_SET_MEDIA_ITEM)) { return; } dispatchRemoteSessionTaskWithPlayerCommand( - COMMAND_CHANGE_MEDIA_ITEMS, + COMMAND_SET_MEDIA_ITEM, (iSession, seq) -> iSession.setMediaItemWithResetPosition( controllerStub, seq, mediaItem.toBundle(), resetPosition)); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 93816b33fd..194f074264 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -889,32 +889,6 @@ public class MediaSession { * @param controller The controller information. * @param playerCommand A {@link Player.Command command}. * @return {@link SessionResult#RESULT_SUCCESS} to proceed, or another code to ignore. - * @see Player.Command#COMMAND_PLAY_PAUSE - * @see Player.Command#COMMAND_PREPARE - * @see Player.Command#COMMAND_STOP - * @see Player.Command#COMMAND_SEEK_TO_DEFAULT_POSITION - * @see Player.Command#COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM - * @see Player.Command#COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM - * @see Player.Command#COMMAND_SEEK_TO_NEXT_MEDIA_ITEM - * @see Player.Command#COMMAND_SEEK_TO_MEDIA_ITEM - * @see Player.Command#COMMAND_SET_SPEED_AND_PITCH - * @see Player.Command#COMMAND_SET_SHUFFLE_MODE - * @see Player.Command#COMMAND_SET_REPEAT_MODE - * @see Player.Command#COMMAND_GET_CURRENT_MEDIA_ITEM - * @see Player.Command#COMMAND_GET_TIMELINE - * @see Player.Command#COMMAND_GET_MEDIA_ITEMS_METADATA - * @see Player.Command#COMMAND_SET_MEDIA_ITEMS_METADATA - * @see Player.Command#COMMAND_CHANGE_MEDIA_ITEMS - * @see Player.Command#COMMAND_GET_AUDIO_ATTRIBUTES - * @see Player.Command#COMMAND_GET_VOLUME - * @see Player.Command#COMMAND_GET_DEVICE_VOLUME - * @see Player.Command#COMMAND_SET_VOLUME - * @see Player.Command#COMMAND_SET_DEVICE_VOLUME - * @see Player.Command#COMMAND_ADJUST_DEVICE_VOLUME - * @see Player.Command#COMMAND_SET_VIDEO_SURFACE - * @see Player.Command#COMMAND_GET_TEXT - * @see Player.Command#COMMAND_SET_TRACK_SELECTION_PARAMETERS - * @see Player.Command#COMMAND_GET_TRACKS */ default @SessionResult.Code int onPlayerCommandRequest( MediaSession session, ControllerInfo controller, @Player.Command int playerCommand) { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 9dcd3bcfa9..1b84511f87 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -24,6 +24,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; +import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH; @@ -679,7 +680,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; private void handleMediaRequest(MediaItem mediaItem, boolean play) { dispatchSessionTaskWithPlayerCommand( - COMMAND_CHANGE_MEDIA_ITEMS, + COMMAND_SET_MEDIA_ITEM, controller -> { ListenableFuture> mediaItemsFuture = sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 2a8bbb8b59..109d6afc1d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -29,6 +29,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME; +import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA; import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; @@ -846,7 +847,7 @@ import java.util.concurrent.ExecutionException; dispatchSessionTaskWithPlayerCommand( caller, seq, - COMMAND_CHANGE_MEDIA_ITEMS, + COMMAND_SET_MEDIA_ITEM, (sessionImpl, controller) -> sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), (sessionImpl, controller, sequence, future) -> @@ -873,7 +874,7 @@ import java.util.concurrent.ExecutionException; dispatchSessionTaskWithPlayerCommand( caller, seq, - COMMAND_CHANGE_MEDIA_ITEMS, + COMMAND_SET_MEDIA_ITEM, (sessionImpl, controller) -> sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), (sessionImpl, controller, sequence, future) -> @@ -905,7 +906,7 @@ import java.util.concurrent.ExecutionException; dispatchSessionTaskWithPlayerCommand( caller, seq, - COMMAND_CHANGE_MEDIA_ITEMS, + COMMAND_SET_MEDIA_ITEM, (sessionImpl, controller) -> sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)), (sessionImpl, controller, sequence, future) -> diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java index 4267345a60..f00a739c91 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java @@ -22,6 +22,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME; +import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA; import static androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS; import static androidx.media3.session.MediaUtils.createPlayerCommandsWith; @@ -133,6 +134,12 @@ public class MediaSessionPermissionTest { controller -> controller.setPlaylistMetadata(MediaMetadata.EMPTY)); } + @Test + public void setMediaItem() throws Exception { + testOnCommandRequest( + COMMAND_SET_MEDIA_ITEM, controller -> controller.setMediaItem(MediaItem.EMPTY)); + } + @Test public void setMediaItems() throws Exception { testOnCommandRequest( From 9a73ae90a049cb5c5a7fec6de6c55943dd58db98 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Thu, 9 Jun 2022 17:47:15 +0000 Subject: [PATCH 22/45] Merge pull request #69 from ittiam-systems:rtp_amr_test PiperOrigin-RevId: 453905355 (cherry picked from commit 58f7ac25a79d20af3faed321e9d3d3458b4394e5) --- .../rtsp/reader/RtpAmrReaderTest.java | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpAmrReaderTest.java diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpAmrReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpAmrReaderTest.java new file mode 100644 index 0000000000..cce1a3db44 --- /dev/null +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpAmrReaderTest.java @@ -0,0 +1,220 @@ +/* + * Copyright 2022 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 androidx.media3.exoplayer.rtsp.reader; + +import static androidx.media3.common.util.Util.getBytesFromHexString; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.test.utils.FakeExtractorOutput; +import androidx.media3.test.utils.FakeTrackOutput; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.Bytes; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link RtpAmrReader}. */ +@RunWith(AndroidJUnit4.class) +public final class RtpAmrReaderTest { + + private FakeExtractorOutput extractorOutput; + + @Before + public void setUp() { + extractorOutput = new FakeExtractorOutput(); + } + + @Test + public void consume_validAmrNbPackets() { + RtpAmrReader amrReader = createAmrReader(MimeTypes.AUDIO_AMR); + + amrReader.createTracks(extractorOutput, /* trackId= */ 0); + amrReader.onReceivingFirstPacket(/* timestamp= */ 2599168056L, /* sequenceNumber= */ 40289); + amrReader.consume( + new ParsableByteArray( + Bytes.concat( + getBytesFromHexString("00"), // AMR-NB Codec Mode = 0. + getBytesFromHexString("40"), // AMR-NB ToC, F=0, FT=8. + getBytesFromHexString("0102030405"))), + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40289, + /* rtpMarker= */ false); + amrReader.consume( + new ParsableByteArray( + Bytes.concat( + getBytesFromHexString("60"), // AMR-NB Codec Mode = 6. + getBytesFromHexString("C0"), // AMR-NB ToC, F=1, FT=8. + getBytesFromHexString("060708090A"))), + /* timestamp= */ 2599169592L, + /* sequenceNumber= */ 40290, + /* rtpMarker= */ false); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)).isEqualTo(getBytesFromHexString("400102030405")); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(getBytesFromHexString("C0060708090A")); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(192000); + } + + @Test + public void consume_amrNbPacketWithInvalidFrameType_throwsIllegalArgumentException() { + RtpAmrReader amrReader = createAmrReader(MimeTypes.AUDIO_AMR); + + amrReader.createTracks(extractorOutput, /* trackId= */ 0); + amrReader.onReceivingFirstPacket(/* timestamp= */ 2599168056L, /* sequenceNumber= */ 40289); + + assertThrows( + IllegalArgumentException.class, + () -> + amrReader.consume( + new ParsableByteArray( + Bytes.concat( + getBytesFromHexString("00"), // AMR-NB Codec Mode = 0. + getBytesFromHexString("68"), // AMR-NB ToC, F=0, FT=13. + getBytesFromHexString("0102030405"))), + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40289, + /* rtpMarker= */ false)); + } + + @Test + public void consume_amrNbPacketWithInvalidFrameSize_throwsIllegalArgumentException() { + // The payload frame type is 8, expecting five bytes of data, getting four. + RtpAmrReader amrReader = createAmrReader(MimeTypes.AUDIO_AMR); + + amrReader.createTracks(extractorOutput, /* trackId= */ 0); + amrReader.onReceivingFirstPacket(/* timestamp= */ 2599168056L, /* sequenceNumber= */ 40289); + + assertThrows( + IllegalArgumentException.class, + () -> + amrReader.consume( + new ParsableByteArray( + Bytes.concat( + getBytesFromHexString("00"), // AMR-NB Codec Mode = 0. + getBytesFromHexString("40"), // AMR-NB ToC, F=0, FT=8. + getBytesFromHexString("01020304"))), + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40289, + /* rtpMarker= */ false)); + } + + @Test + public void consume_validAmrWbPackets() { + RtpAmrReader amrReader = createAmrReader(MimeTypes.AUDIO_AMR_WB); + + amrReader.createTracks(extractorOutput, /* trackId= */ 0); + amrReader.onReceivingFirstPacket(/* timestamp= */ 2599168056L, /* sequenceNumber= */ 40289); + amrReader.consume( + new ParsableByteArray( + Bytes.concat( + getBytesFromHexString("60"), // AMR-WB Codec Mode = 6. + getBytesFromHexString("87"), // AMR-WB ToC, F=0, FT=0. + getBytesFromHexString("0102030405060708090A0B0C0D0E0F1011"))), + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40289, + /* rtpMarker= */ false); + amrReader.consume( + new ParsableByteArray( + Bytes.concat( + getBytesFromHexString("00"), // AMR-WB Codec Mode = 0. + getBytesFromHexString("7E"))), // AMR-WB ToC, F=0, FT=15. + /* timestamp= */ 2599169592L, + /* sequenceNumber= */ 40290, + /* rtpMarker= */ false); + + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)) + .isEqualTo(getBytesFromHexString("870102030405060708090A0B0C0D0E0F1011")); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(getBytesFromHexString("7E")); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(96000); + } + + @Test + public void consume_amrWbPacketWithInvalidFrameType_throwsIllegalArgumentException() { + RtpAmrReader amrReader = createAmrReader(MimeTypes.AUDIO_AMR_WB); + + amrReader.createTracks(extractorOutput, /* trackId= */ 0); + amrReader.onReceivingFirstPacket(/* timestamp= */ 2599168056L, /* sequenceNumber= */ 40289); + + assertThrows( + IllegalArgumentException.class, + () -> + amrReader.consume( + new ParsableByteArray( + Bytes.concat( + getBytesFromHexString("00"), // AMR-WB Codec Mode = 0. + getBytesFromHexString("D5"), // AMR-WB ToC, F=1, FT=10. + getBytesFromHexString("01020304"))), + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40289, + /* rtpMarker= */ false)); + } + + @Test + public void consume_amrWbPacketWithInvalidFrameSize_throwsIllegalArgumentException() { + // The payload frame type is 4, expecting 40 bytes of data, getting two. + RtpAmrReader amrReader = createAmrReader(MimeTypes.AUDIO_AMR_WB); + + amrReader.createTracks(extractorOutput, /* trackId= */ 0); + amrReader.onReceivingFirstPacket(/* timestamp= */ 2599168056L, /* sequenceNumber= */ 40289); + + assertThrows( + IllegalArgumentException.class, + () -> + amrReader.consume( + new ParsableByteArray( + Bytes.concat( + getBytesFromHexString("00"), // AMR-WB Codec Mode = 0. + getBytesFromHexString("A5"), // AMR-WB ToC, F=1, FT=4. + getBytesFromHexString("0102"))), + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40289, + /* rtpMarker= */ false)); + } + + private static RtpAmrReader createAmrReader(String mimeType) { + switch (mimeType) { + case MimeTypes.AUDIO_AMR: + return new RtpAmrReader(createRtpPayloadFormat(mimeType, /* sampleRate= */ 8000)); + case MimeTypes.AUDIO_AMR_WB: + return new RtpAmrReader(createRtpPayloadFormat(mimeType, /* sampleRate= */ 16000)); + default: + throw new IllegalArgumentException("MimeType " + mimeType + " not supported."); + } + } + + private static RtpPayloadFormat createRtpPayloadFormat(String mimeType, int sampleRate) { + return new RtpPayloadFormat( + new Format.Builder() + .setChannelCount(1) + .setSampleMimeType(mimeType) + .setSampleRate(sampleRate) + .build(), + /* rtpPayloadType= */ 97, + /* clockRate= */ sampleRate, + /* fmtpParameters= */ ImmutableMap.of()); + } +} From 7c0b787bdb81647b4907fb348b72b024114cf9fc Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 9 Jun 2022 15:36:13 +0000 Subject: [PATCH 23/45] Add session URI to Intent used with the notification The service handles three different types of `Intents`. Custom command and media command Intents created by the library and media button event Intents from other sources. Media commands from the library as well as from external sources have the action set to `android.intent.action.MEDIA_BUTTON`. If the data URI is set and can be used to identify a session then it is a library Intent. If the Intent is coming from an external KeyEvent, the service implementation is asked which session to use by calling `onGetSession(controllerInfo)` with the controller info being an anonymous legacy controller info. Intents representing a custom command are always coming from the library and hence always have a data URI. Issue: androidx/media#82 PiperOrigin-RevId: 453932972 (cherry picked from commit 8b592fc77aeead345adac999eda27da55df0ae01) --- .../media3/session/DefaultActionFactory.java | 26 ++++--- .../DefaultMediaNotificationProvider.java | 13 +++- .../media3/session/MediaNotification.java | 20 ++++-- .../media3/session/MediaSessionService.java | 10 +-- .../session/DefaultActionFactoryTest.java | 27 ++++++-- .../DefaultMediaNotificationProviderTest.java | 69 +++++++++++++++---- 6 files changed, 123 insertions(+), 42 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java b/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java index cf63e6cc4b..699f9cb991 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java @@ -66,20 +66,25 @@ import androidx.media3.common.util.Util; @Override public NotificationCompat.Action createMediaAction( - IconCompat icon, CharSequence title, @Player.Command int command) { - return new NotificationCompat.Action(icon, title, createMediaActionPendingIntent(command)); + MediaSession mediaSession, IconCompat icon, CharSequence title, @Player.Command int command) { + return new NotificationCompat.Action( + icon, title, createMediaActionPendingIntent(mediaSession, command)); } @Override public NotificationCompat.Action createCustomAction( - IconCompat icon, CharSequence title, String customAction, Bundle extras) { + MediaSession mediaSession, + IconCompat icon, + CharSequence title, + String customAction, + Bundle extras) { return new NotificationCompat.Action( - icon, title, createCustomActionPendingIntent(customAction, extras)); + icon, title, createCustomActionPendingIntent(mediaSession, customAction, extras)); } @Override public NotificationCompat.Action createCustomActionFromCustomCommandButton( - CommandButton customCommandButton) { + MediaSession mediaSession, CommandButton customCommandButton) { checkArgument( customCommandButton.sessionCommand != null && customCommandButton.sessionCommand.commandCode @@ -88,13 +93,16 @@ import androidx.media3.common.util.Util; return new NotificationCompat.Action( IconCompat.createWithResource(service, customCommandButton.iconResId), customCommandButton.displayName, - createCustomActionPendingIntent(customCommand.customAction, customCommand.customExtras)); + createCustomActionPendingIntent( + mediaSession, customCommand.customAction, customCommand.customExtras)); } @Override - public PendingIntent createMediaActionPendingIntent(@Player.Command long command) { + public PendingIntent createMediaActionPendingIntent( + MediaSession mediaSession, @Player.Command long command) { int keyCode = toKeyCode(command); Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.setData(mediaSession.getImpl().getUri()); intent.setComponent(new ComponentName(service, service.getClass())); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); if (Util.SDK_INT >= 26 && command == COMMAND_PLAY_PAUSE) { @@ -126,8 +134,10 @@ import androidx.media3.common.util.Util; return KEYCODE_UNKNOWN; } - private PendingIntent createCustomActionPendingIntent(String action, Bundle extras) { + private PendingIntent createCustomActionPendingIntent( + MediaSession mediaSession, String action, Bundle extras) { Intent intent = new Intent(ACTION_CUSTOM); + intent.setData(mediaSession.getImpl().getUri()); intent.setComponent(new ComponentName(service, service.getClass())); intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM, action); intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM_EXTRAS, extras); diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index dc88c45510..992a6fdad0 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -140,6 +140,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi MediaStyle mediaStyle = new MediaStyle(); int[] compactViewIndices = addNotificationActions( + mediaSession, getMediaButtons(player.getAvailableCommands(), customLayout, player.getPlayWhenReady()), builder, actionFactory); @@ -173,7 +174,8 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi if (player.isCommandAvailable(COMMAND_STOP) || Util.SDK_INT < 21) { // We must include a cancel intent for pre-L devices. - mediaStyle.setCancelButtonIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP)); + mediaStyle.setCancelButtonIntent( + actionFactory.createMediaActionPendingIntent(mediaSession, COMMAND_STOP)); } long playbackStartTimeMs = getPlaybackStartTimeEpochMs(player); @@ -186,7 +188,8 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi Notification notification = builder .setContentIntent(mediaSession.getSessionActivity()) - .setDeleteIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP)) + .setDeleteIntent( + actionFactory.createMediaActionPendingIntent(mediaSession, COMMAND_STOP)) .setOnlyAlertOnce(true) .setSmallIcon(R.drawable.media3_notification_small_icon) .setStyle(mediaStyle) @@ -292,6 +295,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi * buttons are marked with {@link DefaultMediaNotificationProvider#COMMAND_KEY_COMPACT_VIEW_INDEX} * to declare the index in compact view of the given command button in the button extras. * + * @param mediaSession The media session to which the actions will be sent. * @param mediaButtons The command buttons to be included in the notification. * @param builder The builder to add the actions to. * @param actionFactory The actions factory to be used to build notifications. @@ -300,6 +304,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi * notification}. */ protected int[] addNotificationActions( + MediaSession mediaSession, List mediaButtons, NotificationCompat.Builder builder, MediaNotification.ActionFactory actionFactory) { @@ -309,11 +314,13 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi for (int i = 0; i < mediaButtons.size(); i++) { CommandButton commandButton = mediaButtons.get(i); if (commandButton.sessionCommand != null) { - builder.addAction(actionFactory.createCustomActionFromCustomCommandButton(commandButton)); + builder.addAction( + actionFactory.createCustomActionFromCustomCommandButton(mediaSession, commandButton)); } else { checkState(commandButton.playerCommand != COMMAND_INVALID); builder.addAction( actionFactory.createMediaAction( + mediaSession, IconCompat.createWithResource(context, commandButton.iconResId), commandButton.displayName, commandButton.playerCommand)); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java index c9c39af9bc..14064cfc55 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java @@ -43,12 +43,16 @@ public final class MediaNotification { * Creates a {@link NotificationCompat.Action} for a notification. These actions will be handled * by the library. * + * @param mediaSession The media session to which the action will be sent. * @param icon The icon to show for this action. * @param title The title of the action. * @param command A command to send when users trigger this action. */ NotificationCompat.Action createMediaAction( - IconCompat icon, CharSequence title, @Player.Command int command); + MediaSession mediaSession, + IconCompat icon, + CharSequence title, + @Player.Command int command); /** * Creates a {@link NotificationCompat.Action} for a notification with a custom action. Actions @@ -56,6 +60,7 @@ public final class MediaNotification { * to the {@linkplain MediaNotification.Provider#handleCustomCommand notification provider} that * provided them. * + * @param mediaSession The media session to which the action will be sent. * @param icon The icon to show for this action. * @param title The title of the action. * @param customAction The custom action set. @@ -63,7 +68,11 @@ public final class MediaNotification { * @see MediaNotification.Provider#handleCustomCommand */ NotificationCompat.Action createCustomAction( - IconCompat icon, CharSequence title, String customAction, Bundle extras); + MediaSession mediaSession, + IconCompat icon, + CharSequence title, + String customAction, + Bundle extras); /** * Creates a {@link NotificationCompat.Action} for a notification from a custom command button. @@ -76,18 +85,21 @@ public final class MediaNotification { * SessionCommand#customExtras command's extras} will be passed to {@link * Provider#handleCustomCommand(MediaSession, String, Bundle)} when the action is executed. * + * @param mediaSession The media session to which the action will be sent. * @param customCommandButton A {@linkplain CommandButton custom command button}. * @see MediaNotification.Provider#handleCustomCommand */ NotificationCompat.Action createCustomActionFromCustomCommandButton( - CommandButton customCommandButton); + MediaSession mediaSession, CommandButton customCommandButton); /** * Creates a {@link PendingIntent} for a media action that will be handled by the library. * + * @param mediaSession The media session to which the action will be sent. * @param command The intent's command. */ - PendingIntent createMediaActionPendingIntent(@Player.Command long command); + PendingIntent createMediaActionPendingIntent( + MediaSession mediaSession, @Player.Command long command); } /** diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java index d44c3d0da1..680c8834b9 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -354,19 +354,11 @@ public abstract class MediaSessionService extends Service { if (keyEvent != null) { session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent); } - } else if (actionFactory.isCustomAction(intent)) { + } else if (session != null && actionFactory.isCustomAction(intent)) { @Nullable String customAction = actionFactory.getCustomAction(intent); if (customAction == null) { return START_STICKY; } - if (session == null) { - ControllerInfo controllerInfo = ControllerInfo.createLegacyControllerInfo(); - session = onGetSession(controllerInfo); - if (session == null) { - return START_STICKY; - } - addSession(session); - } Bundle customExtras = actionFactory.getCustomActionExtras(intent); getMediaNotificationManager().onCustomAction(session, customAction, customExtras); } diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java index fc7f5b1402..73c68ce21f 100644 --- a/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java @@ -16,10 +16,13 @@ package androidx.media3.session; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.app.PendingIntent; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; @@ -39,12 +42,18 @@ public class DefaultActionFactoryTest { public void createMediaPendingIntent_intentIsMediaAction() { DefaultActionFactory actionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); + MediaSession mockMediaSession = mock(MediaSession.class); + MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class); + when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl); + Uri dataUri = Uri.parse("http://example.com"); + when(mockMediaSessionImpl.getUri()).thenReturn(dataUri); PendingIntent pendingIntent = - actionFactory.createMediaActionPendingIntent(Player.COMMAND_PLAY_PAUSE); + actionFactory.createMediaActionPendingIntent(mockMediaSession, Player.COMMAND_PLAY_PAUSE); ShadowPendingIntent shadowPendingIntent = shadowOf(pendingIntent); assertThat(actionFactory.isMediaAction(shadowPendingIntent.getSavedIntent())).isTrue(); + assertThat(shadowPendingIntent.getSavedIntent().getData()).isEqualTo(dataUri); } @Test @@ -71,7 +80,11 @@ public class DefaultActionFactoryTest { public void createCustomActionFromCustomCommandButton() { DefaultActionFactory actionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); - + MediaSession mockMediaSession = mock(MediaSession.class); + MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class); + when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl); + Uri dataUri = Uri.parse("http://example.com"); + when(mockMediaSessionImpl.getUri()).thenReturn(dataUri); Bundle commandBundle = new Bundle(); commandBundle.putString("command-key", "command-value"); Bundle buttonBundle = new Bundle(); @@ -85,8 +98,11 @@ public class DefaultActionFactoryTest { .build(); NotificationCompat.Action notificationAction = - actionFactory.createCustomActionFromCustomCommandButton(customSessionCommand); + actionFactory.createCustomActionFromCustomCommandButton( + mockMediaSession, customSessionCommand); + ShadowPendingIntent shadowPendingIntent = shadowOf(notificationAction.actionIntent); + assertThat(shadowPendingIntent.getSavedIntent().getData()).isEqualTo(dataUri); assertThat(String.valueOf(notificationAction.title)).isEqualTo("name"); assertThat(notificationAction.getIconCompat().getResId()) .isEqualTo(R.drawable.media3_notification_pause); @@ -99,7 +115,6 @@ public class DefaultActionFactoryTest { createCustomActionFromCustomCommandButton_notACustomAction_throwsIllegalArgumentException() { DefaultActionFactory actionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); - CommandButton customSessionCommand = new CommandButton.Builder() .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) @@ -109,7 +124,9 @@ public class DefaultActionFactoryTest { Assert.assertThrows( IllegalArgumentException.class, - () -> actionFactory.createCustomActionFromCustomCommandButton(customSessionCommand)); + () -> + actionFactory.createCustomActionFromCustomCommandButton( + mock(MediaSession.class), customSessionCommand)); } /** A test service for unit tests. */ diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java index de56133863..4cca95092b 100644 --- a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java @@ -22,7 +22,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import android.net.Uri; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; @@ -119,6 +121,7 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext()); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); + MediaSession mockMediaSession = mock(MediaSession.class); CommandButton commandButton1 = new CommandButton.Builder() .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) @@ -155,6 +158,7 @@ public class DefaultMediaNotificationProviderTest { int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( + mockMediaSession, ImmutableList.of(commandButton1, commandButton2, commandButton3, commandButton4), mockNotificationBuilder, mockActionFactory); @@ -163,10 +167,17 @@ public class DefaultMediaNotificationProviderTest { InOrder inOrder = Mockito.inOrder(mockActionFactory); inOrder .verify(mockActionFactory) - .createMediaAction(any(), eq("displayName"), eq(commandButton1.playerCommand)); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton2); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton3); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton4); + .createMediaAction( + eq(mockMediaSession), any(), eq("displayName"), eq(commandButton1.playerCommand)); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton2); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton3); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton4); verifyNoMoreInteractions(mockActionFactory); assertThat(compactViewIndices).asList().containsExactly(1, 3, 2).inOrder(); } @@ -177,6 +188,7 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext()); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); + MediaSession mockMediaSession = mock(MediaSession.class); CommandButton commandButton1 = new CommandButton.Builder() .setDisplayName("displayName") @@ -192,6 +204,7 @@ public class DefaultMediaNotificationProviderTest { int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( + mockMediaSession, ImmutableList.of(commandButton1, commandButton2), mockNotificationBuilder, mockActionFactory); @@ -202,10 +215,13 @@ public class DefaultMediaNotificationProviderTest { List actions = actionCaptor.getAllValues(); assertThat(actions).hasSize(2); InOrder inOrder = Mockito.inOrder(mockActionFactory); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton1); inOrder .verify(mockActionFactory) - .createMediaAction(any(), eq("displayName"), eq(commandButton2.playerCommand)); + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); + inOrder + .verify(mockActionFactory) + .createMediaAction( + eq(mockMediaSession), any(), eq("displayName"), eq(commandButton2.playerCommand)); verifyNoMoreInteractions(mockActionFactory); assertThat(compactViewIndices).asList().containsExactly(1); } @@ -217,6 +233,7 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext()); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); + MediaSession mockMediaSession = mock(MediaSession.class); CommandButton commandButton1 = new CommandButton.Builder() .setDisplayName("displayName") @@ -226,10 +243,15 @@ public class DefaultMediaNotificationProviderTest { int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( - ImmutableList.of(commandButton1), mockNotificationBuilder, mockActionFactory); + mockMediaSession, + ImmutableList.of(commandButton1), + mockNotificationBuilder, + mockActionFactory); InOrder inOrder = Mockito.inOrder(mockActionFactory); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton1); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); verifyNoMoreInteractions(mockActionFactory); assertThat(compactViewIndices).asList().isEmpty(); } @@ -240,6 +262,7 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext()); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); + MediaSession mockMediaSession = mock(MediaSession.class); Bundle commandButtonBundle1 = new Bundle(); commandButtonBundle1.putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 2); CommandButton commandButton1 = @@ -262,13 +285,18 @@ public class DefaultMediaNotificationProviderTest { int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( + mockMediaSession, ImmutableList.of(commandButton1, commandButton2), mockNotificationBuilder, mockActionFactory); InOrder inOrder = Mockito.inOrder(mockActionFactory); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton1); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton2); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton2); verifyNoMoreInteractions(mockActionFactory); assertThat(compactViewIndices).asList().isEmpty(); } @@ -279,6 +307,7 @@ public class DefaultMediaNotificationProviderTest { new DefaultMediaNotificationProvider(ApplicationProvider.getApplicationContext()); NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); MediaNotification.ActionFactory mockActionFactory = mock(MediaNotification.ActionFactory.class); + MediaSession mockMediaSession = mock(MediaSession.class); Bundle commandButtonBundle = new Bundle(); commandButtonBundle.putInt(DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX, 1); CommandButton commandButton1 = @@ -291,10 +320,15 @@ public class DefaultMediaNotificationProviderTest { int[] compactViewIndices = defaultMediaNotificationProvider.addNotificationActions( - ImmutableList.of(commandButton1), mockNotificationBuilder, mockActionFactory); + mockMediaSession, + ImmutableList.of(commandButton1), + mockNotificationBuilder, + mockActionFactory); InOrder inOrder = Mockito.inOrder(mockActionFactory); - inOrder.verify(mockActionFactory).createCustomActionFromCustomCommandButton(commandButton1); + inOrder + .verify(mockActionFactory) + .createCustomActionFromCustomCommandButton(mockMediaSession, commandButton1); verifyNoMoreInteractions(mockActionFactory); // [INDEX_UNSET, 1, INDEX_UNSET] cropped up to the first INDEX_UNSET value assertThat(compactViewIndices).asList().isEmpty(); @@ -307,6 +341,10 @@ public class DefaultMediaNotificationProviderTest { NotificationCompat.Builder mockNotificationBuilder = mock(NotificationCompat.Builder.class); DefaultActionFactory defaultActionFactory = new DefaultActionFactory(Robolectric.setupService(TestService.class)); + MediaSession mockMediaSession = mock(MediaSession.class); + MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class); + when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl); + when(mockMediaSessionImpl.getUri()).thenReturn(Uri.parse("http://example.com")); Bundle commandButtonBundle = new Bundle(); commandButtonBundle.putString("testKey", "testValue"); CommandButton commandButton1 = @@ -318,12 +356,17 @@ public class DefaultMediaNotificationProviderTest { .build(); defaultMediaNotificationProvider.addNotificationActions( - ImmutableList.of(commandButton1), mockNotificationBuilder, defaultActionFactory); + mockMediaSession, + ImmutableList.of(commandButton1), + mockNotificationBuilder, + defaultActionFactory); ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(NotificationCompat.Action.class); verify(mockNotificationBuilder).addAction(actionCaptor.capture()); verifyNoMoreInteractions(mockNotificationBuilder); + verify(mockMediaSessionImpl).getUri(); + verifyNoMoreInteractions(mockMediaSessionImpl); List actions = actionCaptor.getAllValues(); assertThat(actions).hasSize(1); assertThat(String.valueOf(actions.get(0).title)).isEqualTo("displayName1"); From 74fbf0171f21316a137d1721d5845f51b5870802 Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 9 Jun 2022 16:19:51 +0000 Subject: [PATCH 24/45] Fix bug: playback is frozen with HLS chunkless preparation This change fixes a bug where the player is frozen with HLS chunkless preparation because the audio stream wrappers are not marked as master timestamp sources before preparation. #minor-release PiperOrigin-RevId: 453941815 (cherry picked from commit 9221eeb2d87f049c668a056f7fd7901b91dd51e3) --- .../java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java index 101bb7a2ed..863b83f06e 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java @@ -555,8 +555,10 @@ public final class HlsMediaPeriod this.sampleStreamWrappers = sampleStreamWrappers.toArray(new HlsSampleStreamWrapper[0]); this.manifestUrlIndicesPerWrapper = manifestUrlIndicesPerWrapper.toArray(new int[0][]); pendingPrepareCount = this.sampleStreamWrappers.length; - // Set timestamp master and trigger preparation (if not already prepared) - this.sampleStreamWrappers[0].setIsTimestampMaster(true); + // Set timestamp masters and trigger preparation (if not already prepared) + for (int i = 0; i < audioVideoSampleStreamWrapperCount; i++) { + this.sampleStreamWrappers[i].setIsTimestampMaster(true); + } for (HlsSampleStreamWrapper sampleStreamWrapper : this.sampleStreamWrappers) { sampleStreamWrapper.continuePreparing(); } From 0fd24c2fa35db1f50c7fa16aee6a9df601794472 Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 9 Jun 2022 17:27:12 +0000 Subject: [PATCH 25/45] DefaultTrackSelector: Constrain audio channel count The track selector will select multi-channel formats when those can be spatialized, otherwise the selector will prefer stereo/mono audio tracks. When the device supports audio spatialization (Android 12L+), the DefaultTrackSelector will monitor for changes in the platform Spatializer and trigger a new track selection upon a Spatializer change event. Devices with a `television` UI mode are excluded from audio channel count constraints. #minor-release PiperOrigin-RevId: 453957269 (cherry picked from commit e2f0fd76730fd4042e8b2226300e5173b0179dc1) --- RELEASENOTES.md | 20 + .../media3/exoplayer/ExoPlayerImpl.java | 2 + .../exoplayer/offline/DownloadHelper.java | 2 + .../trackselection/DefaultTrackSelector.java | 397 ++++++++++++++++-- .../trackselection/TrackSelector.java | 17 +- .../DefaultTrackSelectorTest.java | 125 +++++- 6 files changed, 514 insertions(+), 49 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 52d85073d3..aef2c6fd92 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -39,6 +39,24 @@ `DefaultTrackSelector.Parameters.buildUpon` to return `DefaultTrackSelector.Parameters.Builder` instead of the deprecated `DefaultTrackSelector.ParametersBuilder`. + * Add + `DefaultTrackSelector.Parameters.constrainAudioChannelCountToDeviceCapabilities`. + which is enabled by default. When enabled, the `DefaultTrackSelector` + will prefer audio tracks whose channel count does not exceed the device + output capabilities. On handheld devices, the `DefaultTrackSelector` + will prefer stereo/mono over multichannel audio formats, unless the + multichannel format can be + [Spatialized](https://developer.android.com/reference/android/media/Spatializer) + (Android 12L+) or is a Dolby surround sound format. In addition, on + devices that support audio spatialization, the `DefaultTrackSelector` + will monitor for changes in the + [Spatializer properties](https://developer.android.com/reference/android/media/Spatializer.OnSpatializerStateChangedListener) + and trigger a new track selection upon these. Devices with a + `television` + [UI mode](https://developer.android.com/guide/topics/resources/providing-resources#UiModeQualifier) + are excluded from these constraints and the format with the highest + channel count will be preferred. To enable this feature, the + `DefaultTrackSelector` instance must be constructed with a `Context`. * Video: * Rename `DummySurface` to `PlaceholderSurface`. * Add AV1 support to the `MediaCodecVideoRenderer.getCodecMaxInputSize`. @@ -171,6 +189,8 @@ `DEFAULT_TRACK_SELECTOR_PARAMETERS` constants. Use `getDefaultTrackSelectorParameters(Context)` instead when possible, and `DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT` otherwise. + * Remove constructor `DefaultTrackSelector(ExoTrackSelection.Factory)`. + Use `DefaultTrackSelector(Context, ExoTrackSelection.Factory)` instead. ### 1.0.0-alpha03 (2022-03-14) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 9f8cd262e5..389112484a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -380,6 +380,7 @@ import java.util.concurrent.TimeoutException; deviceInfo = createDeviceInfo(streamVolumeManager); videoSize = VideoSize.UNKNOWN; + trackSelector.setAudioAttributes(audioAttributes); sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_SESSION_ID, audioSessionId); sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_AUDIO_SESSION_ID, audioSessionId); sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_ATTRIBUTES, audioAttributes); @@ -1375,6 +1376,7 @@ import java.util.concurrent.TimeoutException; } audioFocusManager.setAudioAttributes(handleAudioFocus ? newAudioAttributes : null); + trackSelector.setAudioAttributes(newAudioAttributes); boolean playWhenReady = getPlayWhenReady(); @AudioFocusManager.PlayerCommand int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java index 25f6c98114..2874601a70 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java @@ -110,6 +110,7 @@ public final class DownloadHelper { DefaultTrackSelector.Parameters.DEFAULT_WITHOUT_CONTEXT .buildUpon() .setForceHighestSupportedBitrate(true) + .setConstrainAudioChannelCountToDeviceCapabilities(false) .build(); /** Returns the default parameters used for track selection for downloading. */ @@ -117,6 +118,7 @@ public final class DownloadHelper { return DefaultTrackSelector.Parameters.getDefaults(context) .buildUpon() .setForceHighestSupportedBitrate(true) + .setConstrainAudioChannelCountToDeviceCapabilities(false) .build(); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index f680f58e19..fbd9f6e57d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -15,19 +15,29 @@ */ package androidx.media3.exoplayer.trackselection; +import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static androidx.media3.common.util.Util.castNonNull; import static java.lang.annotation.ElementType.TYPE_USE; import static java.util.Collections.max; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Point; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.Spatializer; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.text.TextUtils; import android.util.Pair; import android.util.SparseArray; import android.util.SparseBooleanArray; +import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.media3.common.AudioAttributes; import androidx.media3.common.Bundleable; import androidx.media3.common.C; import androidx.media3.common.C.FormatSupport; @@ -40,6 +50,7 @@ import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.BundleableUtil; +import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.ExoPlaybackException; @@ -50,6 +61,7 @@ import androidx.media3.exoplayer.RendererCapabilities.Capabilities; import androidx.media3.exoplayer.RendererConfiguration; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.TrackGroupArray; +import com.google.common.base.Predicate; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; import com.google.common.collect.Ordering; @@ -65,7 +77,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.nullness.compatqual.NullableType; /** @@ -101,6 +112,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @UnstableApi public class DefaultTrackSelector extends MappingTrackSelector { + private static final String TAG = "DefaultTrackSelector"; + private static final String AUDIO_CHANNEL_COUNT_CONSTRAINTS_WARN_MESSAGE = + "Audio channel count constraints cannot be applied without reference to Context. Build the" + + " track selector instance with one of the non-deprecated constructors that take a" + + " Context argument."; + /** * @deprecated Use {@link Parameters.Builder} instead. */ @@ -680,6 +697,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean allowAudioMixedSampleRateAdaptiveness; private boolean allowAudioMixedChannelCountAdaptiveness; private boolean allowAudioMixedDecoderSupportAdaptiveness; + private boolean constrainAudioChannelCountToDeviceCapabilities; // General private boolean exceedRendererCapabilitiesIfNecessary; private boolean tunnelingEnabled; @@ -734,6 +752,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { initialValues.allowAudioMixedChannelCountAdaptiveness; allowAudioMixedDecoderSupportAdaptiveness = initialValues.allowAudioMixedDecoderSupportAdaptiveness; + constrainAudioChannelCountToDeviceCapabilities = + initialValues.constrainAudioChannelCountToDeviceCapabilities; // General exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary; tunnelingEnabled = initialValues.tunnelingEnabled; @@ -746,6 +766,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @SuppressWarnings("method.invocation") // Only setter are invoked. private Builder(Bundle bundle) { super(bundle); + init(); Parameters defaultValue = Parameters.DEFAULT_WITHOUT_CONTEXT; // Video setExceedVideoConstraintsIfNecessary( @@ -788,6 +809,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { Parameters.keyForField( Parameters.FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), defaultValue.allowAudioMixedDecoderSupportAdaptiveness)); + setConstrainAudioChannelCountToDeviceCapabilities( + bundle.getBoolean( + Parameters.keyForField( + Parameters.FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES), + defaultValue.constrainAudioChannelCountToDeviceCapabilities)); // General setExceedRendererCapabilitiesIfNecessary( bundle.getBoolean( @@ -1082,6 +1108,36 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + /** + * Whether to only select audio tracks with channel counts that don't exceed the device's + * output capabilities. The default value is {@code true}. + * + *

    When enabled, the track selector will prefer stereo/mono audio tracks over multichannel + * if the audio cannot be spatialized or the device is outputting stereo audio. For example, + * on a mobile device that outputs non-spatialized audio to its speakers. Dolby surround sound + * formats are excluded from these constraints because some Dolby decoders are known to + * spatialize multichannel audio on Android OS versions that don't support the {@link + * Spatializer} API. + * + *

    For devices with Android 12L+ that support {@linkplain Spatializer audio + * spatialization}, when this is enabled the track selector will trigger a new track selection + * everytime a change in {@linkplain Spatializer.OnSpatializerStateChangedListener + * spatialization properties} is detected. + * + *

    The constraints do not apply on devices with {@code + * television} UI mode. + * + *

    The constraints do not apply when the track selector is created without a reference to a + * {@link Context} via the deprecated {@link + * DefaultTrackSelector#DefaultTrackSelector(TrackSelectionParameters, + * ExoTrackSelection.Factory)} constructor. + */ + public Builder setConstrainAudioChannelCountToDeviceCapabilities(boolean enabled) { + constrainAudioChannelCountToDeviceCapabilities = enabled; + return this; + } + // Text @Override @@ -1381,6 +1437,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { allowAudioMixedSampleRateAdaptiveness = false; allowAudioMixedChannelCountAdaptiveness = false; allowAudioMixedDecoderSupportAdaptiveness = false; + constrainAudioChannelCountToDeviceCapabilities = true; // General exceedRendererCapabilitiesIfNecessary = true; tunnelingEnabled = false; @@ -1475,6 +1532,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } // Video + /** * Whether to exceed the {@link #maxVideoWidth}, {@link #maxVideoHeight} and {@link * #maxVideoBitrate} constraints when no selection can be made otherwise. The default value is @@ -1499,6 +1557,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { * RendererCapabilities.HardwareAccelerationSupport}. */ public final boolean allowVideoMixedDecoderSupportAdaptiveness; + + // Audio + /** * Whether to exceed the {@link #maxAudioChannelCount} and {@link #maxAudioBitrate} constraints * when no selection can be made otherwise. The default value is {@code true}. @@ -1526,6 +1587,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { * RendererCapabilities.HardwareAccelerationSupport}. */ public final boolean allowAudioMixedDecoderSupportAdaptiveness; + /** + * Whether to constrain audio track selection so that the selected track's channel count does + * not exceed the device's output capabilities. The default value is {@code true}. + */ + public final boolean constrainAudioChannelCountToDeviceCapabilities; + + // General + /** * Whether to exceed renderer capabilities when no selection can be made otherwise. * @@ -1566,6 +1635,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { allowAudioMixedSampleRateAdaptiveness = builder.allowAudioMixedSampleRateAdaptiveness; allowAudioMixedChannelCountAdaptiveness = builder.allowAudioMixedChannelCountAdaptiveness; allowAudioMixedDecoderSupportAdaptiveness = builder.allowAudioMixedDecoderSupportAdaptiveness; + constrainAudioChannelCountToDeviceCapabilities = + builder.constrainAudioChannelCountToDeviceCapabilities; // General exceedRendererCapabilitiesIfNecessary = builder.exceedRendererCapabilitiesIfNecessary; tunnelingEnabled = builder.tunnelingEnabled; @@ -1654,6 +1725,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { == other.allowAudioMixedChannelCountAdaptiveness && allowAudioMixedDecoderSupportAdaptiveness == other.allowAudioMixedDecoderSupportAdaptiveness + && constrainAudioChannelCountToDeviceCapabilities + == other.constrainAudioChannelCountToDeviceCapabilities // General && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary && tunnelingEnabled == other.tunnelingEnabled @@ -1678,6 +1751,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + (allowAudioMixedSampleRateAdaptiveness ? 1 : 0); result = 31 * result + (allowAudioMixedChannelCountAdaptiveness ? 1 : 0); result = 31 * result + (allowAudioMixedDecoderSupportAdaptiveness ? 1 : 0); + result = 31 * result + (constrainAudioChannelCountToDeviceCapabilities ? 1 : 0); // General result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); result = 31 * result + (tunnelingEnabled ? 1 : 0); @@ -1712,6 +1786,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { FIELD_CUSTOM_ID_BASE + 14; private static final int FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = FIELD_CUSTOM_ID_BASE + 15; + private static final int FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES = + FIELD_CUSTOM_ID_BASE + 16; @Override public Bundle toBundle() { @@ -1746,6 +1822,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { bundle.putBoolean( keyForField(FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), allowAudioMixedDecoderSupportAdaptiveness); + bundle.putBoolean( + keyForField(FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES), + constrainAudioChannelCountToDeviceCapabilities); // General bundle.putBoolean( keyForField(FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY), @@ -2004,8 +2083,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** Ordering where all elements are equal. */ private static final Ordering NO_ORDER = Ordering.from((first, second) -> 0); + private final Object lock; + @Nullable public final Context context; private final ExoTrackSelection.Factory trackSelectionFactory; - private final AtomicReference parametersReference; + private final boolean deviceIsTV; + + @GuardedBy("lock") + private Parameters parameters; + + @GuardedBy("lock") + @Nullable + private SpatializerWrapperV32 spatializer; + + @GuardedBy("lock") + private AudioAttributes audioAttributes; /** * @deprecated Use {@link #DefaultTrackSelector(Context)} instead. @@ -2015,14 +2106,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { this(Parameters.DEFAULT_WITHOUT_CONTEXT, new AdaptiveTrackSelection.Factory()); } - /** - * @deprecated Use {@link #DefaultTrackSelector(Context, ExoTrackSelection.Factory)}. - */ - @Deprecated - public DefaultTrackSelector(ExoTrackSelection.Factory trackSelectionFactory) { - this(Parameters.DEFAULT_WITHOUT_CONTEXT, trackSelectionFactory); - } - /** * @param context Any {@link Context}. */ @@ -2035,26 +2118,88 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s. */ public DefaultTrackSelector(Context context, ExoTrackSelection.Factory trackSelectionFactory) { - this(Parameters.getDefaults(context), trackSelectionFactory); + this(context, Parameters.getDefaults(context), trackSelectionFactory); } /** + * @param context Any {@link Context}. + * @param parameters Initial {@link TrackSelectionParameters}. + */ + public DefaultTrackSelector(Context context, TrackSelectionParameters parameters) { + this(context, parameters, new AdaptiveTrackSelection.Factory()); + } + + /** + * @deprecated Use {@link #DefaultTrackSelector(Context, TrackSelectionParameters, + * ExoTrackSelection.Factory)} + */ + @Deprecated + public DefaultTrackSelector( + TrackSelectionParameters parameters, ExoTrackSelection.Factory trackSelectionFactory) { + this(parameters, trackSelectionFactory, /* context= */ null); + } + + /** + * @param context Any {@link Context}. * @param parameters Initial {@link TrackSelectionParameters}. * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s. */ public DefaultTrackSelector( - TrackSelectionParameters parameters, ExoTrackSelection.Factory trackSelectionFactory) { + Context context, + TrackSelectionParameters parameters, + ExoTrackSelection.Factory trackSelectionFactory) { + this(parameters, trackSelectionFactory, context); + } + + /** + * Exists for backwards compatibility so that the deprecated constructor {@link + * #DefaultTrackSelector(TrackSelectionParameters, ExoTrackSelection.Factory)} can initialize + * {@code context} with {@code null} while we don't have a public constructor with a {@code + * Nullable context}. + * + * @param context Any {@link Context}. + * @param parameters Initial {@link TrackSelectionParameters}. + * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s. + */ + private DefaultTrackSelector( + TrackSelectionParameters parameters, + ExoTrackSelection.Factory trackSelectionFactory, + @Nullable Context context) { + this.lock = new Object(); + this.context = context != null ? context.getApplicationContext() : null; this.trackSelectionFactory = trackSelectionFactory; - parametersReference = - new AtomicReference<>( - parameters instanceof Parameters - ? (Parameters) parameters - : Parameters.DEFAULT_WITHOUT_CONTEXT.buildUpon().set(parameters).build()); + if (parameters instanceof Parameters) { + this.parameters = (Parameters) parameters; + } else { + Parameters defaultParameters = + context == null ? Parameters.DEFAULT_WITHOUT_CONTEXT : Parameters.getDefaults(context); + this.parameters = defaultParameters.buildUpon().set(parameters).build(); + } + this.audioAttributes = AudioAttributes.DEFAULT; + this.deviceIsTV = context != null && Util.isTv(context); + if (!deviceIsTV && context != null && Util.SDK_INT >= 32) { + spatializer = SpatializerWrapperV32.tryCreateInstance(context); + } + if (this.parameters.constrainAudioChannelCountToDeviceCapabilities && context == null) { + Log.w(TAG, AUDIO_CHANNEL_COUNT_CONSTRAINTS_WARN_MESSAGE); + } + } + + @Override + public void release() { + synchronized (lock) { + if (Util.SDK_INT >= 32 && spatializer != null) { + spatializer.release(); + } + } + super.release(); } @Override public Parameters getParameters() { - return parametersReference.get(); + synchronized (lock) { + return parameters; + } } @Override @@ -2068,11 +2213,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { setParametersInternal((Parameters) parameters); } // Only add the fields of `TrackSelectionParameters` to `parameters`. - Parameters mergedParameters = - new Parameters.Builder(parametersReference.get()).set(parameters).build(); + Parameters mergedParameters = new Parameters.Builder(getParameters()).set(parameters).build(); setParametersInternal(mergedParameters); } + @Override + public void setAudioAttributes(AudioAttributes audioAttributes) { + boolean audioAttributesChanged; + synchronized (lock) { + audioAttributesChanged = !this.audioAttributes.equals(audioAttributes); + this.audioAttributes = audioAttributes; + } + if (audioAttributesChanged) { + maybeInvalidateForAudioChannelCountConstraints(); + } + } + /** * @deprecated Use {@link #setParameters(Parameters.Builder)} instead. */ @@ -2103,7 +2259,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ private void setParametersInternal(Parameters parameters) { Assertions.checkNotNull(parameters); - if (!parametersReference.getAndSet(parameters).equals(parameters)) { + boolean parametersChanged; + synchronized (lock) { + parametersChanged = !this.parameters.equals(parameters); + this.parameters = parameters; + } + + if (parametersChanged) { + if (parameters.constrainAudioChannelCountToDeviceCapabilities && context == null) { + Log.w(TAG, AUDIO_CHANNEL_COUNT_CONSTRAINTS_WARN_MESSAGE); + } invalidate(); } } @@ -2119,22 +2284,33 @@ public class DefaultTrackSelector extends MappingTrackSelector { MediaPeriodId mediaPeriodId, Timeline timeline) throws ExoPlaybackException { - Parameters params = parametersReference.get(); + Parameters parameters; + synchronized (lock) { + parameters = this.parameters; + if (parameters.constrainAudioChannelCountToDeviceCapabilities + && Util.SDK_INT >= 32 + && spatializer != null) { + // Initialize the spatializer now so we can get a reference to the playback looper with + // Looper.myLooper(). + spatializer.ensureInitialized(this, checkStateNotNull(Looper.myLooper())); + } + } int rendererCount = mappedTrackInfo.getRendererCount(); ExoTrackSelection.@NullableType Definition[] definitions = selectAllTracks( mappedTrackInfo, rendererFormatSupports, rendererMixedMimeTypeAdaptationSupports, - params); + parameters); - applyTrackSelectionOverrides(mappedTrackInfo, params, definitions); - applyLegacyRendererOverrides(mappedTrackInfo, params, definitions); + applyTrackSelectionOverrides(mappedTrackInfo, parameters, definitions); + applyLegacyRendererOverrides(mappedTrackInfo, parameters, definitions); // Disable renderers if needed. for (int i = 0; i < rendererCount; i++) { @C.TrackType int rendererType = mappedTrackInfo.getRendererType(i); - if (params.getRendererDisabled(i) || params.disabledTrackTypes.contains(rendererType)) { + if (parameters.getRendererDisabled(i) + || parameters.disabledTrackTypes.contains(rendererType)) { definitions[i] = null; } } @@ -2151,7 +2327,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { for (int i = 0; i < rendererCount; i++) { @C.TrackType int rendererType = mappedTrackInfo.getRendererType(i); boolean forceRendererDisabled = - params.getRendererDisabled(i) || params.disabledTrackTypes.contains(rendererType); + parameters.getRendererDisabled(i) || parameters.disabledTrackTypes.contains(rendererType); boolean rendererEnabled = !forceRendererDisabled && (mappedTrackInfo.getRendererType(i) == C.TRACK_TYPE_NONE @@ -2160,7 +2336,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } // Configure audio and video renderers to use tunneling if appropriate. - if (params.tunnelingEnabled) { + if (parameters.tunnelingEnabled) { maybeConfigureRenderersForTunneling( mappedTrackInfo, rendererFormatSupports, rendererConfigurations, rendererTrackSelections); } @@ -2316,10 +2492,50 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererFormatSupports, (int rendererIndex, TrackGroup group, @Capabilities int[] support) -> AudioTrackInfo.createForTrackGroup( - rendererIndex, group, params, support, hasVideoRendererWithMappedTracksFinal), + rendererIndex, + group, + params, + support, + hasVideoRendererWithMappedTracksFinal, + this::isAudioFormatWithinAudioChannelCountConstraints), AudioTrackInfo::compareSelections); } + /** + * Returns whether an audio format is within the audio channel count constraints. + * + *

    This method returns {@code true} if one of the following holds: + * + *

      + *
    • Audio channel count constraints are not applicable (all formats are considered within + * constraints). + *
    • The device has a {@code + * television} UI mode. + *
    • {@code format} has up to 2 channels. + *
    • The device does not support audio spatialization and the format is {@linkplain + * #isDolbyAudio(Format) a Dolby one}. + *
    • Audio spatialization is applicable and {@code format} can be spatialized. + *
    + */ + private boolean isAudioFormatWithinAudioChannelCountConstraints(Format format) { + synchronized (lock) { + return !parameters.constrainAudioChannelCountToDeviceCapabilities + || deviceIsTV + || format.channelCount <= 2 + || (isDolbyAudio(format) + && (Util.SDK_INT < 32 + || spatializer == null + || !spatializer.isSpatializationSupported())) + || (Util.SDK_INT >= 32 + && spatializer != null + && spatializer.isSpatializationSupported() + && spatializer.isAvailable() + && spatializer.isEnabled() + && spatializer.canBeSpatialized(audioAttributes, format)); + } + } + // Text track selection implementation. /** @@ -2453,6 +2669,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { firstTrackInfo.rendererIndex); } + private void maybeInvalidateForAudioChannelCountConstraints() { + boolean shouldInvalidate; + synchronized (lock) { + shouldInvalidate = + parameters.constrainAudioChannelCountToDeviceCapabilities + && !deviceIsTV + && Util.SDK_INT >= 32 + && spatializer != null + && spatializer.isSpatializationSupported(); + } + if (shouldInvalidate) { + invalidate(); + } + } + // Utility methods. private static void applyTrackSelectionOverrides( @@ -2777,6 +3008,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } + private static boolean isDolbyAudio(Format format) { + if (format.sampleMimeType == null) { + return false; + } + switch (format.sampleMimeType) { + case MimeTypes.AUDIO_AC3: + case MimeTypes.AUDIO_E_AC3: + case MimeTypes.AUDIO_E_AC3_JOC: + case MimeTypes.AUDIO_AC4: + return true; + default: + return false; + } + } + /** Base class for track selection information of a {@link Format}. */ private abstract static class TrackInfo> { /** Factory for {@link TrackInfo} implementations for a given {@link TrackGroup}. */ @@ -3026,7 +3272,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup, Parameters params, @Capabilities int[] formatSupport, - boolean hasMappedVideoTracks) { + boolean hasMappedVideoTracks, + Predicate withinAudioChannelCountConstraints) { ImmutableList.Builder listBuilder = ImmutableList.builder(); for (int i = 0; i < trackGroup.length; i++) { listBuilder.add( @@ -3036,7 +3283,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { /* trackIndex= */ i, params, formatSupport[i], - hasMappedVideoTracks)); + hasMappedVideoTracks, + withinAudioChannelCountConstraints)); } return listBuilder.build(); } @@ -3066,7 +3314,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { int trackIndex, Parameters parameters, @Capabilities int formatSupport, - boolean hasMappedVideoTracks) { + boolean hasMappedVideoTracks, + Predicate withinAudioChannelCountConstraints) { super(rendererIndex, trackGroup, trackIndex); this.parameters = parameters; this.language = normalizeUndeterminedLanguageToNull(format.language); @@ -3098,7 +3347,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { isWithinConstraints = (format.bitrate == Format.NO_VALUE || format.bitrate <= parameters.maxAudioBitrate) && (format.channelCount == Format.NO_VALUE - || format.channelCount <= parameters.maxAudioChannelCount); + || format.channelCount <= parameters.maxAudioChannelCount) + && withinAudioChannelCountConstraints.apply(format); String[] localeLanguages = Util.getSystemLanguageCodes(); int bestLocaleMatchIndex = Integer.MAX_VALUE; int bestLocaleMatchScore = 0; @@ -3375,4 +3625,85 @@ public class DefaultTrackSelector extends MappingTrackSelector { .result(); } } + + /** + * Wraps the {@link Spatializer} in order to encapsulate its APIs within an inner class, to avoid + * runtime linking on devices with {@code API < 32}. + */ + @RequiresApi(32) + private static class SpatializerWrapperV32 { + + private final Spatializer spatializer; + private final boolean spatializationSupported; + + @Nullable private Handler handler; + @Nullable private Spatializer.OnSpatializerStateChangedListener listener; + + @Nullable + public static SpatializerWrapperV32 tryCreateInstance(Context context) { + @Nullable + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + return audioManager == null ? null : new SpatializerWrapperV32(audioManager.getSpatializer()); + } + + private SpatializerWrapperV32(Spatializer spatializer) { + this.spatializer = spatializer; + this.spatializationSupported = + spatializer.getImmersiveAudioLevel() != Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + } + + public void ensureInitialized(DefaultTrackSelector defaultTrackSelector, Looper looper) { + if (listener != null || handler != null) { + return; + } + this.listener = + new Spatializer.OnSpatializerStateChangedListener() { + @Override + public void onSpatializerEnabledChanged(Spatializer spatializer, boolean enabled) { + defaultTrackSelector.maybeInvalidateForAudioChannelCountConstraints(); + } + + @Override + public void onSpatializerAvailableChanged(Spatializer spatializer, boolean available) { + defaultTrackSelector.maybeInvalidateForAudioChannelCountConstraints(); + } + }; + this.handler = new Handler(looper); + spatializer.addOnSpatializerStateChangedListener(handler::post, listener); + } + + public boolean isSpatializationSupported() { + return spatializationSupported; + } + + public boolean isAvailable() { + return spatializer.isAvailable(); + } + + public boolean isEnabled() { + return spatializer.isEnabled(); + } + + public boolean canBeSpatialized(AudioAttributes audioAttributes, Format format) { + AudioFormat.Builder builder = + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setChannelMask(Util.getAudioTrackChannelConfig(format.channelCount)); + if (format.sampleRate != Format.NO_VALUE) { + builder.setSampleRate(format.sampleRate); + } + return spatializer.canBeSpatialized( + audioAttributes.getAudioAttributesV21().audioAttributes, builder.build()); + } + + public void release() { + if (listener == null || handler == null) { + return; + } + spatializer.removeOnSpatializerStateChangedListener(listener); + castNonNull(handler).removeCallbacksAndMessages(/* token= */ null); + handler = null; + listener = null; + } + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java index bfde8b19c5..f6ca0f3eee 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java @@ -17,7 +17,9 @@ package androidx.media3.exoplayer.trackselection; import static androidx.media3.common.util.Assertions.checkStateNotNull; +import androidx.annotation.CallSuper; import androidx.annotation.Nullable; +import androidx.media3.common.AudioAttributes; import androidx.media3.common.Player; import androidx.media3.common.Timeline; import androidx.media3.common.TrackSelectionParameters; @@ -112,7 +114,8 @@ public abstract class TrackSelector { * it has previously made are no longer valid. * @param bandwidthMeter A bandwidth meter which can be used by track selections to select tracks. */ - public final void init(InvalidationListener listener, BandwidthMeter bandwidthMeter) { + @CallSuper + public void init(InvalidationListener listener, BandwidthMeter bandwidthMeter) { this.listener = listener; this.bandwidthMeter = bandwidthMeter; } @@ -121,9 +124,10 @@ public abstract class TrackSelector { * Called by the player to release the selector. The selector cannot be used until {@link * #init(InvalidationListener, BandwidthMeter)} is called again. */ - public final void release() { - this.listener = null; - this.bandwidthMeter = null; + @CallSuper + public void release() { + listener = null; + bandwidthMeter = null; } /** @@ -178,6 +182,11 @@ public abstract class TrackSelector { return false; } + /** Called by the player to set the {@link AudioAttributes} that will be used for playback. */ + public void setAudioAttributes(AudioAttributes audioAttributes) { + // Default implementation is no-op. + } + /** * Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously * generated track selections. diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java index 60d69cdb99..a90b542769 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java @@ -31,9 +31,9 @@ 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.mockito.MockitoAnnotations.initMocks; import android.content.Context; +import android.media.Spatializer; import androidx.media3.common.Bundleable; import androidx.media3.common.C; import androidx.media3.common.Format; @@ -68,14 +68,19 @@ import java.util.Map; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** Unit tests for {@link DefaultTrackSelector}. */ @RunWith(AndroidJUnit4.class) public final class DefaultTrackSelectorTest { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + private static final RendererCapabilities ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); private static final RendererCapabilities ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = @@ -142,7 +147,6 @@ public final class DefaultTrackSelectorTest { @Before public void setUp() { - initMocks(this); when(bandwidthMeter.getBitrateEstimate()).thenReturn(1000000L); Context context = ApplicationProvider.getApplicationContext(); defaultParameters = Parameters.getDefaults(context); @@ -877,11 +881,18 @@ public final class DefaultTrackSelectorTest { * are the same, and tracks are within renderer's capabilities. */ @Test - public void selectTracksWithinCapabilitiesSelectHigherNumChannel() throws Exception { + public void + selectTracks_audioChannelCountConstraintsDisabledAndTracksWithinCapabilities_selectHigherNumChannel() + throws Exception { Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); Format higherChannelFormat = formatBuilder.setChannelCount(6).build(); Format lowerChannelFormat = formatBuilder.setChannelCount(2).build(); TrackGroupArray trackGroups = wrapFormats(higherChannelFormat, lowerChannelFormat); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setConstrainAudioChannelCountToDeviceCapabilities(false) + .build()); TrackSelectorResult result = trackSelector.selectTracks( @@ -957,11 +968,13 @@ public final class DefaultTrackSelectorTest { /** * Tests that track selector will prefer audio tracks with higher channel count over tracks with - * higher sample rate when other factors are the same, and tracks are within renderer's - * capabilities. + * higher sample rate when audio channel count constraints are disabled, other factors are the + * same, and tracks are within renderer's capabilities. */ @Test - public void selectTracksPreferHigherNumChannelBeforeSampleRate() throws Exception { + public void + selectTracks_audioChannelCountConstraintsDisabled_preferHigherNumChannelBeforeSampleRate() + throws Exception { Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); Format higherChannelLowerSampleRateFormat = formatBuilder.setChannelCount(6).setSampleRate(22050).build(); @@ -969,6 +982,11 @@ public final class DefaultTrackSelectorTest { formatBuilder.setChannelCount(2).setSampleRate(44100).build(); TrackGroupArray trackGroups = wrapFormats(higherChannelLowerSampleRateFormat, lowerChannelHigherSampleRateFormat); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setConstrainAudioChannelCountToDeviceCapabilities(false) + .build()); TrackSelectorResult result = trackSelector.selectTracks( @@ -1454,9 +1472,67 @@ public final class DefaultTrackSelectorTest { assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 0, 1); } + /** + * The following test is subject to the execution context. It currently runs on SDK 30 and the + * environment matches a handheld device ({@link Util#isTv(Context)} returns {@code false}) and + * there is no {@link android.media.Spatializer}. If the execution environment upgrades, the test + * may start failing depending on how the Robolectric Spatializer behaves. If the test starts + * failing, and Robolectric offers a shadow Spatializer whose behavior can be controlled, revise + * this test so that the Spatializer cannot spatialize the multichannel format. Also add tests + * where the Spatializer can spatialize multichannel formats and the track selector picks a + * multichannel format. + */ @Test - public void selectTracks_multipleAudioTracks_selectsAllTracksInBestConfigurationOnly() - throws Exception { + public void selectTracks_stereoAndMultichannelAACTracks_selectsStereo() + throws ExoPlaybackException { + Format stereoAudioFormat = AUDIO_FORMAT.buildUpon().setChannelCount(2).setId("0").build(); + Format multichannelAudioFormat = AUDIO_FORMAT.buildUpon().setChannelCount(6).setId("1").build(); + TrackGroupArray trackGroups = singleTrackGroup(stereoAudioFormat, multichannelAudioFormat); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); + + assertThat(result.length).isEqualTo(1); + assertThat(result.selections[0].getSelectedFormat()).isSameInstanceAs(stereoAudioFormat); + } + + /** + * The following test is subject to the execution context. It currently runs on SDK 30 and the + * environment matches a handheld device ({@link Util#isTv(Context)} returns {@code false}) and + * there is no {@link android.media.Spatializer}. If the execution environment upgrades, the test + * may start failing depending on how the Robolectric Spatializer behaves. If the test starts + * failing, and Robolectric offers a shadow Spatializer whose behavior can be controlled, revise + * this test so that the Spatializer does not support spatialization ({@link + * Spatializer#getImmersiveAudioLevel()} returns {@link + * Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_NONE}). + */ + @Test + public void + selectTracks_withAACStereoAndDolbyMultichannelTrackWithinCapabilities_selectsDolbyMultichannelTrack() + throws ExoPlaybackException { + Format stereoAudioFormat = AUDIO_FORMAT.buildUpon().setChannelCount(2).setId("0").build(); + Format multichannelAudioFormat = + AUDIO_FORMAT + .buildUpon() + .setSampleMimeType(MimeTypes.AUDIO_AC3) + .setChannelCount(6) + .setId("1") + .build(); + TrackGroupArray trackGroups = singleTrackGroup(stereoAudioFormat, multichannelAudioFormat); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); + + assertThat(result.length).isEqualTo(1); + assertThat(result.selections[0].getSelectedFormat()).isSameInstanceAs(multichannelAudioFormat); + } + + @Test + public void + selectTracks_audioChannelCountConstraintsDisabledAndMultipleAudioTracks_selectsAllTracksInBestConfigurationOnly() + throws Exception { TrackGroupArray trackGroups = singleTrackGroup( buildAudioFormatWithConfiguration( @@ -1476,6 +1552,10 @@ public final class DefaultTrackSelectorTest { /* channelCount= */ 6, MimeTypes.AUDIO_AAC, /* sampleRate= */ 44100)); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setConstrainAudioChannelCountToDeviceCapabilities(false)); TrackSelectorResult result = trackSelector.selectTracks( @@ -1568,10 +1648,17 @@ public final class DefaultTrackSelectorTest { } @Test - public void selectTracksWithMultipleAudioTracksWithMixedChannelCounts() throws Exception { + public void + selectTracks_audioChannelCountConstraintsDisabledAndMultipleAudioTracksWithMixedChannelCounts() + throws Exception { Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); Format stereoAudioFormat = formatBuilder.setChannelCount(2).build(); Format surroundAudioFormat = formatBuilder.setChannelCount(5).build(); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setConstrainAudioChannelCountToDeviceCapabilities(false) + .build()); // Should not adapt between different channel counts, so we expect a fixed selection containing // the track with more channels. @@ -1592,7 +1679,11 @@ public final class DefaultTrackSelectorTest { // If we constrain the channel count to 4 we expect a fixed selection containing the track with // fewer channels. - trackSelector.setParameters(defaultParameters.buildUpon().setMaxAudioChannelCount(4)); + trackSelector.setParameters( + defaultParameters + .buildUpon() + .setConstrainAudioChannelCountToDeviceCapabilities(false) + .setMaxAudioChannelCount(4)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1601,7 +1692,11 @@ public final class DefaultTrackSelectorTest { // If we constrain the channel count to 2 we expect a fixed selection containing the track with // fewer channels. - trackSelector.setParameters(defaultParameters.buildUpon().setMaxAudioChannelCount(2)); + trackSelector.setParameters( + defaultParameters + .buildUpon() + .setConstrainAudioChannelCountToDeviceCapabilities(false) + .setMaxAudioChannelCount(2)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1610,7 +1705,11 @@ public final class DefaultTrackSelectorTest { // If we constrain the channel count to 1 we expect a fixed selection containing the track with // fewer channels. - trackSelector.setParameters(defaultParameters.buildUpon().setMaxAudioChannelCount(1)); + trackSelector.setParameters( + defaultParameters + .buildUpon() + .setConstrainAudioChannelCountToDeviceCapabilities(false) + .setMaxAudioChannelCount(1)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1621,6 +1720,7 @@ public final class DefaultTrackSelectorTest { trackSelector.setParameters( defaultParameters .buildUpon() + .setConstrainAudioChannelCountToDeviceCapabilities(false) .setMaxAudioChannelCount(1) .setExceedAudioConstraintsIfNecessary(false)); result = @@ -2399,6 +2499,7 @@ public final class DefaultTrackSelectorTest { .setAllowAudioMixedChannelCountAdaptiveness(true) .setAllowAudioMixedDecoderSupportAdaptiveness(false) .setPreferredAudioMimeTypes(MimeTypes.AUDIO_AC3, MimeTypes.AUDIO_E_AC3) + .setConstrainAudioChannelCountToDeviceCapabilities(false) // Text .setPreferredTextLanguages("de", "en") .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION) From f6b987d8ec3084cb45ad23ee5bf13a7dfcacc040 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 10 Jun 2022 09:20:32 +0000 Subject: [PATCH 26/45] Ensure `DRAIN_ACTION_FLUSH_AND_UPDATE_DRM_SESSION` is always executed `codecDrainAction` is set to `DRAIN_ACTION_NONE` in 3 places in `MediaCodecRenderer`: * The constructor (so there's no prior state to worry about) * `updateDrmSessionV23()`: Where `mediaCrypto` is reconfigured based on `sourceDrmSession` and `codecDrmSession` is also updated to `sourceDrmSession`. * `resetCodecStateForFlush()`: Where (before this change) the action is unconditionally set back to `DRAIN_ACTION_NONE` and so any required updated implied by `DRAIN_ACTION_FLUSH_AND_UPDATE_DRM_SESSION` is not done. This change ensures that `flushOrReleaseCodec()` handles `DRAIN_ACTION_FLUSH_AND_UPDATE_DRM_SESSION` before calling . This probably also resolves Issue: google/ExoPlayer#10274 #minor-release PiperOrigin-RevId: 454114428 (cherry picked from commit 222faa96d063ba4a7f7e9fe8089228394bf1f97b) --- RELEASENOTES.md | 4 ++++ .../exoplayer/mediacodec/MediaCodecRenderer.java | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index aef2c6fd92..2dc28a2bb0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -69,6 +69,10 @@ for audio passthrough when the format audio channel count is unset, which occurs with HLS chunkless preparation ([10204](https://github.com/google/ExoPlayer/issues/10204)). +* DRM + * Ensure the DRM session is always correctly updated when seeking + immediately after a format change + ([10274](https://github.com/google/ExoPlayer/issues/10274)). * Ad playback / IMA: * Decrease ad polling rate from every 100ms to every 200ms, to line up with Media Rating Council (MRC) recommendations. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java index de0568de56..aac827d3a0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java @@ -856,6 +856,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer { releaseCodec(); return true; } + if (codecDrainAction == DRAIN_ACTION_FLUSH_AND_UPDATE_DRM_SESSION) { + checkState(Util.SDK_INT >= 23); // Implied by DRAIN_ACTION_FLUSH_AND_UPDATE_DRM_SESSION + // Needed to keep lint happy (it doesn't understand the checkState call alone) + if (Util.SDK_INT >= 23) { + try { + updateDrmSessionV23(); + } catch (ExoPlaybackException e) { + Log.w(TAG, "Failed to update the DRM session, releasing the codec instead.", e); + releaseCodec(); + return true; + } + } + } flushCodec(); return false; } From b8c8a413422d8bc729927cb96ba01dcdd37805d5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 13 Jun 2022 11:00:12 +0000 Subject: [PATCH 27/45] Use correct placeholder PlayerID value in test The default constructor is only allowed to be called on API < 32 and the test should use the defined UNSET constant to be API independent. #minor-release PiperOrigin-RevId: 454568893 (cherry picked from commit e8bcdf437ee3f8df9622615960f6b754536220bc) --- .../java/androidx/media3/exoplayer/MediaPeriodQueueTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java index 17ff2cfc19..155450ec7e 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java @@ -1286,7 +1286,7 @@ public final class MediaPeriodQueueTest { countDownLatch.countDown(); }, /* mediaTransferListener= */ null, - new PlayerId()); + PlayerId.UNSET); if (!countDownLatch.await(/* timeout= */ 2, SECONDS)) { fail(); } From 6377f9130df89127da606474baa250d9bdb3f617 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Tue, 14 Jun 2022 17:10:08 +0000 Subject: [PATCH 28/45] Merge pull request #10322 from DolbyLaboratories:dev-v2-multichannel PiperOrigin-RevId: 454641746 (cherry picked from commit 970eb4444c54f7bf899e429d845cf978b97dced7) --- RELEASENOTES.md | 4 ++++ .../src/main/java/androidx/media3/common/util/Util.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2dc28a2bb0..76ce7b051e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -69,6 +69,10 @@ for audio passthrough when the format audio channel count is unset, which occurs with HLS chunkless preparation ([10204](https://github.com/google/ExoPlayer/issues/10204)). + * Configure `AudioTrack` with channel mask + `AudioFormat.CHANNEL_OUT_7POINT1POINT4` if the decoder outputs 12 + channel PCM audio + ([#10322](#https://github.com/google/ExoPlayer/pull/10322). * DRM * Ensure the DRM session is always correctly updated when seeking immediately after a format change diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index f80525bdf6..7238fad0fe 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -1723,6 +1723,10 @@ public final class Util { // 8 ch output is not supported before Android L. return AudioFormat.CHANNEL_INVALID; } + case 12: + return Util.SDK_INT >= 32 + ? AudioFormat.CHANNEL_OUT_7POINT1POINT4 + : AudioFormat.CHANNEL_INVALID; default: return AudioFormat.CHANNEL_INVALID; } From 0d84cf9210d3212b35e55fa202deff57a9b15a25 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 14 Jun 2022 16:45:16 +0000 Subject: [PATCH 29/45] Spatializer: Assume linear channel count for E-AC3 JOC streams #minor-release PiperOrigin-RevId: 454884692 (cherry picked from commit 118d689a08b1097a346f543111ea65deead56459) --- .../exoplayer/trackselection/DefaultTrackSelector.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index fbd9f6e57d..e0020888e6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -3685,10 +3685,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { } public boolean canBeSpatialized(AudioAttributes audioAttributes, Format format) { + // For E-AC3 JOC, the format is object based. When the channel count is 16, this maps to 12 + // linear channels and the rest are used for objects. See + // https://github.com/google/ExoPlayer/pull/10322#discussion_r895265881 + int linearChannelCount = + MimeTypes.AUDIO_E_AC3_JOC.equals(format.sampleMimeType) && format.channelCount == 16 + ? 12 + : format.channelCount; AudioFormat.Builder builder = new AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_PCM_16BIT) - .setChannelMask(Util.getAudioTrackChannelConfig(format.channelCount)); + .setChannelMask(Util.getAudioTrackChannelConfig(linearChannelCount)); if (format.sampleRate != Format.NO_VALUE) { builder.setSampleRate(format.sampleRate); } From e3e92b244891420c2964a7ea209e66dcf94e202f Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 14 Jun 2022 20:33:41 +0000 Subject: [PATCH 30/45] Suppress lint errors `RestrictedApis` in lib-session PiperOrigin-RevId: 454943102 (cherry picked from commit 252ae4c7a34889b8622cc2750c238dc40be0277b) --- .../androidx/media3/session/MediaLibraryServiceLegacyStub.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java index f3d415d4a4..ad5912b796 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java @@ -24,6 +24,7 @@ import static androidx.media3.common.util.Util.postOrRun; import static androidx.media3.session.LibraryResult.RESULT_SUCCESS; import static androidx.media3.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES; +import android.annotation.SuppressLint; import android.os.BadParcelableException; import android.os.Bundle; import android.os.RemoteException; @@ -116,6 +117,7 @@ import java.util.concurrent.atomic.AtomicReference; // TODO(b/192455639): Optimize potential multiple calls of // MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded() with the same // content. + @SuppressLint("RestrictedApi") @Override public void onSubscribe(String id, Bundle option) { @Nullable ControllerInfo controller = getCurrentController(); @@ -141,6 +143,7 @@ import java.util.concurrent.atomic.AtomicReference; }); } + @SuppressLint("RestrictedApi") @Override public void onUnsubscribe(String id) { @Nullable ControllerInfo controller = getCurrentController(); From 676f766e394ae84ea22a4c85d5bd2c1f6de7d43f Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 14 Jun 2022 20:43:26 +0000 Subject: [PATCH 31/45] Make HttpDataSourceTestEnv require API 19 PiperOrigin-RevId: 454945333 (cherry picked from commit 7f89531c5b0c894946c2ca4bf54cfc66e3d4507b) --- .../java/androidx/media3/test/utils/HttpDataSourceTestEnv.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/HttpDataSourceTestEnv.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/HttpDataSourceTestEnv.java index dad7531277..48ae08e1f3 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/HttpDataSourceTestEnv.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/HttpDataSourceTestEnv.java @@ -19,6 +19,7 @@ package androidx.media3.test.utils; import static androidx.media3.test.utils.WebServerDispatcher.getRequestPath; import android.net.Uri; +import androidx.annotation.RequiresApi; import androidx.media3.common.util.UnstableApi; import androidx.media3.datasource.HttpDataSource; import com.google.common.collect.ImmutableList; @@ -31,6 +32,7 @@ import org.junit.Rule; import org.junit.rules.ExternalResource; /** A JUnit {@link Rule} that creates test resources for {@link HttpDataSource} contract tests. */ +@RequiresApi(19) @UnstableApi public class HttpDataSourceTestEnv extends ExternalResource { private static int seed = 0; From fde58ed91d9843b36e2e656c546d58ef3fc6e0bd Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 14 Jun 2022 20:59:55 +0000 Subject: [PATCH 32/45] Add `many` quantity for fr an fr-CA See https://issuetracker.google.com/208178382 PiperOrigin-RevId: 454949204 (cherry picked from commit 1f380c1dd36b4554286d0f4e93bc0479579eb991) --- libraries/ui/src/main/res/values-fr-rCA/strings.xml | 2 ++ libraries/ui/src/main/res/values-fr/strings.xml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/libraries/ui/src/main/res/values-fr-rCA/strings.xml b/libraries/ui/src/main/res/values-fr-rCA/strings.xml index f30d732a13..fbcc917ee7 100644 --- a/libraries/ui/src/main/res/values-fr-rCA/strings.xml +++ b/libraries/ui/src/main/res/values-fr-rCA/strings.xml @@ -31,11 +31,13 @@ Reculer de %d seconde Reculer de %d secondes + Reculer de %d secondes Avance rapide Avancer rapidement de %d seconde Avancer rapidement de %d secondes + Avancer rapidement de %d secondes Ne rien lire en boucle Lire une chanson en boucle diff --git a/libraries/ui/src/main/res/values-fr/strings.xml b/libraries/ui/src/main/res/values-fr/strings.xml index 6aaf078c29..5c50bbd517 100644 --- a/libraries/ui/src/main/res/values-fr/strings.xml +++ b/libraries/ui/src/main/res/values-fr/strings.xml @@ -31,11 +31,13 @@ Revenir en arrière de %d seconde Revenir en arrière de %d secondes + Revenir en arrière de %d secondes Avance rapide Avancer de %d seconde Avancer de %d secondes + Avancer de %d secondes Ne rien lire en boucle Lire un titre en boucle From d867ebd1dfa63edd7cab236c7d32ea32c9b81b2c Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 14 Jun 2022 21:10:10 +0000 Subject: [PATCH 33/45] Add lint base to make gradle lint run without errors PiperOrigin-RevId: 454951844 (cherry picked from commit 29bf4c8aabd9003e907d31563b9eead4658c1fce) --- libraries/common/build.gradle | 3 +++ libraries/common/lint-baseline.xml | 29 ++++++++++++++++++++++++++++ libraries/session/build.gradle | 3 +++ libraries/session/lint-baseline.xml | 30 +++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 libraries/common/lint-baseline.xml create mode 100644 libraries/session/lint-baseline.xml diff --git a/libraries/common/build.gradle b/libraries/common/build.gradle index d0123731b0..048fe60f41 100644 --- a/libraries/common/build.gradle +++ b/libraries/common/build.gradle @@ -30,6 +30,9 @@ android { testCoverageEnabled = true } } + lint { + baseline = file("lint-baseline.xml") + } } dependencies { diff --git a/libraries/common/lint-baseline.xml b/libraries/common/lint-baseline.xml new file mode 100644 index 0000000000..efedf9f2ec --- /dev/null +++ b/libraries/common/lint-baseline.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/libraries/session/build.gradle b/libraries/session/build.gradle index 7627d7d6a2..114a98bed6 100644 --- a/libraries/session/build.gradle +++ b/libraries/session/build.gradle @@ -26,6 +26,9 @@ android { } } sourceSets.androidTest.assets.srcDir '../test_data/src/test/assets/' + lint { + baseline = file("lint-baseline.xml") + } } dependencies { api project(modulePrefix + 'lib-common') diff --git a/libraries/session/lint-baseline.xml b/libraries/session/lint-baseline.xml new file mode 100644 index 0000000000..6dec23241b --- /dev/null +++ b/libraries/session/lint-baseline.xml @@ -0,0 +1,30 @@ + + + + + + + + + + From 080b1862c2f194cf628e010009e5f69bfd76c11d Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Wed, 15 Jun 2022 15:28:22 +0000 Subject: [PATCH 34/45] Merge pull request #9915 from dburckh:avi PiperOrigin-RevId: 455094147 (cherry picked from commit ad3348cc69d240ed3a1248496938fda57e20368e) --- RELEASENOTES.md | 2 + .../androidx/media3/common/FileTypes.java | 9 +- .../androidx/media3/common/MimeTypes.java | 4 + .../extractor/DefaultExtractorsFactory.java | 12 +- .../media3/extractor/avi/AviChunk.java | 27 + .../media3/extractor/avi/AviExtractor.java | 557 +++++++++ .../extractor/avi/AviMainHeaderChunk.java | 56 + .../extractor/avi/AviStreamHeaderChunk.java | 88 ++ .../media3/extractor/avi/ChunkReader.java | 212 ++++ .../media3/extractor/avi/ListChunk.java | 94 ++ .../extractor/avi/StreamFormatChunk.java | 150 +++ .../media3/extractor/avi/StreamNameChunk.java | 37 + .../media3/extractor/avi/package-info.java | 19 + .../DefaultExtractorsFactoryTest.java | 3 + .../extractor/avi/AviExtractorTest.java | 41 + .../extractordumps/avi/sample.avi.0.dump | 1091 +++++++++++++++++ .../extractordumps/avi/sample.avi.1.dump | 751 ++++++++++++ .../extractordumps/avi/sample.avi.2.dump | 487 ++++++++ .../extractordumps/avi/sample.avi.3.dump | 91 ++ .../avi/sample.avi.unknown_length.dump | 1091 +++++++++++++++++ .../src/test/assets/media/avi/sample.avi | Bin 0 -> 334534 bytes 21 files changed, 4819 insertions(+), 3 deletions(-) create mode 100644 libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviChunk.java create mode 100644 libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviExtractor.java create mode 100644 libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviMainHeaderChunk.java create mode 100644 libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviStreamHeaderChunk.java create mode 100644 libraries/extractor/src/main/java/androidx/media3/extractor/avi/ChunkReader.java create mode 100644 libraries/extractor/src/main/java/androidx/media3/extractor/avi/ListChunk.java create mode 100644 libraries/extractor/src/main/java/androidx/media3/extractor/avi/StreamFormatChunk.java create mode 100644 libraries/extractor/src/main/java/androidx/media3/extractor/avi/StreamNameChunk.java create mode 100644 libraries/extractor/src/main/java/androidx/media3/extractor/avi/package-info.java create mode 100644 libraries/extractor/src/test/java/androidx/media3/extractor/avi/AviExtractorTest.java create mode 100644 libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.0.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.1.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.2.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.3.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.unknown_length.dump create mode 100644 libraries/test_data/src/test/assets/media/avi/sample.avi diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 76ce7b051e..59e3b6653f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -95,6 +95,8 @@ * MP4: Parse bitrates from `esds` boxes. * Ogg: Allow duplicate Opus ID and comment headers ([#10038](https://github.com/google/ExoPlayer/issues/10038)). + * Add support for AVI + ([#2092](https://github.com/google/ExoPlayer/issues/2092)). * UI: * Fix delivery of events to `OnClickListener`s set on `PlayerView` and `LegacyPlayerView`, in the case that `useController=false` diff --git a/libraries/common/src/main/java/androidx/media3/common/FileTypes.java b/libraries/common/src/main/java/androidx/media3/common/FileTypes.java index a7ba44cdcc..c5ec04927b 100644 --- a/libraries/common/src/main/java/androidx/media3/common/FileTypes.java +++ b/libraries/common/src/main/java/androidx/media3/common/FileTypes.java @@ -44,7 +44,7 @@ public final class FileTypes { @Target(TYPE_USE) @IntDef({ UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG, - MIDI + MIDI, AVI }) public @interface Type {} /** Unknown file type. */ @@ -81,6 +81,8 @@ public final class FileTypes { public static final int JPEG = 14; /** File type for the MIDI format. */ public static final int MIDI = 15; + /** File type for the AVI format. */ + public static final int AVI = 16; @VisibleForTesting /* package */ static final String HEADER_CONTENT_TYPE = "Content-Type"; @@ -116,6 +118,7 @@ public final class FileTypes { private static final String EXTENSION_WEBVTT = ".webvtt"; private static final String EXTENSION_JPG = ".jpg"; private static final String EXTENSION_JPEG = ".jpeg"; + private static final String EXTENSION_AVI = ".avi"; private FileTypes() {} @@ -179,6 +182,8 @@ public final class FileTypes { return FileTypes.WEBVTT; case MimeTypes.IMAGE_JPEG: return FileTypes.JPEG; + case MimeTypes.VIDEO_AVI: + return FileTypes.AVI; default: return FileTypes.UNKNOWN; } @@ -244,6 +249,8 @@ public final class FileTypes { return FileTypes.WEBVTT; } else if (filename.endsWith(EXTENSION_JPG) || filename.endsWith(EXTENSION_JPEG)) { return FileTypes.JPEG; + } else if (filename.endsWith(EXTENSION_AVI)) { + return FileTypes.AVI; } else { return FileTypes.UNKNOWN; } diff --git a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java index 0ad8752a5a..28928ba15a 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java +++ b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java @@ -56,6 +56,10 @@ public final class MimeTypes { @UnstableApi public static final String VIDEO_FLV = BASE_TYPE_VIDEO + "/x-flv"; public static final String VIDEO_DOLBY_VISION = BASE_TYPE_VIDEO + "/dolby-vision"; public static final String VIDEO_OGG = BASE_TYPE_VIDEO + "/ogg"; + public static final String VIDEO_AVI = BASE_TYPE_VIDEO + "/x-msvideo"; + public static final String VIDEO_MJPEG = BASE_TYPE_VIDEO + "/mjpeg"; + public static final String VIDEO_MP42 = BASE_TYPE_VIDEO + "/mp42"; + public static final String VIDEO_MP43 = BASE_TYPE_VIDEO + "/mp43"; @UnstableApi public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; // audio/ MIME types diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java b/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java index 65aff22e3a..eff4e7c0fe 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/DefaultExtractorsFactory.java @@ -27,6 +27,7 @@ import androidx.media3.common.Player; import androidx.media3.common.util.TimestampAdjuster; import androidx.media3.common.util.UnstableApi; import androidx.media3.extractor.amr.AmrExtractor; +import androidx.media3.extractor.avi.AviExtractor; import androidx.media3.extractor.flac.FlacExtractor; import androidx.media3.extractor.flv.FlvExtractor; import androidx.media3.extractor.jpeg.JpegExtractor; @@ -103,8 +104,11 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { FileTypes.AC3, FileTypes.AC4, FileTypes.MP3, - FileTypes.JPEG, + // The following extractors are not part of the optimized ordering, and were appended + // without further analysis. + FileTypes.AVI, FileTypes.MIDI, + FileTypes.JPEG, }; private static final ExtensionLoader FLAC_EXTENSION_LOADER = @@ -309,7 +313,8 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @Override public synchronized Extractor[] createExtractors( Uri uri, Map> responseHeaders) { - List extractors = new ArrayList<>(/* initialCapacity= */ 14); + List extractors = + new ArrayList<>(/* initialCapacity= */ DEFAULT_EXTRACTOR_ORDER.length); @FileTypes.Type int responseHeadersInferredFileType = inferFileTypeFromResponseHeaders(responseHeaders); @@ -412,6 +417,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { extractors.add(midiExtractor); } break; + case FileTypes.AVI: + extractors.add(new AviExtractor()); + break; case FileTypes.WEBVTT: case FileTypes.UNKNOWN: default: diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviChunk.java b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviChunk.java new file mode 100644 index 0000000000..bd3b05eae9 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviChunk.java @@ -0,0 +1,27 @@ +/* + * Copyright 2022 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 androidx.media3.extractor.avi; + +/** + * A chunk, as defined in the AVI spec. + * + *

    See https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference. + */ +/* package */ interface AviChunk { + + /** Returns the chunk type fourcc. */ + int getType(); +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviExtractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviExtractor.java new file mode 100644 index 0000000000..21f1175c05 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviExtractor.java @@ -0,0 +1,557 @@ +/* + * Copyright 2022 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 androidx.media3.extractor.avi; + +import static java.lang.annotation.ElementType.TYPE_USE; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.ParserException; +import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.extractor.DummyExtractorOutput; +import androidx.media3.extractor.Extractor; +import androidx.media3.extractor.ExtractorInput; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.PositionHolder; +import androidx.media3.extractor.SeekMap; +import androidx.media3.extractor.TrackOutput; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Extracts data from the AVI container format. + * + *

    Spec: https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference. + */ +@UnstableApi +public final class AviExtractor implements Extractor { + + private static final String TAG = "AviExtractor"; + + public static final int FOURCC_RIFF = 0x46464952; + public static final int FOURCC_AVI_ = 0x20495641; // AVI + public static final int FOURCC_LIST = 0x5453494c; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int FOURCC_avih = 0x68697661; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int FOURCC_hdrl = 0x6c726468; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int FOURCC_strl = 0x6c727473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int FOURCC_movi = 0x69766f6d; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int FOURCC_idx1 = 0x31786469; + + public static final int FOURCC_JUNK = 0x4b4e554a; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int FOURCC_strf = 0x66727473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int FOURCC_strn = 0x6e727473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int FOURCC_strh = 0x68727473; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int FOURCC_auds = 0x73647561; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int FOURCC_txts = 0x73747874; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int FOURCC_vids = 0x73646976; + + /** Parser states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + STATE_SKIPPING_TO_HDRL, + STATE_READING_HDRL_HEADER, + STATE_READING_HDRL_BODY, + STATE_FINDING_MOVI_HEADER, + STATE_FINDING_IDX1_HEADER, + STATE_READING_IDX1_BODY, + STATE_READING_SAMPLES, + }) + private @interface State {} + + private static final int STATE_SKIPPING_TO_HDRL = 0; + private static final int STATE_READING_HDRL_HEADER = 1; + private static final int STATE_READING_HDRL_BODY = 2; + private static final int STATE_FINDING_MOVI_HEADER = 3; + private static final int STATE_FINDING_IDX1_HEADER = 4; + private static final int STATE_READING_IDX1_BODY = 5; + private static final int STATE_READING_SAMPLES = 6; + + private static final int AVIIF_KEYFRAME = 16; + + /** + * Maximum size to skip using {@link ExtractorInput#skip}. Boxes larger than this size are skipped + * using {@link #RESULT_SEEK}. + */ + private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024; + + private final ParsableByteArray scratch; + private final ChunkHeaderHolder chunkHeaderHolder; + + private @State int state; + private ExtractorOutput extractorOutput; + private @MonotonicNonNull AviMainHeaderChunk aviHeader; + private long durationUs; + private ChunkReader[] chunkReaders; + + private long pendingReposition; + @Nullable private ChunkReader currentChunkReader; + private int hdrlSize; + private long moviStart; + private long moviEnd; + private int idx1BodySize; + private boolean seekMapHasBeenOutput; + + public AviExtractor() { + scratch = new ParsableByteArray(/* limit= */ 12); + chunkHeaderHolder = new ChunkHeaderHolder(); + extractorOutput = new DummyExtractorOutput(); + chunkReaders = new ChunkReader[0]; + moviStart = C.POSITION_UNSET; + moviEnd = C.POSITION_UNSET; + hdrlSize = C.LENGTH_UNSET; + durationUs = C.TIME_UNSET; + } + + // Extractor implementation. + + @Override + public void init(ExtractorOutput output) { + this.state = STATE_SKIPPING_TO_HDRL; + this.extractorOutput = output; + pendingReposition = C.POSITION_UNSET; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException { + input.peekFully(scratch.getData(), /* offset= */ 0, /* length= */ 12); + scratch.setPosition(0); + if (scratch.readLittleEndianInt() != FOURCC_RIFF) { + return false; + } + scratch.skipBytes(4); // Skip the RIFF chunk length. + return scratch.readLittleEndianInt() == FOURCC_AVI_; + } + + @Override + public int read(ExtractorInput input, PositionHolder positionHolder) throws IOException { + if (resolvePendingReposition(input, positionHolder)) { + return RESULT_SEEK; + } + switch (state) { + case STATE_SKIPPING_TO_HDRL: + // Check for RIFF and AVI fourcc's just in case the caller did not sniff, in order to + // provide a meaningful error if the input is not an AVI file. + if (sniff(input)) { + input.skipFully(/* length= */ 12); + } else { + throw ParserException.createForMalformedContainer( + /* message= */ "AVI Header List not found", /* cause= */ null); + } + state = STATE_READING_HDRL_HEADER; + return RESULT_CONTINUE; + case STATE_READING_HDRL_HEADER: + input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 12); + scratch.setPosition(0); + chunkHeaderHolder.populateWithListHeaderFrom(scratch); + if (chunkHeaderHolder.listType != FOURCC_hdrl) { + throw ParserException.createForMalformedContainer( + /* message= */ "hdrl expected, found: " + chunkHeaderHolder.listType, + /* cause= */ null); + } + hdrlSize = chunkHeaderHolder.size; + state = STATE_READING_HDRL_BODY; + return RESULT_CONTINUE; + case STATE_READING_HDRL_BODY: + // hdrlSize includes the LIST type (hdrl), so we subtract 4 to the size. + int bytesToRead = hdrlSize - 4; + ParsableByteArray hdrlBody = new ParsableByteArray(bytesToRead); + input.readFully(hdrlBody.getData(), /* offset= */ 0, bytesToRead); + parseHdrlBody(hdrlBody); + state = STATE_FINDING_MOVI_HEADER; + return RESULT_CONTINUE; + case STATE_FINDING_MOVI_HEADER: + if (moviStart != C.POSITION_UNSET && input.getPosition() != moviStart) { + pendingReposition = moviStart; + return RESULT_CONTINUE; + } + input.peekFully(scratch.getData(), /* offset= */ 0, /* length= */ 12); + input.resetPeekPosition(); + scratch.setPosition(0); + chunkHeaderHolder.populateFrom(scratch); + int listType = scratch.readLittleEndianInt(); + if (chunkHeaderHolder.chunkType == FOURCC_RIFF) { + // We are at the start of the file. The movi chunk is in the RIFF chunk, so we skip the + // header, so as to read the RIFF chunk's body. + input.skipFully(12); + return RESULT_CONTINUE; + } + if (chunkHeaderHolder.chunkType != FOURCC_LIST || listType != FOURCC_movi) { + // The chunk header (8 bytes) plus the whole body. + pendingReposition = input.getPosition() + chunkHeaderHolder.size + 8; + return RESULT_CONTINUE; + } + moviStart = input.getPosition(); + // Size includes the list type, but not the LIST or size fields, so we add 8. + moviEnd = moviStart + chunkHeaderHolder.size + 8; + if (!seekMapHasBeenOutput) { + if (Assertions.checkNotNull(aviHeader).hasIndex()) { + state = STATE_FINDING_IDX1_HEADER; + pendingReposition = moviEnd; + return RESULT_CONTINUE; + } else { + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + seekMapHasBeenOutput = true; + } + } + // No need to parse the idx1, so we start reading the samples from the movi chunk straight + // away. We skip 12 bytes to move to the start of the movi's body. + pendingReposition = input.getPosition() + 12; + state = STATE_READING_SAMPLES; + return RESULT_CONTINUE; + case STATE_FINDING_IDX1_HEADER: + input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 8); + scratch.setPosition(0); + int idx1Fourcc = scratch.readLittleEndianInt(); + int boxSize = scratch.readLittleEndianInt(); + if (idx1Fourcc == FOURCC_idx1) { + state = STATE_READING_IDX1_BODY; + idx1BodySize = boxSize; + } else { + // This one is not idx1, skip to the next box. + pendingReposition = input.getPosition() + boxSize; + } + return RESULT_CONTINUE; + case STATE_READING_IDX1_BODY: + ParsableByteArray idx1Body = new ParsableByteArray(idx1BodySize); + input.readFully(idx1Body.getData(), /* offset= */ 0, /* length= */ idx1BodySize); + parseIdx1Body(idx1Body); + state = STATE_READING_SAMPLES; + pendingReposition = moviStart; + return RESULT_CONTINUE; + case STATE_READING_SAMPLES: + return readMoviChunks(input); + default: + throw new AssertionError(); // Should never happen. + } + } + + @Override + public void seek(long position, long timeUs) { + pendingReposition = C.POSITION_UNSET; + currentChunkReader = null; + for (ChunkReader chunkReader : chunkReaders) { + chunkReader.seekToPosition(position); + } + if (position == 0) { + if (chunkReaders.length == 0) { + // Still unprepared. + state = STATE_SKIPPING_TO_HDRL; + } else { + state = STATE_FINDING_MOVI_HEADER; + } + return; + } + state = STATE_READING_SAMPLES; + } + + @Override + public void release() { + // Nothing to release. + } + + // Internal methods. + + /** + * Returns whether a {@link #RESULT_SEEK} is required for the pending reposition. A seek may not + * be necessary when the desired position (as held by {@link #pendingReposition}) is after the + * {@link ExtractorInput#getPosition() current position}, but not further than {@link + * #RELOAD_MINIMUM_SEEK_DISTANCE}. + */ + private boolean resolvePendingReposition(ExtractorInput input, PositionHolder positionHolder) + throws IOException { + boolean needSeek = false; + if (pendingReposition != C.POSITION_UNSET) { + long currentPosition = input.getPosition(); + if (pendingReposition < currentPosition + || pendingReposition > currentPosition + RELOAD_MINIMUM_SEEK_DISTANCE) { + positionHolder.position = pendingReposition; + needSeek = true; + } else { + // The distance to the target position is short enough that it makes sense to just skip the + // bytes, instead of doing a seek which might re-create an HTTP connection. + input.skipFully((int) (pendingReposition - currentPosition)); + } + } + pendingReposition = C.POSITION_UNSET; + return needSeek; + } + + private void parseHdrlBody(ParsableByteArray hrdlBody) throws IOException { + ListChunk headerList = ListChunk.parseFrom(FOURCC_hdrl, hrdlBody); + if (headerList.getType() != FOURCC_hdrl) { + throw ParserException.createForMalformedContainer( + /* message= */ "Unexpected header list type " + headerList.getType(), /* cause= */ null); + } + @Nullable AviMainHeaderChunk aviHeader = headerList.getChild(AviMainHeaderChunk.class); + if (aviHeader == null) { + throw ParserException.createForMalformedContainer( + /* message= */ "AviHeader not found", /* cause= */ null); + } + this.aviHeader = aviHeader; + // This is usually wrong, so it will be overwritten by video if present + durationUs = aviHeader.totalFrames * (long) aviHeader.frameDurationUs; + ArrayList chunkReaderList = new ArrayList<>(); + int streamId = 0; + for (AviChunk aviChunk : headerList.children) { + if (aviChunk.getType() == FOURCC_strl) { + ListChunk streamList = (ListChunk) aviChunk; + // Note the streamId needs to increment even if the corresponding `strl` is discarded. + // See + // https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference#avi-stream-headers. + @Nullable ChunkReader chunkReader = processStreamList(streamList, streamId++); + if (chunkReader != null) { + chunkReaderList.add(chunkReader); + } + } + } + chunkReaders = chunkReaderList.toArray(new ChunkReader[0]); + extractorOutput.endTracks(); + } + + /** Builds and outputs the {@link SeekMap} from the idx1 chunk. */ + private void parseIdx1Body(ParsableByteArray body) { + long seekOffset = peekSeekOffset(body); + while (body.bytesLeft() >= 16) { + int chunkId = body.readLittleEndianInt(); + int flags = body.readLittleEndianInt(); + long offset = body.readLittleEndianInt() + seekOffset; + body.readLittleEndianInt(); // We ignore the size. + ChunkReader chunkReader = getChunkReader(chunkId); + if (chunkReader == null) { + // We ignore unknown chunk IDs. + continue; + } + if ((flags & AVIIF_KEYFRAME) == AVIIF_KEYFRAME) { + chunkReader.appendKeyFrameToIndex(offset); + } + chunkReader.incrementIndexChunkCount(); + } + for (ChunkReader chunkReader : chunkReaders) { + chunkReader.compactIndex(); + } + seekMapHasBeenOutput = true; + extractorOutput.seekMap(new AviSeekMap(durationUs)); + } + + private long peekSeekOffset(ParsableByteArray idx1Body) { + // The spec states the offset is based on the start of the movi list type fourcc, but it also + // says some files base the offset on the start of the file. We use a best effort approach to + // figure out which is the case. See: + // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/api/Aviriff/ns-aviriff-avioldindex#dwoffset. + if (idx1Body.bytesLeft() < 16) { + // There are no full entries in the index, meaning we don't need to apply an offset. + return 0; + } + int startingPosition = idx1Body.getPosition(); + idx1Body.skipBytes(8); // Skip chunkId (4 bytes) and flags (4 bytes). + int offset = idx1Body.readLittleEndianInt(); + + // moviStart poitns at the start of the LIST, while the seek offset is based at the start of the + // movi fourCC, so we add 8 to reconcile the difference. + long seekOffset = offset > moviStart ? 0L : moviStart + 8; + idx1Body.setPosition(startingPosition); + return seekOffset; + } + + @Nullable + private ChunkReader getChunkReader(int chunkId) { + for (ChunkReader chunkReader : chunkReaders) { + if (chunkReader.handlesChunkId(chunkId)) { + return chunkReader; + } + } + return null; + } + + private int readMoviChunks(ExtractorInput input) throws IOException { + if (input.getPosition() >= moviEnd) { + return C.RESULT_END_OF_INPUT; + } else if (currentChunkReader != null) { + if (currentChunkReader.onChunkData(input)) { + currentChunkReader = null; + } + } else { + alignInputToEvenPosition(input); + input.peekFully(scratch.getData(), /* offset= */ 0, 12); + scratch.setPosition(0); + int chunkType = scratch.readLittleEndianInt(); + if (chunkType == FOURCC_LIST) { + scratch.setPosition(8); + int listType = scratch.readLittleEndianInt(); + input.skipFully(listType == FOURCC_movi ? 12 : 8); + input.resetPeekPosition(); + return RESULT_CONTINUE; + } + int size = scratch.readLittleEndianInt(); + if (chunkType == FOURCC_JUNK) { + pendingReposition = input.getPosition() + size + 8; + return RESULT_CONTINUE; + } + input.skipFully(8); + input.resetPeekPosition(); + ChunkReader chunkReader = getChunkReader(chunkType); + if (chunkReader == null) { + // No handler for this chunk. We skip it. + pendingReposition = input.getPosition() + size; + return RESULT_CONTINUE; + } else { + chunkReader.onChunkStart(size); + this.currentChunkReader = chunkReader; + } + } + return RESULT_CONTINUE; + } + + @Nullable + private ChunkReader processStreamList(ListChunk streamList, int streamId) { + AviStreamHeaderChunk aviStreamHeaderChunk = streamList.getChild(AviStreamHeaderChunk.class); + StreamFormatChunk streamFormatChunk = streamList.getChild(StreamFormatChunk.class); + if (aviStreamHeaderChunk == null) { + Log.w(TAG, "Missing Stream Header"); + return null; + } + if (streamFormatChunk == null) { + Log.w(TAG, "Missing Stream Format"); + return null; + } + long durationUs = aviStreamHeaderChunk.getDurationUs(); + Format streamFormat = streamFormatChunk.format; + Format.Builder builder = streamFormat.buildUpon(); + builder.setId(streamId); + int suggestedBufferSize = aviStreamHeaderChunk.suggestedBufferSize; + if (suggestedBufferSize != 0) { + builder.setMaxInputSize(suggestedBufferSize); + } + StreamNameChunk streamName = streamList.getChild(StreamNameChunk.class); + if (streamName != null) { + builder.setLabel(streamName.name); + } + int trackType = MimeTypes.getTrackType(streamFormat.sampleMimeType); + if (trackType == C.TRACK_TYPE_AUDIO || trackType == C.TRACK_TYPE_VIDEO) { + TrackOutput trackOutput = extractorOutput.track(streamId, trackType); + trackOutput.format(builder.build()); + ChunkReader chunkReader = + new ChunkReader( + streamId, trackType, durationUs, aviStreamHeaderChunk.length, trackOutput); + this.durationUs = durationUs; + return chunkReader; + } else { + // We don't currently support tracks other than video and audio. + return null; + } + } + + /** + * Skips one byte from the given {@code input} if the current position is odd. + * + *

    This isn't documented anywhere, but AVI files are aligned to even bytes and fill gaps with + * zeros. + */ + private static void alignInputToEvenPosition(ExtractorInput input) throws IOException { + if ((input.getPosition() & 1) == 1) { + input.skipFully(1); + } + } + + // Internal classes. + + private class AviSeekMap implements SeekMap { + + private final long durationUs; + + public AviSeekMap(long durationUs) { + this.durationUs = durationUs; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + SeekPoints result = chunkReaders[0].getSeekPoints(timeUs); + for (int i = 1; i < chunkReaders.length; i++) { + SeekPoints seekPoints = chunkReaders[i].getSeekPoints(timeUs); + if (seekPoints.first.position < result.first.position) { + result = seekPoints; + } + } + return result; + } + } + + private static class ChunkHeaderHolder { + public int chunkType; + public int size; + public int listType; + + public void populateWithListHeaderFrom(ParsableByteArray headerBytes) throws ParserException { + populateFrom(headerBytes); + if (chunkType != AviExtractor.FOURCC_LIST) { + throw ParserException.createForMalformedContainer( + /* message= */ "LIST expected, found: " + chunkType, /* cause= */ null); + } + listType = headerBytes.readLittleEndianInt(); + } + + public void populateFrom(ParsableByteArray headerBytes) { + chunkType = headerBytes.readLittleEndianInt(); + size = headerBytes.readLittleEndianInt(); + listType = 0; + } + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviMainHeaderChunk.java b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviMainHeaderChunk.java new file mode 100644 index 0000000000..4a6c11bc4c --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviMainHeaderChunk.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 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 androidx.media3.extractor.avi; + +import androidx.media3.common.util.ParsableByteArray; + +/** Wrapper around the AVIMAINHEADER structure */ +/* package */ final class AviMainHeaderChunk implements AviChunk { + + private static final int AVIF_HAS_INDEX = 0x10; + + public static AviMainHeaderChunk parseFrom(ParsableByteArray body) { + int microSecPerFrame = body.readLittleEndianInt(); + body.skipBytes(8); // Skip dwMaxBytesPerSec (4 bytes), dwPaddingGranularity (4 bytes). + int flags = body.readLittleEndianInt(); + int totalFrames = body.readLittleEndianInt(); + body.skipBytes(4); // dwInitialFrames (4 bytes). + int streams = body.readLittleEndianInt(); + body.skipBytes(12); // dwSuggestedBufferSize (4 bytes), dwWidth (4 bytes), dwHeight (4 bytes). + return new AviMainHeaderChunk(microSecPerFrame, flags, totalFrames, streams); + } + + public final int frameDurationUs; + public final int flags; + public final int totalFrames; + public final int streams; + + private AviMainHeaderChunk(int frameDurationUs, int flags, int totalFrames, int streams) { + this.frameDurationUs = frameDurationUs; + this.flags = flags; + this.totalFrames = totalFrames; + this.streams = streams; + } + + @Override + public int getType() { + return AviExtractor.FOURCC_avih; + } + + public boolean hasIndex() { + return (flags & AVIF_HAS_INDEX) == AVIF_HAS_INDEX; + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviStreamHeaderChunk.java b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviStreamHeaderChunk.java new file mode 100644 index 0000000000..99f83e8824 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/AviStreamHeaderChunk.java @@ -0,0 +1,88 @@ +/* + * Copyright 2022 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 androidx.media3.extractor.avi; + +import androidx.media3.common.C; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.Util; + +/** Parses and holds information from the AVISTREAMHEADER structure. */ +/* package */ final class AviStreamHeaderChunk implements AviChunk { + private static final String TAG = "AviStreamHeaderChunk"; + + public static AviStreamHeaderChunk parseFrom(ParsableByteArray body) { + int streamType = body.readLittleEndianInt(); + body.skipBytes(12); // fccHandler (4 bytes), dwFlags (4 bytes), wPriority (2 bytes), + // wLanguage (2 bytes). + int initialFrames = body.readLittleEndianInt(); + int scale = body.readLittleEndianInt(); + int rate = body.readLittleEndianInt(); + body.skipBytes(4); // dwStart (4 bytes). + int length = body.readLittleEndianInt(); + int suggestedBufferSize = body.readLittleEndianInt(); + body.skipBytes(8); // dwQuality (4 bytes), dwSampleSize (4 bytes). + return new AviStreamHeaderChunk( + streamType, initialFrames, scale, rate, length, suggestedBufferSize); + } + + public final int streamType; + public final int initialFrames; + public final int scale; + public final int rate; + public final int length; + public final int suggestedBufferSize; + + private AviStreamHeaderChunk( + int streamType, int initialFrames, int scale, int rate, int length, int suggestedBufferSize) { + this.streamType = streamType; + this.initialFrames = initialFrames; + this.scale = scale; + this.rate = rate; + this.length = length; + this.suggestedBufferSize = suggestedBufferSize; + } + + @Override + public int getType() { + return AviExtractor.FOURCC_strh; + } + + public @C.TrackType int getTrackType() { + switch (streamType) { + case AviExtractor.FOURCC_auds: + return C.TRACK_TYPE_AUDIO; + case AviExtractor.FOURCC_vids: + return C.TRACK_TYPE_VIDEO; + case AviExtractor.FOURCC_txts: + return C.TRACK_TYPE_TEXT; + default: + Log.w(TAG, "Found unsupported streamType fourCC: " + Integer.toHexString(streamType)); + return C.TRACK_TYPE_UNKNOWN; + } + } + + public float getFrameRate() { + return rate / (float) scale; + } + + public long getDurationUs() { + return Util.scaleLargeTimestamp( + /* timestamp= */ length, + /* multiplier= */ C.MICROS_PER_SECOND * scale, + /* divisor= */ rate); + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/avi/ChunkReader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/ChunkReader.java new file mode 100644 index 0000000000..4f788429ce --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/ChunkReader.java @@ -0,0 +1,212 @@ +/* + * Copyright 2022 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 androidx.media3.extractor.avi; + +import static java.lang.annotation.ElementType.TYPE_USE; + +import androidx.annotation.IntDef; +import androidx.media3.common.C; +import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.Util; +import androidx.media3.extractor.ExtractorInput; +import androidx.media3.extractor.SeekMap; +import androidx.media3.extractor.SeekPoint; +import androidx.media3.extractor.TrackOutput; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; + +/** Reads chunks holding sample data. */ +/* package */ final class ChunkReader { + + /** Parser states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + CHUNK_TYPE_VIDEO_COMPRESSED, + CHUNK_TYPE_VIDEO_UNCOMPRESSED, + CHUNK_TYPE_AUDIO, + }) + private @interface ChunkType {} + + private static final int INITIAL_INDEX_SIZE = 512; + private static final int CHUNK_TYPE_VIDEO_COMPRESSED = ('d' << 16) | ('c' << 24); + private static final int CHUNK_TYPE_VIDEO_UNCOMPRESSED = ('d' << 16) | ('b' << 24); + private static final int CHUNK_TYPE_AUDIO = ('w' << 16) | ('b' << 24); + + protected final TrackOutput trackOutput; + + /** The chunk id fourCC (example: `01wb`), as defined in the index and the movi. */ + private final int chunkId; + /** Secondary chunk id. Bad muxers sometimes use an uncompressed video id (db) for key frames */ + private final int alternativeChunkId; + + private final long durationUs; + private final int streamHeaderChunkCount; + + private int currentChunkSize; + private int bytesRemainingInCurrentChunk; + + /** Number of chunks as calculated by the index */ + private int currentChunkIndex; + + private int indexChunkCount; + private int indexSize; + private long[] keyFrameOffsets; + private int[] keyFrameIndices; + + public ChunkReader( + int id, + @C.TrackType int trackType, + long durationnUs, + int streamHeaderChunkCount, + TrackOutput trackOutput) { + Assertions.checkArgument(trackType == C.TRACK_TYPE_AUDIO || trackType == C.TRACK_TYPE_VIDEO); + this.durationUs = durationnUs; + this.streamHeaderChunkCount = streamHeaderChunkCount; + this.trackOutput = trackOutput; + @ChunkType + int chunkType = + trackType == C.TRACK_TYPE_VIDEO ? CHUNK_TYPE_VIDEO_COMPRESSED : CHUNK_TYPE_AUDIO; + chunkId = getChunkIdFourCc(id, chunkType); + alternativeChunkId = + trackType == C.TRACK_TYPE_VIDEO ? getChunkIdFourCc(id, CHUNK_TYPE_VIDEO_UNCOMPRESSED) : -1; + keyFrameOffsets = new long[INITIAL_INDEX_SIZE]; + keyFrameIndices = new int[INITIAL_INDEX_SIZE]; + } + + public void appendKeyFrameToIndex(long offset) { + if (indexSize == keyFrameIndices.length) { + keyFrameOffsets = Arrays.copyOf(keyFrameOffsets, keyFrameOffsets.length * 3 / 2); + keyFrameIndices = Arrays.copyOf(keyFrameIndices, keyFrameIndices.length * 3 / 2); + } + keyFrameOffsets[indexSize] = offset; + keyFrameIndices[indexSize] = indexChunkCount; + indexSize++; + } + + public void advanceCurrentChunk() { + currentChunkIndex++; + } + + public long getCurrentChunkTimestampUs() { + return getChunkTimestampUs(currentChunkIndex); + } + + public long getFrameDurationUs() { + return getChunkTimestampUs(/* chunkIndex= */ 1); + } + + public void incrementIndexChunkCount() { + indexChunkCount++; + } + + public void compactIndex() { + keyFrameOffsets = Arrays.copyOf(keyFrameOffsets, indexSize); + keyFrameIndices = Arrays.copyOf(keyFrameIndices, indexSize); + } + + public boolean handlesChunkId(int chunkId) { + return this.chunkId == chunkId || alternativeChunkId == chunkId; + } + + public boolean isCurrentFrameAKeyFrame() { + return Arrays.binarySearch(keyFrameIndices, currentChunkIndex) >= 0; + } + + public boolean isVideo() { + return (chunkId & CHUNK_TYPE_VIDEO_COMPRESSED) == CHUNK_TYPE_VIDEO_COMPRESSED; + } + + public boolean isAudio() { + return (chunkId & CHUNK_TYPE_AUDIO) == CHUNK_TYPE_AUDIO; + } + + /** Prepares for parsing a chunk with the given {@code size}. */ + public void onChunkStart(int size) { + currentChunkSize = size; + bytesRemainingInCurrentChunk = size; + } + + /** + * Provides data associated to the current chunk and returns whether the full chunk has been + * parsed. + */ + public boolean onChunkData(ExtractorInput input) throws IOException { + bytesRemainingInCurrentChunk -= + trackOutput.sampleData(input, bytesRemainingInCurrentChunk, false); + boolean done = bytesRemainingInCurrentChunk == 0; + if (done) { + if (currentChunkSize > 0) { + trackOutput.sampleMetadata( + getCurrentChunkTimestampUs(), + (isCurrentFrameAKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), + currentChunkSize, + 0, + null); + } + advanceCurrentChunk(); + } + return done; + } + + public void seekToPosition(long position) { + if (indexSize == 0) { + currentChunkIndex = 0; + } else { + int index = + Util.binarySearchFloor( + keyFrameOffsets, position, /* inclusive= */ true, /* stayInBounds= */ true); + currentChunkIndex = keyFrameIndices[index]; + } + } + + public SeekMap.SeekPoints getSeekPoints(long timeUs) { + int targetFrameIndex = (int) (timeUs / getFrameDurationUs()); + int keyFrameIndex = + Util.binarySearchFloor( + keyFrameIndices, targetFrameIndex, /* inclusive= */ true, /* stayInBounds= */ true); + if (keyFrameIndices[keyFrameIndex] == targetFrameIndex) { + return new SeekMap.SeekPoints(getSeekPoint(keyFrameIndex)); + } + // The target frame is not a key frame, we look for the two closest ones. + SeekPoint precedingKeyFrameSeekPoint = getSeekPoint(keyFrameIndex); + if (keyFrameIndex + 1 < keyFrameOffsets.length) { + return new SeekMap.SeekPoints(precedingKeyFrameSeekPoint, getSeekPoint(keyFrameIndex + 1)); + } else { + return new SeekMap.SeekPoints(precedingKeyFrameSeekPoint); + } + } + + private long getChunkTimestampUs(int chunkIndex) { + return durationUs * chunkIndex / streamHeaderChunkCount; + } + + private SeekPoint getSeekPoint(int keyFrameIndex) { + return new SeekPoint( + keyFrameIndices[keyFrameIndex] * getFrameDurationUs(), keyFrameOffsets[keyFrameIndex]); + } + + private static int getChunkIdFourCc(int streamId, @ChunkType int chunkType) { + int tens = streamId / 10; + int ones = streamId % 10; + return (('0' + ones) << 8) | ('0' + tens) | chunkType; + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/avi/ListChunk.java b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/ListChunk.java new file mode 100644 index 0000000000..68e6cbf6f9 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/ListChunk.java @@ -0,0 +1,94 @@ +/* + * Copyright 2022 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 androidx.media3.extractor.avi; + +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.util.ParsableByteArray; +import com.google.common.collect.ImmutableList; + +/** Represents an AVI LIST. */ +/* package */ final class ListChunk implements AviChunk { + + public static ListChunk parseFrom(int listType, ParsableByteArray body) { + ImmutableList.Builder builder = new ImmutableList.Builder<>(); + int listBodyEndPosition = body.limit(); + @C.TrackType int currentTrackType = C.TRACK_TYPE_NONE; + while (body.bytesLeft() > 8) { + int type = body.readLittleEndianInt(); + int size = body.readLittleEndianInt(); + int innerBoxBodyEndPosition = body.getPosition() + size; + body.setLimit(innerBoxBodyEndPosition); + @Nullable AviChunk aviChunk; + if (type == AviExtractor.FOURCC_LIST) { + int innerListType = body.readLittleEndianInt(); + aviChunk = parseFrom(innerListType, body); + } else { + aviChunk = createBox(type, currentTrackType, body); + } + if (aviChunk != null) { + if (aviChunk.getType() == AviExtractor.FOURCC_strh) { + currentTrackType = ((AviStreamHeaderChunk) aviChunk).getTrackType(); + } + builder.add(aviChunk); + } + body.setPosition(innerBoxBodyEndPosition); + body.setLimit(listBodyEndPosition); + } + return new ListChunk(listType, builder.build()); + } + + public final ImmutableList children; + private final int type; + + private ListChunk(int type, ImmutableList children) { + this.type = type; + this.children = children; + } + + @Override + public int getType() { + return type; + } + + @Nullable + @SuppressWarnings("unchecked") + public T getChild(Class c) { + for (AviChunk aviChunk : children) { + if (aviChunk.getClass() == c) { + return (T) aviChunk; + } + } + return null; + } + + @Nullable + private static AviChunk createBox( + int chunkType, @C.TrackType int trackType, ParsableByteArray body) { + switch (chunkType) { + case AviExtractor.FOURCC_avih: + return AviMainHeaderChunk.parseFrom(body); + case AviExtractor.FOURCC_strh: + return AviStreamHeaderChunk.parseFrom(body); + case AviExtractor.FOURCC_strf: + return StreamFormatChunk.parseFrom(trackType, body); + case AviExtractor.FOURCC_strn: + return StreamNameChunk.parseFrom(body); + default: + return null; + } + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/avi/StreamFormatChunk.java b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/StreamFormatChunk.java new file mode 100644 index 0000000000..7ecdcd2816 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/StreamFormatChunk.java @@ -0,0 +1,150 @@ +/* + * Copyright 2022 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 androidx.media3.extractor.avi; + +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.Util; +import com.google.common.collect.ImmutableList; + +/** Holds the {@link Format} information contained in an STRF chunk. */ +/* package */ final class StreamFormatChunk implements AviChunk { + private static final String TAG = "StreamFormatChunk"; + + @Nullable + public static AviChunk parseFrom(int trackType, ParsableByteArray body) { + if (trackType == C.TRACK_TYPE_VIDEO) { + return parseBitmapInfoHeader(body); + } else if (trackType == C.TRACK_TYPE_AUDIO) { + return parseWaveFormatEx(body); + } else { + Log.w( + TAG, + "Ignoring strf box for unsupported track type: " + Util.getTrackTypeString(trackType)); + return null; + } + } + + public final Format format; + + public StreamFormatChunk(Format format) { + this.format = format; + } + + @Override + public int getType() { + return AviExtractor.FOURCC_strf; + } + + @Nullable + private static AviChunk parseBitmapInfoHeader(ParsableByteArray body) { + body.skipBytes(4); // biSize. + int width = body.readLittleEndianInt(); + int height = body.readLittleEndianInt(); + body.skipBytes(4); // biPlanes (2 bytes), biBitCount (2 bytes). + int compression = body.readLittleEndianInt(); + String mimeType = getMimeTypeFromCompression(compression); + if (mimeType == null) { + Log.w(TAG, "Ignoring track with unsupported compression " + compression); + return null; + } + Format.Builder formatBuilder = new Format.Builder(); + formatBuilder.setWidth(width).setHeight(height).setSampleMimeType(mimeType); + return new StreamFormatChunk(formatBuilder.build()); + } + + // Syntax defined by the WAVEFORMATEX structure. See + // https://docs.microsoft.com/en-us/previous-versions/dd757713(v=vs.85). + @Nullable + private static AviChunk parseWaveFormatEx(ParsableByteArray body) { + int formatTag = body.readLittleEndianUnsignedShort(); + @Nullable String mimeType = getMimeTypeFromTag(formatTag); + if (mimeType == null) { + Log.w(TAG, "Ignoring track with unsupported format tag " + formatTag); + return null; + } + int channelCount = body.readLittleEndianUnsignedShort(); + int samplesPerSecond = body.readLittleEndianInt(); + body.skipBytes(6); // averageBytesPerSecond (4 bytes), nBlockAlign (2 bytes). + int bitsPerSample = body.readUnsignedShort(); + int pcmEncoding = Util.getPcmEncoding(bitsPerSample); + int cbSize = body.readLittleEndianUnsignedShort(); + byte[] codecData = new byte[cbSize]; + body.readBytes(codecData, /* offset= */ 0, codecData.length); + + Format.Builder formatBuilder = new Format.Builder(); + formatBuilder + .setSampleMimeType(mimeType) + .setChannelCount(channelCount) + .setSampleRate(samplesPerSecond); + if (MimeTypes.AUDIO_RAW.equals(mimeType) && pcmEncoding != C.ENCODING_INVALID) { + formatBuilder.setPcmEncoding(pcmEncoding); + } + if (MimeTypes.AUDIO_AAC.equals(mimeType) && codecData.length > 0) { + formatBuilder.setInitializationData(ImmutableList.of(codecData)); + } + return new StreamFormatChunk(formatBuilder.build()); + } + + @Nullable + private static String getMimeTypeFromTag(int tag) { + switch (tag) { + case 0x1: // WAVE_FORMAT_PCM + return MimeTypes.AUDIO_RAW; + case 0x55: // WAVE_FORMAT_MPEGLAYER3 + return MimeTypes.AUDIO_MPEG; + case 0xff: // WAVE_FORMAT_AAC + return MimeTypes.AUDIO_AAC; + case 0x2000: // WAVE_FORMAT_DVM - AC3 + return MimeTypes.AUDIO_AC3; + case 0x2001: // WAVE_FORMAT_DTS2 + return MimeTypes.AUDIO_DTS; + default: + return null; + } + } + + @Nullable + private static String getMimeTypeFromCompression(int compression) { + switch (compression) { + case 0x3234504d: // MP42 + return MimeTypes.VIDEO_MP42; + case 0x3334504d: // MP43 + return MimeTypes.VIDEO_MP43; + case 0x34363248: // H264 + case 0x31637661: // avc1 + case 0x31435641: // AVC1 + return MimeTypes.VIDEO_H264; + case 0x44495633: // 3VID + case 0x78766964: // divx + case 0x58564944: // DIVX + case 0x30355844: // DX50 + case 0x34504d46: // FMP4 + case 0x64697678: // xvid + case 0x44495658: // XVID + return MimeTypes.VIDEO_MP4V; + case 0x47504a4d: // MJPG + case 0x67706a6d: // mjpg + return MimeTypes.VIDEO_MJPEG; + default: + return null; + } + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/avi/StreamNameChunk.java b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/StreamNameChunk.java new file mode 100644 index 0000000000..8d7a9eb5be --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/StreamNameChunk.java @@ -0,0 +1,37 @@ +/* + * Copyright 2022 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 androidx.media3.extractor.avi; + +import androidx.media3.common.util.ParsableByteArray; + +/** Parses and contains the name from the STRN chunk. */ +/* package */ final class StreamNameChunk implements AviChunk { + + public static StreamNameChunk parseFrom(ParsableByteArray body) { + return new StreamNameChunk(body.readString(body.bytesLeft())); + } + + public final String name; + + private StreamNameChunk(String name) { + this.name = name; + } + + @Override + public int getType() { + return AviExtractor.FOURCC_strn; + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/avi/package-info.java b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/package-info.java new file mode 100644 index 0000000000..41b6f5a4ed --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/avi/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2022 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 androidx.media3.extractor.avi; + +import androidx.media3.common.util.NonNullApi; diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/DefaultExtractorsFactoryTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/DefaultExtractorsFactoryTest.java index 5c6394482d..0654d5aa6b 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/DefaultExtractorsFactoryTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/DefaultExtractorsFactoryTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.media3.common.MimeTypes; import androidx.media3.extractor.amr.AmrExtractor; +import androidx.media3.extractor.avi.AviExtractor; import androidx.media3.extractor.flac.FlacExtractor; import androidx.media3.extractor.flv.FlvExtractor; import androidx.media3.extractor.jpeg.JpegExtractor; @@ -70,6 +71,7 @@ public final class DefaultExtractorsFactoryTest { Ac3Extractor.class, Ac4Extractor.class, Mp3Extractor.class, + AviExtractor.class, JpegExtractor.class) .inOrder(); } @@ -112,6 +114,7 @@ public final class DefaultExtractorsFactoryTest { AdtsExtractor.class, Ac3Extractor.class, Ac4Extractor.class, + AviExtractor.class, JpegExtractor.class) .inOrder(); } diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/avi/AviExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/avi/AviExtractorTest.java new file mode 100644 index 0000000000..ab479240e0 --- /dev/null +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/avi/AviExtractorTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 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 androidx.media3.extractor.avi; + +import androidx.media3.test.utils.ExtractorAsserts; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; + +/** Tests for {@link AviExtractor}. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public final class AviExtractorTest { + + @Parameters(name = "{0}") + public static ImmutableList params() { + return ExtractorAsserts.configs(); + } + + @Parameter public ExtractorAsserts.SimulationConfig simulationConfig; + + @Test + public void aviSample() throws Exception { + ExtractorAsserts.assertBehavior(AviExtractor::new, "media/avi/sample.avi", simulationConfig); + } +} diff --git a/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.0.dump b/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.0.dump new file mode 100644 index 0000000000..666526a13d --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.0.dump @@ -0,0 +1,1091 @@ +seekMap: + isSeekable = true + duration = 4080000 + getPosition(0) = [[timeUs=0, position=9996]] + getPosition(1) = [[timeUs=0, position=9996]] + getPosition(2040000) = [[timeUs=1835152, position=160708], [timeUs=2335648, position=198850]] + getPosition(4080000) = [[timeUs=3837136, position=308434]] +numberOfTracks = 2 +track 0: + total output bytes = 252777 + sample count = 96 + format 0: + id = 0 + sampleMimeType = video/mp4v-es + maxInputSize = 6761 + width = 960 + height = 400 + sample 0: + time = 0 + flags = 1 + data = length 4279, hash C8074EBB + sample 1: + time = 125124 + flags = 0 + data = length 268, hash 14B72252 + sample 2: + time = 166833 + flags = 0 + data = length 268, hash ED2AEF52 + sample 3: + time = 208541 + flags = 0 + data = length 268, hash DD9B546B + sample 4: + time = 250249 + flags = 0 + data = length 268, hash B60F216B + sample 5: + time = 291958 + flags = 0 + data = length 268, hash 89782584 + sample 6: + time = 333666 + flags = 0 + data = length 268, hash 61EBF284 + sample 7: + time = 375374 + flags = 0 + data = length 268, hash B4D111DE + sample 8: + time = 417083 + flags = 0 + data = length 268, hash 8D44DEDE + sample 9: + time = 458791 + flags = 0 + data = length 268, hash 60ADE2F7 + sample 10: + time = 500499 + flags = 1 + data = length 4656, hash F1F35C82 + sample 11: + time = 542208 + flags = 1 + data = length 5119, hash 3CFA0CA2 + sample 12: + time = 583916 + flags = 1 + data = length 5466, hash 7E1F6F63 + sample 13: + time = 625624 + flags = 1 + data = length 5990, hash 70A2D835 + sample 14: + time = 667333 + flags = 1 + data = length 6476, hash E633D374 + sample 15: + time = 709041 + flags = 1 + data = length 6761, hash 922BC7A6 + sample 16: + time = 750749 + flags = 1 + data = length 6501, hash B03632B9 + sample 17: + time = 792458 + flags = 1 + data = length 5824, hash 89BCFDCC + sample 18: + time = 834166 + flags = 1 + data = length 5816, hash 4B321EB2 + sample 19: + time = 875874 + flags = 0 + data = length 5307, hash EF15AF2D + sample 20: + time = 917583 + flags = 0 + data = length 2791, hash B48241CD + sample 21: + time = 959291 + flags = 0 + data = length 2505, hash FB9EE72B + sample 22: + time = 1000999 + flags = 0 + data = length 1747, hash 89DC0982 + sample 23: + time = 1042708 + flags = 0 + data = length 1948, hash B8642019 + sample 24: + time = 1084416 + flags = 0 + data = length 2134, hash E6115E1C + sample 25: + time = 1126124 + flags = 0 + data = length 2035, hash 86FD9E1E + sample 26: + time = 1167833 + flags = 0 + data = length 2109, hash D66E00D + sample 27: + time = 1209541 + flags = 0 + data = length 2427, hash 63E16CB5 + sample 28: + time = 1251249 + flags = 0 + data = length 2485, hash 38F83F6D + sample 29: + time = 1292958 + flags = 0 + data = length 2458, hash 48900F9D + sample 30: + time = 1334666 + flags = 1 + data = length 5891, hash 4627CBC3 + sample 31: + time = 1376374 + flags = 0 + data = length 3154, hash B7484F2C + sample 32: + time = 1418083 + flags = 0 + data = length 2409, hash 93E50DB6 + sample 33: + time = 1459791 + flags = 0 + data = length 2296, hash 73A46768 + sample 34: + time = 1501499 + flags = 0 + data = length 2514, hash F71DCA93 + sample 35: + time = 1543208 + flags = 0 + data = length 2614, hash BDD6744E + sample 36: + time = 1584916 + flags = 0 + data = length 2797, hash 81BED431 + sample 37: + time = 1626624 + flags = 0 + data = length 1549, hash D892E824 + sample 38: + time = 1668333 + flags = 0 + data = length 2714, hash B3EE7E2A + sample 39: + time = 1710041 + flags = 0 + data = length 2002, hash BC9E16ED + sample 40: + time = 1751749 + flags = 0 + data = length 2726, hash C31D5A82 + sample 41: + time = 1793458 + flags = 0 + data = length 2639, hash AE67DC59 + sample 42: + time = 1835166 + flags = 1 + data = length 5011, hash 630ADA59 + sample 43: + time = 1876874 + flags = 0 + data = length 4356, hash 76CE0D21 + sample 44: + time = 1918583 + flags = 0 + data = length 1986, hash AC41A7FC + sample 45: + time = 1960291 + flags = 0 + data = length 2792, hash 497D3A2D + sample 46: + time = 2001999 + flags = 0 + data = length 2176, hash FADAC8ED + sample 47: + time = 2043708 + flags = 0 + data = length 2463, hash 379DE4C8 + sample 48: + time = 2085416 + flags = 0 + data = length 2472, hash 9E68BAC5 + sample 49: + time = 2127124 + flags = 0 + data = length 1960, hash 38BC3EFC + sample 50: + time = 2168832 + flags = 0 + data = length 1833, hash 139C885B + sample 51: + time = 2210541 + flags = 0 + data = length 1865, hash A14BE838 + sample 52: + time = 2252249 + flags = 0 + data = length 1491, hash 8EC33935 + sample 53: + time = 2293957 + flags = 0 + data = length 1403, hash 78D87F2C + sample 54: + time = 2335666 + flags = 1 + data = length 4936, hash C34CC2D0 + sample 55: + time = 2377374 + flags = 0 + data = length 2539, hash D0EDEC2B + sample 56: + time = 2419082 + flags = 0 + data = length 3052, hash 3F68900F + sample 57: + time = 2460791 + flags = 0 + data = length 2998, hash B531AC4 + sample 58: + time = 2502499 + flags = 0 + data = length 1670, hash 734A2739 + sample 59: + time = 2544207 + flags = 0 + data = length 1634, hash 60A39EA5 + sample 60: + time = 2585916 + flags = 0 + data = length 1623, hash B18B39FE + sample 61: + time = 2627624 + flags = 0 + data = length 806, hash DA70C12B + sample 62: + time = 2669332 + flags = 0 + data = length 990, hash A1642D2C + sample 63: + time = 2711041 + flags = 0 + data = length 903, hash 411ECEA3 + sample 64: + time = 2752749 + flags = 0 + data = length 713, hash A4DAFA22 + sample 65: + time = 2794457 + flags = 0 + data = length 749, hash F39941EF + sample 66: + time = 2836166 + flags = 1 + data = length 5258, hash 19670F6D + sample 67: + time = 2877874 + flags = 0 + data = length 1932, hash 3F7F6D21 + sample 68: + time = 2919582 + flags = 0 + data = length 731, hash 45EF5D68 + sample 69: + time = 2961291 + flags = 0 + data = length 1076, hash 8C23B3FF + sample 70: + time = 3002999 + flags = 0 + data = length 1560, hash D6133304 + sample 71: + time = 3044707 + flags = 0 + data = length 2564, hash B7B256B + sample 72: + time = 3086416 + flags = 0 + data = length 2789, hash 97736B63 + sample 73: + time = 3128124 + flags = 0 + data = length 2469, hash C65A89B6 + sample 74: + time = 3169832 + flags = 0 + data = length 2203, hash D89686B4 + sample 75: + time = 3211541 + flags = 0 + data = length 2097, hash 91358D88 + sample 76: + time = 3253249 + flags = 0 + data = length 2043, hash 50547CF1 + sample 77: + time = 3294957 + flags = 0 + data = length 2198, hash F93F1606 + sample 78: + time = 3336666 + flags = 1 + data = length 5084, hash BEC89380 + sample 79: + time = 3378374 + flags = 0 + data = length 3043, hash F3C50E5A + sample 80: + time = 3420082 + flags = 0 + data = length 2786, hash 49C8C67C + sample 81: + time = 3461791 + flags = 0 + data = length 2652, hash D0A93BE7 + sample 82: + time = 3503499 + flags = 0 + data = length 2675, hash 81F7F5BD + sample 83: + time = 3545207 + flags = 0 + data = length 2916, hash E2A38AE1 + sample 84: + time = 3586916 + flags = 0 + data = length 2574, hash 50EC13BD + sample 85: + time = 3628624 + flags = 0 + data = length 2644, hash 3DF461F4 + sample 86: + time = 3670332 + flags = 0 + data = length 2932, hash E2F2DAB0 + sample 87: + time = 3712041 + flags = 0 + data = length 2625, hash 100D69E1 + sample 88: + time = 3753749 + flags = 0 + data = length 2773, hash 347DCC1F + sample 89: + time = 3795457 + flags = 0 + data = length 2348, hash 51FC01A3 + sample 90: + time = 3837166 + flags = 1 + data = length 5356, hash 190A3CAE + sample 91: + time = 3878874 + flags = 0 + data = length 3172, hash 538FA2AE + sample 92: + time = 3920582 + flags = 0 + data = length 2393, hash 525B26D6 + sample 93: + time = 3962291 + flags = 0 + data = length 2307, hash C894745F + sample 94: + time = 4003999 + flags = 0 + data = length 2490, hash 800FED70 + sample 95: + time = 4045707 + flags = 0 + data = length 2115, hash A2512D3 +track 1: + total output bytes = 65280 + sample count = 170 + format 0: + id = 1 + sampleMimeType = audio/mpeg + maxInputSize = 384 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 384, hash 4FA643C4 + sample 1: + time = 24000 + flags = 1 + data = length 384, hash 5ED84707 + sample 2: + time = 48000 + flags = 1 + data = length 384, hash 5ED84707 + sample 3: + time = 72000 + flags = 1 + data = length 384, hash 5ED84707 + sample 4: + time = 96000 + flags = 1 + data = length 384, hash 5ED84707 + sample 5: + time = 120000 + flags = 1 + data = length 384, hash 5ED84707 + sample 6: + time = 144000 + flags = 1 + data = length 384, hash 5ED84707 + sample 7: + time = 168000 + flags = 1 + data = length 384, hash 5ED84707 + sample 8: + time = 192000 + flags = 1 + data = length 384, hash 5ED84707 + sample 9: + time = 216000 + flags = 1 + data = length 384, hash 5ED84707 + sample 10: + time = 240000 + flags = 1 + data = length 384, hash 5ED84707 + sample 11: + time = 264000 + flags = 1 + data = length 384, hash 5ED84707 + sample 12: + time = 288000 + flags = 1 + data = length 384, hash 5ED84707 + sample 13: + time = 312000 + flags = 1 + data = length 384, hash 5ED84707 + sample 14: + time = 336000 + flags = 1 + data = length 384, hash 5ED84707 + sample 15: + time = 360000 + flags = 1 + data = length 384, hash 5ED84707 + sample 16: + time = 384000 + flags = 1 + data = length 384, hash D77E2886 + sample 17: + time = 408000 + flags = 1 + data = length 384, hash 5C372185 + sample 18: + time = 432000 + flags = 1 + data = length 384, hash F9589AE7 + sample 19: + time = 456000 + flags = 1 + data = length 384, hash F14EBCAC + sample 20: + time = 480000 + flags = 1 + data = length 384, hash 2B688404 + sample 21: + time = 504000 + flags = 1 + data = length 384, hash E889FC6B + sample 22: + time = 528000 + flags = 1 + data = length 384, hash 53CBDEC0 + sample 23: + time = 552000 + flags = 1 + data = length 384, hash 91769951 + sample 24: + time = 576000 + flags = 1 + data = length 384, hash 749935FF + sample 25: + time = 600000 + flags = 1 + data = length 384, hash 2B794BC6 + sample 26: + time = 624000 + flags = 1 + data = length 384, hash B6A1870B + sample 27: + time = 648000 + flags = 1 + data = length 384, hash 7D729EEC + sample 28: + time = 672000 + flags = 1 + data = length 384, hash AFBD0EF5 + sample 29: + time = 696000 + flags = 1 + data = length 384, hash C1DDC412 + sample 30: + time = 720000 + flags = 1 + data = length 384, hash CF1807A4 + sample 31: + time = 744000 + flags = 1 + data = length 384, hash CD1E8F85 + sample 32: + time = 768000 + flags = 1 + data = length 384, hash FF56854C + sample 33: + time = 792000 + flags = 1 + data = length 384, hash F6F8D897 + sample 34: + time = 816000 + flags = 1 + data = length 384, hash 9C3F1566 + sample 35: + time = 840000 + flags = 1 + data = length 384, hash C5D788D1 + sample 36: + time = 864000 + flags = 1 + data = length 384, hash 81FC222A + sample 37: + time = 888000 + flags = 1 + data = length 384, hash 749C0516 + sample 38: + time = 912000 + flags = 1 + data = length 384, hash 63C232FF + sample 39: + time = 936000 + flags = 1 + data = length 384, hash FB4FABBB + sample 40: + time = 960000 + flags = 1 + data = length 384, hash B787C813 + sample 41: + time = 984000 + flags = 1 + data = length 384, hash E18B955C + sample 42: + time = 1008000 + flags = 1 + data = length 384, hash 2085B856 + sample 43: + time = 1032000 + flags = 1 + data = length 384, hash BDF70D7C + sample 44: + time = 1056000 + flags = 1 + data = length 384, hash 47838243 + sample 45: + time = 1080000 + flags = 1 + data = length 384, hash 5CF6CC33 + sample 46: + time = 1104000 + flags = 1 + data = length 384, hash 2A979CF6 + sample 47: + time = 1128000 + flags = 1 + data = length 384, hash 26D5CF5A + sample 48: + time = 1152000 + flags = 1 + data = length 384, hash E1BFEE5D + sample 49: + time = 1176000 + flags = 1 + data = length 384, hash A4DF110B + sample 50: + time = 1200000 + flags = 1 + data = length 384, hash 8595335A + sample 51: + time = 1224000 + flags = 1 + data = length 384, hash 5CA30C8 + sample 52: + time = 1248000 + flags = 1 + data = length 384, hash 1219C18C + sample 53: + time = 1272000 + flags = 1 + data = length 384, hash 41DC2F24 + sample 54: + time = 1296000 + flags = 1 + data = length 384, hash 664A60E1 + sample 55: + time = 1320000 + flags = 1 + data = length 384, hash 4338D4A1 + sample 56: + time = 1344000 + flags = 1 + data = length 384, hash C65E6D68 + sample 57: + time = 1368000 + flags = 1 + data = length 384, hash AE2762E8 + sample 58: + time = 1392000 + flags = 1 + data = length 384, hash 8CFEAA7F + sample 59: + time = 1416000 + flags = 1 + data = length 384, hash A96A80B4 + sample 60: + time = 1440000 + flags = 1 + data = length 384, hash 69A84538 + sample 61: + time = 1464000 + flags = 1 + data = length 384, hash 9131F77E + sample 62: + time = 1488000 + flags = 1 + data = length 384, hash 818091B1 + sample 63: + time = 1512000 + flags = 1 + data = length 384, hash 6DBECC10 + sample 64: + time = 1536000 + flags = 1 + data = length 384, hash A9912967 + sample 65: + time = 1560000 + flags = 1 + data = length 384, hash 4DA97472 + sample 66: + time = 1584000 + flags = 1 + data = length 384, hash 31B363DC + sample 67: + time = 1608000 + flags = 1 + data = length 384, hash E15BB36C + sample 68: + time = 1632000 + flags = 1 + data = length 384, hash 159C963C + sample 69: + time = 1656000 + flags = 1 + data = length 384, hash 50B874D + sample 70: + time = 1680000 + flags = 1 + data = length 384, hash 8727F339 + sample 71: + time = 1704000 + flags = 1 + data = length 384, hash C0853B6 + sample 72: + time = 1728000 + flags = 1 + data = length 384, hash 9E026376 + sample 73: + time = 1752000 + flags = 1 + data = length 384, hash C190380F + sample 74: + time = 1776000 + flags = 1 + data = length 384, hash 20925F33 + sample 75: + time = 1800000 + flags = 1 + data = length 384, hash 9F740DAF + sample 76: + time = 1824000 + flags = 1 + data = length 384, hash F75757D6 + sample 77: + time = 1848000 + flags = 1 + data = length 384, hash 46D76ED0 + sample 78: + time = 1872000 + flags = 1 + data = length 384, hash 11DC480F + sample 79: + time = 1896000 + flags = 1 + data = length 384, hash 3428D6D8 + sample 80: + time = 1920000 + flags = 1 + data = length 384, hash 16A11668 + sample 81: + time = 1944000 + flags = 1 + data = length 384, hash 4CFBA63C + sample 82: + time = 1968000 + flags = 1 + data = length 384, hash 2B6702A9 + sample 83: + time = 1992000 + flags = 1 + data = length 384, hash D047CEF9 + sample 84: + time = 2016000 + flags = 1 + data = length 384, hash 25F05663 + sample 85: + time = 2040000 + flags = 1 + data = length 384, hash 947441C7 + sample 86: + time = 2064000 + flags = 1 + data = length 384, hash E82145F7 + sample 87: + time = 2088000 + flags = 1 + data = length 384, hash 6C40F859 + sample 88: + time = 2112000 + flags = 1 + data = length 384, hash 273FBEF8 + sample 89: + time = 2136000 + flags = 1 + data = length 384, hash 2FF062B6 + sample 90: + time = 2160000 + flags = 1 + data = length 384, hash 73FF8D58 + sample 91: + time = 2184000 + flags = 1 + data = length 384, hash F2BAB943 + sample 92: + time = 2208000 + flags = 1 + data = length 384, hash 507DEF9F + sample 93: + time = 2232000 + flags = 1 + data = length 384, hash 913E927A + sample 94: + time = 2256000 + flags = 1 + data = length 384, hash AFFD0AED + sample 95: + time = 2280000 + flags = 1 + data = length 384, hash EE0C6F4C + sample 96: + time = 2304000 + flags = 1 + data = length 384, hash 70726632 + sample 97: + time = 2328000 + flags = 1 + data = length 384, hash B5D49F8 + sample 98: + time = 2352000 + flags = 1 + data = length 384, hash B341AF3F + sample 99: + time = 2376000 + flags = 1 + data = length 384, hash 6AC1D8C4 + sample 100: + time = 2400000 + flags = 1 + data = length 384, hash BC666685 + sample 101: + time = 2424000 + flags = 1 + data = length 384, hash E58054E8 + sample 102: + time = 2448000 + flags = 1 + data = length 384, hash 404AB403 + sample 103: + time = 2472000 + flags = 1 + data = length 384, hash 265A86B8 + sample 104: + time = 2496000 + flags = 1 + data = length 384, hash 306316F6 + sample 105: + time = 2520000 + flags = 1 + data = length 384, hash 7BFDEA60 + sample 106: + time = 2544000 + flags = 1 + data = length 384, hash 2EFF8E5B + sample 107: + time = 2568000 + flags = 1 + data = length 384, hash C06CE84C + sample 108: + time = 2592000 + flags = 1 + data = length 384, hash 9069A01E + sample 109: + time = 2616000 + flags = 1 + data = length 384, hash 4A78F181 + sample 110: + time = 2640000 + flags = 1 + data = length 384, hash 57FD4BE7 + sample 111: + time = 2664000 + flags = 1 + data = length 384, hash B09DB688 + sample 112: + time = 2688000 + flags = 1 + data = length 384, hash 5602C52F + sample 113: + time = 2712000 + flags = 1 + data = length 384, hash 77762F5D + sample 114: + time = 2736000 + flags = 1 + data = length 384, hash 6A0BDB6 + sample 115: + time = 2760000 + flags = 1 + data = length 384, hash 2428C91 + sample 116: + time = 2784000 + flags = 1 + data = length 384, hash DEB54354 + sample 117: + time = 2808000 + flags = 1 + data = length 384, hash FB0B7BEE + sample 118: + time = 2832000 + flags = 1 + data = length 384, hash BDD82F68 + sample 119: + time = 2856000 + flags = 1 + data = length 384, hash BAB3B808 + sample 120: + time = 2880000 + flags = 1 + data = length 384, hash E9183572 + sample 121: + time = 2904000 + flags = 1 + data = length 384, hash 9E36BC40 + sample 122: + time = 2928000 + flags = 1 + data = length 384, hash 937ED026 + sample 123: + time = 2952000 + flags = 1 + data = length 384, hash BF337AD1 + sample 124: + time = 2976000 + flags = 1 + data = length 384, hash E381C534 + sample 125: + time = 3000000 + flags = 1 + data = length 384, hash 6C9E1D71 + sample 126: + time = 3024000 + flags = 1 + data = length 384, hash 1C359B93 + sample 127: + time = 3048000 + flags = 1 + data = length 384, hash 3D137C16 + sample 128: + time = 3072000 + flags = 1 + data = length 384, hash 90D23677 + sample 129: + time = 3096000 + flags = 1 + data = length 384, hash 438F4839 + sample 130: + time = 3120000 + flags = 1 + data = length 384, hash EBAF44EF + sample 131: + time = 3144000 + flags = 1 + data = length 384, hash D8F64C54 + sample 132: + time = 3168000 + flags = 1 + data = length 384, hash 994F2EA6 + sample 133: + time = 3192000 + flags = 1 + data = length 384, hash 7E9DF6E4 + sample 134: + time = 3216000 + flags = 1 + data = length 384, hash 577F18B8 + sample 135: + time = 3240000 + flags = 1 + data = length 384, hash A47FCEE + sample 136: + time = 3264000 + flags = 1 + data = length 384, hash 5A1C435E + sample 137: + time = 3288000 + flags = 1 + data = length 384, hash 1D9EDC36 + sample 138: + time = 3312000 + flags = 1 + data = length 384, hash 3355333 + sample 139: + time = 3336000 + flags = 1 + data = length 384, hash 27CA9735 + sample 140: + time = 3360000 + flags = 1 + data = length 384, hash EB74B3F7 + sample 141: + time = 3384000 + flags = 1 + data = length 384, hash 22AC46CB + sample 142: + time = 3408000 + flags = 1 + data = length 384, hash 6002643D + sample 143: + time = 3432000 + flags = 1 + data = length 384, hash 487449E + sample 144: + time = 3456000 + flags = 1 + data = length 384, hash C5B10A14 + sample 145: + time = 3480000 + flags = 1 + data = length 384, hash 6050635D + sample 146: + time = 3504000 + flags = 1 + data = length 384, hash 437EAD63 + sample 147: + time = 3528000 + flags = 1 + data = length 384, hash 55A02C25 + sample 148: + time = 3552000 + flags = 1 + data = length 384, hash 171CCC00 + sample 149: + time = 3576000 + flags = 1 + data = length 384, hash 911127C8 + sample 150: + time = 3600000 + flags = 1 + data = length 384, hash AA157B50 + sample 151: + time = 3624000 + flags = 1 + data = length 384, hash 26F2D866 + sample 152: + time = 3648000 + flags = 1 + data = length 384, hash 67ADB3A9 + sample 153: + time = 3672000 + flags = 1 + data = length 384, hash F118D82D + sample 154: + time = 3696000 + flags = 1 + data = length 384, hash F51C252B + sample 155: + time = 3720000 + flags = 1 + data = length 384, hash BD13B97C + sample 156: + time = 3744000 + flags = 1 + data = length 384, hash 24BCF0AB + sample 157: + time = 3768000 + flags = 1 + data = length 384, hash 18DE9193 + sample 158: + time = 3792000 + flags = 1 + data = length 384, hash 234D8C99 + sample 159: + time = 3816000 + flags = 1 + data = length 384, hash EDFD2511 + sample 160: + time = 3840000 + flags = 1 + data = length 384, hash 69E3E157 + sample 161: + time = 3864000 + flags = 1 + data = length 384, hash AC90ADEC + sample 162: + time = 3888000 + flags = 1 + data = length 384, hash 6A333A56 + sample 163: + time = 3912000 + flags = 1 + data = length 384, hash 493D75A3 + sample 164: + time = 3936000 + flags = 1 + data = length 384, hash 53FE2A9E + sample 165: + time = 3960000 + flags = 1 + data = length 384, hash 65D6147C + sample 166: + time = 3984000 + flags = 1 + data = length 384, hash 5E744FB2 + sample 167: + time = 4008000 + flags = 1 + data = length 384, hash 68AEB7CA + sample 168: + time = 4032000 + flags = 1 + data = length 384, hash AC2972C + sample 169: + time = 4056000 + flags = 1 + data = length 384, hash E2A06CB9 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.1.dump b/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.1.dump new file mode 100644 index 0000000000..94a7031440 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.1.dump @@ -0,0 +1,751 @@ +seekMap: + isSeekable = true + duration = 4080000 + getPosition(0) = [[timeUs=0, position=9996]] + getPosition(1) = [[timeUs=0, position=9996]] + getPosition(2040000) = [[timeUs=1835152, position=160708], [timeUs=2335648, position=198850]] + getPosition(4080000) = [[timeUs=3837136, position=308434]] +numberOfTracks = 2 +track 0: + total output bytes = 165531 + sample count = 66 + format 0: + id = 0 + sampleMimeType = video/mp4v-es + maxInputSize = 6761 + width = 960 + height = 400 + sample 0: + time = 1334666 + flags = 1 + data = length 5891, hash 4627CBC3 + sample 1: + time = 1376374 + flags = 0 + data = length 3154, hash B7484F2C + sample 2: + time = 1418083 + flags = 0 + data = length 2409, hash 93E50DB6 + sample 3: + time = 1459791 + flags = 0 + data = length 2296, hash 73A46768 + sample 4: + time = 1501499 + flags = 0 + data = length 2514, hash F71DCA93 + sample 5: + time = 1543208 + flags = 0 + data = length 2614, hash BDD6744E + sample 6: + time = 1584916 + flags = 0 + data = length 2797, hash 81BED431 + sample 7: + time = 1626624 + flags = 0 + data = length 1549, hash D892E824 + sample 8: + time = 1668333 + flags = 0 + data = length 2714, hash B3EE7E2A + sample 9: + time = 1710041 + flags = 0 + data = length 2002, hash BC9E16ED + sample 10: + time = 1751749 + flags = 0 + data = length 2726, hash C31D5A82 + sample 11: + time = 1793458 + flags = 0 + data = length 2639, hash AE67DC59 + sample 12: + time = 1835166 + flags = 1 + data = length 5011, hash 630ADA59 + sample 13: + time = 1876874 + flags = 0 + data = length 4356, hash 76CE0D21 + sample 14: + time = 1918583 + flags = 0 + data = length 1986, hash AC41A7FC + sample 15: + time = 1960291 + flags = 0 + data = length 2792, hash 497D3A2D + sample 16: + time = 2001999 + flags = 0 + data = length 2176, hash FADAC8ED + sample 17: + time = 2043708 + flags = 0 + data = length 2463, hash 379DE4C8 + sample 18: + time = 2085416 + flags = 0 + data = length 2472, hash 9E68BAC5 + sample 19: + time = 2127124 + flags = 0 + data = length 1960, hash 38BC3EFC + sample 20: + time = 2168832 + flags = 0 + data = length 1833, hash 139C885B + sample 21: + time = 2210541 + flags = 0 + data = length 1865, hash A14BE838 + sample 22: + time = 2252249 + flags = 0 + data = length 1491, hash 8EC33935 + sample 23: + time = 2293957 + flags = 0 + data = length 1403, hash 78D87F2C + sample 24: + time = 2335666 + flags = 1 + data = length 4936, hash C34CC2D0 + sample 25: + time = 2377374 + flags = 0 + data = length 2539, hash D0EDEC2B + sample 26: + time = 2419082 + flags = 0 + data = length 3052, hash 3F68900F + sample 27: + time = 2460791 + flags = 0 + data = length 2998, hash B531AC4 + sample 28: + time = 2502499 + flags = 0 + data = length 1670, hash 734A2739 + sample 29: + time = 2544207 + flags = 0 + data = length 1634, hash 60A39EA5 + sample 30: + time = 2585916 + flags = 0 + data = length 1623, hash B18B39FE + sample 31: + time = 2627624 + flags = 0 + data = length 806, hash DA70C12B + sample 32: + time = 2669332 + flags = 0 + data = length 990, hash A1642D2C + sample 33: + time = 2711041 + flags = 0 + data = length 903, hash 411ECEA3 + sample 34: + time = 2752749 + flags = 0 + data = length 713, hash A4DAFA22 + sample 35: + time = 2794457 + flags = 0 + data = length 749, hash F39941EF + sample 36: + time = 2836166 + flags = 1 + data = length 5258, hash 19670F6D + sample 37: + time = 2877874 + flags = 0 + data = length 1932, hash 3F7F6D21 + sample 38: + time = 2919582 + flags = 0 + data = length 731, hash 45EF5D68 + sample 39: + time = 2961291 + flags = 0 + data = length 1076, hash 8C23B3FF + sample 40: + time = 3002999 + flags = 0 + data = length 1560, hash D6133304 + sample 41: + time = 3044707 + flags = 0 + data = length 2564, hash B7B256B + sample 42: + time = 3086416 + flags = 0 + data = length 2789, hash 97736B63 + sample 43: + time = 3128124 + flags = 0 + data = length 2469, hash C65A89B6 + sample 44: + time = 3169832 + flags = 0 + data = length 2203, hash D89686B4 + sample 45: + time = 3211541 + flags = 0 + data = length 2097, hash 91358D88 + sample 46: + time = 3253249 + flags = 0 + data = length 2043, hash 50547CF1 + sample 47: + time = 3294957 + flags = 0 + data = length 2198, hash F93F1606 + sample 48: + time = 3336666 + flags = 1 + data = length 5084, hash BEC89380 + sample 49: + time = 3378374 + flags = 0 + data = length 3043, hash F3C50E5A + sample 50: + time = 3420082 + flags = 0 + data = length 2786, hash 49C8C67C + sample 51: + time = 3461791 + flags = 0 + data = length 2652, hash D0A93BE7 + sample 52: + time = 3503499 + flags = 0 + data = length 2675, hash 81F7F5BD + sample 53: + time = 3545207 + flags = 0 + data = length 2916, hash E2A38AE1 + sample 54: + time = 3586916 + flags = 0 + data = length 2574, hash 50EC13BD + sample 55: + time = 3628624 + flags = 0 + data = length 2644, hash 3DF461F4 + sample 56: + time = 3670332 + flags = 0 + data = length 2932, hash E2F2DAB0 + sample 57: + time = 3712041 + flags = 0 + data = length 2625, hash 100D69E1 + sample 58: + time = 3753749 + flags = 0 + data = length 2773, hash 347DCC1F + sample 59: + time = 3795457 + flags = 0 + data = length 2348, hash 51FC01A3 + sample 60: + time = 3837166 + flags = 1 + data = length 5356, hash 190A3CAE + sample 61: + time = 3878874 + flags = 0 + data = length 3172, hash 538FA2AE + sample 62: + time = 3920582 + flags = 0 + data = length 2393, hash 525B26D6 + sample 63: + time = 3962291 + flags = 0 + data = length 2307, hash C894745F + sample 64: + time = 4003999 + flags = 0 + data = length 2490, hash 800FED70 + sample 65: + time = 4045707 + flags = 0 + data = length 2115, hash A2512D3 +track 1: + total output bytes = 44160 + sample count = 115 + format 0: + id = 1 + sampleMimeType = audio/mpeg + maxInputSize = 384 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 1296000 + flags = 1 + data = length 384, hash 4338D4A1 + sample 1: + time = 1320000 + flags = 1 + data = length 384, hash C65E6D68 + sample 2: + time = 1344000 + flags = 1 + data = length 384, hash AE2762E8 + sample 3: + time = 1368000 + flags = 1 + data = length 384, hash 8CFEAA7F + sample 4: + time = 1392000 + flags = 1 + data = length 384, hash A96A80B4 + sample 5: + time = 1416000 + flags = 1 + data = length 384, hash 69A84538 + sample 6: + time = 1440000 + flags = 1 + data = length 384, hash 9131F77E + sample 7: + time = 1464000 + flags = 1 + data = length 384, hash 818091B1 + sample 8: + time = 1488000 + flags = 1 + data = length 384, hash 6DBECC10 + sample 9: + time = 1512000 + flags = 1 + data = length 384, hash A9912967 + sample 10: + time = 1536000 + flags = 1 + data = length 384, hash 4DA97472 + sample 11: + time = 1560000 + flags = 1 + data = length 384, hash 31B363DC + sample 12: + time = 1584000 + flags = 1 + data = length 384, hash E15BB36C + sample 13: + time = 1608000 + flags = 1 + data = length 384, hash 159C963C + sample 14: + time = 1632000 + flags = 1 + data = length 384, hash 50B874D + sample 15: + time = 1656000 + flags = 1 + data = length 384, hash 8727F339 + sample 16: + time = 1680000 + flags = 1 + data = length 384, hash C0853B6 + sample 17: + time = 1704000 + flags = 1 + data = length 384, hash 9E026376 + sample 18: + time = 1728000 + flags = 1 + data = length 384, hash C190380F + sample 19: + time = 1752000 + flags = 1 + data = length 384, hash 20925F33 + sample 20: + time = 1776000 + flags = 1 + data = length 384, hash 9F740DAF + sample 21: + time = 1800000 + flags = 1 + data = length 384, hash F75757D6 + sample 22: + time = 1824000 + flags = 1 + data = length 384, hash 46D76ED0 + sample 23: + time = 1848000 + flags = 1 + data = length 384, hash 11DC480F + sample 24: + time = 1872000 + flags = 1 + data = length 384, hash 3428D6D8 + sample 25: + time = 1896000 + flags = 1 + data = length 384, hash 16A11668 + sample 26: + time = 1920000 + flags = 1 + data = length 384, hash 4CFBA63C + sample 27: + time = 1944000 + flags = 1 + data = length 384, hash 2B6702A9 + sample 28: + time = 1968000 + flags = 1 + data = length 384, hash D047CEF9 + sample 29: + time = 1992000 + flags = 1 + data = length 384, hash 25F05663 + sample 30: + time = 2016000 + flags = 1 + data = length 384, hash 947441C7 + sample 31: + time = 2040000 + flags = 1 + data = length 384, hash E82145F7 + sample 32: + time = 2064000 + flags = 1 + data = length 384, hash 6C40F859 + sample 33: + time = 2088000 + flags = 1 + data = length 384, hash 273FBEF8 + sample 34: + time = 2112000 + flags = 1 + data = length 384, hash 2FF062B6 + sample 35: + time = 2136000 + flags = 1 + data = length 384, hash 73FF8D58 + sample 36: + time = 2160000 + flags = 1 + data = length 384, hash F2BAB943 + sample 37: + time = 2184000 + flags = 1 + data = length 384, hash 507DEF9F + sample 38: + time = 2208000 + flags = 1 + data = length 384, hash 913E927A + sample 39: + time = 2232000 + flags = 1 + data = length 384, hash AFFD0AED + sample 40: + time = 2256000 + flags = 1 + data = length 384, hash EE0C6F4C + sample 41: + time = 2280000 + flags = 1 + data = length 384, hash 70726632 + sample 42: + time = 2304000 + flags = 1 + data = length 384, hash B5D49F8 + sample 43: + time = 2328000 + flags = 1 + data = length 384, hash B341AF3F + sample 44: + time = 2352000 + flags = 1 + data = length 384, hash 6AC1D8C4 + sample 45: + time = 2376000 + flags = 1 + data = length 384, hash BC666685 + sample 46: + time = 2400000 + flags = 1 + data = length 384, hash E58054E8 + sample 47: + time = 2424000 + flags = 1 + data = length 384, hash 404AB403 + sample 48: + time = 2448000 + flags = 1 + data = length 384, hash 265A86B8 + sample 49: + time = 2472000 + flags = 1 + data = length 384, hash 306316F6 + sample 50: + time = 2496000 + flags = 1 + data = length 384, hash 7BFDEA60 + sample 51: + time = 2520000 + flags = 1 + data = length 384, hash 2EFF8E5B + sample 52: + time = 2544000 + flags = 1 + data = length 384, hash C06CE84C + sample 53: + time = 2568000 + flags = 1 + data = length 384, hash 9069A01E + sample 54: + time = 2592000 + flags = 1 + data = length 384, hash 4A78F181 + sample 55: + time = 2616000 + flags = 1 + data = length 384, hash 57FD4BE7 + sample 56: + time = 2640000 + flags = 1 + data = length 384, hash B09DB688 + sample 57: + time = 2664000 + flags = 1 + data = length 384, hash 5602C52F + sample 58: + time = 2688000 + flags = 1 + data = length 384, hash 77762F5D + sample 59: + time = 2712000 + flags = 1 + data = length 384, hash 6A0BDB6 + sample 60: + time = 2736000 + flags = 1 + data = length 384, hash 2428C91 + sample 61: + time = 2760000 + flags = 1 + data = length 384, hash DEB54354 + sample 62: + time = 2784000 + flags = 1 + data = length 384, hash FB0B7BEE + sample 63: + time = 2808000 + flags = 1 + data = length 384, hash BDD82F68 + sample 64: + time = 2832000 + flags = 1 + data = length 384, hash BAB3B808 + sample 65: + time = 2856000 + flags = 1 + data = length 384, hash E9183572 + sample 66: + time = 2880000 + flags = 1 + data = length 384, hash 9E36BC40 + sample 67: + time = 2904000 + flags = 1 + data = length 384, hash 937ED026 + sample 68: + time = 2928000 + flags = 1 + data = length 384, hash BF337AD1 + sample 69: + time = 2952000 + flags = 1 + data = length 384, hash E381C534 + sample 70: + time = 2976000 + flags = 1 + data = length 384, hash 6C9E1D71 + sample 71: + time = 3000000 + flags = 1 + data = length 384, hash 1C359B93 + sample 72: + time = 3024000 + flags = 1 + data = length 384, hash 3D137C16 + sample 73: + time = 3048000 + flags = 1 + data = length 384, hash 90D23677 + sample 74: + time = 3072000 + flags = 1 + data = length 384, hash 438F4839 + sample 75: + time = 3096000 + flags = 1 + data = length 384, hash EBAF44EF + sample 76: + time = 3120000 + flags = 1 + data = length 384, hash D8F64C54 + sample 77: + time = 3144000 + flags = 1 + data = length 384, hash 994F2EA6 + sample 78: + time = 3168000 + flags = 1 + data = length 384, hash 7E9DF6E4 + sample 79: + time = 3192000 + flags = 1 + data = length 384, hash 577F18B8 + sample 80: + time = 3216000 + flags = 1 + data = length 384, hash A47FCEE + sample 81: + time = 3240000 + flags = 1 + data = length 384, hash 5A1C435E + sample 82: + time = 3264000 + flags = 1 + data = length 384, hash 1D9EDC36 + sample 83: + time = 3288000 + flags = 1 + data = length 384, hash 3355333 + sample 84: + time = 3312000 + flags = 1 + data = length 384, hash 27CA9735 + sample 85: + time = 3336000 + flags = 1 + data = length 384, hash EB74B3F7 + sample 86: + time = 3360000 + flags = 1 + data = length 384, hash 22AC46CB + sample 87: + time = 3384000 + flags = 1 + data = length 384, hash 6002643D + sample 88: + time = 3408000 + flags = 1 + data = length 384, hash 487449E + sample 89: + time = 3432000 + flags = 1 + data = length 384, hash C5B10A14 + sample 90: + time = 3456000 + flags = 1 + data = length 384, hash 6050635D + sample 91: + time = 3480000 + flags = 1 + data = length 384, hash 437EAD63 + sample 92: + time = 3504000 + flags = 1 + data = length 384, hash 55A02C25 + sample 93: + time = 3528000 + flags = 1 + data = length 384, hash 171CCC00 + sample 94: + time = 3552000 + flags = 1 + data = length 384, hash 911127C8 + sample 95: + time = 3576000 + flags = 1 + data = length 384, hash AA157B50 + sample 96: + time = 3600000 + flags = 1 + data = length 384, hash 26F2D866 + sample 97: + time = 3624000 + flags = 1 + data = length 384, hash 67ADB3A9 + sample 98: + time = 3648000 + flags = 1 + data = length 384, hash F118D82D + sample 99: + time = 3672000 + flags = 1 + data = length 384, hash F51C252B + sample 100: + time = 3696000 + flags = 1 + data = length 384, hash BD13B97C + sample 101: + time = 3720000 + flags = 1 + data = length 384, hash 24BCF0AB + sample 102: + time = 3744000 + flags = 1 + data = length 384, hash 18DE9193 + sample 103: + time = 3768000 + flags = 1 + data = length 384, hash 234D8C99 + sample 104: + time = 3792000 + flags = 1 + data = length 384, hash EDFD2511 + sample 105: + time = 3816000 + flags = 1 + data = length 384, hash 69E3E157 + sample 106: + time = 3840000 + flags = 1 + data = length 384, hash AC90ADEC + sample 107: + time = 3864000 + flags = 1 + data = length 384, hash 6A333A56 + sample 108: + time = 3888000 + flags = 1 + data = length 384, hash 493D75A3 + sample 109: + time = 3912000 + flags = 1 + data = length 384, hash 53FE2A9E + sample 110: + time = 3936000 + flags = 1 + data = length 384, hash 65D6147C + sample 111: + time = 3960000 + flags = 1 + data = length 384, hash 5E744FB2 + sample 112: + time = 3984000 + flags = 1 + data = length 384, hash 68AEB7CA + sample 113: + time = 4008000 + flags = 1 + data = length 384, hash AC2972C + sample 114: + time = 4032000 + flags = 1 + data = length 384, hash E2A06CB9 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.2.dump b/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.2.dump new file mode 100644 index 0000000000..ba5d85a844 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.2.dump @@ -0,0 +1,487 @@ +seekMap: + isSeekable = true + duration = 4080000 + getPosition(0) = [[timeUs=0, position=9996]] + getPosition(1) = [[timeUs=0, position=9996]] + getPosition(2040000) = [[timeUs=1835152, position=160708], [timeUs=2335648, position=198850]] + getPosition(4080000) = [[timeUs=3837136, position=308434]] +numberOfTracks = 2 +track 0: + total output bytes = 102418 + sample count = 42 + format 0: + id = 0 + sampleMimeType = video/mp4v-es + maxInputSize = 6761 + width = 960 + height = 400 + sample 0: + time = 2335666 + flags = 1 + data = length 4936, hash C34CC2D0 + sample 1: + time = 2377374 + flags = 0 + data = length 2539, hash D0EDEC2B + sample 2: + time = 2419082 + flags = 0 + data = length 3052, hash 3F68900F + sample 3: + time = 2460791 + flags = 0 + data = length 2998, hash B531AC4 + sample 4: + time = 2502499 + flags = 0 + data = length 1670, hash 734A2739 + sample 5: + time = 2544207 + flags = 0 + data = length 1634, hash 60A39EA5 + sample 6: + time = 2585916 + flags = 0 + data = length 1623, hash B18B39FE + sample 7: + time = 2627624 + flags = 0 + data = length 806, hash DA70C12B + sample 8: + time = 2669332 + flags = 0 + data = length 990, hash A1642D2C + sample 9: + time = 2711041 + flags = 0 + data = length 903, hash 411ECEA3 + sample 10: + time = 2752749 + flags = 0 + data = length 713, hash A4DAFA22 + sample 11: + time = 2794457 + flags = 0 + data = length 749, hash F39941EF + sample 12: + time = 2836166 + flags = 1 + data = length 5258, hash 19670F6D + sample 13: + time = 2877874 + flags = 0 + data = length 1932, hash 3F7F6D21 + sample 14: + time = 2919582 + flags = 0 + data = length 731, hash 45EF5D68 + sample 15: + time = 2961291 + flags = 0 + data = length 1076, hash 8C23B3FF + sample 16: + time = 3002999 + flags = 0 + data = length 1560, hash D6133304 + sample 17: + time = 3044707 + flags = 0 + data = length 2564, hash B7B256B + sample 18: + time = 3086416 + flags = 0 + data = length 2789, hash 97736B63 + sample 19: + time = 3128124 + flags = 0 + data = length 2469, hash C65A89B6 + sample 20: + time = 3169832 + flags = 0 + data = length 2203, hash D89686B4 + sample 21: + time = 3211541 + flags = 0 + data = length 2097, hash 91358D88 + sample 22: + time = 3253249 + flags = 0 + data = length 2043, hash 50547CF1 + sample 23: + time = 3294957 + flags = 0 + data = length 2198, hash F93F1606 + sample 24: + time = 3336666 + flags = 1 + data = length 5084, hash BEC89380 + sample 25: + time = 3378374 + flags = 0 + data = length 3043, hash F3C50E5A + sample 26: + time = 3420082 + flags = 0 + data = length 2786, hash 49C8C67C + sample 27: + time = 3461791 + flags = 0 + data = length 2652, hash D0A93BE7 + sample 28: + time = 3503499 + flags = 0 + data = length 2675, hash 81F7F5BD + sample 29: + time = 3545207 + flags = 0 + data = length 2916, hash E2A38AE1 + sample 30: + time = 3586916 + flags = 0 + data = length 2574, hash 50EC13BD + sample 31: + time = 3628624 + flags = 0 + data = length 2644, hash 3DF461F4 + sample 32: + time = 3670332 + flags = 0 + data = length 2932, hash E2F2DAB0 + sample 33: + time = 3712041 + flags = 0 + data = length 2625, hash 100D69E1 + sample 34: + time = 3753749 + flags = 0 + data = length 2773, hash 347DCC1F + sample 35: + time = 3795457 + flags = 0 + data = length 2348, hash 51FC01A3 + sample 36: + time = 3837166 + flags = 1 + data = length 5356, hash 190A3CAE + sample 37: + time = 3878874 + flags = 0 + data = length 3172, hash 538FA2AE + sample 38: + time = 3920582 + flags = 0 + data = length 2393, hash 525B26D6 + sample 39: + time = 3962291 + flags = 0 + data = length 2307, hash C894745F + sample 40: + time = 4003999 + flags = 0 + data = length 2490, hash 800FED70 + sample 41: + time = 4045707 + flags = 0 + data = length 2115, hash A2512D3 +track 1: + total output bytes = 28032 + sample count = 73 + format 0: + id = 1 + sampleMimeType = audio/mpeg + maxInputSize = 384 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 2304000 + flags = 1 + data = length 384, hash B5D49F8 + sample 1: + time = 2328000 + flags = 1 + data = length 384, hash B341AF3F + sample 2: + time = 2352000 + flags = 1 + data = length 384, hash 6AC1D8C4 + sample 3: + time = 2376000 + flags = 1 + data = length 384, hash BC666685 + sample 4: + time = 2400000 + flags = 1 + data = length 384, hash E58054E8 + sample 5: + time = 2424000 + flags = 1 + data = length 384, hash 404AB403 + sample 6: + time = 2448000 + flags = 1 + data = length 384, hash 265A86B8 + sample 7: + time = 2472000 + flags = 1 + data = length 384, hash 306316F6 + sample 8: + time = 2496000 + flags = 1 + data = length 384, hash 7BFDEA60 + sample 9: + time = 2520000 + flags = 1 + data = length 384, hash 2EFF8E5B + sample 10: + time = 2544000 + flags = 1 + data = length 384, hash C06CE84C + sample 11: + time = 2568000 + flags = 1 + data = length 384, hash 9069A01E + sample 12: + time = 2592000 + flags = 1 + data = length 384, hash 4A78F181 + sample 13: + time = 2616000 + flags = 1 + data = length 384, hash 57FD4BE7 + sample 14: + time = 2640000 + flags = 1 + data = length 384, hash B09DB688 + sample 15: + time = 2664000 + flags = 1 + data = length 384, hash 5602C52F + sample 16: + time = 2688000 + flags = 1 + data = length 384, hash 77762F5D + sample 17: + time = 2712000 + flags = 1 + data = length 384, hash 6A0BDB6 + sample 18: + time = 2736000 + flags = 1 + data = length 384, hash 2428C91 + sample 19: + time = 2760000 + flags = 1 + data = length 384, hash DEB54354 + sample 20: + time = 2784000 + flags = 1 + data = length 384, hash FB0B7BEE + sample 21: + time = 2808000 + flags = 1 + data = length 384, hash BDD82F68 + sample 22: + time = 2832000 + flags = 1 + data = length 384, hash BAB3B808 + sample 23: + time = 2856000 + flags = 1 + data = length 384, hash E9183572 + sample 24: + time = 2880000 + flags = 1 + data = length 384, hash 9E36BC40 + sample 25: + time = 2904000 + flags = 1 + data = length 384, hash 937ED026 + sample 26: + time = 2928000 + flags = 1 + data = length 384, hash BF337AD1 + sample 27: + time = 2952000 + flags = 1 + data = length 384, hash E381C534 + sample 28: + time = 2976000 + flags = 1 + data = length 384, hash 6C9E1D71 + sample 29: + time = 3000000 + flags = 1 + data = length 384, hash 1C359B93 + sample 30: + time = 3024000 + flags = 1 + data = length 384, hash 3D137C16 + sample 31: + time = 3048000 + flags = 1 + data = length 384, hash 90D23677 + sample 32: + time = 3072000 + flags = 1 + data = length 384, hash 438F4839 + sample 33: + time = 3096000 + flags = 1 + data = length 384, hash EBAF44EF + sample 34: + time = 3120000 + flags = 1 + data = length 384, hash D8F64C54 + sample 35: + time = 3144000 + flags = 1 + data = length 384, hash 994F2EA6 + sample 36: + time = 3168000 + flags = 1 + data = length 384, hash 7E9DF6E4 + sample 37: + time = 3192000 + flags = 1 + data = length 384, hash 577F18B8 + sample 38: + time = 3216000 + flags = 1 + data = length 384, hash A47FCEE + sample 39: + time = 3240000 + flags = 1 + data = length 384, hash 5A1C435E + sample 40: + time = 3264000 + flags = 1 + data = length 384, hash 1D9EDC36 + sample 41: + time = 3288000 + flags = 1 + data = length 384, hash 3355333 + sample 42: + time = 3312000 + flags = 1 + data = length 384, hash 27CA9735 + sample 43: + time = 3336000 + flags = 1 + data = length 384, hash EB74B3F7 + sample 44: + time = 3360000 + flags = 1 + data = length 384, hash 22AC46CB + sample 45: + time = 3384000 + flags = 1 + data = length 384, hash 6002643D + sample 46: + time = 3408000 + flags = 1 + data = length 384, hash 487449E + sample 47: + time = 3432000 + flags = 1 + data = length 384, hash C5B10A14 + sample 48: + time = 3456000 + flags = 1 + data = length 384, hash 6050635D + sample 49: + time = 3480000 + flags = 1 + data = length 384, hash 437EAD63 + sample 50: + time = 3504000 + flags = 1 + data = length 384, hash 55A02C25 + sample 51: + time = 3528000 + flags = 1 + data = length 384, hash 171CCC00 + sample 52: + time = 3552000 + flags = 1 + data = length 384, hash 911127C8 + sample 53: + time = 3576000 + flags = 1 + data = length 384, hash AA157B50 + sample 54: + time = 3600000 + flags = 1 + data = length 384, hash 26F2D866 + sample 55: + time = 3624000 + flags = 1 + data = length 384, hash 67ADB3A9 + sample 56: + time = 3648000 + flags = 1 + data = length 384, hash F118D82D + sample 57: + time = 3672000 + flags = 1 + data = length 384, hash F51C252B + sample 58: + time = 3696000 + flags = 1 + data = length 384, hash BD13B97C + sample 59: + time = 3720000 + flags = 1 + data = length 384, hash 24BCF0AB + sample 60: + time = 3744000 + flags = 1 + data = length 384, hash 18DE9193 + sample 61: + time = 3768000 + flags = 1 + data = length 384, hash 234D8C99 + sample 62: + time = 3792000 + flags = 1 + data = length 384, hash EDFD2511 + sample 63: + time = 3816000 + flags = 1 + data = length 384, hash 69E3E157 + sample 64: + time = 3840000 + flags = 1 + data = length 384, hash AC90ADEC + sample 65: + time = 3864000 + flags = 1 + data = length 384, hash 6A333A56 + sample 66: + time = 3888000 + flags = 1 + data = length 384, hash 493D75A3 + sample 67: + time = 3912000 + flags = 1 + data = length 384, hash 53FE2A9E + sample 68: + time = 3936000 + flags = 1 + data = length 384, hash 65D6147C + sample 69: + time = 3960000 + flags = 1 + data = length 384, hash 5E744FB2 + sample 70: + time = 3984000 + flags = 1 + data = length 384, hash 68AEB7CA + sample 71: + time = 4008000 + flags = 1 + data = length 384, hash AC2972C + sample 72: + time = 4032000 + flags = 1 + data = length 384, hash E2A06CB9 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.3.dump b/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.3.dump new file mode 100644 index 0000000000..2be55648da --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.3.dump @@ -0,0 +1,91 @@ +seekMap: + isSeekable = true + duration = 4080000 + getPosition(0) = [[timeUs=0, position=9996]] + getPosition(1) = [[timeUs=0, position=9996]] + getPosition(2040000) = [[timeUs=1835152, position=160708], [timeUs=2335648, position=198850]] + getPosition(4080000) = [[timeUs=3837136, position=308434]] +numberOfTracks = 2 +track 0: + total output bytes = 17833 + sample count = 6 + format 0: + id = 0 + sampleMimeType = video/mp4v-es + maxInputSize = 6761 + width = 960 + height = 400 + sample 0: + time = 3837166 + flags = 1 + data = length 5356, hash 190A3CAE + sample 1: + time = 3878874 + flags = 0 + data = length 3172, hash 538FA2AE + sample 2: + time = 3920582 + flags = 0 + data = length 2393, hash 525B26D6 + sample 3: + time = 3962291 + flags = 0 + data = length 2307, hash C894745F + sample 4: + time = 4003999 + flags = 0 + data = length 2490, hash 800FED70 + sample 5: + time = 4045707 + flags = 0 + data = length 2115, hash A2512D3 +track 1: + total output bytes = 3840 + sample count = 10 + format 0: + id = 1 + sampleMimeType = audio/mpeg + maxInputSize = 384 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 3816000 + flags = 1 + data = length 384, hash 69E3E157 + sample 1: + time = 3840000 + flags = 1 + data = length 384, hash AC90ADEC + sample 2: + time = 3864000 + flags = 1 + data = length 384, hash 6A333A56 + sample 3: + time = 3888000 + flags = 1 + data = length 384, hash 493D75A3 + sample 4: + time = 3912000 + flags = 1 + data = length 384, hash 53FE2A9E + sample 5: + time = 3936000 + flags = 1 + data = length 384, hash 65D6147C + sample 6: + time = 3960000 + flags = 1 + data = length 384, hash 5E744FB2 + sample 7: + time = 3984000 + flags = 1 + data = length 384, hash 68AEB7CA + sample 8: + time = 4008000 + flags = 1 + data = length 384, hash AC2972C + sample 9: + time = 4032000 + flags = 1 + data = length 384, hash E2A06CB9 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.unknown_length.dump new file mode 100644 index 0000000000..666526a13d --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/avi/sample.avi.unknown_length.dump @@ -0,0 +1,1091 @@ +seekMap: + isSeekable = true + duration = 4080000 + getPosition(0) = [[timeUs=0, position=9996]] + getPosition(1) = [[timeUs=0, position=9996]] + getPosition(2040000) = [[timeUs=1835152, position=160708], [timeUs=2335648, position=198850]] + getPosition(4080000) = [[timeUs=3837136, position=308434]] +numberOfTracks = 2 +track 0: + total output bytes = 252777 + sample count = 96 + format 0: + id = 0 + sampleMimeType = video/mp4v-es + maxInputSize = 6761 + width = 960 + height = 400 + sample 0: + time = 0 + flags = 1 + data = length 4279, hash C8074EBB + sample 1: + time = 125124 + flags = 0 + data = length 268, hash 14B72252 + sample 2: + time = 166833 + flags = 0 + data = length 268, hash ED2AEF52 + sample 3: + time = 208541 + flags = 0 + data = length 268, hash DD9B546B + sample 4: + time = 250249 + flags = 0 + data = length 268, hash B60F216B + sample 5: + time = 291958 + flags = 0 + data = length 268, hash 89782584 + sample 6: + time = 333666 + flags = 0 + data = length 268, hash 61EBF284 + sample 7: + time = 375374 + flags = 0 + data = length 268, hash B4D111DE + sample 8: + time = 417083 + flags = 0 + data = length 268, hash 8D44DEDE + sample 9: + time = 458791 + flags = 0 + data = length 268, hash 60ADE2F7 + sample 10: + time = 500499 + flags = 1 + data = length 4656, hash F1F35C82 + sample 11: + time = 542208 + flags = 1 + data = length 5119, hash 3CFA0CA2 + sample 12: + time = 583916 + flags = 1 + data = length 5466, hash 7E1F6F63 + sample 13: + time = 625624 + flags = 1 + data = length 5990, hash 70A2D835 + sample 14: + time = 667333 + flags = 1 + data = length 6476, hash E633D374 + sample 15: + time = 709041 + flags = 1 + data = length 6761, hash 922BC7A6 + sample 16: + time = 750749 + flags = 1 + data = length 6501, hash B03632B9 + sample 17: + time = 792458 + flags = 1 + data = length 5824, hash 89BCFDCC + sample 18: + time = 834166 + flags = 1 + data = length 5816, hash 4B321EB2 + sample 19: + time = 875874 + flags = 0 + data = length 5307, hash EF15AF2D + sample 20: + time = 917583 + flags = 0 + data = length 2791, hash B48241CD + sample 21: + time = 959291 + flags = 0 + data = length 2505, hash FB9EE72B + sample 22: + time = 1000999 + flags = 0 + data = length 1747, hash 89DC0982 + sample 23: + time = 1042708 + flags = 0 + data = length 1948, hash B8642019 + sample 24: + time = 1084416 + flags = 0 + data = length 2134, hash E6115E1C + sample 25: + time = 1126124 + flags = 0 + data = length 2035, hash 86FD9E1E + sample 26: + time = 1167833 + flags = 0 + data = length 2109, hash D66E00D + sample 27: + time = 1209541 + flags = 0 + data = length 2427, hash 63E16CB5 + sample 28: + time = 1251249 + flags = 0 + data = length 2485, hash 38F83F6D + sample 29: + time = 1292958 + flags = 0 + data = length 2458, hash 48900F9D + sample 30: + time = 1334666 + flags = 1 + data = length 5891, hash 4627CBC3 + sample 31: + time = 1376374 + flags = 0 + data = length 3154, hash B7484F2C + sample 32: + time = 1418083 + flags = 0 + data = length 2409, hash 93E50DB6 + sample 33: + time = 1459791 + flags = 0 + data = length 2296, hash 73A46768 + sample 34: + time = 1501499 + flags = 0 + data = length 2514, hash F71DCA93 + sample 35: + time = 1543208 + flags = 0 + data = length 2614, hash BDD6744E + sample 36: + time = 1584916 + flags = 0 + data = length 2797, hash 81BED431 + sample 37: + time = 1626624 + flags = 0 + data = length 1549, hash D892E824 + sample 38: + time = 1668333 + flags = 0 + data = length 2714, hash B3EE7E2A + sample 39: + time = 1710041 + flags = 0 + data = length 2002, hash BC9E16ED + sample 40: + time = 1751749 + flags = 0 + data = length 2726, hash C31D5A82 + sample 41: + time = 1793458 + flags = 0 + data = length 2639, hash AE67DC59 + sample 42: + time = 1835166 + flags = 1 + data = length 5011, hash 630ADA59 + sample 43: + time = 1876874 + flags = 0 + data = length 4356, hash 76CE0D21 + sample 44: + time = 1918583 + flags = 0 + data = length 1986, hash AC41A7FC + sample 45: + time = 1960291 + flags = 0 + data = length 2792, hash 497D3A2D + sample 46: + time = 2001999 + flags = 0 + data = length 2176, hash FADAC8ED + sample 47: + time = 2043708 + flags = 0 + data = length 2463, hash 379DE4C8 + sample 48: + time = 2085416 + flags = 0 + data = length 2472, hash 9E68BAC5 + sample 49: + time = 2127124 + flags = 0 + data = length 1960, hash 38BC3EFC + sample 50: + time = 2168832 + flags = 0 + data = length 1833, hash 139C885B + sample 51: + time = 2210541 + flags = 0 + data = length 1865, hash A14BE838 + sample 52: + time = 2252249 + flags = 0 + data = length 1491, hash 8EC33935 + sample 53: + time = 2293957 + flags = 0 + data = length 1403, hash 78D87F2C + sample 54: + time = 2335666 + flags = 1 + data = length 4936, hash C34CC2D0 + sample 55: + time = 2377374 + flags = 0 + data = length 2539, hash D0EDEC2B + sample 56: + time = 2419082 + flags = 0 + data = length 3052, hash 3F68900F + sample 57: + time = 2460791 + flags = 0 + data = length 2998, hash B531AC4 + sample 58: + time = 2502499 + flags = 0 + data = length 1670, hash 734A2739 + sample 59: + time = 2544207 + flags = 0 + data = length 1634, hash 60A39EA5 + sample 60: + time = 2585916 + flags = 0 + data = length 1623, hash B18B39FE + sample 61: + time = 2627624 + flags = 0 + data = length 806, hash DA70C12B + sample 62: + time = 2669332 + flags = 0 + data = length 990, hash A1642D2C + sample 63: + time = 2711041 + flags = 0 + data = length 903, hash 411ECEA3 + sample 64: + time = 2752749 + flags = 0 + data = length 713, hash A4DAFA22 + sample 65: + time = 2794457 + flags = 0 + data = length 749, hash F39941EF + sample 66: + time = 2836166 + flags = 1 + data = length 5258, hash 19670F6D + sample 67: + time = 2877874 + flags = 0 + data = length 1932, hash 3F7F6D21 + sample 68: + time = 2919582 + flags = 0 + data = length 731, hash 45EF5D68 + sample 69: + time = 2961291 + flags = 0 + data = length 1076, hash 8C23B3FF + sample 70: + time = 3002999 + flags = 0 + data = length 1560, hash D6133304 + sample 71: + time = 3044707 + flags = 0 + data = length 2564, hash B7B256B + sample 72: + time = 3086416 + flags = 0 + data = length 2789, hash 97736B63 + sample 73: + time = 3128124 + flags = 0 + data = length 2469, hash C65A89B6 + sample 74: + time = 3169832 + flags = 0 + data = length 2203, hash D89686B4 + sample 75: + time = 3211541 + flags = 0 + data = length 2097, hash 91358D88 + sample 76: + time = 3253249 + flags = 0 + data = length 2043, hash 50547CF1 + sample 77: + time = 3294957 + flags = 0 + data = length 2198, hash F93F1606 + sample 78: + time = 3336666 + flags = 1 + data = length 5084, hash BEC89380 + sample 79: + time = 3378374 + flags = 0 + data = length 3043, hash F3C50E5A + sample 80: + time = 3420082 + flags = 0 + data = length 2786, hash 49C8C67C + sample 81: + time = 3461791 + flags = 0 + data = length 2652, hash D0A93BE7 + sample 82: + time = 3503499 + flags = 0 + data = length 2675, hash 81F7F5BD + sample 83: + time = 3545207 + flags = 0 + data = length 2916, hash E2A38AE1 + sample 84: + time = 3586916 + flags = 0 + data = length 2574, hash 50EC13BD + sample 85: + time = 3628624 + flags = 0 + data = length 2644, hash 3DF461F4 + sample 86: + time = 3670332 + flags = 0 + data = length 2932, hash E2F2DAB0 + sample 87: + time = 3712041 + flags = 0 + data = length 2625, hash 100D69E1 + sample 88: + time = 3753749 + flags = 0 + data = length 2773, hash 347DCC1F + sample 89: + time = 3795457 + flags = 0 + data = length 2348, hash 51FC01A3 + sample 90: + time = 3837166 + flags = 1 + data = length 5356, hash 190A3CAE + sample 91: + time = 3878874 + flags = 0 + data = length 3172, hash 538FA2AE + sample 92: + time = 3920582 + flags = 0 + data = length 2393, hash 525B26D6 + sample 93: + time = 3962291 + flags = 0 + data = length 2307, hash C894745F + sample 94: + time = 4003999 + flags = 0 + data = length 2490, hash 800FED70 + sample 95: + time = 4045707 + flags = 0 + data = length 2115, hash A2512D3 +track 1: + total output bytes = 65280 + sample count = 170 + format 0: + id = 1 + sampleMimeType = audio/mpeg + maxInputSize = 384 + channelCount = 2 + sampleRate = 48000 + sample 0: + time = 0 + flags = 1 + data = length 384, hash 4FA643C4 + sample 1: + time = 24000 + flags = 1 + data = length 384, hash 5ED84707 + sample 2: + time = 48000 + flags = 1 + data = length 384, hash 5ED84707 + sample 3: + time = 72000 + flags = 1 + data = length 384, hash 5ED84707 + sample 4: + time = 96000 + flags = 1 + data = length 384, hash 5ED84707 + sample 5: + time = 120000 + flags = 1 + data = length 384, hash 5ED84707 + sample 6: + time = 144000 + flags = 1 + data = length 384, hash 5ED84707 + sample 7: + time = 168000 + flags = 1 + data = length 384, hash 5ED84707 + sample 8: + time = 192000 + flags = 1 + data = length 384, hash 5ED84707 + sample 9: + time = 216000 + flags = 1 + data = length 384, hash 5ED84707 + sample 10: + time = 240000 + flags = 1 + data = length 384, hash 5ED84707 + sample 11: + time = 264000 + flags = 1 + data = length 384, hash 5ED84707 + sample 12: + time = 288000 + flags = 1 + data = length 384, hash 5ED84707 + sample 13: + time = 312000 + flags = 1 + data = length 384, hash 5ED84707 + sample 14: + time = 336000 + flags = 1 + data = length 384, hash 5ED84707 + sample 15: + time = 360000 + flags = 1 + data = length 384, hash 5ED84707 + sample 16: + time = 384000 + flags = 1 + data = length 384, hash D77E2886 + sample 17: + time = 408000 + flags = 1 + data = length 384, hash 5C372185 + sample 18: + time = 432000 + flags = 1 + data = length 384, hash F9589AE7 + sample 19: + time = 456000 + flags = 1 + data = length 384, hash F14EBCAC + sample 20: + time = 480000 + flags = 1 + data = length 384, hash 2B688404 + sample 21: + time = 504000 + flags = 1 + data = length 384, hash E889FC6B + sample 22: + time = 528000 + flags = 1 + data = length 384, hash 53CBDEC0 + sample 23: + time = 552000 + flags = 1 + data = length 384, hash 91769951 + sample 24: + time = 576000 + flags = 1 + data = length 384, hash 749935FF + sample 25: + time = 600000 + flags = 1 + data = length 384, hash 2B794BC6 + sample 26: + time = 624000 + flags = 1 + data = length 384, hash B6A1870B + sample 27: + time = 648000 + flags = 1 + data = length 384, hash 7D729EEC + sample 28: + time = 672000 + flags = 1 + data = length 384, hash AFBD0EF5 + sample 29: + time = 696000 + flags = 1 + data = length 384, hash C1DDC412 + sample 30: + time = 720000 + flags = 1 + data = length 384, hash CF1807A4 + sample 31: + time = 744000 + flags = 1 + data = length 384, hash CD1E8F85 + sample 32: + time = 768000 + flags = 1 + data = length 384, hash FF56854C + sample 33: + time = 792000 + flags = 1 + data = length 384, hash F6F8D897 + sample 34: + time = 816000 + flags = 1 + data = length 384, hash 9C3F1566 + sample 35: + time = 840000 + flags = 1 + data = length 384, hash C5D788D1 + sample 36: + time = 864000 + flags = 1 + data = length 384, hash 81FC222A + sample 37: + time = 888000 + flags = 1 + data = length 384, hash 749C0516 + sample 38: + time = 912000 + flags = 1 + data = length 384, hash 63C232FF + sample 39: + time = 936000 + flags = 1 + data = length 384, hash FB4FABBB + sample 40: + time = 960000 + flags = 1 + data = length 384, hash B787C813 + sample 41: + time = 984000 + flags = 1 + data = length 384, hash E18B955C + sample 42: + time = 1008000 + flags = 1 + data = length 384, hash 2085B856 + sample 43: + time = 1032000 + flags = 1 + data = length 384, hash BDF70D7C + sample 44: + time = 1056000 + flags = 1 + data = length 384, hash 47838243 + sample 45: + time = 1080000 + flags = 1 + data = length 384, hash 5CF6CC33 + sample 46: + time = 1104000 + flags = 1 + data = length 384, hash 2A979CF6 + sample 47: + time = 1128000 + flags = 1 + data = length 384, hash 26D5CF5A + sample 48: + time = 1152000 + flags = 1 + data = length 384, hash E1BFEE5D + sample 49: + time = 1176000 + flags = 1 + data = length 384, hash A4DF110B + sample 50: + time = 1200000 + flags = 1 + data = length 384, hash 8595335A + sample 51: + time = 1224000 + flags = 1 + data = length 384, hash 5CA30C8 + sample 52: + time = 1248000 + flags = 1 + data = length 384, hash 1219C18C + sample 53: + time = 1272000 + flags = 1 + data = length 384, hash 41DC2F24 + sample 54: + time = 1296000 + flags = 1 + data = length 384, hash 664A60E1 + sample 55: + time = 1320000 + flags = 1 + data = length 384, hash 4338D4A1 + sample 56: + time = 1344000 + flags = 1 + data = length 384, hash C65E6D68 + sample 57: + time = 1368000 + flags = 1 + data = length 384, hash AE2762E8 + sample 58: + time = 1392000 + flags = 1 + data = length 384, hash 8CFEAA7F + sample 59: + time = 1416000 + flags = 1 + data = length 384, hash A96A80B4 + sample 60: + time = 1440000 + flags = 1 + data = length 384, hash 69A84538 + sample 61: + time = 1464000 + flags = 1 + data = length 384, hash 9131F77E + sample 62: + time = 1488000 + flags = 1 + data = length 384, hash 818091B1 + sample 63: + time = 1512000 + flags = 1 + data = length 384, hash 6DBECC10 + sample 64: + time = 1536000 + flags = 1 + data = length 384, hash A9912967 + sample 65: + time = 1560000 + flags = 1 + data = length 384, hash 4DA97472 + sample 66: + time = 1584000 + flags = 1 + data = length 384, hash 31B363DC + sample 67: + time = 1608000 + flags = 1 + data = length 384, hash E15BB36C + sample 68: + time = 1632000 + flags = 1 + data = length 384, hash 159C963C + sample 69: + time = 1656000 + flags = 1 + data = length 384, hash 50B874D + sample 70: + time = 1680000 + flags = 1 + data = length 384, hash 8727F339 + sample 71: + time = 1704000 + flags = 1 + data = length 384, hash C0853B6 + sample 72: + time = 1728000 + flags = 1 + data = length 384, hash 9E026376 + sample 73: + time = 1752000 + flags = 1 + data = length 384, hash C190380F + sample 74: + time = 1776000 + flags = 1 + data = length 384, hash 20925F33 + sample 75: + time = 1800000 + flags = 1 + data = length 384, hash 9F740DAF + sample 76: + time = 1824000 + flags = 1 + data = length 384, hash F75757D6 + sample 77: + time = 1848000 + flags = 1 + data = length 384, hash 46D76ED0 + sample 78: + time = 1872000 + flags = 1 + data = length 384, hash 11DC480F + sample 79: + time = 1896000 + flags = 1 + data = length 384, hash 3428D6D8 + sample 80: + time = 1920000 + flags = 1 + data = length 384, hash 16A11668 + sample 81: + time = 1944000 + flags = 1 + data = length 384, hash 4CFBA63C + sample 82: + time = 1968000 + flags = 1 + data = length 384, hash 2B6702A9 + sample 83: + time = 1992000 + flags = 1 + data = length 384, hash D047CEF9 + sample 84: + time = 2016000 + flags = 1 + data = length 384, hash 25F05663 + sample 85: + time = 2040000 + flags = 1 + data = length 384, hash 947441C7 + sample 86: + time = 2064000 + flags = 1 + data = length 384, hash E82145F7 + sample 87: + time = 2088000 + flags = 1 + data = length 384, hash 6C40F859 + sample 88: + time = 2112000 + flags = 1 + data = length 384, hash 273FBEF8 + sample 89: + time = 2136000 + flags = 1 + data = length 384, hash 2FF062B6 + sample 90: + time = 2160000 + flags = 1 + data = length 384, hash 73FF8D58 + sample 91: + time = 2184000 + flags = 1 + data = length 384, hash F2BAB943 + sample 92: + time = 2208000 + flags = 1 + data = length 384, hash 507DEF9F + sample 93: + time = 2232000 + flags = 1 + data = length 384, hash 913E927A + sample 94: + time = 2256000 + flags = 1 + data = length 384, hash AFFD0AED + sample 95: + time = 2280000 + flags = 1 + data = length 384, hash EE0C6F4C + sample 96: + time = 2304000 + flags = 1 + data = length 384, hash 70726632 + sample 97: + time = 2328000 + flags = 1 + data = length 384, hash B5D49F8 + sample 98: + time = 2352000 + flags = 1 + data = length 384, hash B341AF3F + sample 99: + time = 2376000 + flags = 1 + data = length 384, hash 6AC1D8C4 + sample 100: + time = 2400000 + flags = 1 + data = length 384, hash BC666685 + sample 101: + time = 2424000 + flags = 1 + data = length 384, hash E58054E8 + sample 102: + time = 2448000 + flags = 1 + data = length 384, hash 404AB403 + sample 103: + time = 2472000 + flags = 1 + data = length 384, hash 265A86B8 + sample 104: + time = 2496000 + flags = 1 + data = length 384, hash 306316F6 + sample 105: + time = 2520000 + flags = 1 + data = length 384, hash 7BFDEA60 + sample 106: + time = 2544000 + flags = 1 + data = length 384, hash 2EFF8E5B + sample 107: + time = 2568000 + flags = 1 + data = length 384, hash C06CE84C + sample 108: + time = 2592000 + flags = 1 + data = length 384, hash 9069A01E + sample 109: + time = 2616000 + flags = 1 + data = length 384, hash 4A78F181 + sample 110: + time = 2640000 + flags = 1 + data = length 384, hash 57FD4BE7 + sample 111: + time = 2664000 + flags = 1 + data = length 384, hash B09DB688 + sample 112: + time = 2688000 + flags = 1 + data = length 384, hash 5602C52F + sample 113: + time = 2712000 + flags = 1 + data = length 384, hash 77762F5D + sample 114: + time = 2736000 + flags = 1 + data = length 384, hash 6A0BDB6 + sample 115: + time = 2760000 + flags = 1 + data = length 384, hash 2428C91 + sample 116: + time = 2784000 + flags = 1 + data = length 384, hash DEB54354 + sample 117: + time = 2808000 + flags = 1 + data = length 384, hash FB0B7BEE + sample 118: + time = 2832000 + flags = 1 + data = length 384, hash BDD82F68 + sample 119: + time = 2856000 + flags = 1 + data = length 384, hash BAB3B808 + sample 120: + time = 2880000 + flags = 1 + data = length 384, hash E9183572 + sample 121: + time = 2904000 + flags = 1 + data = length 384, hash 9E36BC40 + sample 122: + time = 2928000 + flags = 1 + data = length 384, hash 937ED026 + sample 123: + time = 2952000 + flags = 1 + data = length 384, hash BF337AD1 + sample 124: + time = 2976000 + flags = 1 + data = length 384, hash E381C534 + sample 125: + time = 3000000 + flags = 1 + data = length 384, hash 6C9E1D71 + sample 126: + time = 3024000 + flags = 1 + data = length 384, hash 1C359B93 + sample 127: + time = 3048000 + flags = 1 + data = length 384, hash 3D137C16 + sample 128: + time = 3072000 + flags = 1 + data = length 384, hash 90D23677 + sample 129: + time = 3096000 + flags = 1 + data = length 384, hash 438F4839 + sample 130: + time = 3120000 + flags = 1 + data = length 384, hash EBAF44EF + sample 131: + time = 3144000 + flags = 1 + data = length 384, hash D8F64C54 + sample 132: + time = 3168000 + flags = 1 + data = length 384, hash 994F2EA6 + sample 133: + time = 3192000 + flags = 1 + data = length 384, hash 7E9DF6E4 + sample 134: + time = 3216000 + flags = 1 + data = length 384, hash 577F18B8 + sample 135: + time = 3240000 + flags = 1 + data = length 384, hash A47FCEE + sample 136: + time = 3264000 + flags = 1 + data = length 384, hash 5A1C435E + sample 137: + time = 3288000 + flags = 1 + data = length 384, hash 1D9EDC36 + sample 138: + time = 3312000 + flags = 1 + data = length 384, hash 3355333 + sample 139: + time = 3336000 + flags = 1 + data = length 384, hash 27CA9735 + sample 140: + time = 3360000 + flags = 1 + data = length 384, hash EB74B3F7 + sample 141: + time = 3384000 + flags = 1 + data = length 384, hash 22AC46CB + sample 142: + time = 3408000 + flags = 1 + data = length 384, hash 6002643D + sample 143: + time = 3432000 + flags = 1 + data = length 384, hash 487449E + sample 144: + time = 3456000 + flags = 1 + data = length 384, hash C5B10A14 + sample 145: + time = 3480000 + flags = 1 + data = length 384, hash 6050635D + sample 146: + time = 3504000 + flags = 1 + data = length 384, hash 437EAD63 + sample 147: + time = 3528000 + flags = 1 + data = length 384, hash 55A02C25 + sample 148: + time = 3552000 + flags = 1 + data = length 384, hash 171CCC00 + sample 149: + time = 3576000 + flags = 1 + data = length 384, hash 911127C8 + sample 150: + time = 3600000 + flags = 1 + data = length 384, hash AA157B50 + sample 151: + time = 3624000 + flags = 1 + data = length 384, hash 26F2D866 + sample 152: + time = 3648000 + flags = 1 + data = length 384, hash 67ADB3A9 + sample 153: + time = 3672000 + flags = 1 + data = length 384, hash F118D82D + sample 154: + time = 3696000 + flags = 1 + data = length 384, hash F51C252B + sample 155: + time = 3720000 + flags = 1 + data = length 384, hash BD13B97C + sample 156: + time = 3744000 + flags = 1 + data = length 384, hash 24BCF0AB + sample 157: + time = 3768000 + flags = 1 + data = length 384, hash 18DE9193 + sample 158: + time = 3792000 + flags = 1 + data = length 384, hash 234D8C99 + sample 159: + time = 3816000 + flags = 1 + data = length 384, hash EDFD2511 + sample 160: + time = 3840000 + flags = 1 + data = length 384, hash 69E3E157 + sample 161: + time = 3864000 + flags = 1 + data = length 384, hash AC90ADEC + sample 162: + time = 3888000 + flags = 1 + data = length 384, hash 6A333A56 + sample 163: + time = 3912000 + flags = 1 + data = length 384, hash 493D75A3 + sample 164: + time = 3936000 + flags = 1 + data = length 384, hash 53FE2A9E + sample 165: + time = 3960000 + flags = 1 + data = length 384, hash 65D6147C + sample 166: + time = 3984000 + flags = 1 + data = length 384, hash 5E744FB2 + sample 167: + time = 4008000 + flags = 1 + data = length 384, hash 68AEB7CA + sample 168: + time = 4032000 + flags = 1 + data = length 384, hash AC2972C + sample 169: + time = 4056000 + flags = 1 + data = length 384, hash E2A06CB9 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/media/avi/sample.avi b/libraries/test_data/src/test/assets/media/avi/sample.avi new file mode 100644 index 0000000000000000000000000000000000000000..9f95d3fa717129d1498bd0dcd103ddf2db66ddf4 GIT binary patch literal 334534 zcmeEv2V9PA|MyjCNNK2)XhP(9?$=I-{*Pm|C`T!b9P#5Zf+I5!$Bg2uu(r``Ul;sT98EA zv)9QM{>S%n-nn&;@e(t2rqf7Y;QE}6#3$(9JI9eo1Mr7-P7X&18(hC#5k5fw)1Jph z5+~t%g$dD4rT=TXa_J&*c=ibDCn_pit?6G59}II~m;=Kc80Nq*2ZlK?%zjft;*E!HvsLmk;|E`4UidQ=N1kQsZ1|NwH*IT?su#p@&f2wY% z?xz)Hh)ah59_GL>2ZlK?%z6#kTRaI0-h+ADl)OCJ+ z&*5)|IWWwD|1BIq2+{Fz1c_|#yi)}#skkoN6SkEk&;r9~KqU`}#4kgF3M`2&4t|6~ zbuSn#TdcptXeL6Cm4CB8e}jdidA&5KsRPRs5#R6lkHG>UvB{9~f&!X2rADZU7{W)l z0n4oc%T;HpV=yPNC6ffX;fqv3lDLRvFu9kU)|y(CPw|u-KKvK>;6WlC^_CmqW?w_j z4odCUpmb6GTMs))q(J?@$5Q`$naH#<;OB|lzXv~Iso{SUEoEZxfA1mi^8(x7M)Wa% z6D|Gkl{eGMK=fDj{~n@eTJ?YLA@DQtZ=#>D)D-Vw(f?lybby~3Y<~}a!cw#UCR+O6 zYwAoZ1AZ3h|7}G7|S+OaFW2&9t&k5_27CdGPP+NPn~H z5o5W3hdL5dJ#SbJ4AK3&kB05x-_7x%pEoRrq51Of{*<;Ko%vrrD0HL3wb_piJZLVQ z=}$JQzUmSm^r7-5^_X@_6)Mp&z7MCJ%k=#PHpcfcv~!uhzp79Dp6Rnh+IjHI6z`wE z&tRi|2H$7X&Skp3K%epb6WX~<-@o)`eE)`aF8%vqUHx}!<lvI}ro2uOn z_nezrv_WUun^~H-#5~kFUaZhmxfHw5#a~Tpry)=K7*%62x!KEisK1rHom@Fb{a*3&(ziOt;y`XGDm%P z&4uuHvlI4m9DDnDmaF9nzQ2Tco(s<7x2TKzR$#pB=I%IVjIh|QHH;DeBt}$BkX<}& zNm;3758qV2#pOG>`R%IPpQW7SyAx7YDJ;UjiZk({?U^N$EJ};Rl%9=wq489_p6%iC z;KeS2+@p`_Nvn31J=c4vC%_lJhDy*Pn&#>>Av z*jb{qJ!F~I{+P}JHvui{r*kJu$wlsXnt61ddA!~I8Tl4^m0O*sZI_8nwRRTUt|Xte z-C$NNmtLObcFxHaYj19_`E({MSDZUAbfmB{ugz#ZL2W)Z(Z=ato!oYw*-Gh@WS zQKvP$9ERB~ca6MNNN)Y|lJb*GZ#gy_n#32qZd9#5`j_OTFIydr>+bb+IP2V5Ar)G% zhpRF5NZsBzy6WYoy zd~x)l2@+woZ_b%|b!_iG!n4jOce-+{{K;#6?ZKJIcAV3U%%?z_G_VfWZG$;r3olXlAXwF%ap z+^XB5Cn#Ih_bMtquY2a^fq`#;5r?hVr_WI$*&HFAz4Ts~hwVbx-uMk8WIMed^6*3n zoF$3)O&>9vH&5JJ=i`X-w0@7L{mB=WT@GKUscU*(>3hZqi8Y1E-d^{Db93hV8*aDy zFl{`SR!XLN0@tH+jBzh(LYP)C~87p()JL>k0PCtsHyf$;dHh z3}QL;c2Azdqr9+kL7X)k7q4Vt2j7|T(ed1}TNXa|cKy`SU@+ls#=I|^_i;q^xJ7(W zd-(WtW&fhB{+@0|E+Q)PPlOt*YHAYHJ2pnsYxI~S7m6;K^lYmNYaesHVqoBXbGuiE zWQm2Z?!YC%C8EWG#gR+TzdA6_oZT?)#Zj(x-!s;ZP+s+2GstM_?X=9Ut+rW`9G@*U zImKlCcQl`mKlZ^YS2ZMZzpu__56WzlV%y^0iWk*F${|lI+2VQTmG5GA*foNWBPsvA z&M0~fvb0vd zFI(54#N@2ycCm+w6)go`-Tw6Mf?Cas9ND`hVI7`81&Yw#QQlL6X76HSXY($e$nJ6a z>7CDU-pbz(-um%v3frZ9g3a`o7@zqm)J*>2Tm8{=A|qdQ~2xW=2zVYd4ETW4N!${7Fd$Jk5E7?BaouAw^5XQZLb6jI@7emR){8#fWYFFr*A zf^vEt5?a}5V=bpv?QJYdvE$7N7R+1DlPh+jU|XZZSeJHB<={hp&M%#cikyy)&~G)n zx9Vhw>A*!VA)`a1$j>=W^NlmBKCz`F_w%T(i4GOHvlUy}ypKFia(W}@*Q_RXBCbZb zlXQmUb0(yTG|Bw4(Uu9X)eh%Jfn95ug67i%xT{Y<&6j$>}Y9X=6sX z%MNhs9{23s@25|)QD$4szp$Ur<-#FZiJaR)+s@xPVzIkr*39tBU!FeKoV?0-^3Ahx zS|Ox)97&FBJYP?YW?T5&`u&q>(zm;Ae|u~ZV`XDA(`4Jds9jzpc8k(^Gn{TMca$Al zaV}(>=-LDE-{L~Hl#w@wYpJ)C+AW&IIYzu8M&oL}KzL1o(zXoVJ?T^URyjKiQ2YeH`3%T^FIRh7Eol)mLyVx`Ra^F zUvb_wrv3;cRD|)&$p3VYq^PBfNc1T~QycU5<5n0%S~=EMF!uMk5r^8p-RdVt&OS&^ zE2G#s{^91rXT9XQ)>L@Rz5Jo}WD;NO|IHS|?sc%#@aHejiKH{#p&;V_%?8hcwgE}k z5)DcQIZf*qJ01zrf3ajwG`S-`B3dA3kIa+(W+I+$Dz)R!M|!+2)Q(tYA3N0dv0QoD)mvl#>Vm4D7?>wjUzBo>a z%&+98Do0KS06+_<>}vZprGu@x0g+kGKpJ%AX~Bc~n}5GY8!eQwpigoKZ-%8r3wA+J)VEeU+W=v0a3QkeCI!pXNJo+g}nn-KRuI)-r&?`>SKZ7 zw4Kwx*G+do$Y!+ha-_bZZ@_v(BjpDV7Si>(xR<6QbaIBmQNRU4F=))f@L}(^YToC^ zR~7}W9N!e?s}S?mqjv7RHS%etydzIqW~8=lIkeL({N=pG2aG8PnTTK<0_1{XW0LAx zYcco?97O8$3mhWQ&ONQQt}2^Eipiyu^b$zI3X_(lnf^NlGM-Iv?7??ORS6n@_kB@wmCK`(EFMRS~AOj|yLwb2zPawMvnl z_&TVSf8OH*lhj`+)Vs#*R1ek|Lgkq#PHX)``0FENbRtC^{Hl_NurZd9eHxWxe`?!u z_ z-}n8jt7AMuE>BSwQILu-^N_x3@yhUsY}WhbFZnrxt+w}T_uQsB074@0%nam{!Il6? z7@OCbEBI4e ztgieX6R`t9+-|C(2S^MS!+jq=f3e8M<`RSfEFAlV5;FjuIDv4Vg%tZF1{atXi*FXvCJ5_A$CX-&?;IMMMdx-+Pj_NS}I zcP+?fjTB+Y*~}{J3I_CtoMk12STw`I4RsS(F`bwH>uhG|4>J0 z3uWa)FLgZ-;SPf0U$OrmJ_t^zLOV_A3V{Kl@}FGvPZ`EweHe_jL#p{7vmNx|38?U7 z-vBp=aY_#_Ppy(G{s(?zu#OWZ$MncDW|bn=3%8`jVE>f0ea^CN;)d@s_!MSL^kyRI$)!@vIONLw`Lh--~=_PS3nyb;MsiMBY1c(%mM7 zZ%>L83`v(~izu9*oATF8^?0NGx#y>x%RJL0BDlP)+ACkq*?!=pd9H@V*Q; z^&1Y7Nb(EE71m$&^J33k9o(xd!M`-dGE8jQ=k;9AlEwPRZ+oJlkyg1q@57TrmZ2jw zQzot#otB$^^mOW0Pi@cG7o}4-v@N|?`8Dn9H~CjllM|ZXZhUKI?QN){^qPy4lwc{$ zJHh{1&4ow(8f#?rc#^GSoK+>q%dB(`^GZ9!>tL4AJpLMwXVjF|C+82yoE!07c{3+j zH0{>w*UIGs(=TqQU#l7%qyBW+!1=z7*{1w0kJ$X!^_Fx$t(?5L>AaMh$K}?PW4;R- zCOCv`@$puATBI=ZtdjnUH1TbUn@_U)KF!fn<*CoNY|*zMt9>oeGfVPfZxa%nvry4R zYxW^8TQ3eyfALAx0xs(UF*LM)Ed$XFYd1oqE)8Be#-UucYNg zu!XiB7PF)0UbGmH$YFkVrx3)o(F;bfct^BybailQ|B&nJmM$Lb+=jw|pMV>Z*rVPf z^|fsA+VS5>p|-Gi&4^E~X(=ctV7d=bO?4fvbaVd6W3^_$Ika4zxEP9Uu)4@!0$&DD z_iG&e6{g^_-jHjb@lB)c7qE^s0+T`H{Xe?}S0YLMAxVDxMUqUBB*Q5gM7&u0JHYGt zSv~1~md-au=+-?|V_g00MHKvZRW*fEh$uhMlWCT4s+(X>y9OohAmbjrOo!BL9H?r< zI!awK5*TqO=93xMebbF#<!O`={z=5%n#(Ni*cD{=vz5>+iN=awufL}YnAeycK(U%J z(lPO^{^QN>(BGQ|xXKgT9XHAQ4BL}uOU9a;cwT#NDw1wOZ(qQRJo6j6t!%04b$IiwE1)AZkA81aDGD(gM~j(zKR_FUn5 zuwMLmXK&NQ*i{QfcExThR@lXT!(LtehuQ@xXQgqw11FxHF1BoDL#6Wi<*g$m#dl31#7Ef#}ag@+(zuEvcSrud?{KYgmhn+NI1wzr!2MTm%#M`Ssk-bThI%Q5ii^ zgR+lCC75!s2CV*P&+{uE2)2Wvk%_aA3ZcrS`o#L(fCOcMF*hLSkPs76Sx#dhhJ6}~ zArl5Cz~rHRt4c@-v*2N&iIlATzO3FR+s}I(`dy=IM~b)vgx@K(v0Au@BPDdo_K5G7 zER@IfBc^HSqqRcgM{v< zOnT)gbo%0mX0?4^lfR1HKGO4ka?q;EBqj~ds>+8-mMiUPG-UcE5Dq#)5>M^0!K z6lqetf35FA{Y8#DL*kqQuC&T<74XMuWDdzShqOo>=e6;g0v0#CqpnAGRz zHG@*Y>BT6@_!BoS`MZ3XSbNF5{@sc8kKwe`xMVP?|$U zf=d~^E777f6;|0pL*rQiL_WVXKYFYK_lM$HBst9%XR9=`PsijCjt=psF})xPI>tr6 z4i!;^1y&bK8*f-|D=rUV2P&j$8vht~wj`*1mRi2i;l(1xRi2Wrl?*u%fjQtOZKlE# zYjPR(1z7h9i~q@{;TDysK_G`_BM4O~!s#ABEYzOqO8px}>LIEOl?ixXE1WL7~+Kk_pXn7h?1{GFkAqtx46fB4!&Z2P(1o4X~`ycbm8M10% zKfVsOy0ZV0s}1g`BL<9RwmvFj@QpzedKoT)UhAHOG!d0LhPaOS4zQ0V&=X+u^v(=b z9Ur+T2H;BT&>RPy0ZtJsV8j}PD5#$nci>PzbdWs`b_PFl{I_>ywiY0KsB@doDS1ak zsITDA+(?vN2P-|nzfR~s4Z)qkcSI3AF1CcC>dTP7UHZLMiHS`t(|N(K9XHO`VfQJ#CU5 zam1@|_CmWog@W7{&UPw^lzF$)yd^U>ul<(3(9zm;qnX0W z_S0=&pCp|bMS2u+=z|w8cUkcvu2EUBPr2hqAMz7zZpp5>H*T81!56C?Nb!flp1wJz zaJbfJbbRmn`MG08Kj+EwfAHvmShAMcs6%2K9FT|{N(a3FVhmK8Dy&4$P zZ&oVJ8|lV>_sg1(Zkf9_6>M$o7UozZrYU|f5*~@!~O;|mJ{9#6Jq12A^hr_41*Jbvum}cW*?!LG>!}jtE zseyqnsmC_ARlV0O>R3K&eolN)mH)_>0*3w@j@hiZykcvNjmI*1eW`_8uImo8T@on1 znBB87jNN3(yH8011|0l;hW8b9z6uu!&HE6dGv;&l`ZN01rPR4z_#LhmAg6K>UZ-K&#dqFY8oS|=P=>H$kwl1lP7C`Ds(KI z@p){WknZQmLvb8z2Xfc>jOeraPGUQ@qTS}<45>A@UtipV!}4nLpM`GVlZebcSD}S9Es*A4^{g>+EV(ab-DJtpR zgt~Z%&|c9WB1s>_HCk-ysfYkI0@QqCs;{~tubPCsG9UmdAuxT1u@ie?gm8=yU~pv_ zg#tKwWJRr#p9;{_;v^A!&~NFU4ig}fA?o7Bs0ndJb?k?m zN0j#(^Jk2kgWWs<>$u)qMg&}F(u&C~qEhBhXkFXST5Dw*G@-k*;tt)kPvko4U0DW^ z7=xKqzH~5#K9KX-Z)l$}05L&hZhC~zem6ApI#Pc=?xG|S1eg*8+@_1XmR?+9abrYu z+yIOEiAlvE1!8dwmqZj7uoQ6gSb<^@DO5RUcGEv4{O(W&p^J_&R#;V1E_v^+=AkJ0 zEi)c&zLO*(^x`p9u*< zRelffw+Md3@hb`y&M-}%TSA(s7C%<-ZN(fD5oL{LK)O( zm^vrtiY8?SM=Hz>D$NMm`f2QEgne~Y1rY8uk%M($)(n-95fl)Q>1wlc?2N)Puhi&i z*P&EkQw56KQhm7+mB@VjmMb6zM5S_ODqN@<1fe}OvSH~uAr{dK77&Y53;L-3O&2sx zZ2+6Xnx~0IChHt-XWWgZFu%*s8eBVFFg1qc%yN8bOkL%I0;0=Nj0Vn{s7z=)$M2wI z`+T?evJ=<>ruk*_?^W{Ts^p)*q?=(eX-70|H{ z$7dageLau>B2FGG=DI7A%2AC3glg!&6%cdkLq(J46noHTD1jjZC!$tl)jzFH%KiI| z%R#Y)NgWBxTN8e$_RJTk`zRTq!D)IiM56D+dAl<9etEUEbM7@P=XrWM_~povWk(vi z554)cb7@fJmL;pdg)LiiMgDMqxTe!lwz-0@7Ay!*YwTUO-}~qR{k zw!5WIx_1``8{blDllXSVH96;Dm(B=L3)$nBZp2+1^Ic-c#Nb%{+=Fv$1--h$<@Gck zWX2v3zt(5|wso{*j=bfCiPyMK++20`&b{Tf?Bi^Ylj={6I3jbo`0A8u+w{|=zH)1)a8qAe}7=sRp$|)US=JkWZpp>+Oh$;xK)sgv>_Sjfn zDOwZQ^41_c54G;Ib5oI*r?R#Cr=efaHZ7Lw3H=#NqNE=_v0JZqZ@tvAudnuGz3?k_ zo@W}iQ2O?qwk^EIp{^UIH9SATsW(nuMORp~yhVF#pRLQC)h{M4IJNCr7f)#Cfuky^q*1raNfj<`jH3(pX%sTVw0 z`Oze%aJ$QfH!G^k<_f$t*7&|)fk?<(qxs_M!V=Pxy!)MAwo2-}Cw(WbbfzyX&~ z2ZcdYsQsBx>@PbQaSos~6%$I{kJUI*NtE^tW8DF)nxW=VZ(eXXk?b8Ci}H4KuDxa% z%@G2ZnBxp2P9jw$Air6cq1`l*qXH2qakNdDqU7|YkCR%oL?{-=*@En%PdPAlQRyo6K7 zwB0`D=89fGX^Se)j$nDTq>3Tc6OeYI7_r zfMC=Y__Yp0I2BH{@btmuSSDt=L-2x5BZ3#Ym1vdwzR0BwmgY}luZ2ZLcz~aJYsriFID^= z3)(yu(9E9BVaV4S)2#uVvlyh(DmBnbrCSXYu+C#EMyFL$;{vew9{V~wIfa;VXeOp) zvS^b}w9w-hGN8^_|6?MM6;s1p*5xzFmR``K^NwabL$*Gct^@-?AWxTWT`R z9;6?^cEtDvP|g}LY2p(GecG+t3Pvyw8Np-JJ&<74qL2)xaO$VoEfi(~6b(%pm_pev zG^sg>V6Q7p@eq5dQ%!J)3LJy)N@OjrolN^mXpe={yL({6&0HIYI0a~dJYoAC5lA^% ziXnB_081R{5+~?!hb~7bv^h}yoSvLxf6nkB7@Q(q)0{LYU&h;`=|dMPQW0Cw8r%jZ zk?U*+)4~J-EQjbKkLM50z~H_}&XsqYkQO zKvkWXAHy4caBl(34q4z(2<1!Jhp#UW&8VnpWF_*R9D-CTJ-?y^sx=@|Il{sAut?*j z_wS{F9o(QSA6i48rnNu_EuJ9yW8f8u58z?cl$OvxVh9V)ubkH(N$zY#WB^Ry4{+}R z?)d8fi9leuCyh?TV$Xr76fN^HsYiJYJON+d{MIsHIZ@DL8*I9SlRzmyYFzhV=LVDj zONmG%6`PMsTazO-_pc;AV0ahW2lh2MwbntCl`a&-9;AD1SQm(U(GH>P0S3|4j{#z! zU$d>7LbHt;W-oVOY?nsPix*5<-+}0-6)OleiAk;WG$h~mgoi=UfK-gAt8 zGIh-;c2PCmOo~Hyk3c{dJwR$iS_ZrMkZcgshuXYX1zZXxR@HnEaJDZBY-Dadm^OK| z$l-qGO1C+Q{l3YoF?_-ex?86lnCG)<6yJ4|#gi80^ziReY~z2*)4lE`UM+re898rsL|%c$%`e{E_s~c zv8E%>;8^f9@yXd*Ap#*|{7Dn@GWfV%*R{JO322R0v^JRkZGmb+N?xMVLJLy+v1@!; z$8U+69EyI{sxP#>x;QqayHKM!d*hVen8NSV;>99NM9j(eeFbwrS;p)hJ=yhR*s-`V z^+F{@(X-}Xwiv%geQwSbzA{Ch7wH#giPo1sE?cdzr+x0qG0%1htU0V>-tsg`$1GuU zlFlvTHSLTm@C&B z`@FK^%xqO#?d$M}ee}7C{#mo$4)mISzaVptZRg~LN|PdD`;2`Yl`g~<+!xagSY43i zl$Vrj;J0ATHnr|>%8!rn{F)h8v&}3rKa$_~m?t;PD%tUH>i*r#a@a{?4^VlxvqnVQ z(C}Q^JRgg|Nu5?#&Ma$bJpOfYyT4_|vl}VTlwZGZ?$Ma&I$dbxZsW7JN{=n1R4%ZM z=bY^Ci+ijO_ z%@=;EBoaDH%z0Xwp=ne|^LAr{ePSI(stH-?`*-}9va@pCXZx=gyrzHhny^4tcd4lC z#G8pGn(~q|3JaU976^PPetIHkq>|;5wT+e+A0>WTDrWso`1oTp*;8ISF7FEr(pkj* ztj(1B@#{qcWa%%xTg^!%_9aC#WY`ks^NpFKIQd#Y*z`#DLtg8GPH%KLr<-#l?BRno zX1B^nmpuGL$XxqO4x7sa3%PJ zm5ou!n?|G}O)wTBg<%0G13z<9=9smvqM+>L9K!^7n%Bx*iS4%}?pbz^2=73O=TppDAX&zn-tgfD%Cg8|`O++!;-G^BIykj%k`cE~7P?!Rs6a=V?AMmCw9Pd-K3# zPazN9j$EM3jqidO%7|gO!kFSz5aHoV+1rU{qhNhF?qZ4VsSP3Ks0Y^y!f6W#)*xbr zRzw!m-e|ilAe5(zrk5^jGCHg!B4H#SKyhFI}!iNiLAbD%n9)AwTX;jMUJkys?&|Vq#j=!Ya3;vG&X|YPy{{ zx?Qx<=h48oZH%Ml7AWC_Q0%0zn{JkDgcLXuv5$z)XmZEyvZGFlHQsH6CAmP+9)(jt z4hkMO}l7#`Jn>7E+p%s4)hJ49J0@CAi)B1{QTYkqgp^ zsXUaK=#t&PIE{XkSoFb;KT#cav~9K5gldlq@EX}Kimx;%;kX%qb%(~>a$K~f{e-!W zlS7(}q{y)6;?qH@)WBq%phzU;UO8k$L*o}UXkaoK1S(mm^}(<(CW=sXI2;ViI~oEF@#+3Vx((fbl42iy%NBu}dOPiC1}<4Jnq2n^A_i4-sg@3$-V-#CrA_(d zNh>VYqHGb3Jre8tp!3x`wEBGMx~N7YlMSY4J;P=$Sy_Hf$?#os-hxPbcEd#n7Nvy* z=^j7TePck@qFLab=JIV9{XNfUzT;G_jm1@W0yAJ_3W`Oj^3Y%PyLU9uK!^wl2U8!# zs-YLyB=ncl^|F(G#H67#!)26133(EbjA4S1_23*6^zf?&Qjyn^Rx=>^6B}b?wGk@5gf_GRK*}Srr{%&{&uC{z6b>p4OA8 zQd1VbuBdoFQKpgiX?CBZ#5lLM`t-tOlH}VoEyEVZl#F75_28u9&vmQLo~va%xwyKL zBPhKJbx-~|egb$L{Q~YR5$chIIY4WU0MUY|JwX}trYmF7fCa?zI-KvSw2!uwIW+cl z+#2VrfuBRuJ_kCTP_9kXmzw(WL2F%SQQ7Bfiep3BuBlI(#WiPFM#GnxR-Nm%9=v!e zrD|8`G2o zh3oWJofEZN!+9`i^Xg_>-|G0;SVKFtQ^nrf0@r+AF2cM<(I2b*;suwwhouv6{Ef$^?T8Kmx1A^k`0z60P33kn7y)fG>?BKZm(cJLm$_#< zor>ieqngt16ES}J^S!X;;0*&vdV@NC9Kc1=nrA4`((Lc5Y#xgKL2^KVF4YKq<+d~w zfz_hWr2_B2_$RD{2&G)8f z&6UnIioD&ktW1_DB+p(NaYK6BsIPquUE)PSVpiK12Kqg?Z?3T7QdC6kM}EJSs*p7^ zzoac1xMmT3z*Pus-e8@lpsp5$dX-hEz^Fm@$ia>M8l6bTJ#~T2GhX(o*gtFYQKRh(lc{8&5W{XOxvq{Tv`8Z8sG!x$hty^WM0<3xjyKq* zZx4U52Y9Fsn5*LZ}2-rSGLu-fnA*Bu9E5F5`e>bOA z&Lwlx)a^}6M|?FHm#wfWb^qZPStuH1Ala`*L&2~hcEFSV_NbLiQ$!|4Mb*>!a2GXx zB=ncjeHW7#dt%%?$^rNP(sA^Mw1*nZ?g^^vk)(}rKO;)Jz!h)`I9fr{VU&%ih#bMB zeQ=1+L3qTpFxqknog^yu9f)Q)g!u{<@^Pd^+`0q@^bxrk+>$T`3LT(d7|4_5Q3OE< zW>5sr=$CmwWmT7WAkr|FKo_0O#MGaw$@FtD4~V7ZQ_!6uT?01-E}ke&5<&!4 zq5>8(SMX$Yo(APgiUuXPu9@x}?>D3yW^%e&wtDuSZKfZg&E7Wk^m-OCRb*#FPTPn| zXVhI+S_oADN<54!5rN;(tUKa5CV~(dEC(J;NVWP-Q3+DSu9} zyTF>hV+DZ+(a9mwG29=a$uu;zYMC*r9Mh#lmjW{Yg6!$JK7@~D1SaEPyob&<99LF7 zW70YZ?z4YoMummzi5^}h(i(U}*ldsn#?ZAZksTA(Oflp5uN3dU<*7`>JLR7jj z#K6n2hy#42{gMXK53;%21|rMn6RnpP-E z+O9=(nSNt8Ik4|F6>oCOpv^uoo8u_{dxQQx0fzDRT(!WcURaU^Cgejub%$# z*t*#|SaG@dlk%=;la%)(>E9W^5=e^+Illl@o?MQ%GSdxzE9a?Up z)o?7gXIcIdw!8?A^XoWQToLXl^?P-$A*mz8!o_M%T2zJ6{jIl!_obDc?W;?@Fp}L{ z)+#T^SUX9WcX7@dtqr9Yzl+cLH2GEehUEEz|ZY<$choy#TKSZeBZLqaM7mJbq&?;bq7``<>;~O zJ30q)(0}`iLs?r-ZWQl4-`RnST{k9-c&_pzVAoDxp1bTToP5WzB|07+ze;|T^+x8}W_Bqd>Ck0nz9-pDg-@F2aGr>#7=_oRJ`DA~Guk(_S%Q1zUq(Kz z1+$E5lA;S;Rle1wZ_~JUe16+fO9(X@0PsbbdASK8n41_^$OkZ&Rz0gdPP+J zndh>X0ym{B4L@e`>E@TCV`Rw!-J51@Nmnq6xKZM-FIaS;=w(8&HTNyGO$QXLu9l_7 zO>$dSnbtKqQLfdxR^Y31QCgq@$@R9mlj-Qv=yV?$uh$b(YUNwC1HzQ$jE}i&5-|?o z=__Amd@(ut!eyQ<6GL1w_8jjsQ&$u6O59p zp0d!hsKv#T$K}Yz#+gK#IDd`EI^ljW#kN>z@pNUb-KhJtSmIymK8>~L8l$M}bzMbD za$pt?x1Wj|tr8FzIERbSiIkcHO1#<=w(%z_24%vlUnqn{Q#Tw!?C?zo<()*NitvLr z@0(PIw??9Oe&Yg&eNtw|b4p3n^GbVNqOYAX05ASArg$u%fdqWK-_SJ^JXgJIS`@V` z%<#^^o>>qw&|-NU)_8IB!cDTm4Ac$0u1?qpM0k-yj%z1vwX` zwoZe4QD2Wz3HDHM&8aS?(7> zNbCLb9h6{t2z98eW8JxwaN}^U&HiX04fS_?q1;|0YpAzAH^vVKLlErYstrgS-4=zY zE|oYa?`S8=YSeL)L_tXWJ!T7xeUnR-Sv|=d|B^vo4B0uINmKu#P zjiO@0Dm$s606d}(GM(9_vm^)NLzU@cBD>OoY_UTHptLiBmsMq04} zv3$&qcb!Djj2=Fr1k;_m7XxWD78JcK|!IO_J! zWFn0p(PrR_i6h36@b)KK$vOzFk!S}f;~X3*4QeW)K(4b15?)#=NCghK2U1`N7@Xle zIk){)_*}vqp+tn_%YccX+Qy))Parxxh6=P{ouuc%NalnR3~WLf2>euW5{(ezdD_Ld zh^Cd*KP0TS3YZy+WmBmc0XAcc@ZwnwvMP6Kg$F;0y1}FjDeXNu>O0Q}%AidbmZq$_ z{K>vLSv=aU+in-2ja;Dc-S9waut*)9VhSChjM@)A2$?6NAxC8Gz!bPaGfsEP zD9wK4wDj|MlovtKR!_9u2PyY-U<~q!fo@%6ZN)Dkk7$&j2~*0OMASJm#)%`XWBmF8 zZAuDE`y=xy2BE+U)v^G~i6RqC2P*AsJT(LE_8`STs^gA-rAbxt?T_YnB-gx1c4$)- zyHa!Zo~D52{zv;COkvyj&Z+fty~N2DVPkzhiFGkNo*%0P9-mXYzJ9{!Hj$JBz33;` zRu@Vsbb39#WF5(Qv39Td*F{1>A+L;m4N{+YS6faW-5NQ`-}?NubL=lBj5d(lqN2^+ zVp$}5X}tQR-uqQHQx|;AIvXDty6`}z(ZdHh+VXz--0qhiem82|_-NMZ(zeV`U4f@| zrREwqZq8UcZ-@QTtfN({J)Jjg4djiOsaDGU?t?+*H>1sFQY*!*a--KhO_biG6k&E@ ze(`aO8%47p+fHD2q1ZlPNvKh#BwdWkgZ7!I!pT~JBisW$+d$5u4Gp=$Tb)p)q7d_r zWsV;;(S9u}ZVBFT?3Mg{#-OgQy^G7PBC9~JPXn#SbAiYiwSuM>qdYf?01^~rV7EcQ z3KG{N*R+FF?Du%T*fUU&Ket-flcREi&yjOjxfMt79IBO8rk&j+lJZ_&H7z8SJ`%z zi&`Jf*{zzIP}yu2?ZcUWk=HT9p~#YV7q8_-Zm<5u3%UB9u5Otx>^a%oxFdqw*}Qsq-?bc*tFQ5}IIHvWdZGl!Zqa?)#_hE{FsIt> zubs)CCN1UU%Wv6Kv*i={Nf`Jky7LG5fYZ(c1BFM zjfj|iZ*{QLPMsg$9|R?pxhC)Ou9n%iQ`z4@F!=Kxy;${R_dL-9_jtNwytZ6zaR<4e>N?k~NzMRV)4_5~AXc^^vZQt_1u&b$5Pb^poR zTn}WHD!!8Kp6a`{rTR$H8I^h&A0?qP9bfG31ZY=eR^8FMxQ^E^(OlUz#8Pj9!gOI# zwv6T9GUM(gYV#FFJTc`vem9?#9NU;IjPZyrIpU2}9fW@bET{S;syo3wBxD#6T!2w% zQtOy>SK~>K+G@}?sy~YbXrT;D*sMYR5PT&F4;~E8qI65kKw7wtBJe2?!o5G9wJ^JRbFo37Z>zC#B0O7r{=krj>*}R&n2XvX!F{c z28cDQCtRJNb7r;lWr240{zX>cUs|Ew&B5>s#BMrsg1sHewADIwdBer zHx8(32RsdInDq6*?v-X8*X&LUJbvxbs+O|xqJqEfBcDCbJGZ%;FB`W`adNOmg&dAS zs1YZjg*ZgEn-3eE&D!ijaacusfvE%pLjIEmDG-lI5m^T63Pb0vKA2+zx6&SD7uw|8 zoc+`xwDl1(o2ws9!y$3Hq2cN`ILb91!+;Y7z^?HVb^Aqlh8-ZWH7e@RlA_>>5x&8*i>?K`3(;nh>a$ zx2X(yXN(mcjKjq8vfT!jm!hXtKhJ|VRT7C^E%vuOe4d)r1^SG3a1B@y;qj zB_W$@^8|FUhV?>;4hHH_ph#PN7pw#afGA$$zbytOWsr7jFN!s=T|8XfeIC8!;UT; zJRQb}A5p6S!qhhVct}eZcK#@%G=XcQStS@dC6wfgoov5P)yOV1BJy0f&lf0T4wxj5=C*yFF?W`CL9D5S# z2?-8rDgdX@{0UhECSxYHEc-sKzp}jqx}!1`HdpVvH2JT^ql* z53dNQNo7={5Qa&Tiv={=A_3_-Kz1x`W8ez&4xeWF#!9LtRLa@ft1sNg~st4hT`v1uq&5Ei|J_Ahfq4 zZaKUME(^WFClnP}(7s-n;RdY+H_!l7r9~2Hs5M3>{nXko{Te;sgQ!~R-a8QEb<1Y_ zgnLP2*)~@@I5(-K=A>Iv3tvo!-=%zS1!2`}%nr`zg14vY;x&T`(l&=8&q6d`kk^4F z^pcy)kWwK)#wTl4_CU8h7?osD4hE~(Z0iFm9jsYLt0&=8-+{GImk6^Jbub5 z*hVmHVF!h(LkQi#uQ4r@P{JoH-W4qyWq3d@{UYqdGoYSDjf#-9VvKD~18=2^l^aVR zTnNY;RnhE;`5WxK-O;XXL?zHnu(7Cu=PqxDcP=GD$N}poyax#FQv0o5V%kQK(>~d_ z;y}x12W3YV0u87`!h-ihsM8~f!4dsPnA~^%)DK^1RdsNttUDL9DImmeS{U}BIEH@9 zVGr^gC^Z7Auphw_7*+d>_Z8Xb9s6;N@M+N@CjpaY!8}N3jFUd7&#|$ffOme2Zv5 zH}`hQSQi8@$>X{-AZhj>e4(fHgAR>^COT+7g5b_i60|k^NGzfTGj2aHTZk^cbP)-Ak zbFQyq+w@#(?w-B&UbkAb=I+18++)1uC0sJy;?+pWxVduU8m)Y%j^a7HGeO*5rCI2& zln%bV0Tap7Mo1~9DZf9yTQ(%-<5LB4gYT4?@psAfU0e7#=LbzLBFBB)6+cSoB3dp5vigl#{3d*8;bua%b;oZLD-bxOzi zET7LD&BA7st;MOdkJitJVsW zQ{PP!>Jm_IYv;Ist+(}3e2n6BuWJr2o;@odWBR&7WlDmSbbotvtLKliRxiioPStygEEj0 zq1L&PtDx0^K_9JQ_egOzwCzEQ)nVYR3q2$Y8cz^#1EK&>q5K0*#JGi_s0zL{B7Ss* zca_1k8Gv7Ex9)@KTz4hzebO3Ny}fx`&dLxM&)!!+UemOvyth&3&cP+S@6j#Wxd&1v zxQJGAE251f;O3BpAj8JHVSq-cdPYHd0uulz1Vp|9RZ2!CKSkvkY>03HPzjD2!s15o z0ob8|^@90f_m2vw)VPwcC-5eO1!M0GQy2&m&-YcYb{QY@ zqN8F>8?-#Ay#OMH#4-dp#R3~tl?RH^250mgdUZYuF=^sQYsVE_%qt`(%r>SRg18Qe z3c_%>3&YDGF5;iyHya=jPJ+My$sPwX8IskzQOc0>7QZtBwYnS)Q}sU#v@M0mGycx~ z38^&k870h#p*&dSbBjGa=QF&N^c|>Y5bY&$$TvVi1U&~u)JRkDiqI>AP@G68GInpE zV?;tq(@H`Cz-N3B!EZpeMFspQq-4c3DnM+Y4n!Xb=W^h3-~p}tscpb6TjDY#X!NvD za5V1sVrnaN!7=J!+QM7xy>D{P+*Uf*R!Vw6y&8-yaD5ELk}9~;b?=YbzaO2!+sqM$ z_tVo7Yx)Ktv_@Klg;nT@B-)#lfpSft7kCLY!9Fn}r&iMe|A&KexC{?I;{Y7-h)~Zg zRN+E3XJ}r-Fn$}=JvL%*fqp9o7BQi$@Iqyv5oCv_IrrmgV|RaU7Q6@y5yJy?YMy0>ka}!)FBX|?f+v44+W`IH3Y^` z50TIb=LQKLp7#PxD+cZmi4jt5&;~St4|a;;LaH4QhDd*RTeD>q#|GZ@m_}&pp)Fg# zp)PihEFCl{wh-|`cO3L(FD=oN>O)(O7;5Js0A>Zkzq-qGM{K|k*suH&sz61 z4X1SUI%6h6fggko6LyM3isj>iZ4$k)rVSS$kW$dXB}_K`Ogf&jP$k znPw6?=OaZmlgQKJ<}Bv9aePjUU1xw%g}2GXR`I=|3Y1H$y+7YMQ070buAMtC-0>3WY6(c^I{oBXK>p!Nd zlcRM61=tRal6%1;t7q=d_m@eIY7+O>`HzjQwm2tj91#(?$|aH%o)H6?{_OJ&Yx-&RE_j_)Dc56XUV>D600r|R{{K!rQEELP|WmCHZ8 z7(JTXtM9z_wqTBfGCNMq%(O8S3VW0Ab>7O%wWR$bd&uVV4XyKTsLq>gZ}xC|W=^rjweDVbdqP8>N$OG1prrN3z#R6f!XLIN=C21W+&=2O`754ncpogZ zP1AVyUOB#4Z2Q>rW5X^nAW{@`opX$<2#-qD2CsybB_! zmW)h;4*M<;dcasqBZ5~VWFr)TfoD_{?nH$H2p6bd)b%h$U+>miZVa(pjY6%`brhMx zcU+tYaA0}Eh5?e~)Fd>N0@9%&3%F52mY_U1G=YE!^37LoXq6; z+PKubv9G%< z8GFf*ymMW7{hrf-1wMR|U)FO(=j^;}S$*K)l9cyxyL7q7>{!Ja?bmvK+1NM^lQMye zq3adfrP#S0rB@!0t5e<76#r#)ww6YqNvXCdXDOTad|r9W@q*)8`ppLJWNk28v;J|+ zgk~+{#}g8w7LU{B8sSH=eHn{FItXPlT#4VPh!_d4fq^%#A|S+{)BPHgPsgQ5%yYWm zLfJwYiZ_Xv4BoYjI(5640Xsor9L*OtO|$wux>A^yHMplG(5#c1$1vP6Bc=zz6J|@) z-7!pK)F7%Cga^@TgWs8DPio#tCr`baD8#;U7+TU4D&{|{;J0Z;YY|Bss|ajcS&Lg-UUMhZn) ziMGfn4I@o64JCUdBeW#SNPE#RvZbYrhEmp{LCQ*@j`4q8*Y!Tf>Avsp{rGOl$R1Ys}iGD>?aGw1;kgnaxkL!0V5P_w=OsWs~xWIqrR- zv-jiMRsda7EGSf8vv1-ea{;i|- zu~+pNz1(~Uvv;oLNWRcmSYBFo<06dR^PI6rs3ITnL-vUuCD!Xs2&5vI}D7fMn4a9A;rrj)Hd`Wk(#hgE@$=@W4cHkra2) zS71@txf3)?UJbcPR?a+E*M4xr8pr)EM{RES-%LrLb-~^w@aE%PQK}mrHQd}-aqfG5 zXQV-AUgq0!_x6N40#~JN|6I%)-qpRpb?vLWdu@u>o-gVQ^D;FFT$mPqm5$p@kOb(C zt?QFpHegGiu{WTe1qw>MHiv?e7=)Gyt{waXY=$1o_L@mAZ`TcJcgWFk+Q5wRwV_YF z4vq)69d77zq(rgVzi?oKq!~ajhZlrDsk|LYlk^ubOeTdr$bNAwq+a25V2}y?B|(yv z)L|gmi19N5cu-=(!4Xgl3x=|3MD=-2$O*IapcDa_C=Z&$K=9g+bn|v4gS9!-#-RlL zUTuhQZOFsXFE;>sdi`s}bf%^cn{c*{??m}kPJE2I=<%p}PstmV9w41!xAYMWI9tRH z8ib?_3EY_NViNyNPApfBXaI#|5~)l~ z!^G`K>${pxCL05yUm`8wi{vbKuVMXYb z=>2thGTzloDI!n>C6qdZaN&ONxIn^A>ODM&i+}yB{%@FrI(saoL<7>ysGWb&_jc;q z{C`c9X z^5>rbd@oKiQGu%79}ErL5ed8atAB-gFgvu5@4r$gXVKZhO8S=8A9_;U1f2&ZDB-ZH z(v`#}2x9wmNDfU30`0)!HWt<&Ga9I_z}JCPEJq}9Oec^828a;_8X#t=*= zk`m-ZxAAI|v7uw^3f)qUd=O?y z(S`Ku$k9z%EO*Dw*3kF~kpQL+c8}+Hzl3I3y~iz>SpFb~4s$`+(8R4El7fij%9QIS z_CZvl`_upghBF?X9zj=QfkxEsbEV?Ke>6c(1vN`Lgw=`iVyw^r*FQ)_ae_oq;S(jJ zideERQUL4j%^_VAP}(NwVSu|li0WM+8#rA9Z?D3UgouZdIc_?P$&KZtVLvBh(MdQ^6G=oAU`#bkymwd$MJ+&Npthi>@htHn+5qTq$^cjXz!u?Rx+oC`V&Qjq))s+$ z@R#Vl=EJxRMZIE&^zLW%Jh@Po{(|U}$v+0dL;b%0gMxYi-oiTi0&c2^LwvG_A5`O> z^ntYYq_Bqiz#Y^fo#7nq^+NS8LQBEe>RApLOp$}szg|HDn3V-VbY-O^0Aipt-1#?i ze#^W4)=Qg|>XbrrcNM%xw_@x`@Tw-Qzkkuu%Mb=>!s}Cew=l!LFXSwoCAnq+52=Sy z0E`1-SPe_0RcQExG$p}9o`%95HDnDd0oU?=tWrxk736$S#s|ZVpnhbyo-`AHU=|WY zV#8W6<|lC=9~-oS55R$g*2B00@FS&GmXNpIq?lJ?Q0suq22aO)1w4D!C6%2l?fw)` zM>E5^FL>**Lp*f!;5luua8Mx7KOhwAE7UZpit?E5`A|*`J#0_*Ai{*rNHs}ofs}lZ z29it`@<`wfS{*v`(p?^Lkg8h1n?M{k=fnCC@eV?z8rX)F6M0{DX2N7^{*OaH-D@=0 z1XXNoq^R`$YggMdE}r5kAHI#3)i<~xv{K(BKC&Rj_J+nYBjJQy?ft?U=DNTC-1>BD z|C(Jh&7W~!5Hw%lN~=ka9RI~q$Ly}@(Q9`qT}BUA8>q&nFJDq%96LGv{rtmmu$%9J zY~*X*y~jhI<=#x2$aTs_Ht`{U@~eeUJUf)luLd};J-hRA!tTb4ak|AJuBF4#cg79h zdc9^-hMnqMl`F9cMGwW3_W0-I+}UZk%YWOXOvUxbO>Em6xJ6bBU(6h@5f`*+vh~(; zhinHYh#3BuyjHoW5+_8aq`(3W?i4}}0!I=sZtd8I36IZ!rEIRG>_1$K> zufN@VdF9e$jw@w~gu?iZ!`>K1)U7vI)^KfVO4c#O!xmD_eDu#P?Yn;5*~?^Fjy!xx z<2Spx?&{9_S10GRjenaiwfg-H)fIwsx=VPtxL)60zwzcKesO-G1y_o`a`Vrdtkstr z=n(22y7gOuv75jO71t8={tdzW_Z`D!j03JpJh`qb+)^s`@yd-=bC?UFns*mwr%Y|# zBDH4J?d7zQ+kbYY@|iE;wQ^LN{7=i)RaRBob9-mUw5C2dC>(v@kEL$o`O~T!JU6|l zur{zUe_+4GsZh$rM%FcC5+A&xq*F8^=43#HfS%FZ+qUr(iQD=()u zLQM%J!JnRs-j`v7JaiS-fbJcBh(R6r-b!#+`yr!&hCkGWkzp60E@*(WZ@}%#arGYG zeroqsK{w$X%d!26)gIU$QSZL}0*Yd=$r`mE$f>vpf1*=yl06K3`l{@(Qv)xA@%UVw zl)QHEPB|$8^&Xeux8S4}$@%?}he-0$Fh&f;hlI*do9rQAYm*Wz3I$<75Q% zwM$-eS4@So-w-XhFN*7{EXr8G{{U_wQ}G+_jsiMp2hgQVHSF@7fw@#)^J$YuH5M|U zDALLx-)42rFnfFk{-Dw=sWG4yA;<`;11NVlYqSO883v<2w3)w0koL`K)wSX|f2UHG z+Gn_O;6gx%#2tW>&k}aPUc3O7sECUB0P8qUxX63bN3t;p*%%K}m;okZ@!8l)NJDmY50?BgzPDM4KVqU`m#ahLGl=5(|`eSn@!6$S!nZqX1%Iava;}F z9yvvoCc;DvJJ=-+^r|0JBfy~Um;{sq&EPQw?@*WZe@|E*K8=?}7}0u;5*G*oD8Qlv z8WB5jnKFw^9>A_9dr#e$B_Sn!*F*>ZS&BxxzH0g%ak7_L7PhdrPM5F;(Q_xH_5DLfV9|>S>h<@nNnJ06_s$> zQhR95ukuZ%t6XK*o_N1ntK;FJw-+i7MF{+uZr#!xvSW_Sn$}2ng^bS5IaLvbcTVP| zXuY!Zt<1b6+)%nT|6ygD6m5!BLwieqoc@Z>GlO68=zbSA;!ah!IKnNnQseFY{`w}# zJ*%hP`(06+6LbH|-FeyTcIY0wQrb1wG4}F3uX7t+_~&juo}rrHlCQT_T;QDP((y~K z8ZG@2@v(CMbJDzE}lHq1= z9Q8$FrKoO2s{56xXQETIt|roCwWE)x`dyUs)7>Lu%d|M05E`iP0&k3ga0S zL4gJnrFa816qVn*-k8TwnCQA#0N7%Yz=5=7oeI|N&#^lu`y)W|>>!7%uNa*v2%#3~8BV5=;vunvm!n(<1SR)9?+{G(5mI7LbSL1sH@kT+|VrPVs}d}TSiRhn%1BqbFH29+6{)mg0!Ue zA!CvF>`m7_LVRLxN$tExTY5h_X|*<8Omyctw}o;2_CAe;`4Lhxb#*38Vno;P@|~sn z>8tMg)nbX;>pwMF4aQ3B4^w*8;}AIQda#J-we~d;^P0S47i8Z)^yOVllTg+3X%}{nNxB5L$}ajxsv_z^b&^tmJ=~;A5^*wZ`$_> zHjFg?{+=eQV{l^RuJ?AAi0hf#XuM^cL%bhe46vH9Th`+McgQY_N&JED?X6>9I&A5* zitjC`zd;!(z!LDPke~vpBk&Cz6y{tV!H?LtqSGAyn&crICx+&6Vqgq?Rs0mXLw0;A zz8(AX9SV9QteA%aEDA*+7PRdLUcu!!p7lwEH;^98Guh!=5kG>hSPB0Q0MdNZ%4oqm zd|fMYI+Poay9LiW)^|aQalq6gywUGtlALHs>z&R&77Nyi?9Z-PvbkgG&rQx=TsO2s z1X^VSruuf|s2%wj+MyUV-_(BXsfXzc)=Z|!MXZeFrb`?CI9o%DU|e1{OJ#ktZ`{=W zmwOozr-Sx7-+m1oo+Dq;UAqEuQ*n^k`2R##!&h!WMU@D*A<$qvi3S z+zD6Yns2LSJU+f+{D<>vi)!ciPulCDt8;dRieTaCZwGFB6<>9~_Im|1A&$1SM*L`XW@hlA55S?uyEs zNYg1_45Z>(ZW1tzgl-a$@picaDrtaSryd66ix^+4;=mF=mdm1RbL^3WSLmd0J@OMg zNQsOL9lH^rhcF!huMlkR1^sVHdj*^#cw&q+T8Wr$j zwMo_tdVv#nRK6z;7ODaH;Qq$?x@2boF+H3<;jeaLIgwwz$Dsc>$$7`Tu9nLo{`ahH zdmMkFEk*1Uike$uIUG`k$L&LW0DLn30JseMvXD9>eIYF&a(~WL3-SVLfEaZ64m6ZV z#gwI2l(V8T4cxVL?&Qcj*c`z1T6O`UFNYmQ^dZ+R;QrqV3(`B#Lm)l z8T%7&v0=e?lrAfgG66^Q!()=T%8jubvT!Qy10#t7D!Juhiy}G2d%W#i6)P>|a5#RU zn29no$PM8F(66k6IuX+&3)T%Lht5Dg&azwdh;IBK6(-*1OHIGS1$3L4ZB@c8tN9m2 zWM0P8?oW*V&UpW)*h-#figFG(6bOuss$fyD3iu3fuo}H;q7i_pWh9De=;Y6LMMpVYz=LT1Twy-jJ-Z^@@CECe~R35uW*j@>piw3w)8?GM| za5@fB1V92Mr43dg3|S1D0R+sCDFjRCxClp>Ni2$`7L^2r$qc2 zKuBs~dE-0cki-yi0YMd;o{(*^`0$JEcv~zv2Lqi9XAoXD*#VF3lLzLJScKRO`h;)P z*By$sPjXS)@GSFor zt-}_?R@`_bTDS>Z3mC=rAbZIkFQv}>st;giygJ%PN^bWoSdsfz_pyzOCsxk-<@dSk z);;a$bPRTlihj0!rSh7!)|25(%}G9I?_G?TTl2oyUHpvZrkUYRGy&dHhbXO$@yxPF zg?CJ`J3gX|6H^uoifnYM@*RJpPbqKa#X}PUxdiuEq<^(JYpf|T?e`JM=bP4qFMS%g zv!nFsQM*(}^_!!H+tlYSim{d+5Ll@ebZ(tX`3&Rha{Go<63w*opH@8x_u<;AyX|T9 zW~bqUe#?Ft9jqVixx%Z!W4XuS?WWL%m$OBbojNq*j_Fj@ihoJ*ijft%($&0ZVd%~^ z8(tcoIwoh@`}~^f?L~HuM`q{yAMi@QvEtlC|Dj`_cWcW2$hozDv`f&dSMhOV{3U*& z4$UA5F0K=MLK|28vmy4=`k=L6Jao=&oOho;_*O{ALH!y05A^`;!@|@JLYiXa?VAEzVvq))YWQ&3QN{tob zPSa;!={@aqsdrn>bgQqSN2gDjlWZLL%K5~s&RvJh!&40MxFzSsyfd!x?5MhwoSzxOr%!KBNj|*CJL1s5p8)BHa<>A8^#)s9w`iW)dzI(? z)Gi+`2d)WLaXYgT=ARemcV2Z}FF-!UTFY|UZwH-CGyy|Xo9}9JipwU1z1kuqYjb(g z(~zgTjy3Evb4gtEX|K!I+YKu}#Sa*riO77oeATym=F7OadJU%q886k_lW4r()zD&m zxJ#Pll@q+t+l(}N9OB+OdqsGOE0=RmOS8%>F*53}D+}QXv{I#o1eH!|+&@uRX~jP4 z+p=EEwlDW;-uy$x{`_l|t-VsM=jV-$-*Pu{DYAahH#Ygb-BuZ z;@BuR23aq1s*tcyWfgGyIO?MS%Lx=wv?k#adq|SCfiEdQNEcUVk#hj>0RA7m2762m zA$tgS)K8(ag8&PY?dkj3fgGL}h7!v5V-k7o(Xh#Lhri$`to@z=e?V!)=09fT7U}%r zkUIP2v5*7Y$!Dz61XC(Fr6j#0)bxPDh#%So3RxgRF`WR$_rp95>)z0u%7WNiSseQv zTo|mpNe7V*Z^Jq$M~#pMs`GyVSVAHKqK%3}@Fxdof)*oO1H5C^{#MPTq=?7vP?|mx zoM?PpKDe0iS=~*vbPLENb0j`^L^u;@`O;{IA|p6a(t;r^HVk4R@1wT&Q;88U8kPo8 z68{7fhZv1GkD)b6W05M_w^;fsyP{|MGHwlW~17o%X&G{i70Cqq!LmEZWA+$y&!RkPh54Ig=6iTVBo2)Z>a0VHV zzKjeMrODm!{Pi%kD)@(XWR@iwhaWjKxqZP3W=*%rCLl!gtF$_!UXXX((t5N@(HWsi z;1c{6bCo>P5VRQTwMbh3g5|JMb&?)6T4Vo@$IND^e?hOdpJc=EQx-ZRH8M;)%TzE< zLpTp^31|ku6S@jE6#^cb5rO2s^@p*YBWW~4V-SqwzmtMZghdL#Wil)aio|X$Ukq0>YZcVX+%4^9Gfg7vW+RxqiH1I|G zKR4569qQesU3vn4MxZM{9=xV)$+Y3r`{3lw#3_k^E#(Q#??`HAfImAb2^UmBeJb9L>viJitT zi?0ggM4qgjYR7n2@y1mD^TN7!OoQxCpE4%e4YuZNS7zAQEl;%Hv@g(D>HeNlo(b~% z#7#%)b_uS?uYP=IpVW?P^LrkhS4SdZqNfug?IddCgIK~&L4m1AclrtC;K;}XvJz4) z?ErM_%xghU9hr>?KVW13js2yaJT zL(H|a%UoyOv(&SeoiKWSN?pCUiS-)cTD~JbDob*eubHv2Mu>xv z#aoTrpRFfl@Gh?_&^6g|PH&<{>UVu5P&hm`pi7-&4r>1&ZI`9~e>v4Ab}JO2dRo188+?c_Nys&>)?`qEbbxpsEW5r75mYPQe2R7ZdKNopm-sTyv57g_Z%Sj47KPB>O z^{3tK2gEnH_7n-NDH`+_!OxpaB)}oCOx#PXK1(wFi zi*RMRt~s_aYxDa@pQkB28I5C<`wMap+#FO~)msqeGh>RVrNFZZmi>25O$ap}tlBC5 z?v33Wv7l3Y+V6LWPq@${`PwPOI-QQNT&RUIX(|n@6%3VYzEi0Hi_tq5G zGjol`->I?w5iw<6*2OC+#tKD(%DKv+d=+=~uWKzzR9vW=d~H=@Q)0~Xgji!{qqnm0 z8ri+D0+K*)Ma>rIfe|gG^aHIJa2c8uV}%@GOBN@S(8)UHm{kx)4PH)B@%Eacf*v1s z)tK!oIw@ZPM!A#C=U_P`fNb!VGq=!(85E#!ChvwO6o&ye zVCVq-rr>|q&*1n_X(Sm0?@&b-R!LzZ1m$3r@QT%JWkWwqCXo!*Pd1YQqU!=Y9bv;$ zG9?PS5|6zBK5AkR*$zk<>Zskal}JjIZ$dngf8R6~2S5+dD@eHDG|Zgl8Gop_{B1U+ zR+N@Cn}5OC5Ax`Qd00W&%}j*pJ%)9uY!ta5q}c2)bMOxo1-6*DB5Q<_GB_|$6I8!v z@3W?^k7+>Kk2u!Pk}J|FbUF&!Tzm(&F_aO08-T;MiAW$oqM-JL3Tg#BiN^ka<87MY zQXw#vz|xn{94Ci(;TQ%8k)_u_4HUoe1&K~!Sj?iPkHdLjGBFXJuukSD^GB&SD`!rVTWUh@4eeCe&cpolO)8vk(6oT?lq2ArayrfQH9l zUCxmGyX99w$irbzdkdAr!NcoNN|%o?HUfJ{6<8Jqaz(f9%vY)e2UtY%DV(2@$nXT` z-?%Y~X(bEBY($KMFw0sIn*yMji9GMpX7=+a@OKc5M?FwdgFsrqicsIN=7IFl^DVFs zNr6g7$wf$k7z5R!1eL zeWN9;dSR{boW+4m=29@05$(8UDhVsTy#|m=CfCcXK8u>>!MWnm(&m0E6n- zk0F+#-IZ`#Y#r?051TMn_=`uO5jYPYfX=YnvbawGZy=SXLKSpHz!&2SzyVI^6k;W0 zG%)CcS_;h`ZLX$ax-xlLl1R^P06Kpa{NDw`+9WwFbO^g{jTL0K9V)kt8PC4&? zqv|hl!N4oBWsWe$CKHDekZF8B@svc-biu5OnybIkg*~TStV|2K#d|W}7Med`IR8!2 zkO3f|iyv45q5&6x1yA+BkscCkOpa~hv=Yj8C{KngaKzv)OV?P`5%2JKY%RbpCw}Z^ z_+z$L2!O`}LH7`8{Na=j(vv|lN#c&Hp_etAGL@emX}181{fsTmmY~5|Xb5`&KcJ|C zX3L>m32UOJOvb|AagYh1Dl(XXYM*cieQErJeSc`(iCFnHj6G_s%SR?56;lw7M^sJXS#8S`HSFb!f_Vy z&x|DIY0k_te#AAyYU#R*RwBajPfHKim@7Kl1TM)CI;p#G=DgNS z@r@dh)lX4hC-?uYuOqX}M{8z;>EP@U5s?>{LpMQFr1br1`pIa?R#jnzwX&nTr?f%b zMm+>v`2|zPo3ID_>RHAPm0VS1%?Q!3P9f(wM3@}h3Bd)~G__9``}F~5g5ml&Js(w7 zK%oZ(3IYIGfr#xrkUQ!}-UGgNAXS9k(7;`qRG2WW#x`3-r1`^Cc-l3iv-?W>MTdG` za|yE~ID8Pn7Icch*frpcOEPDHFN4EUJk_lrFb_IEP`^Xr0g+B33i8f?v`_;AZe9?oemR6q&qS-VmLA(A^1U6!9yqzcfe==O%*;Cg9-Rx z5u9%psH^4eIgW;URZ%7~#y_T_D$)o{|KVQM)+_B9l9_Hn8G=v>SV|Fdn%!HX9_$75 z)cqwdF9>{<1aeVP7O7N#Y;`|5NgciNLqqqR=XN8B^)Wv(^>BbDr75Iwl^{RNv;iRF zuhts~2VtE0ZlYu7+CeHhT%OQ7e1pSh@dF)Pfd6wW@-PgL^T3u#&=A1?cxn+{8^W44 zx-Wc?O-6N)rlL3SXQ;iv;vi8Z6nPDjC?jD)Hxbr=NQ78Ral8rtIvxoG@EgH>nE&Jz zUNBso0Bh>we)46w@uzMM^*a7g#<&xDRjHrVy9oTu$gGWz_W4HBVcvFw7O30f5dPih}X5*z{@R0;-4l>__@%txGG; zKe!uv^V!1w^R+7`SFZiUbv*mf$d#x2at${&RfTEF-qW6(Ei9{wI&Nr_QWg>=JeQIj zv5n1)(h!zrQU><7xW_yPenvn5%llobps-ybk*uGY>0;Qbi2IM^PMrjZB7a8SKy?>P zCE+X>wC)J{42%KmClUwZ`fzG_!Cfc)LN7@NS*O|MKG`MSd7%~M!ov65KUDSnPD*gH0CqUDaC~-)A|jj2XDEv7&d+xqJf44QjvLf&SbU>};~COf-%yoHntu*W3K) zv-kD8!J$cFd4|rh!xY zpZ!1+wL3djyPu;!8|!fs*Y&_pdr^@e-5Y0gU>I2_%A>*<)&o?dXluS>F6~jqvqkbZ zzqqG`bf|`Lw!5a;oXtHADwFEA%rMyKA~{!d67SLh^$gjalf|U>R>jnZ&z>NV{+e%` z_KyAZB%wlCx6Ix(YR_JkZJMMi!moVZD)L9^(MK}7W;J@e zP*F*`q_XMxJrV@}v!6kV{+>8ls``PkR1wExCg2;oO8zXSE=dO zCoOc+un6?@NnVlAR35X*dG4Mse1qnjdkw&#r}U%CJZfZ2;t)k zxt;MRo7p=&`o@X=!gQzF2QL-1)%qIa`9CO)SDdmcY<IZ5Mf zT;#=9wGH#ir)-!VbAJ;te4>*OFtPUSAPR`UV-tzqP|`waG8LHrVib87Dfi*OH1n#7 zjPVx|Xa7Tl8bO^0z>~Q0&aN#Y?1d@_WLrFmT3AcT#00DdW&X$@h<-x&PbQPl+0B{n z(;jCwx>(NP>68_2sJPm{NX7uXA98@s5UkNj&n(XlhQ;=9|FOh{pC}36X#>}X4FSoP z9dim#Q5D5QY(QjBtDB4l!d~H;J$Ym*3qC;1#cP0hDWn6K$q){aRyIS?44p6mL^b0A zC^fZGM|lAFzy(YdjbP z`k?+q^bHn_=Ee}rzVP;qz&4vZv2 zE5TtKGRKWkqwkYK46XPM4Nu;sKnIXb_RO z0mNoMO7}Pc)XLKMxaE%C$m4Ja<#@NX5B&9KZ zV_g8r15^zF1Cy+nl;uHISwxL0hun(s6fwIn+K8+rg};fB^=|CmK&LxG4+zz0xlLGT&L1wb2X4DxZ`=)W);1^jrlbP|2Y@15O>%mA)Y z1^vg(NM`Vqs{U{Zw7sV+ncfKdQ(uKpj%(Wp8UZrV;Vcy(E_1jOYGXJKAs^jxUs8UO zQZmY(Q3wED1CBzj;5+6ci$ua*mT18D;FEjdYs490l|}l-a&^RVnlYvUT|&hcq@hfp z67Lf19O{Y#IvPGmTLrC#J?U@3P;gxQFT{Y_lS7?gG0v5sK&<6AWKkm;tT#x2L-Y(F zHDZD6|0E|O4phnUiz!0z(QT}2O$m_9MH5+KKlfX<6YY*EPQkDNO&A0$jr3G;qpOr$ zwy{M4QKk^hKGak~q4P2hB@nErGA3I~ks6VaU4r|_)Dl~%U~}RVv@;NuctI5CxJ&X1 znm~vFC#IYgjarjFVw#v%z7W=#eX(pi;y*(HfW#sNh-{L!j79dWImMqUITH zA}EwZeP%apJ7rkIWtG1HD!JMcPxAc51ve@;!U?7aV7IXXm;tZ|P2A zCwA_4;G4z6XBoO~@1dQs>kch`GFx3m@uX@_ajVyXb4}ach}_tBv?S*JkvF@KF|$=< zKY4LG%+o8JJGDbr@$Qjlsyol!-C=gyCu+~s#f4XoHXMHxF!(yORsXJ3_u4bu-bN7` zs`L_H%@z?EEeWj&Vb6ALHBZxhbizOR-iN4LVY60k{kik`!J+V!&;F-h+Qxj|vQ|KH zVU>*h%BE9cGoC+n3i(+Z_(AK_Rtt?NvGNC-_q*-x>=qoiLE4pB(&iL;Q_twsk?0KN zNcp2v2Fn{Rxjru)SI{2L1)EJWO24}EDDzv+IsV($r2NiSx{SzEk5FAByNybs(@XXR zrS8z2_K#ZXw6e60M7yqvV`~-~20qkvoAP<$TdOdU_o)HQGPgCxl|P09H@iyD(Wa$+ zURm@?zInCP%5*NC6DMdTfhKC!=Z`-9@q+7%Go5c{%NkRY4|UI-muIfMi24Zmut$iC zi#vP$nDPH+Pu$KwW;Kl7zO{zrl%J94m$I{c9jX{g5E7$t0gA8OS<0Up{&~=cyjW_ zkY;vs;N$v3p9Zc6ouMzKIcQ3c==w7rPn30dBY6LSu4Z;4e1AwYbbsCa;1c@5oT=g3 z_1$3u3y&1S#j8Wp2&|_$Y=mpig4Z99bkQA(-9l!n#WpNx zXuF&DqRZ2n|C_5!kPqC}F3a7P@Th2EvD#>1aH}3bmhgrK zlnvErYErqY@?J*qY6yl1ZQ@?R*qZ*Xn2XD#B;;5@E5DV4Yd3DYLF`2vgQ^7eBZf<}J*AD2y{nPxa=#JdLvInq+M+16M4br8D zfsoBYU7o`a2WT3bvrErNg#hCU>k41`9_ilheV(SsxL;i)UAh;(7pOUe7YnP@r@Kcr zs1q_v{Wj7E1yAau|69bxWttkpm?)p?&ut7>JYG*rwKZvIcNXY`MMXFZRKkCP7Pslq z9mTVS4e#;7a}a^;+I91IYQSp%E8>77rd<=deQ&zs#g^p4VjbaZ9(dmbWVCs3i+3~NR5cOCR;;NmYffA?=uYtz*XrPa+1W)SrOVI6qXC6&9mg&Cfo z%zzL~-IWG%13~sY7J6K##ltLh)eP>X2kD zbBJlZf1uE>Ne9I=ImepR0Dd`nbVOS=JK$r? zjIeAP9>SLW&FyQ`$2;;(lM3elkv6DeZS*H~X78U&A9ZH!cpk&i?WNqCXph`NJc}>J zh_4JQT>fR;;XC0IUp%iq^-%17UbUcO$=yQF<2Uln4hNUojVo~%Ez##)tk9Rn%n8uEClB}JW4KcK0m>rSiN-0gTcJ{Rv(#> zr7>&8L<&^aJ7|1dF648tB31suC4mdq&aN@?j8+sGdh)DZd{sEIWD8`;UcUEr78`he z^F7i9%KJA%!NidI#WobRnRZ!uhbASr*Ly#dR=0Pk5-Nqy)Onxi&MFy{=#iOu5B=Z$ zDo|vG^LS*4Yi|t%TIM%_#NaAl)ctR@(KMp#X_@9f862dFjGS$jB%tyUrlgF;(?BR z4{BtLj;1vSI!vL-1q1t z-^I#F&+XT9&t}u0P0~hYtC-DJvlQQ_AAI}NXMLq2mKyk9hJaOB*Qv2*(?Dj??zMkdYlcvQ+?#t3f_;@77wpR<==J?MDUMlP;3 zT$E*8D6FPCs(`iLwDk^=UIxx~c8g!t)x1(z zR`;|tPX1+W_EtsBrY^(J$XIE|v}->@%7z0g3@^7wnqOIZ&F>B~bH?FEp)ISVO2YVx zOY{qQhjwbbk>laBaB;4o%Lj{T7z?bE79IUj6Kv2LJ4-Hrd%T#Y?z9BH*#}A*XN}0;l(yL(_4os!`=&7O|jWz>SPh+LpPMD3Vc)%n$*%i zdqUyGlFC`~Ms+hwlX!Js-Fhn;;xID%?5i`9A$$rhCN)M}r7G(^yZa>WmpBDE?n#?w z7wgkBP;>M1DAz?h#-*2vv8Q6qu9+>VxYGZ*SLA6E^Wu*Koo^oV#Tk1{dSS#zJG$#& zV0r2jo+O3gq)D;d`F5HC5>p}rJE9*+m<_4~g()rzu)mNkRkV8f#@i{qJLlft6eu+& zS#T=@Lw}j0Uydgd7({qEw-*q&(;z@g%&7vQuR-iZ!?G0xm8K21~31qK()KJKU&h6Kaq=P*}BluV&~rgae>r0_u?yt`Pwtcf&(z z6SolEIh5B2>xIC8@CJBWe@REFHAa2YUw^9}FmHVI&!KcFFIy$ggEKB09d}-m_Pi`H zPV}1G(d9$54MXD3A1E=u>o1p^Jt4?&dI)#fQ2%&^P+HWJ#H&Ij71a}Yw@7h0=+pUj z9eGnAn_98cb8xcHa(=O%Ww!pCxj%Q5t~pR~q9s=3l*m~=?Z<9cU{iFMw!6{PQ%N7E zn*aP7xyI=nZ7T2l3#rzImK|F^><-EBE8{=$GCxx~EtvcB_@XLsKN!3qEj9#mQ5%7n zh=3HMK$;owq;z)}{$HrvfUauEswxy~A-+Hm0mIS|FAeC3@Ba`={+b0Ed2nE)DSZVtf?=S-Lbk#8KamzpIsl^>p@`9%1 z&Be7YH4-2ncJ~%V118eTtHC?awENltRy>q8a|^Lk1-1E;-&6z$7dij+2o8p@73vIu zKG+dq~1f-lPmz*ce*Sli7Xk>J}PU8V&h zbO2D2=GWC5me&o}1q|yA2pKr`8^^5@6t}<#N5eHj0VLXoYXqbJqya|+gaP@2nH<3O zj14ge7`-g7O2Xaqgg3av!m=?jpfv+VtrnDQTY%)DSqxqquI0IL0%IGmI9hWX7nhv_ zo7ZcZA=1l%xw9Rkm9tB4Ogh;xNN{E}HejBTX7BTu0;8?!g$;1=^Z{KNY{nXt8DlpB zz{Ml43Vu2VR{NB_Xn#Igw-1MbbZ|j+$lV~$`gTP%Om84I+;1d2WLD zN*%Hpx$1c>e$`TWTwEIrkA8g^SNRP7l}7&qIB$|!7l|ad>0-;=-5uTvmcqG{m)fEl zc#*EGmsS;OrXzT2m`ON02;gDkX28=z4eH>VDAPf{X-s2&Y8H$d$mg>iCq^3-=pg?A z1cOKcgvkQ&ETY&}Xg4Rz~7Zy@> zLm)We{cx>EU5|jUN#IHDz^f-+yLi-zO`{Bie7BhwF;Tz`7+oy&K&s(8QqYn|Weg|A zV=-AkQ$a}XI2jvs?kpy125)ez_)!o?%w5j3-nUBxHfbB&EXrsVJqKu-(=~s z6`;O$@Oz(OP_!mchVPS&95i!MfOp8~kS!OMMk_+zBWXe`r$OppG-Ti=czBGbQFZ|K zB-G`D)mD`bNFS#_)#o$qVQW%PD`};+u)W9L`g2Rkb+{5S4@U0}_HNln^kb@}G1!(WXUD zLtG3YIZSy{GSFWS-!KB*mTn5bcS9a4nJYJQqAhJPO7t3$!i)zFW=oe%gcZ0FN%yi` zBS$yg`6sO zu(N={s5*eS2Upf<>eYrwIsM)Egz?E-TrPn}KiyhA1*L78V9r;H+C&>466{FRu7~J= z!k+;TT`3y`E${?g6en7kuavzG3^VSZQTI-0xY`n}s9l;Y8A4zWSuDB$1L`W2jld;~ zgc=Ndkcol#iQcv#31I)#gA;fr5~0dg?_>#Ohi6rEw$aXN-5T7c9#b0jK(Ge$32Ue| z+~FgDq^O`Myma%~z(2@sprpf(Y8g(zm}9^gJE=T|Q;#NcTyzFFF84L91ENU5AoaXi z)vI zbCu5)NVrS^fT=>&!Y?Ht$iw%_RxYlchEZ+R#FW;-Woi`aJy5Oa;lLv>63DF;Ac1w? zjcjaA<_>-@WJV~;oNZF~9L8F4Mp6dk47&UR=`!LI^|mcIr}o##e2{}cj5j5}C9)p_ zF`1gVb}|i84Ly&=kq0wo+%Fs8XB5KyC-#FYRBmYFAJAWwN9o`aW-{YuiGu_~091Il z!r!Y0CP>#qDNH`!Q!5=?r#i&{xlKM0Uzo4W?^(4`Cu9BgO3g@04LnYLstfFPO1hAKrSFH7V_u39@sEQcLV3b@+uGyO~E-p6y$T5PqSfN`pnMNuKLfp zC&kU`>8B6IcYXKIdZguaw|VWaKpEPbNqhprfA$TWyVUuxTu!_?LrLw7YMOZ3A;$H6 z&Nmj9J(N?6dTL^GQfYUu?YajaBqYmEZ%__iHeJ{7ok^6)5>Y|9&~-YOGc)Jsh-WAC zxWuj=&f#9jn-T3OBRk#n*rZGP2E#YMUyc;BG(S4I*6PiE$2WySf5c{I%)8+fxQTu~ zb-e(PmtHKetObXvw(HPm&b0XQ^Ko02 ztuEW=zz2sl-R3$kIrjs%t}x#uL$xv;iM1gi+G{1Rl?6;p+>kLi_~qlfbDu{YC!caO zqbqdWm5Y7YC)s9vQ?NYF`b4#p&9}voflwq~?!I)-*SM3jdJYD0uWDH_LqfOEVcik` z!{4(rY8G(T^u(UYO*dI$`e}vhA|>k>pOmGm^k0h4NxZ%@^yu6d*cjhZwxhzi!GYzA|%$kHQv-qdOUp~Km{7=sP`_G3~xQ9GFrLQnoj>jQQ z|Jsf9o+C=W{OdZ^U%m2PH|eZ7|Dl3S@(Y*Un_Q*BuemZt!$|V?vt27a`q~si3ST^2 zZysCy_;Kp^3`@l(A-n0zxEHD|EStUVn#R<9(cV}0p7fgB_@#u)X=dsdF3sr4qI`>n zB<_trac#r4XZ(@UT&s?@M7nPd{~WmdT*=m@m7&HZ6l>FbA&+TKdfy4=@c6omLF zyv#nLj+e*q1I$z2L+3Lg(^?#2@ox?q;2yxP7}sr0}L$*G?8hyTxDH{r2{j zYuXJ@|Eybm&A{b_{p_9g9Us(8=UNAH@oX%ZXR0d{?xMIme3pjGr-`CssHWd02-1R@ z{sxoW@h0?tv?`&H-$3^O_y0b`T$QGCH+Li5=pdu=m*ES^!d_;G^b#ob&(WQ8PpI2n zs7b1+#dAZiZ1G{;yQ<{R*fkF3Ue~AX_~E zYX2jQ?SsRu17%DAFV{~5#t#a3E@<{E74@{wl?F#v&8x{Q(rIYxWfTnrMMIUeY#?BO z*FcI_%9z>QMAN{<;^0wm{>vOfdc^c;?w;oc8pHvLU7v8b(a^?o8Qc$O)lN*Bhwhw zU~pi7cM+rbQMb<=AAfbfw_UI!ZG~Y-kGQ91(O{2O9%Gwy*;2s7j-~Dp*goFug7=@w zH1!nF8@fN<5Hm_WEq3JLZ>~k_294!wjBNzN^m%(^rsZurl4^cZ<;sc0)1?xQ^7d$7 zUCnd(g+swy`Pp~3+CH*2?6TOFrJ+&Pye)O{m5OJNRduU8w@e5#GSpg3Tbp{p`m+(A zjF|jd3wfG_K?uXe<;9Tn!Fzefto|uGYfvk7Elof|sYS+4#WG8B?qb_tyd_&lE#`97 zEZ`BNakpRKOKUyqYCY?At;yRZJ{oedr;g5<#e40@4`<#?%g)^kO-<(Zac{ct+1Kp+ ziL;ArEq(SG+3a~sQ|^dLop~bX=dW&$kEMR2z04LLg~ICCVA(FA1I7=Wd&F)H6b%-( z_yGf=_y=^Q8|cPrv}xi{Mf9K71ds-od?4zL=BkGs=ij8e(5mU!?d>+X6KBzBj!O-X z?z8%ykac~1O3dq~-+OHvWdCtXqMtFHJn8kFfDo5oG@CZ-{<^2b-V1>?DEyoq5Hq* z$MPu{ty7LIP+sFdTi(xZbcv~;^6#~Qf%MG+{I9q?$HfK-8x9tYS6p+Xbw2O=^S{Iy zol1N^eG4z%^08XVTTvSunrE#Qr!`?oaS-E3R^Ht=(t=DiA#0JZ%M1&Ai)%}Fc@D<- zpNhKttoC8Ee8S;8yI%eo-h*wvjiXn2b#yn6j(j^Pk4m7RsElAh_|sALdYf0H2p9y0 zmuYL|`y?m^2ko~)B0`2QP~e@<^E~L7x^GbIlP{)Sow_!DX05@$)^507{?WN~WvGB* zjkH*a9;3u_Mf#SYW$LLv^A3M43A`e`%;w7E>uMuXB1wOO+DG^I-4ObHCDf%w^pJ3~ zsh8e)PnGl6op;#FRC*ds2sCK5V7A)0rM_sYyBcOzp6U42Cpu$maB)S{-9xS_Maqi} z_<8Q`DiyZV)YUK&n8tUAJ}$K2TI!I5X~&z0ca@n7126SdcShDt7u?;i9iML0>p6ut zdAn4vLgc2!G`HpS9SXkJjFZNBZgXY^k9N@(s~;$!Z|OEeWkbY@kl3Ikssx-X-7M7Y z+g&8Zs|OIUjaQE0+$GdgG|=nYB<7ytsJjZ;`rsGg3H1$W{f6ymYRizyqfJb>)2DFYA+}Im;xc9 z2c8=G{FE+X1T8wfsJYh@QdmYVLcBs>pvN$PE9ScD&I7T{K0brHo8)=|hxl|A8t6_X zj|ZCl+hTe3LhDi53@&4Z;*H_hXX-=d5j&vvT*jdSHTQy|w0> z-Fw9>>#PfS${gmj!EtHRk|q1IBya-8R1><5!P!$~qT4ze*{ zbuqaIpTjBS%+s(1$h`+qQx}ELp=EN zbx+W68;YNQ7%A$U(Ww4m>L5S6*355&eH*5XJIM|n8&A^ZiTHs*zJ)f4OHt-JD$*taCEAFwReqCY&l_ldxc+F@Y3#fr2 z@`Wg1xJ^fiktl_Dh*BLPG$C+hMq@F-v5Y=`jOPXajj)5cglo*0^D=>1nnK9bZRQ+$ z#M}WEfpJ||6{Qs|P_7TQI)3L4&;KnuL>TGc#f>x+ywoM?waIB2K^gXTq~TQ3Wxe6C zS?O>%aW9+TXC_m5XdoshW0L13DBo@4!`ty&A_8wR(dRwL-|G;@jbG6}_#pf{?(y&p zG{KsP6UtYNq4WF)`EU7wC^5}fjG$7qWC-j_-CHj=@#(M5Tgdp0kNd&|?X(1$CUs@} z)ntZC_`p;K)O_F>LV9!%7JqyEn4pVxZjjLlrVqf0572|(DLDIkXMMLycvKfROW!~0 zJbNyLPEmFkO@GzOzQh^}m-54?eKB8l2FhXK8KAofMt7rP4xL}8hcVtLCGCSU{8(yz zcgN@^8J|&}fqFgrwt(@@mnC4ld26nJrHOm+_FSHR9LHc>lA`?9my1&E1vL<3;+6WL znuhYnYf~@0%f#qa&!NEGU?5;yL4C%MUe5^#hDHP~@FyrepCLx$L3P|X$NDAc4pIayfx*-NNhcKmqD|Kxwt33w|Fw+8JMC2L#nR0O`)CVk7^ zaJBI-+2Cn^oKUJb$y-lAz5RLU+s^jYgDXq>`;wz8PnV27lL^qk;iT>p za+Q3NX0e}JM>=GsORred*XH&E^5ssZd}J${-FHp+OwX+ zxcSLGiMw~7^LXVlR6G5~8JtE5Zb?IebbwI>lXZUjid_!R_pQ}ClGmhlq^)Cm`ix(v zHA3^3>Hj`5T-3l`o|{`q3|$*nVDp#X_h(iwtUmkx%4OhiIO%Zf5uAj0(Q=P7JsjJL z=_|EL|C|5GB;dUb-Q5<>y=d}o*GOQZHtAN3>w5jiJ)fjLeM})&*|o)p` z$6Auj4%XgHJI^@MIYW2Vo&nWO^w3MOd%kQ84k-Tq-;iIhlItA_W_rKnW!GPS3P1E4aeZ(bvq~v8)9A8@1 zIe&9s%4N-E9ecfWt%FRpr*C|C*Ur?&FZ+1AqFCGR5SQ3F=SscGhjKbSG*g~zqxbv4 zF|AK37Rrm6YMW%9(l7Ffx_fy+fP* zHhnHeb2CTI=uRG=UH@R8f{jt(wv&Y9)U28%`*o!+zeM@pJt=q^%KsSsvVO}Ft(|vZ zo_8(NhZM1!o`&xuI#ZRW6J1RpJ==(uo3kvKfmHJ84%*P$0!e3PeG3^S` z=8xrPQOFZ4cGL^jSmdmO?BGE@H52MO*u~e0#&~iQq(!idBgSGXM!!OsvSGXmV@B`^ z)d9_uHGEkSMJ$V}Vb@MDefMV(NIGM=E{kZKL`uhe&=#IDVR zoG5LhKU29OQgfo!SXU2nI$y3*Gc+s#FTR6Nb+c(S3P1p3U@WGkPMMRXgenrkoB`nqCoV2`!mYP$Zj;x`u%23}q2xMTL@qmqN2xKgcZ` zNs-ONjU+#!CWTzj#jCPITY*m-Z|bSjsfYA+T|ghbk1wI77u`Y2Ze=_M5RrY9Mv9-m zSY1hKX^VwVW$ppTm$8*JmQqVr-mP_hjOss&YnZiO#$C zvyyG&zbp&cpLRN`otpV3(r`>56oT@cEMLhIphM~`DUKPz^?%F1mGmIi-0Shry&kS7 zjRG{I+|w;D{(9XuxOx4aZL&|_j+Z5Oyj^~K!@T8(R~!u(ZoeSj`sL$nrmjnXTihp# zkKeg1w*0uC>CQOI>;-J)FPz2Xm|vvc-*t}30}e#S>13+wqxT<1_Pab+H?pmK>iqpu zLbXxjvYXvv*NfJ-wKs4+d~-Uv_L{cQ{*#mW{iN{|G3S02oKtFG5$CZ(CfLD(6hJS) zxB~llS<#PZBI;c`&oku9DVi?c0I`_~Cc9%OfUqVfcnu2Pr`AYFv`&^`-kR`L0(ZNEQ(mqP5P z{a`Ia|FiLK#T&e6g3hEvCIQjij5|qk5iSu@OTU&_o{~1uXkKBqiK#M_=s)huesn`( zf-~ne%M=A{+Br^^2H*iDDk2dO%A)@S`m_<~)4lBA_%&22N?`gJ-8QVx%pIa4Zz^8( z92Zy(=$RW(Z=kF|CXIEzH6$BKP{;HJdlD{Ft!x*>d@>c>uk15iwU4w|O~4lB`ZjBVvX=6 zbKZ)p2Nx(MD#(BAR%*!X=Xt9)#zR*?JfHrA=QqI&=NTX=ZSW0dr0XeJ=HID}> zMkvP(LVhzQ;1cTmqX3zWEgW9OuC0;@pQqIf=?2(2~Ti;~7MzDg1N$D|ZYfVQqSV z19-#elJh?Qp=qFllh;Us#|VKa0_X%Nmnf=r1LrVFZp8y|-pdZsJID_Gb4xx;!WchS z(4T?oIxeI*3eG!}whg?Bav#d{M z2l0h0V&!b64qt+wiDLjg~yHyYF>Cb)=>BWK8jP8ELv6$;JptMjb07Ptp zgebqzLi%VcsTn|H_c8suj{TUZ=~rjAkd777_kB2#eO z3aT6cd7)fmB#XNRC>5`S2B$Ths24R#q(G4lZjY)XmF62BH);vEGbp=Je$hK26k^oJ zV;S&4B1K-qXTOAP`nKjFkaR`NeN0dGJ5!9+qYWq;g_R!TE1b2^69-zy`tJ2 znC?D4-W27ADFE&pb&1hyGT+EBI=a_SrB^r2y-5LXL`QufFji@upklw5p#|~N(+4SAm1SbA1 z6a#C}0}FuUh%V^oL5xSGr0!%(!}@M`gYpjO3u{j3>^t-UYDvXVZF^4g zj|;^co5}h|3f{EElUsvnsRn&giaFZX-KD1#{UTAJ%3f>h&-| zS<*PFL?Q#5#&{$|!~k>%MS6fBV+pTB|4*VCbN=aS2ha1TC8dk2#twl=LdJ#XKc_XE zosG8~c^uWn7#TEYp2)>j$x#0%!%>pNPa|*HEj8z&MUafFdCFpvmlB)xC8>0^ljUWGOk z&1a9xisfn5{RSrk>2boTGp)Ln(0+MZ(9eq%m8vRqSCgd?kkF9wA zDJv~-ml0!l^}`nPkQS1}EivgnOKF1}N<}_0S~?EVrMZ%UuM=OSNoV`*)M#oO;}9Qe zKFnG2J9v7*%b9CliIdk{O_5CxIs5HKtZd)24=3q%Gt<|twhc&I^f7wo=!Nj{v6FR% ziEHW(?pjc8f(`UY*&jH}-*ZddyThN(t8X)5myylkp0fAW07-FGi$xCa$w zDstl;2}BfF6}t?o#zKm+a8zGgMc>%8(^K(d7gfl`&w+RlJW4e8`3W4_x zLg0pgIrQ#V9F>m7pJPU_h}1c9%kSxaU)!g)i5@-}rh9v%P5|?v&rR*HKAU7IiIsXE z<_c*i@*)$TF)k849g}+2uTfpbWPdoIZ1l}^bZ_kHx|UHQ6`&*5gm0o}rr2o2Gl-dL z?>$e14G2zFN{jUbiP40X#@WI~*Q1uLlS;3(sJPq(mwR71KBH8*WbO^&;GP_fc}7L}$2GCa$tmT!~7FN=Zm*sEo%l-Ncs0%4Ljd0I6bXBF@jhUx7X#1ln3&tTxeaS?}V0 zGdZiI>TkNZCtMnvJwsA|j&0>t^W&FopJ+^`uOUcCZZ5BQ5ucW?wWDL>(!x78PWI)v z)}_eKNIo##tZY(G`02vZ4V*Gsw@@hJm30M70;dw~uPY0wVkC!4W91nyeo{RfRp_%X^_m4!?3L5s;W)j=#-(>@$aRQU2qzeir}|GJ zTN?o*e*-;Z&FmThw~jn2Z4(dwN~D5K3L98CO2B>cArwG+4ceDuq6@)ZFu`X@>PgHl z3dVT@L@b^IRlcxSTmnEKDmYzOQh1Ahv6cZvhwI%zQ4=s;@UPcOHNeHr6YLPq#E@{} zbEN+STkAWEsE2`g8GeopLBx=HQj!8`0O;%7RQGAFmEH zxfc<7i2z?PZgcUlYBA8n18FHbFy<+kv6c3K@q&FApqQ8v?L0f`4(J>btRh8fow5Mj zMv^_Sf2j8s7CC$cMLmdY3n8-gF*=6g_yZI>HvcSk8T}dKCBQEZlr^RlB?|dokp};; zxX&0Jd>ABBo~V^e5CMWiBv1E`L;8D>akv2~1OxLX+VCidsN#3g>|nhuzu3Y23v?g> zkPrtS0C-f#4Y6NzsY)=i|*p!5Q7^Uk)7_ z###WX$oTV9reuZ3aK-ZmV51HOK{O#`1M-4#ZKtUV>y&|203d>p!y_O8QfWunq1@fH zLW&MZ3Ywxn1FXH9+gRBw;5PYH#`9JR`A80^e?@M^P*J5na*U|yNe}@l#cqWVQXc&k z9e_HwPI({>)Y`Aqz^zZ9ed1{&^kbzCRtHbV7-$r~XTW4nA-%JTYDB%}3HClAWiA2@ ze|mTT-*Q2U#YoankQAgsh|%Xi+ZI6qAPheC73(>oEz@&+mv z4E#>qc5=+C+>W#=y}QcJ5|@C(iC+_oGogwz$b%{>+c<0Kfp?v^rp=Gu=2Ep&>rC)s z>+mffWZxWe%`Pb2sj)O<#=1I{@QSk;ze`BZ3B#&?Ey{3r+;H4-e#tpayxDf#d?lXC z#WOwyN^kY4nqMDa9$m8T9H zKS|3GmYZk$nqRn+c`I)CWvq!>P~d{)JsOU6caOUjWG`4C8Cadb=H!~?D<6OCw|(0? z*)`WT$vk{$T`{l0@OsMoxnVAkmM-6ttNY;dZR&xz{cA^_eHKvf4a(DT;?nzqp0EPN zrDc~Yrz;sO^~?+K_q*nJWhZ%LDLo`YBlptWUq-)nG+aC@nKGz8ArRgg_U%qK3}>!9 zm!o3n%f6t&~^u2)n9ws{+RZyZ`mr+7xgu4)6U*FKWw$8F>%#0lU1fpE^sR95s9qD za&iRv#f;U~!z)fmt#q9uM_**>7K`)N7*Kyw-m4S7jiGk0hn>zxIT|@Yz0#} z|7P!tUFwC$%^IAfEpDzoqB~mNr1N6M{M+j}?LBT4MlW*DU*Emje0!~3p6$2Zn}5|l zy51M_Q-1ZQuq%Q^LhdEmITw81R$V3@vlMsR7w}t8D$?YF#@U=G4ZTE1Tx6V$^|OE? zitY@>EG63Sl2==_h(2@A@2@|-YTuF+9MOumpOau2_?MENoZ0GZC+cKsDvd2=EdRdh zZGwU&Ze6(Bx200NS#}?02R4p`Yg9Ihx30&1Ns(L-JR~nyK5N=8Q_aC%$49?ZNH7h1Jobu-6JzTmNx1fI7E7x!R_AYxX44+U4 zig~oqr)ANOU6Ii*0(ai@*S9+aY#DvVmS8NQ|Ku*69fAjKSLrDN877ZDc3d zMYI$!fH!#FKecmuK_0q_4BHBSfS=@o-?iWwG?E}ZaPcpzA$Zv^NmjQLE*d12LaiN8 z1T>JuG4&w2iaM7abfCUTQ3huTp&#`%HnCfvi{bawCXer%MrVW5uq&1mRRJH*!90SC zwh4KZXe=xwzj|f^&ERqel#WUvp*{Cs@<+xFFv?} z>!J6E_D~9;X0(Dfp(l7sNsd)e4hti9fW$}}Rj9{-lOZHq@yKiBoo_x`m(Nl6;3@_MT+13Eq8J0|> zs9^r}JeheI9KRomq+P6O0JxDW9!5!~la(4f4XzFdDYrBz98aSLJiy@6cEZ4)G0^!CRT@2lrNV$O@?~x$e+`euyA;q1Nnte&44F}) z8Q>HaF+%pBp{LCJBe{|ZB|39GR_f_Z-p-qqAJWF#6W6`r#_xNOVLenX7RV$@S~rXZr8 z8Bi@I!Gx~>kPaX+>F|V)s?YT6RD@GinPD#li4+v>b`w$wYas3dUhxN$j;6c!&X@gl zHFgd08}B!HE&Tp(Y6g!?`fXo+);IC>X*cb;dk)>Zy|MP}@htPrBE(9TOa#hfM5~WJ405<|K#o_9q$x6z zDtu!TAsf5t?wR8~q;dT(-=aq=IxQ^vLK0=SnDte@xU84|G+~8yqar{76x^}>L!S|< zm;$4S_NMn$M!F&7DS&q@d4zq`g%f@7= zh%|tDx{N7SMXX%*K*XEf7Ex=rYG50XpO|{28@L84 zWP?xu;xsY3r^ExE4WJZ&HzRPL&qbXEfKdfZCxo9`C<-vs2-B*wh)^RD@{|4MV2~{v z7IG0}x&H;W9Aj1!d(+ou0VGu9RvU1P{z9RkEmG9#4bPz+0rZVVAb?g70Gbo*OYBnK z?#H}U0E|E%!Nqt!wq?J_eS+Q6uLv0SJnvN{Xp{CC!=pViTNAP&x53D{o$OqGQlO~X z9>W+y1s%Ys{R(Ypo0H2j9|a*ufqK7#c(1s!R8}$K1TOi`9@?kGU-)nMTcaHeh0i`UIXCkZQ)534bsv$(G4haoF zQC1}pL&^bLK{qU(w+JX#H$t7^AjF6^MhCDT824pN-!bTuL&^{^X z0RR^a+a3DBG2ulF8qv5A+ADy_z^lXRumH3Qkaq_m62qnuqo_28_|rzwOEa1}Wu_mw zb?x6OVqjx#u_|I){)?4XCy1wOZFT)yrQJb%*RQ}tvy5$rg0GUpUDc2N{b4L)Y424> zvh)u5D6b7tF1x;_`2FQ^^0KGRN;8{FdS_1vJ3j7xlm5tBN;UzvWR*@z&%3gq(&+vn z?Q@B5>XP;fgKr)Yx3pewNME>~p1i1MR!GS5C}wx;hk16%`7yNK?EM{nYOA&tt(qk_ zJL=g!&v_#?op&$23v=G`E6}@#uyCl&{ILHlpGo6OQPFX!1*%Dr|-F4Uf8BN)iS5vJEvMCEv zmBZ}3H3mA)*oF}I$9-F#*tW7qa`92I`L&Cu>o@LlTqJexuCL=e>5~k#Q#&&*^Ec^) z@A(>opO?0N(At~6pESe9NU-MGh*9(WFQFx)7Pb2qkG9Kq_`Udhe(bNmz5hE88*$?4 zi#>L}SD5eg?$?A_D?QUy)$?lA2Km_;%l3cE-mwf)1e~1IMi0-g*A|f_lN7F9=vwO` z!0(8jYw>$_FJ8YjB9sqjy^uIrp=V#AbL-xfD|;N{ zC;$FzY`&nr=I}E7J(-ty&u`z2NV$YK}kiuF`0|g(m4v!>k<|n zuRFDL;kK&LGrOMj?ckX|yRbv@HEuM0ME*=#a=KILi=duQP1dGH+d^l_*_+gV>hin% zsj1J%>!ynb+e)>w_~^LLJEHtq@@KDtn^LJk5+`44UGuoAf0tQ6Fw0SqTv544O=~ zK9GYLl+maG@_X`Q3lNDy7Wnwufw=elBQ-Dwr-R-dq5`h%Z2uM}$pU)<7IIW)S2P~P0cKgJ;DE3u%-3p=m{^4x=Fq-cUL(w|Fcse`~8Ee&FV zO9CXPaT_o?XG2M7$r*T(fSec!)pWtmhN6;|E@gc`jksHm+4B;r2%T z*yYm3#&>p`*UvQz^9|gpbyy|8YsQ|amI+QRRKwz!u|@($CvQQc6apVzFb?p>day^1 zPajYTC!NRP=2vAVMq6-HyBkK=h?)>?aJ-G30T{!ms2L9OqgK5mtKQk%;*mWh`e)DS z)l_yZBd*9eT}S@+RwJom!j^O^X}nm(TSq_B zg!Q}=RrOwl;u$V0$}SD49^8?3=A7P^%D`x`GvB|sRrI-&j)ssN76q@h`y6QQ<}_R+ zKUdzMsu0>R;yAS_qcTED*cea1niI!y;yOX}TTqn5Z{pT%-Nzj*D5K~>`0H=ERXj`H z<<^qLn(P#YYA7pnm@Q{{%=LRYU+tS@nZ9SUY$kjpOz?m7?*qI zihh8c6^%q8&5N=R+;DZ~+o~CQM0xYKe`kFYcVe;w=Fqp6jbuz-?^$D&zHh&{$CETd z%KDO0S;?6mtCNN1t1nM?BYezLHV0*aEg=lk%m#K4J1@cqOb&zbpP~Hh7RNp8H@k85 z=Z3b1n!yOzbm$iO+Gm+FUsm|$!G2Fim1!LbYKy<89N&|6M7JW~IZsmud*U#ds*wyQ=2FxCOFJB@wcd~Hke zr`f-4Rjs&##T1n*_nPNzSs=#kaZO{A;@%a#(S4JMzjxb8vawNNJBLJ~RV^tQT>7bK z6JyDD%QAvPhijyYijWe{yTmD=LvjfxEimrzyn#N9XSV=~Zp;JpvN$p`IVRb7RGw7` zm5qtFF`L}&W**AHs~j?g@1Q(U{5{OWRM8wN5)>oBOX#DPj%fbalA@ne0OzD&eYRDe zfQ2X6H9T*MPNO1Pq*$ke+Q0vZAQ;mNnCsXjJXgw+rU-Auj4gG^OpXrYFN%z)bA#st zfN6p5Y!SIyNoeU;;-`Q+Lx+gew~?(7Q*5%Ou&#kG%_lT-W&ngGx6Xim+A!t}iW`^E z%=4zA-LqL57EoyT!PnB|j-GJWK zrr5yw#NTfc_^a5Z6MU{XB(Zw9w5jdLqK1Oq$xUs>vO=g5AjM&=vgXWq5nA01>SRgQ zf)5@z%<}GRG7%Mq3*aui93gmviFb|OuVI?Ez%+LwEkHhu9iWkqS#~q8kDnaOZ*-$Xw_^husKHg4oFlx7H8jWKY%7_z%b-iKhxKK&42c?%FJ3*n9H zgVl!-wcQ*(M;KWuKt%w+x#BR#b$a}y7-qPrB{~2i7Z}4Ryo*()ZAY`#{KY@UPg;+l z`Z&9PC1<%2{~}8x6bY6BFPoAoyYJv4`T=HADR%%*1C( z6)Gz~l!k5%w5YKRM1Pr?B2*$rnoV@Zk(GoHB4B{0V1O;cdg`c53?1Zy9rt2B2&to8 z)UbtLw11WQUzF#YApTZ=^E0Vs?dy_a%*@)NGV;rP*K4@SFU$D6GByA8+w%JQu>7mP zo~GXkEStH)_jOmN>ERQ*0uStViD|3ZzwgELoc-0tA4{b=lL#dryw?_wZCFP-72c;; z9B^>qj>Tz~xS2Sd!kg(cyZ7hC-hZ?=-Qm{S3j-1*af)BJT##P4q;J9E$?Y=^Q6Ic^ zP#G3`Twl2}p~CLm^oo<2y30&;exI+HJW=MMHt+aL&&>=e+)T%PNlDf=r^GC6i#sSk zmOQu5r)SPgwYFSP^p`;C#+izeyC0LU6kJhyXzIJtFMA@>?XfodIeGe`^tpG}y-Ajs zz9!^JWW(+Knn&B~_lB%1!YAMePF)@T`8Q|!@9sIUp~S>Et)8<*e`1?oN5%~Gb=%jG zJxg7;%^p~N+I|~FW9-o2G%vHN^+3bu<@ySCQES!;pXTE%B?$}Pc?G$;7v;$wYdSeo ztf12?!|g)sf;$@4hg-GgJG4so=63AA<+lEo%Ed$Jj>b0&OrzbVr~Y>yb|i=&Q+fGp zhW^OoC!SctLnk(H`Ea%94OeOjZUrO+`rEdyPI+yt%Uwr<`vzyM) zc&-W)E7LFE`TR-ZmV?%2+^xA`b+ZF@%BRV{QJo&VUOv}Y-!KI^)N`!^Ki@LuSrjQzO7I0-x_D%;Q*AMEHceh-osaH{oyJ}KU$qq$MU{0-Cv_? zrPcV=_MOkHees(DuKCGnDi573yR^nL@!O@}bF)q^aMXTuJXzwyL{3m70>-3z2g8Jt z9X)z>m;9DL-(cmCPPdsEA0~^FKh(3{$4@8gouk9bSxMGb_FpcE1?u_jvopT6UE!zh zCGR)B6_fp$o-P;e5(XAulUlS6q3y3z46)0E&+H{B5ft(Z7A!BRk*e*459nnZ0m20p z0w1{Y8fMZwXg8YK5YR3U{H_qz6$3hOVEw)9)lX6vk^*&VNLG^ISdr5y?KP0_L0cOQ zBuo)u>`luuNH&7bzCKhXRG0(qtg3Ss@@h;tlnfdKU0{=RivIaNqNTqO-2iaa8`77Qc%z3yaHaWoCE?IE>4WqEcQsxBjd+D$H{@vdYLFmvXAfkNVe7_YwIk z{^bs*e>Qk^+~G70WlXcFXDt;(s;$ORSI$~x>-6)Z<*BQC^kMFRY)a%qrEV)Ch+R5o zd!vAzV>HgrgLy!2TBh_6Pi~FcL#D-*5Hpi((?-UxUJf5)eLYNgADg7R`gFl7kFh=G zhF_+Cj^!g8FjsxR{chpm^?RW` ze_H92!PR@cvhbnPhMphdS+HD5+h+HM<-Jmq)EGEAV$$;;Vt%uuJ`F}Inxn6uAHHK-|U)lmGd z2KMWsINOY#3IFOt;{y{O3;3z*3$CO2#}nsD^Ad=8d5Jr6N8}$g`if-`7;0;X@0YHZ zUadeKny$O~k>`f$d9VzwRRu}vgZ;ykfw_XiQZ!OUFG-^4iPi>79>A}&7^c_zGFCZ8 zBbfF6=rs@*raiX{vQ)e9g>BO@+e7Vx{8HZir3B?elrMO(Ql%&4DsGEb(j;-UT!Jb4 z(WLi_z3Niq%h$K#zdV^6ZJ~8@YfsQ-$IR`fPF9I^Bqu@(DR3od28VzPaGRoc5(Y`{ z;Yk8U$0tM+!^*){ZxCVf>C&LJk!9H8G>{MzrK+GgZ!pimtc9>3G&rIcQUsp^@Qr5x zrh-;a44A-_CB*(SOmwUR+&Zj%K;gNc(dV-;_P|h|CH|zX7XQjC;H44C3z(hUdSVY5bx-uSz`qmskgec6 zzp1lYU@S#27Qac}5^PlZegM$9_`Cti9)n~>kPGEdwQvZNKgW;UdIb>EZY1$S_zNm< zLwgg@)S7X_ac~1FDR3>)g0z4&ObM302Jq%G@)vMa(Ri?QSXD4OK47kfO*qK|sSZsWOdm|L5GW2lV)~&KfxU^h1jZK(^Ek<3cPgMI zMT1MgC!mR{61^0|CqT)j=$8nH8qkah{-Rw5J_I#n{!OMuQ^){3TS$Q$K(C08VS$s` zPV?`d9&35z}pNd*fhp0lZ{>>2I&==t3UTmh3MnoTgTT0Qt3@Y}9 zKJ2%AX!cAR5T@u~?IY@YI+M;Sulk>E2?|d9g~L`isaY58E`GXWK5qwUPrCE^YHg;x z%9UGRrHULaGz_-yF%eUy%)j!?b+X`fVI@^81%Ar%#DKW<7NfI!lP`JX8O%E`ee83A z?ZxMWZ*5;*rIYRoPOP=4+!}n&+h;U&_rmFy^v<1U6^2ur>RgQfx|JOr?boyE7*6qn zeSSsOZShUJUEGr!40p^ub)9}=$C&iq438r3R}FII)xq7}@Vi)H=Tc8ctxCVKd?EM4 ze(Uc3($%}D-=0;sJ}V!R$?K@9p62tVLoq62TmBdO1B3bm7o1kXEltS}DZ3PmAKIR| zUv$p*X@?jdSGR8YtvR}B)gd$0jSQA+EV!%58@|a1iCuog|Mckj*)4SpSVh-3cw zt_+L;f+}73$!6tc1w;K!t<#$xw^D_|E&ziVz#~P}qBEmRV1p*y9=Tn3J_^yo9}9|k z7aFb3tn=LQn4++cXV1?YYOpU<-y*p8J(f9e<6V*8!i8gF1-k17Pf+eyoqK!ghtbMk z2RAIY{t_z!qI3Vjqf?qr1#xP64U#kgyaFR6-GacK4LbyS?;((3C&Gdsfuz+1PUY64 zj`bAZ%%aU=ems`dx%GIW%42yy;};}oZ&`70i@A8tucO+^vI_3f?K|G=j4t`Pv$!9> zXCBGI{Py`-%b$)-^q9?!#Z%K7nH=#vSq#0iRoA|WU?E0$}j%8ibPvps!gp3`oGBe|bSKP9*BrA#wl$1u~ZNE{0C$Q4Vn9y>8@`RDVCrZ5!9c?yg*g=ag+ z7kxwv6M1C@u=gcz@cOzoD7Hh#M6p4?o%ddD*r&6!tD4jew%z4(Lt z3Jcwt?&bIPeCLHIMB!tp?&(1uO8Gg2inWpDOHNi_h&Z1gDFR7okzg23=naQ)J%WW` zjF6i@7Bi9U_i_JBmr#0}#_{>$a%M^sRn1QE^PO{UQ$1x??6AObw49ZW2unPE$1Enh zx?8hS0uEB@y-wA;SS-CzzI90%W06F3#`+EX;UF`K9GgXfB6I|X2y|Q+KnxUHCorgn zMGHo~$X@CpzR1E7VnY%noOmUIJ}pAwr*H_dLiqY*^Kc}br3MiJ;0G-RhH@TM=nqzi z(3Tj)KEU}<_jE6|xi$)sY%x?wl^|GRKnDd91na1n*Fd-np#cFYa22?SX(*Psfwpx} zmWN#ri;w>YX2TPPAsiv_B!Xjt2=6J(hRGa6cm=N0*q;%=7Y*6%GW{al`3-xa|1Cm$uwMy{(gy?tOvC`H7CCe! z6A^sFvX?&?N+^Wt5W-*Br3qmA;EMIpQFQuPDh%&5yfKYP!g>J3$RQj3GepEi2eV{B zy@0o{q1=}NQ-;gP&}WaX{twv;t`y;~0i^~Zt55_}>cG!OiiUn(1T(_eQE+oWsA9We zq}bC%xAv^4x|0^YE$5(9mI4(G<1(`*&>I81#cQ5uFl;4S>wt4QfH)Dn-8n zl;{P(4x~Z{Ln7#nG%+=no_~%fL}?~G8^XnuhlD#J?6pVqc^x3RDXosdc~F>o8%eP~ z2pV69IO++P-vrHtPCcqPYgq_03qA4P)F;r2AW}TQWOQ=={{zkeX@-M5`-mdd|04{# zI8LNGgs{=soEY{aYh#c&O)>g^&>VDl-Z%7cg5FnWiC>@0+CnlEGK1G%+<44z*5_M4 zBzZx_+gzpQmNR+@3zF`Kho*a)Y_IdN?%mP)_FD4HhNIV274DeVvg(FU-@bBC8Lwj@++pTWYBzZ|p2LCD7gWB^qs8v9#@Cslmg=N#x3i1>br@XW86#pf53Y zbAEH1yky4RmTL#97d~8&XK>Eff8ntb0I_ z()p0_^)p+srC}|nGNy9Fdy!^>) z>rMOo4{qFbdpJhZ-+4lFBuD&hL+&+|Z_ktR)8{9wvs>)f{>bBC%BK~Cz#YtDT)LZ+ z`yHE_S%+^)X?snh7zL`WU1|UF?eCF`r9QvfPR7|@_nP^4(44L7mF7QxTJrlEE6qar zDrc`#LC6TuU6;L%-<~&XZ;96i2l9iqnf3dvo;fiW%XAKX3Bxa(lksJnM27a>?n^7` z^kPq}T7DHe0ZHN*RkRZy+Y%i6Hq?u9qaDK!7-VO4Y6 z&h~464u990?>RrK!?t#JQCDV^;3O^%DTz0D67_9K-=!kW+)ED^BTfMoIVI;oxX5E zLJld=7kEuos9|O)fp0xQ@lQVHo4i~`B zG7R7s$j?~zW%Zdfp0uv*3y#T=f$S`YG~Hz3_TtTMhu#KKMbDj1ZoL4M?9_`KLgZMg zouvuk!~y|lY}5~BAnt1;S{L;G74{InmX#HcOO!~=IkZiV;67%_*l{!n7hGoFJ}x(@ z6*F{Xbvj>ZRnSUP<@aa^yjc| z?Ra=~KgsF)GkrZ$N_={*(hHoz0#}_J-Hmd0Dz&0@GCy z2t@b^X?=!pYYZm`{hrS=0Oe7b8HNy8JVOxnAi`O%nG}U5no;|Y1n?U3oVu8j4LfqS z3on?7H4{{-In?^i-|dJYh9iNV2AYfAE>DV8kG!84o>)DU@no)rkvS>QQv~T?fX-rP z4U%iXe&CR!W_UcZA+!_U8NwnMl7?UFX%{C+i?Dn# zFX+{!?6z2c$C$LtiuyPRD1bM7SU_Np$ zh^@n3fUT3!0q^qXwqRl2a3w5z3}%=?@hM^r*gldGW zg$O-Q4c323aLYR)PHOyLJO=z2ph?kiqG5PpBIpTIaZ<@Vdmu&s2F$lF+MNj>r78Il z*N9XO<^UTa^$74eX#5W$DmsoDj>0Uj??Jk4hpwmOXpn~w$k|VQZ>xuxL;SXv1V;I{7n+7r^{dH;~HDK<_6@%JJHADnvOE%_tm z0K)Mw2vkg20~c5JF2@iicK3e}JxH$)J_rtvhP6+oMUet`g5U_#1po}CkiYXyrzQ%n zWMF}c3pdMyfv|%kOl{_b-GysgQuJ^4O{EE4E$Bdv?X<#vC2USkBEShA9UBZ79Wqad zB>xY#{bM=kfa_+MD6gs90uj={_QDMc;G48;#O!YWL9(pl*&XsL&eAc0FoOr*j6Ez( zkx!q-$u>5D=>I;Nh00=J1PH@dbq=5_UeO?&A}v5R62Zy- z0YB$3^^SWM%@6|WKwweVm|^eey}<+MY;Gr-luG0|hqV=R~0Hf)xFF z0a3#fMA@wgD?_&i$mw{!=r*#?f0XgOe}o-GZteRwp@7Bm?^d?|i>3eP!2+5{E*LVJ zGNnepv6v|#Fb*$nhjKk^Lk>IyfaE5^9i5;tFzuJ{au;kFf;v#{NH<6+*j%M42BOuA zvD}~FLu^VQufu*#V`{c;L7{_&CN@p6I2*R$y&r)7<~j%>Ftv4nA?yH;3*vrdyUbL7+P$#?XxEid*FIA5;N%o|y(8iM`tG8R{ zy$!Hp>X^5DZ~9pFw5aIDab4xQ-+B^~3Aoas>GNrF($9--FUxSh5&6w$$;MS@RQoTi zTR^zDgGM|;yw_`Y?(WX7Ut1Ts-%}Qof3$s1*>5{dySu%sJ~ijfj=7fPoi%RByYZSf z`$1j;Zl;F#O{;C13x6d%U$7|ikjMSk_DSi_#f}-3&ELtew>;;tDwXh-)U>3roOfz& z?)GXy{s8IHdV@$GAA9vp^Uj37h_Kw!vnkI+|5M8Lk_V&7mQqXZtIgW(u`gq1ye^I{ zO`GX(R#wV!{rSikRoAQgKTXSA^ev5^%N+8WX|&(`Bb$@H$CPw-ld7WSx$P?kJJSjm za%HviE!{K3HGU@K>qiIQD>rn$Te41r?rLXh6#V7g&TC>Fj=vuLCnvGJg0tQ#-|L#M z-|o7=@wHjo{tS)c;M9wI_Ik`%w&Mrsi|0!{&d>>zb z#G!KTHSesO1MidSG(3kSpWoW3*}Q$}5q;7fd|>F-0dKAOh6m(s+NTskHx-uWip*r?L6n&E?VDn(k8#%uVh|n=GB zuCtT8YuQCF*4pyirvK8BOQxwMrl$ueZK?%Z=hsTjPOLRO*I#x}|D())R@wV2o4bF^ z4|r$tL-{l#*VumM`sY5=B*+88tKZcgl%K)DEfeUm+=(3+QD#MI`b2vnASGbD+T~#x!=L{{K!|w!Ld^Lx8ZdD}2tbBzLERB>GavA`@MTO247sNv zC&ZMAAB>mJmdVf=3R=vYeAj#RwaoR1&(Ci5>ag_)@lP z1Ap7_jNg&5BJCcp_s=T5$aK?FZ677B{u*p&Q_&T4a@v#m zEsT8+z6~#bH?rsX;pUMrNv|KjcdOv%!@XesuofCoxem3Xh{DI8xwTP?Lo*HP1QlEX z6_7GB;(Ya#q;cn z^ZHGu$84N6CH2$`SQZp0v1}2Z$oV%=iY!RPZh~MEo)}74{Eh)eUj~7`uPw$ zFkd3UCd9ivb7FtVlfS#M2CLR7>$hwUnuO=s}C?z6@HFBVd~!L@$DE#z~vq)fI7Z*}WK#fQ5qI z4CDZ<-2?~|i>+Bi$0F}!GbijbW+Rr(uo3@PUm|(SgeVOI)u2S=7MT?h2HU4ZVt}5z zh5!3-?mzd1$+GB!VZydDlmaRNI)TQI5jb$-zdA+NklFUjr4TOrcv{VXFgo+cq)>qA5$N|S z@U!_}_Yu=%47Kkmyg3#JRs^|3!=wc~0lAoYL@^!rzyQozRzwIgjKWT^8tOP-N``5m z^qJB#P?+Quvv2_4=fuc?`CwvB38e~5fK+XbSOR9k-V;yjJNk$a$@&fm&oBtYEq75> z4I(4;sW$tA7ZY1L_~UY^tNm#I?6vt>WM*Su13YF+Dk&&yupYrR4WP?ThW=J8VWC>X z(VAc-GjVI$otY#*^F@kFAT7=o`0oI3f$9*n0D;s3OE>(0rAaUg%B?W4MkIWb66+SP z52T+d-@@JCjWSrtj{{r4=OrgUfYldb=0MDSEEW?x!l_+dCUpu7+%V0P5q54d0cM6% z=lr}6kVFY_w*+i0kXAhqzq&c9OdY~e1q*`b5pe^tn_Mo8vq;Be zxeOW^0BW?=HJ@Q9FAFsPmKp$5#aO2{0d5AL#?p$qHtL<}3*IpFi=)ZwD;OUb@Xg5r zEzbx3VfPPG0zC(fU`w!;=2|pR5^Zsv9)OPi?g2#m0|u9ns93We*jtQSJ+@yhmL6$; z^=4@CIZ@KM!q}(vV92C+q&4t7V5$y=Q7mz6lBv=jV$a>A@I?!O-2h?fdm}@gM?LOo zTnJHWUdkc*WrMKoivyZ}P~G(eret;ibK0CS-9^A#+l6<{_P7Ot@mpfw!0;v48< zHS~yO-*aw*NKW^)&tkUcuW&5Dz@W&2DgVfWANZ0Vq%@{9GL;D2j>Xk#+TrEp>*G33 z7CjUYE9YyrkNJJJF;@Qy&eAX+&OIxY;y~)k;2Z25&bDn0q|WkIzi*?8LYcT?ed$ zQfv0yo2a+o)Sp7BqI`KRJN=h{Y zs+lD>BG}91=9mUJESa%XajbXcxvWk=z`jAI7Xu8HT`XT4-)(FQn zIh%)xejT4Ed{=DDyJM3A7GK|U^6@#(S!eD|sZ|uzttmP>_v!vUCvBW^{iX?wJ}m!s zS-~4&@f#;rX=yKgln^j`^{fS}a#=%VSenxcrN&Mkdst0KTdWGJnf=^q=1ctuGdXJo zFNj%|@CF(({vZI3i(|qh9#nF6qhl&>i%iakal8CF$qFybZju$Am;rh$T`IxLqofKYK4iflrFD5l zc$2|v7>;d+8^?(?C+Wf1l?vQ;HqRH!CfJh##y7U9LklF*_;99Cx@IyBEu!r)WUj#e zyhR{v*Lj!8cwxnJs{j_J$U`|LC1mu2Oe4YYUlmhZ;Tla6Wc0TMBk|tCl>mD_Yl=SK zS2N=EOw1&4&3c&3;e$%T1?K$WF8u+p3*X#2AFddKF;QRO5N>MF2=82=IkcGoZFrgA zLXf*{umUiL9eq--xjm`iV$l0xK05~d+T^4Q zir4N^jvX)__iDg6DFoA%prld~%gbaSU1i`X715U`Uga$UOZlhyR9C6^MJoXLMB~M{ zyV%Dy3Kc3m(jWf5?ww40_M zTP6=sg8*;BRlp|rj1wkWB##*$g~iD*Iw=;QPb?3>3m}V`e2n4{huq8zgQwM~@!&HG zV4t{nj;PX{JFsv{r~Y$Z5xfq&uQ(!k^q6sjq9e?Qm+A=(dG+Z-;_zQYpi8h4H1=*f*I3BFo|^!7L-0L#;XFza2jcb;F3tU90d_2|6ViuO_V|IT z?0ryt@oF7{aOw&K3?kKD^?cVWgXEU!oLGn)#5F4e-H`v;ARnd!b%32f4Qc?W0j?MIG+@-M`XydAwIxX0Y*X8?m6`a@rp2`(41mKq9AM$0z13Fx-+qJdA(o^?|}wt zxPPekAn8oVdq}(LU(3lH2az&$7~aXXt1uWixIUPy%Y;~~XB^E*q>`fU=noVC!2h_0 zD7BtWcey+~G!RH*2L_FxyL$b)6@ z)?y;%Xx>QJu2zOHwf@X2k6fiSZ zLem~7=hhg+JcmwHQ?RPRjnK+z&T=t(ahNTMyM4uU0X;eqpkK)j-obYo51 zqL#~wgH_sFDAjuG=Z#^_@QjF8a|5HOSPW@VlDN_U4GDRcDw;NWe&nU;5W#IJ{b`uqT|~FX8bv#4l9R7%omaQ%2D7Quz0L7gU+midy(VN5^b;q zn}!_BZJ^)>MnRs>QQ!K}3~x$?w&7F;GzSB4t5uXDUpy45kXD>TGLVHNw;2!U zvwr~PF(89$u?AwGj!y>h${^J8B#TZ^1(4Ar#3Dl+DJ>ZNVV9?-g2xd#$ z?ihfSZ634OTXj5LTNKk~T;B|9kya**7_s!2xT~~D>W5dcgR?(W2jY2Y!a?fTz6)y|C zljAt?#M$P+BAKN08>^%vi={gw+E_-qMW?j)x5KN^jCD3NYCqV}#@--b;gP2EHbR## z`+S=9_wg%>cQ>^0&9N>#&p$ot($UMB+uLmSh_l!S4t?}l(lz{zw{PyYwP_cO&b)Qn z+A%_6?J#-ab*q+I$#k`4iD@pHpYdUe5^KSjXQe|*SjwGF&FOc~S}l(k)>2>BzJ6Zl zgQ%1971AR|JpXfc@qM@3qu?be;#bRj$B7E$$+3ps7fhQ_x-w;w#jZys?onS7y=&V7 zKMME+2g@sEIjPiUUXeP8bO4uNmQOVVSbvWJtF5a#hFO@0|H91)_Y83f8Pt z&9^h9`E2x^Xfn=V{Dg4^`ucF>B1_61KJsP#yT|J+w9dXU zHae|j-3Y<(@;i&lccn&sQuMJt5^`1DuQa)(2a=p$!I%6--*h+l3we+nsO=1R1E=%f z!(;Y<8}N#k^z=x){t~%`d$CCay|_cs?}2evcU{1n0Q2O0_{Fr04gG;-m~g_c2;k~p zzc#{%R{sIK(zxy|Y5u`Cd(nmX|5MT6!9hw#+jDaMiB7xrEiOLC`bgL{-}~2&gaHT8 za`8GOF&NwDb`?%&cuQK&$G-gv@$}_4KjDh+aMNJaiDdpP zP09abMky|_V`M`3r^J6E1giU8g`pK+>=VTJ)u;{*-{KL`__Hp)*-3Oh{(&$a7d_1C zH!oT8a+vFGPxU|;!=Y|wq3NWti@&D3N|Hkj?&9Nn1!9tHWGuLq?wu^LN zLfPOl+Sx%k0wEwHc&=Zs;lO0jiyPG;Sau)#s&Hw(?P5<4%a6XaJg%d9S0(XR{i5rP&4 zTqvPns1@*dYNXr@py9?TPW4L0-zdC-;v+gflAXjSW@Rdgy`8xR# zA1Y1M+wIdo$={B}OnFq2R(Ox81A0E^^{TY3$uwGpJVcu79;WI}CC2}7`?K^ta z8MoeJu933c@eCp<-MPs(Uey?EjEM>#x zywidm6NYeBZhRRuYe|Jp-TlOV0be`9^>XxUM)Kg)1?ZRc}$fMp>Ck#i)2;~e`*>P&M<@XbB3yX74 zONqXlYLs(7HnP>|f@$Q>unX?av!imdV>M2V{OI)IiqWXpY^ULTae_S|^BU63GEDg6 zE)I37yZ&~4n(6sn2lA4B_y;V=HkK(1cHn;@u>8o$J!L+!c9LV>)OK~hzY?UIAlUBf zIm%%8i{th$>dP{p)xQYt%HF&0yxhzi0^p9JT{VsmzJq>KD zs*!V-s-?8h?9nf0UgRC<8(b}V)`2Vitw_um#C3i)#8)L(m@lOYX>Z+xdg;hD!$uJ^=+^S};zHFnX`Vb2{4H0UCN5!<8`u@u&iucxANKV7JKNSiVrvo5LDT zJlL+KWPV|*wad!cv7?OkXHUETJNIYHO*Mr!559~{InOJ%4vDLM_r9@l_>+qH+n0~} z^2za*xbo3^ihS2@O%%O*d8V!9_TcPJUnw8m=^H|HYU7mESzOVWju2&GOW41 zrnP*1d4i7e%oyj+!Nz=Jrq`MVcPQ3g_KevxY0iY4H$CwBTbm(@1-_f2VLfQuSJ}nU zUrK9logp0X0&;!>$qx+Yc&`6|@{9J7;j4+QGI!O5j|}S#W73Uzb#kFS4C5W*A7)i^ zUW=J_7a zwdy_v7=!WGT{|^Az&f_TPD1EPZt`)-n^)q^?HsQa%RQVt&VBwkSCiTN9|lDqwX&H# zUd`i2c2(7lAP@Nz9v9aX$h{k+7j?=1e)1Yy zPv02P5jTw@b}oz0us&*${rL5l2bxz-oih;E)eWJ55?0ZiY$j3koqT#R#MJyC>ak1z+xxcu=>Vpg+ij-IxY>u^R+RZeb|PZ@k!C zYy3o5ZK9v3pHig8%*C-^jcP7mEV{T?acxM*%zZvxv)B`7X-Mkmj}n*lTe8~V%zVT3 zDkrbmg{f74^D9^a%@zFkD?a_N2X+RmsO#{A@W_py{W|P!40v-NJn<)Bw@t52+}L~Z z&!G$L_SOr<8-f3SVgtfdo9aYIP)QcQ8qmnju14ScOm`pVzYPKj6x7xQ>;xc<2#m+# ze|_yo)H!{woFV1(XI!n)H(YJQ;;V({XLx@h=5>10?bH zS0+3BOVR%!1txbiZb76?5*STu&R;{mDCtHzo9vJ)JN{x3vBCmkg`a1Q9GsW)D8Ftk z=A2Mi#5&TiovJ3kQJn}>@qTZ(G1!>8Uy<*InV<~P@mJ$-D?KSlJUS$-p$*uLm9z9E*S8af?gQU{uN9Cg#ixHLk9S8G`@Qb5i>F%!PwUp<;^L;9uJPU|@+{zyGVQ9MWkFb`Gs`TUo+ zdN9n9&`V3#Pa!{m`KpF+#f~lD&BO+FWHMzf7#x>Jl3@VS){;O03IqZMM%0g}+>rHbGjr0SYl35=M8mGk5}t(LceHFA$=*BB8YEe_@iG*p~4OBD>^$o(eg?mnD!4vbr<}i1wvHy@oP9l z;fP}sfuN4$mJd+55LCiMNMzwMG?j#B3YLK-{f5 z|5`v*T|5nNlf(aLV1W7kOEn-=66Z)20u*zlOfTcfmwRHMYJq7OLV}tGnJ`9;>;!vi zJa_P|{xQMvR%3J{XTqrex8i5~NTB>mg)h{zbuH$e^_bCR>I~6$UwWVNc+Gz z7XI0=6!P_RtGxp=-m%7;W3drB>~sGrkV{xGVT1<}5MvJ!4Ef5R|mzcK&(&HoY@&m z7Vo`bfuG7wiP<53+ecT9taf=ZC$M~xPVnbM`x9@?=UODp8TV{K>)z{AJ?7bN zFY*`x52$aGIk5Md?2&I8M=yDLJP|YvvzBN+c;?_MexE>xVn-zdPuYnR*DAbobn$sD zuasEPlU|{Aw$v-JPkp#q>AmQJt&^IfkLSFT)C(N9mL2tB@}-OMJJL$)U07nJ$yY@M z1con&)3_$yWtZ3Pvn}U9N6xDOb3;qoBKS4co?aDKbaA$R`D=>Sw#1}FFYSHeUuH_K zUb!v#W%itHmDdIMtW*q_ES0`=+wjFDo6w@OjS8~{@0}QDr}6DU{pau}SHByScpsM( zJb3W*BClT`+N?(lvu(D`-@Rbi*Qd@uE%vx3XvoN2Ds+B$-M#2judTt<^}eTXH;yuq z^ZbXM>o-wfqd9H7=(1<|fjI^O_R@=Q+$fUX^}*v~QES@#5vN-3etM7MXoXb=>6xEh%<~oHVS$#yoq0?H$MY-uv=#cNr z{r8_&x##>irD&=45A)B{7yViKebowKt1Z`dX(c8J-rX5GTw3c_6{qma<=HIutA;j~ zFiWJ?eY!>t$E?iKCOTsIs7#TlnuCXugw~mseb*2dd2m79;B@3%pH8PMvu4SxY`mRN ze*9V2`=nfxTenrd%$jI6eE1r(w&Vj}50uT-tqDy$@@@5ZqZ>Z6SsGIAZ`?f=TNd5c z82hoM=tC^7qM9WRb3ri4H>FtynYhjB7=5m{4Ypa@gU!oJe5GF2sTKF%a6MD;Jwsi)0nFfGx^R#e8!G$-4 z%PjXLiv_mg;-$Z5cVWuI_AsT$g6;v@6Pd!3iyxix#UA^ag!yZnOtt@VB(*r~`ERca zC$#r(sL;aM=&ds0IB^e)rT$F@Z*JWl-j>+p8x9RGg;w);On-u3`ix$atYFy?BX`hA zJ(B#ULF|`G^=rq^rFZ6seloaO=uM`cD=a9dpkXi2uma3jmG+T|ae+;+C~L6SE$aEPhhQ3ED@ZzCi1hVOl)xN{p!$=bJ{=C<$VwA zmx-8uq&#)WwtG|IWwD?1d6viPiFr=vDp`e5CL@IFx?r6RcX1Q5n7d!RB_;}eu;>bm z3f^K>>+5$JNCfF}J+ogMsc2Nf%$h3@-BoK?D*f_&+{KIv8PzvmI-KP@zO)8KUXb{V z{q>^96Cc5)71ETvb@T(*3&N>x9$n}HaH-j$wC73Az0BcldVi0auEMa zTyJ2A;pRDQ2_KdnAYd2)qaF2gi5ddMGeN0aU0hNt>i(k|nR8#&-<%Bdd9{97&v1BG zxE0ukIWIuc@R zda#MF^fIt%p1=N05eNbMprm8te;Et+fL!Ev+YfOi<=JKe@a}js18IUb3wf5c5qgLGh0kd~{Nrj06AlJ^SRhDO z#%zIe)(Q#v;*l)sS+GVAYX9t6hL2$o zsH@K*s^&h(479?78@MSIXoP78*@JZeo{Nd4s^|eFT67DxI(&Z}zN?HUsK05UV*5KH zFg^@j|IIu|AF0x(a&)=b*<&U-QUUch&x&yopbBEG4P+2z8%TJmGw!)drL9=jM7*m% z(DZZpcURO4@wz%#O$&kk%fK8 zV38B}FKRtL`7reauAxJ10R*V5H>ae~FG>b}rmCe538Jf6R1&78U zs`hZeCDPzrUierHeu3S7)L{WwX{>?rCv(AWC?e0!!Tn)$IWp5U6L#UvP^GXTGrsT`ndCOk5AsKuFs;KALm=QRPzwinWHGxf z10V2FM*dq$X!04C}!Bd{=Zw6A~S3@}j>R5x(V4Huy za=eO%;y=(hiWiuo#)2Aw^?$D-lbm3Q<|2?3KJS%K2de;gh;1HN;k87tOC~%wZBx{6 zu@l|xqZTUGJl4y}?C2x+eXHBH4SsojsAgum`$cupEP)|feyg3MRu3tz8mnlK@+SVD zhe^j49M(7}&*qzc!_cDgxnH6VOZR(Y%!E^SmYnk9C?woq&Defia3VY_Bsi$>ld{U3 z`NQ7VP8R)=-!3PpcKsxugqDR#h5Mtp+qEktb<9p&e|5|zO|eRC=_~cb4|-=;{t0Rq z3c7SLHBNEk@J>EHA|&cFDzx+y|thUel8^XN~Rd*4b&%uAUSUpu6GZJqe4 zJ6E@azAKaoisbvIGUoi4y;D!Mn7-@yBc5~T+hj}C$MIXcVs|`p@tpz>R)y~QSQ*s2 zV_{^Nh1~A+m5UEL3>_R@Iz%*Tf#b88YCbEnZabEnn@v#WKl|+5_Gk7#;`pW-=)PE= z+O=!u65mPNo{c!w^0mRT=N~&g_p>e>XXg>uZmYdi)%IUKD_g2CcEeePu(?9_4<^^l zX31pkYBL;}?V~$y`Yt#7(sKfDJiQEN{G|sLyMCm5rlbSM!}`=OpO(?v(~kJqXZWq1 zxO?=1PEi?Fo{Dt-ow`*Dqa;i7N#JORq-#w;sq9jJSI7^lbIq9Y!k%X8^TuD_P(_lTXWcs(NdS!~Sq%yngFr1m(lrs)(Y?-C#J+B0_2_|(NGPpaoU z5S5q^yL8(YedF;?FDF^3Wee%R@_Cc&H}Ceh2u~B2VY7?ZSE~x;+CCL*(4F$svf2OG zsofrJi6?a*-Su^zF8VCNVCk$~Lljz-b9>)tcccr;9aTIu_x82&K}Ba$gqp^@XmNQ| zE4J#&sf8LBN8M`m{dOr-O((;D;=_%!S4R#!2CUBu=H`dzhz|n-nlNe zw%44_taNxcQEXcH>zhTRDbQ7_owWS*r4Nu zJ>rXvmSI?=F=o9+=R4~$Zfd(SMQ^C>oF|IZ=Rw{;cf_rxWk+q?+;LNZD9#bv6Y9zn13}H?< z=4(lIAYeqMA(Fxu&qApj23I7g~(L}3qM{~Ke7#E=gnbpX;sgEap6Gp#{d zfK=Gzib*;y(gdP70BPI98NRX*plNbo?Iv-WR>k;qdj~`h01pt=tQKL4;b;LHpNB{w z>A^@kWLU&C!LnOW7ELG%jBj!9PNSPZdNMwJMRryq$i+Zw%Ty6-#Sq*Ohbs>fpC7(_ zs3|%qD^Q?tMKF;<2Cq4!05c4Tt}Y-QOW0J>fdVi<66b)S1un-v+_46PrA5h6e9Mcr z*_R@3!7?%W*+C;)4QCD!y`F6?G%VdWFs5}mx@iQc$U*Po}S zL`rOmg^?mNeDV&Tw9A3T{FK69JgZ-?eeq@IUx@SVTcK!V98GHX`czP0c#Nq+u@y$lkfR9$J1(#LO$Uyig$LDCiQ*hc(%jwG zzEa*quIzy%A75?nmZ`#g>{CKZJ^RkNh*$UUMU*WOUm$L-tSh7UX6x|^DWNb23uS4^ zz97SKsjYZ_*)B>7!bLN%axydp-hrMHOc+Dgrd9a zJj54!)Na8c1yC7Q6(p%gs!%4wmwyS5J5_Q*yz)kcYoMxph%0i>NRU!OZUN8~sRuq- zhVTg^17nBikceMK3Y4HIzNt52%|`_!UrFK~P@NBBiKJT`Ny3=I7_vU4D3Dj~U~}N5 z|5qPiF(d-Owvs?12n!$k0oTSVznfTS5`5zj&sGKrz#A&|w# zQVW<7*g^=m1Hu_;KwIB{NNN%oQh<$aP!K#OGd6I3vA7RoBng>5tuXqrtTt2{>}eT1 zP2+>(XUHO)B4ew1sNbH=^CD`QvvS~!28S6cYeAkk>hRnSmULhi?J$mlx zg!xDq(WD^~5BG2VQ$pK~CN@zbL^^>Vo#Kl()&@(5V~xMkDz!?75g%b1--Ok{C0f9| z?~lTb!Fe1NAO(u-)oS!=rjTwFU%ZL4MGCHNPI3X^NI`A@+O{k5h(PDW%{Ac3DqcF` zSI8l2!w2q| zVDo{+?w(rvL`e4OqGkJk`KT}1?m2hm@wY3^8>EeS`(^J)soN$pEeAE@=eO&Qu+f&> z_0Z+m&pj(_=QZ5Z-!$Uz!IEW1{e~S~bHnZOjm$T+;jZNQZ+kc4muhwxV}BYHf`UE83IE)CDO$u1g=&! zZhEp(R8R?~#agP{qW^rk+a5AH`hM{v?L+w! z7Bme1R#X&nJm#$HjlH9%I$eCae)+8*w;u1QcHJiDmM-9_=&Y(KwyxGAS$JD@e&$W5 zUyA}`Z_Sim`$Jwn{@IVLQZdtXnJqW&4R5Y!H2s{b7dgSHQsdHv(dO$fto$4M;rcAH7j$Uwn(g1IyLjU%9e+J zq_%qu=A0eemZ|e7^HF0@k3n$$r$I0L!!FddD7Hv{wqeiDTkm}^A#$<2=bJGe3b)xy z0`_7L++-=8fYD+OtI#`EYw++5+)_%=X6cnnOHAFbtW@ zgdeCKQUl;DUihDCC4si#@Tl9e9fzGd4h>UZ@Y;Taut02@G1uuyWzl;0vp(1 zhQpar(XyS{4zla~Bp!-*{VJMgQq-yHw0L#m`s3y&T{>Lz2s38CnER+*~8TeQE>yAtgtnkJb&w(5OdkC_?SsAULqQ5+UQdOQb=2(h;3j1)K~!V1Q&1m_-3| z{pbg4J)%oQwg~A;x0hQ?9n;(diiA6e3ycO~gUdajc)JwVZQJ&fPM@h~N(4Y1pfoPh zvS#LU20~9ptbc*raO5IbW(&cYj5jhqgk}!sV2=pIL1TbK4)Hg{9zZPoxDPYfL~tA^ zU=a*!U^A^KMT$cib|V?LA~+B?EudWQruvwHyb**2qG#v{Bv@)Ftc<>4LTqF^HBpL= zqZR#LLLcFv3hY$@T!6NS5Rs5Zq@2S8onV0dU0|WS0#Gc7wSgXIkRGgG21i3|iGB<1 zWYHoEzBSRXQVlDyNQEU0Eh4(*;`9t|fr8ybj2X>Ct+;as2oe$aBA%N8qw8S!mdLYQ z0Y^6Af-Sf~1h_L2h8Q0{?qQumz@;VS#hQS*WdpQ=P}i11-d0;|CK#JR6~=Pm-&Yo^ zSPlL>#L(F9|)ugb+7>|_D+yLh0{3W zl}Ab&guRli_po9zW-7(cC_xT__#APB-mTWqNNjB|zGwlcu-gKKh4MoRPT7hs zPr$Z-KHe888Eaq-#PU;?;TqB$%z(mfZfxA(p$2j*RtYTdb%Md0qM>QL$Gji;)0-rC z^+G@8j4W}_xa4ByMtM!u;qWMNrAUpz$_6C#Ng!Z)|( zfU$|2@Y3JyP~_@eEggtM-$k&WI@CUD9$IBpp*Y+?een)~tm%GJ0z@Y6JuUt%`S7PY zgQ|oIYkQsDk(}+j3LIpu)z!wHnVjMF;=r!cdt#hiUq_ohGV-^`P}}bJ{e{S~M{e(a z4ult6_POkZf zBx{}*L?0K5AN}=Yd#H2-o5dPkCdJ=6Dz>we@(VL}1l?^t@zXRUWSr`C z|0Tl(yr!qE5dWiR{^XE*u~VGniu{U2$6{Og6wh)Fhs}7J_3%@-z=imW^9nV$e-A<& z`j{CBpY0nwrl1&Im<)E)&=82(uM+_uxE&;lnPl8c6fC2Gj6Uh2wbJN<0Rq6F<(xju zn7)q z5LD$wu@3MX0&ZYJpa+SAx;HL{(mn&NbtK|b$n~;h>}wlz{EJ@L4bOK$hx!9(^Rb6( zjcSA7W%i^u<^xgFBvET-aR5QF$v_L-d=r*fCEDg-j4yV`LrMT;(+o$rs@+h~r2jvk&9$Ldm_KYMI)Oej z!=SfD(qsEoejIiZUE##oj#^TBp=L&5hkjeOTQADYBelq+6ZV5Oob$_94Jz*5&=d9QoPE5 z%Pn|y?aZRMS!<6a2eV!33@1;0vc;@(WBmt#ZF(|hA7g?MDr6tQtPAbB0BNZ(cnv z38Wn)I~q)bpQZvzlq*3~*;#0dlSH5Z$+?k(c-hL6kd+1-ewXqjeF-oM0diL1eP@It zx^tQj^s9$pRGcJdE3uED=cgEbWf-dgowW56jVe5RbvLxjp2h%ABic|ls1a=V% zCLR-rFQ~<#Xl+Od`n;9_H$lGJmp?)PlnTl3)vvssB!waRd-ZK-7obOmN9c@xJB01D zYEyzB`oLhG4_Fwyn+@9821a%gycC7tLIBR>H8?HhcZX64Q3^q_H>Dtlx2UUyp6;dy ziD8{nDLYvctsGV4)FOUdgkbY7a0wU=~XzE5VtDcnC#Vda4o5vfjA4D){bUiqew1kt!3;2HFWN&mY(mGpYXgvUeI)9Q?Lvjc z@C6j%d=$`u^?*K+*2dZ(>fuDH_b4UU(InWP5n!RE9@qtZ`asyGwDt$((TY1p z-bJ8Ii3U%B(HAJq>f!W#5EmH}7Jy!f9L$i3aF7(}6E0!|(_b25M1-dMW%fU(pp5bd zO<_$()k-E!Q;a^thW0*WrWm;)yDJJUK#uoMb#6 zz;Zg(3Nj2f3#gS5_X30M^L?suKgFB2IZ3m?Ic8EsOXNsVIhVh`ngE9=g`9E}x;mtA zV{G0h4N&-9kR7&RL5-8h^&867ycv#5RLmqFQJ}{yluD2$>faIc!;h|n#9$2z*mrDR zE2+BSv1Gun5LL0n2D<6h=6ziG68dSo&@I@RE~ptRGBEhZF(ud#2O9#0frKarY%6bf ze^($54I?*VfS3h=ATm*fLvmbxQ07bz zv?((@RZHsZzl{X&i?UxQLlks|2E>&fLMr4AYOem7epr5wN&t&NXU}8UCm9(ksh`bB zbAjtY0}*x$i(gI0eYLJqw##kN4^z~Zz(D03Cxvs;f@!ODxyt6i9Fn`wX$`5<@2#e2Yy}Y0y{JM0q!{^4C zyCYx+tBC0{=FJs+!inWSX%e%e(^VmGsBcYIfk{Tg^e=9)H3hdlw9QP`rEhM1`a(rs z;ap|kqg5&kPHtD4D?Q|S(M;c`{Ds>$IL5^cHO;-?_Ce5OZSaVsle3Lx>7-86)YN0o zm9btDR@;$G2dfcHy zZ40zy2HzApKXSsSqkB($a_b5|ddM;Ymwd3Mdo8)7F~~4*sD|CF*A8cEb*J8(UMXMf zH$idAP3LtLm5;p7teuzPu=bn1^MVUMyIW4qOVmCrr?-3S?Rrmj8|8yH&kI`YNi)_S zW@wqBm(ZY>Zn(niyvv0};qxChoPGL6!NT><(s#Gh6V#$KqArFE)I)0oznj@?H*rOe zX}KSVO;MklD4w|Rc+%5HmKkflM~iH%6)2Z<)vdaq`?dhYxWtpq`G_&^TjM;d%VUw$Zz&x?-CznoiJa+5kp^ixr z`Y@!Oo4Lnuj*E1}M=5m;CEbcu!D*_a4&B`NTfXj@@UtO8I{5@2)KvW+S%I@)|qn+s2KloS;o__8h2P_BdHS?8;~N1rz7oEgsV*c<0fh zm0FMI=dtXfoqq2hcW9ipvD@g5(bjykRShHKHEf@Uel*az;p6sZn3-};nnm;7>mxte zZOi-V*vl%}_By_t#bP_o@K}+}k!n^73S6CgJpJ0I>4pn8*9neyO`GUovq~m@+-|3( zCI!=UOa1zMr*#($d%o)O=WAaKi}oo!E;WgcK5yIWGyTn65u4E3lowZvZ=Y@`y7n#L z9z!IP(w2v{;tQ3-%CeVDQRvCPWs5V%a<>77gAGu@Arq)(7+|M6eKNnCwT!SEg_= zTQe<$Ac8=w@h70#$DX1(YiNpo3uHPY$b1OU2LT1qxhjnx06$@W$tvyeK{+N6&M+g8 zA_VAwm#!vl+q#KYBwJJOv;741!=_%)C909 zY6ny^RjN1;fjBUXXifNY1_wAr!anQMSAN>Rn9`9yNX+?& z!Db1Y!x2#X8CFR9J?bwJ~<`a1~G=G()n~p)5$QnJf$xfuyB(NRC3CI!& zs{Z%-E`|W08UQX16?cPZC@KQcgVQ5u#Q`&ks0EaSLNwBWTG>Q?q3aPl>MHu8*fvt# z=iS>G@+bA%^|zqxt7pJ?4P3`i0^R*h6Sp6<9td!NW`W^)guvY2S;WUJM??K&$H)ij zC-9r_LFG`cLs89`b$DNo%9$;iKc{V~mx_`D5W=ZiP|UxR2}3PV zje>hdc~Or9k#9vYLWPLI6bE-r;6V>BGOC%KPHzYQL)1&vPRx9g2tAXILz0t3HbeQ- zuU^n$EDQ$Fs9#DO1xN`8gJcl4f!uH=HHRnyZx07+z7)%2AEFnmh`?e10V!T!O;I)d z5sYKR1U6H^HV)b%Qh>uv>Ga-G0CNyKEp zfh_3*EVo#QM;fLOMvO#4Fd9J&o^Th8)e(SbrX>(4xwH#j2?8T%$hITJ*T4?y44N*j z(nn0019YP2LYsiaiBPAAFiS_O9s4K=Q)d#)qC4$C5a0pTe!Ck29VF`ji z7lCl-H%}f*&=L4YxVYc%Zr>pw%3V;OfbqG5bCUW5K{}V1?DYS>8(p~H^a!6s5QKy5 zbgG##I0Rjd$-^5q%z!2@f%VUaN(Kaqy;)FNL0)5_1O#~zhJwIy5Ojm$@B`Gt9EJu^ zwotoZF?WmMr>k+Q!VPCy&v2?nfzp)>wx7xfk`BZlIZ!kc$w*px!M$*uCes=S$h}`) zgqL;-Ag~P$hIG7xS>1#)bKDM|1Vkozz}>J;?QtyWpOis_`!ci%MzYxBCk^QF7%hB2 z?u_KRctqFpqy$7T;R-*vYU1AemUkVP0Fi4Oj&Ov78P$OLJV2l(zC)lKO^G#dgB1q+ z1Sk;(@W3QkpvP4QbVPqJQ<|NX3MWC-Sq!X{PjBr*SJqQoY89$^x;+2<#nG6ZoHevnQkZLATw0T zRx{PD^{n6Au9MTh?pfBL^u+5$(fw-0R*k(!4}VZQcK-RrUq3f=1-+f($Q6d_b5Hvl<~@NQI59kc-?)5=bbkC zIn@uJyr#E(^sBXLJK{U)^Y^uf|Gs21XIyesu#tqISxe;C=J=17qzmh8a$O>ZH60P= zObE!kqwOZ4I4rka)Z>YPv~1e?iVt)9cDe0)b<+C5uu+p2&I#&0y6e}g-k~4vrS-M_ z){7mIErmGRxxR1T!lQYyF)p$TQ^(9wzw19XWNfsfp69GBvHv7E_$q1Ch77THRDDph z*7fcBc~y%Ow(q`tGfb^kQ!8$&+aK@tQKrQ{TC@M;o%6LbbpOGQDz8j5ywE0oP}5oB z#qjzIXY*p#?=T(1E_m+Y5MQmr-kr*357a{oo8N4X#}kp!LDSz|o#I^)l=y1=Li-F? zX_nl$3EvV9*-7|p_P8_Qri&k&4~;@De?tENd+X}e#_nVq=F`tha<-?lC>&79+O z_u2QZ`Cb#OcF1qLSK^YE*xGw}gU~)_wOP(4W2Az%HK_X8ToO1eWqi=fG&z1}*=n^y zzOtE8ve(M8mwCT_b;f7i(s|2fY^=YrI<#haU5(YThhDSd;f0gCbSt9#qpM#DC1`1Q zKM^t6t70+L;KGBi$9;8%4t+8~|2*s64HKE#>62c4yTob9ee*}O$TvrIzLjxmhrP$) z+)o|To;$WRCeE9=)6;d#nLVC{m`tkd$zUQkcUt?SRU1L zyK_X+JNt9=oN-u9wKLU}$HKx1G_wJyW`-#sT|#!oQiU$HRE+V0v^jo_u_(A0;F0@z z^`kMiLdG_#3_Jlohj3a+5K{x=gb-?Ia-T(yaT~IPHt}M`7*BHZX>?zHvM>Nz9R;m2 zZNY_j>NBojip?RJClF>%XvDxX1O-?+k%2+z#2?ZmB7FZL!PsyFQUydDz!IQrHUjV1 zW)~isMRWr|B|H{jn~CTqERG-0)Jr3+(dvge$ z=YwXky=GvIV3>$wh<}=NxuF@+LJTz5LqUHkMr_2Hy4d?9(LlsVs)Ug;l-BR-`fwz+ z8K;(*eo*)(qB&T)u(M$dlKw74{18Ss-n`{Lhu4ke-M;f z0c3=O#5UHhlOuGoQ*me?z-N+cmIQlG1ZVVumjQ&(CS7t}pd84PGKfVr?bZ`%2!!9< zM`5W%t$7#{=Jxi+rbFS0BVmx&2E%L;71n2J84vsF2k})`>1>YU1CFu8z2;&jvMpC# zDMBqHa_8g~sqmV!oEAlxj0d6Bb7^5{1}2b)0kko~hkOnRNztdw74*V%1*;x#wb52e zMc-!uKH)RM?B%+$kt}C#8_{b%#+N_7ESOZp{w$@8?GHvF;RB`m^aENV*#^-L=nUy) z6i;lOXbfQjt!00^N85z8#isCO1xFk z?`syV*=HRdBY#c0lzp_Qe(H$o*}&ERpe-DYq-P`Odrxwz@`%m=yq6-tos7c~m|v}V zx@E*d!-xIvtQjdMti~Nv%sPB7>(^0TiKVlmw-)dp>i@*^qUeXx*`ZqI1=CY`^}<2h1({mg+SuZ7+z2$ zI<#R0q8~i71^)As&X|UQx3Bb4==LGnDMlVP1c;M%IRucbARsY|@a)5km2l9Nx-Q#x zhB-tP3-FH;;FB|yNsOz!v2pivm|p=aECj#fK8Ox|pcui>;XW=#p`s6jZQ&5mn?NOr z{erXrnL~ZLw|1i}aUmZw0KDYlD8ez&r~ca=KGY*1o)RngGNOlo$vs|CoQ#VLmseRV~S%>cwZL$L~7}cOrK2g z2=_pwr+q+85V0RCGAcuu0^j%-`*;fgJ7gJp1drZZ8z@V9Y=T*MYy(?kIwNx6L$d_^ z^wUr9B2EZ~_cxu2CIlh_wrXdfC_ZU(Td73AirPP#uegm-1doFC{mpV4gPWVkG$^$| zE@wmvXeS^ctm_yCQL*q285>`xcJ`&OvL|5dO_5|w@+(4qy44y88 z;+Xl3LLvoT9;r!SZtg0SfwVLLGP1KU$D*u|MZAa5!kr?8u0Wv9c}+Ol)STP}Km78V zutb7(=@<_zMu4}e(Snj_^w!LQx8(P65VAw@5J;XXq~ELo=PTh_UX&(+e%=or;m{W3 zU@Xvi=b_yYP7|z#oey+clNBI0?Mku5jk1`Pm_A6&6OqZg4Q6+_Uc}W45hsdFi1m(Lzg;-wX!PIFzA(D(DfTaEsAcRGCNCjY7@Z3v3fA!FKK8S)8 zg?tmR!YQNA&W(n!1?Beuia1rH!{Yd~sydLfAXvzawGqe+HH8u=Wg5m87WKfaGgLvy z^;r;R4sktD73vL$B)xD|2L{G4OwTx?5R#z$VLM|PSxS7u3Vi}70IG~>n~b)mq6yS% z(POowQ>M^_?%^9gu~kpy35uY{unaA!j7X&qID*O7KwQLv%oDH3P6q^pSMKFm7m!E= z#Z2^uqtyJFADbAmNlnN0;#7?RBC)=v%7hymY6GQxNC#eh)MV+}Ug(m&h;*U>>b}@V zZ`Hy|vWc08{M!`%419`Z$wwA{jLwsfde$xuTBk>>d_6MpyP}VMo>S^?;c-FVl0K^+ zR&7rYUC|UW+tMRfVBW^9=MH)#76~uE7`WtfUrmNdvl(mR3FWIAvu=ccTRUfvPTSdU zFg2mo_3pM|%mg8oV`91S!iH(9g2udvV*eOAQK@U}o2K7p`=7Z;Yl+TgpWiiZgGABZ zVVZ8iH9=bDtyfmt>bwohy>&p(U1UQ;&!Eh6dwa6-w}u|zbDz;0RlA@_@5#~L`X8VB zF7)*r8ZWD{%W|54?n{=ZchZRL@@-njUWAyYyw%Q1?@6(r_;A%NkxON}J@^7T$^oxaaB` zH|C%5JET1;ChXd|bMfdYS?P0k>|!lT%$cEcY0CG&0|w>^Hf&2(jkm+pBxEjM9#{po z3$Q<%)tI)*U7vMq)E%FwWy#_RZfB;9NiZ2THmGyRGGn33Ezho)wGW;+*Rvqf=kX;8 zD>)enFL#|DfX()R?rgTAiU$1Lik;^s~OQ~Q8Yle)q z)i?D>nd`Tkrd*Yf(44~Gv~|Xky$d!WNf)Q<3{m&cIEjr=fsNJUn|z$+qd$9ts*OR`ae#7H?HrW-nhu%^yHwH+S4mP zMK6u`mb^c>PVTGYpw>ro*H66o+bK77nnuJlTNA+vw_=v4mOp#PpZ(+Jf>leLcY7pG)N*;$~--$%G98cGGu)B zR#fJxfv5-W#^eO+%)&wbx}?=|hUUTf{W@8|fdc*7xsQuf;< zPm&n6PJE)%qY83HCa2yo|NY+467 z70fX>iU;r;^S&}p?FB(2brKD9LoI}H_>W@qT#6kapaaP94~r$KRupG{Jd?d3o-o9X zdUxR1d<6;EW>(k<%`}dZR0{J#a3qj=wy>H;fz!k`0dO)tKZjtWf#64!J?da73G44U z*qlN=BfZ374(J7K2pKcGF?|F9x_%D9tUqovjBPm^}f58y{PQ{k$+fZ zcEjFR#tE{)PrJ8vT?qrEVNel{2^DRKUC=HA$=C$8G>s&Y9kT^styRq2V!6b=DNU)G ziq|H$H)#i!b;<5p_CTh(t-0e=LqsFU4sppL0?vRm#8oK}@9YJsxCZq04)N;d5Bw~n zBVl;qy;At-8~2mKlHQH4A2MGx^wS};Z8N$<-$tbegxE|zROq<-G1DYV41wi_RaI+1Ioj26K1;gJHDm?Y(;U{ju__2`*AblAbwn*}@UQ$&##&6NO$c zG-MrG>?UO`i;~?Vb9dN!`*!=1!PBnn3wXy(E#Wq_} zE)mCb{{q{CvLQi{rEpd{KL>vU8dFcJ-X9}Xql}w8U)jDj?*CKuncUBa{*M~h7!ic5 z4PvFJgGOR79|+OW@Bsy5;bCADbZpS=3Asq6+WH~^kukR1I2h5|o<4M&%~K-tVC z-~mw>%n$YjIsyYv+Vx#XrYKLNI96_m>@L-+e{BYfE@>aa=&mX0qSo7 z7y+%2MpMOrA%-JnJWdZ7N0$JepwQzI94Zz>tB11m2x!CZ9LwPt$#Gj53!>ok9`6@OEEw%g!lUHtpklRrcdu?(|+8bgEqlI zaQgiQP<0-7*a@e9VfIr_;PnSOV6_IwR)ZEp3U?OaQzrns0LiN~a?pn&j9UUayj~Yb z-Y;;YKA18a0VOC&&&Txg551&?9%W3`9mzsI5venoWdrv8CAYnpdjk8+RjIg>SSWXkyX92@V4Rts&N z9^t7eImKE)L&rPWKA8XT@v|*9yM?WM-E|WWoVAR3P$$-PCi!CZ=4m5p@~4zoUTLw^ zt2&&#YlgUk)BX79(WlJEj~B>nX#3h#A28*v;O4!+%Ejtf(Vh7t zPCh$iGTlX`_wcx!)QYd*Jig_Q9(+N@XQ)@E(Q2Wn8v+UPe6ccmnF^|{rqcSar)J(e zTy{RwN-kN~t-3O3fD-G4$DHE^i_fko-+O0k`i*D96(a?1e~w!=+4ZUXon5aU_vcn@6vMRl_X|Jq8jcXn#)iQjrkqH-lSCvdTlZY7QYC&5F`ej6o*# zI9Ls5RKyM7n#)}&gePv<{S@(RF=jvuiDEG%AeooKXmpSgN__LyIXP01tQ(lDU?)o; zh@{`31H&9Ps7O6VT)7(dPP$SS-V`ZO0sRm^$eK&h2v!^!)}TPgE@|UX(&jA606Y(q z2E+)lj=De4um= zQzD_GH$Tg2$2NtikbvnNXe9SwyME*1*E)Al5p|={YoU6;FB(+1W2M>yQ_M)x0r`%O zf0FnTs2lbxCcPp+z=wzq!oVv)#Q;%R&yt@VSIe5FHl)X|ynI~1PjiTH(xHaW>n6np zF517ee0@Yr@riV;H+%tW;6)cReGSBkEz}{5Yvv@6#9NSKBIrP%ZV3-@xnn?H7sS)N z8(eB&@V6i~5rg`+-j8J;jRGwN%gWPUSUFVQ^`FK{H`qSeUdM8eUipWC-x`%=ldp{k zIDNK#%`6SkjFs&N2A|N4ZcDMtt-Kq&Y2^d%$=Kg?en@EAM8C53=QajNuYxZ2-k}yu%nc z@$hVNU>Jz*uvXdCoZNL%vA%UP#wpeuFwY|}FCY^&--C#t3QMh&5}2m&ov}xhDa;F? zqug3qIrNn7F?ab>3O!M=hC(W?rB7ETX4q^jV%q5J#z9=ddN$5knD21sAJJd)4o5&J4hQg;E>m@O1wxPj7 z0{{^5C^!U38JMdtVKA~r?2*^Cebcl>@*Fri1CprP&`?9jWr}H0s(@+HZA5Slj$VL4 zK~pG84Cu;mMj5pmPHvJt)gWRPcmNbJ^by7;o%9BC0IEa-*^?iI5B;IA@i8zGEO~&) zspP6Kj=LjP1T#Jt7ak+e*VQTb4AzMtw&VMKP}an3h}sC$AmTxn#?uc>8t{<|!C=DZ zhXtDl%$xFvvBGPcFw!%{VWN>#92p*^z6v>pj&Mj&449!Gs}2@}uvfv~REt~AanVZ` zEs&^EgqS1fwG{L!0)U1zg4F|DMgzBg89fF#W)wg4PKSq7U?KSaFe+}86*FodKR})Z z<6)Q3nDu#}6IUkizG_FU4Wq>DGN*TRvMFm&QK_SMe z*;BY^AZ|=}#)Tj%UF@;X7e*QcvhY|wFjPzziQof$4;+nYG(_E|=A&X02Y1ca5!%n=(< zcfj}h9H?UUBj{I+kzp@P3}k6DZ#OvFe<1{JQG$&~Bd9r}Sy6GMP`&~wqKqJmY5t`1 zDo1?IP+U+E@&aaQrhy`6_x&8Y{}b~JOI)hJe7~R-LvPBkE`jG-0G7JvAk_86>Nu#J zm>Gu>u;JURI(#+7eF*qaJ5(d|SHVIDf@f3?Wy^$!WB`Hd?)mf!Wh)FJH6zka+*UAVBiia>!&DeStP#bLyqRk^x;LMLWT&8_|chmk9H5)d&6N4>}moThhm3 zxIFl9`bQ86c1FSnp(BsjxI4%$Rxj9h5*)-C3j}H7BEcl~Mo`d1$r9c=-m7r)Z`0K1J%1syD`yupv`Q?QV*WS{3oGUOlJGvo$oZ8HR z5v!(EO2*0uolO_^aBDrPJe0LmY<^9?(f;XLW1XkW3zZ5BJGJxerBe?JuM6B-(i;Ed z-{E>;bkbfic`Yv$<<>K1vz=Whl%S;+^zOC0ICuFNTWi~enyR3pjlO~tj^vqx;> zj~cx%M!({m=JF4AQ5j{;NmW6M-;~Q=&se(Lvs%6E^f$pB{@-_Ovmf!ZamE3@aG^Ot zdm`jhOKD)_&M$Ui`Kk> z<7Z@#K9P~RZRGGL4_8QBs=L0fYxve}CniTvc{HqU@0feLk9NO`-#Sh5neD=*qKUUw zC@nmGRn1I#Zc2*bSm708b~km4*LBC*s#*tVhG$>p)kCYO;D%Xqvd77Nwio*T-l!rv zY2YcBkGggdf|5Ggosujq_c~9TOM045O(Taq5Atu!X}_6Ob=u}zYVHEVriP(zC1c~S z%@2Kc(Xb`m0Vrs2;SB2T!q~$KB#^V2PEV9Vbm( zwvO1#!N;6I)h&?Q+zNWsPV!ya`V)iBtRzT z`uK-lg4HFAVQ$it#|<-ys`=>KW9xhs!q|x#_)NBIPhn4cSd@v2E1@YLaTZb}GsN;D zN$A2bdGJ*bp*W%ovpaYipxR+obwIUDLdUh`#@!U#!~;-p0@?$V@( z=9w%`@I^TugoJ>m7>vjK8Te#2iO(ntJP863ohN7f8PL2v(o7ACLLpB!-slePqK(6P zftPF!FMT=&?EW*h9G2qtL5N1YZQuK|T-D9iyPJPpa9(86i~Rxnd{q_=n7rtq?0UQ6 zQ696Zz6BK~x^g3&(qxKm29n=`$p$o?)&oru17V7qg;1Yb zJabF~X*37q3Gfmu9Y?|d1?oh=3ta;%^#q9_OtVlt(Mk9@f{_%{yx!i<2rtD+`X*Ea zsdOAfcK9bJzX8b;L`*hJQ|UO>>FAph*oU=iP|_TjjDCQ%ff)K1oXDv66IAv)iHQb# zJOYDZ{|*~&LyRs7A$b*p;&BFWa4Uq^V?Z%K4ZFY&+$C8tr#1COd*NEDY70T!B6LIfKKS2N|#nmS@6FUYLG}f)T*e z?+7trW|I7#SP}5RXa%UmuIV@?KL{#x1XppnUmii(2(3Wig`gfHnecq7Lp?XCG7u4H>kFFMbDPdBf^5$zYH( z98B4x4F2>Bq$Gj(=%92f2kn=Ni&T*bvh1P6SP=03>@mDqrlucgAM=M43Km01c8VC0 z;eoCiU>?9=aX78hL95e%%vRBv8b-lgc?zhoF#tU;Y0&!%NCmdT7*@#U2Y68>f=ui} zDiVnY1>6SgT%mXY;O{9!(1^F8)}2F0g55@t!E&jbuEK8Nj9$ z?7b8y0A-N1euu&Y)-v3%(RdeTd04au zu|S?&$i$;IDw;;xCpbyWo64Eth(UpLUPD?TLJ&VDy6tS7%K-0g;G+HdI1qvsFT!dU z6&x`A@l>$^YwQm!bo0=#+qP@TEI%W=a)i&7lf&OW(OETbh@7}*T(X>w*{8?h9Zf8%Sal7cp%+qD1gQP3!=9?X> zcrzodD&ftH)~LkXCm+v$x!9Y(ZH>?Jn&F*CCUp48Iog%k4}4?&O@CHi)77`7$6h*X z1=%~Y3}g0w@KU?}@$8b=>cVxAPa>bL_deK?ezE0C=Xb%S+Z4sbw=ED{nVjp?rFCuJ zWZq}L*AuIOAF}x?#VS*G<5#-VC*b+)`ZXDJ0hY_y%RWp#$d{O z%j~a-_fMInPMhi-x-B*$BF}YA#Ngp~Rg#OJMB0>$dB5++*Kw@QTHX8=X-jYJcyhDa zLN;j7;)v~21&2?s%2PiVz$d~dGp8tC>$&TMtJZvu|M2L+vMF3(y~@0)lKOL9uGfs5 zreT+;80=-0<^M@v(eG`C*vU(4_q({vJzn7!C?;MJHD{^jcJs*}h3D-WvwH12Egebs z;sGfYnt2%;9IG?-MohHtnPU7lVB}Y;*Jh>);?>bU0gqmZ?$}e{;>~C4R~xxr-+GbL zIkkyNJH(HVw)3cGHI1)U^jRyLJj~~w_=sggvhF=J-<7ad!(eE(kG#Z$@%o~^X4^~T zYgQKTeKu}@=#1gFMR)GdxY(k6+P%ubxypUM&p}gFQ(IeWq3|q`aiTN4*14&q@2rZo zig7->G&cFu-jwTZJLJ}!*y~}MZTjJ`v8hq5`|vGC_naMO>7zgHYTS8euPs5h;fD&g zr>6PsFcougvgAMVbx5?KSoy3`lUjRb#5^8lzImpxg#75FvFko|4RU&T?cuzq=?UZb zD{ZH&*v2R|EvTKdP27gnAVAu-~SYQ!A z!VpJyKTAZ^3_u*xd;sE@=C@L5^u+)W8geLS2(<7XNVgt>HSA}lLITq98x;(Qh<#3y zGL}jfM3y7OPOk!80s@n#sE&o*5Ss890QOw0vyX6uK;FL=ZWDxd^)8o9QZ;UNg=b+z zNK7*0F=mh|9VQ$Ip;cl62{vXP0(TBT_i8aw4zh4mV&Jk%d%s1bPp)5C?{hyXxM9 z<{%V8-S6sN$PPqxN%0CvA?!;F5cb27{bEfy1&eq4+r-XFRq8rK`QPoT|b?BIie5>DS|nAQbe<;r3nRV#D`i`b|sHTcDFf-#zr zZL~*F9Xy|mxfg)9Al%afm1AdWf?#-Fup+j}CHH}BqJ<*V?ARyii6-mUBV4==8_elf zMi*@aZd^@*l^Sa*<5>u3$Vv@k!@ZPax>$h<$&4KHb-2{LUFvtRL$Y+j7!puO2yDRo zff3YigQ9op=J11Vj8p-UTd+_7Au+@7f(l;1yU5jK*r9`S0>#VoT>!pQH-#$~2MJ!r zf!ok_xfH#GOSJ^65!Q;GsDTqP;3@=Hx@0`gS#E}Y_yFIAc@01{RuvTzY&b^+a6}nE zWE%+QL{LCk2onPdK|~U|;NplT%V8{V_@J8@L-GuFC4tsqo9zieB%9$wAqR>lkEOyJ z3>f~AU#KdGzXypN$J^^*UnxcQ0BPOuu7Lnn+7+S#(j0`49< zDW5`mBWgmnd@v%R0)T;6f$6Csa{Y5KFbdv2J7Bjr0%uqf!A;dLP8h;)f127Qf%l(H zdAtM3PL&#AJOypA&yWn;y>w*3-))Mk7?{AYri|=Sc4e@7+zi<+jKl&pLg^TG8(#wO z+elzCCLFMF1m4Mjn0Xs*O0Ag?fJI~`hvVgnXazcje8d4>cLY3Ro`Tik=B;iZl|YUP zdLfUy1X&YIH`rRDW{C+w70I3232CNua!`izal+q*^9i(<5K>MDZSYhgxYmG!#_FyA zI0-A|FvotGKprIu76fZuXuqV(aQurJnLRIsnDXF3p5L^^SleybP(&71;1xk6X@V*Q zq(%5@46YQypxO^IVzvTp$Gn4w^*=Zu(&5Lf*(y@ z0l4fB3HW1c6SSH@IG%I?b}->6ar}Ko>Eqr2(%}r+N7YEbpaO|FA_KVN-Qt`O!`es| zDTDT~^hy|2!>r~GXGm=XXE9C6*sCEaJdlS?y&&)8V9p1iPi((#1{#cj>Ccljc(?Eg z25LHGyV|t7Twi(7BXWU+>iGp9HT~116F+L-$TMH3X7;H}MzLZ@<8r&YC4SQ)XUgjA zXmp%ax$T;x&K@flhqGeiz6{$~c;{XIy8DlN_uPq3x_xNlsIVCAU$e!9-P4rE2*gyU zKOOJUtWkVo$I{CWC1Mx76k4lxaI}K!?97{DFRZI>6|GZh82!D+_QRR^7L(mv=W0s- zJkq)6Qr_|-6~))vcFtaQGAvnA!Dy4yOKTnDkd2pCzk2-bwQ%HHgR(le?39A(hYFqe zY;CGejg7O`G1rwSn|y44bVC38d#%oG^?4=eQ6-{r_~n#J$EJR+4?>S@u^D@GNyt!@5T`XO+c9zVJTKTRlIy+nY%`o!; zhb;tr<|#*mC9b zn0N2b>|A0#X7IXS8C^f8JbidPOHF9RdCSnW;FX?b0*RP4*t4^SySNwQ|IZqzCx;pO z|7q6H*(f;FuZG~Q;<->s%R{t6+=X%d_FoXMa(D=ADY3vQhba+cat%;KtAi)V1@|+E zeH%uDWAlR#RIt?5L;nYfM4+qEEzAO9o9B{>Cjor~!{fRL#nER0gdC(>(Yg5rQVCe~ z|01n8AR=Wfx%{GO|K#2$i__cgd}$&T-ovnS2KeKS04@mWas{76fGh5)Yh}X}=JgH; zQ~VcG-~|*xB!coc?!iIOD{Wo^kJGZh3<~Xfz;1Q$vMU>#ekep9tNwoJbC3U*b)Ky?1BpQPm2Fy^_Fd>yU9c; z!25fqF?4YA_<$j?s$LP><229co^HS0^7Lgv-r21JdAB7U4hZ(Qesp%nx5-~yjMpqx z8n$oQe#;lzNV!=as%Wqym1u`^)QSX85GGOwZ>Yg{W^KUb9C%G6rb!>MC_IU5Eux5S z8m1f+5M?m}#B%64(Jb$>+PBHopM9sbR9l@jHTLw#SMBBhs$Qfuvaa{N$1yNY($i3^ zkfOz<5+Kd=4j~8=2V%1%&mh_%h!^4|$puKx0m20!ag-$DqX^#hAK2yhfRh8Xb0E$$ zP+LR_zPh#^FoFCpa{QSKS&+FlIw*yV|D)_nf!mBf&j^cKcvN<$gi25 z5i$G{^)sra^zp-?e734EwTWlr@CGQ({J%<1C=f=FgZKt>G(gG96nE3_R595bfh(h_FAkj#xam&=GB z%M6{c9|vUrUy~22wm?{vpn3sYM}nx4!d#q9W&y5+grMG-T}}hTND^?>0XW2wFjW(w za2Y4x^OIPI;s(Il9tCY|0^zNVjCuvrBYz{XVTs`=Ih=w|L$9@yXj~9Z(7{mnu6v*?eg1 zvtyN!HkuMz7vpc=;dEhq6qibyO*9$0!$Dm8kp1}m*XCcH9d|ff)#=87G`+`sta#ZJT|I@Np+V-8 zTAPOU`|?s}YQM$9t;4f62Zcx2XzaQt_V3VRQyOY^=qs)Ly6fVnx1NbI=d3bx?#CY* z?xQr;{hbBB(zt%MahP=(gzv<0(w0bz_SKWG54LjJkx>o8RFY2c~{9S>%-;FI@ev2lzlnZ zD@kcu?8ro)wX3Y+ef6wmj=%h}>&~eM7n6^6M2IA^wx=x)zMp*ImfsuWr*m)KG-!fGcq(YJZtLe~BBDs}U&_uDx@AYZ*}_|eZ-CI$<2ZrgXqe{JI;yNgjLx2@Uu zVeFOapM$@D*Zh@R6~BD6@GzN<6}s_39R@zCEV-i!d`^6PlO}!Lz3R*Ty7Wh@-j?n% zt)F^v&;W<@#CMyAxEc5+yY6uaf0X#fN7KM=(S+>T55#?K4vlC}$rttTRdJ7*KJd}Z zjkRY!tXPpDK4fP4mAND1jj!EI+A<~fv8jNVnB^3W%KL|JHchbBoO%E3i!qO~?*3SO zNB!C%*CjQZ5BB?K@~!y&PUcrv?fbXx&Ffn+=N?2$bxQ{38K}3_pDeRBFm>Lboj?7E z{Nz$^R%f>1fq`m#S|d~^l|9fHdFJ7aJF1vxZ684PA+g{Wn)%Nsz!&&~k6)FqGC8qT zc&L#6KnwVKW|qe~uZf0Z^~O&atEa0ASI+XuSiwi3(yWj*o+q+`213;Y{4YSyXa05d z{;LZcVe$bw_AC0t{P_%CvBUiaHw^0oUig*LM!&a!tbYc#J^Zu}>8h?iAg!>`E1-=u zdg)gL7hbfKrWoV6aIcdyD1DMcLQx~EJSV?~$1cMQG;y$}FG4S9+=kIx7y@```9J>? zNMN&voHFBorqq;hsHx)Rl(%=_hC^dvuLdIxh!}Bk8hB*ps6qV-IPXfxhJj$&j7#kW z(h{WF-A8za+_BOP>I;d`$@nF(ln)@Lk4}(iUJ`hyp??A^AOH4D=x1Y;IMLrv+!v-X zu(Klfpi%u}gZdLZLktc5;rE6r1Drl=OM%x$aBow3fIYSxga1yTh93|QCeW9fzTeoa z{LW)2Jl5UAD|D{WajxzOFbU}iNPE+G{TZ|;*5qGR@9)Z$v8C!oEsgCz5i$=tWuj~2Kz-hgkMjdQ6Dxm09%waYH+NdLkN-S6>ML61^2pX z;PxmG>rz+);i{fJY5FwV(3^bu%`m?o8>k(EDA^QTID3Yb@cNX;^M-(`-9!6PH)glH@56dR+pM>_lYFR}g+vD-Df-xTHSiJxj8>w4>Bftrtd^;lQ$ zkam}@vPvxtue$>kX4Eg+{`){U7dm=DhJTv;#_RQ)Ky^%uXQvCF-NT6kmzirtjn{kV zpHWzU%zQ?sJ35jmE-5v-vi_S9pO=XY2??Q9rudHq1*#X zSFK&~>X`9fP1gf&qLq8Ze6H~6mW>>4`2AZj>u~C>-EMwG-hZ;W7OK2|;gs?V|Mmh; zgdyTYIX(ikn1Xv3sTTkxQ>I_T13XfrkArY|6bdbyi!(Yd6gnwr1TEi|rm%H;qHS}{ zo1%I5Uh_v*_W$QW)X>?6cdlHN5xyKaX0GD7v8Dq*I(QD^pPqU7oR`o$S&zHP#k&1# z-fLTKoso4rZA4PYN(12qU#1uUp^G*b)aYs+SmF67*}2JOl)K;?!v>|Y)ON4h^DWcTq6&?Z z+U=c3FHZW$Q17LP@srXYpA>7}&9vd)ICb2(oV+HPeS;tyDOv;DwmP|g z;#Ibb0^{jTE`-^YcY`hXBk-zZI;n2Mlm(aZmAC=+L<8MIl;)XW!O2Bm<97PxRUOD5 zlI#Cu#*c8H<)>s9bsaS^bQ8|na6xAJAtnDSE8Ws_S$S`?V_r&ab+dQ#c`LN>s zpbB$~#a8x77sr&%QIm1E46lTmaINeXFX2;~!ur;_;pIiAJ`L8$*%WyGi?D%#uhC?& z!yB8bWa@N&{FvTea5Jc-*i1RLYQ8}CsO_N}AMDsKXPd+tJu*uAvUP`AX|%XxMDcjT zuousJ`EB*5d~&~Wi+|S6=FdwaY)8ly$QjjctlQLKt!i8E=*#bz*ytrhr{{()l-x7) z(|&FA&%aLSzLLiv!S*sFvZWO^dakQl!chSPU(^Q9W*ni5eZz%mcqKpJ3Ty+UPPfs& zz<;5ly8d(GDSzDsFX$U?CL*1u;jf(FAfG?F{Gh0Ng^#4ubFbHiJ0cfL%xc$@tMxRT ztsZ>q{)^B{BrxzsYRUmIgJGNg;+<&F<7I-xZH)VDn#c@2Vx=`OwbL9TwLv|Y3iMRR z=9(=4SZo3TbhN!_VLK);)`q|P=oay!ci(EJ#KqoK?D}b`pAgc2JKvO1TNI@0y7eXt zRQ!5-)8MW;JGy(Jn}_&M$tT~~5rbkqFkB`|_5puH(hxEb=KL&099P&zFj2Q#3@8{# zyTn#TpE>iNZWMCj?jE-P>%YTq|C2`|6U`4sRL(#(Q&RfEHn51_$6*Vc($Cflondz- zJ?pM-W6Nz%z?Dz08=<bHyB4{@ z|Mv%aF8f_EiYBs$vsEncJ4l+nh;JiqKv|)ggO|2$Dx4m zDCHc^*+Nz(sE3WA{N8+mhzZlQZ?GjGiv&RgS(NuM0Fm(RGQ=Kg=TvMOD5zfsJ$=C4 zcWA`c5;1L?TvLst0^*37PqzYed3ts2>1={$d?~ z&Z(AQ=(OMZ22QEDG~(W94f%Byx%-Uj&##5lVFk zbCjLfFm_%_$M~S^59t1em6p6(*xSVwxw{Q`4WX~<*%a{Lc{4- z2-v651CpDbKdj?V3t$T!IilbJpIL*>pr)UeeBLG>N-%In<`V#2oWzbpE5dk+4K5B)(dhMe1_eQVXt;R0_p7q0j?*v<0r=qc`=zFSu~E}Erk zpXQbJVPtgj;nk^4A16C{6*+5-T&^xutF(0ewd&~p3yr_+d!#jH)$lNhTS+3B+Oq0- z$A=DF?!V!L;FxN`nANNc~ne@A+U01D|cz#2Ry~Uw_($)r?EL=JM-2ef%&S@(HZL_m& zcbPXPUm2A?+(B+m5P!8>-u{G*9xNNX;H}eTJ#|_u%XQv%>W_-6-0f&n6Cp3X(NyXm zH-{jFAH8R+1__)zP_Rq&taZ-&YdzbITCaUqOym=I_0#H+q^ZNJ<%6m>n7mx#C$~2% zB1iOJJ(sBbvdpDxj;6%EaJ2C*Iguv+YOG=R_lIKJ+6_O9n0a}Y+Yx?;T77L(ZJ)!t zh2;6lizaQlsp4>Dj;LqXB%ia@5`$Der#yQ0FjA9`f2583rNlGdf|G;!jZP}9lvQ>e z@4Pf(@!_z84-%*H?g@gE1O?jjABZSlwl5gXL7^DSpqL-gZ*7M#AhOe(MLy58{@sjr;hcefRtUO8}MROzK%ty6oy?+|Snn(b;0gRewf^Kz0!H1G{=~ z8$4_b!DU58vhdMYdH<)PLWTa(xR@%MtiC#MkDP3QvbGK3DJFuxtfiJ$2Bj!6Llha( zMkR;cGYg;$MbMI->0=I2Gll}Z`#bdMjmiWMSmsd?2>$kZ7knoU*~7!p5o0&nAZcR% z%t@2R*hxKv7kA(VWyff+nOC%2U7{cjp)PHq2%F70T>u~@TQ1OM7}ZCh&Espn$aaB= z5<}&2^>-yG&FJUa9ok{8YcbiBls={M2P$iV`h zpK|~^_)q$#%hF}^%pj1{*SP$IMZX(b?KyT)==$t6QDCiuQL|ioL!po!%%KOewoSRC ziem2kMQeR~U?fhCOA0T9(szO`ZgA#z1v3NEo9M@X;NY+NLTPX$+VDhDCM#L9_)PG2 z>54lJxRRg8SOSRr=eA*7t$?a{zx35PJj`QlKp3;T0#9-L@njzknfGe5NxdS1Cpz&U zQ&0JF$*hJR==qy#gm$QmtNLDZ{f@!K#k*L6)6{tTg`EhzGYw_~*cJMPS`N*yau553 z$&qo$nD>bp;l?ELBDTEWbw(wQ``-FWb+y~}n|Dsjcz<5g@ZMZ{)&0VMb8JU+-uKpR zT~Q|)=rm+Qke1cHmcZXcNOx2C%5NdZUhySY%mjeF)_FIL6~W;(;h7yEm9b~;v}A8J z?nBC*=7g_N1C3&X`wg8v^y*r&;n_U*j2NF~$=87U2LerV6}4P+6F)RJoef_j#Q$Yk z$)*Pnrrns+-bJ=o;4ebb`3zgm(Agq^a3GZ&y0}pv1Y@C1_2MQTL-ArSkZt%Wx}&GG z)l6D~?=E|Dv^6!oRP?3BJdSEJxbGu2CcV8~swoiU@FgzHTg0k}4i1AKNo8N=iHT$f z3LZvbe)s}5qV!&bpWuKE@%S>fK1PG?>lKyddb;Hs+Hvg+4jl*6BmUqAU^^rC3tB!o zTnn!dgI)CwKnz9#m=hRZUO+|dCQwc!f|3GJ!0t<=K#fBIczhnJnfVFbHglfnYd3vw zao&#!lwGjkxKl8Sb?c!(mVn!;U_53A8+%qjA6x@LN&HR}?~t&i-tSExYGC3L6;n0A z-1!bzLRq6z zt`jAUfeZ!)uF-A5Kxpl?kh|lyrj;mbat*%>1aks~FhVA?0#7L5OCaG@WaI%(?4}gs zdO%4E2#Z+G8HaFLGqV+W{5DI?6rQe#sEqvhk-JJWv;%_@X#C#aIdv>_0KXogWM$ zq8EC|0Xt$o-2TzAplMUixh=U*{Ba9u>;py^nS;LlFY6VJqbJr)z5H`S2ocX`++5pK~mOr!_IHe%Pqa4##obVP)ZAZQ${nDT_2Spt)x>jZ4r{> zQDcR^{}UhFcm|Th=cy`gcLPp|)t6#DL<=ZG-)96;fel{xb~PdjB@wVjRL+fho-39|jcgDfxqMTBwxLlw#g31{>t zH^8b{dao(hubC;z&r88ybVwA?Bk8@6zTFK(#rRJ0>@dH7!DH9JJi$$HsT}{yxZ%I_ z$99>Fk#H;41`8LhT)OYhKS^%U$}PVp2cUJIoWkz&B{4U7$A`r&U1J{%@i`;F{iIMWo!3Fr(9)KyVS2MOwr zVLcon88C^l0NB`#DGZR!i1Uly<;HPP$x9f5G?Cw#%JRBRoZ<~TNEbF8REAgIUErwa zH*qrv7dq>k1#zAS$_bIo1>!N$#5$jXP&$aQ%m#ws%MZOY2oZ*KzSeTsVrDdYCwK@& zgfoS_4THfrn3NFrol^J_X3S%lF~ABrlE?&6_~0=j79O2qzt9FGnMHjrH*`>k!4}jH zoCnY~Oe2OWaMC@3${X;7k(8uMf`qaQ7Mz@qA+VVvXCR!@Ex23SAKd<>coPf&Pu)Rd zE_Zl}0IWImB5Inl#|#c_7)__@rIM4FY~J8K(T1zxu)92{M~b-V9G=I|Z2!vLmX7f(&fvMpRtR>fIa za=gRRn=9cxZkdVhtNjkKd`_EZAG_5dZ5w~&qiUk8bLx!ahd)eQ<{9x-{e`xTx|4R* znv8G!#$tH&n*}!KA~&}$8<)f9p>3>mZ@t9WoUUS!b(}$5F4K|~lU)GpL zkJ=tvVbJZMId$>9&}aX;zuWn$`JC3R={idXXelM8nn=mqf4L>Ju08mdQd7nkYgyTm zL!6GTc)3M%)PkkiIo(&nEBOQl1Vs$F88gg(RN~lATKY+AU-()}`gZZpxTHNtuG~i= zYkMBw+EE4P+|TUSe*1jV`kp%*tk*Dk6Wapwx8q`c{958YH~9+ju?A%A zJg{8u%|<>|K0djF-_(2rBCXp~F7V$RRb?w=<88gh;r-6--&J0;-dXZ`<@@^d@T%x# zSNjX>&YNYj`$WN&X#>Z*x<6ADi#BmyKI`za#p~V%lur3~+@WbxE#w}G%o}p#DoaT@ zd~9HKzt#d7K7PBZce2UB;ae{VX5Nqs*{?G#OuPN^&PEmHa}|%*!jM5Z?EdClvcF|b6QknJ*-kKD%!94S(zR1{2aZ> z*(z>_!*D(Ur=Z%*DVpUj+W*cze_vQyqb67SY`u~sf* z&v0p@4g9SK1!WH{eUeUIO11 zmsm|~R#zC;vtP`y~DsD|c` zCB@}NEFNhRdrbw^&swOW6c$sj)!PUafkYN!_=tes-X}6+lJ{?_Z24Ns zHGaE`1=bfS!6 zLG+7R<4yV(8xFNq(cAudh%WYR2FGfy6e?jg4qXuaLPj+U;Pwh02%RZ_iGAXwy`2{~ zyE<&Ua7bS2(6s9##EQo+jsKbzs{8zV`it=|u5Ow7Xx@?N@atOEOJ2u@NX~p^IBJGO zmhMvilnX752=cuZsDs7u1X<*`P#^-2)++H7l|Wc|)g_1*Q6@Z9KqLd@IzhsM(u5Qb z7QPfM66$gGWF@w!8WSYEWBcAfYXL#U(oQAe%eeVJbpecqd8lM$9V4>}h*;hVBE;D< zp%Qw>W9X>K{ic-#(Rl!M^(DyJ0BolUtr@(-NQBbH1P2Hj*bky)eJpbn!b`ovKxRkK zYQDw~bc;mfp?Ze=3jnu~KB-H1C*WFNf__Q;O&sm8W{q(>)dl*8p~28NqY#Mz)~#ZS zX<$k?3ic0-{J?-^#*34g?}gCKN!};hVb!-nNukaM-*W=2;2Xg}4J@!(ayCMM;T*pZ zF#tu3(ft=;{CR8Av-TS~Ag81Bk27`tT&!42ka><~Ar{jO4lCMx24te#o!Ccag@u(>?814R4to{;K)yP^U7U>Zfwycrr+6`<7_W zb7nVaDII!n@ME>5vy69(5zReKn_P$hQFK)Hcgd^ttq43AIKK9qK_WtGtf@r(&7w^mv^oc5Su zbN6c2oJ-}EYE=RUS1L`gIXqIkVAOrp_3H2CjyT(%cyiFe)_>t?+kf`TxjCs%H9xH_ zes8gq&gA4m|CmWh0*6#QoSNel3Y0TcKBU{l`rhnqz92ec;me^O`OEs7l{GK*zaZdk z+-zC=mGvq)MC#*>pTorY7S%lT@Np2Bvo3mPxRRX8+>w=0nbqftXBlOzba-;pxw~Hd zMqG`($I?#+UTg@Qm6RK-ZY}OT_d>w>X^IDX0{vS<@0oygz($XVM zzb#d&H07I9bj9qAL#_IBA-A;s^2YznaM*g}mu8ju6+~2TnKv*jsAzBlB@sP=8&unj z&QL9c&c-SX8|H2FKDyEP#h_s@-EUAXk4q{+*REU2gf))hnbIpyu@udNY0Nx@{-F}8 zRAz;WG2E$uWrbM%VXVrrFsmSrCC!AdQmk9!h&v(wvj2t$ba{(4;G8jn)9$ghLx)`Q z`@qnSJ04@x& zGE+_`B(R_?OJM4O3sHmBWyU2#^)S7L_!)^(n5Q7di&U5ZIW(qQnQgT)R;=1mA#Dik$9#8ls71azCtFBbY6ayD0 zMEn-G>+6W;`YNmmZfNlUH%Dj(Q7!a{cLUY7U$^K~j45YuCAbsMLENbiVrn2I9d=x} zOqlWk%0Q$5VkMPoEv~9)E7j_}?|GM;_GFshqv|s4Wx5lOT8u7Gs1^IXbFjO-U~NL~ zT=k^X?BHyTiJXCeke++4j7!g`SfY>Ji1l^7G z2m}`is~vadHGH()<8vx-P4jligx%e?Z@lje4c?0`ov=uP3E>aSj;Vr&{YFs97R9Px z6*44MfH#Dq!S!Il={rE(n$m7BjT|X~R*Y%7C|{)88wCcEsJ?giLI#mf5X&{k9F8Ev zQ!=1r&cE2H1v+nDHmK`F6Kx~(`3wpnN^RoUI*Cb&G5@d#AC1(o)^IfOAMBgmFTNd| z{3cAXSIdQI0CQ)Sg5sixtQCmd5$MxS{Ry$>^g0{cp>ClV!(#qL`9YzFa)h^`P|Kc4 z@OoPC7L0nxRAWCzFcs|x8WUszF}`w)yQkhG4#9=;3_FS%d}$?~owa zK>{Rri*f`cgf(x?29p+~*&D=iRt*ASL@%{$0sg#$C=pc3{%IWiv3oEewuCsShs_e+ zf|v^fa&O^637RS~KJsRZMo?qDM?8b|VLF~m!s$I82?Xzfe}OGw-bbSMfWTfI9!@4zw01iyYV2m*Q za*N5$$RniVV1^KK_;2I)DfAZ~p;C zu`Ej*2Uxm6yQe}JksyQ3Ndc6g4y2>XYj~rid9jx^gd>21h?fBIloAh0MEqa1$9B5N zGFT@f7f%HX{Fi~<9Q=jh$B$Ks2#>4nZr))TOM#N~8w7T@ba=EvEQP7)g~2f6U>Fa! zMFNF*>usM8UK_*w0<4#vUl1yhzHYDK!1ZOl3`V_N+CT(1aQ+gSF}H$XfUJ<2#I3~R z#*Lfcb}+kysqvpg1y-65!$)zR03)uEK+Fw54-*-1lNYG$=$BFsiGA7N5(w0DZ-Ie^ z#;Pq=tcKagtn)HADZRL;#=8*0p`-o!oK8i2%MH2*HIUJG2@?t`A!lR69Rx!)5kawd z<`N2y_dCAB6|a?&r+JKt(JD}0%hbr}LIse7M)O1OaxZN0>h=o7a>{)Ow9pH@Ocwuz z{)12u%TQOK-3z(|5_^!6HAo%tH=Uc5^<&hWpc%8lfISg;=;k$8=oK_`3^ZdwB?4N1 zJd=fk;A>8q)Ccj0$)(C3@qN++@Z-E@_m}&>6Zb=J z2TTQPuTh}MRP@BO4F+Zq2ffED!Ja;O2cJN^LY?DlB!3?=HT)7E*e_;-RwU*B|K@ z)Cv;jsRX{`8#!3zywcZ@zK=fu|Rp_LV<{_cnByZT6P2P_eGedcE=d;D0Xl?99C~ z#98*EiBphvU5LO_kCLSy1oE`kx&@rL7MYT3nZYMErp8-Vh-LBMRzI18*w(zk84EUz9TShF@=V*-6u;dqE!?)=CDA8yfF;?ZF`LyaDt061GkC z`mA2GFjHFW!mwAByJquQ$7VeobbC$L^y!5M9u9KP6w`@xuXt3E*A~$n{hHr7xzzVr zPRPH;r#d`Knms;^U6)^N{Mkfxbo6!E`x!?2-A)*vyL9Hv;$2hU&)xUAvgLg8h03{u z1eK(Wo{OX(;%~p5=N_GD`0(NKi}z;LJMTM@?RKOkB){R>+HqSx6zn|n%51Et>b_?Y zp~Gv32OXJsc=@iQu<5xeV@>L;)r0wTV_B{rwAvOO7Z%W-nOSo3nL>-UebWJH8Ilz#b0`z+3}eeTEM#Rbix~lro^@8$y%tXmDaF@k<~t zwKG8;D}h;NB@v&~J(`HpQjq43(7PK9LWC>}jUZ%W?1I?LY@J|AWn7;6;-+Fw z$YY+UR&4YKw1%CuLrlR?rZ zF~TU&G;ywa(PW~2RZu@sIQ{{m`T|Tx&`{k>*GJ(41NU9TFS;^->L4zcVo)eErSi;z zNI6V~iU`Su5SD%$Be}i|z;hg54UA4tw%bdqM^Z@M^w5Nr<(=O3=AO%~F>VOR5F=T+ z<c){}U{@0RWYs8IP z4p*N2akN3pTuid-_#}Is<@Va55eqilG@tR}mWr?8x!T{xW3-LE7^7s69>OE%3do9L zt-?Alzw;n6CSHX!SL;G=7b?0z-1m;sO~5HSP6WhPeT{?*^^oEI<2`jCje6y(uI6qU5OmXpl_QHB}Nmapa~M3qz**>tPlwzSkrwF@B*|3 zvC0!n2(NILS(Cp6EpCB%kt6*qJ~g?4D3d$zy0G;z3}JB)4D3AqadEVDfQ#BM;!2E*62nkJ2mjAuus)K?ey# z9Oe#H4g43Zjwk@IFdtS9D3kDrTeth5^^mv1?8t|5R&Or>UwoysF?|fRlS+C9b{U&O zz7Pk6V-*LehDi!P8H=MgL$wY%DXieT1Zb+@&-h{Bw`IDkAkGaKrWI?7jlTBv!dt@q3WDoDOoy^ zGJ&TEeB;MjSA~)bX8^9zqc!zn)>+C32!!5=^r>FvN)b6pT?_^nhysXVgv9i{UUF%0 zGf1fiq;&U>gtZ$%C2|LYD^O*^VlV<|R9P0#1=3@bO-<_rs|<^kcPWFU9zYt?o*MLa z1jO=#Z_ue9CNT`sHL$YgttU65wX`r|02QDTL3%hSNROcu)+En@S%C&RDwLMNtpLc! zqyz~u^bxi7h{nstP?F$5r1+Tc<}n~i{DTzt*knRYHNd1f1?qJ0U;Fj>U>Z4K5ciYS z$djz=&LFEg0;*e4F<3E#w@brS;Zke@?(?x7eReYe*`y4E+jp@cd89X1zlBwXi9na& zFtFSju$!~EvaSO{g+R9%cwATK&R**Z*^=$?+X) z7ZyGDjgyqOTd{mYeeE>)`0Ul!Jaq;ts~zhfaX|O+(~8+KLqt|8?EZ32J$IMFpj|@` z#yNKHXz6&g^0e2Tx>b2mp)nSPo2HCUdzcu!!Y^j{>H)?})f`2;6K*Zpp5mQcF{m}< zY*yl&u)2Sw3tz&Cr9D?295Ut&_sG09bh>)(KFbiR!*6}%Ys#-HOc?96LB`B?%Zeu_ zPCRL-EWe*NE<#lQozl6`0ed%9kDQpZ!T7A4WrCE%d%u(wE{{*|?KQaL@~|wRaF^d< zGjBdVr}TTOnm5|@MFV#XKck)C(4riEeg4h2TNNh@C`js7+#6x8a^a($v~-AbrFH+1 zHIYlSbW_#{u{OV*HhuL%=S6o_iw<^l?)~^~@6G9|!kV_41D$4z>JE8sH&{+U_PKG= z=6vJq@vF>7>x%IHo{o3B$ki5YXXTM9ti^Fh(>>G^lPge# z%s2Yxg>7MHcMiNacfiVwh1Dx=Z5bVIGv$s#f^%7s!}V!0@nt`}CdD1SYG7sjR&2!J z7o#PnjdlDg@LVRr=Z%_J+zI(u>p|o0NF|rJ{W|NoqH>RKqJV8~Mt$b#q!+th=pBCS zctA`&LQ`h^yU)@89GUu151&Pr^Dy6{S@#t7tA-;_4^U-mtMIK506ooI|G;TYR{p0SS33Kca zm**-eXf2y+>}v(`bu;YM3l(6sv9m zF`dG2sIFyuJ=aB~fIWm&V38F2xq4`3Na?s}53+%doCwGlBH>TNG9<}sB}p#})`^r!xa zNN!Y`5HJx33qt9)^AIS?^!ENdtZiGbv2C6|;bu}qrmE*f@~0sHt6?(GKNo+HUapdW zOR#5Vaxqegh=ZN@xVh&ae6>M6);V{*q{)rQtz*|*H2Tm`b-PV`@@0XtF|BgCYnR`> zJTlp8^fq0|wXNc1x$=fe-5`AiV={yy0^U513c=DO(EIs-3d2>c*=+`j3$sOyhXtMc z_fgi02a-eEZ;&S$P(od_-|2RcG~z6fG!&9B)`8Out5BN1#&}REg18b?rfh%2@Op@| z@o_~_H-l#Lb$eDk34QrGkhCv0)Oi7*=kzB@LLMTt?j;ZrkT&R)2{9%&zc~kHXxdS2qYJY-U2b|2I_R32noz3 zCWztoBCnnd_9GnY$?(FzEGky$3>8+v&qvQ8GGoA$Kve{*5-D8 z2-ruF7PcTl!|X5?!7BsFn2$bvmxy3i#3%togD7E`1PR)(80t)Q4?;z0)^aL@y`X#+ zSUyyTx!j;&jors-{LV3^@;*M?x zQR17BlcQizcbJ<1sF4XE5uav;LS=$k602o4-V-VFmD0f&ZedJ>c^n-By+nq*hS^7G zD2yL^nb4w~@brwzdkK-iAW96lJFGjLINg4k0|a>(p+w&$$2(qWDQ~h^N6O&-!jg#CVu$ndRko1;md7!J2yM z?NYQa&p{Nvq1+?|s4y_U0wyu_W_HqJwgtlcPr{?B4nwGCjOdkuP~aE$<8#~fuC^c} z0;SAsMuKU^z?CIat7Zxt9lZ$dZd`38|29#P{x8bjJCMuw`yaPMif9lSNg)a?Nk(L( zLTPG=mWZNK(bLMxPADRy(4;|1c1UG36&a{}(WsgWX#fiRk)g8COkLN8Hopcjb| zSdB`e$%0`+3z1Uhy__CYCTnzsFma}cDUA<+x!zOn5IH3N>Z%3i%+4ktos>wvkO`!IU3cMd5M238I- z2k4X1Z57E`=oSLBfaS{|hPAnRqbW{VqlhR2bF#;a=nmj%$ck8&{+FUqh%7syxZ|V* z1Q!Q`-G^m8efCpeq_UZT1_={(-jKB>P-!KofP*560+7mRVxgRao{&rQIhsMq>aV+l z2xd@b0CKG(M1A|k#hJIf;^Nsa*aEd?HJP7c0^c61UO#cgukX{=v>g2%;8k7p zU6W^Jn{PDBw}`IUuc?i%fvRb6jfvTV5? z6)$bT6%za5{>X7XmtLkmP%_Gz@uqQknDTDt?94fhvrJ!@KG+^5q$#t+_K=HL;Qkax zTf_4Bly`^uSJ*BXtrz_Al8c?!s56!C_#8BiEHus*6`RC7^I2dZE@<%9dx(Sbvc#8( z;~%Pj9e(uGY^j+?e1`A@R_u&QKjMAz_K&B}Z5{V~b4+%)eDv&R`J8i`Txym5j~Hy3 z*AS~SJbs#V|H$JJ*Z6j}*DN_R);Mki*SQOKg;T;O`23a&(fTptaQ?&Vlg)M7*Os<( z>YSRV#JRUEs56qfHmgK+ZO@c* z+L`hE^$$O4T4r<`Y6~rszVXpy=jW3rN1OfNs|j)qy)PJJenVik@{^$M??-awtBbi+ z^0$}Yk}?!Z>k`V_awfQHeNJ+JB+WPQkfZFrcaH${$IV=t zCm?ltowE22OKy`>LI?O~R2*^`xBUK+ooWYH@7r4Y`mLN{`Koszm;zW0V~;=8`M*i$M)_Q>gHugd*pc1}AxapDy1*TvDLTBFrl z)<0Wkle*<%{!`(=6T9*yM}0Bmi;VG@HY(a+HGCxGX>&-4I_52zblzg+r|pgfuZUo9 z69{mW8^?(cKp`@xUKcKh<7L`Grcj|X8>%!qN>H@1Pc@{5Jl2sqBf-nlZ{xiehENdm zv>+|D$rA?xFOX4RJ+so-kLBs;!P^FpyPIJsz^>s14(|XX8pb?>bQ25JBX}Kqj3XD~ z0Bi+@7xe&DdK1{L5u5o4&A8!@wy6WuvxMsP_$Ki9Vu0Sj46}yMG0XblWmkxJ{q#q-2?VSsrH$#}_bFjX^#<7(@-^K?;BX3AE2Z zc;ilIKahCDhF=5DuNsktlqxQ2dMt+uz8$`!q}>#ND&WYQ;qkfnYXFB<#VQ0b~M- z*i4%w7^y4*_YcNxHw8CT-c|tlc>vWMc&v_ncV`{Qy1N6+Xjn|0ESg2l*03T|{9_8!&Z!>ovYxlB*-#hQ2!+Zc3MCy{9h>} zot}^pr#pt|Aq9p#@U{+`E@KIf<`TLDPX4b2Q9w5isVm`rFeG}yIZT-&(_*QQ-3eaO zRX|DTz>;2YJ?jy`))k%1ruw&8M^-5{Dl!M4EG0Z2)Q7r`H#y|VYOvcyYw z1%3ZBT_m=NsXQt07u6IIARd79muoQFrc+%F!Uo4-E*y3@h(y{(VbXxu!3%DxbDDoD zmtwv^1ofwUz~Yd7=#W9$XtYn+i(4Ej7bl7sVNC*tRuN2Rz$pP_e6R#d+k6}A0^Jf^ z_zo=*yg#oS7q$&g!TgC`4x?w^ndvncM-yc&bd&HlPO98+a#!y^j8gN!clC&(ao#HS zSNNe8{1JD?K4mMAR|FXs*zM+q9)FUyVd|gPO8CGC`qZ|t(G8ueC0k&@HYTQaNY4~_lwz<6@~7}a zMKHAK5QW~44BD~kIdyr1=3QVNPPF<;!`Bi`t6xd0%~jkRsy$%0P{Sf>b=2Dv0YaG$ zrU55!p74%3Gb-!w0v{ncsi!fYw(kDD_-PJ9?4h!V#=Nx`BP@1Y90=JkdVkfiy&LCR zT7;ax-(xY4OK3vogtS}3_%t^}j&(S>I_&bH#2do%t`0AgF+IQEwZN=MYte}1t)KKt zV#n3nFBjf$TV$DI#nfj?0-;W8rv6SB8ai$Cx}P4ulJ;7RD{2bO{IRy(dE1I97ALAM z9PQQmk~lqh@rR%tGoEs(adD~SI|mMT6P85b*^86I2W~+UV}gdv#2L zzfmtred*b(=*HQPeD~?DzB$Hd+UF5>PV()W{`j4Z(e%Yr{B~buAX%J3C4#!duJ(48ny4tjf*!EGg5aQlX2#&ise05%zxwlmfosqV=Kpg zK9OwpWUFa^V3cU#vw4X%V;1kr6+AScwO#1a^P1A2rT1F-R{EQHwInl|6QXMee#Q9O zRE#CKncmrI&9%mpst1l6Ny4B34U=50zz)GT^dcznWe`Xrgbq%756)hXA5P<}YG!kPmiYZP?*-FQ-HqY*<_hHN;`Uyb)Fe_S;I*)06u+*Lt5ws)PKz4>7aq`x z$Zg4$(d7_FjE)8r;A)vrlb8=AI`kd7JCG`Pv%$feehL0ZU-u+*h{3o4>8I4D zpi?9n;D3jF)Lc&-lIL#zzKXJ4^~-{~J&|ZYm>Ajpqv!uY6#EvX4X+-}LS&6iB`}7K zc2gq7!9;^(6n2D=2gsLLFD7GzFl2$WKY`u+<9zWM-v5u?1!}KByDScc=>tmQodPn3-Z&)25dSVgV1# zf;7y)pagBQI^!6FthUGtFr~Psp*4b{7mp5vLMFXn5Dh(^xGN}WBfXOV52u$vwP0mZH>ySueJH!Qa zQbEW>px|msLcO+v$ff~%UpW+)hXtB9lrM02fLP#FBg{$>Hx#u5OqaIzm4z~6xF9&D z0#Wq8pgIl23uKOz#P{Eoe7t0k7a<}ls`ZoFL6UuXAv}6(cu5~)$<%%sv9>UQhE{}2 zWn+isl!(IBDUGtYm<)R*J;H!FNIDgOHFD$!1KfY5Wf49xK;91o(cBUB1NHPoV8e#F z+6tsSK(-9hh#Y7c2cL^g8e8!3#rH6({G2?D z@LzW$(y=}z03Sx5{rp^QdZF21zvD{9MP1EwAe!@J7Kb)2p3u>-etGQl1?`g;%vIbO zw#sdjX=vVT+t_o;pM{=njCM3IpKxl-W}nDtjV6WhA$$|>cDT*84OuHFKk=lM<`@;R zzN^-KUzX$wPIZkq7k8)0!fJ#duWzqu;TQEO0mDaK)EOvZ(S=)v< znBJD>5;SkjZyH+GZYQ+TXqyI?qhDjh{Pahqp(T%GV&eTz+Lc~kuvlerWS6JGl)99X z_Te!qI?}3N#qS+A3z0Mv3isN>wNEA_G9zWH_k!o=GLP0~rKj-mtcf!SI6S+{E+}P~ z`tafc%duAGKV0wGZ|!`@b#$S1{Bl{-Wu}hC<=M@hZIW~L)`hByiw;%j&i_30d_8aQ zPQFR++!Ei9da?VVvB{Ho<*r+X$J>2eYj3SJQQyj)puO2tFj7R}>bDs?Lod314J=+T zchxRypCW;!0goEbUpbe4z;Bbl6`K;-_fv;1=7~_=H!XJ9&`@2!J^fNo7HPD5M;~#U z$Qz~089pB9E$19ntVO?zEO+-l^-iEA+qE z2p+r{w5rWz$cD5ebDLRRv2UX_($l9`6|S_t>n!4JV8A85*t%@^z30Xb6O^PgyUHey z>hWrf5wh+}KEJuiCj55I_&Z})O@GAdDL{pFiL*S~3aMXlj$8$E^wQ@3=e|a=|=3M4?E43_?>F!RQJ|^E4!8 zL0$&IqX#iO{arwY9tiviUQbXg_T7VhM+lLh$rKPZR@U0(_Ch)T?J)5b*U2SvyEB;t zJ&sm2Sfx7JM^O)?VC=L&Cpj>%swgPPM99OlYlIAtcrT}3KR~M}r$oN82m}Iu0xCkJ z=UVakGesG}gOL!hWDrgPrwDuy&;lX9!h^|e0H0qiMUxkHI4FdtcMX|!s4A%=x;T3Z z)UjDO9sDOP0t_jLTbThBk;?CvB4;%0FK)DSo0On3Zu;1Z7Z19r3VMwTmPqQvhbFn1vQC3H_grlkO zH^e~D0~K9C{nQV*zNHdQa)FS!K(hLGrT@j@6=97bInwuCGLrfOvhThuZ?iitdEw@f zVLd|o?fj*l>+X@Yklb?L{$-9Z?>OyR%Ll|d5W%@Jp&a?41XkqI&>2L*SO|1UqNE!d zC{p;9km>ISfvI)>%US$OhwU?; zE{=tWjZz$pw4nF|1$BgGU>kJG0b`RHly~e!faZs|8Z#u1KN^PN$%aZjEaDJWf+wIL z3y^UTp%^A+@URIdP72T(VAj*nHa|%Aet`cmfk1%P_mA~rmCPe+F_uHCKQ&J_%3yh$ zW)4O+^o$j10zmktBG~!^Mh$`wsFz0jQU#`LKzk8PVi8ZcVu61EbYE0k0O&FTx6#nN zAqO6a42jO+Et(h?eb$TU%E*6x_C|Vu7HLdGiKM6Eu#@XlkpsOjOHyfhjes|ntTEf8 zFdR1rGX4A;>Oyb_?Qsy(2p{Wyh4a&Gk`os0h|(xb0dOMD(6C=&VrQxiq>u&2$d5t- zx}eDe5rPaN+LJKV#eN!VRITek1vD*K?(*?q_Q1S{2(O5NaIg>nWQ3p{(9r{Q4BnoD zy_5Gn34CO8$Kqv>ZW(1`7v&TeB3ewL+hg-Z<9F1s9VIqsoI;TQ0D=I3pN5Wz z+4$=-X*x&*@dqW(L0Uiq4PN`~%>iMy3|cwx!kg!Ws;z~Pc+OssdB$c)Oj%|v?^@Rx%PmuWseat(*!G59yX?MPQOK?mG@`Bv_V-&X<)U<8VE1l=gGgEGkSh&mvnV}LZJ>*sWuWpvAmpWAKbaq{KhY)x0 zIlay1qHUw<&D%F$?ygJySX59{V#_$ft#x*O&Ah;|zsAwsz(a|s4@74*QA8Md>w?SlP z{?lhaY(FR$doSihkZ-wp+RRrh6;Lx?VPfHFr?@>N{p^}^6P!mZiF>!^OPH3((8vb= ze2Xb^bsw%K+RD0C-#IJ97jE&QdcpZ~o@2*FHOHn5n|AtLr^&lTQPu~puD6`G?9mu& zvtG&Mhk8B+214eu*Op}-vKp_wVdUJ?2C6nUA0IE)$U6Q={_U>q8cIB}Qv{RtM{79T zzw#!t*m0AnMAyfL6Mp$(dLn_t+Nbe_s?Qs~(pS!A>b!f6XVyROJW=_r%Tn}H(PMYV zvqfT+YLlMq8QZh`cCbc}lK9eydh5*5xu-O)H3r+QySnF!hGvZ7>vdgAhy5P8^!TzD zM`yms3SU)yH?{oySIdU)b5>S8mK)l){#MbmP49j$aaw=Ei0U|2cWTI@+pb%VKDpGs?Zpt)M;5B>36pab{JM0M z#%5-U&Pxk6@-%&R?)I*(ir(FCrI&oVJgN8F*Dqf3>c{HL?l&KvYvvddxn#-eh9`bl z0X1hL$RCNgK>%P@JU9sg0S%~Gm}XMAB=@YZ?FMp#W!t)AmB3wWv1W~YdJs{ zpa8(T`UEx@`^o;oY<1h5vHNXy1YSG&lA=WbzEFom2@zC>ll$n403eY?4*0{>f^GnU z0>q+dssLs|gyUf;vgz{6(v?Ik1u9E7uj@b2D4hSmbn(pR+C4RU;B&BSTbK{O3=<}yu!zF2rbFqHKn_wZA=re}hp`|-K_)BE zt{y2{mf}x>Sehig(h|LTx9xUIyx`DQUM)N>z_FtwTx*EG)bG06lg0S!>^tUF*=?$q z$?6+3w|0nY>jeUX3(3D2$eD{(=y@)XBvf_glR78x2*7Q!KVLy&XTYS{cWv|LfEmFeOi6Gn5_# zI#L>A)zT1%`0A3n9fJv$*&Pft^U7LP*nY!095c94#(Izmq7XsKVEu=-Ne+m`K!UR@ znb?hvm8NQ2XWlnj`FrWIi{MKz6Yn5c%{7MmY1D;b9h>X6_|@;(A0L>Clmv#LS(CaEroBBm@NCgAT1T-kJ$sCZ)ktZsHwKAkEA=9M#DVva6y9Oo-}>W{I{IjX_Hg;O!=gV8?sK6_JuJ>Vn|W|BAL*w1ydff(93g}`VO<7F0P${mq@#8lfV(p0&L`m5uv`QZ0w>o0Bw0K@Pk5ckYGX0 zk%OpQxgrt~rk zs$vQfqJI3;OzW2!+7>pEg-U5X0%>qwEhn;h!^O*O^90UVu(8g3XKv?f9{SyF?LtY* zRSx6N?+zHY&VFC2)3UuHDS46eWwJHyADUrvs&bn}lzx8k!>h+8uNa*^f7xqcqsRkR z@9Ug8t3SL_*tP$l^chEkiz(YSKe~G{EAy3Jg6joy^{lS5?bdG`xl$Ln?M$CA{*-~@ zmXgAA+BI&ePn>r;o8{LV&KuFV*88UJg-Ko?R^+}K6(OdoZ8FV$a#P;*Q3>{{Il*uG z3v|Mher-FPG;fB$v6b8|iswxmKaCLN60-K6Aie4N@|C=Ys%(#(R`C?RsHr0%GkfFG zT{BvE?u9M+*!#v*y0`a?7uTE^!$*t5at`F#*;g9v$($qhvD;`%x|(6g*`+o0^SzH6 zj64$Ump%OWgwv~PH*OfKnBscE;+X5R*1V3a!^5X`?hD_1>#@bgu~Yf{o>^VHUXo)u zOlEYJ?4_1zwo4Q>htKyq#Yhu+a>(({p7eNYp1AsHw#g&(6u#Gab4|$L3Co_oX6Qs| z@et2x>8}G4rtyk%>fyOJ&so!_ONGSSmvz2VwHv=W#Q)BA(|Bu5^%2_>zX|CcRy$&1 zEnC=FU7K*j!m3_r?#^qPzSapwFI8sDzx#24LPfZLlEj3PlpYUrBezb2?t2A(HF^Ol zT^jSN1)5H_99^O$CVnKgYn0Ch14(BdLvF6zxk4xFb7oF|yJg&#!VI|~&suZ0wi>T4 zT|GA4WJz|^@WtH>r%PHAU!8JyN3E%Tlh^e0gYG=#jvA$nNB17ARQoE=wRD%#&`9&( zttw&p{ykk&O~)D^xMUzY#3X*nf{oe+3vQ)c?^t<2^73e*qd(S482KO1_W7(aJM1mb z6~q0}t9uu#{w$Mi^K5awc|-O}zp9ro7q>T;c*3{Phjxn-hB;TuWW}_lsG6m4jd9sH z=E9PIT=~ywE3aRW(tRJN=Do6fq*R>3@Cyn@Iwwwe-v8t1tvb8R_oa%dtB1v0(^;te zXh;5pDIr6RL*%xXIgk!PS{4Kw#v+C+Byf;! z3qfmVat^pS@&?rFIDrgwLB|ZL5z<94lt}xytT9mv%qb_#5K^{2q4TYO%#34fU zexMh1%&clrB%yI4moy%Nq7URp?CKE(06h?@C}1jsQ=wobQ27%i*EouRkk>)|%?QL1 zCQ9ZxZv@Qkj|8%e$a+u2PFPeNC&rcu6BdF#KsBVi2KbO7ok742jT7KL(4EVQ-zn5G zHXsNlN0DAxF#@eb;W=hV$Eu<{g-wucc=nDc!6frYb&J8Ac>JlE0+X}wa4`h~pluYU zeosY2m+MrtoSp!L1pwXJXrzFoc>t#bmLaSm$P7wl^vo{};y^ovAdSKBAYl3pdtX4& z_ajoUX)sEHoCLv_0A9HP!h+ZGVOUa(z`oo2)HH5?075GU_zwg@C}b=W=JU$_f(5b^ z$ai4?8*C6sTgMjY0ouSGXzy1B2N_V*lgV57vj#yTPW#5A#OT8SsZv=0=W9Q4`c(`r z0q=`)wW;OM@2fenj`3@*dF3-!*1tBMzQXTWrJdjE5Vh}HOhhh484R1e@Jm?gsA-qR zzl{uNbFSR2^Vu(^M$9_s{$%|-TXMF(yH&2|^)#(ARN&}MiSv6O1{F2W=9AJed_4Pu znzNJV3|nJ)h+Pr=E5>uZDk%yVAZ_muy&wJEVsx?GMe8eG*aT<58q+D@I zo_cQ5%s7ejw|Fx9?E++eD6cPjFFX9y$GzRtEIMrUWD9Kr>ela%>DhDg?6kAzRL|V+FhO7k`BMKIy6kHA|Y|Ta&E!fvYFM+l~Xh~ zoG&^Ny!4mf`;#v#_D47WKHqy||F_@wRG(it_3PFer3XXp)Sd4>;u3%La(?H68Qk3C z>T(ySpS;Yk42_Q3&$eowaDiB*sUK zu?Ltj_Dd72W5fT5{|3!sCHER-rQ|vlXratO#XC?x+d!yG@InW8<^=mxOKKe&Rh#*` zzq_q2O)@JyX6WP=(QJhtFb52AR9Gds(qYS#(2HFj7F2DAv{C8`RT#3h;p^hw)8(E{ zZ{9gcEnO&C5$vgth+LZL<(|56`L=IQ_{Ruy7oju9 zMJ^1Dq0eQH% zhw=NxtngP`gWhe{xP4gr!&}&~c@QQ$WxE%Z2^~o|Fn(%sM$=H~8!$~pFEW8)i4-$q zG714BK$0lMnh`y*LyP2tR%+!^E!f~PUX!48< z5k4plxs@5=CI=5gtcEB`LVBqNpaUwj(ea+uGBAX3Pe+kTs@-wV00cn&K6~|kvM>hO zht^nC2z27OY@n;fBNueHvA;;~jCIxcwc&Bo*u^`ytU8bAM50)Fn|$6f70vB54Sm zIWkM)9CKX}yiLo5BgjDzV!(6kJbG?FL=H@S@gpgfmK;IOj)WMDbp8s=+?~ktS2#p^ zDok^%{0B?LjJ6Exp3a3W7%@|@1!P&4#O+dU7C>XWiW;nyz@bB|a7bG&n|FOy1OiG6 z+rp;cA|#w+Vvs0@%ktew3=IsJu5a%PozM&CC~4@kl6PEVjt~l$ERhhn4vYvQHl3%V z!LWM;n*<_3R%K8@;K=gPj4+0x4q#c-pSQ7*0Eo3n2B>2j%&#hh=`}P&fY9O3EwJiK z4k`c`2uXxgfQudEBEH$p84DfWaq)apeDpxX$Y8m@aJAj*DTja7+jMETjIdJL-uZdd z+hr|%>RYhmpiba0Dw`+lbY5G&sbp{5iOZ>t|kKD7lHYab2|5z?@EAAB! z`FAZW5X!c@5ccVZtHj=E`7H*^#8r1krmlK;&St&QhL(_Lpd@$nZbgwa0Z=6SnZGBa#k@#k)U(U$GR~D%*G>A@& zJr-=xIy$P_&oCh)|GvYKv=dvtoR1u-&il0{_kjL&?Ttc{ltypXo9%MxY;1q@!)@yQ zX$$unb_F`z=nC5S=u_j0uZ>@}9V{Ilv0qMXf0rsh7xx_RE$2r>e}u?bFt2e&UWd~SHAvgb+&wm(LHl-!42HQ?tR{~O|odw-EH^gZb*LIfSANgg5>?# z++1ATSA*dX_`$_3&s824@_}ELPiv$R{CR1i-5&cH+EX;AO`oEvp#fJebBP7Q|8gaE z_LakD3*y&HJ3oc$n{(dB+!k;dFz>|vivj!DU>US?UPI%Kop>8Az>`&tE-#&nAWqXy z4&aS_a6_BE@f#u+^QT^8fs6jfZ~tnW*(ufP$K0lu&NmdXVe&o$`hauTp{Zu*{P|0l zqKMb@4VO7gQa$!r2rDEj;7&5WBOlNeqh@}UWqlTt zZ|iCtJpQgM$Ms`rPM&ito2+9mJf>m~?QhZizgIY*lZ zOJc0oQ0SEKppj_0qe8+HTu3DET{c--73K6|(hHgCn=8_;UJO0G=J}6cWAiklSN*25 zl!J^fFKSu(A|qEVdc@gJ+~S)y<%o~f8X5PUyL-xripx%07qIBh`%U!B(L2%iUdm3; zNWlP5&a)DLp+S#P>aCgCSm0drF|P^j+n{^}6D-X?W-vG@4ba;bpzZ5fImEUGjqmZp5S|NMG3ek`#<+XT$ z{YBqZ#P4Kua@Zf6#Xr>#-Ej^V(ldyteZ@0JIjM`wGD4@N9SSl@$hf21@k(wc|ND{! zF`NAZ9(Z3lvijhOTcKYsJ@*P4e|F!m2N@$a25$^ia+j0JzL;O?;1iMZwY$=I=$#Um zRp*T^^c|C}tgZAqAdn%qI7YiQ`{dPAg{cxt0+&{ezLIC4dRVq;zlyu*`}wTWMt(~EIa%PA9Jop?9k8%Ehbd=_}; zPqT=RsU@BV9poP)qL3Q+FKhGBi8}W$-j?YZD_r+^_dWGXCF^8_JLGKgeLtB*2Q5+_ zEjc=JtEEI*?aT|t#ttGQ?mFjM@GG|DZLefFeUCYEeAn(@Dyoyz9OEw4PfXEXJ>iRq zd)eE4zICa`)8<{)8Xq1w)JkpJ9m#%)7g4VgGeTm%AH92aqN2pa_%Qg8h_rKP6ckJ> ztlp}*&)94pCif`qURl?^=+Zm!?I-j_#if?)i@j-jP-oQFU)PB`DI@7?>;{J5gE34W zuZUxRup4CEqeoS5RliG4K?_(&k_fSo2I9eBAune(PUgUv<4!LP3UrEhMrVgU zP&=#X;c2$hYOm1pv7hFjjt~1*amyY!)dQV2&+P4M*>CpwAP`Qthw#$wg_Z$nfCVaNR1k+ZDrkQT( zH}O#H`1g$_bRWFY3*Vb0Sc|1aV)K8R_@Lm-mOC=l4WpU?#g4m*N)R6b(E_N!!T*{G zU;5jfyKzvxr=VdwW#(j*k)QuIA;JOZ^Z@b3|8mnzFa+TVh*!jjyN7uVIqqkK6|lq} zagZZMzlO?6C?OY6D}j?NGbEy73YQa{)ADhtL8L_aM`*F#MZck+k~7l^Xrx3N-+yYB z8g<%aI5Oe1HegsXo)JwU>W^Y$KtkAGtqqn76{`We!(!$LI*nkWWy*j_D_{vQ8YxiT zK)Y!UCqR!>T0xYf_kh1h0B2_>ENe%}^aIw0&s!CUD8gCnrA(a}m>~a7gu2j1YA2=9>=TWjE*eY{veFX7C(HmM;8*A{gP8U|h6H_sQV*eO z03^K+tbPEAf6pZ5Up*25Ke0az_c)T+mWqKv+F%J1oF@z4pQxPVs5d&bx!|NZeV6D%n?sH5+VpHpumMjEPA~M zs;QV<#8KCDXy;0eGg&$NwS?0pl+6=XeQM$GqjnzV z>u&C}m0nveaYHC4B~9-5;_l!n4*!&b9_vlIcE%gsN=5^aGnEN$;m^Zi3J08zfihHWt{Pzs_*0h!NPpb$X$lwnqBvb6=OpjQMqRmHc%{sXKdA z-NM6uj4peFORHk4Y&-IOn9sgCUo)^-;{2zf9kwWjrY;)+lSVS+~0Mk z|I`t;MhT0WnnQLa5}6>i8qk4hyUvOv)5^F=2v1zo601jFnF`jaTSi ztoo!oO_5jjkIcT;zT)zu z?^|Xw9v&Y*s`JD1=SLiFDlb3X;wE3hrJT&LU=A|E& z*8I}9{9GTSJE3iZft1{)`jUvfN5|V;oZ{qu^QzsNThnvSFZ|wkHYN9w&Zx!CwpuE4 zRxJN}|*;lpbeni^#T&IqMYOjBv+xWnqA#n7#jBKHVYLl?%XXWWy zGenmTzh!E3WYI};rz=f7+2@wcSs43z#3Gr?k3+D&{Ej~JaA~)UJDH}z?XQNcxvD2H zaf4T2Mh9k!a1#K`?7=B;GaQ&kFO@+)Jb@2e z`xlh(nd@tBIJ@`14V?JRy|!!;--%nUFLsG^sg(L=!IPu#!SY}w1Kl=F9)m2^Sl?-Z#Vk_ zrI36My>PVsFX{$>TVe`yt8h#)O(bO=IGLPXLsnG$O%DtPSi68Hc2k6O;(8AFY7#6o zoI+^oL?S71`jK;B;GjVBaGG&5iyYkDikRAm)Q^AJW9q5F7`T9LJcBbID`t28k}H17)&7*WC49P0YT8nUHBg?KPalf!PB=M*Ko)U#ibg0V_+Q9 zLecn-jS{pYqdLqF&~a`d56@#cL5^zC89;8*ni;$hR ztt^9|FY<_#P7_Oj>4?!GL1Xcqwa0A)^Av%s0+aLe_|INMmqkw1xb| z1iQ1IiUd?B`EoT{8yE~8G6?&Tz+CVg$G;jR>w7$qUP|g{pxuqqv2VJdvHZ5hYKNRw9f7C`3EEP zfTd4y{rd^}JLu%BTtc)g(~W7Mw_ttj8||_9M!@t&d!K`xYix|WxOiISpTc)!@2(Sm z)}dP(a#(2`ocq3{-gVr&EI*-7rM(-=i%wOsjWg>}`KfbUysU)QBx5Z$(J zh#{9l*Q$&&+!ea@H>$ryF4*dQ*Zu2P|6(rle7Ta0F`MqMQ#Q7Bc{VEZPmknNj`eFl>8doLJ^$R8PcwN-6;j`xvmSNk;f(h)#v}{N zGz^>nO0doGNz%zj${AYe0lk*XJ5Ox&*PO_w@pZ>x;WLcs{UsBp z#Lmim3BEL9UH7eX&GKps+?@?l&-gmLnR|MF^r-2_&rT9>c=~hg`EUNl&$^e4Sg>iuFt!&ydfPPTTF!3gFjzE_)}&l)sbCkWQNe4F>>u<(q$4A<^=Q;v z13m%qvNh!;b^=2urOvyzLgPtKnuSu8WBjy^zBk9_fBLH9Ub)GkrBJqaarWEfUDhGS z>6Ww4%8HFL&=|?bCEnPxcC6JG-5hcIrnhoih8hk1)DS)Uk>&eC*BYe^HW;6rH1uh3 ztJ4Rwf#3;4k9BQZD-1MhT=6*Rzi!`^CK*a$mqzyY$o zF?uKPX9EZ@VS^_8htM)rfkF$2P4T=Fco|mO($090XW4;Hb(|QcM-h5thXyoH+JKdy zfbA6ro}a#VL@kOe80OgD@jsmB|BqXR0<#+Ncy|| z&ApU7h%3$yuKeXVDM`WyvH%v*DIo|5nI_`%XAs;ei1KFchNZ>%lX#y*tez z=fa0?-tG%e;%p8`<&-7lHPN*SnETKherTsj0NP-N2ERxN3Sj_EVDq!R*S7_G@Hotw ze3EaDa6p}Xtpk+68< zq;SHu9JM|;#e)(%1mh%8eKd$tZDKROitl+x$o5pb4yv4BgZ_IR)M9A?KuBN@ zu%{T$93~5s`60(i0d)byr57-BQ`Yh9U82UsPi~XO@45LJwp)-zzUx@9nq%y++5@e1 z5DXcJs=yQ)b?HjXcL!sjK%aUNmty~4Cuoxe&`btFnOT;)WG6K+Ku`~CDuyg=;6K{I zWzZ=Haar#nvP~HiaY}^T$ z?)Eap)&4!&K_W9(!H)}pceoOv|A!Lsb6m7i>7fI!0UvByG3lT!4LAoUfnKr$p>r{& zDXb%Qe5jIx6<95BJ+)sJMMlXEawgbWMT?MwV9p*}Kyn>zVTB!N`^@diq-#7jkxmAf zC-xAEu%TDDWgT?{GrN4$W$Q3LK z>Dz}}vxvMv&D4oS^y#BW3biL5I>#mD)c;WX4det#JKS}JnOF)78WTxC9SMa5v@p<+ zT5k{+)5;Vq2(7Uj1OS+I2bz&Pn2|IdmGqvrc;o=$DYC?iNhhA5=||LnBwVv6TWsPv zyg5EV)JJbcKS*uoihwR~Ul%PD`UxN(oJpl68RyI@0?LBsg6TrK(b_uK65|7g*!$w3 z9ZWe{T|$Fxlsb7?M6%Y>6JA(G4bO4p5b!t+*rDclM^qbzukIKllo&fB9Q?iph?KYu z6c))S3z$M;+8WA`CI^J|7weev;kboeZnR_m>tP=%^JI(bFREd85A4mO3NWy9ft8GN z#Mh>_Fqu9`DdA`?EXyE6uDJOSU#g>qJp6AkcQhrG#Ay=o94W#FlKtPTp<(=k*97V% z=!t<_3~;EEJT*cAcU%mno5hnxMay2I`@#2tI?TGSk>U8~hQt4+?ElaQ%`4Bp7!5F&|`*_ZP4o?QEXaLn43RS z=sR?l`x#8nnMF9+N=XOD|B`be&@GK08dbIrXlV5ZpD^eQ+zxM^)@e^>zmYIrB^Z)n zH-5jcch<^|_#Gv(VQ0U59#)mVqkMV=|Mp=@8C6O}va<5xj&gDKa<$hA+wMy>sccsk zeAczOy?&FzVeWV?zUC|EHhj%&6cWB>5L4Z=)8dNrH>>mNW`emgzU3jBhl6q=9-hpP z{-PybP?09>KU!C1*X@kQT{=ED8iZe;TDWiR0jo7L(k+*_96Uc@d%;Wn{f{5pHaqs( z^vmqDTjs4}bHzB3OJ%mQMzxOA28KXtaila)^t@Cdh4$Bhx@*TsJj|0f4!@?LzCm++ z^lIIox>Jfzct0)g?owVbHUE0H>g|a~v>YE;9D1y2y_ezq-g5?S9%HX=<7^bIZqt8O78095)iTSk*YEeTnR@&h!}z4K^#LR$jbt+G)nvlgBDf z$_q~5;%Rghj!Y5f{lpaU)(#R`Xhs$s1j zj}gEWV(^c@Tm~2y=4K?l!X$t|RdIN+HUjnxLheBh*=L^qluR020@@CU~j z!$eM=!_ED1n{&(n?7@IrFnFUM12eorlHvI=Gdx4e%qbiWSLwVHdAXdl_35-cl6BEf zv4_bZe?E8w{l_f^VNag=$^u9RUji6^m^v~k8ITrkxdfuot+Kh!KYml|o7N=hKgggF z#9)zA`vN&PEB+TPAqNrX#}UV{}AUtJ#Vwq#3h>ZSC#Fqy{17jY&0O- zYrVV%X=1uwbuuJl48rXC&$M~)hEa-L&;gw1YQ&1~IMG2LL}oU~M-AfY|H$Yl8)QOZ zkLKt-@w3;IH)lq5dijJT+3M73geh*Q`?^HR=-m*-mqCWSi5H7#vY7#xqRW-+`-I2v9<=L%*4pQJTOE$z-T)mrmpfT;jiI)dxN^w8tUx!yfmW&7=n#K<({b$?%6_!9%)N8yfLjxP3l2~?u z2y=x&W`@HRz8k(p7rs5s6d|^ zMzUQv-lBU5VUJ0ti#~j!<-cazNS=lh=_Dznj;MzhS(n*V(PE9ae}98xpE8RyHq#o! z)$Z`*kS>*S8ZfVDU;@f%E4ZoF13?KeiPe9UVk9{w12}|3*hubM%FV(YKuZyG|MFi> zl=j2}Lbg{}VUknEHpMK+9~t+}L~&Tp5j>)8}-cz94et8&N}#q{yCF>7b=UYZm7h83DMT zSr$|DY|M{1&5Wsjnr9;A5)Ueq?V$hyf$xGuApn~H@&(-_Eh>P1fD}lxL2sgE0L)9c zDvR)8lJb+)Gzhox5EYp@cT!}n5%Bq+~7e-?st1>epf{BTn# zD^8ZW8WCWRV;|3E#%z^9d&rKdbe|>~UX+8w@^H>6w}sSfXs=I948@{L@@LWkrlEB9 zhP50x??eGowTklfm=dR8nzjz81e54vF~ZgyxJ8(Kqy@-+2KynkLpXy8k2A*Z(W}oy zP!FjI>h537(e$H5l8RC-iW%xUIGalCMQ;n6bd59zEs#_im+CzD^V}E6{$W4+9Na-#{+{({w(xmEG#3%FrykrQ>Jt+r&OQ4Bn6u@G0X5Wngnv*k z2$@orC-pWb&kUR`^p*Rmnb9vjB0$?YjCpt``P~+7YrkhHbtUF4*s@~{GvE&OYiyUSSi>z$q+;kNkl|>MhI(PWp=iB45^dH|6_}$C+efMEe zcXjB|MK|lML#sb4UR*wW*6!WDS((u0cAMaKD<7Z4bn)_-?6CrCAMNsW9_ElX%zMnk zHCu*+hG}vSm$!bjS;G9&-2T!=uZwjH8W&w*1b>>bJ2&oUQu&YNSNHyKYpM9*$i)+L z!YQuxjKR!}Sw1CcA*G^2dE#XGgiA|0zwfFNOv#k;Opi}ma7jEW`15PCkjyPxCP$B~ z-&nqE+m^C(67omv!>t!;q%K}+^J(j`NXNA5}Lx30KLjo+{`hEoq8xOiQc*+{ENxLPX--<#{VTG(fnz3EBo5!#tu z@I{yEa}FW9)Z*vs-@meM|Fhepyq4&yynVM+`oe4Z&E1>j8}fYqxz87RjnqtPZCu11-eo9? zSX+qh@J;@0<0Vm%S2N&Zd2-;(rh^^(*ZP;q#!Rr`;-BR_#vnmaDf_r&x#S8#)vUyd zb(;ceS_{;6I0~w+7tshRU1wAnxa+9U)mpv_cP~6kz1kemcCKSZd418-OV&j{Zl>?r z@b$~-BcWMaJ%0o&zrS#iIj=X*l08X*A+7528wKCUWK69M?$@k}_5Stz;S-OQ2S$~b zbZ&hVS95i-eSPQmYTZ>l%bs)R5-6Mu?r(BG$!O}Rb8^8g&@QQ$q&%rskb;uWba`SbD`W?F zo}7nF%^#Mo!$h$>yTVovP6Y0U;wmJp?4-De%2V5zFHwGn?tpfO+Qd`ht|OanPu`{muSDAl~9R-ty$(y;8yEmTfTQaxFc zHZi~n(FEcTJ*9!)bw?T$Wo9ptm;H8MuZM^9clcz6L24UUaP#ywgRRWu7_xt;;YQc` zVG0p9FwlGaN={{V*{?3atgoqVe*D_1&Cx~&OO|qPPnfW?w}mW~t4Fs6UQF6Pbqpp$ zxWp4rG`hyUgX^JBz}7dn#50mQoHTEy1(;8e8YAnveMU@?+amqXi{^$nNzE3UIBAjM zHv9L7Mvljn1CH~deKq+@4<;r1(Q|{IAk)wwNl$BdTXas_qa7x`%{}+zb-wrBw43HM zr>PtH!lQ%%%`%ZWma~v0{5l6_75E5z0G8v_9qP)*o7WCRrO&w}aZ^QrEAM8`tQ#u= zy{>IxkQp}_&6MPEShaxXd;uoVVHqDlSV|-fmr2OO{1Xo= zVhN5gPD5965>Ed0Az)z;z#u~51P+;&pg~p#25&$D5XW&Mh60}hM3>lcH|c@~h&KQ` zlf#qolP)`G<+c1jy$`I&+ zp_!arM?l`JWD2Ugf>}@}@RByhpr}Z?f$)mLcMRxJAD|nIFg(8qg_Pm}LIf&dwt;5w z8pS8M)*3i~RfVEjf`tNsRlr*Co*cgS9`PhN3mA6r-512~)N~T?K!XV%N-sum7#&3; zpa7U)C}KgR0T>44xVh;JPA$@|hqR+WVGw{Aa40gojo=dT4C)zH4g(7WWl~KNsNqQ< z6Y3KID$9%YQ4o$grw9n4zy~SKgNXq;JA|N*bRmkF0(@8*_<>`TjQ-h98?FiFI3f>3 zj8M!7+Z7>j1c<~Z{s30ARe=x_?0v-$2+>d-1P(+XT7(y&b*eoE;;fAdlpsMB_am;^ z8^MZI1V^yY7Y{4qq0v?m)j}Y_fBFMVH1t#Rj+2u)BD8E!k_QwFu?mj%gL7Xn74r{R zg`N~loQ=4~D|HC_-x0#;2XuVDeQNjHbZ!V=-DorXvpH{2b zZzvx1Lf9~WKldq3S!)Rk;akqrt0xp59`p3k;Q7l{OrC3GoNHi&*3VHqrJ4`NU)K-6yfbU! zMkmqISD9&QJ2O%py2Ra6OLrzp9eeG`c)RNUop9sLRa<)F>{a6cax|A~&F{_c?Q(kO zD6PK9xvIN(gzZEJnRlAzDW0;qBYaEv4vx2G^tpet-NPmo9od?&F->xt-g&-LB_$ zZs*+kZ=|0mr#Ps-_qWU~N%393eBBSddPYW;lpJSlK>dt{%4Erx$FZ>SP%cC8UADkO z8p_PPZG75$S)P57mu?&DzES12!}fGQP=ob;vt2Hx4p-K09lqW9^e9uM2CMj~Jvyo( zy3}7ON){ao#HGn7_8aHz(BfO&5AVq+>W#V{jj)TFjkt8F3Ry>SSK02)aV0lhKVklJ z?Om?A2wJU1CF%(OD_j-_YV&mx^lP793Mf&^2^;S?!_m;Kc5~+rm-#V)okVkL!#Qh$c^>WFlR z8sEA^|M`YL!-2kd)5=S?HRLB{eBAD)B(u?#s~b!kDOTL$^N&)|TcpVvy&CX5d_VD^ zP;t==o$m_f+X2UdPe4A=7+QXX&ojEL{ zvmwaRfj*eC_zH%B2JqASaoj@yaDjXI!3aYP7eKU11eP!$upH4<$Lrb;>Jwoq^z`7i zsRqeD!40g(m0oY%1*80*1A9rD29K~>aoK|B60qF`+@V6858rP5>~QVAQ%j{@+%2QX z2mKXk*MHy@o`@5x5Uqj0?io2iQ5ewC6DLuo#mdd~+5KV4zNvISg>t zygSY{z64Fz4um-qSepLgc~*tN({54Ec-x;=7fF zrStzdQlXFFU`Y%$0$fE-A|M149Do)Nhl0TahQk+JN>>H<?QZ+<5zOVo0(FZ#%0s zQK>9bf+HULSoQ91fI&|I?X)m~Y%qZjX{DIe?l31STal5Rj#;TUXytX_yo zBvgrZ1Du3lL608ZRZDrG+Tc`5j1!@tcu_^bnU$%FV*tWMFaSSft$6K$HYq?(IM~9l z7eZbC510k5BUMP6Ko7b3eL-4Da;RJ{OGs)p%I-}L%(1Wqu#4H_pQR~>7PfPqD@Fo zx_dT@zL)Py(VAUD)Sb%uZ+wjXvZ)Ud$nvn+dVIka>R zTP!fRc=$WBr+GQK8fW>cs|cUVh^C7?Zp^&n7A=|h%})$t#Zlq{S~qn3_L^HNY@q9w zQp=sJu@l={5Rfmf6qK2hsu`1SoufhfP{S@kF(_!LMWp?s(}C~OHus;0vFBX*PW!5n z^P7l>pZUcLTYU8gF0-?nwR~n|juNK&(E7t(jlGHFG$y04Eswt)t>$^{ zVAwk4r`lYJNxcH0>k52j^xF+n-B$O*=OBts@y?xeT5l}sUA&Kk`7so>I2UreChljH z7{8DjRq^by{ITQ5dvQkH7UIQa`L!@! z^HqbOwY$uhQ)ecVim%sgdU+**@lK1U_OZL}pW))4U~8HeWkj zp?pfSO1aT6Al+A&u;@wk9*=pG(7Ry8+5o>nsIn&VUo3yYJZE|rcxI~x zW6CCcU`ZP{^#tSwL$M#Lq7hKUC1whMs5Wpid0dXTMJ=87#!>a~Dpa*H&un>urGEyafHop_GJ!OCoTdgs$dm4%w2PIgA55kb??8s8bOg z0FDxlfp0G!O2Iu^NCt?rf=GNx5CR2I6M;JjxsVQ(#uoMvdPI;7B)J^-5UsStV;Nzp zW9}trGXd&CiD^+?h;_(mgNDpXwa8RTkrsbRuFSt{wdhF%`%tJTEFIxkj1Wfh5&iB* z>(g1Q8US*KV-)P1Zd(i@l4IBKpzSamYa)6Tp{aO7mXVD%HdG$j<7{=U!dpqe{ZhCn z;n3wA4&5*qig*hJtxh#8hk8+!wD6R zmK&3s?)4>$eS_2FSo)*n~a_0T-8h3P3U z2#Y9Rh9axcCnAotFp7D0{(_t(?!i(8;boeamq~#Z0_IjkvZW5K1thiX;sP|o^Sy{4 z6dp>B0d2Lx+L=XIT?dW_5jt{EE9IGU+&7~@au&nu{6$5)_-Z38j#eQdYOg}9!hKL6 z%#6-Eny92#Rr+_p6YmtLG!hq2@(iXFOMh8BbHPC~))qy1gY<)sY&S^#EQGQmmmR=Y zjBstDX;EFb-)7%)@`Z9gukpxG3SL=ui~V`mu>IKOYwLZpbBT#lb4N8SN|rPpgoRSB zzk`D5ph@xF-I&(%0u=b~so|f6+sh~e3vwg57Lu>s0lQL^HeOU5eT*r))$0g6T8Q>tcG@8@Ts~doT@qv3Mp)v&S;B-QY{B z3Jy%4PJZd5TXM`-DLPMPhi@qDLM&Cs_K4CVq`Nqm$dypxpfC zjG+Hzo@#Q<9TKM)=-PGnaM7_Yn zz6|_uMY>ERf81WbVU4HK!lZWRpT3Ys@=a@aIwpD!wsbtd{Z{`{u>NK(FWOf!dXaQ9 z^#}8=+7zeVdK`SilR}KiATc&0h}kK*YuG3`D^y$fCI8uc{x#Qv?&+(ivetcz=H2mF zSc|@XOY!lTAH3pgZTT3dE6PS9`1A*<*dxidB*>HN9#Ab>c)G7`bYHvNO)F&QMPaui z+DT{jVL5{a3}^fUqc%K##h?~r7);xt?FyX)2Ir}e#S3NQ3qQ1OvzmeV@x}1=kGIrIYcfd>&e## zur>gTCHz=1V4H{^C=8`11M9+#@Be-qs5l&AUs zYCI+=H=Hz15%W%$*P^~q_tJAdp-&MRK%^{@X~0${EZbDn;TP^30IN!LtmR*-v6IQf z^(pd(WWs&il<;BB1X!p6j@@5gJwPS}n}Bnb4}@cSmKs*iuxO*dcC8BOXEaqNpt9ft zC7{KMy7l}p)JgZCE_`tSn_a7;XRyg|7Hr1Bk_OoReGtY!0ht?^nK40eSnzz1qd$?+ zy$pV$IcIDHOv^!+VpF;R8B_>H9n8_-@ofkX%|T3dcN{h$g^TdPlmPmID@S5}*P$iu zAvMrc{qC5{ULEA91ECm9nUK_P9t5vpGiKPRJH_lk<7n_QoO z8zmi^L?$G(<)?2JA=Q4p|k9ytdBZ3QR3q2*~vJWFpJ!KQyR8Ovj*i+`%MPXyLgW5&M`F6WH~V& z61gSQ#&0_1(H>%|;|`CUaqG&xMa(9<79`*Jo~>RRd)z$aHs5^{+DwsvR*5rJ^K7i= z$r>+o<~xPyXpm7D*>t?RSN!X(LjK0YNecJ(S99aUg(!0O-##*B9F!m@Cz6}rF{UGW z{dUPM%^>of+TC(=PHf!y=8DXP0wu``edeqj#npgSA%eyK5^W9hK*El@5cIMd9 z)F`IkG?~vhGK*^StOKYP1pl6j|+L# zFGJbHPR7wG)9MBNh&iNvP5kSY6MKsG^*BW{?J+TL?hubTA)DE*taY!QB64%1(3p2> zNXhkikxk7{?(pjTV%oB6ui~E?jk<2hD<-ToPRfD@-G#Df7jABqiSiPXekx6@_`TP~ zJ$1V4*=^ICdk?f`@Z@XBH}ZQK(Z7&#<=QnqooxBqb<2>Z=tYks#n-rk)OQqb$#h=b z4`4kWOTB+J!fRk4T%n>;cRGgZg+-yXQH?!w!SSDTAA3@1SfjTEhg;pep|MHHt})M} zHTW`Xs%TgJ)mE#0*~j)2C1>>+wQ%>hy^|3jqgI+%x7hT_X_Q01W59>8`?`V2r(+$RTy8AxMU6%UA<-Oe^g@wOJT-Rnx;KkEqCMBo2PMgYXN{*eyK-cBF zT7CIU{8_cBcRlxVV~*9wj+xvJA|tad7T;A)q;TTj;wz{k%h2xhOIv$iJy**8gZz@rs#}ru=p$K=LzZ*6e;!5}p5+E<1Z=*dLjXmI zg)vOFVVd7(H?~YJh?WhP&B%w?j#D41&^tup_fR+g*$-MHeUpQyOX(67t1#7wJmJKz zja%$@l+g$FcpX2QF-3d)ddyy^1g)ssBitVjDinoRBkO|bz`W1I#lg#6Lu?oRw3fwY8 z#&SU>cb<VP=YzuRY|Z+6IxsHXZ~3%pLBhgqECJX$e*-A7K54mmfnBQ~vva=L{Ca`^|3ouj<1Hj#l=3D?2ft;zc#D`CY zGDkVmy?0KGKBGMD#^z(x-uiixhm-MC*U3XfMufJpo<*^lz*ams5HTMQ3a#T)XspMo zL{_qrQHtX2B#k;2D+r?##rzTp|IxPZ_gKCfmq=I7E_k9UbR@=t-|VEw{+bJkTS}5S zZsn=%+z@(1t}}YyQ*kPtt0npwm(pil9j31Y7+iFtX^l$oQn+}kApD|ua82G(sc0L` z^YL+1tmzk1){M)k7!G=R^XqCFWVxL&x62O7mV9$jC?l`FL994c>uBuWdSUkQXSI8K zHuu%hWhMW3kR_$q!#*%3{Wg`s?BZSW+3K3qajNWsoujc42eb{P$ppys-SqDytiMzr z+WBd#{{e-c`Ww$Y@cXVoA!t2V-Nxt^D5v~9ZQqxU#Xm+Swga2Q-Ji#AzZ&uCHN$~V z$76+^xzH7bCI$vW_F41+E_bOh#eq`AGc1vCx-vLWvRAS=L&v z@5*qF;t=Cw>Z81um8L%=cb-*$WSrNoqOH2%P{yC%F3e)>YQKrBYuMS;F67)j-`=SA z#kEt9Qt-!vQdWzuORaG~c<$8)t(Cq5% zZ>;NBqaw{qo??@@ZRa6k&E0OD#b2EMlj2%=^8UAmN(FD+o<4d{(D_$;`eMZmdSP-s z#ogOv1oV%-5S+1MJ1J%VC~*x9nNW>qt$+n<{nrRhABRnzi7Be5KI-Zj{$?#>a!s$R zS-N38cJmD1Ti4+m4IOZ&6yMIsejS)LJP0CGX0xaPy zejf{sK14i9oh^N|o%2`!#CbA`v;>J=DPGa}k5#$&a$K85_0QP8?cM)5FI<+-t9?uJ z@h5G%d%SXVU%G~r&uq__k-V?(8ZYsAtBq#r2Ey5|Hdl50y0>&*V*OQeYr@bR3^c_R zk9SUbUKx41@`DVVn${-ybS__y2vs<#a^65zFMQeRb@P#|9;h<(!JK!@XsoT+yrW;y z4N!mBbm=osWyZ)SbOk;>$D^b;_)R!ucomhEE^HGk3x8}Ns9Ia~*WSJV<6rE&hdukf z{n5zxFDu-1f9UTQX!x3gz&47G0{xH-t1u`>9M}}z8~znpI!2zxM}GFJRj;M)tKPWJ zTH5_-ws#A@tcfaJnwif&^))L=`I~z=AN&TBiR@XgxLZ%gtn9h*$L_^FA3*zq=7jD3 zRiJXQ1y|N#qti(liE?iKr-khmAMqn)0-LYV(Fz}Soaj4|V0oNz&laT6VP8%-a(=?P zDU>ecz?TV=9Z-U5$oCXM08NabKS3Sd#LN$@&tcjZc~3BZD>676u+>o1Ox#m7U)=;y z0huhMqp>6Z;B~a-gvDxOQyB>N0h6u9{kkoVR*@T#0%=2SNk#?cNuUFfKMhP5vW1vx zFYO0Ih~;^Or(qX;IJ$%E2Z3a+NOL62<&E$R3Tz^f!isR)QVDKx@OUlXwEw#VNWj#q zHmodTW(%(Y`OC^No49mSo$TI1KhcVq-lVO*G6?~}X<@1a-fm<7T40Gwn>ZMi=3f^~ zrp@u4BXlD2fJ%tW-5xLXsm@f-hZo~a4=adsw<`Q1E=|ecFKh$CV2Zb{^m}wukU6!Y z!sDcwz};sC&l488=7t)+on$(H*>^B@2bd~s=A^#tO9HEfnM-I=KXgqMgp!jV zCey{u$y@WzXxEOBg-2%QDuMGClw8!+RHfFQd{MgsOd9*dW{ z0>}qeMpx-Yr?EJSUV*o1k`f2Z9G~YM8L?#KkvMQN$MC#r%opb{o;`(r@_N7gA8`7L z=8YY^szI(ic&b(O!L2QJwG8`pzR<1x)ZBL|UF}Pk#HKgf&W0a6Gk`L8-5KM!^95X4m$aX z1#ZPpGcXryoAPOV54V^9(3}fC;ue9chnbj%# ztYeD^HRWio?#oKKL6nT(sTMH}8Pj8DabhwTwi16i(W0Hx%FWGG`0?2l>7zOZemXpC zKV4e;UB+_Tyv7wr*Cca)5PI`AXqN`Xey@YZTYseG6y1u^%+U+iEj#pPi(;lUxxM*e zB`q7z?59_L@%OkMD{!85~=bW;XQwEIe-DeNn`m zZ?TE$m+S9|N5>2Yz_L;+)$O#4=@cdRlmGIBTWfq*C|Nqvi_a*4U*JnJKZu>mghu%C z2NXg$oJ2yL?8(BlBpJS62AeM6_z<0AMC{a7`6^X4osiFky}2~+eDTii_U<<2sp+!v zkoSxR1O1ri#oeYZ@#y%y*Cfe$;gij;3kqn2*In3N_mWW> zFgik$NP)w-ImABtCzWz_JKYY+*jN>1r}(H1$evollhz<2xm`Gig#U zS`?KDtqvP6y%~5^Mt@A%i`5oK`t4Pqz4K9hGR`GAf4O!B{+iP<$svI%o4;Q!Z$%Pooq#HVsr@O$+VE@H#|upaTr*cwL`!FO(4-+-f}KVm)1jJ^8Jnu{J8#cN6;HcMKAI)6aBaYWMDfCvmYa)(67(8Wr{W;SC1ef zvlF!lmj3|&Q{wXsOpl?i#y_*GHH*%tpy0DR4?UV-b=vpw@q7CbafV{wg9fUbHIFd4 zd`RZuzNNp1*6+Q3aIV0EEtJws)Yd%CRIhc<>u$U{E*!HzO3z0A6uDQ3Gn-7i@^cm) zxz|3UZ%a;Exwl=Fv$_%$sX?|$NyvHsE6q*vxzCSnYB<~T_Pl#0^Neo;3aQI#$D5p3!;@aRx!ex02`ACu|#t9h~pMRICuM5-x55-zx$PLFJT z5gcD^YPH?$r|H&r0dHFj#+zDXc0aWJ$e-#@IFpo5dvgSyCQO(}qQOilw?HkD1kka7w%{(pp>zTr-F3pO7 z*_*wmwr}pv^_E|3RX;R&=*iX89VSav6N~*#Hy5}Qd1t3fp1Tq*g@|0#3DVo7C^Tj% z@Q;QkqZkzJGG*kIKS=3v+kRY&QBU1`g8M+?;q1b(pKGt?bLn2B*J^c`DGiquf4QVz zzSp5v@57y8f|EleUHS`3z629?)tLFrTpycCUO|hDG$WaN&0inCi`tb)^=Tbh9RF8u z8#y!QKi4k>UwrVU`q$kS!N&dEnsd^u*MCQ?8KEz`V^DD0meOD0NC~GjtN(S$H=DK! zOpzP3xgHRna=*O$$F%LPovoV^qJ-Z%rS6QnRz(y2a9I4n_CzOhOQBlIq&x2iKtsa* z8y;8QsAh(1*BSS3ifOzoD*WAW-KISGBdqWB&gXc(DKXu^!2L5X$%rFtFGB~ntn^eC z^X9qSZH*SR%DLK083B*p$}U-rIaYl4)k-}OoOE%d_N&YmwqQ#ZiU9L<51hWUhHQ&` z$TX5onRtWsc(tjsy+M3@!~VG~7EK+)nOY0K=O}f5DCZ?{Rnruly0NR__Ut<=R;#d+ z2ILo$j#KDwu6Y?zab|t3s>7{I2Cv8T?w&bmW1EdM{MTg--(h`nFedG!42tQw%l)jZ zj)kA`h}>XAj7*Yim=NaMSsqF7KWMt}*ecM2!l!Jp)10@Wv!SOd&{^4or-?{;MjOKY zFxl9msYlK4e)V)Fs`rUfF|ZKeZGg5-E~95GuF<*zMwrDmeXH zp^h107C|`1PlloD@%rhy`djX=3c8;)MpeVqw@;kVmQaW z4XhsP*m99zv>=)mYj%d5Pe5>Y>*BrvR-SwSuK8id8YL+hYGR3Gf-iy+*#2N}cq@<& zALK{Ej)6vtFxXDsPs4IBA?qeJY3(%ZP5sYRjG^%CTg#wRJcrNpOIDZ4YMxGME8PMTRcn{2skSwpQ6 zWa_1*AnMFE%(y#YT=q@DF3JVXsS1@VrVSFmuVzea4%6e~7L(7T<%{045HKi`s-^uf zG>W{oA&ugO^SG3uSG!vdRI`%|4t<>a!lYmR#j)$k_YQ`JfJat)EDCp)$W3hwD_d#FOAC40HBkzvidxX(+LHsf3G z9m*C|6)!&Z^6Oqlw^yGiWthcO$ejfW`a*xK&$yXRrU9VY!%5$=uKn2>GKzzq=W}GG zs+_;Gn624WTgQ}=7aogXa^2nBLn-G7_q z9{>7K8Zt%W4#g^#5{=l)rtK~gR4QXyGHMq+weB$8TTq|QRR8?Vl$U#A-K)utmq#M4 zQyESj*iW%7Y3W`+wS6T*^ts11O9!oWFZWTi79cAS#g49$|Nooem}j3~j>Am%Nlsmq z;%G?;&m{C^$nC_Cn{DIgOkpf-6yIaQhQ0UV(w5WQ^bVmMKX}ry@1^)JQjQrsp;FHr zWU3*M($k=LaCg{F`#;e=<^G`EEW`#VahSXU@$+_QK`7 zMXr85t~q#;q`U0-`t!3P0izmm@*Hqqh@?tVf2hdYZ+N{>P9h_@18Ct!mg!5t6Te? z#3r0sC?^%x2>%7w(n=_%4CgcyS{Pr7k==6`(c@Z9A_<6q3vMVgq5IJ95-?lQ`odgg zi1MGSDE}|5t1>Z70@-jX(m$<(+)w9>e(8?HGXzK+3RrLZ<7dbw{cG+EoRRU{y}Und z8f(w2q_|f8Xy4xamyRDxXm&xf%t;h=Ir}ROjh)$O+L*bOVciGCRF<~Br}6=?+9V7- zeA*Mc&=s!+P3giK1VXi{Ss)!f<~^=Xcz@!oSQe)$=uAx9eiC_KV3GpN!ni&99T?w_ zzG7M8n$n2;q*PCErL9&A_+(&B`RDV-&j#-EWtfY zfnbOJ4lgtS8Hy{S}m2scrMQD zNwtGlq)=1qB5$0(Yr)Km$_x9w_Is~QOgqpYSnJ3nbynXhz{-OrIiRlfc^q|ACtajQ z<(;9t!|hcY_q~pDu?;GyYdIHiXL|_0kFlbwjoPssTIUh>kx$mgOY)jEJT-QQNi;ZS zo0LjJa#XBmfo@#6VEv0Rf+P}f;T~^b89$PW;@HwoTqdwl1p^dUQ4=Up^87sjsZqiL zJ!|m{csMJTOUKGZsnbXz1YaSe+@vmKZF~9~vYw&Z&{Q@^W{Hci?ai~WHY5HqKZe8ea%lz zKbim1#dYNF-J#Lk6dNgqmY}<4x>A;mvZ54<)?zb}`xNp2A+ln#9OvF;MnxqJ5# zsU1g5B`ekP>3e=pUmklst#Ry7TXK=tR&(mh@rsW&eN1Y;?$46N+$MOjZSTP2Ovl#7 z!!}_ewqK$mzPDTaWYXH~{O6`W3v|ZMuT4L{z~%{jD9=Fe;06XaAQLbiTuVW#fhDoR zB&8XRA=i_rzXSb1sK1g@P+}vY3Z@bC!UFwB0QwVAiq(MFC!oR+A1m<`dBDU~Dk2?Y zX@l!c^QGc4mSCUyH5^T;UGio1v_`2;qCuOZt?Q2KM!t3m$_}zQ!oDaO{L*J@qGP0|D!F-E0H$@A%A$>Bd`ZX6@O_!j@6a@`p z%`}B`)EfjvMoiA?=qW^`{H)L5b$zV+w=0(=8U!5cUQ)|3)qxLXT-Gt8>gqS|KGa6C zh+wmkty-n=L1$z=hg|n-Yj0tU`Cp0o??`a1awv<`qh`JCjA zecC5R(hgByNZ!8?7pFvEUg(k(*1*JcgLZ%cu1-oFv4+jS$jbxW0q$89}M z&yr8OwK!N#Hc<;BvKAle9W&1f-+frvFhT0OPVvcyk$bUbk+v9cs z>*vycWEQvR-H85GCUfD>_Ze)6tRBd##`N-6EE(Y=-+#vY^1c6Qdj$vzhU^x`*We`Xi?eZSh) zDUM=;9g4t71?n8a2kNg+XPdE-X# zgpq8+rt^Cl*ezSSX{f?t(x=xn5x;(GNJM7uMB`6v1oWTaZ!1Jy`}45E{0Y#6BsBmW zS#Ax68diS&+qN$t7#I>P!!X)3%!&2?e~E?rER+kq1__cO!!VYM^sngr109iJ?|e8- z5RhQzJV(7PAd>IfCaIq~_exCe?z^fbKJP|CXn*A!D+rL00r#>NbBg9 zDf~~uMo0H=Sa#h$1b*6Zp@)Qiw_}fNY!!2AtqF4Z$j)o|K-q2-`Ii=$A6?%)(sdgg!C3f5{2^!A)->^ZC3^9Ob6U)vd5 zI)ka9!z%;%F@g8yl9JC7;C`(AaESKbxyVY=?B#p^m4hlp$iX>^1RWE5Oc!9ut;>G% zy6@#B1dz)7_q{Kep}=@b#D-2Ml4am9E*QX5c;XAMTPeEv>3{0J^n$~IZ^xd&aLAW# zO|lkvBHMp3mOG?V?e71UIEp9o1{te#Ed#qGg8hZe#-3dc0j^T#gaF{*8-I;i_&2CQ8UgKXkC``wezRmuRrNTrCCCn8&VjiNl_?tSy@tJL( zvu)MnSkH@y_;Wzaat|%%%S8sf2-^F|dH%zhE$xceIX%Ce`K8J`B;Vs8>^d*(uH*<4 zTQuXHftlm{{|qfO5k#2hkz*jh9w-$hbcM5W=iRWW>k4m$5vhP4UMh?)Pg?P|fdwXk zt*`inbS!bR_i(%~)322FwJO8oU3}k#1E|D3v0Bm)z(zfcbwr5J;-Hb7q7o`0J24d#AL`jAe2ZE!>n%Kom18GG!t2{>UB zXC0_gXvk|XO#fme^t~$mgS5~5N>edG#fij$BPav>c?3y`;8=k9Vsx5@%DFeC)d#Qr zu}WaCKBRv(yQkUlle!ICw323n+MNJD{jS>u4*#Kg8J0koJi(00N`Fj%_#j*74SKSL zp41HYJ*$U=@AtB8h7JS9jW0fKpZFlIM|G7tM?qe7#z}eql zve-&lpTlCKKB71aD0V{J0a@`{HS5RfDA0w{+`$ddjpeN`DqYTd9*A56qbsby-iCSs zfMAuu$j2)qRlVG!(CC@bjf;IMXx8k9`1dcIxQ9^)qT7s;UFUVfUu>wT+e;h`m2Lc> zn(h7sHn;uL#mDdhCTKZjg+$4f6Byp z3cZI_e;T6-6CqOB%5LXCZIR0Sr=?3vr(e)p$#4GWY9qqsBQtzu zQl~11yyk{eSUA+z2i5Iox~e&_+d-`@>4~VZZ#03po3De6l|_Hcg-ai3{f{@Q8<7=w zZu{IQl<|kA<+`E_ePz^v*UaZmU7aESz;kf;U`OX|)5I!{>%})xj`$08gynk1+)(pp zGteqERsUk%a54x^-0_RHxL;*0uqRd1opUF@^_th2Qp_K>Zj|(OIr>{`x|MQMQj1ks zr}Z#<;W4U)^$)mCWFPET;2Y#QqBgA<^q#&qet?1VwA+>3GUd42i9uxhJ)}~K1a336 z3QlUCvHAU0^6-@#hOru(ZS@nm$$!j!z3Y0DT!}wR_x+d8RMguqD2YlDD_C}TtNqM+ z&#~UqXN!c3SJU+3-n?V2y6fXDeJ7JX%hFm9ONT?NHOM$8nRTe@cNrX{l@86<8BC~X zXIOJ-`;q&b53lynj|6E`Z9HVk#B=nZ@R_Ujkw@uT3!HbS-1Ssr`TQY_PLYW%Q{Pzs z^}a~@J(L%@mp(6w)y9l=N~TFE-QMdy5g`|7emLrE_wc6C=EbvR%F=b}_Tl3u(+9Gx zKiYpO9=N4JMyDj9L0d3sE?8MrBo$NEhZnoNHrLUECKR?HP zokCxLLf^|WSm5AxrgBD>N|WJO_YF!qkxgzd>-xSXY2>Gh+|BtOZ(wvxX)6|K+x&VhiT(ohrRp^sUEdFV?mTGO5C zcsTJzP6#A%g|*IfnMBtxO+T=2jIO2q5WnKoV;^D74SP~yL01~Bi}rCpE3G4gWFfE& z2`g>raRU4gE4zXo*lTD760My&a6+gSwg5}^5W+;lW2%V6FG{nHa8m(p+G7i?5MR+M z1IG!&^LFU@a?mTV5dhX;>CslipZ?9pga${l*SQuz%FzlV-n zJzD7{E2InboXd0*J<5M6EhK?tA5tu%^M<0$tJd88%C)# z12m$WyT{EI>`yYZ5?eS%jUk@;u=B;poAmjcA5YRSjya0=FjBN(tl=b1ScpMfkuQ8URvVz+fkMP4N0!YQI4t1P^{sLT*1ug77IywK< zV8}V$ODwE!M-{wz2WOxk?3riOy8nNihKqvp>hSOwVDunq>$n=UYYwBX?6kz?S)o&h zi!*RCR*FPPw;3s*$kX_Z<+n-6OHd>2S5EdoG%i1|I->|$t|3*zTSbzQP!H6gP~ZwQ2pzQ0i~si@=B%U+ zMSCcv5m(T*e*2(9SnxqHh$bRB;Q+`gDv*a-bA`<$>QYv?jba1p$nJ4J{xNlo>q^>b z;H@UkQwT+ak#-N~04i)n3b4jZ!cNdS=36*{q_$6i7~3z?goys)=ess&`#D4q&}Rnh zMF$i}(_>5Um5i`1f9HPl7QBa0U`3q(6!>pXBDI0Kwh3}D$CxjaUpo{xcShAihx*d` zov;lCq<2Y1Q+>&PP*l5fdJ=-*;F2VihyXD__Ud=g7R;i2}r@pl;Msl2DOKLh;7~ zq&vm<1L)9O#^C$VlfbWLIM9Mo3&aaPDBdEt8oU80avIC9oP|m3G8<74M-0Rzuf1Q7 zgk`ybsCuZR0=-@fL;c^eFTHYscp7A()8Hv=5(^VF2C>B159DL*UCu52_XQ-n#EQT5 z;lYF(kpV!rfP~}te}83$)Oe5r5)~-?a3cfo4@GsWa1lQAuf|4jvdzam2P^2TA7xdXt_zxVWdbDc4H@_l9t85vdHj!aLj>ms*Uj2UD#SxXD* z*F5kGlyF;;?w-sWf0)T0bCjR>bZ($cAxEjtGOx5b zZt=>Z&9O#Ty&num`C+sn%Fm=;B&Ba4A;29@Moj4E{7elST#+&x_qv0D**sfU= zWH}wMYE|0n?X4KgvadOM&e)4qRLHPBS=8sDvu6(57OihCQhT12;9N_FI*ES0ui2W^ z<|mUT+52pX=rhaRnRiR@$=G1);@inX5>Jk&S2v>Ss!-UO3ZhE$H1V4|Z^o>H*W(k~ zNdzKNtWC9TXdMM=?mfNTvy9DLbhdZLseV)-Nx_>#=1v)+Lt%)^{L&l*nw!M zT&oVW6!HvcZMr|N%C0QnCo?&HK7Q}*%&E>ObGzd3+m(KL<>Wf|>F_gpp{2yd0sC2E zcNk(MoU{ohInk*A6f!zoPny#9s-%cFcv`-(R);to!-$=$fWcGD&aBZTxA9 zIq0LHD7Mv?>%+&DrgM z!#}u;BCMxuA<(TORvorm+DCw?#+6TvA7~Ox>k?4-ay&Ia=uSgL*z$tm0Q4BHN$ODe z^c)-#J7$X&K4%Wr6h%tMQ`aDz0uTbuMjNOERSVJDt98wX5Q=mzvsdGC*Z1tISgR`J z!Y~$k$UwTa#zjOz{8v|ib;0W*qRbR>zbx9d%fp8M z?0gy~Un2aCup@!>a_+G|?|;53n{_?L?ieDMXL|9-!Toz~XKfa_D9%c^Mb;#rv(7Jw zFXENlR^|0T9S;y+m4BAa8uB;OK#!cvMHSWqILtLxw+!=x;G!ZKV^sv zBy^qEJT-p%VMg~yPKGa58w$T0+dTKnW$hr-(a_OgjdP&h-Z?&K{rJRV)8d{8s^?~Hx*6;`5fST^^->Lf5OxuEE?Pa1E6x1P zF?C=|5E)qz{i%<3T;bQXs)M&ubZ|&UpV)ed<;tEj)x&P5Z@rNG^10-!h*0ir9fj{)BI z@3`-NuJf7EqVSb?OkVj{$BxujS{f9oPQC$w0~##`LS<1cNxAGeh~ahz$tUe9^L z_|cBD#Tv9C%;X|BKd}isJRy)3;>$^KZidH8k$p`@-2@xu$@N~d)AX-Zh*bxi&kXN~ zKfBF7r|i+#tWNWo+hMQmA~K((+m`vN)U5rDOXYf|U3#M4ziyYk8DcQ0 z$$b}Nl47DkPAxieLc%J(x<@?f;haT6vD6P&dYiMNp{Y7SJz28UeL~I5o6_WyQyb3y zYT^-u!5N0;t$^W+VTsv!; z*I?mUL0a<2{PJQZmdG0de?sir9=Q0mz2|mejnEn_id))s*iAQJYlh zPdr^WiA`$`$dsPq(&IRo$s#dDyFmpkDSj9tB>@Zi@J$8i=W8h5RVp|1REy@;dwW%a<_5h_M4w4VwVEj3_W09%Ukj(hq>rFkBBsJY(e9(k#8j~jae|tz z+i3Vmusf5|%Q4}HN{lsEr&4`SKluVBfTl4X$%JVu3TU=gR2cl^QK#5CELU|JkqMuv z#zQka2vm+JtrIvHdHi|isj}0hPU}w>U^i_*_n#vpJ8&}-EY_obq=VpOuu%9#T$)t4 z>!|S_GWZF`(fnv8+=n@l;4u__tj1}tBOp!K{Pp*U_1rOB|C6i$Qi`Cr@cnoF zWMr1QP~uYAKFMlh|D!VBmdZ}5LXpdrxzMPq_0R_n1W!>C&>ra_x}M{peUaO{5s zy4-dI9$zjMAv)rMbEI7p)x(6jh6M^W!T{?)>z?V2#*1;0d+&*BG1 z)tm-0@`!VzTmo3m!2+9-ZSL^WMAc_j!E|YIEQ^s{I8&rjl zGsc=6$;&ZJbWM}I^>gj*gph&L^0M15UAgkx#U|~hrDOWZIz85Cc?rVLbpg*hKE^oT zYM}_DQ};J}!X?^!(fl<-hR0J*i|YD5PrtFQip=WsIPptsWx-xLf#_JUlDxB5ZLR+C3=%^bA1^m!#E zszfyf%%Y1zs*fE=+%0XfXLK*^FX0bH(zmxdR4tV@iM0o5kdbK^ePXoJw`AnLWNJoZ zp?59+Dg!S^FnQ07_P1fNt#9cg#BL<(UVQe!sB>f5)?oI9Bk#q1SS8l|y5viG@{gAwbFb}7&;kbc!W%Gvuc)>{DE}+t-hb*zJBI(oL645 zr@G=G_2+ud77LH{2dsp9ulu0$sLO&iZU8nCf*hw7eZ$ue;|yZgoE7wkK?b#mPbOxX9KiDS2svgOSR z2^yA*t!8)nAMmkK+sM}LXV}xf$7bTzAKytO7Hi3(H^gw4b{=DjOIJdFRKDY;to9OY zHlYhSZ!Im*{r2vO*XJXz#_s+UF2n9Dz9^ZJ^(;l^jx6Uzok@!5ghyZ51@?xHv2zM} z2l=WbIrK?9jPr>gW8+}v)i;%2V0B?JEEBBOTNl5+yfdm^+%0|a(i(HsQDjTGNhgC^ zY0r6pvH&+Ll{T&lKskcBdJba}Q+4PkT!1N5oH|&xjc9rlqV^N6!4xOjNF6+ovG=PX zo7p7*vXjzfhM(TkJH@r2mx#o2k1L0gCNmmJImF->ym0ZQIwU(;x6b$0wca){!Mkfs zcxux8nt=QkkVAIN64!}HS3>2W z8Q~aI>U(0;Zf%uv%#&~hS;#EpRe)CH16yEn1g}|nud8s5&?~9>U3YG<=FdZuer(b- zW`zzJXh-&!*jqDQPZ^(iit;|#US&>XBuKZB*9FL2-YO4z`2I_2NJzv@T?LvN+;l!t zR(IDPZz@#FoccII6VfPPPNzLL%8wU;{{^4`CpAhTXIgQbAh?9283?H9(7oO0I8@}| zSdeZ>4(Ca1FP6UaTB3|krp2vo(S1sm$8LA{+a<@4j5rUN%_t*th#l3!Nh%U?n&hfO zuz~adD)Suo2U8FpUrJbf#>mftCBfw{;G^q%`>>|)fxUx{K6SQ(b_&n7?I(J%+qf0b z0%@yUhZ9S=+CCxo9@cO_DUc0yWc?A#pMd%K4E=*w@i?Ptfr2brmq}2#%vwPtV29u_ z21%*xB`Td=y>tHN@!eC7Hl+* zUDFb|7$rKM5f)S+Yhd7+8+z7hLbS2VbEg5)dWmika-OUuEMKW ze}eIZHv+4xY2hsVCW>$POVFIZh6~AKlaV^=VLwUC8z%+U5i+CT1lPd|HaeRp6yXO_ zM)w{9y7zBt@~892yH<@Rln7ApG53R85ZDSXXObhV1d1YAf51cI;G~R8LnagNW|Zy^ zlY2YNkb)~3H4iBLr>Z)%X&Kc4Eydu&|Kxn3!Av&+J8*##CczjKrB26AM?k-E4y=@e zeW}8@w5$ZsF$U-T00%&8v9rk$k=Pjz%O`OH^G4BO1T~Pfpd(Hiog#{S$Z(m|%cv4k zf*4dP;tb@qLFWnKkIg}eRm&ujc!J&r7&4;>Nqg$>Q$MjZfm|@dH$sqf$^ttu9gPfZ z%QN*#XJUs;U^s5M0wlZe9D+62X%`>|{cs{`JeDK-Z{NePK_~2bthgb8mcKv=HadgU z0|q3yFQxIv@W#Pw@SA|A@eIUT$b=t+fl-9Dn5+oZyfVc zKUDf;QRc*xGV8p}LEh}Q4qfu>CdPNxquBk=sOZel z_&z+yVXjq6+iM!06UI)~Rm4s8O!lVrfnyO?ub=8z@*9+FQ{VZ;{m=i$*p~-V*#-aG zl2F-_Eo+J>OOi?m5egNhMM$NTC0Z1+?|ZAFB(2(Lp+%My(n7l;YemXZ3UA{6X3q26 z>w0^?-|z3Ay6%0RbLPyMGv_mN=FFV%?kbpZYMFsd#Mct5xr-8yKg>SRelU4~^70(} z6U~-Z;!8MIh3HK$bMFY0x}{M$LG@MV^hF6%%#LoTn69Pi)5hgyJnQ-5^KEX^=6#$K z>s9L++cC54?6o5YC+ur$+g1B2W5wQoY?Jy195V)a46DK>ctig2EqPLdiW@cc9z6@y zdVS5j-k_Fk9!w!e-bNV*xGk4s#XGmhCDcIBuH0_h)ZJ#C! z*;EgRsfC6#^-s6^682@+HRWTXr)Kjg<;K1WNs+Gj9CJigK)U|dvB=!6IEy#xhpnW? z`THi$8D}LraN@?kv#&Wg)_iDLxs_KtSbyR&%d_8}ESA{4FJi2ec6P42`43(f&7LJu z8%u(>t_qL6m=gm(&j@LNyr{l8d z>$_?zm)(zOe=Xc|QMPuPp~Xt~&|nTL>nq{2zNo4%keIyK&3p0wLyDXdh?jWyDc{3p zo<#k9h>pSw(WJ);>bQi6eGi3WusjP?u#uyDqispBzvCB8Ld2qhlJ($tAUn4SDtSnz zL0-hVF2f5Eje95(2>CRqj6<66m%b9hiSB6ViqcoqdqE^ytI+N6q;vW%2ndXEXXlaH z5|kjLh8x-`pd?b80hLUF0HS38xA4J-5${8jR72Zp$M34E295K9JMUeq2E2SEsV2u;wHSwCZ z0^@f>@j&{5bitVW+_xO+cZzih@7dX7#*=};HbjcwrBW?;16!$NAGl|d z%^1drIYh`TYk1EcqyLUyV4k%Ozu?Smljpp(*`PP0@3s)8qeZi|n^MH|AgM3#w2?uu z%qW;Bdf%WQ1SW!@6rzYg)x?Q?+|x^R1`=_Q`0Vcv4RNX6QtQ`7-KZ@Ba@SCKLnw*C zOmHFW?FE4ZLh2G^xgT!pcyLT?X)@4E7$|N$gJt8&)ZPp6>o>vf4#}O?QBw(2(2__K z$m9ZM3XG&c5he*lI_c8JG)RDv09e7YgkjX-Cu3XxCarx3zw8Z5SPHCQC;Qo~>J|PkPhELv6_Zg!BPBQQ|W3?_eMl2AWf)0OzRPPACPTbbCmJtUwNq ze+I=P$g+=e`ctc@VZ;PvkJso)j1(Tb=YamUMOwWd)&h8u5r8QtnX9VOw_FSsyZX;K zIL{siLlingbP4%gP;wg)Iu6e{^aBD7un_pdR+C|P$_IkQ5$63*LV$dDUq4B(?a@cS z)p}DTPpUsyv9Cb$mVkW$oY2zS`%mankHADK?G-Oo@sV7*|7!|CsOYTjKm-;Xa7icx zHUuIGz4SD^n!RWy*uc$;i7p;v;NhPUqQFKQQxB9=gK0uH#-CxEn02h@%4MOxwBjil0a~X4?h#xB+7K8-pH5?W31*N2#JW(D08<00 zuBhj{u7ZV7Wk^z1c7mNk4s2*qK_)UY#N%=q7FAI62~%ro*1{cQqJt=E%ZreDO==MIe@x~=1zRd} zeH~H*NVn{PJ~aXOk*YO8Pbkt8LJ{b~D%S|5eCX0apy0}F2>hjI1$jVm4TJl)n=z1Ppjl zjnh5QkxjUsNv>{^=m;kSDwq4VtXD|N`vhG%Qw^e_IU5Y2Bn8lb%(a6|Bk8}_QJu4f z&ooIqU6M$;&}8gV5EH^-`)F0=3XQNASZU&5ntYil;|>DLVP~C|gyYOidEgk# z%NrYRs85CS0_up^anvNE#l=NL6gCY=0j1CEmav7Sj3WX7cGzLM`hb6*3bh5pLV=C2 zRMsP53{|lY&Nyp{Vxp_l(##(q!X|{{;Go9A98(ZEa>daFQS*d6WDH5P_Ub4{>eot% zjF>$X$`Q6vYp`*r{p#nR#b(%|Y*$$HE0ddB7k7PnU+=ln=8BWx&*pviG&D+L-Q`2LRZ`op59pk{+Mrq_ z_u;|nuiHhID(m^gHW^N<)N;vG@JW(h%-!o-B6HUC{#ei6qcIhaYz|I&cH)%$nYDvL%9-r!LwqSGTeMF-IJ_cV&D4&&SxO;rSfrJl5JQ~i|5-?aEok*VkLT~61<2DW;9+IPua{;l%;8?)8; zp09Tq&F#$fkG=j%5k9`qQTkCj+L9M)J>*Jx9Hk#Ua(5bYWM1`W(Iy_m9?YKdp(=HKf7r{&p>s;{n0AdHMi+DLyAtA4Wn)Q$H zUjqe`3rTZS5d{M0RoDs?;YPq}xfOA=j4=AcNK)b^R^T2i3>3=@1O$*sn>2%qgH%Ma zMM~MKi&GRJbnnQ04&q6>1mIzS^gtrL`GAFhg$wYT)*$f4+F;C=-y#?)^fYpiBP1E# zf_h9&%+@IF)b}f1bSm>J7&q7vw$-b&=6T$~&!$13;sCUv7Su!Zsx*>t!fR!!UE~2N zb)*U7BSV;|d1PMHD1_`#?=$a1y>96Tg2%zn^v3XMR2MSJteF@~T7zv#Dn+XhW@7?1Ux9aUUT9rSyH7%vEbx$eF zj--YJV@0*k9BvI0^PB{XH7pU(Qy=s|EFpA4Lmt|JkTuT5n|tCXHK~o_xm~e!axdSB zo;`&>1UDCy^Q@gbF#u$NIcFm&gAvkSkU=7>Bc_o8!Y7z-2z8}D^pFp7Xf722+@i`V z`Fb0VmeHHh6n2A8u*WFXDqV5ltJ?{Tfy65BScsu=uFsCVbgPQD>m)Tk&&gE&FM7jmx7(K8L za3pO4MFM7r#Q}CdW_1U-Bm%;WnH!b`_=V;SVU`A)y#xqqm>gmr@$n*qIa!3G{Q$Yv z3;Whc=v`R%Tkc$)v!F!%w);T@lM9iOS3mFog!6jdWb{m;6w2em)!M1l0$tWM`;~%n&^aW@WkOz%A%Ny4^va;M1jQYi9%MMN z-QjQMo?Ft z8zmkQF#ZWgXpmJIB_0iC!ub+Eia1U3caZBTPO4ux#p1{*I}iZ6@6R8oIfd~RRMOxs zQh|yv2HGpC3X_^doKd@YS%L=aK|@{^bA+NvLl88*N-|i`+k1EyBzR7NGK{;Wsj13*ot{((6Q z%%@V37D_br>P|)@32ePIRw=KBNSzE=f>hFeVqMT11pv;Mg9$Obvojz@o=vt$^G6td zd{6>FN(9!*?8Dp zAW~+4JeAD26vYvYxnUUPgW~)&H&YX(UcXjS6eWvnKEp;RNJ0{+Z3~!=Z4QU&1I<80 z4IE5Rt~MNcdCU;plczHk|G`ngH}sU!BO;a!yat|Pwv&Jj9l_j2mao&ss2AJ2@wd(o z+ZvS%Ell#O?m3-BbUrPTNhv^?4ebCv~ z@u^j`>Owo2xj-7%#u{PU-i=TttOZ_3#( zo2PsDsmf9)I3Ly^XBw+m8q&^24_2Kr)6%m?iOt~g4Q=U>TVmofy8#|v1e|*P2 zX3ZgK>3?DskSD0)V@ttj^wTZbt%yOpQ#KX~5x-g@<}3rh~YI&tSrssL}et6)U% zQpZDHJ&z57?C!tNh*~DSH=@SENU=Zb#neKTfVB75HXPEk`!?mf{k|uk&b2)-;^05% zxS4OVdFq`-=Vyinhla_T{Mj$WJ`df&CIaSfUyc|4>Cp0f|Hdy4f_$axT2q%jUzync zCF-c=j*=|x#RWY#9aqc}Y`!<)xI~^+=7lwfcRX)BeD{5#o2{|+@90Ceou>n@t=Kom zhVxCU*$e+^E#e&k`xpCM+sviOVPR?M8KbpKuzRW**P=rbn!LriruNe3WVkQKtUHq% zY*idPTVtJ4Muu+1y*1ygm-p73p6#@%ZSm!~?uXV-&TIQRcyr*Kpg_tfD~`2NM!Bb! za!;F zQ&F9sTBivAZA~1m5&%NCa-MzUzyyIOK|#PIJZk`(fUa3NQbi55jP{5>S{iEbxlSLg zE;wmIgXS*`68v9W}|{O=lV@3w9P4Ugx>%Y-8EMPF-cD^+z)cXhidYm@Eo(xD3KrB9izE0sn9#_{8{33=A=0nM`y8 zPm9}R{AP3?;eJ#a>iI%ay8g4Z#G$nM)?40=&cD31oyAApsBtyv1;ZT+d2mxcI{H88 z2jKEZiGq$HhyV-@89eL|CWILR3IS^kje`HpbO5Pkc+Zt|85L1EYJ6+hY}Zw@udO`3 zZ3^UZK1+>KxAvF*Ttgi(t>E2H{RWyE(QrUG1oZULriCssLG*ywAy6 z=Jbjs)`AKCyTb&>XDX#xT)t{c)3N?d#3(WQ*)}2GDOkPxPfwWQ6x|}*;vARdd7f~W z3}p=(2_Tl8A!RVp5F8BR3xFZu*eHrvOQFOABG6qH^^IwL!1u7!W%TCCY=}y&d&N~% z-~?X{A%Cm-hzaP8Q8VTQ<3kB)Af8}o5@Cb3G1P7B>-}Mb4=rd0-!fPYLhNpH|5wkJ zR|auJ_%)qX4wG5g{Y(z#stXz_a$P?U&8IM^aNSMJCpeG713Z+g#v+_K;s%Ggs3N`p zm)*%cZy45v(E5K~AzFaU*ZZ? zifPk{G7>;N0s1CZO_t~_5osx zG(o}_G;v_;P|O1LCjQQb)^VYNMxd1x=xMM5mM`&NDUo%+$M3Xh{@&PEEsG78?3@6LTs2I zay&2DzOR$RFuFN!$y(sS7bo-7+@)5D*$I)7&cZx3VN0zww-+5P z^-$?@<4-A=@b%E5dsP}UbxQuRDLwBOEoCGtk^PAO*9_I?r|&^E`5B$b6$kC3bgzHB zVNk};DSy=4CUM7)S$YXwmdd`%Z%&Ij@o;5MnBu&HmzVc&wcVYZXgBR>+orl3t6~BL zUTpmQ!G8HEgG8TeZq~7Fo}0AyYvm6zKHICVa(tZJT(@HZ2M7O+y`@K1EfLd?IjhZa zSZ#k!bVdEWFS9pfgv?3(qa5a}J3;J4xM}h$gF|N~mwg`PqI_nr>Y$RF$e2D|r$1Lu zS1vMi4%Z&F=WWS>QR!;y=LrPV+kf43^8Bsmv1*c`Q1&eJ%AqR8rLE{Y5v&wsi9vnEn`1Hy#B_6INdApElh%N!z~tZjl+c>nW~{w#zbu)e`eX z>kQ61R52b4udXP~zZ5PXY5{la--l+}=Im3ksA^}JHTyikJ%}9>p$ee1 zy45D>Rhp$+28!RIBmsIPo+a@X-Wmcfsj zO5`tVw{YggrpPSm6?`acGF>dxq4cM`skYpLar4Kj370N7Sb6pCMT>i@RG0nE{gKf- za95-2UD55JA5$(f4z12ey_cQY!zrAtPbynm0pdM_&nh$W#1mr4}W78MFW?}g{+_wYc7?F@~9%ma&osJbb9hU8{|gUb=7{b@O#M6Yd)HUe} zE4($^lJin9P4nxu+eh7&!tDYA7-`=erC7n=2l&*Og058mh_VnMfL=wlI~b#d#!Fjd z_ua~0>!aRp*el-iWo%FBRHaMaGc$ipXzG4c1x83y&;1v=v00eej&z7CLXfL6cSGV?S zsvbJ-h5W<|Da1EB<7@E^=2rtB2wuV|!~lrKL}o8=$xjO;2QD*U?A( zgKML$B?;pFZKNVHhBz|eB#BN!-vIVPjz3VASw{#V)VVGFA3dX_%>`6X5Qbxj{`1xW z68bYdM>zE$?6A&wL5#^kg&zVc`hW^O=$wIzuwt4&bSp)~(G5Ke!(}oMWdgl5hWAGU~iy@WHEtOC%Grjxr(S$e|*_tj-Yh9gq^m z>3j|=Lu;{R>9{Yt+Y%#CXI@=&4 z#Kg2P1m>|JVQc{H>&GuibQNndFt(^FVGMD@m&yzgpZ#A8nI0#>+JSIUc4`TlFVU_+ ze>qcNr>0L8fYT$%93D{9UvitITNYu6!AN588^lsl$^=MH2_vQd zuMWt0h9LtcN>U78AlFc~Z2-KEOT8#)#EHWvKCa_x4s$+>+F6wP>(5tQ6R*4lDn{D~ zyYFvb5-HiJeaCX4$I{13J&o>ni;woVTIMn?E$+ahq?RB7z9pPL6wGxF8a+F;`GQ!Q zW~H~u`(@+Ps&nss658=wYpYF7zp=_eBMuSESoH}XWiB2uS+`lJ@}l(&H)+nWh-e;# zJSk(d{AneeQ6g(UiWk?!X1|)ZaxmXa%O=j}cFiM?2?I-%l{>C5mJICCz1mq^clVp0 zsd;sil~~EGeiI`EuR-cC%-KL$UW-X%UBQUo#q(*yq=lbmYD4 zWn^iZ$O{!+d-R)Itm)18Bmsr-+{z|vwm)96{6>+*4ZpxmWpmF4w8f8F;&S7P#jKRo zwpSX(ZA-(|MnhiAI7jI%mnK_&Un}e}D{_qXYMuvErk_hR zI(aADWh2j`s0CGaZ$eBWo{QNhwFoEfklNGSrNk8(&vAN9w43bZn3W7Yk;xJIQVW7k zq*d}A;4$IgI9Qj+zocrqqKT3f=jiPbt1U)d(C(VQ{m&|ikBy1JnugPSc3Xz4tVxmfC}Vxb@;*KF_{ezjx;TIoXWmbMM*MKY2dz!7}W;*qY1dW!DSsoV?F7zW&PV z={Lq$OgRuHFgilb?o964oNS(FPv%GHr_S8EL}&b^kYn#UKilPsP1oPu=C`N8aL1FN z_@7%=wwFBF?$uo?sL~?$X>a5`y;dvhB)1=D?56pwk!4u_aV%Mud@_Cir$+I3v9pVn z%QLOrgpcr7XbDU^pEo&C-3_OzeT+_arRzOYUfGP#eKG?W^pbHQY_$n~g2J%es0my>N*7B&U;p^w4L!zm_Ydu%m zu9c|vC!lk%_*u15z(y$72K~_fU$S%b{DDzhxY39mKvV{Vh6G|<^Sf`5$uGk+S?^7zpOjo9-@#`+L`@}_}Mj)A4s^#h|qQ*7JCXJ zV4DcR>YUBOTVultaBek=GsrutTPTKz zZexPVY0lk!9Uo!7d(HG$33;+_~BS8T@g@X|Ii4RKh29~egFgpR&~Gxja} zAcjVEkZD#R@@B<b;CZ}^JMkD%0JCnYY z5JZqC%^iyNe%JW^ngeJzP(8v_)94+4CrfmgLh37Y@RCyCAZG*KGf#?;GXNr%{cJ8{ zG`@$(Wh-P0S}El`hVaT0f~}bjczBASbuUBqcggIrqB@{y5Qrf1AS~i_qX~MC1Ury5 zhtwlKw2m}VBZ*34d&twI4XnNASMJx0^ui! zUztI?-0%_*Ny{_9YJ}Uczc8awV2t=%6X4QtWHb8vyC_FBI*+}n6Hy!{HM=;)Xlp!x zBsuillGYGeQ=qyE?$Wci7Vs=DgXRH&0&*u9j2=G0@t5L`xzG$1sIfKMK zO%n&plYSD0k9v~A@NiRrKL~!v%`_@6@`WC@PHmuE{;cuhYn0>Mi+f^2AOqU(`?din zVqs(GB4~&-VHbOXGvNq9=1%E~(VM zaBu(d$^G&N?X@1?&Ysi4xAn(^MH81VojBd`cz8S4m$?f!9bP|5IalgZ`X6=1hYaBo z%f#o>Lm$(f24cE=*DhRH-L!KKN7^{v6YcBP2ToZQe%SBS=c!AzT8uuGN-Xl@5cQn! zxbw}+%`RfvVs9f7<~t#R8-Vk3%pkUG@2x;yyqBv19?*dP?jtx==(l19Zm0$U&k ziWmc=cHy}O2BV%=qlmzXB#KlJR1!WwxfO6Q6e92pDi9oMbn`YF~Yzt*K!MdNz4O8>INsd$O+yB zL=bT$TL~kp0b%kY6zb#+@|&?Gxw1<$Fh6pOqeZI1l)X3FT@D<#sQHwy#eawLb%GXO z^}U3SegPO@Gr9p3E`b~1HZ;+h8?IQ()q8WY?eXE~8yIcw(0!ionC)qM^vn6;3CB^PsL!^!&6>&iA1;ds@ z&_Ic2OGHD^s*>f<8Ep5zPVU{|(q-_uCuzckZ-c(Ea)=<<{CD^&%Q?n!agNlphAgb& zC&9QIc05()N*&ydC_R|mHS;+Kv9`J-iP zfb8n@gX@$E7%+ozuf!!N?(7ygtA!Ei!W;nU%^c}@kko9#Iu0SOAreo zTp`dLRyK9F6IBkRP@}>Gs)&EfgeHsl5{F}2>HVGTchF?{w`~wLi{UqqVG5jD8<=iI zn$?t@3Z1~A1cWL8zuA{PIHGWa$G|o?I9FzR`mV8#(J9v&?%J3zp|4bG$@k}c03HbM zX=DV`{SWc(A;Bip8=%#Q!?QjJPU0)UNGlJC1AEF9(TxZb;xO+SqQwV|d?G(m@x~a_ z98^U7q;$+KfioCo4sK%%x@LL%O4+^i7`p*YfrB~-q>}{b_H}{EErg@7jZqD3zC>K- z6nbLvV0c9!(=usA4kICl=B%q(-C7{AUVyS^BW%ROk7^bQVW83I`qwMa98ft>vXz*W z!hU=N)(iqWfI?NEw`?J|?BEAPPStptFV~O9_Xmm}jzlIbWlRqlor|Ir?*_oUoIfBb z2EW9|@{$MfM)7=vO7(w(9tNm$3R?jem~ijHVa5TE$iq7hiEC77#Ub=6Bj z;~iEJmcR8&!}+Tlww_D1{;nf_Sv7KuVy<3xLA=n!@QV}7_WI0|Dof+Z;-9eeis@Uq zN482IrO!?@vRDwgPHA7<`~B7%B638-E4f3)RC~XOE0nURdSwymyPTiRu@<>Gqd? zm9!~_F9|hG$yY~YPK_qYi%msgzUHMy{{b7j+pGK)uA<6^{nC$Fx`@Kxnp?%V$SLsCN)?{h0q3ylW8Hx9$pS7-5|FAb5 zwf=K{&H*XKW*swgF|Damzb{njc3plHpsCxoBKuTSxvg~0c9$KC`A3z_6OoRQ`||A4 zSl+QA!5r3Wb>F0IKN@mcQL(g`)?)m*xG>+g@}IXqRs z_2Iicy)ok+bfw%3Z>q4oZ&zv=Hq*p*0{1&T8TpzS=aPgnEMH$U!NTy3|nP5tS?o1)WNuB>yv=$oyjcsSu=uTp!6y%7|z ze__V)D1EMld?K&pdx=dJYj+$od2>fIU6ij^?{vIT+<_b)PE#q~(7CrI&ox>1WYt7% zblUT#`J3i1MaMFQ(_#s#A?CYyU)~YwQjpB5d$8B>x6&Q0H&Hh{zdg_9dHi+G_O-Rb z;v7P2?%RcKFycZ?e3^&{mI1jRyspbcgEK%O`bg;=@|mpt)h>VeCU%_DUepci;J4Pko%hbY*i=vd@n zUD<%mfz28t5ZoM+x0Y-z+1e=pn>~VN#Kho~WPh-WTaJO~l6LW-zY}ghj=-)g%?A33 zHWJK$o&HB@o6c0o25x1vW5DT}@@_Ut|!3mfkvf&;5WQ zWP^+1F|pd0-u*3m$M;X1YYUq1>?t38hlJpAQ0$>M6Hs(`QUz*J=Xy)%Y{c9(z$}&n zkV3cgK%Z!JHRRZ)9dB3vj;=g!Q_Z^DZzBgoJX6IdR%~Cg$1Q~GO}f3s`YJuOQC%x> zfQRuKS8qdspi_bPEw~qrgM~{+Bp8uCAxlLJHaj|_ed_TjFdvj`e3W<+Rla9$%oj+P zNfg!KIMBI4)|W%9W~snwf36QeC^`cZTA_jl&Jte6HeuJ+m>39QIpu7un=I}eAAsBy z^3JI|RJG;J>t{J8^WWb+$s>|Ex##%vU2eCZUD;lWuEB{Rab4i=pujV^#6|(Z2881! z6oc9E^H9@nzppcvnJ>E)HuvJ4yQ8&MD!$&*I;SFWllQ0JAN0HR){nNsL_;K4v=;Y{ z?y?gB+sqNyHx_8+0d}E2;Kt9~^625^{n4!J?UoU}6aW79uNlq*3BHmAEp=L1PGe)? zw$tMqnK|9AHU_$i-CAGWTOv+M%#RT6;tqK-sVBB1X^x-ur=5nwPP&HwJcDNd^L`<& zXZZ++R~^i{Im?9_=qFKhyR^ZjlL-c&nvKzCp?CTvgtjF@Wj#Gtxu2cT`z}YJ@`a7= z0|fdfk$)5G8!_^bz~=fOfQpzi5y-%G3bNlo8ku;@WkE+OL<$mjW3S74H|kxcd$2Dg zx+g26FXSpKpcBj`2e)UuhAadQVg?RNv@VBU@5uEgg1HsG~4D^NGqUhG7Rupst!SE*3a2SF*b;2%cccnxLShm1(jY6=^SkM{W zbk#@jq<_5w({ji_u9EX{0>Y`F6NK?-)Sy6Ck5Fxtsq=6DCM>&hIg%`cJ~a0M`Gxq0 zf1z-QHij9KMN^_hT}NqfX7DPnr1AXJoUi%w;ONIe<-fAp9shTVnr4cQ%t-4Fpqg)h*W zns!YinJOSTCTJdz+#+M3^22&cotj{Jj{+Vig5lBjz%wp-pP>y0!+#-fU^v2QaP8pn zJ#kdRz=kAx1E?TdG<1d+FU1XXX|%#NfiVxd{opqbMBy$TIdvfI2zX(kJw>(nY3 zO;EpUMol$LC2i>;ZwmzdaEO54gBVDS%J7~EZ8MQLDkHEG$eGk^FdCqFVho^BMH5L& z9E&>gIM6`b07D}!gCKN)_1&sCNf9%mCS*GYx8&@V%LS)xSQ$R|oJ;6t0f`hh^IX1M z-NWi1dnUZ;?v=k3DWsw(@G^LV>8c=m>E;_dlMN+yY4Db8neTIP)H#s}c~-`gUP$}w zwQza3xkLZ$6Kx^2_cfU&6Go4hyJkGN;gV5n-m`{^3+)lrYG*lgj5y>c2vy%*CXhLM z{?T!*Q)6EnU5$F#|5bdgtjf)-$B(vJf0y=;x-2O3)$P+9!_U_w!n2x79li)u?Q@Kk zU%{W|aQ$#Zh)%&n*S89rPp(g^GF|`CI`@3E=4vB>X(jW;g(bwURkv$LeGd)Zyd-(~ zb{|j2Lk>$eHe3HG;yW1r(9BGHbfkS((ztZf-WPdy*L9uxv%0Egi3v~VeTAExWBZ3v z&2HV~5ZUjeAgRRtty|*Z#|81p`lGh5-zI$c=iEehJ@=JQk9~7bRck+KB-H#WIgPQn zZEA~xy<1CeN7C`f4~`#9k}`bI8M+`vRsY=3_N+y(T;2}aGrDRw#FzKk6}b6;nA_I!?eov-Ix9<8hqA<+SBVU3Jyi%i8fvbmiZFii|9FKDMrMyqKTk<9gjQdG_yB z9Oo=m+4B2KCu&Gqx?JM~yeNX1lVrc&i?({@Hs@bLHB>uIEYWR^!yuPCWdToD>)rSD0FO zXmLra%HBAg;K}I+jHg!``^>IB^?63Jz>9I)qCU>De2_1rskc!+dApiqu=|SxuUlgI zc;gqCo?omYx7xxOG4*tGvSg&f z`F*0{oTGjGEcI@>hz8efVMMg@I|f`jJd`&lss4r223-T!%-X$tDRPezJ_-aLT3Atk z?gPKDarU@1x-k}?J+jt+uIp0`&pgSOKmF;3#=}aB>NmS z%6T&e{(ui0obntG5@I_?$?|B6K@175kUbuAuuVY$myL77%8K8utIbA5W2a37Z>r(aLp^h%}3B0tI{v;3KeSgGV3y+!AWF+fZ+X_ zhOU&)jPf1^R2<*a_q`BiDj>-KiY!Cb?ZY(V|9kd8SO{|lz6BZTQB27jnw^0icodPs z&B#lZnEdY_2l}xPGcc-Eda$1GiVPL12)9ZNW0H>rN|{x03XS;dFA`{U^8S= zuvjxa)>b(bL{gKPDF!eZgV<-#JUO_y`Xo!WR`7lWR)MCL64kdJas`(enY+mV8fu?` z7?sxs$zp|Q6c7)#3eUji!iOFY9~c)!f3Qys!bu$HuYdiiVsd}AgX8&xt}d00W12ce z&r%%_46Uf$cgdqw4~J8io7X_3c$XP#mDjcWU3{D3JMy&<81}~_zoT4Z{E_eL_yq=X zY!9Z3?r*x&+v0of@FtNAtdT{+ig)*O^FB1^Cx`6VWanLCT%LNn>9NiJE45EG@~^sbB_`yx z6XHmzZzx4i&Ehx6k_TMWALwtui73zTP09l`@^?{j6vqKl$y;wIsHNj}d0GIZ!Zbi- zM6rMf1u%AmoO^D^YUfmaYijT=_ewhkRz7=p@uu0MCxa(k9{3tORr}o>*W)fb%dLIK z?+<$xKQVbyYUs7)#}Y}!r3V%Zuc`QLG9@|iOmpR=*a%C%>YLe}Jv^26z4|%9YUei0 z7oBA#c;Q8U^kwZwpsU9XYt#W->41RA`rjIalBiLXWHZy^~uwFkHnf~74=OPJLuPa zv(Vq7?s-_`WyYr1Lo)Zq&-ooC{b^#7?M0WiF`oA4K{=4vH?1isi9zHw<$G)4bb!#tD3U!TV`nMIm^S|fY zq)-Yq5G40Ie1w|mUerhCdc|+)>1N6S_Y#@&2Kgi2EjIyFf<^IiWXkX zCHyR_Wk~x!lWxu%JnwYu%nScnCKZ+eEA;o4K9Nk=tS2R<9;fBowRYY70yA?hlg#Kb zTc`ex7pwSsHFeYFxqPN>voF5jynHVpd0SjSWmNmQjc2~ZX}WJ|ZoPEFXth>*sc52S z7XS0N=^CdWE=bybBP-4%Vv}rn>hBZot(|c)XT+Cx|8h6|r)9k3CrPu+y%h}=|D5u_ zHX%|#uTb);cw(Yc*#zWH(f2hK6(%>YKRiDsd*dD(g#!+gZC4+^=X29_@UuhF$HM_F z9+ns7{?D@Zs1b;$d2TOY05g#hL&!mDyBxreOm#8Df81Qv`fSF#-0Yq`HnEAaH%@=lpC#A+XNq|C%GWpC%X1cnTfV%pNN2Cy z>$?JCw|-Pkx?gJiqH4u84`Gcd|DQ3lHbczOI>C>shvNhIyKh z>dy-CfYRzX|C067ua(tq%Pnkh^(ZvY2-2wt`udV5;!MJd+Cx%RMG3t_y4NY~+D|JR zm|t3QXf3G+!|I^4Kc#VU^BQ2YxUe`&jUgC%YhdIuW0M#1v2Z}z|Azr?%)=tnP)Ee* z>8VQwdrMzfH~naN8u0ez!??Zbb8hvVDphY?6kIiy%VfSIIBcs=&)(ZH>GG=ZAnWbU zInPyBFWk|qsoqxWLA8kMn@mb=!=v|O-K;z6HzX$ZTY7x$tk2E=kdj|S=7~8VP#YBd z!ZEQl15OoF;J*PF14PloAnKujMkKF|Nq(or~D@twHJC6WCX9eZc0I~)`k;5kpSW;6&I->UTlFp}~a zG$f3Sxo1<)rc;eDpkI(xU@PnvHOQaaW%wQRLH>e=K{*d>I+VOA>{Mjqy^6KVoinnZ zeJ-|Zw4Tlv`9Ljm(`m#}39(O=O$T)goaSU5Dh z_M6via2MZ*&(eV6XE>I>-pvQvkQ>7+fML~uNOw@c1V#q)L*|@*GOxkn{l3+@QpH)> z+ZLZSnB}`|Ehvh-z7YaLjh{8^EO!5EiKrt`Cfc?EYyuc{x>r7+C<=2RC%m5N*5d-} zC`vMb{g{vfQ%E(h2ZVVQl73-<6_AE-B2lhEb=35Ar3`EA^(};dvsuGIje-pj?8k(G zd989)_3#W00|%t{{D7$#Ku`w)O2j9KU>p3xi?C=9IA{w5vrM1}H&_D>G_%;Wfp0=# z7l>RSKndJy3H6Vq1KBV>!T%$$eLg(a0M9TPM?v;p_!gEFfZmSbU528H4cM_P5|~LK zljc_6cU*LZW<7&o5fnXP(wl-kAvtN$-%LK*myjfU7Lqw(hnFnWqm<|Gke4hrz-(;GlyU|S#?Dy^qs z<=>+xH;^?dU={F*8ZoVJV3o7%sPAa+Q<@RsQGNzWASf;nzn~Q4?=Y>s)EMEne`=0zqzrn3RSC{6eEv7&s9E{EZ&mh%JMfax!m2L%Z&;xbFnI;?3eSi-FIs zB|AKzADV_Fn!Xxp!eAHi0ye{6DX3+bjWQJmbJaDlbSKecU`?Q2K(@gwQh(76vQfhR z5e5!R?xRN$J_eu~s6a!SG=ZGx2)`U&q{dgv*8p3v>>#wRhXj%}5lBE-RZ9n$EtHBQ zjdTsOn!=`(?tv!B)Ih6bcA2fPFgyc=8R#FRb;v*TPFX!m?U+@)>x4Fu=9{WBR;vjb)u^2|;Yhx**May0V^MUeiG19T}m8mDC>^_F9 zwHQ5R-8~LTuI0H4eZF3?UbHXetDd9j$~QL?l!X=rFSD>r9=BXZSy4>*tm`Z52^)XS zk=bd!Qe@HZ`)iG#ht=$VEph2->T^f_D!Y}z37<1d&+F*(SVg9encF0)vV7y+2kBQj zWey3fc}K*Bm_nTq<+dOVKPgE$r^2RA=HI`>p#(|Xkzm6fKRcc#lf zH1NFOp;THh5I5MIog!t#wPt_hkMR52<4#>(k)&WeC3fkQ(u4?~W~FF;5$ncNQV#E@ zT=7YtVLDSW{mEPHFN5A^9khOoUKbq4FT12`;*H67WR^>Na}<7hnBv^s6tqu*<6!!s zfYlT3pVou%Vk_4SU+p5{1$dANZQv0ubx`^DFv!&>b0i)vYC-ZdpnwdVj--ay{_s7{ zC=H#|XtAiG_-?u8yq?x)ygKK$=R&3pmjQ&lRoQh zq-L<1Iy;2ufx}e3G2mkW#dqW3cQEksr>6H(_pXb*PX*J*bMBtZY`ATcMsV`gu64N+ z#7h!uH>p6y0JvIMKAJ-<)cG_Cz=lDAxM27NG{`!r#0Y6BG@HI#gd=37EQW_6l0X+Y zk1;8ux${SH>XaV2BPp2_nL2~l1%gt&4fslhr`{UEgL}Na-`Ooyqq(Y_`xJT21a=id zA%hD11~<})@kKbJ%8EeYZ{4qPzgLm&;yFpL^sBSDw3*V5^6Gbk+nJ(}JiF`bc0lTh0ia5)A&35$=?kur{r9Uh<3|rS*B7}kdbV%#)1HLFFDB#F*OjSVwrw>9mL!Z;hEw%-C4|g~D zXp8r-=m?Wi_B_U{34_aK5UTT+Dq;POH{&od1cp!VrgF4H-f1U71QoD@Im5~NX3Z4a z%YJes@|>4_1<~_2V%R~TcuC4=i2WclTu4Gd02kj#D#AH>jw#h936X&K{|Ke=$p5-P zQU~wREz-)XVR~#hS5(YgJa4Ksg(oqTvTEV z_V9N0R1`o_0QM+`vFiN5R2Zf&d?6`DdI0rJ6O4?axInXYIEgqe2X8Q?x=AiO%xdTI zAFJoTBn&E1K^%)F)d_HxP<9&To_x;3>k^e1mk)U_{@^|U5kTdz>H~92d?pl7-R)V|KsuL z5P)--im|1Lz#R=}hL9%67XzJvB+$#3#ds5zlGH3~cq{19M-3bAF+gRhB6ulXtF@;3 zEL>$f!U zs(O)B1e3#5$h6==e{2d7m!il^fEM%}Y5;E@p<~}T(5g}qDy7-&eBx;h8c5*9&Fr;W8ByGg) z47e$qqgeapi<*%>*b%aF+}BBwW3}5K3I~pz#jW|^+k!>$pNc)h z-6lQttd;xzwru+5f)ewrdjpQm*Id^}@OwGNo~<3)n{|vso`c_dQEzObZH8Ki^@fX? zT8|E!2;bn*y|+dG*`qk6pVN)=obx8_n08d;&75OAzIzTkYi(F3H7`nG*36!+^l7#K z|c4PC#+)KG}@@po<-Kh*ewQO?e^8IxoCeb@m+ox+f96LH)??Qg}=jG|tFXP21M+#IddM`J48~4Ne~K&Mlp(x;^re z^W}9;LqEb5Ie4NuxRp-MciDdF;U)Ezv0N*f_NS;E`y#YJ)C6s=X1cbPotIu<5ST2( z+3x%x)SMKj1&e?(R{(!Q=_Sa1!B%>SSYQa;s1$WU0QZ|pz-A)RwpBe$^pD?T3^H@A z4omd#`kq$(q5kDfH zWbUrCsD@Cg3pBtmF_dAqkC`s-IJr+i3_utmv3rC7%RpW^1u~xDU9Dfb{Dr!2blr&* zr6hk%lRe#-`g<}|BdkEvlMd1EgLj9sZ@^ZI`_i*UH_B9G;_7b*S&&-*NWqHY&oQyi z@RKSS3+soVWFGIbq;JfN@)%)HRxn{~CN-qtFJO$AXAkcFQW5ebsJ#%c%SIqa$fd$+ zGAW)|+n!l03h${Z#Xf%20FmO5OAcW!v`L$0Xf)BbC|VZe=4cU>d2w?q-d`Q~Po0TI zC)_R|n*eP?QJg;?@@($c9oqolOAB}MdZV2Q9YV6vVT*-%1sP?91e_nixn&lq0k!y- z5aA^0PxQm!ZD2kz#Na>w(BG3cmy}*Hd#u#-O?hXjluc9a#8LN(pY`05j>kbedpE&EgEsSvor-g2NI0WOs8+m?G7ZtjA-9H+wX+&|*)5K4 zZa>VY!A=@+!QizCI>Cn~OXC2ckPili4@u);F~gc?Qb(>%c}FJ4ea6mp~( z_`swE(F7XEBzryzBlE65%}zAV^zkk-+H=tEz)449D&f6niUj6=@M9mjW(+zKZwQc; z)H4VWo*1N1t!*sXR)U~MP)uOY!krDYXW3E|cDYYjGX`(8RDHveL2UYx8WNS}fY@y? znSroaF#QZ%Qo*YoOo<&S2tXuUV5mxnaiO~c%}yC0LBoLVFB5E74UFI=HjQsZ7yvNa zupG8O$WVjhqP7x1UEPJOb07!Sx$ zrj-qO{8?ElK|J8KVGFN_HQZsb8=hr)Ji|YAgEhky0-;9uL9vJdkN%y66S9Yr5hVX* z`XF=Y#u#89C8oJ@Wx~m+#FsqHY1B`H?58|naU>|!KyDr#s|Ws&U2#R+_wj2MYi>!| zbxd95XTqr`I*URMJb!(#k2jinJjK2L0op5X{f91JuJC|;q zTPyb3t!WIA`586+J-?9Tsx@3WKe}JpYe=0@nC+<8&AFHV#%BMm_1xcg+xzEvUJ1sy zf7RPz+0Ht@l-_B#H9Kwe{(qdk2|ShG_CJ0IQAZ&aQIX0}geD|2C4@>u$D$J5c&a6~uo!0w&f%Pg_Ysd<{Fdv9G0kJO4Eh$2$rb~?bECt|%$8_LUZLez@-=iO zrIpF*MhU1KUDoM7OEqj^VD0NaH}|<8&!V-vYR5m<+smgI!lwsEO6;;t^_3FEBd+pK zS`l$1qRl4zewK=H#*xCRHq1c6S;?hsH1!PY%J2QCy%K3}ScIDdIwa~1kP6{p<(=2<)`K#uVE!GE3I*V!z zJ@(e>UtE4!{qU2j-wSh3B~QHe{J=z={JxQMZZz8Nm?fR_dF_(TS0kS8TYmkDW=+5C zyNllT<-)-YTW`zM8S$<#){v2{5bAz$MEh%(hjQik?8fWQ^_;gx(Knt*;+6cL?%lKxcY)TsKcqmS8*43XY2OV{8be^`#;q2oJ_ z@9E99JNx`&;$aJ!u)BZ*_$so@1E-G0nd#TEkYDH8AizDWn|{F8=G-_zy{Zt=XG?U@ zZMK?U;I5$b0k{$g@8XlD8Q=k0N!e%-{TbsJ3D+eki_SM6d=47+T#>u*mpMTAF;37K z;gt25RwHi-Ma4Yt2$}A_Z_1PtzRze8h42Se1m`f`V?k369|qacaEo+LfsZ2F+5-)O z%Xk?npwb9 z7B>}y!bxa|{2pev!&B`i0us8iP&-YhH zgSskvwv28wq?`hW#Rm@cB1i#?5y23FgqvtI`F-f}DtsLm$$?=`NQAz+A+_Dr`xJ;E zIoDrd>4wkpyN}*WkvLyEC30u@fvT{#=Wf`47f|wg2WhahJ(PD<#@)ho93c8f9y%^& zpO|K!LjG%{K%W)-+X2)5JNG+?4jr*2C5VQNWQe^=vejE39klLu`f;f0(=`isUD;Nj z>xQ;G;U3rd+-effo27JlT&a|j?((?hJzcDEv=2Vw+Jmz0wfupbD?uYM5kx&WTt23` z)c>)`J1-B@p(ppuRl^0m?&*A=_dL}-cl7r8XByt0wU=(IpKg{q+NYEW&N4gy`PT3S zP0$mil|5vcIEfz~QJlC56e0M@xkD=X?XkyugmVlI$9o$u4qMQsT=czlO|{pv#=z^k zBsfyWN2$r<&um@@rUPIZ)E$oaD%OrI#XMqAMl#4Gd4YZ?fVWtS4p6h<20AFMgNl;}Mtk<$c2-KHUa${@ z8X>@mjyw~)1LU*~90JQz4(Uj$y5e&o0 zvrbDOaw{AtN-7$orlKSZ`YrDK{7xmZ3~DC~0;YahP=_-1tddAxpba{TkO2ON&c z0m_xGNQK)l5W;u8f~Ss{u*Cz=R00#wfeNA=v8GglXeaXgbt8E4u-%H1U2*T1lG_zq z(HZ>Pxga+eXSRAEY2XZ&fVN1*N48UyVobXb_sW7@M0?1tQaAPU5-gcu?9`sMBPoK3 zf^;m}^-|-AwjyPuRzoTbbQJsSC^HlL7Z4tS`{3K?NCu)OV8GdRLGZMCWiz7KWMFx~ z=wcsxRBJ(Ze!A}ZlCD}^zm*#d;nD;a)U%5xS_aE71KN&iQ6RQ6NChEv6uQc3>`+S~ zpmhj~0U7GEfii3`u=N}#1RbE}0>z3$Vy2%Y`a(;)2K*js#g;S5j5sgj;|O)|H|p8^ zr6FW%!@`F2Ad=!NX!YM(D8Z>5kX#8uYokC)`BnUkaNPlk@pPYsP}BUw-H7Ewn;IY& zR%TSjAGJPi$c=S^(|=gg|ERs(<77+UR*#C(g@V~@WIcmkGP) z%*x3Npb5Rz7P!+4q5$ybNYKT^f54wEQ+@qUG~S1jYdq*`XYBTVu5%7IiV8Ts-dw_3 z)G%@OnePYlbKlGFFD|#3CF&L(s(7MRe_X{F@xv2^D)|>Ev^`|ZanVuizhV|H7^0xC z*tlcd`w1D5MNT%EW=ZP?0nvBRYuX?{7V(Pg^ zj?VieD`&InfnYVmO!bn7mXQ9K+U@hAzf7rHbkb}_=umpB$eFp*9(OI*iErw3T-N=< zvhRoF^6lF_|>1*w7H$WdH%@psh1wDzRt9G?7e?W-8U)v*fAtZ4njV5H2RVFfS{H0jP$5e3z#n@>{#V#k)Z6J zZZ}i($Yr}fnx~2-@9%J_J^Eav_Np;|QbqV`@v&J4tv8AdtVqc&&)87Z(_``ebL}Lf zy^)G0qMJtETdkM)HS@`qGp*$^vPxqbL((m-1v#B@FtZ=?X!(Sw1UI*_&2m8oIj1J* ziqVDVXW5_i_Bt;q*QsMNxP@_&nVfJbRGcPq$h)0KL}X}+gv04$mrgGnP+rv*YU)}y za-@yv%d!0>GrZs4`!=I3=xnBt>)}m{UWC;PM%l*1#eZC|;%HJr_V0r0+dS+wf8UgQ zwY96Ze*J_k{fiO@*ZqED)lxoE-}Zp+`E11qHJX7QjrXwc%mBU)(Lp!Fw)vBo%f>w* z$dJRev9>S3o{%UDrPNOcXT;%GZf3R4(%q*5!*_iSP!B}0TCgukh983C%cix4daIfm zCF*<7J~zsbx#mmWR9Mo$xj)T=Z5-NmYid~~KQ&um{?9EpwAGWIF3L8+Zp|BT^qzLH zauf#R2sO6FWU=yKZ%{%mF1*9MP^?aO--!H7A3!9! zcA+i-cZ56{R)wf1_%Pl@*u4JM9mJfHsK3!*tOSA{7+TVg`avkin^#8LmUI z`Txb(Lfu2dIRUun0EA11c(Fx+#tGMTP#2MEwHR8lsVH~>;1f!r4~q_@R+QddRst(^^_`f)AZlD%@T5SMfyIJMkq31Y1){iFgAs03Qcw&&eRlNA6dTba%2+ zFL8v#-Bq08N~Usa(5n%X7m!vS@FjN=Q;f0ZihAT!51>Mxb%FiNqKxAAsB2lBgS45_9 zC2J?*T=KzH2lm5}aD>SoBk{5C{zMRUuU6R}=_iS*`ln4=EpKF8QFb-kBtE---z1;B zJo(Y|{#~_qG}Bzu(LGf9!|TU1%xn~NW=8O5O`wkik%JlvohED5Y2LJU)Cl>Qz^aro zlWsx5BbJMz7I*(Jx@RY+EmX$WI~H~*f!SG$-~rGds31b5 z2nUcEw;)#4hZPRYW{mnr?@v;Vu#We-oSAx5t1b7gQQg{vy282i6FZM|Cbenk*{Y{& zrTTkkmLd-k$_t~y9T5fZ{=5o1Mbg(st?R!`Jaf~(M6l|^56}#lBi;R zrS8X9@LDi~J@eSyh`-Wc%hFCoHqhWT=+ z&f#sM`nsGp4S?wem=u9xF;R>o8VCiTG(Z8-Ls(p(G$b5?K_{yoggtfORrc$)AW8#T z3E@XXb}3}LA$b`j*k$Pj`3eBjw}@_oT*Ez7?h#2)5fD<}JVh4VfxdB?h$T9__3%V= zBs4NQG6*qakjo0xmIcL7Ct&b_P(d(-ygHKqhArXuxK(_0fC(fYE;egS`*Ip1;d0E*&cua{~aliW#cY|velZ&EL3K~0AlP5Jw@%pt&Xq$);cW|j@z+N z1N08QAhiM&25U04H-CMPh7(%#%+qNVGF}*byDxc^lZvP|gmd5xD2L5Lfua02rfz^u zr=PCpUYF${q_c{M3gm7J)P;eJEn>LJkIFJOJ7Pu)*{T*qK~ZH7m?>NfK-j-p3=bm_ z{Oc<=Qo?PygnD5~mF9&Mu^`L5qkniy|IelWKfHhiX$#921p^p;rt$VIv2$-7f7{V4 zP)WNYEHW*_Ju_|jmpNm)vc3s>n>J@!YWZm8tlM7JF{OB%KrOw`e5Jf=&Yipb&t7C- zmAWkVdHHxLTe>8FN}t${BBsloO|)?q_6|2Haw>Rl$LO54X^w2?Q7-aan*Cy*%hLVI zBD~hKzYFvAMx9uQ|D?%E-?Flvy0m59SkrjP z)a8}fmFN3xc-Fn)-$+;7Ljyp^bEtXTcS%K;uATOQL*b(?8r^gbDHL|Lsy!fn;K4?_ z+v+KN+R1aAM}+Ggd*NI2Lf649OLiJxx6o9Pqto9{51!#(J;&qn`e5y#G(^@3`Y^`u z&{Q)<1bY}%B#C6Z2qli6ZoAvlO6tOvD^5-~R4y;!xnH}Gsq~$rS1{qR(9n{Q7vi9nVE4xJ1UbF>^5qBW>9r1>6o6H|LQr0;axwjpR#4l`q{I0_UVE{Msl#|UzxLpi;@T{9O3H3xX6Z`FGJ8*CbZ}qsbtF;Hf z(#R8@c0zk~7dIkzqCD^@L4$&NWNKm#}pMgbx|>MwV1dI2GiNS0h6@*z^ac zS>Q-iva5?OI`RPB!fD{iH({$7U1G*2F(|Qz6uZJPkbfYK4ZBgWkg?3XFXUiHs0exE z#Kl1zP6KSxaGC%DA`l}X1J`Ka3v$6Ak!7MbL7sBrdIPjFvoE0O@No_BycHq6rbcJm z0XLiCPB=Khx?U8F14Q%@90%EEw91aa(N!%my02d!A!77UfEJPqdBN%&$Tf*wBBTkZ zkaV8ypxPbIoLKM>*;586x#e(s1%d}(luMsh=A69l=3pDAMV1zsZvMa zbs24G4t>^enb{U)Z6kb;(LlS2{3aLuxZwX}@rLOPo0=y^2sDy(+hNTly>(D~P#Fst z`2EtUD-T*Z-9BJnApuuPpfCv^T{vig+|U0}F7Dn@yqmoop@wpFnN~y#Ty)#M9?6~+ zT?v(urM2PfZACy--Rd^ZX+@8^_aJZI^g!Db4kB>aZMO7Q2qWxLK&QBHmb+Xj@}6ps z!M-R6h{ZUd8H++DaRO=-Xa`*kfxBM~x$FH)T_I04R7gfw_VJeACS(hR<&W{FN7$Ii z>$ykXdNQG*s;$XEajaZoWx2;mv^$K@3UYGF*lTM8N7?@+s{*)~4>$>EcWF!9#;Pzx zK*2d5R#Xp)^n%7@vI9WLbBKsw*il)Tl2?6wlkBx8w+jR`N)F%Z3_H*^Yn)@7;XJ9W zH9dZjL*-l>xG6AQzD2EJ@&@i`-z=IZ0o3a1Lq$o17@B z>F!dxH9GT=>ZSg-UU#+&N6=FeROU5TUu`u2f8!T8T@cT-2OYRTedH2BS9uWRT6JeZ ze85&g6lH?PSsUx1>uoHK0-B13tGB9c?wwO5`@@$cM_LeWHisM^h6IMYI$Vvw{*NZ1RYCpj2NQ@x&MT1;FdC3c_r1I z|1I&1i8qzdM4_a3C*OoF!Y$DIFyug%n)!Q@Li%e&mMkS16ZVYhLSuwZm9Qv{|HpQ~ zTO39QBAFntFLn{0KAl{{!*=oE<`))fsD=1Vfd%!~hRv-1wf;brOePE63`sO~k`aAP z_(%^F$dMEqpg?a(^WKA7OtcLwB*Ft=xjDgJgnW@%Mx>^ploz4`)3HfFZa07vw5I@H zQ)+>CFuWkaH(JW^ms5pU^lq#9>=E^jEI&;`9!gDClkhdTA6Bb0aNrLsk4f z_yQOBhKkmaBu4G|3-kQl8!)V>Q!)glh;D~tb~>$==+IG}#il&iGYrKQlq|oAWEuuF zpvFSI&_pnJM93XE6_Gg0GLiew3p*B=dtt9@m{>7nyAxO=SSW}acYqqhjd@2``E+Ll z;we#sP{m3rssNg1Qx*Rn7L2N#I{1GEF3j5}VLw*J_jq-T-q_nbdGN?vrSjgD*U%=(|Di$4D{(=5-p zkhp46g1!6F>Iq{+y;fY>t7#Lr_~n}pu~T=$Y#>+r$=Kj<>#M|uTS$6Eb+b@TU{ubwU)EhOg z(Z8?z&dbV1#q`OP^(pIiPj4J4;Jss(?3~JFCBIgTez3YMsm^Gxsj-H$jQHmfeo?NH zCtb~NO>e2SIyC>$h&cY{TC>G7E?(IvHM8zS#Vmymx#^D9S4uwwKA4?rI#cjkWdCHL zhSMLKwDL2bIH~V?cX{in*Tt&?dhZX46jX}Pg!pJe7B?NOr>b^TtedXrn=(G3;NZ-+ zcl1T7z9&(~p)2r9LF4+anEyB9PNC(&iC|t#XiR;VyXLo`_lHVIArT|Y-C8|%UEZhxjglmzWw|^2(#P=$Mhek*?`S;h zJ~KVvR&Db3PtWID=tQK)DZko#fW~(wC01{p=fynH=w0SdR$L6r@I}~j49P#Ob}bCj zkN}S$eSqBd$Dv&s5X_}D5in`&PhaEbqt@@j_p0<1Pb{7QI3yVW8kMJ^OhRm~Sjz#} z2e@Pz*co)rg0#;Z8-QS0Bkvr+QrK2Rp;iajlbvHO7!2YF7#sAloEc6r98j0m<2czj_IH+@x;#%_S?@9*E9@oDytV5OX*^`bP7Sva<~lzj?18K56cArPSAg7 zH(?olf^M~3Y3R5iziQ8m26ZKgp1wQ8dO#XZ1k>bZR)Q{;B=5U}4k8K@PUiCv9uiKlAm_*H z96P1;Z^{P7pQ9FJEoki~58-2a&XAo|;a|G+0ikiUIZk2%`{sHwm!ct7F2vDz62*M0q{A(@8qPx$- z9~T*m94|+_(en8ruQcCYUj0A@oiifia?rUDqVp&M>Ik~egw2KEV;YOl0kGGoR#_(9 z5AGvmRR}_0!Qq!0eiLrSZht5{_M+?3rWZ?+qYK8 zt2~Pfx-z%YG*kZgX9pgk{8P^!Wv`oVvM6L_+I@7Zhvo2`2GH65Cb*1=9=I30A~^O% zBFVq>3Y4J%#~`AelXk-54CrmDS=qspVOx;u9KC(RVYQ-L56?TdX?Ru7U4DB`0C}E= zyC`yx1Oqs@d1tU3zV>EN85LDRppDR8aK%f|md)Us$+9t>Rs6>qez?v=_*bFX1DqCU z2?x=q#T=!Bp=Pjf3219x0_=Ok z5iWGrMwS}?!~H%x{0n?x9HR-1W9{G%zHMR^2(4-u29*A7v!U0l*OA`_$7;w009Qm3 zIB4$f0R{wtc~mLhA=cGNhuB;gi3>lQU8nShLkud zGzhw<5YhP2Gm27oYy$R!|5ZXs|*4pDebPp-c=+6wwFgt4>j37eV1jJw)ALi;~bWc2R%=xq%z* z%X(Ol@?nS4L%tTkp+P9#vPUruZ9}z^VZm*5I^`_}XefjWlx~U=AS8n9{318yc&N7l zP!qtq%Jq@Xvd$LMB*mSy{EUPvuVn95RTrEpe@z`;{i9az`Nat%HWVO8Z8vcr{f<(q zciurWsF$?&m_1i}jq$QBw~Z$v6QeemYr7`c9rtMee(;^$@w!Eab639^pU@&_E#_E$ z=vnUfD;o_K|12J-II&dbX4ihjex~9J%iF(f;|edf`%SSKf2%+LM6#pB6rX{ih>tFc zs;0EWSDp&(Bv@l`Nobn)dY0=$L8hE=_@#onFal&FfaK`5L*( zVB4a5nqPB&&GVwK<2haMq|Tzmq_ll*N8J(H3V5PL1fR7A|H zL13A}^SWOKcKal*)3U#&*R__fRX8Z-dFP1tmeTg@RfX5njv48>-xgl0Jx!@>v1wC} znT-{r;fktVjXs4`kbZk_Wr z!Ftu3S6(hn-jvg0+V2xrukc+pR6b9|VXXZHr?M$ES*vdjPKkS;C#K}M?P2!Yoy()| z6a;V8`jffj%1KQ#nVpB$7F~O7DkH)(Lsem2bIgkA71VKnXi28ziZsnX4LPc2qN5|x?eX?TYt&?%uGhVF`sz0$Ja&f0$+zyF;qzY|nYdx0fP!X1^3@!L z&HI1(@Bd-z7oWka=QO5W7?k0j%;1Ziav`)s-N&6K)!NzlihmyUHu>v{g)*Cx4t@FL zmr^o!b@JFU>wt^(aWjsac}mhVx)e@(&8Rwgzgk}Cr*E;wh!MxPJRO(u*>9T4DaG}x zK2PXN`bYR4d09e)-kbH!b8K zpJtrbgKbCR3>;&x1-+7zIUqN~vNf~r#F~PVoGy!m>-AYD8y_zH@r;?{l4X46bKd+b zKO;0P4u~x?qVY`6l1QtYnI1kd>~&CN`)~cpkq1VcT)N$8+};HHW-FQ;ru5j@`WR?kTH!EFTPC~NO*4PPw2_9pGy>X7cLcfh{`z8l z_z{9*fMHkqipUusLnvt#3neu-1o>9Q?t$YoI2;Gfb{t)H3wc=JvBMdiQWQMKeLm-R zul+zz@q`k@A2mVFB#21p9;$PzecAGevp1{!laiL4N{M`#NjU6{=P(N-IvY9K>dAejaU z-J6YwqkbM$wSUBxAzxnRuPrQA3*ayTN|k|_BWO)9^;|tdUKo9#v$$0D2lima>`wG~CR1t2McBZ{0&_+xNC0j_**?bLiL>UDbN=C;|# zcI~&-TH}!tmJpSCQ4BLr0j_WG5d{X|0f;wa=O=*Ki*;n6%%oQ|2nF!5!dMY%M=YZ+ zz);AD2Q?!3G}%>cy;=0EE4gCZqeO|%X@Z)$$D{3Dm^Pg8pM3E_x4%ZT)9**>ci+oT zbR13G5_wo3GG~vm3B^U2)nXLfCEA}n!w#UvH{@Y0#dRBFB#}Egn22}V*p~1 z$$E$iD6|aIUp*^!Cz#8EDq~%CI26|HLhe$?hCzM9=pe-IFn^C*tK|LL6%z@m6fYc0 zbKrl;524Q#sKQ!yx|z(io; z)C2!Xr&tjd7&al80qf0%h$dE{NEpzn$H3M6KSr)|bO%+5!4ZSWbe1PdWJ4WvMkzrL zgn?-+8}|AMLinTBxCaozKrq52?hvQSm9&oeyR=NE^s%eh5RX0vUGPRrZ2hLF0M~pV zT-ZS%=zdbn5>X}4FpgKoHRoqpc8Is|%xkT*Eo0{};}{*@uG=`M1Xj|=LlRy20lDm9 zhrm$ zH8zSocoEv}*FDN9Ld>iiKl ze#=K%@g}LG!SA=KzRr6iWvObhP{?lgvRws6$t#YzJQDjcW^e9;d+SDwdpY*`53NhP z%9GA}{?ZkiR7T?|DjuaIs47R#SnjJQ`1|bVU+d-S$Hh%JzwBtIT_XQCOWL0o+GABK zG#B(tp71*B+A$~HJFz=HRh-(;KhsVA%YaRB!P)Lu$AtTg`&#d^;#2pShh5}v?~cs! zcl12-VYBC4hQw(jCmK!PRDTC^2B~nv(%4m3qDJ+Mdmu z@GR|E+UTe|DOc?u)Gw@BVC^;3qid2;vBG$Apyli0WiOZMjP_Z4wqoj=%*>8pA)#ZE z$xmo9yekjYHs`L8JIp6?(rIG!wod6STmOu743|*VNqgoQ>l}Q3$3s!E8HWLu2lk)g=T&0($VsU=b$~h);`6tkB#W)o;XL(zW z*msPIwLb6~3eI2S{!v`pXoM1TvD{{Qd%;Tc+-;$*V?Lj!Pv5elxHzUor}A^fpPAlE z&3UH$K2T|~bMc}N48D&iA}0BoeDio}Q#SADG1GM_yMGi6OtqK3wkNeqlvl`zwtDk) z!}swM=lyb7l{|8H+DmJz0^O5mb;jmiym;7t?H3D0n}D1B;jy>J9RI2L>zr+_*@m{^yj5=SR!_Spx_hi8 zTw{A$?Y+C^lf|zNf0_OJ;HQaBqnzqeG-?85qt|Ze{;n-7;>v%%)F7Y!Ub`$-NN>}= z86od-3J<=$e(mOcq4YYr7qXJZ>w5F!xZcyYN1y7g6FDEBv@ep! zuWajG@r-?AG^aVW`?^}rZ{0paX>@AG4a?-3A2Y3#rOj2BM$Nn{J*7is^2)4wucc;M zb&G5oJ_JoNRoa_itSLNYmD;H9Ekc?Chjb>X%l_##j5-~^?`XO|!dlzNXh_?2KjB0a zKDm?MK;+zp)su`BONZ@|{=Ki77{BXZH)fhUSM`pjhQ1-sma;CxBt^a{XV!B2>KMYr zL)HduNa8mqulXhlKm>?~_vt970Wf|+*@#uSJQsvKSWeh&Onkf-gD!NunX>5wz5#R-t)1YZ zSU@N!+AFI%)#jZfg2|3$|XaR;t5e5Oq>Ce2G1d~N1L2J@Q7#7Bml|ci6J68B0f|Ibbi{LZ4 z!HepJG68F$A&0ssc^z_5E&))3KfZCINfC|SL59H$O~SfJ<(9AZc67b(~t zR0M?)fzcaWC0QP&v`Itl!OzW>znkcJ4d*b5)PeTG!3}oo9w=Wa+Ug#Tx=`Sy4#o*2 zJVgC~?od=0{(yxMfv?D*i-s%g+epYehibug{*)(2GSZxV#8YM5lld)0x@QJ^K7;~dw7y= zUMCm5*~0t7OzTHig^Th}t;tM{J&-5*(61&*!QE}CP|QV{nXoSno*n#$1O&&=2YecN zH2^lCd)57Y*Q3xwJ_hIp^~?&0`Xk!Kpd?ZA_I4HtcTKjKuA2L%Ny_|z?CzvT+beUn z)AqN^9d@4E)xBdAKW~2L$GZ9J+GP8F34!Er%vKv>2mWhJ9!`i>8h~QAS=&HzI+{Ej73Tg51|&Mt8L$E*Jm#cB09T6cj|MJK2R0{~D0~gHcmQDllqL{Op3RxQ zUWT9QU%5UI)if(g|6yWNQ#9891Oz#9peKL`@&h_|86CLGx%FgjQ4M=)Q3e?oIf8BRn8@h6@|Iv|8n>hBHpBwr-2`&`ixdtrucQ#r z^V349dpLl3U=$%jZx!(rz!8WeSl?EDRHV;=y5X4CUQ37%YHRq!WVKWuUKDc?Kkj=;!=Ue!6WLK&Gm^U_*cA?^!^5}zpR?<3a zGpg?$(d@PwAKr4MzaX2wZ*njd(c(d)U?8LB5ChK%Z>+PHqbSPe5f0yCo(^t=3FFj}(Ib#3XWj5_8 z&Kr%*4=nF18o#X!NEMpQz0%eEYo43zzst8;f9DOoUjd{J9v2uQ4u2!h?|3|x3)Qp<$c2}hI4HqTeSF>hPP&W;rSFL&)8 z3=WFWXgrpiWk*`~KJfj1k=J$YoHca5stGb%*PQO!dA@yI(5%g!{PMi^zH9r&8@Eh1 z)LXlJZpb^0iAf37OhKyy(dNBNf3K$L#8-Y2$+h}+%FokgN%rF1%Hcn-YiU;V1kV-O;<8EFMB;A1y=9XoSBhyl$RwY)lS+2* zH%~WLpHx~`v}BUvvd`N`Y|hC3@nuOu(VFgP&;DTbVp`on(|p>D>4z`B$=z9d>}<}- z<4rrpr^jDEfH06g$v4^7O9$bE4O786)&V|hpjhMc)3Ag-9mP`3>> z?+cJ=6j&G<)9e}`(Gois&S@hg!sVh<7HMQ$WLuk2j2B8|pi2@s*w00=NV@DSV6aT3 zQ)eYLSpX!#8-N65*j@Krh7$9&AsKNU^r0AQw(F0DKpwe)MT`u7LaPi5%7Bej&dXD9 zP5Jry=ugB8piEw@-cXPKJc1dCQhFhTgfcLrvcOko5T=$~zXlKz+-3mraICX)AV++} z562@339#K`1p>AnL*xcxi?DwWUta@*ODuP9K^o}B8yup6Jfd_NL|`CtM*#g3;Of%@ z!kvD#xr)dVfglA2a0XyQi2P!xr-sDZgDD88iS*4WhtCNm;2v-sMmJ*^#DLmnWS|sd zkY&2>EzwHKT2E*sHv~Y-$dE*9AIhRj)e3wX1diPP?^891h8uRg5W!1Ux?~0j1k3su z2();@P^ASu(W7GCrQzs>>VWnMO@DA`DQwKMvfKVm=5UrK*2pq_0mjSk>p@C_;5myt zBKn2AC=fIgTveP7yoJc^1|s?0qxw)%&cHs;#YN43V5(sL>Z6+18}crwcQA)~)o|)P za9$(~qCDBJ zU}fwPEhX8*rVZ_RHX2t%#?17N-}?SCp9JTm4Fa^y8|6xP(gByD9pzbqcz$IaVSaEhh0&S3VBkJ-Hx6>VDQ!%D! zoa1uIsp~QgyWOQ{ZhL!Rn~dt<+BjcTrE{&FTFn(Jc5BJPxe#ztnmREP1w-;xqK$ni z42&wpBLoSM{wCsLaMm+bwkyVTH<`Lrf4Tpmv$BUn6jTR0b()=QIs-=_ci#~G19KGy zAeh3^U{458Aoon-)Pf)X$CyGR(H;gNXOxUX6&!{YK^I3kvtKh*1hh!zy)7N87c2L|6q zw-#6$;$$?^4JZp_1t|zYJY<$@B)b4*GM4T#AOScO^NPsj*F|hb5GxaO`Yi!UKw|+W zqX`CEXiy;pn(g5tXIL1eC9>X42W9L7B8@vnk1$CCQy=e9d(}zCnsJO2Q?;t$CX>FS zUfYhX&;NTeKr&mjXNXTR89Rd*P&$o|ct|H;P?o#da%ze5iuj9_({gs)L|*Lkq$iVxl8igUcL@w;lC%#Pral>Y~qT z`iGykEibxOgW3vXha8%w&KCC3Ey4sahCWuVb4)NX{4pe<5r13r+b@Be5}oL2CdB8| zOoVam=gRT$fyV1E`^x&Gl-0^Rv+T7iH$-i|YMYt1D)eO3=gBFmSwYhk*Stt7RXM!I zD|yT75EZkLqvv@Z5&AeUzzjJWVM7 z{e|@SbMfsj#Y--EXpE+()jng49%EmjUM9%*$xXmMSHdc+UHq(aQ}>cIhpa2jVToeW zqdcnwKBT3!7}Ml=W9{GEH|G<&m(%)ea`-Z{=nV_c56%BGpce`EbqWfed4Dcs#)Aj6 zS>bbK`!smVA0C=Wle?c;v8j>o^0W=bWoP*BtT*`4HfwI+XbrgM>3Y`ItGm^d6r4j2 z1|CeQ^#}-^o!(_v$u~=UTKY!wqPTZ~hvwx!((8zSDY{C0<<^(C=h?hpW~5>m8G3nv zVX(r7W&Aad!^&?|?vH)?NT&u&)(82Euvu8wqZVP-{!s67E33(iFk*69rdKvMt`9I1zEW@hw!?|zPA1} z&S*LBg>_ET_D6LlJLgRIOz;)y$oe+wP`aX(I-dr;SXBQspUH9hg$vJOkGjNFS`}^k zp4%U(@xzx_kpD?eaEny@k@!v7`p+$@W7hvj7oqX+Ke#n*R;Yx-jM@4Hiw|EqddqHt z9^W08MIWAsU$<%>D_NYPUV1zs{)zuhhCt8qSm#OZE2uUz%)irQ(@B+DZuMP6oz9=*A1qO$VKp4D+Hgx^bU{V-;3 zj&SeSRly2zAF_`d_1#Enw-Sp~TsBtUO}!?mw_?WW632)4Tt(H7-!W+LRZ-jCdFu1k z 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 c Date: Wed, 15 Jun 2022 13:01:33 +0000 Subject: [PATCH 35/45] Support setMediaItem(s) in MediaControllerImplLegacy These calls were not implemented so far as they require a mix of initial prepareFrom/playFrom calls and addQueueItem. We can also support clients without queue handling to set single MediaItems. To make the calls consistent and predictable in the session, we need to ensure that none of the play/pause/addQueueItem/ removeQueueItem/prepare/playFromXYZ/prepareFromXYZ are called before the controller is prepared and has media. #minor-release PiperOrigin-RevId: 455110246 (cherry picked from commit b475f1f2daba8e0ed2497cbf17f4b834e58c59a4) --- RELEASENOTES.md | 2 + .../media3/session/MediaController.java | 92 ++--- .../session/MediaControllerImplLegacy.java | 323 ++++++++++-------- .../androidx/media3/session/MediaSession.java | 10 +- .../androidx/media3/session/MediaUtils.java | 4 +- .../media3/session/QueueTimeline.java | 6 +- ...aControllerWithMediaSessionCompatTest.java | 76 ----- ...CompatCallbackWithMediaControllerTest.java | 296 ++-------------- 8 files changed, 252 insertions(+), 557 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 59e3b6653f..959fdba3e1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -179,6 +179,8 @@ of requests. * Forward legacy `MediaController` calls to play media to `MediaSession.Callback.onAddMediaItems` instead of `onSetMediaUri`. + * Support `setMediaItems(s)` methods when `MediaController` connects to a + legacy media session. * Data sources: * Rename `DummyDataSource` to `PlaceholderDataSource`. * Workaround OkHttp interrupt handling. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 348750b813..624a0faa7a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -81,13 +81,14 @@ import org.checkerframework.checker.initialization.qual.Initialized; *

  • Controller Lifecycle *
  • Threading Model *
  • Package Visibility Filter + *
  • Backward Compatibility with legacy media sessions * * *

    Controller Lifecycle

    * *

    When a controller is created with the {@link SessionToken} for a {@link MediaSession} (i.e. * session token type is {@link SessionToken#TYPE_SESSION}), the controller will connect to the - * specific session. + * specific session.F * *

    When a controller is created with the {@link SessionToken} for a {@link MediaSessionService} * (i.e. session token type is {@link SessionToken#TYPE_SESSION_SERVICE} or {@link @@ -127,6 +128,34 @@ import org.checkerframework.checker.initialization.qual.Initialized; * * * } + * + *

    Backward Compatibility with legacy media sessions

    + * + *

    In addition to {@link MediaSession}, the controller also supports connecting to a legacy media + * session - {@linkplain android.media.session.MediaSession framework session} and {@linkplain + * MediaSessionCompat AndroidX session compat}. + * + *

    To request legacy sessions to play media, use one of the {@link #setMediaItem} methods and set + * either {@link MediaItem#mediaId}, {@link MediaItem.RequestMetadata#mediaUri} or {@link + * MediaItem.RequestMetadata#searchQuery}. Once the controller is {@linkplain #prepare() prepared}, + * the controller triggers one of the following callbacks depending on the provided information and + * the value of {@link #getPlayWhenReady()}: + * + *

      + *
    • {@link MediaSessionCompat.Callback#onPrepareFromUri onPrepareFromUri} + *
    • {@link MediaSessionCompat.Callback#onPlayFromUri onPlayFromUri} + *
    • {@link MediaSessionCompat.Callback#onPrepareFromMediaId onPrepareFromMediaId} + *
    • {@link MediaSessionCompat.Callback#onPlayFromMediaId onPlayFromMediaId} + *
    • {@link MediaSessionCompat.Callback#onPrepareFromSearch onPrepareFromSearch} + *
    • {@link MediaSessionCompat.Callback#onPlayFromSearch onPlayFromSearch} + *
    + * + * Other playlist change methods, like {@link #addMediaItem} or {@link #removeMediaItem}, trigger + * the {@link MediaSessionCompat.Callback#onAddQueueItem onAddQueueItem} and {@link + * MediaSessionCompat.Callback#onRemoveQueueItem} onRemoveQueueItem} callbacks. Check {@link + * #getAvailableCommands()} to see if playlist modifications are {@linkplain + * androidx.media3.common.Player.Command#COMMAND_CHANGE_MEDIA_ITEMS supported} by the legacy + * session. */ public class MediaController implements Player { @@ -478,13 +507,6 @@ public class MediaController implements Player { return impl.isConnected(); } - /** - * {@inheritDoc} - * - *

    Interoperability: When connected to {@link - * android.support.v4.media.session.MediaSessionCompat}, then this will be grouped together with - * previously called {@link #setMediaUri}. See {@link #setMediaUri} for details. - */ @Override public void play() { verifyApplicationThread(); @@ -505,13 +527,6 @@ public class MediaController implements Player { impl.pause(); } - /** - * {@inheritDoc} - * - *

    Interoperability: When connected to {@link - * android.support.v4.media.session.MediaSessionCompat}, then this will be grouped together with - * previously called {@link #setMediaUri}. See {@link #setMediaUri} for details. - */ @Override public void prepare() { verifyApplicationThread(); @@ -980,44 +995,6 @@ public class MediaController implements Player { *

    The {@link Player.Listener#onTimelineChanged} and/or {@link * Player.Listener#onMediaItemTransition} would be called when it's completed. * - *

    Interoperability: When connected to {@link - * android.support.v4.media.session.MediaSessionCompat}, this call will be grouped together with - * later {@link #prepare} or {@link #play}, depending on the uri pattern as follows: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    Uri patterns and following API calls for MediaControllerCompat methods
    Uri patternsFollowing API callsMethod
    {@code androidx://media3-session/setMediaUri?uri=[uri]}{@link #prepare}{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri} - *
    {@link #play}{@link MediaControllerCompat.TransportControls#playFromUri playFromUri} - *
    {@code androidx://media3-session/setMediaUri?id=[mediaId]}{@link #prepare}{@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId} - *
    {@link #play}{@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId} - *
    {@code androidx://media3-session/setMediaUri?query=[query]}{@link #prepare}{@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch} - *
    {@link #play}{@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch} - *
    Does not match with any pattern above{@link #prepare}{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri} - *
    {@link #play}{@link MediaControllerCompat.TransportControls#playFromUri playFromUri} - *
    - * *

    Returned {@link ListenableFuture} will return {@link SessionResult#RESULT_SUCCESS} when it's * handled together with {@link #prepare} or {@link #play}. If this API is called multiple times * without prepare or play, then {@link SessionResult#RESULT_INFO_SKIPPED} will be returned for @@ -1027,15 +1004,6 @@ public class MediaController implements Player { * @param extras A {@link Bundle} to send extra information. May be empty. * @return A {@link ListenableFuture} of {@link SessionResult} representing the pending * completion. - * @see MediaConstants#MEDIA_URI_AUTHORITY - * @see MediaConstants#MEDIA_URI_PATH_PREPARE_FROM_MEDIA_ID - * @see MediaConstants#MEDIA_URI_PATH_PLAY_FROM_MEDIA_ID - * @see MediaConstants#MEDIA_URI_PATH_PREPARE_FROM_SEARCH - * @see MediaConstants#MEDIA_URI_PATH_PLAY_FROM_SEARCH - * @see MediaConstants#MEDIA_URI_PATH_SET_MEDIA_URI - * @see MediaConstants#MEDIA_URI_QUERY_ID - * @see MediaConstants#MEDIA_URI_QUERY_QUERY - * @see MediaConstants#MEDIA_URI_QUERY_URI */ public ListenableFuture setMediaUri(Uri uri, Bundle extras) { verifyApplicationThread(); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index 3855969830..05fb4335cf 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -31,16 +31,12 @@ import static androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_R import static androidx.media3.common.Player.STATE_IDLE; import static androidx.media3.common.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.session.MediaConstants.ARGUMENT_CAPTIONING_ENABLED; -import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_ID; -import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_QUERY; -import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_URI; -import static androidx.media3.session.MediaConstants.MEDIA_URI_SET_MEDIA_URI_PREFIX; import static androidx.media3.session.MediaConstants.SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED; import static androidx.media3.session.MediaUtils.POSITION_DIFF_TOLERANCE_MS; import static androidx.media3.session.MediaUtils.calculateBufferedPercentage; -import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static java.lang.Math.max; import static java.lang.Math.min; @@ -60,7 +56,6 @@ import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat.QueueItem; import android.support.v4.media.session.PlaybackStateCompat; -import android.text.TextUtils; import android.util.Pair; import android.view.Surface; import android.view.SurfaceHolder; @@ -112,30 +107,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private static final long AGGREGATES_CALLBACKS_WITHIN_TIMEOUT_MS = 500L; private static final int VOLUME_FLAGS = AudioManager.FLAG_SHOW_UI; - final Context context; + /* package */ final Context context; + /* package */ final MediaController instance; private final SessionToken token; - - final MediaController instance; - private final ListenerSet listeners; - private final ControllerCompatCallback controllerCompatCallback; @Nullable private MediaControllerCompat controllerCompat; - @Nullable private MediaBrowserCompat browserCompat; - private boolean released; - private boolean connected; - - @Nullable private SetMediaUriRequest pendingSetMediaUriRequest; - private LegacyPlayerInfo legacyPlayerInfo; - private LegacyPlayerInfo pendingLegacyPlayerInfo; - private ControllerInfo controllerInfo; public MediaControllerImplLegacy(Context context, MediaController instance, SessionToken token) { @@ -177,6 +161,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void stop() { + if (controllerInfo.playerInfo.playbackState == STATE_IDLE) { + return; + } PlayerInfo maskedPlayerInfo = controllerInfo.playerInfo.copyWithSessionPositionInfo( createSessionPositionInfo( @@ -244,6 +231,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void play() { + if (controllerInfo.playerInfo.playWhenReady) { + return; + } ControllerInfo maskedControllerInfo = new ControllerInfo( controllerInfo.playerInfo.copyWithPlayWhenReady( @@ -258,36 +248,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - if (pendingSetMediaUriRequest == null) { + if (isPrepared() && hasMedia()) { controllerCompat.getTransportControls().play(); - } else { - switch (pendingSetMediaUriRequest.type) { - case MEDIA_URI_QUERY_ID: - controllerCompat - .getTransportControls() - .playFromMediaId(pendingSetMediaUriRequest.value, pendingSetMediaUriRequest.extras); - break; - case MEDIA_URI_QUERY_QUERY: - controllerCompat - .getTransportControls() - .playFromSearch(pendingSetMediaUriRequest.value, pendingSetMediaUriRequest.extras); - break; - case MEDIA_URI_QUERY_URI: - controllerCompat - .getTransportControls() - .playFromUri( - Uri.parse(pendingSetMediaUriRequest.value), pendingSetMediaUriRequest.extras); - break; - default: - throw new IllegalStateException("Unexpected type " + pendingSetMediaUriRequest.type); - } - pendingSetMediaUriRequest.result.set(new SessionResult(RESULT_SUCCESS)); - pendingSetMediaUriRequest = null; } } @Override public void pause() { + if (!controllerInfo.playerInfo.playWhenReady) { + return; + } ControllerInfo maskedControllerInfo = new ControllerInfo( controllerInfo.playerInfo.copyWithPlayWhenReady( @@ -302,11 +272,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - controllerCompat.getTransportControls().pause(); + if (isPrepared() && hasMedia()) { + controllerCompat.getTransportControls().pause(); + } } @Override public void prepare() { + if (controllerInfo.playerInfo.playbackState != STATE_IDLE) { + return; + } ControllerInfo maskedControllerInfo = new ControllerInfo( controllerInfo.playerInfo.copyWithPlaybackState( @@ -322,32 +297,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - if (pendingSetMediaUriRequest == null) { - controllerCompat.getTransportControls().prepare(); - } else { - switch (pendingSetMediaUriRequest.type) { - case MEDIA_URI_QUERY_ID: - controllerCompat - .getTransportControls() - .prepareFromMediaId( - pendingSetMediaUriRequest.value, pendingSetMediaUriRequest.extras); - break; - case MEDIA_URI_QUERY_QUERY: - controllerCompat - .getTransportControls() - .prepareFromSearch(pendingSetMediaUriRequest.value, pendingSetMediaUriRequest.extras); - break; - case MEDIA_URI_QUERY_URI: - controllerCompat - .getTransportControls() - .prepareFromUri( - Uri.parse(pendingSetMediaUriRequest.value), pendingSetMediaUriRequest.extras); - break; - default: - throw new IllegalStateException("Unexpected type " + pendingSetMediaUriRequest.type); - } - pendingSetMediaUriRequest.result.set(new SessionResult(RESULT_SUCCESS)); - pendingSetMediaUriRequest = null; + if (hasMedia()) { + initializeLegacyPlaylist(); } } @@ -655,63 +606,71 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @Override - public void setMediaItem(MediaItem unusedMediaItem) { - Log.w(TAG, "Session doesn't support setting media items"); + public void setMediaItem(MediaItem mediaItem) { + setMediaItem(mediaItem, /* startPositionMs= */ C.TIME_UNSET); } @Override - public void setMediaItem(MediaItem unusedMediaItem, long unusedStartPositionMs) { - Log.w(TAG, "Session doesn't support setting media items"); + public void setMediaItem(MediaItem mediaItem, long startPositionMs) { + setMediaItems(ImmutableList.of(mediaItem), /* startIndex= */ 0, startPositionMs); } @Override - public void setMediaItem(MediaItem unusedMediaItem, boolean unusedResetPosition) { - Log.w(TAG, "Session doesn't support setting media items"); + public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { + setMediaItem(mediaItem); } @Override - public void setMediaItems(List unusedMediaItems) { - Log.w(TAG, "Session doesn't support setting media items"); + public void setMediaItems(List mediaItems) { + setMediaItems(mediaItems, /* startIndex= */ 0, /* startPositionMs= */ C.TIME_UNSET); } @Override - public void setMediaItems(List unusedMediaItems, boolean unusedResetPosition) { - Log.w(TAG, "Session doesn't support setting media items"); + public void setMediaItems(List mediaItems, boolean resetPosition) { + setMediaItems(mediaItems); } @Override - public void setMediaItems( - List unusedMediaItems, int unusedStartIndex, long unusedStartPositionMs) { - Log.w(TAG, "Session doesn't support setting media items"); + public void setMediaItems(List mediaItems, int startIndex, long startPositionMs) { + if (mediaItems.isEmpty()) { + clearMediaItems(); + return; + } + QueueTimeline newQueueTimeline = + QueueTimeline.DEFAULT.copyWithNewMediaItems(/* index= */ 0, mediaItems); + if (startPositionMs == C.TIME_UNSET) { + // Assume a default start position of 0 until we know more. + startPositionMs = 0; + } + PlayerInfo maskedPlayerInfo = + controllerInfo.playerInfo.copyWithTimelineAndSessionPositionInfo( + newQueueTimeline, + createSessionPositionInfo( + createPositionInfo(startIndex, mediaItems.get(startIndex), startPositionMs), + /* isPlayingAd= */ false, + /* durationMs= */ C.TIME_UNSET, + /* bufferedPositionMs= */ 0, + /* bufferedPercentage= */ 0, + /* totalBufferedDurationMs= */ 0)); + ControllerInfo maskedControllerInfo = + new ControllerInfo( + maskedPlayerInfo, + controllerInfo.availableSessionCommands, + controllerInfo.availablePlayerCommands, + controllerInfo.customLayout); + updateStateMaskedControllerInfo( + maskedControllerInfo, + /* discontinuityReason= */ null, + /* mediaItemTransitionReason= */ null); + if (isPrepared()) { + initializeLegacyPlaylist(); + } } @Override public ListenableFuture setMediaUri(Uri uri, Bundle extras) { - if (pendingSetMediaUriRequest != null) { - Log.w( - TAG, - "SetMediaUri() is called multiple times without prepare() nor play()." - + " Previous call will be skipped."); - pendingSetMediaUriRequest.result.set(new SessionResult(RESULT_INFO_SKIPPED)); - pendingSetMediaUriRequest = null; - } - SettableFuture result = SettableFuture.create(); - if (uri.toString().startsWith(MEDIA_URI_SET_MEDIA_URI_PREFIX) - && uri.getQueryParameterNames().size() == 1) { - String queryParameterName = uri.getQueryParameterNames().iterator().next(); - if (TextUtils.equals(queryParameterName, MEDIA_URI_QUERY_ID) - || TextUtils.equals(queryParameterName, MEDIA_URI_QUERY_QUERY) - || TextUtils.equals(queryParameterName, MEDIA_URI_QUERY_URI)) { - pendingSetMediaUriRequest = - new SetMediaUriRequest( - queryParameterName, uri.getQueryParameter(queryParameterName), extras, result); - } - } - if (pendingSetMediaUriRequest == null) { - pendingSetMediaUriRequest = - new SetMediaUriRequest(MEDIA_URI_QUERY_URI, uri.toString(), extras, result); - } - return result; + Log.w(TAG, "Session doesn't support setMediaUri"); + return Futures.immediateCancelledFuture(); } @Override @@ -744,9 +703,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (mediaItems.isEmpty()) { return; } - index = min(index, getCurrentTimeline().getWindowCount()); - QueueTimeline queueTimeline = (QueueTimeline) controllerInfo.playerInfo.timeline; + if (queueTimeline.isEmpty()) { + // Handle initial items in setMediaItems to ensure initial legacy session commands are called. + setMediaItems(mediaItems); + return; + } + + index = min(index, getCurrentTimeline().getWindowCount()); QueueTimeline newQueueTimeline = queueTimeline.copyWithNewMediaItems(index, mediaItems); int currentMediaItemIndex = getCurrentMediaItemIndex(); int newCurrentMediaItemIndex = @@ -765,10 +729,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - for (int i = 0; i < mediaItems.size(); i++) { - MediaItem mediaItem = mediaItems.get(i); - controllerCompat.addQueueItem( - MediaUtils.convertToMediaDescriptionCompat(mediaItem), index + i); + if (isPrepared()) { + for (int i = 0; i < mediaItems.size(); i++) { + MediaItem mediaItem = mediaItems.get(i); + controllerCompat.addQueueItem( + MediaUtils.convertToMediaDescriptionCompat(mediaItem), index + i); + } } } @@ -815,8 +781,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - for (int i = fromIndex; i < toIndex && i < legacyPlayerInfo.queue.size(); i++) { - controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(i).getDescription()); + if (isPrepared()) { + for (int i = fromIndex; i < toIndex && i < legacyPlayerInfo.queue.size(); i++) { + controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(i).getDescription()); + } } } @@ -876,14 +844,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - ArrayList moveItems = new ArrayList<>(); - for (int i = 0; i < (toIndex - fromIndex); i++) { - moveItems.add(legacyPlayerInfo.queue.get(fromIndex)); - controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(fromIndex).getDescription()); - } - for (int i = 0; i < moveItems.size(); i++) { - QueueItem item = moveItems.get(i); - controllerCompat.addQueueItem(item.getDescription(), i + newIndex); + if (isPrepared()) { + ArrayList moveItems = new ArrayList<>(); + for (int i = 0; i < (toIndex - fromIndex); i++) { + moveItems.add(legacyPlayerInfo.queue.get(fromIndex)); + controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(fromIndex).getDescription()); + } + for (int i = 0; i < moveItems.size(); i++) { + QueueItem item = moveItems.get(i); + controllerCompat.addQueueItem(item.getDescription(), i + newIndex); + } } } @@ -1294,6 +1264,91 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; }); } + private boolean isPrepared() { + return controllerInfo.playerInfo.playbackState != STATE_IDLE; + } + + private boolean hasMedia() { + return !controllerInfo.playerInfo.timeline.isEmpty(); + } + + private void initializeLegacyPlaylist() { + Window window = new Window(); + checkState(isPrepared() && hasMedia()); + QueueTimeline queueTimeline = (QueueTimeline) controllerInfo.playerInfo.timeline; + // Set the current item first as these calls are expected to replace the current playlist. + int currentIndex = controllerInfo.playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex; + MediaItem currentMediaItem = queueTimeline.getWindow(currentIndex, window).mediaItem; + if (queueTimeline.getQueueId(currentIndex) != QueueItem.UNKNOWN_ID) { + // Current item is already known to the session. Just prepare or play. + if (controllerInfo.playerInfo.playWhenReady) { + controllerCompat.getTransportControls().play(); + } else { + controllerCompat.getTransportControls().prepare(); + } + } else if (currentMediaItem.requestMetadata.mediaUri != null) { + if (controllerInfo.playerInfo.playWhenReady) { + controllerCompat + .getTransportControls() + .playFromUri( + currentMediaItem.requestMetadata.mediaUri, + getOrEmptyBundle(currentMediaItem.requestMetadata.extras)); + } else { + controllerCompat + .getTransportControls() + .prepareFromUri( + currentMediaItem.requestMetadata.mediaUri, + getOrEmptyBundle(currentMediaItem.requestMetadata.extras)); + } + } else if (currentMediaItem.requestMetadata.searchQuery != null) { + if (controllerInfo.playerInfo.playWhenReady) { + controllerCompat + .getTransportControls() + .playFromSearch( + currentMediaItem.requestMetadata.searchQuery, + getOrEmptyBundle(currentMediaItem.requestMetadata.extras)); + } else { + controllerCompat + .getTransportControls() + .prepareFromSearch( + currentMediaItem.requestMetadata.searchQuery, + getOrEmptyBundle(currentMediaItem.requestMetadata.extras)); + } + } else { + if (controllerInfo.playerInfo.playWhenReady) { + controllerCompat + .getTransportControls() + .playFromMediaId( + currentMediaItem.mediaId, + getOrEmptyBundle(currentMediaItem.requestMetadata.extras)); + } else { + controllerCompat + .getTransportControls() + .prepareFromMediaId( + currentMediaItem.mediaId, + getOrEmptyBundle(currentMediaItem.requestMetadata.extras)); + } + } + // Seek to non-zero start positon if needed. + if (controllerInfo.playerInfo.sessionPositionInfo.positionInfo.positionMs != 0) { + controllerCompat + .getTransportControls() + .seekTo(controllerInfo.playerInfo.sessionPositionInfo.positionInfo.positionMs); + } + // Add all other items to the playlist if supported. + if (getAvailableCommands().contains(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { + for (int i = 0; i < queueTimeline.getWindowCount(); i++) { + if (i == currentIndex || queueTimeline.getQueueId(i) != QueueItem.UNKNOWN_ID) { + // Skip the current item (added above) and all items already known to the session. + continue; + } + MediaItem mediaItem = queueTimeline.getWindow(/* windowIndex= */ i, window).mediaItem; + controllerCompat.addQueueItem( + MediaUtils.convertToMediaDescriptionCompat(mediaItem), /* index= */ i); + } + } + } + private void handleNewLegacyParameters( boolean notifyConnected, LegacyPlayerInfo newLegacyPlayerInfo) { if (released || !connected) { @@ -1938,6 +1993,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return state; } + private static Bundle getOrEmptyBundle(@Nullable Bundle bundle) { + return bundle == null ? Bundle.EMPTY : bundle; + } + private static long getActiveQueueId(@Nullable PlaybackStateCompat playbackStateCompat) { return playbackStateCompat == null ? QueueItem.UNKNOWN_ID @@ -2088,22 +2147,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* contentBufferedPositionMs= */ bufferedPositionMs); } - private static final class SetMediaUriRequest { - - public final String type; - public final String value; - public final Bundle extras; - public final SettableFuture result; - - public SetMediaUriRequest( - String type, String value, Bundle extras, SettableFuture result) { - this.type = type; - this.value = value; - this.extras = extras; - this.result = result; - } - } - // Media 1.0 variables private static final class LegacyPlayerInfo { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 194f074264..71edff1179 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -88,7 +88,7 @@ import java.util.List; *

  • Threading Model *
  • Media Key Events Mapping *
  • Supporting Multiple Sessions - *
  • Backward Compatibility with Legacy Session APIs + *
  • Backward Compatibility with Legacy Session APIs *
  • Backward Compatibility with Legacy Controller APIs * * @@ -201,10 +201,10 @@ import java.util.List; * *

    Backward Compatibility with Legacy Controller APIs

    * - *

    In addition to {@link MediaController}, session also supports connection from the legacy - * controller APIs - {@link android.media.session.MediaController framework controller} and {@link - * MediaControllerCompat AndroidX controller compat}. However, {@link ControllerInfo} may not be - * precise for legacy controllers. See {@link ControllerInfo} for the details. + *

    In addition to {@link MediaController}, the session also supports connections from the legacy + * controller APIs - {@linkplain android.media.session.MediaController framework controller} and + * {@linkplain MediaControllerCompat AndroidX controller compat}. However, {@link ControllerInfo} + * may not be precise for legacy controllers. See {@link ControllerInfo} for the details. * *

    Unknown package name nor UID doesn't mean that you should disallow connection nor commands. * For SDK levels where such issues happen, session tokens could only be obtained by trusted diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index f3459fccca..d85bc1194b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -30,6 +30,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME; +import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH; @@ -1058,7 +1059,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; COMMAND_SEEK_TO_NEXT, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, COMMAND_GET_MEDIA_ITEMS_METADATA, - COMMAND_GET_CURRENT_MEDIA_ITEM); + COMMAND_GET_CURRENT_MEDIA_ITEM, + COMMAND_SET_MEDIA_ITEM); boolean includePlaylistCommands = (sessionFlags & FLAG_HANDLES_QUEUE_COMMANDS) != 0; if (includePlaylistCommands) { playerCommandsBuilder.add(COMMAND_CHANGE_MEDIA_ITEMS); diff --git a/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java b/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java index be92deea32..adaf65d707 100644 --- a/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java +++ b/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java @@ -79,11 +79,11 @@ import java.util.Map; newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem); } - public QueueTimeline copyWithNewMediaItems(int addToIndex, List newMediaItems) { + public QueueTimeline copyWithNewMediaItems(int index, List newMediaItems) { ImmutableList.Builder newMediaItemsBuilder = new ImmutableList.Builder<>(); - newMediaItemsBuilder.addAll(mediaItems.subList(0, addToIndex)); + newMediaItemsBuilder.addAll(mediaItems.subList(0, index)); newMediaItemsBuilder.addAll(newMediaItems); - newMediaItemsBuilder.addAll(mediaItems.subList(addToIndex, mediaItems.size())); + newMediaItemsBuilder.addAll(mediaItems.subList(index, mediaItems.size())); return new QueueTimeline( newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java index 882dc54dff..1ea60474e6 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java @@ -29,7 +29,6 @@ import static androidx.media3.common.Player.STATE_BUFFERING; import static androidx.media3.common.Player.STATE_READY; import static androidx.media3.session.MediaConstants.ARGUMENT_CAPTIONING_ENABLED; import static androidx.media3.session.MediaConstants.SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED; -import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static androidx.media3.test.session.common.CommonConstants.DEFAULT_TEST_NAME; import static androidx.media3.test.session.common.CommonConstants.METADATA_ALBUM_TITLE; @@ -37,10 +36,8 @@ import static androidx.media3.test.session.common.CommonConstants.METADATA_ARTIS import static androidx.media3.test.session.common.CommonConstants.METADATA_DESCRIPTION; import static androidx.media3.test.session.common.CommonConstants.METADATA_TITLE; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; -import static androidx.media3.test.session.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.app.PendingIntent; @@ -85,8 +82,6 @@ import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -567,77 +562,6 @@ public class MediaControllerWithMediaSessionCompatTest { assertThat(isPlayingAdRef.get()).isTrue(); } - @Test - public void setMediaUri_resultSetAfterPrepare() throws Exception { - MediaController controller = controllerTestRule.createController(session.getSessionToken()); - - Uri testUri = Uri.parse("androidx://test"); - ListenableFuture future = - threadTestRule - .getHandler() - .postAndSync(() -> controller.setMediaUri(testUri, /* extras= */ Bundle.EMPTY)); - - SessionResult result; - try { - result = future.get(NO_RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertWithMessage("TimeoutException is expected").fail(); - } catch (TimeoutException e) { - // expected. - } - - threadTestRule.getHandler().postAndSync(controller::prepare); - - result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); - } - - @Test - public void setMediaUri_resultSetAfterPlay() throws Exception { - MediaController controller = controllerTestRule.createController(session.getSessionToken()); - - Uri testUri = Uri.parse("androidx://test"); - ListenableFuture future = - threadTestRule - .getHandler() - .postAndSync(() -> controller.setMediaUri(testUri, /* extras= */ Bundle.EMPTY)); - - SessionResult result; - try { - result = future.get(NO_RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertWithMessage("TimeoutException is expected").fail(); - } catch (TimeoutException e) { - // expected. - } - - threadTestRule.getHandler().postAndSync(controller::play); - - result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); - } - - @Test - public void setMediaUris_multipleCalls_previousCallReturnsResultInfoSkipped() throws Exception { - MediaController controller = controllerTestRule.createController(session.getSessionToken()); - - Uri testUri1 = Uri.parse("androidx://test1"); - Uri testUri2 = Uri.parse("androidx://test2"); - ListenableFuture future1 = - threadTestRule - .getHandler() - .postAndSync(() -> controller.setMediaUri(testUri1, /* extras= */ Bundle.EMPTY)); - ListenableFuture future2 = - threadTestRule - .getHandler() - .postAndSync(() -> controller.setMediaUri(testUri2, /* extras= */ Bundle.EMPTY)); - - threadTestRule.getHandler().postAndSync(controller::prepare); - - SessionResult result1 = future1.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - SessionResult result2 = future2.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertThat(result1.resultCode).isEqualTo(RESULT_INFO_SKIPPED); - assertThat(result2.resultCode).isEqualTo(RESULT_SUCCESS); - } - @Test public void seekToDefaultPosition_withMediaItemIndex_updatesExpectedMediaItemIndex() throws Exception { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java index 57996844a2..ca25291cf4 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java @@ -16,13 +16,6 @@ package androidx.media3.session; import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; -import static androidx.media3.session.MediaConstants.MEDIA_URI_AUTHORITY; -import static androidx.media3.session.MediaConstants.MEDIA_URI_PATH_SET_MEDIA_URI; -import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_ID; -import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_QUERY; -import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_URI; -import static androidx.media3.session.MediaConstants.MEDIA_URI_SCHEME; -import static androidx.media3.test.session.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.VOLUME_CHANGE_TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -78,6 +71,9 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { // The maximum time to wait for an operation. private static final long TIMEOUT_MS = 3000L; + // Timeout used where the test expects no operation. + private static final long NOOP_TIMEOUT_MS = 500L; + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); @Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); @@ -122,6 +118,11 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { @Test public void play() throws Exception { + List testList = MediaTestUtils.createMediaItems(/* size= */ 2); + List testQueue = MediaUtils.convertToQueueItemList(testList); + session.setQueue(testQueue); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackState(PlaybackStateCompat.STATE_PAUSED); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(1); @@ -132,6 +133,11 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { @Test public void pause() throws Exception { + List testList = MediaTestUtils.createMediaItems(/* size= */ 2); + List testQueue = MediaUtils.convertToQueueItemList(testList); + session.setQueue(testQueue); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackState(PlaybackStateCompat.STATE_PLAYING); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(1); @@ -142,20 +148,31 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { @Test public void prepare() throws Exception { + List testList = MediaTestUtils.createMediaItems(/* size= */ 2); + List testQueue = MediaUtils.convertToQueueItemList(testList); + session.setQueue(testQueue); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(1); controller.prepare(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); assertThat(sessionCallback.onPrepareCalled).isEqualTo(true); } @Test public void stop() throws Exception { + List testList = MediaTestUtils.createMediaItems(/* size= */ 2); + List testQueue = MediaUtils.convertToQueueItemList(testList); + session.setQueue(testQueue); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); RemoteMediaController controller = createControllerAndWaitConnection(); + controller.prepare(); sessionCallback.reset(1); controller.stop(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); assertThat(sessionCallback.onStopCalled).isEqualTo(true); } @@ -314,6 +331,7 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackState(PlaybackStateCompat.STATE_PLAYING); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(size); @@ -338,6 +356,7 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { session.setQueue(MediaUtils.convertToQueueItemList(testList)); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackState(PlaybackStateCompat.STATE_BUFFERING); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(count); @@ -634,269 +653,6 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { assertThat(MediaUtils.convertToRating(sessionCallback.rating)).isEqualTo(rating); } - @Test - public void setMediaUri_ignored() throws Exception { - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri(Uri.parse("androidx://test?test=xx"), /* extras= */ Bundle.EMPTY); - - assertThat(sessionCallback.await(NO_RESPONSE_TIMEOUT_MS)).isFalse(); - } - - @Test - public void setMediaUri_followedByPrepare_callsPrepareFromMediaId() throws Exception { - String testMediaId = "anyMediaId"; - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri( - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .appendQueryParameter(MEDIA_URI_QUERY_ID, testMediaId) - .build(), - testExtras); - controller.prepare(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPrepareFromMediaIdCalled).isTrue(); - assertThat(sessionCallback.mediaId).isEqualTo(testMediaId); - assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue(); - assertThat(sessionCallback.query).isNull(); - assertThat(sessionCallback.uri).isNull(); - assertThat(sessionCallback.onPrepareCalled).isFalse(); - } - - @Test - public void setMediaUri_followedByPrepare_callsPrepareFromSearch() throws Exception { - String testSearchQuery = "anyQuery"; - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri( - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .appendQueryParameter(MEDIA_URI_QUERY_QUERY, testSearchQuery) - .build(), - testExtras); - controller.prepare(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPrepareFromSearchCalled).isTrue(); - assertThat(sessionCallback.query).isEqualTo(testSearchQuery); - assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue(); - assertThat(sessionCallback.mediaId).isNull(); - assertThat(sessionCallback.uri).isNull(); - assertThat(sessionCallback.onPrepareCalled).isFalse(); - } - - @Test - public void setMediaUri_followedByPrepare_callsPrepareFromUri() throws Exception { - Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media"); - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri( - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .appendQueryParameter(MEDIA_URI_QUERY_URI, testMediaUri.toString()) - .build(), - testExtras); - controller.prepare(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPrepareFromUriCalled).isTrue(); - assertThat(sessionCallback.uri).isEqualTo(testMediaUri); - assertThat(sessionCallback.mediaId).isNull(); - assertThat(sessionCallback.query).isNull(); - assertThat(sessionCallback.onPrepareCalled).isFalse(); - } - - @Test - public void setMediaUri_withoutFormattingFollowedByPrepare_callsPrepareFromUri() - throws Exception { - Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media"); - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri(testMediaUri, testExtras); - controller.prepare(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPrepareFromUriCalled).isTrue(); - assertThat(sessionCallback.uri).isEqualTo(testMediaUri); - assertThat(sessionCallback.mediaId).isNull(); - assertThat(sessionCallback.query).isNull(); - assertThat(sessionCallback.onPrepareCalled).isFalse(); - } - - @Test - public void setMediaUri_followedByPlay_callsPlayFromMediaId() throws Exception { - String testMediaId = "anyMediaId"; - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri( - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .appendQueryParameter(MEDIA_URI_QUERY_ID, testMediaId) - .build(), - testExtras); - controller.play(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue(); - assertThat(sessionCallback.mediaId).isEqualTo(testMediaId); - assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue(); - assertThat(sessionCallback.query).isNull(); - assertThat(sessionCallback.uri).isNull(); - assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0); - } - - @Test - public void setMediaUri_followedByPlay_callsPlayFromSearch() throws Exception { - String testSearchQuery = "anyQuery"; - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri( - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .appendQueryParameter(MEDIA_URI_QUERY_QUERY, testSearchQuery) - .build(), - testExtras); - controller.play(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPlayFromSearchCalled).isTrue(); - assertThat(sessionCallback.query).isEqualTo(testSearchQuery); - assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue(); - assertThat(sessionCallback.mediaId).isNull(); - assertThat(sessionCallback.uri).isNull(); - assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0); - } - - @Test - public void setMediaUri_followedByPlay_callsPlayFromUri() throws Exception { - Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media"); - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri( - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .appendQueryParameter(MEDIA_URI_QUERY_URI, testMediaUri.toString()) - .build(), - testExtras); - controller.play(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPlayFromUriCalled).isTrue(); - assertThat(sessionCallback.uri).isEqualTo(testMediaUri); - assertThat(sessionCallback.mediaId).isNull(); - assertThat(sessionCallback.query).isNull(); - assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0); - } - - @Test - public void setMediaUri_withoutFormattingFollowedByPlay_callsPlayFromUri() throws Exception { - Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media"); - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri(testMediaUri, testExtras); - controller.play(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPlayFromUriCalled).isTrue(); - assertThat(sessionCallback.uri).isEqualTo(testMediaUri); - assertThat(sessionCallback.mediaId).isNull(); - assertThat(sessionCallback.query).isNull(); - assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0); - } - - @Test - public void setMediaUri_followedByPrepareTwice_callsPrepareFromUriAndPrepare() throws Exception { - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(2); - - controller.setMediaUri(Uri.parse("androidx://test"), /* extras= */ Bundle.EMPTY); - - controller.prepare(); - controller.prepare(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPrepareFromUriCalled).isTrue(); - assertThat(sessionCallback.onPrepareCalled).isTrue(); - } - - @Test - public void setMediaUri_followedByPlayTwice_callsPlayFromUriAndPlay() throws Exception { - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(2); - - controller.setMediaUri(Uri.parse("androidx://test"), /* extras= */ Bundle.EMPTY); - - controller.play(); - controller.play(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPlayFromUriCalled).isTrue(); - assertThat(sessionCallback.onPlayCalledCount).isEqualTo(1); - } - - @Test - public void setMediaUri_multipleCalls_skipped() throws Exception { - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(2); - - Uri testUri1 = Uri.parse("androidx://test1"); - Uri testUri2 = Uri.parse("androidx://test2"); - controller.setMediaUri(testUri1, /* extras= */ Bundle.EMPTY); - controller.setMediaUri(testUri2, /* extras= */ Bundle.EMPTY); - controller.prepare(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isFalse(); - assertThat(sessionCallback.onPrepareFromUriCalled).isTrue(); - assertThat(sessionCallback.uri).isEqualTo(testUri2); - } - @Test public void seekToNext_callsOnSkipToNext() throws Exception { RemoteMediaController controller = createControllerAndWaitConnection(); From 67915327786093d6b6830d5efccb4da3376f20b7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 15 Jun 2022 13:32:25 +0000 Subject: [PATCH 36/45] Remove setMediaUri/onSetMediaUri This code path is now completely redundant as the same effect can be achieved by using player.setMediaItem. PiperOrigin-RevId: 455115567 (cherry picked from commit 21d4e8581701e12743626f49823a667d9f05ed68) --- RELEASENOTES.md | 4 + .../media3/session/IMediaSession.aidl | 1 - .../media3/session/MediaConstants.java | 94 ------------------- .../media3/session/MediaController.java | 33 ------- .../session/MediaControllerImplBase.java | 10 -- .../session/MediaControllerImplLegacy.java | 7 -- .../androidx/media3/session/MediaSession.java | 18 ---- .../media3/session/MediaSessionImpl.java | 5 - .../media3/session/MediaSessionStub.java | 17 ---- .../media3/session/SessionCommand.java | 6 +- .../common/IRemoteMediaController.aidl | 1 - .../session/MediaControllerListenerTest.java | 4 +- .../media3/session/MediaControllerTest.java | 4 +- .../session/MediaSessionCallbackTest.java | 38 -------- .../session/MediaSessionPermissionTest.java | 50 +--------- .../MediaControllerProviderService.java | 11 --- .../media3/session/RemoteMediaController.java | 5 - 17 files changed, 12 insertions(+), 296 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 959fdba3e1..cc4fc72c3e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -181,6 +181,10 @@ `MediaSession.Callback.onAddMediaItems` instead of `onSetMediaUri`. * Support `setMediaItems(s)` methods when `MediaController` connects to a legacy media session. + * Remove `MediaController.setMediaUri` and + `MediaSession.Callback.onSetMediaUri`. The same functionality can be + achieved by using `MediaController.setMediaItem` and + `MediaSession.Callback.onAddMediaItems`. * Data sources: * Rename `DummyDataSource` to `PlaceholderDataSource`. * Workaround OkHttp interrupt handling. diff --git a/libraries/session/src/main/aidl/androidx/media3/session/IMediaSession.aidl b/libraries/session/src/main/aidl/androidx/media3/session/IMediaSession.aidl index ac88efa63a..e62fa1bc7e 100644 --- a/libraries/session/src/main/aidl/androidx/media3/session/IMediaSession.aidl +++ b/libraries/session/src/main/aidl/androidx/media3/session/IMediaSession.aidl @@ -32,7 +32,6 @@ oneway interface IMediaSession { // Id < 3000 is reserved to avoid potential collision with media2 1.x. - void setMediaUri(IMediaController caller, int seq, in Uri uri, in Bundle extras) = 3000; void setVolume(IMediaController caller, int seq, float volume) = 3001; void setDeviceVolume(IMediaController caller, int seq, int volume) = 3002; void increaseDeviceVolume(IMediaController caller, int seq) = 3003; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java index be791c9cc5..ce5e4142d0 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java @@ -15,7 +15,6 @@ */ package androidx.media3.session; -import android.net.Uri; import android.os.Bundle; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; @@ -25,99 +24,6 @@ import androidx.media3.session.MediaLibraryService.LibraryParams; /** Constants that can be shared between media session and controller. */ public final class MediaConstants { - /** - * A {@link Uri} scheme used in a media uri. - * - * @see MediaController#setMediaUri - * @see MediaSession.Callback#onSetMediaUri - */ - public static final String MEDIA_URI_SCHEME = "androidx"; - - /** - * A {@link Uri} authority used in a media uri. - * - * @see MediaController#setMediaUri - * @see MediaSession.Callback#onSetMediaUri - */ - public static final String MEDIA_URI_AUTHORITY = "media3-session"; - - /** - * A {@link Uri} path used by {@code - * android.support.v4.media.session.MediaControllerCompat.TransportControls#playFromMediaId}. - * - * @see MediaController#setMediaUri - * @see MediaSession.Callback#onSetMediaUri - */ - public static final String MEDIA_URI_PATH_PLAY_FROM_MEDIA_ID = "playFromMediaId"; - - /** - * A {@link Uri} path used by {@code - * android.support.v4.media.session.MediaControllerCompat.TransportControls#playFromSearch}. - * - * @see MediaController#setMediaUri - * @see MediaSession.Callback#onSetMediaUri - */ - public static final String MEDIA_URI_PATH_PLAY_FROM_SEARCH = "playFromSearch"; - - /** - * A {@link Uri} path used by {@link - * android.support.v4.media.session.MediaControllerCompat.TransportControls#prepareFromMediaId}. - * - * @see MediaController#setMediaUri - * @see MediaSession.Callback#onSetMediaUri - */ - public static final String MEDIA_URI_PATH_PREPARE_FROM_MEDIA_ID = "prepareFromMediaId"; - - /** - * A {@link Uri} path used by {@link - * android.support.v4.media.session.MediaControllerCompat.TransportControls#prepareFromSearch}. - * - * @see MediaController#setMediaUri - * @see MediaSession.Callback#onSetMediaUri - */ - public static final String MEDIA_URI_PATH_PREPARE_FROM_SEARCH = "prepareFromSearch"; - - /** - * A {@link Uri} path for encoding how the uri will be translated when connected to {@link - * android.support.v4.media.session.MediaSessionCompat}. - * - * @see MediaController#setMediaUri - */ - public static final String MEDIA_URI_PATH_SET_MEDIA_URI = "setMediaUri"; - - // From scheme to path, plus path delimiter - /* package */ static final String MEDIA_URI_SET_MEDIA_URI_PREFIX = - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .build() - .toString() - + "?"; - - /** - * A {@link Uri} query for media id. - * - * @see MediaSession.Callback#onSetMediaUri - * @see MediaController#setMediaUri - */ - public static final String MEDIA_URI_QUERY_ID = "id"; - - /** - * A {@link Uri} query for search query. - * - * @see MediaSession.Callback#onSetMediaUri - * @see MediaController#setMediaUri - */ - public static final String MEDIA_URI_QUERY_QUERY = "query"; - - /** - * A {@link Uri} query for media uri. - * - * @see MediaController#setMediaUri - */ - public static final String MEDIA_URI_QUERY_URI = "uri"; - /** * Bundle key to indicate a preference that a region of space for the skip to next control should * always be blocked out in the UI, even when the seek to next standard action is not supported. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 624a0faa7a..e47fc02253 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -24,7 +24,6 @@ import static androidx.media3.common.util.Util.postOrRun; import android.app.PendingIntent; import android.content.Context; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -985,36 +984,6 @@ public class MediaController implements Player { impl.setMediaItems(mediaItems, startIndex, startPositionMs); } - /** - * Requests that the connected {@link MediaSession} sets a specific {@link Uri} for playback. Use - * this, or {@link #setMediaItems} to specify which item(s) to play. - * - *

    This can be called multiple times in any states. This would override previous call of this, - * or {@link #setMediaItems}. - * - *

    The {@link Player.Listener#onTimelineChanged} and/or {@link - * Player.Listener#onMediaItemTransition} would be called when it's completed. - * - *

    Returned {@link ListenableFuture} will return {@link SessionResult#RESULT_SUCCESS} when it's - * handled together with {@link #prepare} or {@link #play}. If this API is called multiple times - * without prepare or play, then {@link SessionResult#RESULT_INFO_SKIPPED} will be returned for - * previous calls. - * - * @param uri The uri of the item(s) to play. - * @param extras A {@link Bundle} to send extra information. May be empty. - * @return A {@link ListenableFuture} of {@link SessionResult} representing the pending - * completion. - */ - public ListenableFuture setMediaUri(Uri uri, Bundle extras) { - verifyApplicationThread(); - checkNotNull(uri); - checkNotNull(extras); - if (isConnected()) { - return impl.setMediaUri(uri, extras); - } - return createDisconnectedFuture(); - } - @Override public void setPlaylistMetadata(MediaMetadata playlistMetadata) { verifyApplicationThread(); @@ -1928,8 +1897,6 @@ public class MediaController implements Player { void setMediaItems(List mediaItems, int startIndex, long startPositionMs); - ListenableFuture setMediaUri(Uri uri, Bundle extras); - void setPlaylistMetadata(MediaMetadata playlistMetadata); MediaMetadata getPlaylistMetadata(); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index 6eb5406951..c80e832a97 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -76,7 +76,6 @@ import static androidx.media3.common.util.Util.usToMs; import static androidx.media3.session.MediaUtils.calculateBufferedPercentage; import static androidx.media3.session.MediaUtils.intersect; import static androidx.media3.session.SessionCommand.COMMAND_CODE_CUSTOM; -import static androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI; import static androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_RATING; import static androidx.media3.session.SessionResult.RESULT_ERROR_PERMISSION_DENIED; import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED; @@ -93,7 +92,6 @@ import android.content.Intent; import android.content.ServiceConnection; import android.graphics.Rect; import android.graphics.SurfaceTexture; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -934,14 +932,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; mediaItems, startIndex, startPositionMs, /* resetToDefaultPosition= */ false); } - @Override - public ListenableFuture setMediaUri(Uri uri, Bundle extras) { - return dispatchRemoteSessionTaskWithSessionCommand( - COMMAND_CODE_SESSION_SET_MEDIA_URI, - (RemoteSessionTask) - (iSession, seq) -> iSession.setMediaUri(controllerStub, seq, uri, extras)); - } - @Override public void setPlaylistMetadata(MediaMetadata playlistMetadata) { if (!isPlayerCommandAvailable(COMMAND_SET_MEDIA_ITEMS_METADATA)) { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index 05fb4335cf..6a2dc56c1f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -44,7 +44,6 @@ import static java.lang.Math.min; import android.app.PendingIntent; import android.content.Context; import android.media.AudioManager; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.ResultReceiver; @@ -667,12 +666,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } - @Override - public ListenableFuture setMediaUri(Uri uri, Bundle extras) { - Log.w(TAG, "Session doesn't support setMediaUri"); - return Futures.immediateCancelledFuture(); - } - @Override public void setPlaylistMetadata(MediaMetadata playlistMetadata) { Log.w(TAG, "Session doesn't support setting playlist metadata"); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 71edff1179..d170298328 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -940,24 +940,6 @@ public class MediaSession { return Futures.immediateFuture(new SessionResult(RESULT_ERROR_NOT_SUPPORTED)); } - /** - * Called when a controller requested to set the specific media item(s) represented by a URI - * through {@link MediaController#setMediaUri(Uri, Bundle)}. - * - *

    The implementation should create proper {@link MediaItem media item(s)} for the given - * {@code uri} and call {@link Player#setMediaItems}. - * - * @param session The session for this event. - * @param controller The controller information. - * @param uri The uri. - * @param extras An extra {@link Bundle}. May be empty. - * @return A result code. - */ - default @SessionResult.Code int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - return RESULT_ERROR_NOT_SUPPORTED; - } - /** * Called when a controller sent a custom command through {@link * MediaController#sendCustomCommand(SessionCommand, Bundle)}. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 57d1533c9a..6fce21470f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -442,11 +442,6 @@ import org.checkerframework.checker.initialization.qual.Initialized; callback.onDisconnected(instance, controller); } - public @SessionResult.Code int onSetMediaUriOnHandler( - ControllerInfo controller, Uri uri, Bundle extras) { - return callback.onSetMediaUri(instance, controller, uri, extras); - } - public @SessionResult.Code int onPlayerCommandRequestOnHandler( ControllerInfo controller, @Player.Command int playerCommand) { return callback.onPlayerCommandRequest(instance, controller, playerCommand); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 109d6afc1d..1b5e554298 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -49,10 +49,8 @@ import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_SE import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SEARCH; import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE; import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE; -import static androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI; import static androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_RATING; -import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; @@ -1013,21 +1011,6 @@ import java.util.concurrent.ExecutionException; player.setMediaItems(mediaItems, startIndex, startPositionMs))); } - @Override - public void setMediaUri( - @Nullable IMediaController caller, int seq, @Nullable Uri uri, @Nullable Bundle extras) { - if (caller == null || uri == null || extras == null) { - return; - } - dispatchSessionTaskWithSessionCommand( - caller, - seq, - COMMAND_CODE_SESSION_SET_MEDIA_URI, - (sessionImpl, controller) -> - new SessionResult(sessionImpl.onSetMediaUriOnHandler(controller, uri, extras)), - MediaSessionStub::sendSessionResult); - } - @Override public void setPlaylistMetadata( @Nullable IMediaController caller, int seq, @Nullable Bundle playlistMetadataBundle) { diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionCommand.java b/libraries/session/src/main/java/androidx/media3/session/SessionCommand.java index 256a92c3ff..35ef391826 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionCommand.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionCommand.java @@ -49,7 +49,6 @@ public final class SessionCommand implements Bundleable { @Target(TYPE_USE) @IntDef({ COMMAND_CODE_CUSTOM, - COMMAND_CODE_SESSION_SET_MEDIA_URI, COMMAND_CODE_SESSION_SET_RATING, COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT, COMMAND_CODE_LIBRARY_SUBSCRIBE, @@ -74,11 +73,8 @@ public final class SessionCommand implements Bundleable { /** Command code for {@link MediaController#setRating(String, Rating)}. */ public static final int COMMAND_CODE_SESSION_SET_RATING = 40010; - /** Command code for {@link MediaController#setMediaUri}. */ - public static final int COMMAND_CODE_SESSION_SET_MEDIA_URI = 40011; - /* package */ static final ImmutableList SESSION_COMMANDS = - ImmutableList.of(COMMAND_CODE_SESSION_SET_RATING, COMMAND_CODE_SESSION_SET_MEDIA_URI); + ImmutableList.of(COMMAND_CODE_SESSION_SET_RATING); ////////////////////////////////////////////////////////////////////////////////////////////////// // Library commands (i.e. commands to {@link MediaLibrarySession#MediaLibrarySessionCallback}) diff --git a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl index cf4e6ebf6c..0f611d7be5 100644 --- a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl +++ b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl @@ -52,7 +52,6 @@ interface IRemoteMediaController { void setMediaItemsWithStartIndex( String controllerId, in List mediaItems, int startIndex, long startPositionMs); void createAndSetFakeMediaItems(String controllerId, int size); - void setMediaUri(String controllerId, in Uri uri, in Bundle extras); void setPlaylistMetadata(String controllerId, in Bundle playlistMetadata); void addMediaItem(String controllerId, in Bundle mediaitem); void addMediaItemWithIndex(String controllerId, int index, in Bundle mediaitem); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java index d4a597f410..2cfaf781b6 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java @@ -24,6 +24,7 @@ import static androidx.media3.common.Player.STATE_BUFFERING; import static androidx.media3.session.MediaTestUtils.createTimeline; import static androidx.media3.session.MediaUtils.createPlayerCommandsWith; import static androidx.media3.session.MediaUtils.createPlayerCommandsWithout; +import static androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_RATING; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static androidx.media3.test.session.common.CommonConstants.DEFAULT_TEST_NAME; import static androidx.media3.test.session.common.CommonConstants.MOCK_MEDIA3_LIBRARY_SERVICE; @@ -1535,7 +1536,8 @@ public class MediaControllerListenerTest { SessionCommands commands = new SessionCommands.Builder() - .add(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING)) + .addAllSessionCommands() + .remove(COMMAND_CODE_SESSION_SET_RATING) .build(); remoteSession.setAvailableCommands(commands, Player.Commands.EMPTY); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java index f24070f94f..80b893e9d3 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java @@ -943,7 +943,7 @@ public class MediaControllerTest { public void isSessionCommandAvailable_withAvailablePredefinedSessionCommand_returnsTrue() throws Exception { @SessionCommand.CommandCode - int sessionCommandCode = SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI; + int sessionCommandCode = SessionCommand.COMMAND_CODE_SESSION_SET_RATING; SessionCommand sessionCommand = new SessionCommand(sessionCommandCode); Bundle tokenExtras = new Bundle(); tokenExtras.putBundle( @@ -969,7 +969,7 @@ public class MediaControllerTest { public void isSessionCommandAvailable_withUnavailablePredefinedSessionCommand_returnsFalse() throws Exception { @SessionCommand.CommandCode - int sessionCommandCode = SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI; + int sessionCommandCode = SessionCommand.COMMAND_CODE_SESSION_SET_RATING; SessionCommand sessionCommand = new SessionCommand(sessionCommandCode); Bundle tokenExtras = new Bundle(); tokenExtras.putBundle( diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java index b079208c35..d145758606 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java @@ -27,7 +27,6 @@ import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; -import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import androidx.media3.common.MediaItem; @@ -239,43 +238,6 @@ public class MediaSessionCallbackTest { assertThat(TestUtils.equals(testArgs, argsRef.get())).isTrue(); } - @Test - public void onSetMediaUri() throws Exception { - Uri testUri = Uri.parse("foo://boo"); - Bundle testExtras = TestUtils.createTestBundle(); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference uriRef = new AtomicReference<>(); - AtomicReference extrasRef = new AtomicReference<>(); - MediaSession.Callback callback = - new MediaSession.Callback() { - @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - if (!TextUtils.equals(controller.getPackageName(), SUPPORT_APP_PACKAGE_NAME)) { - return RESULT_INFO_SKIPPED; - } - - uriRef.set(uri); - extrasRef.set(extras); - latch.countDown(); - return RESULT_SUCCESS; - } - }; - MediaSession session = - sessionTestRule.ensureReleaseAfterTest( - new MediaSession.Builder(context, player) - .setCallback(callback) - .setId("testOnSetMediaUri") - .build()); - RemoteMediaController controller = - controllerTestRule.createRemoteController(session.getToken()); - - controller.setMediaUri(testUri, testExtras); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(uriRef.get()).isEqualTo(testUri); - assertThat(TestUtils.equals(testExtras, extrasRef.get())).isTrue(); - } - @Test public void onSetRatingWithMediaId() throws Exception { float ratingValue = 3.5f; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java index f00a739c91..645fc885c9 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPermissionTest.java @@ -27,7 +27,6 @@ import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA; import static androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS; import static androidx.media3.session.MediaUtils.createPlayerCommandsWith; import static androidx.media3.session.MediaUtils.createPlayerCommandsWithout; -import static androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI; import static androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_RATING; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; @@ -37,7 +36,6 @@ import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; -import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import androidx.media3.common.MediaMetadata; @@ -50,7 +48,6 @@ import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.MainLooperTestRule; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.ext.truth.os.BundleSubject; import androidx.test.filters.LargeTest; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -184,31 +181,6 @@ public class MediaSessionPermissionTest { testOnCommandRequest(COMMAND_SET_DEVICE_VOLUME, controller -> controller.setDeviceMuted(true)); } - @Test - public void setMediaUri() throws Exception { - Uri uri = Uri.parse("media://uri"); - createSessionWithAvailableCommands( - createSessionCommandsWith(new SessionCommand(COMMAND_CODE_SESSION_SET_MEDIA_URI)), - Player.Commands.EMPTY); - controllerTestRule - .createRemoteController(session.getToken()) - .setMediaUri(uri, /* extras= */ Bundle.EMPTY); - - assertThat(callback.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(callback.onSetMediaUriCalled).isTrue(); - assertThat(callback.uri).isEqualTo(uri); - BundleSubject.assertThat(callback.extras).isEmpty(); - - createSessionWithAvailableCommands( - createSessionCommandsWith(new SessionCommand(COMMAND_CODE_SESSION_SET_RATING)), - Player.Commands.EMPTY); - controllerTestRule - .createRemoteController(session.getToken()) - .setMediaUri(uri, /* extras= */ Bundle.EMPTY); - assertThat(callback.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); - assertThat(callback.onSetMediaUriCalled).isFalse(); - } - @Test public void setRating() throws Exception { String mediaId = "testSetRating"; @@ -223,9 +195,7 @@ public class MediaSessionPermissionTest { assertThat(callback.mediaId).isEqualTo(mediaId); assertThat(callback.rating).isEqualTo(rating); - createSessionWithAvailableCommands( - createSessionCommandsWith(new SessionCommand(COMMAND_CODE_SESSION_SET_MEDIA_URI)), - Player.Commands.EMPTY); + createSession(SessionCommands.EMPTY, Player.Commands.EMPTY); controllerTestRule.createRemoteController(session.getToken()).setRating(mediaId, rating); assertThat(callback.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); assertThat(callback.onSetRatingCalled).isFalse(); @@ -248,9 +218,7 @@ public class MediaSessionPermissionTest { // Change allowed commands. session.setAvailableCommands( - getTestControllerInfo(), - createSessionCommandsWith(new SessionCommand(COMMAND_CODE_SESSION_SET_MEDIA_URI)), - Player.Commands.EMPTY); + getTestControllerInfo(), SessionCommands.EMPTY, Player.Commands.EMPTY); controller.setRating(mediaId, rating); assertThat(callback.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); @@ -315,12 +283,10 @@ public class MediaSessionPermissionTest { public @Player.Command int command; public String mediaId; - public Uri uri; public Bundle extras; public Rating rating; public boolean onCommandRequestCalled; - public boolean onSetMediaUriCalled; public boolean onSetRatingCalled; public MySessionCallback() { @@ -333,7 +299,6 @@ public class MediaSessionPermissionTest { mediaId = null; onCommandRequestCalled = false; - onSetMediaUriCalled = false; onSetRatingCalled = false; } @@ -347,17 +312,6 @@ public class MediaSessionPermissionTest { return MediaSession.Callback.super.onPlayerCommandRequest(session, controller, command); } - @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controller.getPackageName())).isTrue(); - onSetMediaUriCalled = true; - this.uri = uri; - this.extras = extras; - countDownLatch.countDown(); - return RESULT_SUCCESS; - } - @Override public ListenableFuture onSetRating( MediaSession session, ControllerInfo controller, String mediaId, Rating rating) { diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java index 2912517f69..7d263e9c71 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java @@ -23,7 +23,6 @@ import android.app.Service; import android.content.Context; import android.content.Intent; import android.media.AudioManager; -import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; @@ -366,16 +365,6 @@ public class MediaControllerProviderService extends Service { }); } - @Override - @SuppressWarnings("FutureReturnValueIgnored") - public void setMediaUri(String controllerId, Uri uri, Bundle extras) throws RemoteException { - runOnHandler( - () -> { - MediaController controller = mediaControllerMap.get(controllerId); - controller.setMediaUri(uri, extras); - }); - } - @Override public void setPlaylistMetadata(String controllerId, Bundle playlistMetadataBundle) throws RemoteException { diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java index 4c34fded93..6b0449ce25 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java @@ -25,7 +25,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; @@ -180,10 +179,6 @@ public class RemoteMediaController { binder.createAndSetFakeMediaItems(controllerId, size); } - public void setMediaUri(Uri uri, Bundle extras) throws RemoteException { - binder.setMediaUri(controllerId, uri, extras); - } - public void setPlaylistMetadata(MediaMetadata playlistMetadata) throws RemoteException { binder.setPlaylistMetadata(controllerId, playlistMetadata.toBundle()); } From 5ebc07ce76049fc3b3ba40ad924169385e89bc71 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 15 Jun 2022 14:48:34 +0000 Subject: [PATCH 37/45] Updating translations #minor-release PiperOrigin-RevId: 455128997 (cherry picked from commit 958105c91c931fa942ce7de4e8311589eca76564) --- .../src/main/res/values-af/strings.xml | 1 + .../src/main/res/values-am/strings.xml | 1 + .../src/main/res/values-ar/strings.xml | 1 + .../src/main/res/values-az/strings.xml | 1 + .../src/main/res/values-b+sr+Latn/strings.xml | 1 + .../src/main/res/values-be/strings.xml | 1 + .../src/main/res/values-bg/strings.xml | 1 + .../src/main/res/values-bn/strings.xml | 1 + .../src/main/res/values-bs/strings.xml | 1 + .../src/main/res/values-ca/strings.xml | 1 + .../src/main/res/values-cs/strings.xml | 1 + .../src/main/res/values-da/strings.xml | 1 + .../src/main/res/values-de/strings.xml | 1 + .../src/main/res/values-el/strings.xml | 1 + .../src/main/res/values-en-rAU/strings.xml | 1 + .../src/main/res/values-en-rGB/strings.xml | 1 + .../src/main/res/values-en-rIN/strings.xml | 1 + .../src/main/res/values-es-rUS/strings.xml | 1 + .../src/main/res/values-es/strings.xml | 1 + .../src/main/res/values-et/strings.xml | 1 + .../src/main/res/values-eu/strings.xml | 1 + .../src/main/res/values-fa/strings.xml | 1 + .../src/main/res/values-fi/strings.xml | 1 + .../src/main/res/values-fr-rCA/strings.xml | 1 + .../src/main/res/values-fr/strings.xml | 1 + .../src/main/res/values-gl/strings.xml | 1 + .../src/main/res/values-gu/strings.xml | 1 + .../src/main/res/values-hi/strings.xml | 1 + .../src/main/res/values-hr/strings.xml | 1 + .../src/main/res/values-hu/strings.xml | 1 + .../src/main/res/values-hy/strings.xml | 1 + .../src/main/res/values-in/strings.xml | 1 + .../src/main/res/values-is/strings.xml | 1 + .../src/main/res/values-it/strings.xml | 1 + .../src/main/res/values-iw/strings.xml | 1 + .../src/main/res/values-ja/strings.xml | 1 + .../src/main/res/values-ka/strings.xml | 1 + .../src/main/res/values-kk/strings.xml | 1 + .../src/main/res/values-km/strings.xml | 1 + .../src/main/res/values-kn/strings.xml | 1 + .../src/main/res/values-ko/strings.xml | 1 + .../src/main/res/values-ky/strings.xml | 1 + .../src/main/res/values-lo/strings.xml | 1 + .../src/main/res/values-lt/strings.xml | 1 + .../src/main/res/values-lv/strings.xml | 1 + .../src/main/res/values-mk/strings.xml | 1 + .../src/main/res/values-ml/strings.xml | 1 + .../src/main/res/values-mn/strings.xml | 1 + .../src/main/res/values-mr/strings.xml | 1 + .../src/main/res/values-ms/strings.xml | 1 + .../src/main/res/values-my/strings.xml | 1 + .../src/main/res/values-nb/strings.xml | 1 + .../src/main/res/values-ne/strings.xml | 1 + .../src/main/res/values-nl/strings.xml | 1 + .../src/main/res/values-pa/strings.xml | 1 + .../src/main/res/values-pl/strings.xml | 1 + .../src/main/res/values-pt-rPT/strings.xml | 1 + .../src/main/res/values-pt/strings.xml | 1 + .../src/main/res/values-ro/strings.xml | 1 + .../src/main/res/values-ru/strings.xml | 1 + .../src/main/res/values-si/strings.xml | 1 + .../src/main/res/values-sk/strings.xml | 1 + .../src/main/res/values-sl/strings.xml | 1 + .../src/main/res/values-sq/strings.xml | 1 + .../src/main/res/values-sr/strings.xml | 1 + .../src/main/res/values-sv/strings.xml | 1 + .../src/main/res/values-sw/strings.xml | 1 + .../src/main/res/values-ta/strings.xml | 1 + .../src/main/res/values-te/strings.xml | 1 + .../src/main/res/values-th/strings.xml | 1 + .../src/main/res/values-tl/strings.xml | 1 + .../src/main/res/values-tr/strings.xml | 1 + .../src/main/res/values-uk/strings.xml | 1 + .../src/main/res/values-ur/strings.xml | 1 + .../src/main/res/values-uz/strings.xml | 1 + .../src/main/res/values-vi/strings.xml | 1 + .../src/main/res/values-zh-rCN/strings.xml | 1 + .../src/main/res/values-zh-rHK/strings.xml | 1 + .../src/main/res/values-zh-rTW/strings.xml | 1 + .../src/main/res/values-zu/strings.xml | 1 + .../ui/src/main/res/values-af/strings.xml | 10 ++++---- .../ui/src/main/res/values-am/strings.xml | 10 ++++---- .../ui/src/main/res/values-ar/strings.xml | 10 ++++---- .../ui/src/main/res/values-az/strings.xml | 10 ++++---- .../src/main/res/values-b+sr+Latn/strings.xml | 10 ++++---- .../ui/src/main/res/values-be/strings.xml | 12 +++++----- .../ui/src/main/res/values-bg/strings.xml | 10 ++++---- .../ui/src/main/res/values-bn/strings.xml | 10 ++++---- .../ui/src/main/res/values-bs/strings.xml | 10 ++++---- .../ui/src/main/res/values-ca/strings.xml | 10 ++++---- .../ui/src/main/res/values-cs/strings.xml | 10 ++++---- .../ui/src/main/res/values-da/strings.xml | 10 ++++---- .../ui/src/main/res/values-de/strings.xml | 10 ++++---- .../ui/src/main/res/values-el/strings.xml | 10 ++++---- .../ui/src/main/res/values-en-rAU/strings.xml | 10 ++++---- .../ui/src/main/res/values-en-rGB/strings.xml | 10 ++++---- .../ui/src/main/res/values-en-rIN/strings.xml | 10 ++++---- .../ui/src/main/res/values-es-rUS/strings.xml | 10 ++++---- .../ui/src/main/res/values-es/strings.xml | 10 ++++---- .../ui/src/main/res/values-et/strings.xml | 10 ++++---- .../ui/src/main/res/values-eu/strings.xml | 10 ++++---- .../ui/src/main/res/values-fa/strings.xml | 10 ++++---- .../ui/src/main/res/values-fi/strings.xml | 10 ++++---- .../ui/src/main/res/values-fr-rCA/strings.xml | 12 ++++------ .../ui/src/main/res/values-fr/strings.xml | 12 ++++------ .../ui/src/main/res/values-gl/strings.xml | 10 ++++---- .../ui/src/main/res/values-gu/strings.xml | 10 ++++---- .../ui/src/main/res/values-hi/strings.xml | 10 ++++---- .../ui/src/main/res/values-hr/strings.xml | 10 ++++---- .../ui/src/main/res/values-hu/strings.xml | 10 ++++---- .../ui/src/main/res/values-hy/strings.xml | 10 ++++---- .../ui/src/main/res/values-in/strings.xml | 10 ++++---- .../ui/src/main/res/values-is/strings.xml | 10 ++++---- .../ui/src/main/res/values-it/strings.xml | 14 +++++------ .../ui/src/main/res/values-iw/strings.xml | 24 +++++++++++++++---- .../ui/src/main/res/values-ja/strings.xml | 10 ++++---- .../ui/src/main/res/values-ka/strings.xml | 10 ++++---- .../ui/src/main/res/values-kk/strings.xml | 10 ++++---- .../ui/src/main/res/values-km/strings.xml | 10 ++++---- .../ui/src/main/res/values-kn/strings.xml | 12 +++++----- .../ui/src/main/res/values-ko/strings.xml | 10 ++++---- .../ui/src/main/res/values-ky/strings.xml | 10 ++++---- .../ui/src/main/res/values-lo/strings.xml | 10 ++++---- .../ui/src/main/res/values-lt/strings.xml | 10 ++++---- .../ui/src/main/res/values-lv/strings.xml | 10 ++++---- .../ui/src/main/res/values-mk/strings.xml | 10 ++++---- .../ui/src/main/res/values-ml/strings.xml | 10 ++++---- .../ui/src/main/res/values-mn/strings.xml | 10 ++++---- .../ui/src/main/res/values-mr/strings.xml | 10 ++++---- .../ui/src/main/res/values-ms/strings.xml | 10 ++++---- .../ui/src/main/res/values-my/strings.xml | 10 ++++---- .../ui/src/main/res/values-nb/strings.xml | 10 ++++---- .../ui/src/main/res/values-ne/strings.xml | 10 ++++---- .../ui/src/main/res/values-nl/strings.xml | 10 ++++---- .../ui/src/main/res/values-pa/strings.xml | 10 ++++---- .../ui/src/main/res/values-pl/strings.xml | 10 ++++---- .../ui/src/main/res/values-pt-rPT/strings.xml | 14 +++++------ .../ui/src/main/res/values-pt/strings.xml | 10 ++++---- .../ui/src/main/res/values-ro/strings.xml | 10 ++++---- .../ui/src/main/res/values-ru/strings.xml | 10 ++++---- .../ui/src/main/res/values-si/strings.xml | 10 ++++---- .../ui/src/main/res/values-sk/strings.xml | 10 ++++---- .../ui/src/main/res/values-sl/strings.xml | 10 ++++---- .../ui/src/main/res/values-sq/strings.xml | 10 ++++---- .../ui/src/main/res/values-sr/strings.xml | 10 ++++---- .../ui/src/main/res/values-sv/strings.xml | 10 ++++---- .../ui/src/main/res/values-sw/strings.xml | 10 ++++---- .../ui/src/main/res/values-ta/strings.xml | 14 +++++------ .../ui/src/main/res/values-te/strings.xml | 10 ++++---- .../ui/src/main/res/values-th/strings.xml | 10 ++++---- .../ui/src/main/res/values-tl/strings.xml | 10 ++++---- .../ui/src/main/res/values-tr/strings.xml | 10 ++++---- .../ui/src/main/res/values-uk/strings.xml | 10 ++++---- .../ui/src/main/res/values-ur/strings.xml | 10 ++++---- .../ui/src/main/res/values-uz/strings.xml | 10 ++++---- .../ui/src/main/res/values-vi/strings.xml | 10 ++++---- .../ui/src/main/res/values-zh-rCN/strings.xml | 10 ++++---- .../ui/src/main/res/values-zh-rHK/strings.xml | 10 ++++---- .../ui/src/main/res/values-zh-rTW/strings.xml | 10 ++++---- .../ui/src/main/res/values-zu/strings.xml | 10 ++++---- 160 files changed, 502 insertions(+), 412 deletions(-) diff --git a/libraries/session/src/main/res/values-af/strings.xml b/libraries/session/src/main/res/values-af/strings.xml index ac138172bf..8ff1ece685 100755 --- a/libraries/session/src/main/res/values-af/strings.xml +++ b/libraries/session/src/main/res/values-af/strings.xml @@ -21,4 +21,5 @@ Soek tot by volgende item Soek agtertoe Soek vorentoe + Stawing word vereis diff --git a/libraries/session/src/main/res/values-am/strings.xml b/libraries/session/src/main/res/values-am/strings.xml index dca4e40a42..167aa23d48 100755 --- a/libraries/session/src/main/res/values-am/strings.xml +++ b/libraries/session/src/main/res/values-am/strings.xml @@ -21,4 +21,5 @@ ወደ ቀጣዩ ንጥል ፈልግ ወደኋላ ፈልግ ወደፊት ፈልግ + ማረጋገጥ ያስፈልጋል diff --git a/libraries/session/src/main/res/values-ar/strings.xml b/libraries/session/src/main/res/values-ar/strings.xml index 884e85ceb2..140f15247b 100755 --- a/libraries/session/src/main/res/values-ar/strings.xml +++ b/libraries/session/src/main/res/values-ar/strings.xml @@ -21,4 +21,5 @@ تقديم إلى العنصر التالي ترجيع تقديم + المصادقة مطلوبة diff --git a/libraries/session/src/main/res/values-az/strings.xml b/libraries/session/src/main/res/values-az/strings.xml index 94293d9202..9b026dc789 100755 --- a/libraries/session/src/main/res/values-az/strings.xml +++ b/libraries/session/src/main/res/values-az/strings.xml @@ -21,4 +21,5 @@ Növbəti elementə keçin Geri keçin İrəli keçin + Doğrulanma tələb olunur diff --git a/libraries/session/src/main/res/values-b+sr+Latn/strings.xml b/libraries/session/src/main/res/values-b+sr+Latn/strings.xml index 7d382d23ad..aab80ab5a7 100755 --- a/libraries/session/src/main/res/values-b+sr+Latn/strings.xml +++ b/libraries/session/src/main/res/values-b+sr+Latn/strings.xml @@ -21,4 +21,5 @@ Premotaj na sledeću stavku Premotaj unazad Premotaj unapred + Potrebna je potvrda identiteta diff --git a/libraries/session/src/main/res/values-be/strings.xml b/libraries/session/src/main/res/values-be/strings.xml index e334cb47ef..b01d57052a 100755 --- a/libraries/session/src/main/res/values-be/strings.xml +++ b/libraries/session/src/main/res/values-be/strings.xml @@ -21,4 +21,5 @@ Перайсці да наступнага элемента Перайсці назад Перайсці ўперад + Патрабуецца аўтэнтыфікацыя diff --git a/libraries/session/src/main/res/values-bg/strings.xml b/libraries/session/src/main/res/values-bg/strings.xml index 87b50ddf78..e24ec1e9bc 100755 --- a/libraries/session/src/main/res/values-bg/strings.xml +++ b/libraries/session/src/main/res/values-bg/strings.xml @@ -21,4 +21,5 @@ Придвижване към следващия елемент Придвижване назад Придвижване напред + Изисква се удостоверяване diff --git a/libraries/session/src/main/res/values-bn/strings.xml b/libraries/session/src/main/res/values-bn/strings.xml index f54c3792fd..2694309f6a 100755 --- a/libraries/session/src/main/res/values-bn/strings.xml +++ b/libraries/session/src/main/res/values-bn/strings.xml @@ -21,4 +21,5 @@ পরের আইটেমে যান ফিরে যাওয়ার বোতাম এগিয়ে যান + যাচাইকরণ প্রয়োজন diff --git a/libraries/session/src/main/res/values-bs/strings.xml b/libraries/session/src/main/res/values-bs/strings.xml index c9c4ac8985..2441a1fca8 100755 --- a/libraries/session/src/main/res/values-bs/strings.xml +++ b/libraries/session/src/main/res/values-bs/strings.xml @@ -21,4 +21,5 @@ Pomicanje na sljedeću stavku Pomicanje nazad Pomicanje naprijed + Potrebna je autentifikacija diff --git a/libraries/session/src/main/res/values-ca/strings.xml b/libraries/session/src/main/res/values-ca/strings.xml index fc4e653346..f9bf84a263 100755 --- a/libraries/session/src/main/res/values-ca/strings.xml +++ b/libraries/session/src/main/res/values-ca/strings.xml @@ -21,4 +21,5 @@ Ves a l\'element següent Retrocedeix Avança + Autenticació obligatòria diff --git a/libraries/session/src/main/res/values-cs/strings.xml b/libraries/session/src/main/res/values-cs/strings.xml index 3eda37e6f8..9c1f3dc49f 100755 --- a/libraries/session/src/main/res/values-cs/strings.xml +++ b/libraries/session/src/main/res/values-cs/strings.xml @@ -21,4 +21,5 @@ Posunout na další položku Posunout zpět Posunout vpřed + Je vyžadováno ověření diff --git a/libraries/session/src/main/res/values-da/strings.xml b/libraries/session/src/main/res/values-da/strings.xml index a03e95d1cb..c8236d5ab0 100755 --- a/libraries/session/src/main/res/values-da/strings.xml +++ b/libraries/session/src/main/res/values-da/strings.xml @@ -21,4 +21,5 @@ Hop til næste element Hop tilbage Hop frem + Godkendelse er påkrævet diff --git a/libraries/session/src/main/res/values-de/strings.xml b/libraries/session/src/main/res/values-de/strings.xml index 27923da79d..f6685e5999 100755 --- a/libraries/session/src/main/res/values-de/strings.xml +++ b/libraries/session/src/main/res/values-de/strings.xml @@ -21,4 +21,5 @@ Zum nächsten Element Zurückspulen Vorspulen + Authentifizierung erforderlich diff --git a/libraries/session/src/main/res/values-el/strings.xml b/libraries/session/src/main/res/values-el/strings.xml index d3b6f555a6..0d90dca7f9 100755 --- a/libraries/session/src/main/res/values-el/strings.xml +++ b/libraries/session/src/main/res/values-el/strings.xml @@ -21,4 +21,5 @@ Αναζήτηση προς επόμενο στοιχείο Αναζήτηση προς τα πίσω Αναζήτηση προς τα εμπρός + Απαιτείται έλεγχος ταυτότητας diff --git a/libraries/session/src/main/res/values-en-rAU/strings.xml b/libraries/session/src/main/res/values-en-rAU/strings.xml index 75e283fba2..0bf10abd2d 100755 --- a/libraries/session/src/main/res/values-en-rAU/strings.xml +++ b/libraries/session/src/main/res/values-en-rAU/strings.xml @@ -21,4 +21,5 @@ Forward to next item Rewind Fast forward + Authentication required diff --git a/libraries/session/src/main/res/values-en-rGB/strings.xml b/libraries/session/src/main/res/values-en-rGB/strings.xml index 75e283fba2..0bf10abd2d 100755 --- a/libraries/session/src/main/res/values-en-rGB/strings.xml +++ b/libraries/session/src/main/res/values-en-rGB/strings.xml @@ -21,4 +21,5 @@ Forward to next item Rewind Fast forward + Authentication required diff --git a/libraries/session/src/main/res/values-en-rIN/strings.xml b/libraries/session/src/main/res/values-en-rIN/strings.xml index 75e283fba2..0bf10abd2d 100755 --- a/libraries/session/src/main/res/values-en-rIN/strings.xml +++ b/libraries/session/src/main/res/values-en-rIN/strings.xml @@ -21,4 +21,5 @@ Forward to next item Rewind Fast forward + Authentication required diff --git a/libraries/session/src/main/res/values-es-rUS/strings.xml b/libraries/session/src/main/res/values-es-rUS/strings.xml index 21866ca55c..c3ea5dcb07 100755 --- a/libraries/session/src/main/res/values-es-rUS/strings.xml +++ b/libraries/session/src/main/res/values-es-rUS/strings.xml @@ -21,4 +21,5 @@ Saltar al siguiente elemento Retroceder Avanzar + Se requiere autenticación diff --git a/libraries/session/src/main/res/values-es/strings.xml b/libraries/session/src/main/res/values-es/strings.xml index b4f698ba43..1a8880ecdb 100755 --- a/libraries/session/src/main/res/values-es/strings.xml +++ b/libraries/session/src/main/res/values-es/strings.xml @@ -21,4 +21,5 @@ Ir al elemento siguiente Volver Avanzar + Autenticación obligatoria diff --git a/libraries/session/src/main/res/values-et/strings.xml b/libraries/session/src/main/res/values-et/strings.xml index cb1fec5338..4bd8e64a45 100755 --- a/libraries/session/src/main/res/values-et/strings.xml +++ b/libraries/session/src/main/res/values-et/strings.xml @@ -21,4 +21,5 @@ Järgmise üksuse juurde liikumine Tagasikerimine Edasikerimine + Vajalik on autentimine diff --git a/libraries/session/src/main/res/values-eu/strings.xml b/libraries/session/src/main/res/values-eu/strings.xml index d9d9070d2a..f53cc746a7 100755 --- a/libraries/session/src/main/res/values-eu/strings.xml +++ b/libraries/session/src/main/res/values-eu/strings.xml @@ -21,4 +21,5 @@ Joan hurrengo elementura Atzeratu Aurreratu + Autentifikazioa behar da diff --git a/libraries/session/src/main/res/values-fa/strings.xml b/libraries/session/src/main/res/values-fa/strings.xml index c78caeb81f..eb8a961f98 100755 --- a/libraries/session/src/main/res/values-fa/strings.xml +++ b/libraries/session/src/main/res/values-fa/strings.xml @@ -21,4 +21,5 @@ رفتن به مورد بعدی رفتن به عقب رفتن به جلو + اصالت‌سنجی لازم است diff --git a/libraries/session/src/main/res/values-fi/strings.xml b/libraries/session/src/main/res/values-fi/strings.xml index c6cb5b69a0..a4d6637218 100755 --- a/libraries/session/src/main/res/values-fi/strings.xml +++ b/libraries/session/src/main/res/values-fi/strings.xml @@ -21,4 +21,5 @@ Siirry seuraavaan Siirry taaksepäin Siirry eteenpäin + Todennus vaaditaan diff --git a/libraries/session/src/main/res/values-fr-rCA/strings.xml b/libraries/session/src/main/res/values-fr-rCA/strings.xml index f963a8fa00..fe00e2480b 100755 --- a/libraries/session/src/main/res/values-fr-rCA/strings.xml +++ b/libraries/session/src/main/res/values-fr-rCA/strings.xml @@ -21,4 +21,5 @@ Rechercher vers l\'élément suivant Rechercher vers l\'arrière Rechercher vers l\'avant + Authentification requise diff --git a/libraries/session/src/main/res/values-fr/strings.xml b/libraries/session/src/main/res/values-fr/strings.xml index 2ebff97fd1..bda0e76cd5 100755 --- a/libraries/session/src/main/res/values-fr/strings.xml +++ b/libraries/session/src/main/res/values-fr/strings.xml @@ -21,4 +21,5 @@ Accéder à l\'élément suivant Revenir en arrière Avancer + Authentification requise diff --git a/libraries/session/src/main/res/values-gl/strings.xml b/libraries/session/src/main/res/values-gl/strings.xml index 185f983387..97c0b5ae91 100755 --- a/libraries/session/src/main/res/values-gl/strings.xml +++ b/libraries/session/src/main/res/values-gl/strings.xml @@ -21,4 +21,5 @@ Avanzar ao elemento seguinte Retroceder Avanzar + Requírese autenticación diff --git a/libraries/session/src/main/res/values-gu/strings.xml b/libraries/session/src/main/res/values-gu/strings.xml index d364ee783a..3c2e583039 100755 --- a/libraries/session/src/main/res/values-gu/strings.xml +++ b/libraries/session/src/main/res/values-gu/strings.xml @@ -21,4 +21,5 @@ આગલી આઇટમ શોધો પાછળ લઈ જાઓ આગળ લઈ જાઓ + પ્રમાણીકરણ આવશ્યક છે diff --git a/libraries/session/src/main/res/values-hi/strings.xml b/libraries/session/src/main/res/values-hi/strings.xml index 4088e7bf1a..99a792cdd5 100755 --- a/libraries/session/src/main/res/values-hi/strings.xml +++ b/libraries/session/src/main/res/values-hi/strings.xml @@ -21,4 +21,5 @@ अगले आइटम पर जाएं वापस जाएं आगे बढ़ाएं + पुष्टि करना ज़रूरी है diff --git a/libraries/session/src/main/res/values-hr/strings.xml b/libraries/session/src/main/res/values-hr/strings.xml index 4e1340b6da..3fa32acbfb 100755 --- a/libraries/session/src/main/res/values-hr/strings.xml +++ b/libraries/session/src/main/res/values-hr/strings.xml @@ -21,4 +21,5 @@ Idi na sljedeću stavku Skok unatrag Skok prema naprijed + Potrebna je autentifikacija diff --git a/libraries/session/src/main/res/values-hu/strings.xml b/libraries/session/src/main/res/values-hu/strings.xml index 891135833b..2ff84c81f9 100755 --- a/libraries/session/src/main/res/values-hu/strings.xml +++ b/libraries/session/src/main/res/values-hu/strings.xml @@ -21,4 +21,5 @@ Ugrás a következő elemre Ugrás vissza Ugrás előre + Hitelesítés szükséges diff --git a/libraries/session/src/main/res/values-hy/strings.xml b/libraries/session/src/main/res/values-hy/strings.xml index 5d3e7d9c70..c5507fa8a1 100755 --- a/libraries/session/src/main/res/values-hy/strings.xml +++ b/libraries/session/src/main/res/values-hy/strings.xml @@ -21,4 +21,5 @@ Անցնել հաջորդ տարրին Հետ գնալ Առաջ գնալ + Պահանջվում է նույնականացում diff --git a/libraries/session/src/main/res/values-in/strings.xml b/libraries/session/src/main/res/values-in/strings.xml index 1bf0d362dd..142339374a 100755 --- a/libraries/session/src/main/res/values-in/strings.xml +++ b/libraries/session/src/main/res/values-in/strings.xml @@ -21,4 +21,5 @@ Cari item berikutnya Mundur Maju + Perlu autentikasi diff --git a/libraries/session/src/main/res/values-is/strings.xml b/libraries/session/src/main/res/values-is/strings.xml index 19628ce30c..dfc6267d5a 100755 --- a/libraries/session/src/main/res/values-is/strings.xml +++ b/libraries/session/src/main/res/values-is/strings.xml @@ -21,4 +21,5 @@ Spóla að næsta atriði Spóla til baka Spóla áfram + Auðkenningar krafist diff --git a/libraries/session/src/main/res/values-it/strings.xml b/libraries/session/src/main/res/values-it/strings.xml index 93778ab509..0be1260e55 100755 --- a/libraries/session/src/main/res/values-it/strings.xml +++ b/libraries/session/src/main/res/values-it/strings.xml @@ -21,4 +21,5 @@ Vai all\'elemento successivo Vai indietro Vai avanti + Autenticazione richiesta diff --git a/libraries/session/src/main/res/values-iw/strings.xml b/libraries/session/src/main/res/values-iw/strings.xml index c69347b80b..d5232cf89b 100755 --- a/libraries/session/src/main/res/values-iw/strings.xml +++ b/libraries/session/src/main/res/values-iw/strings.xml @@ -21,4 +21,5 @@ דילוג לפריט הבא דילוג אחורה דילוג קדימה + נדרש אימות diff --git a/libraries/session/src/main/res/values-ja/strings.xml b/libraries/session/src/main/res/values-ja/strings.xml index 6d3640f4b8..44957a4eed 100755 --- a/libraries/session/src/main/res/values-ja/strings.xml +++ b/libraries/session/src/main/res/values-ja/strings.xml @@ -21,4 +21,5 @@ 次のアイテムに移動 巻き戻し 早送り + 認証が必要です diff --git a/libraries/session/src/main/res/values-ka/strings.xml b/libraries/session/src/main/res/values-ka/strings.xml index 7b88e9ad9d..44e49caf12 100755 --- a/libraries/session/src/main/res/values-ka/strings.xml +++ b/libraries/session/src/main/res/values-ka/strings.xml @@ -21,4 +21,5 @@ შემდეგ ერთეულზე გადახვევა უკან გადახვევა წინ გადახვევა + საჭიროა ავტორიზაცია diff --git a/libraries/session/src/main/res/values-kk/strings.xml b/libraries/session/src/main/res/values-kk/strings.xml index b056cc6910..98f22c7807 100755 --- a/libraries/session/src/main/res/values-kk/strings.xml +++ b/libraries/session/src/main/res/values-kk/strings.xml @@ -21,4 +21,5 @@ Келесі мазмұнға өту Артқа айналдыру Алға айналдыру + Аутентификация қажет diff --git a/libraries/session/src/main/res/values-km/strings.xml b/libraries/session/src/main/res/values-km/strings.xml index 190ffa4f1a..b12cf6a894 100755 --- a/libraries/session/src/main/res/values-km/strings.xml +++ b/libraries/session/src/main/res/values-km/strings.xml @@ -21,4 +21,5 @@ ទៅកាន់ធាតុបន្ទាប់ ថយក្រោយ រំលង​​ទៅ​មុខ + តម្រូវឱ្យ​មាន​ការផ្ទៀងផ្ទាត់ diff --git a/libraries/session/src/main/res/values-kn/strings.xml b/libraries/session/src/main/res/values-kn/strings.xml index 62f7b8bc58..485b09c0bf 100755 --- a/libraries/session/src/main/res/values-kn/strings.xml +++ b/libraries/session/src/main/res/values-kn/strings.xml @@ -21,4 +21,5 @@ ಮುಂದಿನ ಐಟಂಗೆ ಸೀಕ್ ಮಾಡಿ ಹಿಂದಕ್ಕೆ ಸೀಕ್ ಮಾಡಿ ಮುಂದಕ್ಕೆ ಸೀಕ್ ಮಾಡಿ + ದೃಢೀಕರಣದ ಅಗತ್ಯವಿದೆ diff --git a/libraries/session/src/main/res/values-ko/strings.xml b/libraries/session/src/main/res/values-ko/strings.xml index d649d822c8..015471fe5e 100755 --- a/libraries/session/src/main/res/values-ko/strings.xml +++ b/libraries/session/src/main/res/values-ko/strings.xml @@ -21,4 +21,5 @@ 다음 항목 찾기 뒤로 탐색 앞으로 탐색 + 인증 필요 diff --git a/libraries/session/src/main/res/values-ky/strings.xml b/libraries/session/src/main/res/values-ky/strings.xml index 833198482e..350724d9e0 100755 --- a/libraries/session/src/main/res/values-ky/strings.xml +++ b/libraries/session/src/main/res/values-ky/strings.xml @@ -21,4 +21,5 @@ Кийинки нерсеге өтүү Артка түрдүрүү Алдыга түрдүрүү + Аныктыгын текшерүү талап кылынат diff --git a/libraries/session/src/main/res/values-lo/strings.xml b/libraries/session/src/main/res/values-lo/strings.xml index be62a424b5..337bd7be33 100755 --- a/libraries/session/src/main/res/values-lo/strings.xml +++ b/libraries/session/src/main/res/values-lo/strings.xml @@ -21,4 +21,5 @@ ເລື່ອນໄປຫາລາຍການຕໍ່ໄປ ເລື່ອນກັບຫຼັງ ເລື່ອນໄປໜ້າ + ຕ້ອງມີການພິສູດຢືນຢັນ diff --git a/libraries/session/src/main/res/values-lt/strings.xml b/libraries/session/src/main/res/values-lt/strings.xml index 979e6b0f65..a8c02c98af 100755 --- a/libraries/session/src/main/res/values-lt/strings.xml +++ b/libraries/session/src/main/res/values-lt/strings.xml @@ -21,4 +21,5 @@ Prasukti į kitą elementą Prasukti atgal Prasukti pirmyn + Būtina nustatyti tapatybę diff --git a/libraries/session/src/main/res/values-lv/strings.xml b/libraries/session/src/main/res/values-lv/strings.xml index 55facd793b..030bb7a549 100755 --- a/libraries/session/src/main/res/values-lv/strings.xml +++ b/libraries/session/src/main/res/values-lv/strings.xml @@ -21,4 +21,5 @@ Pāriet uz nākamo vienumu Pāriet atpakaļ Pāriet uz priekšu + Nepieciešama autentificēšana diff --git a/libraries/session/src/main/res/values-mk/strings.xml b/libraries/session/src/main/res/values-mk/strings.xml index 509cc64d0d..19487a2d57 100755 --- a/libraries/session/src/main/res/values-mk/strings.xml +++ b/libraries/session/src/main/res/values-mk/strings.xml @@ -21,4 +21,5 @@ Премотај на следната ставка Премотај наназад Премотај нанапред + Потребна е проверка diff --git a/libraries/session/src/main/res/values-ml/strings.xml b/libraries/session/src/main/res/values-ml/strings.xml index 99055c910c..0b68af763f 100755 --- a/libraries/session/src/main/res/values-ml/strings.xml +++ b/libraries/session/src/main/res/values-ml/strings.xml @@ -21,4 +21,5 @@ അടുത്ത ഇനത്തിലേക്ക് നീക്കുക പിന്നോട്ട് നീക്കുക മുന്നോട്ട് നീക്കുക + പരിശോധിച്ചുറപ്പിക്കേണ്ടതുണ്ട് diff --git a/libraries/session/src/main/res/values-mn/strings.xml b/libraries/session/src/main/res/values-mn/strings.xml index 354d1ee4ed..5b9c2604fc 100755 --- a/libraries/session/src/main/res/values-mn/strings.xml +++ b/libraries/session/src/main/res/values-mn/strings.xml @@ -21,4 +21,5 @@ Дараагийн зүйл рүү гүйлгэх Буцаан гүйлгэх Урагшлуулах + Баталгаажуулалт шаардлагатай diff --git a/libraries/session/src/main/res/values-mr/strings.xml b/libraries/session/src/main/res/values-mr/strings.xml index 704ba97df2..35c8acf476 100755 --- a/libraries/session/src/main/res/values-mr/strings.xml +++ b/libraries/session/src/main/res/values-mr/strings.xml @@ -21,4 +21,5 @@ पुढील आयटमवर जा मागे जा पुढे जा + ऑथेंटिकेशन आवश्यक आहे diff --git a/libraries/session/src/main/res/values-ms/strings.xml b/libraries/session/src/main/res/values-ms/strings.xml index 8b9c87d24e..120211affc 100755 --- a/libraries/session/src/main/res/values-ms/strings.xml +++ b/libraries/session/src/main/res/values-ms/strings.xml @@ -21,4 +21,5 @@ Cari sehingga item seterusnya Mandir Mundar + Pengesahan diperlukan diff --git a/libraries/session/src/main/res/values-my/strings.xml b/libraries/session/src/main/res/values-my/strings.xml index 770c00baeb..ea9920abc4 100755 --- a/libraries/session/src/main/res/values-my/strings.xml +++ b/libraries/session/src/main/res/values-my/strings.xml @@ -21,4 +21,5 @@ ရှေ့တစ်ခုသို့ ရစ်ရန် နောက်သို့ ရစ်ရန် ရှေ့သို့ ရစ်ရန် + အထောက်အထားစိစစ်ခြင်း လိုအပ်သည် diff --git a/libraries/session/src/main/res/values-nb/strings.xml b/libraries/session/src/main/res/values-nb/strings.xml index c4f6f480a9..fefb5ea835 100755 --- a/libraries/session/src/main/res/values-nb/strings.xml +++ b/libraries/session/src/main/res/values-nb/strings.xml @@ -21,4 +21,5 @@ Hopp til det neste elementet Hopp bakover Hopp fremover + Autentisering kreves diff --git a/libraries/session/src/main/res/values-ne/strings.xml b/libraries/session/src/main/res/values-ne/strings.xml index 21f2219110..8d282366c8 100755 --- a/libraries/session/src/main/res/values-ne/strings.xml +++ b/libraries/session/src/main/res/values-ne/strings.xml @@ -21,4 +21,5 @@ अर्को वस्तुमा जानुहोस् पछाडि जानुहोस् अगाडि जानुहोस् + पुष्टि गर्नु पर्ने हुन्छ diff --git a/libraries/session/src/main/res/values-nl/strings.xml b/libraries/session/src/main/res/values-nl/strings.xml index 87e8eebf5c..f3735156e4 100755 --- a/libraries/session/src/main/res/values-nl/strings.xml +++ b/libraries/session/src/main/res/values-nl/strings.xml @@ -21,4 +21,5 @@ Naar volgende item springen Achteruit springen Vooruit springen + Verificatie vereist diff --git a/libraries/session/src/main/res/values-pa/strings.xml b/libraries/session/src/main/res/values-pa/strings.xml index 386426b882..86653fca74 100755 --- a/libraries/session/src/main/res/values-pa/strings.xml +++ b/libraries/session/src/main/res/values-pa/strings.xml @@ -21,4 +21,5 @@ ਅਗਲੀ ਆਈਟਮ \'ਤੇ ਜਾਓ ਪਿੱਛੇ ਲਿਜਾਓ ਅੱਗੇ ਵਧਾਓ + ਪ੍ਰਮਾਣੀਕਰਨ ਲੋੜੀਂਦਾ ਹੈ diff --git a/libraries/session/src/main/res/values-pl/strings.xml b/libraries/session/src/main/res/values-pl/strings.xml index 01a659b478..10eddbef38 100755 --- a/libraries/session/src/main/res/values-pl/strings.xml +++ b/libraries/session/src/main/res/values-pl/strings.xml @@ -21,4 +21,5 @@ Przewiń do następnego elementu Przewiń do tyłu Przewiń do przodu + Wymagane uwierzytelnienie diff --git a/libraries/session/src/main/res/values-pt-rPT/strings.xml b/libraries/session/src/main/res/values-pt-rPT/strings.xml index d6ac74420f..0be33ad385 100755 --- a/libraries/session/src/main/res/values-pt-rPT/strings.xml +++ b/libraries/session/src/main/res/values-pt-rPT/strings.xml @@ -21,4 +21,5 @@ Avançar para o item seguinte Retroceder Avançar + Autenticação necessária diff --git a/libraries/session/src/main/res/values-pt/strings.xml b/libraries/session/src/main/res/values-pt/strings.xml index 3f3150177f..7bd7d79520 100755 --- a/libraries/session/src/main/res/values-pt/strings.xml +++ b/libraries/session/src/main/res/values-pt/strings.xml @@ -21,4 +21,5 @@ Ir para o próximo item Retroceder Avançar + Autenticação necessária diff --git a/libraries/session/src/main/res/values-ro/strings.xml b/libraries/session/src/main/res/values-ro/strings.xml index 7a94921fd5..dd6fc779bd 100755 --- a/libraries/session/src/main/res/values-ro/strings.xml +++ b/libraries/session/src/main/res/values-ro/strings.xml @@ -21,4 +21,5 @@ Treci la elementul următor Derulează înapoi Derulează înainte + Autentificarea este obligatorie diff --git a/libraries/session/src/main/res/values-ru/strings.xml b/libraries/session/src/main/res/values-ru/strings.xml index 15539019ba..b643018ac0 100755 --- a/libraries/session/src/main/res/values-ru/strings.xml +++ b/libraries/session/src/main/res/values-ru/strings.xml @@ -21,4 +21,5 @@ К следующему файлу Перемотать назад Перемотать вперед + Требуется аутентификация diff --git a/libraries/session/src/main/res/values-si/strings.xml b/libraries/session/src/main/res/values-si/strings.xml index 720efff6d3..48c60ddef8 100755 --- a/libraries/session/src/main/res/values-si/strings.xml +++ b/libraries/session/src/main/res/values-si/strings.xml @@ -21,4 +21,5 @@ ඊළඟ අයිතමය වෙත අන්වේෂණය ආපස්සට අන්වේෂණය ඉදිරියට අන්වේෂණය + සත්‍යාපනය අවශ්‍යයි diff --git a/libraries/session/src/main/res/values-sk/strings.xml b/libraries/session/src/main/res/values-sk/strings.xml index dd71df61e3..f2d31acf05 100755 --- a/libraries/session/src/main/res/values-sk/strings.xml +++ b/libraries/session/src/main/res/values-sk/strings.xml @@ -21,4 +21,5 @@ Hľadať v ďalšej položke Hľadať smerom dozadu Hľadať smerom dopredu + Vyžaduje sa overenie diff --git a/libraries/session/src/main/res/values-sl/strings.xml b/libraries/session/src/main/res/values-sl/strings.xml index b77b1d15c9..ab3fa88897 100755 --- a/libraries/session/src/main/res/values-sl/strings.xml +++ b/libraries/session/src/main/res/values-sl/strings.xml @@ -21,4 +21,5 @@ Premakni na naslednji element Premakni nazaj Premakni naprej + Zahtevano je preverjanje pristnosti diff --git a/libraries/session/src/main/res/values-sq/strings.xml b/libraries/session/src/main/res/values-sq/strings.xml index ec16268eb5..c8ebb5046d 100755 --- a/libraries/session/src/main/res/values-sq/strings.xml +++ b/libraries/session/src/main/res/values-sq/strings.xml @@ -21,4 +21,5 @@ Kërko te artikulli tjetër Kërko prapa Kërko përpara + Kërkohet vërtetimi diff --git a/libraries/session/src/main/res/values-sr/strings.xml b/libraries/session/src/main/res/values-sr/strings.xml index 9713781784..e3f7665ac3 100755 --- a/libraries/session/src/main/res/values-sr/strings.xml +++ b/libraries/session/src/main/res/values-sr/strings.xml @@ -21,4 +21,5 @@ Премотај на следећу ставку Премотај уназад Премотај унапред + Потребна је потврда идентитета diff --git a/libraries/session/src/main/res/values-sv/strings.xml b/libraries/session/src/main/res/values-sv/strings.xml index 983e7a3647..ad53a1d6e4 100755 --- a/libraries/session/src/main/res/values-sv/strings.xml +++ b/libraries/session/src/main/res/values-sv/strings.xml @@ -21,4 +21,5 @@ Hoppa till nästa objekt Hoppa bakåt Hoppa framåt + Autentisering krävs diff --git a/libraries/session/src/main/res/values-sw/strings.xml b/libraries/session/src/main/res/values-sw/strings.xml index 25484e4f98..241fb50b78 100755 --- a/libraries/session/src/main/res/values-sw/strings.xml +++ b/libraries/session/src/main/res/values-sw/strings.xml @@ -21,4 +21,5 @@ Nenda kwenye maudhui yanayofuata Sogeza nyuma Sogeza mbele + Uthibitishaji unahitajika diff --git a/libraries/session/src/main/res/values-ta/strings.xml b/libraries/session/src/main/res/values-ta/strings.xml index c0ea3beac9..e0b65856a2 100755 --- a/libraries/session/src/main/res/values-ta/strings.xml +++ b/libraries/session/src/main/res/values-ta/strings.xml @@ -21,4 +21,5 @@ அடுத்ததற்குச் செல்லும் பின்செல்லும் முன்செல்லும் + அங்கீகாரம் தேவை diff --git a/libraries/session/src/main/res/values-te/strings.xml b/libraries/session/src/main/res/values-te/strings.xml index c32b4ba9cf..e8a3734789 100755 --- a/libraries/session/src/main/res/values-te/strings.xml +++ b/libraries/session/src/main/res/values-te/strings.xml @@ -21,4 +21,5 @@ తర్వాతి ఐటెమ్‌కు దాటవేయండి వెనుకకు దాటవేయండి ముందుకు ఫార్వర్డ్ చేయండి + ప్రామాణీకరణ అవసరం diff --git a/libraries/session/src/main/res/values-th/strings.xml b/libraries/session/src/main/res/values-th/strings.xml index dfd5bfc500..683e2aae68 100755 --- a/libraries/session/src/main/res/values-th/strings.xml +++ b/libraries/session/src/main/res/values-th/strings.xml @@ -21,4 +21,5 @@ กรอไปยังรายการถัดไป กรอกลับ กรอไปข้างหน้า + ต้องมีการตรวจสอบสิทธิ์ diff --git a/libraries/session/src/main/res/values-tl/strings.xml b/libraries/session/src/main/res/values-tl/strings.xml index ba200975d7..995740a809 100755 --- a/libraries/session/src/main/res/values-tl/strings.xml +++ b/libraries/session/src/main/res/values-tl/strings.xml @@ -21,4 +21,5 @@ Mag-seek sa susunod na item Mag-seek pabalik Mag-seek pasulong + Kinakailangan ang pag-authenticate diff --git a/libraries/session/src/main/res/values-tr/strings.xml b/libraries/session/src/main/res/values-tr/strings.xml index 9ecb219f1d..2795069c69 100755 --- a/libraries/session/src/main/res/values-tr/strings.xml +++ b/libraries/session/src/main/res/values-tr/strings.xml @@ -21,4 +21,5 @@ Sonraki öğeye sar Geri sar İleri sar + Kimlik doğrulama gerekiyor diff --git a/libraries/session/src/main/res/values-uk/strings.xml b/libraries/session/src/main/res/values-uk/strings.xml index c8766454d2..4786db3ce7 100755 --- a/libraries/session/src/main/res/values-uk/strings.xml +++ b/libraries/session/src/main/res/values-uk/strings.xml @@ -21,4 +21,5 @@ Перейти до наступного об’єкта Перемотати назад Перемотати вперед + Потрібна автентифікація diff --git a/libraries/session/src/main/res/values-ur/strings.xml b/libraries/session/src/main/res/values-ur/strings.xml index 5068306a6e..423908db8a 100755 --- a/libraries/session/src/main/res/values-ur/strings.xml +++ b/libraries/session/src/main/res/values-ur/strings.xml @@ -21,4 +21,5 @@ اگلے آئٹم پر جائیں واپس جائیں آگے جائیں + توثیق مطلوب ہے diff --git a/libraries/session/src/main/res/values-uz/strings.xml b/libraries/session/src/main/res/values-uz/strings.xml index 68e7154fd6..fed30dd627 100755 --- a/libraries/session/src/main/res/values-uz/strings.xml +++ b/libraries/session/src/main/res/values-uz/strings.xml @@ -21,4 +21,5 @@ Keyingi trekka oʻtish Orqaga surish Oldinga surish + Haqiqiylikni tekshirish talab etiladi diff --git a/libraries/session/src/main/res/values-vi/strings.xml b/libraries/session/src/main/res/values-vi/strings.xml index 096e2678ca..9307041370 100755 --- a/libraries/session/src/main/res/values-vi/strings.xml +++ b/libraries/session/src/main/res/values-vi/strings.xml @@ -21,4 +21,5 @@ Tua đến mục tiếp theo Tua lại Tua đi + Yêu cầu xác thực diff --git a/libraries/session/src/main/res/values-zh-rCN/strings.xml b/libraries/session/src/main/res/values-zh-rCN/strings.xml index 061d8cb750..0db7916594 100755 --- a/libraries/session/src/main/res/values-zh-rCN/strings.xml +++ b/libraries/session/src/main/res/values-zh-rCN/strings.xml @@ -21,4 +21,5 @@ 跳转到下一项 快退 快进 + 需要进行身份验证 diff --git a/libraries/session/src/main/res/values-zh-rHK/strings.xml b/libraries/session/src/main/res/values-zh-rHK/strings.xml index d92be1d68b..87716dc283 100755 --- a/libraries/session/src/main/res/values-zh-rHK/strings.xml +++ b/libraries/session/src/main/res/values-zh-rHK/strings.xml @@ -21,4 +21,5 @@ 跳去下一項 後移 快轉 + 需要驗證 diff --git a/libraries/session/src/main/res/values-zh-rTW/strings.xml b/libraries/session/src/main/res/values-zh-rTW/strings.xml index a8d4854687..e7603e7d90 100755 --- a/libraries/session/src/main/res/values-zh-rTW/strings.xml +++ b/libraries/session/src/main/res/values-zh-rTW/strings.xml @@ -21,4 +21,5 @@ 跳轉到下一個項目 倒轉 快轉 + 必須驗證 diff --git a/libraries/session/src/main/res/values-zu/strings.xml b/libraries/session/src/main/res/values-zu/strings.xml index d33df71dc5..4f5ee5dddd 100755 --- a/libraries/session/src/main/res/values-zu/strings.xml +++ b/libraries/session/src/main/res/values-zu/strings.xml @@ -21,4 +21,5 @@ Funa into elandelayo Funa okwangemuva Funa uye phambili + Ukufakazela ubuqiniso kudingekile diff --git a/libraries/ui/src/main/res/values-af/strings.xml b/libraries/ui/src/main/res/values-af/strings.xml index 4511839582..dd37229848 100644 --- a/libraries/ui/src/main/res/values-af/strings.xml +++ b/libraries/ui/src/main/res/values-af/strings.xml @@ -37,11 +37,11 @@ Spoel %d sekonde vinnig vorentoe Spoel %d sekondes vinnig vorentoe - Herhaal niks - Herhaal een - Herhaal alles - Skommel is aan - Skommel is af + Huidige modus: herhaal niks. Wissel herhaalmodus. + Huidige modus: herhaal een. Wissel herhaalmodus. + Huidige modus: herhaal alles. Wissel herhaalmodus. + Deaktiveer skommelmodus + Aktiveer skommelmodus VR-modus Deaktiveer onderskrifte Aktiveer onderskrifte diff --git a/libraries/ui/src/main/res/values-am/strings.xml b/libraries/ui/src/main/res/values-am/strings.xml index a35bf1a6f5..343e45fbb8 100644 --- a/libraries/ui/src/main/res/values-am/strings.xml +++ b/libraries/ui/src/main/res/values-am/strings.xml @@ -37,11 +37,11 @@ በ%d ሰከንዶች በፍጥነት ወደፊት ያሳልፉ በ%d ሰከንዶች በፍጥነት ወደፊት ያሳልፉ - ምንም አትድገም - አንድ ድገም - ሁሉንም ድገም - መበወዝ በርቷል - መበወዝ ጠፍቷል + የአሁኑ ሁነታ፦ ምንም አትድገም። የመድገም ሁነታን ቀያይር። + የአሁን ሁነታ፦ አንዱን ድገም። የመድገም ሁነታን ቀያይር። + የአሁን ሁነታ፦ ሁሉም ድገም። የመድገም ሁነታን ቀያይር። + የበውዝ ሁነታን አሰናክል + የበውዝ ሁነታን አንቃ የቪአር ሁነታ የግርጌ ጽሑፎችን አሰናክል የግርጌ ጽሑፎችን አንቃ diff --git a/libraries/ui/src/main/res/values-ar/strings.xml b/libraries/ui/src/main/res/values-ar/strings.xml index c7e48f7879..bef7a468e5 100644 --- a/libraries/ui/src/main/res/values-ar/strings.xml +++ b/libraries/ui/src/main/res/values-ar/strings.xml @@ -45,11 +45,11 @@ تقديم سريع للفيديو بمقدار %d ثانية تقديم سريع للفيديو بمقدار %d ثانية - عدم التكرار - تكرار مقطع صوتي واحد - تكرار الكل - تفعيل الترتيب العشوائي - إيقاف الترتيب العشوائي + الوضع الحالي: عدم التكرار. تبديل وضع التكرار + الوضع الحالي: التكرار مرة واحدة. تبديل وضع التكرار + الوضع الحالي: تكرار الكل. تبديل وضع التكرار + إيقاف وضع الترتيب العشوائي + تفعيل وضع الترتيب العشوائي وضع VR إيقاف الترجمة تفعيل الترجمة diff --git a/libraries/ui/src/main/res/values-az/strings.xml b/libraries/ui/src/main/res/values-az/strings.xml index 5f8cce18e3..86ea052268 100644 --- a/libraries/ui/src/main/res/values-az/strings.xml +++ b/libraries/ui/src/main/res/values-az/strings.xml @@ -37,11 +37,11 @@ %d saniyə irəli çəkin %d saniyə irəli çəkin - Heç biri təkrarlanmasın - Biri təkrarlansın - Hamısı təkrarlansın - Qarışdırma aktivdir - Qarışdırma deaktivdir + Cari rejim: Heç birini təkrarlamayın. Təkrarlanma rejimini keçirin. + Cari rejim: Birini təkrarlayın. Təkrarlanma rejimini keçirin. + Cari rejim: Hamısını təkrarlayın. Təkrarlanma rejimini keçirin. + Qarışdırma rejimini deaktiv edin + Qarışdırma rejimini aktiv edin VR rejimi Subtitrləri deaktiv edin Subtitrləri aktiv edin diff --git a/libraries/ui/src/main/res/values-b+sr+Latn/strings.xml b/libraries/ui/src/main/res/values-b+sr+Latn/strings.xml index 8173fb6361..2acf688bca 100644 --- a/libraries/ui/src/main/res/values-b+sr+Latn/strings.xml +++ b/libraries/ui/src/main/res/values-b+sr+Latn/strings.xml @@ -39,11 +39,11 @@ Premotajte %d sekunde unapred Premotajte %d sekundi unapred - Ne ponavljaj nijednu - Ponovi jednu - Ponovi sve - Nasumično puštanje je uključeno - Nasumično puštanje je isključeno + Vaš režim: Bez ponavljanja. Uključite/isključite. + Vaš režim: Ponovi jedno. Uključite/isključite. + Vaš režim: Ponovi sve. Uključite/isključite. + Onemogućite nasumični režim + Omogućite nasumični režim VR režim Onemogući titlove Omogući titlove diff --git a/libraries/ui/src/main/res/values-be/strings.xml b/libraries/ui/src/main/res/values-be/strings.xml index 82518faecf..05ea71c472 100644 --- a/libraries/ui/src/main/res/values-be/strings.xml +++ b/libraries/ui/src/main/res/values-be/strings.xml @@ -41,11 +41,11 @@ Пераматаць уперад на %d секунд Пераматаць уперад на %d секунды - Не паўтараць нічога - Паўтарыць адзін элемент - Паўтарыць усе - Перамешванне ўключана - Перамешванне выключана + Цяпер: без паўтораў. Змяніць. + Цяпер: паўтараць фрагмент. Змяніць. + Цяпер: паўтараць усё. Змяніць. + Выключыць рэжым перамешвання + Уключыць рэжым перамешвання VR-рэжым Выключыць субцітры Уключыць субцітры @@ -74,7 +74,7 @@ Альтэрнатыўны запіс Дадатковы запіс Каментарыі - Цітры + Субцітры %1$.2f Мбіт/с %1$s, %2$s diff --git a/libraries/ui/src/main/res/values-bg/strings.xml b/libraries/ui/src/main/res/values-bg/strings.xml index 056c5b9e83..3a898ed4bd 100644 --- a/libraries/ui/src/main/res/values-bg/strings.xml +++ b/libraries/ui/src/main/res/values-bg/strings.xml @@ -37,11 +37,11 @@ Превъртане с %d секунда напред Превъртане с(ъс) %d секунди напред - Без повтаряне - Повтаряне на един елемент - Повтаряне на всички - Разбъркването е включено - Разбъркването е изключено + Текущ режим: Без повтаряне. Превкл. режима за повтаряне. + Текущ режим: Повтар. на 1 елемент. Превкл. режима за повтаряне. + Текущ режим: Повтар. на всички. Превкл. режима за повтаряне. + Деактивиране на режима за разбъркване + Активиране на режима за разбъркване режим за VR Деактивиране на субтитрите Активиране на субтитрите diff --git a/libraries/ui/src/main/res/values-bn/strings.xml b/libraries/ui/src/main/res/values-bn/strings.xml index bf438cc335..2235db434b 100644 --- a/libraries/ui/src/main/res/values-bn/strings.xml +++ b/libraries/ui/src/main/res/values-bn/strings.xml @@ -37,11 +37,11 @@ %d সেকেন্ড ফাস্ট ফরওয়ার্ড করুন %d সেকেন্ড ফাস্ট ফরওয়ার্ড করুন - কোনও আইটেম আবার চালাবেন না - একটি আইটেম আবার চালান - সবগুলি আইটেম আবার চালান - শাফেল মোড চালু করা হয়েছে - শাফেল মোড বন্ধ করা হয়েছে + \'বর্তমান\' মোড: রিপিট হয় না। \'রিপিট\' মোড টগল হয়। + \'বর্তমান\' মোড: একটি রিপিট হয়। \'রিপিট\' মোড টগল হয়। + \'বর্তমান\' মোড: সব রিপিট হয়। \'রিপিট\' মোড টগল হয়। + \'শাফেল\' মোড বন্ধ করে + \'শাফেল\' মোড চালু করে ভিআর মোড সাবটাইটেল বন্ধ করুন সাবটাইটেল চালু করুন diff --git a/libraries/ui/src/main/res/values-bs/strings.xml b/libraries/ui/src/main/res/values-bs/strings.xml index 8ce4cf7648..3793c330c1 100644 --- a/libraries/ui/src/main/res/values-bs/strings.xml +++ b/libraries/ui/src/main/res/values-bs/strings.xml @@ -39,11 +39,11 @@ Premotavanje %d sekunde naprijed Premotavanje %d sekundi naprijed - Ne ponavljaj - Ponovi jedno - Ponovi sve - Uključi nasumično - Isključi nasumično + Trenutni način: Ne ponavljaj. Uklj./isklj. ponavlj. + Trenutni način: Ponovi jedno. Uklj./isklj. ponavlj. + Trenutni način: Ponovi sve. Uklj./isklj. ponavlj. + Onemogućavanje nasumičnog načina rada + Omogućavanje nasumičnog načina rada VR način rada Onemogućavanje titlova Omogućavanje titlova diff --git a/libraries/ui/src/main/res/values-ca/strings.xml b/libraries/ui/src/main/res/values-ca/strings.xml index 46aef87dd1..ba2e8374a9 100644 --- a/libraries/ui/src/main/res/values-ca/strings.xml +++ b/libraries/ui/src/main/res/values-ca/strings.xml @@ -37,11 +37,11 @@ Avança ràpidament %d segon Avança ràpidament %d segons - No en repeteixis cap - Repeteix una - Repeteix tot - Activa reproducció aleatòria - Desactiva reproducció aleatòria + Actual: no repeteixis. Commuta mode repetició. + Actual: repeteix un. Commuta mode repetició. + Actual: repeteix tot. Commuta mode repetició. + Desactiva el mode aleatori + Activa el mode aleatori Mode RV Desactiva els subtítols Activa els subtítols diff --git a/libraries/ui/src/main/res/values-cs/strings.xml b/libraries/ui/src/main/res/values-cs/strings.xml index 6144f3cad3..bb0f85369a 100644 --- a/libraries/ui/src/main/res/values-cs/strings.xml +++ b/libraries/ui/src/main/res/values-cs/strings.xml @@ -41,11 +41,11 @@ Posunout vpřed o %d sekundy Posunout vpřed o %d sekund - Neopakovat - Opakovat jednu - Opakovat vše - Náhodné přehrávání zapnuto - Náhodné přehrávání vypnuto + Aktuálně: Nic neopakovat. Zapněte režim opakování. + Aktuálně: Opakovat jednu položku. Zapněte režim opakování. + Aktuálně: Opakovat vše. Zapněte režim opakování. + Deaktivovat náhodné přehrávání + Aktivovat náhodné přehrávání Režim VR Vypnout titulky Zapnout titulky diff --git a/libraries/ui/src/main/res/values-da/strings.xml b/libraries/ui/src/main/res/values-da/strings.xml index e7a07fef48..71ff36a1a3 100644 --- a/libraries/ui/src/main/res/values-da/strings.xml +++ b/libraries/ui/src/main/res/values-da/strings.xml @@ -37,11 +37,11 @@ Spol %d sekund frem Spol %d sekunder frem - Gentag ingen - Gentag én - Gentag alle - Bland er slået til - Bland er slået fra + Tilstand: Gentag ikke. Slå Gentag til/fra. + Tilstand: Gentag én. Slå Gentag til/fra. + Tilstand: Gentag alle. Slå Gentag til/fra. + Deaktiver Bland + Aktivér Bland VR-tilstand Deaktiver undertekster Aktivér undertekster diff --git a/libraries/ui/src/main/res/values-de/strings.xml b/libraries/ui/src/main/res/values-de/strings.xml index 05087df6a2..ff160f6a6a 100644 --- a/libraries/ui/src/main/res/values-de/strings.xml +++ b/libraries/ui/src/main/res/values-de/strings.xml @@ -37,11 +37,11 @@ %d Sekunde vorspulen %d Sekunden vorspulen - Keinen wiederholen - Einen wiederholen - Alle wiederholen - Zufallsmix an - Zufallsmix aus + Aktueller Modus: nichts wiederholen. Wiederholungsmodus ändern. + Aktueller Modus: einen Titel wiederholen. Wiederholungsmodus ändern. + Aktueller Modus: alles wiederholen. Wiederholungsmodus ändern. + Zufallsmix deaktivieren + Zufallsmix aktivieren VR-Modus Untertitel deaktivieren Untertitel aktivieren diff --git a/libraries/ui/src/main/res/values-el/strings.xml b/libraries/ui/src/main/res/values-el/strings.xml index d7d00f184e..c4ed289bef 100644 --- a/libraries/ui/src/main/res/values-el/strings.xml +++ b/libraries/ui/src/main/res/values-el/strings.xml @@ -37,11 +37,11 @@ Γρήγορη προώθηση κατά %d δευτερόλεπτο Γρήγορη προώθηση κατά %d δευτερόλεπτα - Καμία επανάληψη - Επανάληψη ενός κομματιού - Επανάληψη όλων - Τυχαία αναπαραγωγή: Ενεργή - Τυχαία αναπαραγωγή: Ανενεργή + Τρέχ. λειτουργία: Καμία επανάλ. Εναλλαγή λειτουργ. επανάλ. + Τρέχ. λειτουργία: Επανάλ. ενός. Εναλλαγή λειτουργ. επανάλ. + Τρέχ. λειτουργία: Επανάλ. όλων Εναλλαγή λειτουργ. επανάλ. + Απενεργ. λειτουργ. τυχαίας αναπαραγωγής + Ενεργοπ. λειτουργ. τυχαίας αναπαραγωγής Λειτουργία VR mode Απενεργοποίηση υπότιτλων Ενεργοποίηση υπότιτλων diff --git a/libraries/ui/src/main/res/values-en-rAU/strings.xml b/libraries/ui/src/main/res/values-en-rAU/strings.xml index c3a1c8a5aa..af85902797 100644 --- a/libraries/ui/src/main/res/values-en-rAU/strings.xml +++ b/libraries/ui/src/main/res/values-en-rAU/strings.xml @@ -37,11 +37,11 @@ Fast-forward %d second Fast-forward %d seconds - Repeat none - Repeat one - Repeat all - Shuffle on - Shuffle off + Current mode: Repeat none. Toggle repeat mode. + Current mode: Repeat one. Toggle repeat mode. + Current mode: Repeat all. Toggle repeat mode. + Disable shuffle mode + Enable shuffle mode VR mode Disable subtitles Enable subtitles diff --git a/libraries/ui/src/main/res/values-en-rGB/strings.xml b/libraries/ui/src/main/res/values-en-rGB/strings.xml index c3a1c8a5aa..af85902797 100644 --- a/libraries/ui/src/main/res/values-en-rGB/strings.xml +++ b/libraries/ui/src/main/res/values-en-rGB/strings.xml @@ -37,11 +37,11 @@ Fast-forward %d second Fast-forward %d seconds - Repeat none - Repeat one - Repeat all - Shuffle on - Shuffle off + Current mode: Repeat none. Toggle repeat mode. + Current mode: Repeat one. Toggle repeat mode. + Current mode: Repeat all. Toggle repeat mode. + Disable shuffle mode + Enable shuffle mode VR mode Disable subtitles Enable subtitles diff --git a/libraries/ui/src/main/res/values-en-rIN/strings.xml b/libraries/ui/src/main/res/values-en-rIN/strings.xml index c3a1c8a5aa..af85902797 100644 --- a/libraries/ui/src/main/res/values-en-rIN/strings.xml +++ b/libraries/ui/src/main/res/values-en-rIN/strings.xml @@ -37,11 +37,11 @@ Fast-forward %d second Fast-forward %d seconds - Repeat none - Repeat one - Repeat all - Shuffle on - Shuffle off + Current mode: Repeat none. Toggle repeat mode. + Current mode: Repeat one. Toggle repeat mode. + Current mode: Repeat all. Toggle repeat mode. + Disable shuffle mode + Enable shuffle mode VR mode Disable subtitles Enable subtitles diff --git a/libraries/ui/src/main/res/values-es-rUS/strings.xml b/libraries/ui/src/main/res/values-es-rUS/strings.xml index faaf430448..0bc0f6c546 100644 --- a/libraries/ui/src/main/res/values-es-rUS/strings.xml +++ b/libraries/ui/src/main/res/values-es-rUS/strings.xml @@ -37,11 +37,11 @@ Adelantar %d segundo Adelantar %d segundos - No repetir - Repetir uno - Repetir todo - Reprod. aleatoria activada - Reprod. aleatoria desactivada + Ahora: no repite. Cambia modo. + Ahora: repetir 1. Cambia modo. + Ahora: rep. todo. Cambia modo. + Inhabilitar reproducción aleatoria + Habilitar reproducción aleatoria Modo RV Inhabilitar subtítulos Habilitar subtítulos diff --git a/libraries/ui/src/main/res/values-es/strings.xml b/libraries/ui/src/main/res/values-es/strings.xml index f333c1d068..924d349038 100644 --- a/libraries/ui/src/main/res/values-es/strings.xml +++ b/libraries/ui/src/main/res/values-es/strings.xml @@ -37,11 +37,11 @@ Avanzar %d segundo Avanzar %d segundos - No repetir - Repetir uno - Repetir todo - Con reproducción aleatoria - Sin reproducción aleatoria + Modo actual: No repetir. Cambiar de modo. + Modo actual: Repetir uno. Cambiar de modo. + Modo actual: Repetir todo. Cambiar de modo. + Inhabilitar modo aleatorio + Habilitar modo aleatorio Modo RV Inhabilitar subtítulos Habilitar subtítulos diff --git a/libraries/ui/src/main/res/values-et/strings.xml b/libraries/ui/src/main/res/values-et/strings.xml index 1f15da226d..faa44e483f 100644 --- a/libraries/ui/src/main/res/values-et/strings.xml +++ b/libraries/ui/src/main/res/values-et/strings.xml @@ -37,11 +37,11 @@ Keri %d sekund edasi Keri %d sekundit edasi - Ära korda ühtegi - Korda ühte - Korda kõiki - Lülita juh. järj. esit. sisse - Lülita juh. järj. esit. välja + Praegune režiim: Ära korda. Aktiveerige kordusrežiim. + Praegune režiim: Korda ühte. Aktiveerige kordusrežiim. + Praegune režiim: Korda kõiki. Aktiveerige kordusrežiim. + Juhuesituse režiimi keelamine + Juhuesituse režiimi lubamine VR-režiim Keela subtiitrid Luba subtiitrid diff --git a/libraries/ui/src/main/res/values-eu/strings.xml b/libraries/ui/src/main/res/values-eu/strings.xml index 9fb7fa9ce0..90441518b1 100644 --- a/libraries/ui/src/main/res/values-eu/strings.xml +++ b/libraries/ui/src/main/res/values-eu/strings.xml @@ -37,11 +37,11 @@ Aurreratu %d segundo Aurreratu %d segundo - Ez errepikatu - Errepikatu bat - Errepikatu guztiak - Ausazko erreprodukzioa aktibatuta - Ausazko erreprodukzioa desaktibatuta + Oraingo modua: ez errepikatu. Aldatu errepikatzeko modua. + Oraingo modua: errepikatu bat. Aldatu errepikatzeko modua. + Oraingo modua: errepikatu guztiak. Aldatu errepikatzeko modua. + Desgaitu ausaz erreproduzitzeko modua + Gaitu ausaz erreproduzitzeko modua EBko modua Desgaitu azpitituluak Gaitu azpitituluak diff --git a/libraries/ui/src/main/res/values-fa/strings.xml b/libraries/ui/src/main/res/values-fa/strings.xml index 5fd0d954ca..c25aa14a7c 100644 --- a/libraries/ui/src/main/res/values-fa/strings.xml +++ b/libraries/ui/src/main/res/values-fa/strings.xml @@ -37,11 +37,11 @@ %d ثانیه سریع به‌جلو بردن %d ثانیه سریع به‌جلو بردن - تکرار هیچ‌کدام - یکبار تکرار - تکرار همه - پخش درهم روشن - پخش درهم خاموش + حالت کنونی: تکرار هیچ‌کدام. روشن/ خاموش کردن حالت تکرار. + حالت کنونی: تکرار یک مورد. روشن/ خاموش کردن حالت تکرار. + حالت کنونی: تکرار همه. روشن/ خاموش کردن حالت تکرار. + غیرفعال کردن حالت درهم + فعال کردن حالت درهم حالت واقعیت مجازی غیرفعال کردن زیرنویس فعال کردن زیرنویس diff --git a/libraries/ui/src/main/res/values-fi/strings.xml b/libraries/ui/src/main/res/values-fi/strings.xml index cc8acd5635..f57744643c 100644 --- a/libraries/ui/src/main/res/values-fi/strings.xml +++ b/libraries/ui/src/main/res/values-fi/strings.xml @@ -37,11 +37,11 @@ Siirry eteenpäin %d sekunti Siirry eteenpäin %d sekuntia - Ei uudelleentoistoa - Toista yksi uudelleen - Toista kaikki uudelleen - Satunnaistoisto käytössä - Satunnaistoisto ei käytössä + Tila: Älä toista. Vaihda. + Tila: Toista yksi. Vaihda. + Tila: Toista kaikki. Vaihda. + Poista satunnaistoisto käytöstä + Ota satunnaistoisto käyttöön VR-tila Poista tekstitykset käytöstä Ota tekstitykset käyttöön diff --git a/libraries/ui/src/main/res/values-fr-rCA/strings.xml b/libraries/ui/src/main/res/values-fr-rCA/strings.xml index fbcc917ee7..5faeb0b5b2 100644 --- a/libraries/ui/src/main/res/values-fr-rCA/strings.xml +++ b/libraries/ui/src/main/res/values-fr-rCA/strings.xml @@ -31,19 +31,17 @@ Reculer de %d seconde Reculer de %d secondes - Reculer de %d secondes Avance rapide Avancer rapidement de %d seconde Avancer rapidement de %d secondes - Avancer rapidement de %d secondes - Ne rien lire en boucle - Lire une chanson en boucle - Tout lire en boucle - Lecture aléatoire activée - Lecture aléatoire désactivée + Mode actuel : aucune répétition. Basculer le mode répétition. + Mode actuel : répéter une fois. Basculer le mode répétition. + Mode actuel : tout répéter. Basculer le mode répétition. + Désactiver le mode lecture aléatoire + Activer le mode lecture aléatoire Mode RV Désactiver les sous-titres Activer les sous-titres diff --git a/libraries/ui/src/main/res/values-fr/strings.xml b/libraries/ui/src/main/res/values-fr/strings.xml index 5c50bbd517..cc876a8bf3 100644 --- a/libraries/ui/src/main/res/values-fr/strings.xml +++ b/libraries/ui/src/main/res/values-fr/strings.xml @@ -31,19 +31,17 @@ Revenir en arrière de %d seconde Revenir en arrière de %d secondes - Revenir en arrière de %d secondes Avance rapide Avancer de %d seconde Avancer de %d secondes - Avancer de %d secondes - Ne rien lire en boucle - Lire un titre en boucle - Tout lire en boucle - Lecture aléatoire activée - Lecture aléatoire désactivée + Mode actuel : sans répétition. Changer le mode répétition. + Mode actuel : une répétition. Changer le mode répétition. + Mode actuel : tout répéter. Changer le mode répétition. + Désactiver le mode aléatoire + Activer le mode aléatoire Mode RV Désactiver les sous-titres Activer les sous-titres diff --git a/libraries/ui/src/main/res/values-gl/strings.xml b/libraries/ui/src/main/res/values-gl/strings.xml index fa815e60b1..932434af28 100644 --- a/libraries/ui/src/main/res/values-gl/strings.xml +++ b/libraries/ui/src/main/res/values-gl/strings.xml @@ -37,11 +37,11 @@ Avanza rapidamente %d segundo Avanza rapidamente %d segundos - Non repetir - Repetir unha pista - Repetir todas as pistas - Reprodución aleatoria activada - Reprodución aleat. desactivada + Modo actual: Non repetir. Alternar modo de repetición. + Modo actual: Repetir unha pista. Alternar modo de repetición. + Modo actual: Repetir todas as pistas. Alternar modo de repetición. + Desactivar modo de reprodución aleatoria + Activar modo de reprodución aleatoria Modo RV Desactiva os subtítulos Activa os subtítulos diff --git a/libraries/ui/src/main/res/values-gu/strings.xml b/libraries/ui/src/main/res/values-gu/strings.xml index b870f1400a..955ccf1f93 100644 --- a/libraries/ui/src/main/res/values-gu/strings.xml +++ b/libraries/ui/src/main/res/values-gu/strings.xml @@ -37,11 +37,11 @@ %d સેકન્ડ ફાસ્ટ ફૉરવર્ડ કરો %d સેકન્ડ ફાસ્ટ ફૉરવર્ડ કરો - કોઈ રિપીટ કરતા નહીં - એક રિપીટ કરો - બધાને રિપીટ કરો - શફલ ચાલુ છે - શફલ બંધ છે + વર્તમાન મોડ: કંઈ રિપીટ ન કરો. રિપીટ મોડ ટૉગલ કરો. + વર્તમાન મોડ: એક રિપીટ કરો. રિપીટ મોડ ટૉગલ કરો. + વર્તમાન મોડ: બધું રિપીટ કરો. રિપીટ મોડ ટૉગલ કરો. + શફલ મોડ બંધ કરો + શફલ મોડ ચાલુ કરો VR મોડ સબટાઇટલ બંધ કરો સબટાઇટલ ચાલુ કરો diff --git a/libraries/ui/src/main/res/values-hi/strings.xml b/libraries/ui/src/main/res/values-hi/strings.xml index c45305429f..fe18f2a16a 100644 --- a/libraries/ui/src/main/res/values-hi/strings.xml +++ b/libraries/ui/src/main/res/values-hi/strings.xml @@ -37,11 +37,11 @@ वीडियो को %d सेकंड आगे बढ़ाएं वीडियो को %d सेकंड आगे बढ़ाएं - किसी को न दोहराएं - एक को दोहराएं - सभी को दोहराएं - शफ़ल करना चालू है - शफ़ल करना बंद है + मौजूदा मोड: किसी को न दोहराएं. रिपीट मोड टॉगल करें. + मौजूदा मोड: एक को दोहराएं. रिपीट मोड टॉगल करें. + मौजूदा मोड: सभी को दोहराएं. रिपीट मोड टॉगल करें. + शफ़ल मोड बंद करें + शफ़ल मोड चालू करें VR मोड सबटाइटल बंद करें सबटाइटल चालू करें diff --git a/libraries/ui/src/main/res/values-hr/strings.xml b/libraries/ui/src/main/res/values-hr/strings.xml index 38f630ac58..320e47aef8 100644 --- a/libraries/ui/src/main/res/values-hr/strings.xml +++ b/libraries/ui/src/main/res/values-hr/strings.xml @@ -39,11 +39,11 @@ Premotavanje unaprijed za %d sekunde Premotavanje unaprijed za %d sekundi - Bez ponavljanja - Ponovi jedno - Ponovi sve - Nasumična reproduk. uključena - Nasumična reproduk. isključena + Trenutačno: Bez ponavljanja. Promijenite način ponavljanja. + Trenutačno: Ponovi jedno. Promijenite način ponavljanja. + Trenutačno: Ponovi sve. Promijenite način ponavljanja. + Onemogućite način nasumične reprodukcije + Omogućite način nasumične reprodukcije VR način Onemogući titlove Omogući titlove diff --git a/libraries/ui/src/main/res/values-hu/strings.xml b/libraries/ui/src/main/res/values-hu/strings.xml index c987398225..7f24082f54 100644 --- a/libraries/ui/src/main/res/values-hu/strings.xml +++ b/libraries/ui/src/main/res/values-hu/strings.xml @@ -37,11 +37,11 @@ Előretekerés %d másodperccel Előretekerés %d másodperccel - Nincs ismétlés - Egy szám ismétlése - Összes szám ismétlése - Keverés bekapcsolva - Keverés kikapcsolva + Jelenlegi: Nincs ismétlés. Ismétlés mód be/ki. + Jelenlegi: Egy ismétlése. Ismétlés mód be/ki. + Jelenlegi: Összes ismétlése. Ismétlés mód be/ki. + Keverés mód kikapcsolása + Keverés mód bekapcsolása VR-mód Feliratok kikapcsolása Feliratok bekapcsolása diff --git a/libraries/ui/src/main/res/values-hy/strings.xml b/libraries/ui/src/main/res/values-hy/strings.xml index 2b8c7a7612..f77b183118 100644 --- a/libraries/ui/src/main/res/values-hy/strings.xml +++ b/libraries/ui/src/main/res/values-hy/strings.xml @@ -37,11 +37,11 @@ %d վայրկյան առաջ տալ %d վայրկյան առաջ տալ - Չկրկնել - Կրկնել մեկը - Կրկնել բոլորը - Խառնումը միացված է - Խառնումն անջատված է + Ընթացիկ ռեժիմը՝ «Չկրկնել»։ Փոխել կրկնության ռեժիմը։ + Ընթացիկ ռեժիմը՝ «Կրկնել մեկը»։ Փոխել կրկնության ռեժիմը։ + Ընթացիկ ռեժիմը՝ «Կրկնել բոլորը»։ Փոխել կրկնության ռեժիմը։ + Անջատել խառը նվագարկման ռեժիմը + Միացնել խառը նվագարկման ռեժիմը VR ռեժիմ Անջատել ենթագրերը Միացնել ենթագրերը diff --git a/libraries/ui/src/main/res/values-in/strings.xml b/libraries/ui/src/main/res/values-in/strings.xml index dd5571b4d5..b010d3c739 100644 --- a/libraries/ui/src/main/res/values-in/strings.xml +++ b/libraries/ui/src/main/res/values-in/strings.xml @@ -37,11 +37,11 @@ Maju cepat %d detik Maju cepat %d detik - Jangan ulangi - Ulangi 1 - Ulangi semua - Acak aktif - Acak tidak aktif + Mode saat ini: Tidak berulang. Alihkan mode berulang. + Mode saat ini: Ulang sekali. Alihkan mode berulang. + Mode saat ini: Ulang semua. Alihkan mode berulang. + Nonaktifkan mode acak + Aktifkan mode acak Mode VR Nonaktifkan subtitel Aktifkan subtitel diff --git a/libraries/ui/src/main/res/values-is/strings.xml b/libraries/ui/src/main/res/values-is/strings.xml index 01f3bcb362..20f603c476 100644 --- a/libraries/ui/src/main/res/values-is/strings.xml +++ b/libraries/ui/src/main/res/values-is/strings.xml @@ -37,11 +37,11 @@ Spóla áfram um %d sekúndu Spóla áfram um %d sekúndur - Endurtaka ekkert - Endurtaka eitt - Endurtaka allt - Kveikt á stokkun - Slökkt á stokkun + Stilling nú: Endurtaka ekkert. Breyta endurtekningastillingu. + Stilling nú: Endurtaka eitt. Breyta endurtekningastillingu. + Stilling nú: Endurtaka allt. Breyta endurtekningastillingu. + Slökkva á stokkun + Kveikja á stokkun sýndarveruleikastilling Slökkva á skjátexta Kveikja á skjátexta diff --git a/libraries/ui/src/main/res/values-it/strings.xml b/libraries/ui/src/main/res/values-it/strings.xml index 8b50689367..b97ae06645 100644 --- a/libraries/ui/src/main/res/values-it/strings.xml +++ b/libraries/ui/src/main/res/values-it/strings.xml @@ -29,19 +29,19 @@ Interrompi Riavvolgi - Indietro di %d secondi + Indietro di %d secondo Indietro di %d secondi Avanti veloce - Avanti di %d secondi + Avanti di %d secondo Avanti di %d secondi - Non ripetere nulla - Ripeti uno - Ripeti tutto - Attiva riproduzione casuale - Disattiva riproduzione casuale + Modalità attuale: Non ripetere nulla. Cambia modalità di ripetizione. + Modalità attuale: Ripeti uno. Cambia modalità di ripetizione. + Modalità attuale: Ripeti tutto. Cambia modalità di ripetizione. + Disattiva la riproduzione casuale + Attiva la riproduzione casuale Modalità VR Disattiva i sottotitoli Attiva i sottotitoli diff --git a/libraries/ui/src/main/res/values-iw/strings.xml b/libraries/ui/src/main/res/values-iw/strings.xml index adbe03100b..c5499212c4 100644 --- a/libraries/ui/src/main/res/values-iw/strings.xml +++ b/libraries/ui/src/main/res/values-iw/strings.xml @@ -13,6 +13,20 @@ See the License for the specific language governing permissions and limitations under the License. --> + הצגת פקדי הנגן הסתרת פקדי הנגן @@ -41,11 +55,11 @@ הרצה %d שניות קדימה הרצה %d שניות קדימה - אל תחזור על אף פריט - חזור על פריט אחד - חזור על הכול - ההשמעה האקראית מופעלת - ההשמעה האקראית מושבתת + המצב הנוכחי: ללא חזרה. לחצן להחלפת מצב החזרה. + המצב הנוכחי: חזרה על פריט אחד. לחצן להחלפת מצב החזרה. + המצב הנוכחי: חזרה על כל הפריטים. לחצן להחלפת מצב החזרה. + השבתת מצב ההפעלה האקראית + הפעלת מצב ההפעלה האקראית מצב VR השבתת כתוביות הפעלת כתוביות diff --git a/libraries/ui/src/main/res/values-ja/strings.xml b/libraries/ui/src/main/res/values-ja/strings.xml index 7333b977d4..f8970f64ee 100644 --- a/libraries/ui/src/main/res/values-ja/strings.xml +++ b/libraries/ui/src/main/res/values-ja/strings.xml @@ -37,11 +37,11 @@ %d 秒早送り %d 秒早送り - リピートなし - 1 曲をリピート - 全曲をリピート - シャッフル ON - シャッフル OFF + 現在: リピートなし。モードを切り替えます。 + 現在: 1 曲リピート。モードを切り替えます。 + 現在: 全曲リピート。モードを切り替えます。 + シャッフル モードを無効にする + シャッフル モードを有効にする VR モード 字幕の無効化 字幕の有効化 diff --git a/libraries/ui/src/main/res/values-ka/strings.xml b/libraries/ui/src/main/res/values-ka/strings.xml index a18089e129..2ff75b72dd 100644 --- a/libraries/ui/src/main/res/values-ka/strings.xml +++ b/libraries/ui/src/main/res/values-ka/strings.xml @@ -37,11 +37,11 @@ წინ გადახვევა %d წამით წინ გადახვევა %d წამით - არცერთის გამეორება - ერთის გამეორება - ყველას გამეორება - არეულად დაკვრა ჩართულია - არეულად დაკვრა გამორთულია + მიმდინარე რეჟიმი: არაფრის გამეორება. გადართეთ გამეორების რეჟიმი. + მიმდინარე რეჟიმი: ერთის გამეორება. გადართეთ გამეორების რეჟიმი. + მიმდინარე რეჟიმი: ყველას გამეორება. გადართეთ გამეორების რეჟიმი. + არეულად დაკვრის რეჟიმის გათიშვა + არეულად დაკვრის რეჟიმის ჩართვა VR რეჟიმი სუბტიტრების გათიშვა სუბტიტრების ჩართვა diff --git a/libraries/ui/src/main/res/values-kk/strings.xml b/libraries/ui/src/main/res/values-kk/strings.xml index be14948ef1..86d41df02b 100644 --- a/libraries/ui/src/main/res/values-kk/strings.xml +++ b/libraries/ui/src/main/res/values-kk/strings.xml @@ -37,11 +37,11 @@ %d секунд алға айналдыру %d секунд алға айналдыру - Ешқайсысын қайталамау - Біреуін қайталау - Барлығын қайталау - Араластыру режимі қосулы. - Араластыру режимі өшірулі. + Қазіргі режим: еш мазмұн қайталанбайды. Қайталау режимін ауыстыру. + Қазіргі режим: бір мазмұн қайталанады. Қайталау режимін ауыстыру. + Қазіргі режим: барлық мазмұн қайталанады Қайталау режимін ауыстыру. + Араластыру режимін өшіру + Араластыру режимін қосу VR режимі Субтитрді өшіру Субтитрді қосу diff --git a/libraries/ui/src/main/res/values-km/strings.xml b/libraries/ui/src/main/res/values-km/strings.xml index 4e1f62bd90..3e8fd004a1 100644 --- a/libraries/ui/src/main/res/values-km/strings.xml +++ b/libraries/ui/src/main/res/values-km/strings.xml @@ -37,11 +37,11 @@ ខា​ទៅមុខ %d វិនាទី ខា​ទៅមុខ %d វិនាទី - មិន​លេង​ឡើងវិញ - លេង​ឡើង​វិញ​ម្ដង - លេង​ឡើងវិញ​ទាំងអស់ - បើក​ការ​ច្របល់ - បិទ​ការ​ច្របល់ + មុខងារបច្ចុប្បន្ន៖ មិនចាក់ឡើងវិញ។ បិទ/បើកមុខងារចាក់ឡើងវិញ។ + មុខងារបច្ចុប្បន្ន៖ ចាក់ឡើងវិញមួយ។ បិទ/បើកមុខងារចាក់ឡើងវិញ។ + មុខងារបច្ចុប្បន្ន៖ ចាក់ឡើងវិញទាំងអស់។ បិទ/បើកមុខងារចាក់ឡើងវិញ។ + បិទ​មុខងារច្របល់ + បើក​មុខងារច្របល់ មុខងារ VR បិទអក្សររត់ បើកអក្សររត់ diff --git a/libraries/ui/src/main/res/values-kn/strings.xml b/libraries/ui/src/main/res/values-kn/strings.xml index 28c9496216..45b2417f5b 100644 --- a/libraries/ui/src/main/res/values-kn/strings.xml +++ b/libraries/ui/src/main/res/values-kn/strings.xml @@ -37,11 +37,11 @@ %d ಸೆಕೆಂಡ್‌ಗಳಷ್ಟು ಫಾಸ್ಟ್ ಫಾರ್ವರ್ಡ್ ಮಾಡಿ %d ಸೆಕೆಂಡ್‌ಗಳಷ್ಟು ಫಾಸ್ಟ್ ಫಾರ್ವರ್ಡ್ ಮಾಡಿ - ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ - ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ - ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ - ಶಫಲ್ ಆನ್ ಆಗಿದೆ - ಶಫಲ್ ಆಫ್ ಆಗಿದೆ + ಈಗಿನ ಮೋಡ್: ಯಾವುದನ್ನು ರಿಪೀಟ್ ಮಾಡಬೇಡಿ. ರಿಪೀಟ್ ಮೋಡ್ ಟಾಗಲ್ ಮಾಡಿ. + ಈಗಿನ ಮೋಡ್: ಒಂದನ್ನು ರಿಪೀಟ್ ಮಾಡಿ. ರಿಪೀಟ್ ಮೋಡ್ ಟಾಗಲ್ ಮಾಡಿ. + ಈಗಿನ ಮೋಡ್: ಎಲ್ಲವನ್ನು ರಿಪೀಟ್ ಮಾಡಿ. ರಿಪೀಟ್ ಮೋಡ್ ಟಾಗಲ್ ಮಾಡಿ. + ಶಫಲ್ ಮೋಡ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ + ಶಫಲ್ ಮೋಡ್ ಅನ್ನುಸಕ್ರಿಯಗೊಳಿಸಿ VR ಮೋಡ್ ಸಬ್‌ಟೈಟಲ್‌ಗಳನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ ಸಬ್‌ಟೈಟಲ್‌ಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ @@ -56,7 +56,7 @@ 2 ಪಟ್ಟು ವೀಡಿಯೊ - ಆಡಿಯೊ + ಆಡಿಯೋ ಪಠ್ಯ ಯಾವುದೂ ಅಲ್ಲ ಸ್ವಯಂ diff --git a/libraries/ui/src/main/res/values-ko/strings.xml b/libraries/ui/src/main/res/values-ko/strings.xml index 0c06366b57..b6e8d231ea 100644 --- a/libraries/ui/src/main/res/values-ko/strings.xml +++ b/libraries/ui/src/main/res/values-ko/strings.xml @@ -37,11 +37,11 @@ %d초 빨리 감기 %d초 빨리 감기 - 반복 안함 - 현재 미디어 반복 - 모두 반복 - 셔플 사용 - 셔플 사용 안함 + 현재 모드: 반복 안함. 반목 모드 전환. + 현재 모드: 한 개 반복. 반목 모드 전환. + 현재 모드: 모두 반복. 반목 모드 전환. + 셔플 모드 사용 중지 + 셔플 모드 사용 설정 가상 현실 모드 자막 사용 중지 자막 사용 설정 diff --git a/libraries/ui/src/main/res/values-ky/strings.xml b/libraries/ui/src/main/res/values-ky/strings.xml index 4cb3932247..29601c0253 100644 --- a/libraries/ui/src/main/res/values-ky/strings.xml +++ b/libraries/ui/src/main/res/values-ky/strings.xml @@ -37,11 +37,11 @@ %d секунд алдыга түрдүрүү %d секунд алдыга түрдүрүү - Кайталанбасын - Бирөөнү кайталоо - Баарын кайталоо - Аралаштыруу күйүк - Аралаштыруу өчүк + Учурдагы режим: Кайталанбасын. Кайталоо режимин өчүрүү/күйгүзүү. + Учурдагы режим: Бир медианы кайталоо. Кайталоо режимин өчүрүү/күйгүзүү. + Учурдагы режим: Баарын кайталоо. Кайталоо режимин өчүрүү/күйгүзүү. + Аралаштыруу режимин өчүрүү + Аралаштыруу режимин күйгүзүү VR режими Коштомо жазууларды өчүрүү Коштомо жазууларды иштетүү diff --git a/libraries/ui/src/main/res/values-lo/strings.xml b/libraries/ui/src/main/res/values-lo/strings.xml index 8581b0bbc0..bd0f103425 100644 --- a/libraries/ui/src/main/res/values-lo/strings.xml +++ b/libraries/ui/src/main/res/values-lo/strings.xml @@ -37,11 +37,11 @@ ເລື່ອນໄປໜ້າ %d ວິນາທີ ເລື່ອນໄປໜ້າ %d ວິນາທີ - ບໍ່ຫຼິ້ນຊ້ຳ - ຫຼິ້ນຊໍ້າ - ຫຼິ້ນຊ້ຳທັງໝົດ - ເປີດການສຸ່ມເພງແລ້ວ - ປິດການສຸ່ມເພງແລ້ວ + ໂໝດປັດຈຸບັນ: ບໍ່ເປີດຊ້ຳ. ສະຫຼັບໂໝດເປີດຊ້ຳ. + ໂໝດປັດຈຸບັນ: ເປີດຊ້ຳອັນດຽວ. ສະຫຼັບໂໝດເປີດຊ້ຳ. + ໂໝດປັດຈຸບັນ: ເປີດຊ້ຳທັງໝົດ. ສະຫຼັບໂໝດເປີດຊ້ຳ. + ປິດການນຳໃຊ້ໂໝດສັບ + ເປີດການນຳໃຊ້ໂໝດສັບ ໂໝດ VR ປິດການນຳໃຊ້ຄຳແປ ເປີດການນຳໃຊ້ຄຳແປ diff --git a/libraries/ui/src/main/res/values-lt/strings.xml b/libraries/ui/src/main/res/values-lt/strings.xml index b8233d76a9..f1d221e9bf 100644 --- a/libraries/ui/src/main/res/values-lt/strings.xml +++ b/libraries/ui/src/main/res/values-lt/strings.xml @@ -41,11 +41,11 @@ Prasukti pirmyn %d sekundės Prasukti pirmyn %d sekundžių - Nekartoti nieko - Kartoti vieną - Kartoti viską - Maišymas įjungtas - Maišymas išjungtas + Dabart. rež.: nekart. nieko. Perj. pakart. r. + Dabart. rež.: kart. vieną. Perj. pakart. r. + Dabart. rež.: kart. viską. Perj. pakart. r. + Išjungti maišymo režimą + Įgalinti maišymo režimą VR režimas Išjungti subtitrus Įgalinti subtitrus diff --git a/libraries/ui/src/main/res/values-lv/strings.xml b/libraries/ui/src/main/res/values-lv/strings.xml index 61f1c8f4dd..0d0ec51c3c 100644 --- a/libraries/ui/src/main/res/values-lv/strings.xml +++ b/libraries/ui/src/main/res/values-lv/strings.xml @@ -39,11 +39,11 @@ Pārtīt uz priekšu par %d sekundi Pārtīt uz priekšu par %d sekundēm - Neatkārtot nevienu - Atkārtot vienu - Atkārtot visu - Atsk. jauktā secībā ieslēgta - Atsk. jauktā secībā izslēgta + Iesl.: neatkārtot nevienu. Pārslēdziet režīmu. + Iesl.: atkārtot vienu. Pārslēdziet režīmu. + Iesl.: atkārtot visu. Pārslēdziet režīmu. + Atspējot atskaņošanu jauktā secībā + Iespējot atskaņošanu jauktā secībā VR režīms Atspējot subtitrus Iespējot subtitrus diff --git a/libraries/ui/src/main/res/values-mk/strings.xml b/libraries/ui/src/main/res/values-mk/strings.xml index 8a4ff03039..a5d13326e4 100644 --- a/libraries/ui/src/main/res/values-mk/strings.xml +++ b/libraries/ui/src/main/res/values-mk/strings.xml @@ -37,11 +37,11 @@ Премотајте напред %d секунда Премотајте напред %d секунди - Не повторувај ниту една - Повтори една - Повтори ги сите - Мешањето е вклучено - Мешањето е исклучено + Тековно: не повторувај. Вклучи/исклучи повторување. + Тековно: повтори една. Вклучи/исклучи повторување. + Тековно: повтори ги сите. Вклучи/исклучи повторување. + Оневозможи режим на мешање + Овозможи режим на мешање Режим на VR Оневозможете ги титловите Овозможете ги титловите diff --git a/libraries/ui/src/main/res/values-ml/strings.xml b/libraries/ui/src/main/res/values-ml/strings.xml index 12de370917..4d1e4e995a 100644 --- a/libraries/ui/src/main/res/values-ml/strings.xml +++ b/libraries/ui/src/main/res/values-ml/strings.xml @@ -37,11 +37,11 @@ വേഗത്തിൽ %d സെക്കൻഡ് മുന്നോട്ട് നീക്കുക വേഗത്തിൽ %d സെക്കൻഡ് മുന്നോട്ട് നീക്കുക - ഒന്നും ആവർത്തിക്കരുത് - ഒരെണ്ണം ആവർത്തിക്കുക - എല്ലാം ആവർത്തിക്കുക - ഇടകലർത്തൽ ഓണാക്കുക - ഇടകലർത്തൽ ഓഫാക്കുക + നിലവിലെ മോഡ്: ഒന്നും ആവർത്തിക്കരുത്. ആവർത്തന മോഡ് മാറ്റൂ. + നിലവിലെ മോഡ്: ഒന്ന് ആവർത്തിക്കുക. ആവർത്തന മോഡ് മാറ്റൂ. + നിലവിലെ മോഡ്: എല്ലാം ആവർത്തിക്കുക. ആവർത്തന മോഡ് മാറ്റൂ. + ഷഫിൾ മോഡ് പ്രവർത്തനരഹിതമാക്കുക + ഷഫിൾ മോഡ് പ്രവർത്തനക്ഷമമാക്കുക VR മോഡ് സബ്‌ടൈറ്റിലുകൾ പ്രവർത്തനരഹിതമാക്കുക സബ്‌ടൈറ്റിലുകൾ പ്രവർത്തനക്ഷമമാക്കുക diff --git a/libraries/ui/src/main/res/values-mn/strings.xml b/libraries/ui/src/main/res/values-mn/strings.xml index e9fbf7fe53..56273ce0e5 100644 --- a/libraries/ui/src/main/res/values-mn/strings.xml +++ b/libraries/ui/src/main/res/values-mn/strings.xml @@ -37,11 +37,11 @@ %d секундээр хурдан урагшлуулах %d секундээр хурдан урагшлуулах - Алийг нь ч дахин тоглуулахгүй - Одоогийн тоглуулж буй медиаг дахин тоглуулах - Бүгдийг нь дахин тоглуулах - Холих асаалттай - Холих унтраалттай + Одоогийн горим: Алийг нь ч дахин тоглуулахгүй. Дахин тоглуулах горимыг асаана уу/унтраана уу. + Одоогийн горим: Нэгийг дахин тоглуулна уу. Дахин тоглуулах горимыг асаана уу/унтраана уу. + Одоогийн горим: Бүгдийг дахин тоглуулна уу. Дахин тоглуулах горимыг асаана уу/унтраана уу. + Холих горимыг идэвхгүй болгох + Холих горимыг идэвхжүүлэх VR горим Хадмалыг идэвхгүй болгох Хадмалыг идэвхжүүлэх diff --git a/libraries/ui/src/main/res/values-mr/strings.xml b/libraries/ui/src/main/res/values-mr/strings.xml index 5641af2a47..303f5ca52f 100644 --- a/libraries/ui/src/main/res/values-mr/strings.xml +++ b/libraries/ui/src/main/res/values-mr/strings.xml @@ -37,11 +37,11 @@ %d सेकंद फास्ट फॉरवर्ड करा %d सेकंद फास्ट फॉरवर्ड करा - रीपीट करू नका - एक रीपीट करा - सर्व रीपीट करा - शफल करा सुरू करा - शफल करा बंद करा + सध्याचा मोड: काहीही रिपीट करू नका. रिपीट मोड टॉगल करा. + सध्याचा मोड: एक रिपीट करा. रिपीट मोड टॉगल करा. + सध्याचा मोड: सर्व रिपीट करा. रिपीट मोड टॉगल करा. + शफल मोड बंद करा + शफल मोड सुरू करा VR मोड सबटायटल बंद करा सबटायटल सुरू करा diff --git a/libraries/ui/src/main/res/values-ms/strings.xml b/libraries/ui/src/main/res/values-ms/strings.xml index 60cce968c2..2758851781 100644 --- a/libraries/ui/src/main/res/values-ms/strings.xml +++ b/libraries/ui/src/main/res/values-ms/strings.xml @@ -37,11 +37,11 @@ Mundar laju %d saat Mundar laju %d saat - Jangan ulang - Ulang satu - Ulang semua - Hidupkan rombak - Matikan rombak + Mod semasa: Tidak berulang. Togol mod ulang. + Mod semasa: Ulang satu. Togol mod ulang. + Mod semasa: Ulang semua. Togol mod ulang. + Lumpuhkan mod rombak + Dayakan mod rombak Mod VR Lumpuhkan sari kata Dayakan sari kata diff --git a/libraries/ui/src/main/res/values-my/strings.xml b/libraries/ui/src/main/res/values-my/strings.xml index 07f0ad05ff..89ad8e5636 100644 --- a/libraries/ui/src/main/res/values-my/strings.xml +++ b/libraries/ui/src/main/res/values-my/strings.xml @@ -37,11 +37,11 @@ ရှေ့သို့ %d စက္ကန့်ရစ်ရန် ရှေ့သို့ %d စက္ကန့်ရစ်ရန် - မည်သည်ကိုမျှ ပြန်မကျော့ရန် - တစ်ခုကို ပြန်ကျော့ရန် - အားလုံး ပြန်ကျော့ရန် - ရောသမမွှေကို ဖွင့်ထားသည် - ရောသမမွှေကို ပိတ်ထားသည် + လက်ရှိမုဒ်- ပြန်မကျော့ပါနှင့်။ ပြန်ကျော့မုဒ် ပြောင်းရန်။ + လက်ရှိမုဒ်- တစ်ခုပြန်ကျော့ရန်။ ပြန်ကျော့မုဒ် ပြောင်းရန်။ + လက်ရှိမုဒ်- အားလုံးပြန်ကျော့ရန်။ ပြန်ကျော့မုဒ် ပြောင်းရန်။ + ရောသမမွှေမုဒ် ပိတ်ရန် + ရောသမမွှေမုဒ် ဖွင့်ရန် VR မုဒ် စာတန်းထိုး ပိတ်ပါ စာတန်းထိုး ဖွင့်ပါ diff --git a/libraries/ui/src/main/res/values-nb/strings.xml b/libraries/ui/src/main/res/values-nb/strings.xml index 03a2216962..075270b6dc 100644 --- a/libraries/ui/src/main/res/values-nb/strings.xml +++ b/libraries/ui/src/main/res/values-nb/strings.xml @@ -37,11 +37,11 @@ Spol %d sekund fremover Spol %d sekunder fremover - Ikke gjenta noen - Gjenta én - Gjenta alle - Tilfeldig rekkefølge er på - Tilfeldig rekkefølge er av + Modus: gjenta ingen. Endre gjentakelsesmodus. + Modus: gjenta én. Endre gjentakelsesmodus. + Modus: gjenta alle. Endre gjentakelsesmodus. + Slå av modus for tilfeldig rekkefølge + Slå på modus for tilfeldig rekkefølge VR-modus Slå av undertekstene Slå på undertekstene diff --git a/libraries/ui/src/main/res/values-ne/strings.xml b/libraries/ui/src/main/res/values-ne/strings.xml index 2806463f0b..586aea713f 100644 --- a/libraries/ui/src/main/res/values-ne/strings.xml +++ b/libraries/ui/src/main/res/values-ne/strings.xml @@ -37,11 +37,11 @@ %d सेकेन्ड फास्ट फर्वार्ड गर्नुहोस् %d सेकेन्ड फास्ट फर्वार्ड गर्नुहोस् - कुनै पनि नदोहोर्‍याउनुहोस् - एउटा दोहोर्‍याउनुहोस् - सबै दोहोर्‍याउनुहोस् - मिसाउने सुविधा सक्रिय छ - मिसाउने सुविधा निष्क्रिय छ + हालको मोड: नदोहोरिने। दोहोरिने मोड टगल गर्नुहोस्। + हालको मोड: एक पटक दोहोरिने। दोहोरिने मोड टगल गर्नुहोस्। + हालको मोड: सबै दोहोरिने। दोहोरिने मोड टगल गर्नुहोस्। + सफल मोड अफ गर्नुहोस् + सफल मोड अन गर्नुहोस् VR मोड सबटाइटलहरू असक्षम पार्नुहोस् सबटाइटलहरू सक्षम पार्नुहोस् diff --git a/libraries/ui/src/main/res/values-nl/strings.xml b/libraries/ui/src/main/res/values-nl/strings.xml index 0b7c590443..eaf3659200 100644 --- a/libraries/ui/src/main/res/values-nl/strings.xml +++ b/libraries/ui/src/main/res/values-nl/strings.xml @@ -37,11 +37,11 @@ %d seconde vooruitspoelen %d seconden vooruitspoelen - Niets herhalen - Eén herhalen - Alles herhalen - Shuffle aan - Shuffle uit + Huidige modus: Niets herhalen. Herhaalmodus schakelen. + Huidige modus: 1 herhalen. Herhaalmodus schakelen. + Huidige modus: Alles herhalen. Herhaalmodus schakelen. + Shuffle uitzetten + Shuffle aanzetten VR-modus Ondertiteling uitzetten Ondertiteling aanzetten diff --git a/libraries/ui/src/main/res/values-pa/strings.xml b/libraries/ui/src/main/res/values-pa/strings.xml index efe5b908ab..e2579cd646 100644 --- a/libraries/ui/src/main/res/values-pa/strings.xml +++ b/libraries/ui/src/main/res/values-pa/strings.xml @@ -37,11 +37,11 @@ ਤੇਜ਼ੀ ਨਾਲ %d ਸਕਿੰਟ ਅੱਗੇ ਕਰੋ ਤੇਜ਼ੀ ਨਾਲ %d ਸਕਿੰਟ ਅੱਗੇ ਕਰੋ - ਕਿਸੇ ਨੂੰ ਨਾ ਦੁਹਰਾਓ - ਇੱਕ ਵਾਰ ਦੁਹਰਾਓ - ਸਾਰਿਆਂ ਨੂੰ ਦੁਹਰਾਓ - \'ਬੇਤਰਤੀਬ ਕਰੋ\' ਮੋਡ ਚਾਲੂ ਹੈ - \'ਬੇਤਰਤੀਬ ਕਰੋ\' ਮੋਡ ਬੰਦ ਹੈ + ਮੌਜੂਦਾ ਮੋਡ: ਕਿਸੇ ਨੂੰ ਨਾ ਦੁਹਰਾਓ। ਦੁਹਰਾਓ ਮੋਡ ਟੌਗਲ ਕਰੋ। + ਮੌਜੂਦਾ ਮੋਡ: ਇੱਕ ਨੂੰ ਦੁਹਰਾਓ। ਦੁਹਰਾਓ ਮੋਡ ਟੌਗਲ ਕਰੋ। + ਮੌਜੂਦਾ ਮੋਡ: ਸਭ ਦੁਹਰਾਓ। ਦੁਹਰਾਓ ਮੋਡ ਟੌਗਲ ਕਰੋ। + ਬੇਤਰਤੀਬ ਮੋਡ ਬੰਦ ਕਰੋ + ਬੇਤਰਤੀਬ ਮੋਡ ਚਾਲੂ ਕਰੋ VR ਮੋਡ ਉਪਸਿਰਲੇਖਾਂ ਨੂੰ ਬੰਦ ਕਰੋ ਉਪਸਿਰਲੇਖਾਂ ਨੂੰ ਚਾਲੂ ਕਰੋ diff --git a/libraries/ui/src/main/res/values-pl/strings.xml b/libraries/ui/src/main/res/values-pl/strings.xml index f12915791b..ec5bfca158 100644 --- a/libraries/ui/src/main/res/values-pl/strings.xml +++ b/libraries/ui/src/main/res/values-pl/strings.xml @@ -41,11 +41,11 @@ Przewiń do przodu o %d sekund Przewiń do przodu o %d sekundy - Nie powtarzaj - Powtórz jeden - Powtórz wszystkie - Włącz odtwarzanie losowe - Wyłącz odtwarzanie losowe + Bieżący tryb: Nie powtarzaj. Przełącz na tryb powtarzania. + Bieżący tryb: Powtórz jeden. Przełącz na tryb powtarzania. + Bieżący tryb: Powtórz wszystkie. Przełącz na tryb powtarzania. + Wyłącz tryb odtwarzania losowego + Włącz tryb odtwarzania losowego Tryb VR Wyłącz napisy Włącz napisy diff --git a/libraries/ui/src/main/res/values-pt-rPT/strings.xml b/libraries/ui/src/main/res/values-pt-rPT/strings.xml index f791718436..6e70f79c53 100644 --- a/libraries/ui/src/main/res/values-pt-rPT/strings.xml +++ b/libraries/ui/src/main/res/values-pt-rPT/strings.xml @@ -29,19 +29,19 @@ Parar Recuar - Retroceder %d segundo(s) + Retroceder %d segundo Retroceder %d segundos Avançar - Avançar %d segundo(s) + Avançar %d segundo Avançar %d segundos - Não repetir nenhum - Repetir um - Repetir tudo - Reprodução aleatória ativada - Reprodução aleatória desativ. + Modo atual: não repetir. Ative/des. repetição. + Modo atual: repetir um. Ative/des. repetição. + Modo atual: repet. tudo. Ative/des. repetição. + Desativar modo aleatório + Ativar modo aleatório Modo de RV Desativar legendas Ativar legendas diff --git a/libraries/ui/src/main/res/values-pt/strings.xml b/libraries/ui/src/main/res/values-pt/strings.xml index af04650a35..80a7f42669 100644 --- a/libraries/ui/src/main/res/values-pt/strings.xml +++ b/libraries/ui/src/main/res/values-pt/strings.xml @@ -37,11 +37,11 @@ Avançar %d segundo Avançar %d segundos - Não repetir - Repetir uma - Repetir tudo - Ordem aleatória ativada - Ordem aleatória desativada + Modo atual: não repetir. Alternar modo de repetição. + Modo atual: repetir um item. Alternar modo de repetição. + Modo atual: repetir tudo. Alternar modo de repetição. + Desativar o modo de ordem aleatória + Ativar o modo de ordem aleatória Modo RV Desativar legendas Ativar legendas diff --git a/libraries/ui/src/main/res/values-ro/strings.xml b/libraries/ui/src/main/res/values-ro/strings.xml index db77aa55d1..0448518b56 100644 --- a/libraries/ui/src/main/res/values-ro/strings.xml +++ b/libraries/ui/src/main/res/values-ro/strings.xml @@ -39,11 +39,11 @@ Derulează rapid înainte cu %d secunde Derulează rapid înainte cu %d de secunde - Nu repetați niciunul - Repetați unul - Repetați-le pe toate - Redare aleatorie activată - Redare aleatorie dezactivată + Mod actual: nu se repetă. Comutați modul repetare. + Mod actual: se repetă una. Comutați modul repetare. + Mod curent: se repetă tot. Comutați modul repetare. + Dezactivați modul sortare aleatorie + Activați modul sortare aleatorie Mod RV Dezactivați subtitrările Activați subtitrările diff --git a/libraries/ui/src/main/res/values-ru/strings.xml b/libraries/ui/src/main/res/values-ru/strings.xml index 542517da8b..36d9b4c01e 100644 --- a/libraries/ui/src/main/res/values-ru/strings.xml +++ b/libraries/ui/src/main/res/values-ru/strings.xml @@ -41,11 +41,11 @@ Перемотать на %d секунд вперед Перемотать на %d секунды вперед - Не повторять - Повторять трек - Повторять все - Перемешивание включено - Перемешивание отключено + Выбрано: не повторять. Изменить режим повтора. + Выбрано: повторять один раз. Изменить режим повтора. + Выбрано: повторять все. Изменить режим повтора. + Отключить перемешивание + Включить перемешивание VR-режим Выключить субтитры Включить субтитры diff --git a/libraries/ui/src/main/res/values-si/strings.xml b/libraries/ui/src/main/res/values-si/strings.xml index 2f56ca1699..a78f859718 100644 --- a/libraries/ui/src/main/res/values-si/strings.xml +++ b/libraries/ui/src/main/res/values-si/strings.xml @@ -37,11 +37,11 @@ තත්පර %dක් වේගයෙන් ඉදිරියට තත්පර %dක් වේගයෙන් ඉදිරියට - කිසිවක් පුනරාවර්තනය නොකරන්න - එකක් පුනරාවර්තනය කරන්න - සියල්ල පුනරාවර්තනය කරන්න - කලවම් කිරීම ක්‍රියාත්මකයි - කලවම් කිරීම ක්‍රියා විරහිතයි + වත්මන් ප්‍රකාරය: කිසිවක් පුනරාවර්තනය නොකරන්න. පුනරාවර්තන ප්‍රකාරය ටොගල කරන්න. + වත්මන් ප්‍රකාරය: එකක් පුනරාවර්තනය කරන්න. පුනරාවර්තන ප්‍රකාරය ටොගල කරන්න. + වත්මන් ප්‍රකාරය: සියල්ල පුනරාවර්තනය කරන්න. පුනරාවර්තන ප්‍රකාරය ටොගල කරන්න. + කළවම් ප්‍රකාරය අබල කරන්න + කළවම් ප්‍රකාරය සබල කරන්න VR ප්‍රකාරය උපසිරැසි අබල කරන්න උපසිරැසි සබල කරන්න diff --git a/libraries/ui/src/main/res/values-sk/strings.xml b/libraries/ui/src/main/res/values-sk/strings.xml index 4f365fb034..75204eed52 100644 --- a/libraries/ui/src/main/res/values-sk/strings.xml +++ b/libraries/ui/src/main/res/values-sk/strings.xml @@ -41,11 +41,11 @@ Pretočiť dopredu o %d sekundy Pretočiť dopredu o %d sekúnd - Neopakovať - Opakovať jednu - Opakovať všetko - Náhodné prehrávanie je zapnuté - Náhodné prehrávanie je vypnuté + Aktuálny režim: Neopakovať. Prepnúť režim opakovania + Aktuálny režim: Opakovať jedno. Prepnúť režim opakovania + Aktuálny režim: Opakovať všetky. Prepnúť režim opakovania + Vypnúť režim náhodného prehrávania + Zapnúť režim náhodného prehrávania režim VR Zakázať titulky Povoliť titulky diff --git a/libraries/ui/src/main/res/values-sl/strings.xml b/libraries/ui/src/main/res/values-sl/strings.xml index 1ddc0ba757..5bc58e7ae8 100644 --- a/libraries/ui/src/main/res/values-sl/strings.xml +++ b/libraries/ui/src/main/res/values-sl/strings.xml @@ -41,11 +41,11 @@ Premik naprej za %d sekunde Premik naprej za %d sekund - Brez ponavljanja - Ponavljanje ene - Ponavljanje vseh - Naklj. predvajanje vklopljeno - Naklj. predvajanje izklopljeno + Trenutni način: Brez ponavljanja. Preklopite način ponavljanja. + Trenutni način: Ponovi eno. Preklopite način ponavljanja. + Trenutni način: Ponovi vse. Preklopite način ponavljanja. + Onemogočanje načina naključnega predvajanja + Omogočanje načina naključnega predvajanja Način VR Onemogočanje podnapisov Omogočanje podnapisov diff --git a/libraries/ui/src/main/res/values-sq/strings.xml b/libraries/ui/src/main/res/values-sq/strings.xml index 59941e5e8e..0357753ac8 100644 --- a/libraries/ui/src/main/res/values-sq/strings.xml +++ b/libraries/ui/src/main/res/values-sq/strings.xml @@ -37,11 +37,11 @@ Shpejt përpara %d sekondë Shpejt përpara %d sekonda - Mos përsërit asnjë - Përsërit një - Përsërit të gjitha - Përzierja aktive - Përzierja joaktive + Modaliteti aktual: Mos përsërit asnjë. Aktivizo/çaktivizo modalitetin e përsëritjes. + Modaliteti aktual: Përsërit një. Aktivizo/çaktivizo modalitetin e përsëritjes. + Modaliteti aktual: Përsërit të gjitha. Aktivizo/çaktivizo modalitetin e përsëritjes. + Çaktivizo modalitetin e përzierjes + Aktivizo modalitetin e përzierjes Modaliteti RV Çaktivizo titrat Aktivizo titrat diff --git a/libraries/ui/src/main/res/values-sr/strings.xml b/libraries/ui/src/main/res/values-sr/strings.xml index c748764e9b..d20d287b1c 100644 --- a/libraries/ui/src/main/res/values-sr/strings.xml +++ b/libraries/ui/src/main/res/values-sr/strings.xml @@ -39,11 +39,11 @@ Премотајте %d секунде унапред Премотајте %d секунди унапред - Не понављај ниједну - Понови једну - Понови све - Насумично пуштање је укључено - Насумично пуштање је искључено + Ваш режим: Без понављања. Укључите/искључите. + Ваш режим: Понови једно. Укључите/искључите. + Ваш режим: Понови све. Укључите/искључите. + Онемогућите насумични режим + Омогућите насумични режим ВР режим Онемогући титлове Омогући титлове diff --git a/libraries/ui/src/main/res/values-sv/strings.xml b/libraries/ui/src/main/res/values-sv/strings.xml index bb23c56b56..73edb04ff0 100644 --- a/libraries/ui/src/main/res/values-sv/strings.xml +++ b/libraries/ui/src/main/res/values-sv/strings.xml @@ -37,11 +37,11 @@ Spola framåt %d sekund Spola framåt %d sekunder - Upprepa inga - Upprepa en - Upprepa alla - Blanda spår - Blanda inte spår + Aktuellt läge: Upprepa inga. Ändra läge för upprepning. + Aktuellt läge: Upprepa en. Ändra läge för upprepning. + Aktuellt läge: Upprepa alla. Ändra läge för upprepning. + Inaktivera blandningsläget + Aktivera blandningsläget VR-läge Inaktivera undertexter Aktivera undertexter diff --git a/libraries/ui/src/main/res/values-sw/strings.xml b/libraries/ui/src/main/res/values-sw/strings.xml index c594838faf..015e31701d 100644 --- a/libraries/ui/src/main/res/values-sw/strings.xml +++ b/libraries/ui/src/main/res/values-sw/strings.xml @@ -37,11 +37,11 @@ Sogeza mbele haraka kwa sekunde %d Sogeza mbele haraka kwa sekunde %d - Usirudie yoyote - Rudia moja - Rudia zote - Hali ya kuchanganya imewashwa - Hali ya kuchanganya imezimwa + Hali ya sasa: Usirudie yoyote Geuza hali ya kurudia. + Hali ya sasa: Rudia moja. Geuza hali ya kurudia. + Hali ya sasa: Rudia zote. Geuza hali ya kurudia. + Zima hali ya kuchanganya + Washa hali ya kuchanganya Hali ya Uhalisia Pepe Zima manukuu Washa manukuu diff --git a/libraries/ui/src/main/res/values-ta/strings.xml b/libraries/ui/src/main/res/values-ta/strings.xml index d107f42a10..36172865b9 100644 --- a/libraries/ui/src/main/res/values-ta/strings.xml +++ b/libraries/ui/src/main/res/values-ta/strings.xml @@ -37,14 +37,14 @@ %d வினாடி ஃபாஸ்ட் ஃபார்வர்ட் ஆகும் %d வினாடிகள் ஃபாஸ்ட் ஃபார்வர்ட் ஆகும் - எதையும் மீண்டும் இயக்காதே - இதை மட்டும் மீண்டும் இயக்கு - அனைத்தையும் மீண்டும் இயக்கு - கலைத்துப் போடுதல்: ஆன் - கலைத்துப் போடுதல்: ஆஃப் + தற்போதைய பயன்முறை: எதையும் மீண்டும் இயக்காது. மீண்டும் இயக்குதலை நிலைமாற்றும். + தற்போதைய பயன்முறை: ஒன்றை மட்டும் மீண்டும் இயக்கும். மீண்டும் இயக்குதலை நிலைமாற்றும். + தற்போதைய பயன்முறை: அனைத்தையும் மீண்டும் இயக்கும். மீண்டும் இயக்குதலை நிலைமாற்றும். + கலைத்துப் போடும் பயன்முறையை முடக்கும் + கலைத்துப் போடும் பயன்முறையை இயக்கும் VR பயன்முறை - வசனங்களை முடக்கும் - வசனங்களை இயக்கும் + சப்டைட்டில்களை முடக்கும் + சப்டைட்டில்களை இயக்கும் வேகம் 0.25x diff --git a/libraries/ui/src/main/res/values-te/strings.xml b/libraries/ui/src/main/res/values-te/strings.xml index 435128311e..124b8a99ba 100644 --- a/libraries/ui/src/main/res/values-te/strings.xml +++ b/libraries/ui/src/main/res/values-te/strings.xml @@ -37,11 +37,11 @@ %d సెకండ్ వేగంగా ఫార్వర్డ్ చేయండి %d సెకన్లు వేగంగా ఫార్వర్డ్ చేయండి - దేన్నీ పునరావృతం చేయకండి - ఒకదాన్ని పునరావృతం చేయండి - అన్నింటినీ పునరావృతం చేయండి - షఫుల్‌ను ఆన్ చేస్తుంది - షఫుల్‌ను ఆఫ్ చేస్తుంది + ప్రస్తుత మోడ్: ఏదీ పునరావృతం చేయవద్దు. పునరావృతం మోడ్‌ను టోగుల్ చేయండి. + ప్రస్తుత మోడ్: ఒకదానిని పునరావృతం చేయండి. పునరావృతం మోడ్‌ను టోగుల్ చేయండి. + ప్రస్తుత మోడ్: అన్నింటిని పునరావృతం చేయండి. పునరావృతం మోడ్‌ను టోగుల్ చేయండి. + షఫుల్ మోడ్‌ను డిజేబుల్ చేయండి + షఫుల్ మోడ్‌ను ఎనేబుల్ చేయండి వర్చువల్ రియాలిటీ మోడ్ ఉప శీర్షికలను డిజేబుల్ చేయి ఉప శీర్షికలను ఎనేబుల్ చేయి diff --git a/libraries/ui/src/main/res/values-th/strings.xml b/libraries/ui/src/main/res/values-th/strings.xml index 387e9fffe3..fdb108348e 100644 --- a/libraries/ui/src/main/res/values-th/strings.xml +++ b/libraries/ui/src/main/res/values-th/strings.xml @@ -37,11 +37,11 @@ กรอไปข้างหน้า %d วินาที กรอไปข้างหน้า %d วินาที - ไม่เล่นซ้ำ - เล่นซ้ำเพลงเดียว - เล่นซ้ำทั้งหมด - เปิดการสุ่มเพลงอยู่ - ปิดการสุ่มเพลงอยู่ + โหมดปัจจุบัน: ไม่เล่นซ้ำ สลับโหมดเล่นซ้ำ + โหมดปัจจุบัน: เล่นซ้ำรายการเดียว สลับโหมดเล่นซ้ำ + โหมดปัจจุบัน: เล่นซ้ำทั้งหมด สลับโหมดเล่นซ้ำ + ปิดใช้โหมดสุ่มเพลง + เปิดใช้โหมดสุ่มเพลง โหมด VR ปิดใช้คำบรรยาย เปิดใช้คำบรรยาย diff --git a/libraries/ui/src/main/res/values-tl/strings.xml b/libraries/ui/src/main/res/values-tl/strings.xml index 5715b393ac..0757b261da 100644 --- a/libraries/ui/src/main/res/values-tl/strings.xml +++ b/libraries/ui/src/main/res/values-tl/strings.xml @@ -37,11 +37,11 @@ I-fast forward nang %d segundo I-fast forward nang %d na segundo - Walang uulitin - Mag-ulit ng isa - Ulitin lahat - Naka-on ang pag-shuffle - Naka-off ang pag-shuffle + Current mode: Walang uulitin. I-toggle ang repeat mode. + Current mode: Walang uulitin. I-toggle ang repeat mode. + Current mode: Ulitin lahat. I-toggle ang repeat mode. + I-disable ang shuffle mode + I-enable ang shuffle mode VR mode I-disable ang mga subtitle I-enable ang mga subtitle diff --git a/libraries/ui/src/main/res/values-tr/strings.xml b/libraries/ui/src/main/res/values-tr/strings.xml index f367f44bb2..b2fd8b6fa4 100644 --- a/libraries/ui/src/main/res/values-tr/strings.xml +++ b/libraries/ui/src/main/res/values-tr/strings.xml @@ -37,11 +37,11 @@ %d saniye ileri sar %d saniye ileri sar - Hiçbirini tekrarlama - Birini tekrarla - Tümünü tekrarla - Karıştırma açık - Karıştırma kapalı + Geçerli mod: Tekrarlama. Tekrarlamayı aç/kapat. + Geçerli mod: Birini tekrarla. Tekrarlamayı aç/kapat. + Geçerli mod: Tümünü tekrarla. Tekrarlamayı aç/kapat. + Karıştırma modunu devre dışı bırak + Karıştırma modunu etkinleştir VR modu Altyazıları devre dışı bırak Altyazıları etkinleştir diff --git a/libraries/ui/src/main/res/values-uk/strings.xml b/libraries/ui/src/main/res/values-uk/strings.xml index f8f2c47064..2ffcc61d1c 100644 --- a/libraries/ui/src/main/res/values-uk/strings.xml +++ b/libraries/ui/src/main/res/values-uk/strings.xml @@ -41,11 +41,11 @@ Перемотати вперед на %d секунд Перемотати вперед на %d секунди - Не повторювати - Повторити 1 - Повторити всі - Перемішування ввімкнено - Перемішування вимкнено + Поточний режим: не повторювати. Перемкнути режим повторення. + Поточний режим: повторювати один медіафайл. Перемкнути режим повторення. + Поточний режим: повторювати все. Перемкнути режим повторення. + Вимкнути режим перемішування + Увімкнути режим перемішування Режим віртуальної реальності Вимкнути субтитри Увімкнути субтитри diff --git a/libraries/ui/src/main/res/values-ur/strings.xml b/libraries/ui/src/main/res/values-ur/strings.xml index f7b46d01f4..1e410c9712 100644 --- a/libraries/ui/src/main/res/values-ur/strings.xml +++ b/libraries/ui/src/main/res/values-ur/strings.xml @@ -37,11 +37,11 @@ %d سیکنڈ تیزی سے فارورڈ کریں %d سیکنڈز تیزی سے فارورڈ کریں - کسی کو نہ دہرائیں - ایک کو دہرائیں - سبھی کو دہرائیں - شفل آن - شفل آف + موجودہ موڈ: کچھ نہ دہرائیں۔ رپیٹ موڈ ٹوگل کریں۔ + موجودہ موڈ: ایک دہرائیں۔ رپیٹ موڈ ٹوگل کریں۔ + موجودہ موڈ: سبھی دہرائیں۔ رپیٹ موڈ ٹوگل کریں۔ + شفل موڈ کو غیر فعال کریں + شفل موڈ کو فعال کریں VR موڈ سب ٹائٹلز کو غیر فعال کریں سب ٹائٹلز کو فعال کریں diff --git a/libraries/ui/src/main/res/values-uz/strings.xml b/libraries/ui/src/main/res/values-uz/strings.xml index f0c4677b9c..f7639dd95b 100644 --- a/libraries/ui/src/main/res/values-uz/strings.xml +++ b/libraries/ui/src/main/res/values-uz/strings.xml @@ -37,11 +37,11 @@ %d soniya oldinga %d soniya oldinga - Takrorlanmasin - Bittasini takrorlash - Hammasini takrorlash - Tasodifiy ijro yoqilgan - Tasodifiy ijro yoqilmagan + Joriy rejim: Hech qaysi takrorlanmaydi. Takrorlash rejimiga oʻting. + Joriy rejim: Bittasi takrorlanadi. Takrorlash rejimiga oʻting. + Joriy rejim: Barchasi takrorlanadi. Takrorlash rejimiga oʻting. + Tasodifiy rejimni faolsizlantiring + Tasodifiy rejimni yoqing VR rejimi Taglavhalarni faolsizlantirish Taglavhalarni yoqish diff --git a/libraries/ui/src/main/res/values-vi/strings.xml b/libraries/ui/src/main/res/values-vi/strings.xml index 290d96f167..400d321f5e 100644 --- a/libraries/ui/src/main/res/values-vi/strings.xml +++ b/libraries/ui/src/main/res/values-vi/strings.xml @@ -37,11 +37,11 @@ Tua nhanh %d giây Tua nhanh %d giây - Không lặp lại - Lặp lại một - Lặp lại tất cả - Chế độ trộn bài đang bật - Chế độ trộn bài đang tắt + Chế độ hiện tại: Không lặp lại. Bật/tắt chế độ lặp lại. + Chế độ hiện tại: Lặp lại một nội dung. Bật/tắt chế độ lặp lại. + Chế độ hiện tại: Lặp lại tất cả nội dung. Bật/tắt chế độ lặp lại. + Tắt chế độ trộn bài + Bật chế độ trộn bài Chế độ thực tế ảo Tắt phụ đề Bật phụ đề diff --git a/libraries/ui/src/main/res/values-zh-rCN/strings.xml b/libraries/ui/src/main/res/values-zh-rCN/strings.xml index 436449df6d..4fedf2ae07 100644 --- a/libraries/ui/src/main/res/values-zh-rCN/strings.xml +++ b/libraries/ui/src/main/res/values-zh-rCN/strings.xml @@ -37,11 +37,11 @@ 快进 %d 秒 快进 %d 秒 - 不重复播放 - 重复播放一项 - 全部重复播放 - 随机播放功能已开启 - 随机播放功能已关闭 + 当前模式:不重复。切换重复播放模式。 + 当前模式:重复播放当前项目。切换重复播放模式。 + 当前模式:重复播放所有项目。切换重复播放模式。 + 停用随机播放模式 + 启用随机播放模式 VR 模式 停用字幕 启用字幕 diff --git a/libraries/ui/src/main/res/values-zh-rHK/strings.xml b/libraries/ui/src/main/res/values-zh-rHK/strings.xml index 4fe9ee9b41..88a0239c57 100644 --- a/libraries/ui/src/main/res/values-zh-rHK/strings.xml +++ b/libraries/ui/src/main/res/values-zh-rHK/strings.xml @@ -37,11 +37,11 @@ 快轉 %d 秒 快轉 %d 秒 - 不重複播放 - 重複播放單一項目 - 全部重複播放 - 已開啟隨機播放功能 - 已關閉隨機播放功能 + 宜家嘅模式:唔重複播放。轉做重複播放模式。 + 宜家嘅模式:重複播放一個項目。轉做重複播放模式。 + 宜家嘅模式:重複播放所有項目。轉做重複播放模式。 + 停用隨機播放模式 + 啟用隨機播放模式 虛擬現實模式 停用字幕 啟用字幕 diff --git a/libraries/ui/src/main/res/values-zh-rTW/strings.xml b/libraries/ui/src/main/res/values-zh-rTW/strings.xml index 591afff6b5..27dc2bcfd6 100644 --- a/libraries/ui/src/main/res/values-zh-rTW/strings.xml +++ b/libraries/ui/src/main/res/values-zh-rTW/strings.xml @@ -37,11 +37,11 @@ 快轉 %d 秒 快轉 %d 秒 - 不重複播放 - 重複播放單一項目 - 重複播放所有項目 - 隨機播放已開啟 - 隨機播放已關閉 + 目前模式:不重複播放。切換重複播放模式。 + 目前模式:重複播放單一項目。切換重複播放模式。 + 目前模式:重複播放所有項目。切換重複播放模式。 + 停用隨機播放模式 + 啟用隨機播放模式 虛擬實境模式 停用字幕 (Subtitle) 啟用字幕 (Subtitle) diff --git a/libraries/ui/src/main/res/values-zu/strings.xml b/libraries/ui/src/main/res/values-zu/strings.xml index 409adc0e70..05d78e556e 100644 --- a/libraries/ui/src/main/res/values-zu/strings.xml +++ b/libraries/ui/src/main/res/values-zu/strings.xml @@ -37,11 +37,11 @@ Dlulisela phambili ngamasekhondi angu-%d Dlulisela phambili ngamasekhondi angu-%d - Phinda okungekho - Phinda okukodwa - Phinda konke - Ukushova kuvuliwe - Ukushova kuvaliwe + Imodeli yamanje: Ungaphindi lutho. Guqula imodi yokuphinda. + Imodi yamanje: Phinda kanye. Guqula imodi yokuphinda. + Imodi yamanje: Phinda konke. Guqula imodi yokuphinda. + Khubaza imodi yokushova + Nika amandla imodi yokushova Inqubo ye-VR Khubaza imibhalo engezansi Nika amandla imibhalo engezansi From 039e1025523d33145b6260b9b9d3287de39b1c8f Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 15 Jun 2022 15:46:43 +0000 Subject: [PATCH 38/45] Update initial bitrate estimates #minor-release PiperOrigin-RevId: 455140203 (cherry picked from commit 646bf565c35c43ff59d4a7d51ee60036368339b6) --- .../upstream/DefaultBandwidthMeter.java | 503 +++++++++--------- .../upstream/DefaultBandwidthMeterTest.java | 3 + 2 files changed, 250 insertions(+), 256 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeter.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeter.java index 5a7c7e268b..0c9352f003 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeter.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeter.java @@ -47,27 +47,27 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList /** Default initial Wifi bitrate estimate in bits per second. */ public static final ImmutableList DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI = - ImmutableList.of(5_400_000L, 3_300_000L, 2_000_000L, 1_300_000L, 760_000L); + ImmutableList.of(4_800_000L, 3_100_000L, 2_100_000L, 1_500_000L, 800_000L); /** Default initial 2G bitrate estimates in bits per second. */ public static final ImmutableList DEFAULT_INITIAL_BITRATE_ESTIMATES_2G = - ImmutableList.of(1_700_000L, 820_000L, 450_000L, 180_000L, 130_000L); + ImmutableList.of(1_500_000L, 1_000_000L, 730_000L, 440_000L, 170_000L); /** Default initial 3G bitrate estimates in bits per second. */ public static final ImmutableList DEFAULT_INITIAL_BITRATE_ESTIMATES_3G = - ImmutableList.of(2_300_000L, 1_300_000L, 1_000_000L, 820_000L, 570_000L); + ImmutableList.of(2_200_000L, 1_400_000L, 1_100_000L, 910_000L, 620_000L); /** Default initial 4G bitrate estimates in bits per second. */ public static final ImmutableList DEFAULT_INITIAL_BITRATE_ESTIMATES_4G = - ImmutableList.of(3_400_000L, 2_000_000L, 1_400_000L, 1_000_000L, 620_000L); + ImmutableList.of(3_000_000L, 1_900_000L, 1_400_000L, 1_000_000L, 660_000L); /** Default initial 5G-NSA bitrate estimates in bits per second. */ public static final ImmutableList DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_NSA = - ImmutableList.of(7_500_000L, 5_200_000L, 3_700_000L, 1_800_000L, 1_100_000L); + ImmutableList.of(6_000_000L, 4_100_000L, 3_200_000L, 1_800_000L, 1_000_000L); /** Default initial 5G-SA bitrate estimates in bits per second. */ public static final ImmutableList DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_SA = - ImmutableList.of(3_300_000L, 1_900_000L, 1_700_000L, 1_500_000L, 1_200_000L); + ImmutableList.of(2_800_000L, 2_400_000L, 1_600_000L, 1_100_000L, 950_000L); /** * Default initial bitrate estimate used when the device is offline or the network type cannot be @@ -477,402 +477,393 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList private static int[] getInitialBitrateCountryGroupAssignment(String country) { switch (country) { case "AE": - return new int[] {1, 4, 4, 4, 3, 2}; + return new int[] {1, 4, 4, 4, 4, 0}; case "AG": - return new int[] {2, 3, 1, 2, 2, 2}; + return new int[] {2, 4, 1, 2, 2, 2}; + case "AI": + return new int[] {0, 2, 0, 3, 2, 2}; case "AM": - return new int[] {2, 3, 2, 4, 2, 2}; - case "AR": - return new int[] {2, 4, 1, 1, 2, 2}; + return new int[] {2, 3, 2, 3, 2, 2}; + case "AO": + return new int[] {4, 4, 3, 2, 2, 2}; case "AS": - return new int[] {2, 2, 2, 3, 2, 2}; + return new int[] {2, 2, 3, 3, 2, 2}; + case "AT": + return new int[] {1, 0, 1, 1, 0, 0}; case "AU": - return new int[] {0, 1, 0, 1, 2, 2}; + return new int[] {0, 1, 1, 1, 2, 0}; + case "AW": + return new int[] {1, 3, 4, 4, 2, 2}; + case "BA": + return new int[] {1, 2, 1, 1, 2, 2}; + case "BD": + return new int[] {2, 1, 3, 3, 2, 2}; case "BE": - return new int[] {0, 0, 3, 3, 2, 2}; + return new int[] {0, 1, 4, 4, 3, 2}; case "BF": return new int[] {4, 3, 4, 3, 2, 2}; case "BH": - return new int[] {1, 2, 2, 4, 4, 2}; + return new int[] {1, 2, 1, 3, 4, 2}; case "BJ": - return new int[] {4, 4, 3, 4, 2, 2}; - case "BN": - return new int[] {3, 2, 1, 1, 2, 2}; + return new int[] {4, 4, 3, 3, 2, 2}; case "BO": - return new int[] {1, 3, 3, 2, 2, 2}; - case "BQ": - return new int[] {1, 2, 2, 0, 2, 2}; + return new int[] {1, 2, 3, 2, 2, 2}; case "BS": - return new int[] {4, 2, 2, 3, 2, 2}; + return new int[] {4, 4, 2, 2, 2, 2}; case "BT": return new int[] {3, 1, 3, 2, 2, 2}; + case "BW": + return new int[] {3, 2, 1, 0, 2, 2}; case "BY": - return new int[] {0, 1, 1, 3, 2, 2}; + return new int[] {0, 1, 2, 3, 2, 2}; case "BZ": - return new int[] {2, 4, 2, 2, 2, 2}; + return new int[] {2, 4, 2, 1, 2, 2}; case "CA": - return new int[] {0, 2, 1, 2, 4, 1}; + return new int[] {0, 2, 2, 2, 3, 2}; case "CD": - return new int[] {4, 2, 3, 1, 2, 2}; - case "CF": return new int[] {4, 2, 3, 2, 2, 2}; - case "CI": - return new int[] {3, 3, 3, 4, 2, 2}; - case "CK": - return new int[] {2, 2, 2, 1, 2, 2}; - case "AO": + case "CH": + return new int[] {0, 0, 0, 1, 0, 2}; case "CM": - return new int[] {3, 4, 3, 2, 2, 2}; + return new int[] {3, 3, 3, 3, 2, 2}; case "CN": - return new int[] {2, 0, 2, 2, 3, 1}; + return new int[] {2, 0, 1, 1, 3, 2}; case "CO": - return new int[] {2, 2, 4, 2, 2, 2}; + return new int[] {2, 3, 4, 3, 2, 2}; case "CR": - return new int[] {2, 2, 4, 4, 2, 2}; + return new int[] {2, 3, 4, 4, 2, 2}; case "CV": - return new int[] {2, 3, 1, 0, 2, 2}; + return new int[] {2, 1, 0, 0, 2, 2}; + case "BN": case "CW": return new int[] {2, 2, 0, 0, 2, 2}; - case "CY": - return new int[] {1, 0, 0, 0, 1, 2}; case "DE": - return new int[] {0, 0, 2, 2, 1, 2}; - case "DJ": - return new int[] {4, 1, 4, 4, 2, 2}; + return new int[] {0, 1, 2, 2, 2, 3}; case "DK": - return new int[] {0, 0, 1, 0, 0, 2}; + return new int[] {0, 0, 3, 2, 0, 2}; + case "DO": + return new int[] {3, 4, 4, 4, 4, 2}; case "EC": - return new int[] {2, 4, 2, 1, 2, 2}; - case "EG": - return new int[] {3, 4, 2, 3, 2, 2}; + return new int[] {2, 3, 2, 1, 2, 2}; case "ET": - return new int[] {4, 4, 3, 1, 2, 2}; + return new int[] {4, 3, 3, 1, 2, 2}; case "FI": - return new int[] {0, 0, 0, 1, 0, 2}; + return new int[] {0, 0, 0, 3, 0, 2}; case "FJ": - return new int[] {3, 1, 3, 3, 2, 2}; + return new int[] {3, 1, 2, 2, 2, 2}; case "FM": - return new int[] {3, 2, 4, 2, 2, 2}; + return new int[] {4, 2, 4, 1, 2, 2}; case "FR": - return new int[] {1, 1, 2, 1, 1, 1}; - case "GA": - return new int[] {2, 3, 1, 1, 2, 2}; + return new int[] {1, 2, 3, 1, 0, 2}; case "GB": - return new int[] {0, 0, 1, 1, 2, 3}; + return new int[] {0, 0, 1, 1, 1, 1}; case "GE": - return new int[] {1, 1, 1, 3, 2, 2}; + return new int[] {1, 1, 1, 2, 2, 2}; case "BB": + case "DM": case "FO": - case "GG": + case "GI": return new int[] {0, 2, 0, 0, 2, 2}; - case "GH": - return new int[] {3, 2, 3, 2, 2, 2}; + case "AF": + case "GM": + return new int[] {4, 3, 3, 4, 2, 2}; case "GN": return new int[] {4, 3, 4, 2, 2, 2}; case "GQ": - return new int[] {4, 2, 3, 4, 2, 2}; + return new int[] {4, 2, 1, 4, 2, 2}; case "GT": - return new int[] {2, 3, 2, 1, 2, 2}; - case "AW": - case "GU": - return new int[] {1, 2, 4, 4, 2, 2}; - case "BW": + return new int[] {2, 3, 2, 2, 2, 2}; + case "CG": + case "EG": + case "GW": + return new int[] {3, 4, 3, 3, 2, 2}; case "GY": - return new int[] {3, 4, 1, 0, 2, 2}; + return new int[] {3, 2, 2, 1, 2, 2}; case "HK": return new int[] {0, 1, 2, 3, 2, 0}; case "HU": return new int[] {0, 0, 0, 1, 3, 2}; case "ID": - return new int[] {3, 2, 3, 3, 3, 2}; + return new int[] {3, 1, 2, 2, 3, 2}; case "ES": case "IE": return new int[] {0, 1, 1, 1, 2, 2}; + case "CL": case "IL": - return new int[] {1, 1, 2, 3, 4, 2}; - case "IM": - return new int[] {0, 2, 0, 1, 2, 2}; + return new int[] {1, 2, 2, 2, 3, 2}; case "IN": - return new int[] {1, 1, 3, 2, 4, 3}; + return new int[] {1, 1, 3, 2, 3, 3}; + case "IQ": + return new int[] {3, 2, 2, 3, 2, 2}; case "IR": - return new int[] {3, 0, 1, 1, 3, 0}; + return new int[] {3, 0, 1, 1, 4, 1}; case "IT": - return new int[] {0, 1, 0, 1, 1, 2}; - case "JE": - return new int[] {3, 2, 1, 2, 2, 2}; - case "DO": + return new int[] {0, 0, 0, 1, 1, 2}; case "JM": - return new int[] {3, 4, 4, 4, 2, 2}; + return new int[] {2, 4, 3, 2, 2, 2}; + case "JO": + return new int[] {2, 1, 1, 2, 2, 2}; case "JP": - return new int[] {0, 1, 0, 1, 1, 1}; - case "KE": - return new int[] {3, 3, 2, 2, 2, 2}; - case "KG": - return new int[] {2, 1, 1, 1, 2, 2}; + return new int[] {0, 1, 1, 2, 2, 4}; case "KH": - return new int[] {1, 1, 4, 2, 2, 2}; + return new int[] {2, 1, 4, 2, 2, 2}; + case "CF": + case "KI": + return new int[] {4, 2, 4, 2, 2, 2}; + case "FK": + case "KE": + case "KP": + return new int[] {3, 2, 2, 2, 2, 2}; case "KR": - return new int[] {0, 0, 1, 3, 4, 4}; + return new int[] {0, 1, 1, 3, 4, 4}; + case "CY": case "KW": - return new int[] {1, 1, 0, 0, 0, 2}; - case "AL": - case "BA": - case "KY": - return new int[] {1, 2, 0, 1, 2, 2}; + return new int[] {1, 0, 0, 0, 0, 2}; case "KZ": - return new int[] {1, 1, 2, 2, 2, 2}; + return new int[] {2, 1, 2, 2, 2, 2}; + case "LA": + return new int[] {1, 2, 1, 3, 2, 2}; case "LB": - return new int[] {3, 2, 1, 4, 2, 2}; - case "AD": - case "BM": - case "GL": - case "LC": - return new int[] {1, 2, 0, 0, 2, 2}; + return new int[] {3, 3, 2, 4, 2, 2}; case "LK": - return new int[] {3, 1, 3, 4, 4, 2}; + return new int[] {3, 1, 3, 3, 4, 2}; + case "CI": + case "DZ": case "LR": - return new int[] {3, 4, 4, 3, 2, 2}; + return new int[] {3, 4, 4, 4, 2, 2}; case "LS": - return new int[] {3, 3, 4, 3, 2, 2}; + return new int[] {3, 3, 2, 2, 2, 2}; + case "LT": + return new int[] {0, 0, 0, 0, 2, 2}; case "LU": - return new int[] {1, 0, 2, 2, 2, 2}; + return new int[] {1, 0, 3, 2, 1, 4}; + case "MA": + return new int[] {3, 3, 1, 1, 2, 2}; case "MC": return new int[] {0, 2, 2, 0, 2, 2}; - case "JO": case "ME": - return new int[] {1, 0, 0, 1, 2, 2}; - case "MF": - return new int[] {1, 2, 1, 0, 2, 2}; - case "MG": - return new int[] {3, 4, 2, 2, 2, 2}; - case "MH": - return new int[] {3, 2, 2, 4, 2, 2}; - case "ML": - return new int[] {4, 3, 3, 1, 2, 2}; + return new int[] {2, 0, 0, 1, 2, 2}; + case "MK": + return new int[] {1, 0, 0, 1, 3, 2}; case "MM": - return new int[] {2, 4, 3, 3, 2, 2}; + return new int[] {2, 4, 2, 3, 2, 2}; case "MN": return new int[] {2, 0, 1, 2, 2, 2}; case "MO": + case "MP": return new int[] {0, 2, 4, 4, 2, 2}; - case "GF": case "GP": case "MQ": return new int[] {2, 1, 2, 3, 2, 2}; - case "MR": - return new int[] {4, 1, 3, 4, 2, 2}; - case "EE": - case "LT": - case "LV": - case "MT": - return new int[] {0, 0, 0, 0, 2, 2}; case "MU": return new int[] {3, 1, 1, 2, 2, 2}; case "MV": return new int[] {3, 4, 1, 4, 2, 2}; case "MW": - return new int[] {4, 2, 1, 0, 2, 2}; - case "CG": + return new int[] {4, 2, 3, 3, 2, 2}; case "MX": return new int[] {2, 4, 3, 4, 2, 2}; - case "BD": case "MY": - return new int[] {2, 1, 3, 3, 2, 2}; - case "NA": - return new int[] {4, 3, 2, 2, 2, 2}; - case "AZ": + return new int[] {1, 0, 3, 1, 3, 2}; + case "MZ": + return new int[] {3, 1, 2, 1, 2, 2}; case "NC": - return new int[] {3, 2, 4, 4, 2, 2}; + return new int[] {3, 3, 4, 4, 2, 2}; case "NG": - return new int[] {3, 4, 1, 1, 2, 2}; - case "NI": - return new int[] {2, 3, 4, 3, 2, 2}; + return new int[] {3, 4, 2, 1, 2, 2}; case "NL": - return new int[] {0, 0, 3, 2, 0, 4}; + return new int[] {0, 2, 2, 3, 0, 3}; + case "CZ": case "NO": - return new int[] {0, 0, 2, 0, 0, 2}; + return new int[] {0, 0, 2, 0, 1, 2}; case "NP": - return new int[] {2, 1, 4, 3, 2, 2}; + return new int[] {2, 2, 4, 3, 2, 2}; case "NR": - return new int[] {3, 2, 2, 0, 2, 2}; - case "NZ": - return new int[] {1, 0, 1, 2, 4, 2}; + case "NU": + return new int[] {4, 2, 2, 1, 2, 2}; case "OM": return new int[] {2, 3, 1, 3, 4, 2}; - case "PA": - return new int[] {1, 3, 3, 3, 2, 2}; + case "GU": case "PE": - return new int[] {2, 3, 4, 4, 4, 2}; + return new int[] {1, 2, 4, 4, 4, 2}; + case "CK": case "PF": - return new int[] {2, 3, 3, 1, 2, 2}; - case "CU": + return new int[] {2, 2, 2, 1, 2, 2}; + case "ML": case "PG": - return new int[] {4, 4, 3, 2, 2, 2}; + return new int[] {4, 3, 3, 2, 2, 2}; case "PH": - return new int[] {2, 2, 3, 3, 3, 2}; + return new int[] {2, 1, 3, 3, 3, 0}; + case "NZ": + case "PL": + return new int[] {1, 1, 2, 2, 4, 2}; case "PR": - return new int[] {2, 3, 2, 2, 3, 3}; + return new int[] {2, 0, 2, 1, 2, 1}; case "PS": return new int[] {3, 4, 1, 2, 2, 2}; - case "PT": - return new int[] {0, 1, 0, 0, 2, 2}; case "PW": return new int[] {2, 2, 4, 1, 2, 2}; - case "PY": - return new int[] {2, 2, 3, 2, 2, 2}; case "QA": - return new int[] {2, 4, 2, 4, 4, 2}; + return new int[] {2, 4, 4, 4, 4, 2}; + case "MF": case "RE": - return new int[] {1, 1, 1, 2, 2, 2}; + return new int[] {1, 2, 1, 2, 2, 2}; case "RO": - return new int[] {0, 0, 1, 1, 1, 2}; - case "GR": - case "HR": + return new int[] {0, 0, 1, 2, 1, 2}; case "MD": - case "MK": case "RS": return new int[] {1, 0, 0, 0, 2, 2}; case "RU": - return new int[] {0, 0, 0, 1, 2, 2}; + return new int[] {1, 0, 0, 0, 4, 3}; case "RW": - return new int[] {3, 4, 3, 0, 2, 2}; - case "KI": - case "KM": - case "LY": + return new int[] {3, 4, 2, 0, 2, 2}; + case "SA": + return new int[] {3, 1, 1, 1, 2, 2}; case "SB": return new int[] {4, 2, 4, 3, 2, 2}; - case "SC": - return new int[] {4, 3, 0, 2, 2, 2}; case "SG": - return new int[] {1, 1, 2, 3, 1, 4}; - case "BG": - case "CZ": - case "SI": - return new int[] {0, 0, 0, 0, 1, 2}; - case "AT": - case "CH": - case "IS": - case "SE": - case "SK": - return new int[] {0, 0, 0, 0, 0, 2}; - case "SL": - return new int[] {4, 3, 4, 1, 2, 2}; - case "AX": - case "GI": - case "LI": - case "MP": - case "PM": - case "SJ": - case "SM": - return new int[] {0, 2, 2, 2, 2, 2}; - case "HN": - case "PK": - case "SO": - return new int[] {3, 2, 3, 3, 2, 2}; - case "BR": - case "SR": - return new int[] {2, 3, 2, 2, 2, 2}; - case "FK": - case "KP": - case "MA": - case "MZ": - case "ST": - return new int[] {3, 2, 2, 2, 2, 2}; - case "SV": - return new int[] {2, 2, 3, 3, 2, 2}; - case "SZ": - return new int[] {4, 3, 2, 4, 2, 2}; - case "SX": - case "TC": - return new int[] {2, 2, 1, 0, 2, 2}; - case "TG": - return new int[] {3, 3, 2, 0, 2, 2}; - case "TH": - return new int[] {0, 3, 2, 3, 3, 0}; - case "TJ": - return new int[] {4, 2, 4, 4, 2, 2}; - case "BI": - case "DZ": - case "SY": - case "TL": - return new int[] {4, 3, 4, 4, 2, 2}; - case "TM": - return new int[] {4, 2, 4, 2, 2, 2}; - case "TO": - return new int[] {4, 2, 3, 3, 2, 2}; - case "TR": - return new int[] {1, 1, 0, 1, 2, 2}; - case "TT": - return new int[] {1, 4, 1, 1, 2, 2}; + return new int[] {1, 1, 2, 2, 2, 1}; case "AQ": case "ER": - case "IO": - case "NU": case "SH": - case "SS": - case "TV": return new int[] {4, 2, 2, 2, 2, 2}; - case "TW": - return new int[] {0, 0, 0, 0, 0, 0}; - case "GW": - case "TZ": - return new int[] {3, 4, 3, 3, 2, 2}; - case "UA": - return new int[] {0, 3, 1, 1, 2, 2}; - case "IQ": - case "UG": - return new int[] {3, 3, 3, 3, 2, 2}; - case "CL": - case "PL": - case "US": - return new int[] {1, 1, 2, 2, 3, 2}; - case "LA": - case "UY": + case "GR": + case "HR": + case "SI": + return new int[] {1, 0, 0, 0, 1, 2}; + case "BG": + case "MT": + case "SK": + return new int[] {0, 0, 0, 0, 1, 2}; + case "AX": + case "LI": + case "MS": + case "PM": + case "SM": + return new int[] {0, 2, 2, 2, 2, 2}; + case "SN": + return new int[] {4, 4, 4, 3, 2, 2}; + case "SR": + return new int[] {2, 4, 3, 0, 2, 2}; + case "SS": + return new int[] {4, 3, 2, 3, 2, 2}; + case "ST": return new int[] {2, 2, 1, 2, 2, 2}; + case "NI": + case "PA": + case "SV": + return new int[] {2, 3, 3, 3, 2, 2}; + case "SZ": + return new int[] {3, 3, 3, 4, 2, 2}; + case "SX": + case "TC": + return new int[] {1, 2, 1, 0, 2, 2}; + case "GA": + case "TG": + return new int[] {3, 4, 1, 0, 2, 2}; + case "TH": + return new int[] {0, 2, 2, 3, 3, 4}; + case "TK": + return new int[] {2, 2, 2, 4, 2, 2}; + case "CU": + case "DJ": + case "SY": + case "TJ": + case "TL": + return new int[] {4, 3, 4, 4, 2, 2}; + case "SC": + case "TM": + return new int[] {4, 2, 1, 1, 2, 2}; + case "AZ": + case "GF": + case "LY": + case "PK": + case "SO": + case "TO": + return new int[] {3, 2, 3, 3, 2, 2}; + case "TR": + return new int[] {1, 1, 0, 0, 2, 2}; + case "TT": + return new int[] {1, 4, 1, 3, 2, 2}; + case "EE": + case "IS": + case "LV": + case "PT": + case "SE": + case "TW": + return new int[] {0, 0, 0, 0, 0, 2}; + case "TZ": + return new int[] {3, 4, 3, 2, 2, 2}; + case "IM": + case "UA": + return new int[] {0, 2, 1, 1, 2, 2}; + case "SL": + case "UG": + return new int[] {3, 3, 4, 3, 2, 2}; + case "US": + return new int[] {1, 0, 2, 2, 3, 1}; + case "AR": + case "KG": + case "TN": + case "UY": + return new int[] {2, 1, 1, 1, 2, 2}; case "UZ": return new int[] {2, 2, 3, 4, 2, 2}; - case "AI": case "BL": case "CX": - case "DM": - case "GD": - case "MS": - case "VC": + case "VA": return new int[] {1, 2, 2, 2, 2, 2}; - case "SA": - case "TN": + case "AD": + case "BM": + case "BQ": + case "GD": + case "GL": + case "KN": + case "KY": + case "LC": + case "VC": + return new int[] {1, 2, 0, 0, 2, 2}; case "VG": return new int[] {2, 2, 1, 1, 2, 2}; + case "GG": case "VI": - return new int[] {1, 2, 1, 3, 2, 2}; + return new int[] {0, 2, 0, 1, 2, 2}; case "VN": return new int[] {0, 3, 3, 4, 2, 2}; + case "GH": + case "NA": case "VU": - return new int[] {4, 2, 2, 1, 2, 2}; - case "GM": + return new int[] {3, 3, 3, 2, 2, 2}; + case "IO": + case "MH": + case "TV": case "WF": return new int[] {4, 2, 2, 4, 2, 2}; case "WS": - return new int[] {3, 1, 2, 1, 2, 2}; + return new int[] {3, 1, 3, 1, 2, 2}; + case "AL": case "XK": return new int[] {1, 1, 1, 1, 2, 2}; - case "AF": + case "BI": case "HT": + case "KM": + case "MG": case "NE": case "SD": - case "SN": case "TD": case "VE": case "YE": return new int[] {4, 4, 4, 4, 2, 2}; + case "JE": case "YT": - return new int[] {4, 1, 1, 1, 2, 2}; + return new int[] {4, 2, 2, 3, 2, 2}; case "ZA": - return new int[] {3, 3, 1, 1, 1, 2}; + return new int[] {3, 2, 2, 1, 1, 2}; case "ZM": return new int[] {3, 3, 4, 2, 2, 2}; + case "MR": case "ZW": - return new int[] {3, 2, 4, 3, 2, 2}; + return new int[] {4, 2, 4, 4, 2, 2}; default: return new int[] {2, 2, 2, 2, 2, 2}; } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeterTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeterTest.java index d2ca3e7f03..bd3e3cbf1c 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeterTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/upstream/DefaultBandwidthMeterTest.java @@ -38,6 +38,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.util.Random; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Shadows; @@ -125,6 +126,7 @@ public final class DefaultBandwidthMeterTest { /* subType= */ 0, /* isAvailable= */ true, CONNECTED); + setNetworkCountryIso("non-existent-country-to-force-default-values"); } @Test @@ -378,6 +380,7 @@ public final class DefaultBandwidthMeterTest { assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow); } + @Ignore // 5G-SA isn't widespread enough yet to define a slow and fast country for testing. @Test @Config(minSdk = 29) // 5G-SA detection support was added in API 29. public void From 14aced630451fcf235b9622379afd56a2f7359a9 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 15 Jun 2022 18:40:32 +0000 Subject: [PATCH 39/45] Avoid out of bounds when setting less media items than in playlist Issue: androidx/media#86 #minor-release PiperOrigin-RevId: 455182232 (cherry picked from commit 8f844b32fdf4d41d6a5690e57e55d0c383ee7e3e) --- RELEASENOTES.md | 3 + .../session/MediaControllerImplBase.java | 12 ++- .../media3/session/MediaControllerTest.java | 94 +++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cc4fc72c3e..a882485c0b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -185,6 +185,9 @@ `MediaSession.Callback.onSetMediaUri`. The same functionality can be achieved by using `MediaController.setMediaItem` and `MediaSession.Callback.onAddMediaItems`. + * Fix `IndexOutOfBoundsException` when setting less media items than in + the current playlist + ([#86](https://github.com/androidx/media/issues/86)). * Data sources: * Rename `DummyDataSource` to `PlaceholderDataSource`. * Workaround OkHttp interrupt handling. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index c80e832a97..d9d804c1e6 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -828,7 +828,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; Collections.singletonList(mediaItem), /* startIndex= */ C.INDEX_UNSET, /* startPositionMs= */ C.TIME_UNSET, - /* resetToDefaultPosition= */ false); + /* resetToDefaultPosition= */ true); } @Override @@ -887,7 +887,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; mediaItems, /* startIndex= */ C.INDEX_UNSET, /* startPositionMs= */ C.TIME_UNSET, - /* resetToDefaultPosition= */ false); + /* resetToDefaultPosition= */ true); } @Override @@ -1832,12 +1832,18 @@ import org.checkerframework.checker.nullness.qual.NonNull; throw new IllegalSeekPositionException(newTimeline, startIndex, startPositionMs); } + boolean correctedStartIndex = false; if (resetToDefaultPosition) { startIndex = newTimeline.getFirstWindowIndex(playerInfo.shuffleModeEnabled); startPositionMs = C.TIME_UNSET; } else if (startIndex == C.INDEX_UNSET) { startIndex = playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex; startPositionMs = playerInfo.sessionPositionInfo.positionInfo.positionMs; + if (startIndex >= newTimeline.getWindowCount()) { + correctedStartIndex = true; + startIndex = newTimeline.getFirstWindowIndex(playerInfo.shuffleModeEnabled); + startPositionMs = C.TIME_UNSET; + } } PositionInfo newPositionInfo; SessionPositionInfo newSessionPositionInfo; @@ -1905,7 +1911,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; // Mask the playback state. int maskingPlaybackState = newPlayerInfo.playbackState; if (startIndex != C.INDEX_UNSET && newPlayerInfo.playbackState != STATE_IDLE) { - if (newTimeline.isEmpty() || startIndex >= newTimeline.getWindowCount()) { + if (newTimeline.isEmpty() || correctedStartIndex) { // Setting an empty timeline or invalid seek transitions to ended. maskingPlaybackState = STATE_ENDED; } else { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java index 80b893e9d3..63ea9ad1c9 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java @@ -36,6 +36,7 @@ import android.os.RemoteException; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.HeartRating; +import androidx.media3.common.IllegalSeekPositionException; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.MediaMetadata; @@ -56,6 +57,7 @@ import androidx.media3.test.session.common.TestUtils; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -64,6 +66,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; @@ -1055,4 +1058,95 @@ public class MediaControllerTest { assertThat(mediaMetadata).isEqualTo(testMediaMetadata); } + + @Test + public void + setMediaItems_setLessMediaItemsThanCurrentMediaItemIndex_masksCurrentMediaItemIndexAndStateCorrectly() + throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + List threeItemsList = + ImmutableList.of( + MediaItem.fromUri("http://www.google.com/1"), + MediaItem.fromUri("http://www.google.com/2"), + MediaItem.fromUri("http://www.google.com/3")); + List twoItemsList = + ImmutableList.of( + MediaItem.fromUri("http://www.google.com/1"), + MediaItem.fromUri("http://www.google.com/2")); + + int[] currentMediaIndexAndState = + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems(threeItemsList); + controller.prepare(); + controller.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ C.TIME_UNSET); + controller.setMediaItems(twoItemsList); + return new int[] { + controller.getCurrentMediaItemIndex(), controller.getPlaybackState() + }; + }); + + assertThat(currentMediaIndexAndState[0]).isEqualTo(0); + assertThat(currentMediaIndexAndState[1]).isEqualTo(Player.STATE_BUFFERING); + } + + @Test + public void + setMediaItems_setLessMediaItemsThanCurrentMediaItemIndexResetPositionFalse_masksCurrentMediaItemIndexAndStateCorrectly() + throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + List threeItemsList = + ImmutableList.of( + MediaItem.fromUri("http://www.google.com/1"), + MediaItem.fromUri("http://www.google.com/2"), + MediaItem.fromUri("http://www.google.com/3")); + List twoItemsList = + ImmutableList.of( + MediaItem.fromUri("http://www.google.com/1"), + MediaItem.fromUri("http://www.google.com/2")); + + int[] currentMediaItemIndexAndState = + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems(threeItemsList); + controller.prepare(); + controller.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ C.TIME_UNSET); + controller.setMediaItems(twoItemsList, /* resetPosition= */ false); + return new int[] { + controller.getCurrentMediaItemIndex(), controller.getPlaybackState() + }; + }); + + assertThat(currentMediaItemIndexAndState[0]).isEqualTo(0); + assertThat(currentMediaItemIndexAndState[1]).isEqualTo(Player.STATE_ENDED); + } + + @Test + public void setMediaItems_startIndexTooLarge_throwIllegalSeekPositionException() + throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + List threeItemsList = + ImmutableList.of( + MediaItem.fromUri("http://www.google.com/1"), + MediaItem.fromUri("http://www.google.com/2"), + MediaItem.fromUri("http://www.google.com/3")); + + Assert.assertThrows( + IllegalSeekPositionException.class, + () -> + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems( + threeItemsList, + /* startIndex= */ 99, + /* startPositionMs= */ C.TIME_UNSET); + return controller.getCurrentMediaItemIndex(); + })); + } } From 9c02cdb1fb8592dd70003605bb974c95f394e602 Mon Sep 17 00:00:00 2001 From: Marc Baechinger Date: Thu, 16 Jun 2022 11:24:41 +0000 Subject: [PATCH 40/45] Merge pull request #63 from ittiam-systems:rtp-h263 PiperOrigin-RevId: 455347182 (cherry picked from commit dc0e5c447b926c0d1117182c4e4abf0abc0e9dcb) --- RELEASENOTES.md | 2 + .../exoplayer/rtsp/RtpPayloadFormat.java | 7 + .../media3/exoplayer/rtsp/RtspMediaTrack.java | 24 ++ .../DefaultRtpPayloadReaderFactory.java | 2 + .../exoplayer/rtsp/reader/RtpH263Reader.java | 222 ++++++++++++++++++ 5 files changed, 257 insertions(+) create mode 100644 libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a882485c0b..3c7ad99305 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -163,6 +163,8 @@ ([#47](https://github.com/androidx/media/pull/64)). * Add RTP reader for OPUS ([#53](https://github.com/androidx/media/pull/53)). + * Add RTP reader for H263 + ([#63](https://github.com/androidx/media/pull/63)). * Session: * Fix NPE in MediaControllerImplLegacy ([#59](https://github.com/androidx/media/pull/59)). diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java index 39b7d6f0eb..55bb804642 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java @@ -44,6 +44,8 @@ public final class RtpPayloadFormat { private static final String RTP_MEDIA_AMR_WB = "AMR-WB"; private static final String RTP_MEDIA_MPEG4_GENERIC = "MPEG4-GENERIC"; private static final String RTP_MEDIA_MPEG4_VIDEO = "MP4V-ES"; + private static final String RTP_MEDIA_H263_1998 = "H263-1998"; + private static final String RTP_MEDIA_H263_2000 = "H263-2000"; private static final String RTP_MEDIA_H264 = "H264"; private static final String RTP_MEDIA_H265 = "H265"; private static final String RTP_MEDIA_OPUS = "OPUS"; @@ -60,6 +62,8 @@ public final class RtpPayloadFormat { case RTP_MEDIA_AC3: case RTP_MEDIA_AMR: case RTP_MEDIA_AMR_WB: + case RTP_MEDIA_H263_1998: + case RTP_MEDIA_H263_2000: case RTP_MEDIA_H264: case RTP_MEDIA_H265: case RTP_MEDIA_MPEG4_VIDEO: @@ -103,6 +107,9 @@ public final class RtpPayloadFormat { return MimeTypes.AUDIO_ALAW; case RTP_MEDIA_PCMU: return MimeTypes.AUDIO_MLAW; + case RTP_MEDIA_H263_1998: + case RTP_MEDIA_H263_2000: + return MimeTypes.VIDEO_H263; case RTP_MEDIA_H264: return MimeTypes.VIDEO_H264; case RTP_MEDIA_H265: diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java index 2a7310c470..c8de624326 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java @@ -125,6 +125,25 @@ import com.google.common.collect.ImmutableMap; */ private static final int DEFAULT_VP9_HEIGHT = 240; + /** + * Default height for H263. + * + *

    RFC4629 does not mandate codec specific data (like width and height) in the fmtp attribute. + * These values are taken from Android's software H263 decoder. + */ + private static final int DEFAULT_H263_WIDTH = 352; + /** + * Default height for H263. + * + *

    RFC4629 does not mandate codec specific data (like width and height) in the fmtp attribute. + * These values are taken from Android's software H263 decoder. + */ + private static final int DEFAULT_H263_HEIGHT = 288; + /** The track's associated {@link RtpPayloadFormat}. */ public final RtpPayloadFormat payloadFormat; /** The track's URI. */ @@ -214,6 +233,11 @@ import com.google.common.collect.ImmutableMap; checkArgument(!fmtpParameters.isEmpty()); processMPEG4FmtpAttribute(formatBuilder, fmtpParameters); break; + case MimeTypes.VIDEO_H263: + // H263 never uses fmtp width and height attributes (RFC4629 Section 8.2), setting default + // width and height. + formatBuilder.setWidth(DEFAULT_H263_WIDTH).setHeight(DEFAULT_H263_HEIGHT); + break; case MimeTypes.VIDEO_H264: checkArgument(!fmtpParameters.isEmpty()); processH264FmtpAttribute(formatBuilder, fmtpParameters); diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java index 7c09884475..0c1ee768b5 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java @@ -45,6 +45,8 @@ import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; case MimeTypes.AUDIO_ALAW: case MimeTypes.AUDIO_MLAW: return new RtpPcmReader(payloadFormat); + case MimeTypes.VIDEO_H263: + return new RtpH263Reader(payloadFormat); case MimeTypes.VIDEO_H264: return new RtpH264Reader(payloadFormat); case MimeTypes.VIDEO_H265: diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java new file mode 100644 index 0000000000..4aedc65aad --- /dev/null +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java @@ -0,0 +1,222 @@ +/* + * Copyright 2022 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 androidx.media3.exoplayer.rtsp.reader; + +import static androidx.media3.common.util.Assertions.checkStateNotNull; + +import androidx.media3.common.C; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.rtsp.RtpPacket; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.TrackOutput; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Parses a H263 byte stream carried on RTP packets, and extracts H263 frames as defined in RFC4629. + */ +/* package */ final class RtpH263Reader implements RtpPayloadReader { + private static final String TAG = "RtpH263Reader"; + + private static final long MEDIA_CLOCK_FREQUENCY = 90_000; + + /** I-frame VOP unit type. */ + private static final int I_VOP = 0; + + /** Picture start code, P=1, V=0, PLEN=0. Refer to RFC4629 Section 6.1. */ + private static final int PICTURE_START_CODE = 128; + + private final RtpPayloadFormat payloadFormat; + + private @MonotonicNonNull TrackOutput trackOutput; + + /** + * First received RTP timestamp. All RTP timestamps are dimension-less, the time base is defined + * by {@link #MEDIA_CLOCK_FREQUENCY}. + */ + private long firstReceivedTimestamp; + + /** The combined size of a sample that is fragmented into multiple RTP packets. */ + private int fragmentedSampleSizeBytes; + + private int previousSequenceNumber; + + private int width; + private int height; + private boolean isKeyFrame; + private boolean isOutputFormatSet; + private long startTimeOffsetUs; + + /** Creates an instance. */ + public RtpH263Reader(RtpPayloadFormat payloadFormat) { + this.payloadFormat = payloadFormat; + firstReceivedTimestamp = C.TIME_UNSET; + previousSequenceNumber = C.INDEX_UNSET; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, int trackId) { + trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_VIDEO); + trackOutput.format(payloadFormat.format); + } + + @Override + public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {} + + @Override + public void consume( + ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) { + checkStateNotNull(trackOutput); + + // H263 Header Payload Header, RFC4629 Section 5.1. + // 0 1 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | RR |P|V| PLEN |PEBIT| + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + int currentPosition = data.getPosition(); + int header = data.readUnsignedShort(); + boolean pBitIsSet = (header & 0x400) > 0; + + // Check if optional V (Video Redundancy Coding), PLEN or PEBIT is present, RFC4629 Section 5.1. + if ((header & 0x200) != 0 || (header & 0x1F8) != 0 || (header & 0x7) != 0) { + Log.w( + TAG, + "Dropping packet: video reduncancy coding is not supported, packet header VRC, or PLEN or" + + " PEBIT is non-zero"); + return; + } + + if (pBitIsSet) { + int payloadStartCode = data.peekUnsignedByte() & 0xFC; + // Packets that begin with a Picture Start Code(100000). Refer RFC4629 Section 6.1. + if (payloadStartCode < PICTURE_START_CODE) { + Log.w(TAG, "Picture start Code (PSC) missing, dropping packet."); + return; + } + // Setting first two bytes of the start code. Refer RFC4629 Section 6.1.1. + data.getData()[currentPosition] = 0; + data.getData()[currentPosition + 1] = 0; + data.setPosition(currentPosition); + } else { + // Check that this packet is in the sequence of the previous packet. + int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); + if (sequenceNumber != expectedSequenceNumber) { + Log.w( + TAG, + Util.formatInvariant( + "Received RTP packet with unexpected sequence number. Expected: %d; received: %d." + + " Dropping packet.", + expectedSequenceNumber, sequenceNumber)); + return; + } + } + + if (fragmentedSampleSizeBytes == 0) { + parseVopHeader(data, isOutputFormatSet); + if (!isOutputFormatSet && isKeyFrame) { + if (width != payloadFormat.format.width || height != payloadFormat.format.height) { + trackOutput.format( + payloadFormat.format.buildUpon().setWidth(width).setHeight(height).build()); + } + isOutputFormatSet = true; + } + } + int fragmentSize = data.bytesLeft(); + // Write the video sample. + trackOutput.sampleData(data, fragmentSize); + fragmentedSampleSizeBytes += fragmentSize; + + if (rtpMarker) { + if (firstReceivedTimestamp == C.TIME_UNSET) { + firstReceivedTimestamp = timestamp; + } + long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp); + trackOutput.sampleMetadata( + timeUs, + isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0, + fragmentedSampleSizeBytes, + /* offset= */ 0, + /* cryptoData= */ null); + fragmentedSampleSizeBytes = 0; + isKeyFrame = false; + } + previousSequenceNumber = sequenceNumber; + } + + @Override + public void seek(long nextRtpTimestamp, long timeUs) { + firstReceivedTimestamp = nextRtpTimestamp; + fragmentedSampleSizeBytes = 0; + startTimeOffsetUs = timeUs; + } + + /** + * Parses and set VOP Coding type and resolution. The {@link ParsableByteArray#position} is + * preserved. + */ + private void parseVopHeader(ParsableByteArray data, boolean gotResolution) { + // Picture Segment Packets (RFC4629 Section 6.1). + // Search for SHORT_VIDEO_START_MARKER (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0). + int currentPosition = data.getPosition(); + + /* + * Parse short video header. + * + * These values are taken from Android's software H263 decoder. + */ + long shortVideoHeader = data.readUnsignedInt(); + if (((shortVideoHeader >> 10) & 0x3F) == 0x20) { + int header = data.peekUnsignedByte(); + int vopType = ((header >> 1) & 0x1); + if (!gotResolution && vopType == I_VOP) { + /* + * Parse resolution from source format. + * + * These values are taken from Android's software H263 decoder. + */ + int sourceFormat = ((header >> 2) & 0x07); + if (sourceFormat == 1) { + width = 128; + height = 96; + } else { + width = 176 << (sourceFormat - 2); + height = 144 << (sourceFormat - 2); + } + } + data.setPosition(currentPosition); + isKeyFrame = vopType == I_VOP; + return; + } + data.setPosition(currentPosition); + isKeyFrame = false; + } + + private static long toSampleUs( + long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) { + return startTimeOffsetUs + + Util.scaleLargeTimestamp( + (rtpTimestamp - firstReceivedRtpTimestamp), + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ MEDIA_CLOCK_FREQUENCY); + } +} From 36b976f70f33d1d40354f33b8bb36103dc295d75 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 16 Jun 2022 11:21:05 +0000 Subject: [PATCH 41/45] Version bump to exoplayer:2.18.0 and media3:1.0.0-beta01 PiperOrigin-RevId: 455350486 (cherry picked from commit 1c0b4b32a400ab9f7fb316ed7729ea2b8a32957f) --- .github/ISSUE_TEMPLATE/bug.yml | 1 + RELEASENOTES.md | 70 +++++++++++-------- constants.gradle | 4 +- .../media3/common/MediaLibraryInfo.java | 6 +- 4 files changed, 46 insertions(+), 35 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 41d4528ced..b29b2e92b0 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -17,6 +17,7 @@ body: label: Media3 Version description: What version of Media3 are you using? options: + - 1.0.0-beta01 - 1.0.0-alpha03 - 1.0.0-alpha02 - 1.0.0-alpha01 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3c7ad99305..47600cdbc7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,7 +1,16 @@ -# Release notes + Release notes ### Unreleased changes +* Extractors: + * Add support for AVI + ([#2092](https://github.com/google/ExoPlayer/issues/2092)). +* RTSP: + * Add RTP reader for H263 + ([#63](https://github.com/androidx/media/pull/63)). + +### 1.0.0-beta01 (2022-06-16) + * Core library: * Enable support for Android platform diagnostics via `MediaMetricsManager`. ExoPlayer will forward playback events and @@ -40,7 +49,7 @@ `DefaultTrackSelector.Parameters.Builder` instead of the deprecated `DefaultTrackSelector.ParametersBuilder`. * Add - `DefaultTrackSelector.Parameters.constrainAudioChannelCountToDeviceCapabilities`. + `DefaultTrackSelector.Parameters.constrainAudioChannelCountToDeviceCapabilities` which is enabled by default. When enabled, the `DefaultTrackSelector` will prefer audio tracks whose channel count does not exceed the device output capabilities. On handheld devices, the `DefaultTrackSelector` @@ -77,9 +86,6 @@ * Ensure the DRM session is always correctly updated when seeking immediately after a format change ([10274](https://github.com/google/ExoPlayer/issues/10274)). -* Ad playback / IMA: - * Decrease ad polling rate from every 100ms to every 200ms, to line up - with Media Rating Council (MRC) recommendations. * Text: * Change `Player.getCurrentCues()` to return `CueGroup` instead of `List`. @@ -95,18 +101,16 @@ * MP4: Parse bitrates from `esds` boxes. * Ogg: Allow duplicate Opus ID and comment headers ([#10038](https://github.com/google/ExoPlayer/issues/10038)). - * Add support for AVI - ([#2092](https://github.com/google/ExoPlayer/issues/2092)). * UI: - * Fix delivery of events to `OnClickListener`s set on `PlayerView` and - `LegacyPlayerView`, in the case that `useController=false` + * Fix delivery of events to `OnClickListener`s set on `PlayerView`, in the + case that `useController=false` ([#9605](https://github.com/google/ExoPlayer/issues/9605)). Also fix delivery of events to `OnLongClickListener` for all view configurations. * Fix incorrectly treating a sequence of touch events that exit the bounds - of `PlayerView` and `LegacyPlayerView` before `ACTION_UP` as a click + of `PlayerView` before `ACTION_UP` as a click ([#9861](https://github.com/google/ExoPlayer/issues/9861)). - * Fix `PlayerView` accessibility issue where it was not possible to - tapping would toggle playback rather than hiding the controls + * Fix `PlayerView` accessibility issue where tapping might toggle playback + rather than hiding the controls ([#8627](https://github.com/google/ExoPlayer/issues/8627)). * Rewrite `TrackSelectionView` and `TrackSelectionDialogBuilder` to work with the `Player` interface rather than `ExoPlayer`. This allows the @@ -163,36 +167,42 @@ ([#47](https://github.com/androidx/media/pull/64)). * Add RTP reader for OPUS ([#53](https://github.com/androidx/media/pull/53)). - * Add RTP reader for H263 - ([#63](https://github.com/androidx/media/pull/63)). * Session: - * Fix NPE in MediaControllerImplLegacy - ([#59](https://github.com/androidx/media/pull/59)). - * Update session position info on timeline - change([#51](https://github.com/androidx/media/issues/51)). - * Fix NPE in MediaControllerImplBase after releasing controller - ([#74](https://github.com/androidx/media/issues/74)). - * Rename `MediaSession.MediaSessionCallback` to `MediaSession.Callback`, - `MediaLibrarySession.MediaLibrarySessionCallback` to - `MediaLibrarySession.Callback` and - `MediaSession.Builder.setSessionCallback` to `setCallback`. - * Replace `MediaSession.MediaItemFiler` with + * Replace `MediaSession.MediaItemFiller` with `MediaSession.Callback.onAddMediaItems` to allow asynchronous resolution of requests. - * Forward legacy `MediaController` calls to play media to - `MediaSession.Callback.onAddMediaItems` instead of `onSetMediaUri`. * Support `setMediaItems(s)` methods when `MediaController` connects to a legacy media session. * Remove `MediaController.setMediaUri` and `MediaSession.Callback.onSetMediaUri`. The same functionality can be achieved by using `MediaController.setMediaItem` and `MediaSession.Callback.onAddMediaItems`. + * Forward legacy `MediaController` calls to play media to + `MediaSession.Callback.onAddMediaItems` instead of `onSetMediaUri`. + * Add `MediaNotification.Provider` and `DefaultMediaNotificationProvider` + to provide customization of the notification. + * Add `BitmapLoader` and `SimpleBitmapLoader` for downloading artwork + images. + * Add `MediaSession.setCustomLayout()` to provide backwards compatibility + with the legacy session. + * Add `MediaSession.setSessionExtras()` to provide feature parity with + legacy session. + * Rename `MediaSession.MediaSessionCallback` to `MediaSession.Callback`, + `MediaLibrarySession.MediaLibrarySessionCallback` to + `MediaLibrarySession.Callback` and + `MediaSession.Builder.setSessionCallback` to `setCallback`. + * Fix NPE in `MediaControllerImplLegacy` + ([#59](https://github.com/androidx/media/pull/59)). + * Update session position info on timeline + change([#51](https://github.com/androidx/media/issues/51)). + * Fix NPE in `MediaControllerImplBase` after releasing controller + ([#74](https://github.com/androidx/media/issues/74)). * Fix `IndexOutOfBoundsException` when setting less media items than in the current playlist ([#86](https://github.com/androidx/media/issues/86)). -* Data sources: - * Rename `DummyDataSource` to `PlaceholderDataSource`. - * Workaround OkHttp interrupt handling. +* Ad playback / IMA: + * Decrease ad polling rate from every 100ms to every 200ms, to line up + with Media Rating Council (MRC) recommendations. * FFmpeg extension: * Update CMake version to `3.21.0+` to avoid a CMake bug causing AndroidStudio's gradle sync to fail diff --git a/constants.gradle b/constants.gradle index 0d49ac63ab..86c624e778 100644 --- a/constants.gradle +++ b/constants.gradle @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. project.ext { - releaseVersion = '1.0.0-alpha03' - releaseVersionCode = 1_000_000_0_03 + releaseVersion = '1.0.0-beta01' + releaseVersionCode = 1_000_000_1_01 minSdkVersion = 16 appTargetSdkVersion = 29 // Upgrading this requires [Internal ref: b/193254928] to be fixed, or some diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java index 0cf954df4f..4e87f65806 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java @@ -29,11 +29,11 @@ public final class MediaLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3" or "1.2.3-beta01". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "1.0.0-alpha03"; + public static final String VERSION = "1.0.0-beta01"; /** The version of the library expressed as {@code TAG + "/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-alpha03"; + public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-beta01"; /** * The version of the library expressed as an integer, for example 1002003300. @@ -47,7 +47,7 @@ public final class MediaLibraryInfo { * (123-045-006-3-00). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 1_000_000_0_03; + public static final int VERSION_INT = 1_000_000_1_01; /** Whether the library was compiled with {@link Assertions} checks enabled. */ public static final boolean ASSERTIONS_ENABLED = true; From e53a649a41fe55e9e9e9c59ea1f85eff4d87b84d Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 16 Jun 2022 11:50:36 +0000 Subject: [PATCH 42/45] Add lint base xml file for string.xml files Fixing lint errors in the string.xml files makes no sense because these are overridden with the next automated string import. Adding a lint-baseline.xml instead for the ui module. See https://issuetracker.google.com/208178382 #minor-release PiperOrigin-RevId: 455354304 (cherry picked from commit 61ab75b8b88050a12492c412e8c79ebf75fc1d9c) --- libraries/ui/build.gradle | 6 ++++ libraries/ui/lint-baseline.xml | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 libraries/ui/lint-baseline.xml diff --git a/libraries/ui/build.gradle b/libraries/ui/build.gradle index c911f434eb..35dba78fb3 100644 --- a/libraries/ui/build.gradle +++ b/libraries/ui/build.gradle @@ -15,6 +15,12 @@ apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" android.buildTypes.debug.testCoverageEnabled true +android { + lint { + baseline = file("lint-baseline.xml") + } +} + dependencies { implementation project(modulePrefix + 'lib-common') implementation 'androidx.media:media:' + androidxMediaVersion diff --git a/libraries/ui/lint-baseline.xml b/libraries/ui/lint-baseline.xml new file mode 100644 index 0000000000..0b6a29a68c --- /dev/null +++ b/libraries/ui/lint-baseline.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + From e2dff6e6feb1eb78531591494faff6b9e371b188 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 16 Jun 2022 13:54:23 +0000 Subject: [PATCH 43/45] Fix release notes #minor-release PiperOrigin-RevId: 455372269 (cherry picked from commit 4b4e7cb919ca4b2d9c889c5b82994c9d08991381) --- RELEASENOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 47600cdbc7..febe027e2e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,9 @@ ### 1.0.0-beta01 (2022-06-16) +This release corresponds to the +[ExoPlayer 2.18.0 release](https://github.com/google/ExoPlayer/releases/tag/r2.18.0). + * Core library: * Enable support for Android platform diagnostics via `MediaMetricsManager`. ExoPlayer will forward playback events and From 68e5612c0d5b06462c78b5a9961cc6ce3815b906 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 16 Jun 2022 13:56:10 +0000 Subject: [PATCH 44/45] Publish api.txt to media3 GitHub #minor-release PiperOrigin-RevId: 455372568 (cherry picked from commit c7f1b465d4dfcfb1657425e4557e47e627d127cf) --- api.txt | 1800 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1800 insertions(+) create mode 100644 api.txt diff --git a/api.txt b/api.txt new file mode 100644 index 0000000000..c4e42f5772 --- /dev/null +++ b/api.txt @@ -0,0 +1,1800 @@ +// Signature format: 3.0 +package androidx.media3.common { + + public final class AdOverlayInfo { + field public static final int PURPOSE_CLOSE_AD = 2; // 0x2 + field public static final int PURPOSE_CONTROLS = 1; // 0x1 + field public static final int PURPOSE_NOT_VISIBLE = 4; // 0x4 + field public static final int PURPOSE_OTHER = 3; // 0x3 + field @androidx.media3.common.AdOverlayInfo.Purpose public final int purpose; + field @Nullable public final String reasonDetail; + field public final android.view.View view; + } + + public static final class AdOverlayInfo.Builder { + ctor public AdOverlayInfo.Builder(android.view.View, @androidx.media3.common.AdOverlayInfo.Purpose int); + method public androidx.media3.common.AdOverlayInfo build(); + method public androidx.media3.common.AdOverlayInfo.Builder setDetailedReason(@Nullable String); + } + + @IntDef({androidx.media3.common.AdOverlayInfo.PURPOSE_CONTROLS, androidx.media3.common.AdOverlayInfo.PURPOSE_CLOSE_AD, androidx.media3.common.AdOverlayInfo.PURPOSE_OTHER, androidx.media3.common.AdOverlayInfo.PURPOSE_NOT_VISIBLE}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface AdOverlayInfo.Purpose { + } + + public interface AdViewProvider { + method public default java.util.List getAdOverlayInfos(); + method @Nullable public android.view.ViewGroup getAdViewGroup(); + } + + public final class AudioAttributes { + method @RequiresApi(21) public androidx.media3.common.AudioAttributes.AudioAttributesV21 getAudioAttributesV21(); + field public static final androidx.media3.common.AudioAttributes DEFAULT; + field @androidx.media3.common.C.AudioAllowedCapturePolicy public final int allowedCapturePolicy; + field @androidx.media3.common.C.AudioContentType public final int contentType; + field @androidx.media3.common.C.AudioFlags public final int flags; + field @androidx.media3.common.C.SpatializationBehavior public final int spatializationBehavior; + field @androidx.media3.common.C.AudioUsage public final int usage; + } + + @RequiresApi(21) public static final class AudioAttributes.AudioAttributesV21 { + field public final android.media.AudioAttributes audioAttributes; + } + + public static final class AudioAttributes.Builder { + ctor public AudioAttributes.Builder(); + method public androidx.media3.common.AudioAttributes build(); + method public androidx.media3.common.AudioAttributes.Builder setAllowedCapturePolicy(@androidx.media3.common.C.AudioAllowedCapturePolicy int); + method public androidx.media3.common.AudioAttributes.Builder setContentType(@androidx.media3.common.C.AudioContentType int); + method public androidx.media3.common.AudioAttributes.Builder setFlags(@androidx.media3.common.C.AudioFlags int); + method public androidx.media3.common.AudioAttributes.Builder setSpatializationBehavior(@androidx.media3.common.C.SpatializationBehavior int); + method public androidx.media3.common.AudioAttributes.Builder setUsage(@androidx.media3.common.C.AudioUsage int); + } + + public final class C { + field public static final int ALLOW_CAPTURE_BY_ALL = 1; // 0x1 + field public static final int ALLOW_CAPTURE_BY_NONE = 3; // 0x3 + field public static final int ALLOW_CAPTURE_BY_SYSTEM = 2; // 0x2 + field public static final int AUDIO_CONTENT_TYPE_MOVIE = 3; // 0x3 + field public static final int AUDIO_CONTENT_TYPE_MUSIC = 2; // 0x2 + field public static final int AUDIO_CONTENT_TYPE_SONIFICATION = 4; // 0x4 + field public static final int AUDIO_CONTENT_TYPE_SPEECH = 1; // 0x1 + field public static final int AUDIO_CONTENT_TYPE_UNKNOWN = 0; // 0x0 + field public static final java.util.UUID CLEARKEY_UUID; + field public static final java.util.UUID COMMON_PSSH_UUID; + field public static final int CONTENT_TYPE_DASH = 0; // 0x0 + field public static final int CONTENT_TYPE_HLS = 2; // 0x2 + field public static final int CONTENT_TYPE_OTHER = 4; // 0x4 + field public static final int CONTENT_TYPE_RTSP = 3; // 0x3 + field public static final int CONTENT_TYPE_SS = 1; // 0x1 + field public static final int CRYPTO_TYPE_CUSTOM_BASE = 10000; // 0x2710 + field public static final int CRYPTO_TYPE_FRAMEWORK = 2; // 0x2 + field public static final int CRYPTO_TYPE_NONE = 0; // 0x0 + field public static final int CRYPTO_TYPE_UNSUPPORTED = 1; // 0x1 + field public static final long DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS = 3000L; // 0xbb8L + field public static final long DEFAULT_SEEK_BACK_INCREMENT_MS = 5000L; // 0x1388L + field public static final long DEFAULT_SEEK_FORWARD_INCREMENT_MS = 15000L; // 0x3a98L + field public static final int FLAG_AUDIBILITY_ENFORCED = 1; // 0x1 + field public static final int INDEX_UNSET = -1; // 0xffffffff + field public static final String LANGUAGE_UNDETERMINED = "und"; + field public static final int LENGTH_UNSET = -1; // 0xffffffff + field public static final java.util.UUID PLAYREADY_UUID; + field public static final float RATE_UNSET = -3.4028235E38f; + field public static final int ROLE_FLAG_ALTERNATE = 2; // 0x2 + field public static final int ROLE_FLAG_CAPTION = 64; // 0x40 + field public static final int ROLE_FLAG_COMMENTARY = 8; // 0x8 + field public static final int ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND = 1024; // 0x400 + field public static final int ROLE_FLAG_DESCRIBES_VIDEO = 512; // 0x200 + field public static final int ROLE_FLAG_DUB = 16; // 0x10 + field public static final int ROLE_FLAG_EASY_TO_READ = 8192; // 0x2000 + field public static final int ROLE_FLAG_EMERGENCY = 32; // 0x20 + field public static final int ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY = 2048; // 0x800 + field public static final int ROLE_FLAG_MAIN = 1; // 0x1 + field public static final int ROLE_FLAG_SIGN = 256; // 0x100 + field public static final int ROLE_FLAG_SUBTITLE = 128; // 0x80 + field public static final int ROLE_FLAG_SUPPLEMENTARY = 4; // 0x4 + field public static final int ROLE_FLAG_TRANSCRIBES_DIALOG = 4096; // 0x1000 + field public static final int ROLE_FLAG_TRICK_PLAY = 16384; // 0x4000 + field public static final int SELECTION_FLAG_AUTOSELECT = 4; // 0x4 + field public static final int SELECTION_FLAG_DEFAULT = 1; // 0x1 + field public static final int SELECTION_FLAG_FORCED = 2; // 0x2 + field public static final int SPATIALIZATION_BEHAVIOR_AUTO = 0; // 0x0 + field public static final int SPATIALIZATION_BEHAVIOR_NEVER = 1; // 0x1 + field public static final long TIME_END_OF_SOURCE = -9223372036854775808L; // 0x8000000000000000L + field public static final long TIME_UNSET = -9223372036854775807L; // 0x8000000000000001L + field public static final int TRACK_TYPE_AUDIO = 1; // 0x1 + field public static final int TRACK_TYPE_CAMERA_MOTION = 6; // 0x6 + field public static final int TRACK_TYPE_CUSTOM_BASE = 10000; // 0x2710 + field public static final int TRACK_TYPE_DEFAULT = 0; // 0x0 + field public static final int TRACK_TYPE_IMAGE = 4; // 0x4 + field public static final int TRACK_TYPE_METADATA = 5; // 0x5 + field public static final int TRACK_TYPE_NONE = -2; // 0xfffffffe + field public static final int TRACK_TYPE_TEXT = 3; // 0x3 + field public static final int TRACK_TYPE_UNKNOWN = -1; // 0xffffffff + field public static final int TRACK_TYPE_VIDEO = 2; // 0x2 + field public static final int USAGE_ALARM = 4; // 0x4 + field public static final int USAGE_ASSISTANCE_ACCESSIBILITY = 11; // 0xb + field public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = 12; // 0xc + field public static final int USAGE_ASSISTANCE_SONIFICATION = 13; // 0xd + field public static final int USAGE_ASSISTANT = 16; // 0x10 + field public static final int USAGE_GAME = 14; // 0xe + field public static final int USAGE_MEDIA = 1; // 0x1 + field public static final int USAGE_NOTIFICATION = 5; // 0x5 + field public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED = 9; // 0x9 + field public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT = 8; // 0x8 + field public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST = 7; // 0x7 + field public static final int USAGE_NOTIFICATION_EVENT = 10; // 0xa + field public static final int USAGE_NOTIFICATION_RINGTONE = 6; // 0x6 + field public static final int USAGE_UNKNOWN = 0; // 0x0 + field public static final int USAGE_VOICE_COMMUNICATION = 2; // 0x2 + field public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = 3; // 0x3 + field public static final java.util.UUID UUID_NIL; + field public static final int WAKE_MODE_LOCAL = 1; // 0x1 + field public static final int WAKE_MODE_NETWORK = 2; // 0x2 + field public static final int WAKE_MODE_NONE = 0; // 0x0 + field public static final java.util.UUID WIDEVINE_UUID; + } + + @IntDef({androidx.media3.common.C.ALLOW_CAPTURE_BY_ALL, androidx.media3.common.C.ALLOW_CAPTURE_BY_NONE, androidx.media3.common.C.ALLOW_CAPTURE_BY_SYSTEM}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface C.AudioAllowedCapturePolicy { + } + + @IntDef({androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE, androidx.media3.common.C.AUDIO_CONTENT_TYPE_MUSIC, androidx.media3.common.C.AUDIO_CONTENT_TYPE_SONIFICATION, androidx.media3.common.C.AUDIO_CONTENT_TYPE_SPEECH, androidx.media3.common.C.AUDIO_CONTENT_TYPE_UNKNOWN}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface C.AudioContentType { + } + + @IntDef(flag=true, value={androidx.media3.common.C.FLAG_AUDIBILITY_ENFORCED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface C.AudioFlags { + } + + @IntDef({androidx.media3.common.C.USAGE_ALARM, androidx.media3.common.C.USAGE_ASSISTANCE_ACCESSIBILITY, androidx.media3.common.C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, androidx.media3.common.C.USAGE_ASSISTANCE_SONIFICATION, androidx.media3.common.C.USAGE_ASSISTANT, androidx.media3.common.C.USAGE_GAME, androidx.media3.common.C.USAGE_MEDIA, androidx.media3.common.C.USAGE_NOTIFICATION, androidx.media3.common.C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED, androidx.media3.common.C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT, androidx.media3.common.C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST, androidx.media3.common.C.USAGE_NOTIFICATION_EVENT, androidx.media3.common.C.USAGE_NOTIFICATION_RINGTONE, androidx.media3.common.C.USAGE_UNKNOWN, androidx.media3.common.C.USAGE_VOICE_COMMUNICATION, androidx.media3.common.C.USAGE_VOICE_COMMUNICATION_SIGNALLING}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface C.AudioUsage { + } + + @IntDef({androidx.media3.common.C.CONTENT_TYPE_DASH, androidx.media3.common.C.CONTENT_TYPE_SS, androidx.media3.common.C.CONTENT_TYPE_HLS, androidx.media3.common.C.CONTENT_TYPE_RTSP, androidx.media3.common.C.CONTENT_TYPE_OTHER}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface C.ContentType { + } + + @IntDef(open=true, value={androidx.media3.common.C.CRYPTO_TYPE_UNSUPPORTED, androidx.media3.common.C.CRYPTO_TYPE_NONE, androidx.media3.common.C.CRYPTO_TYPE_FRAMEWORK}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface C.CryptoType { + } + + @IntDef(flag=true, value={androidx.media3.common.C.ROLE_FLAG_MAIN, androidx.media3.common.C.ROLE_FLAG_ALTERNATE, androidx.media3.common.C.ROLE_FLAG_SUPPLEMENTARY, androidx.media3.common.C.ROLE_FLAG_COMMENTARY, androidx.media3.common.C.ROLE_FLAG_DUB, androidx.media3.common.C.ROLE_FLAG_EMERGENCY, androidx.media3.common.C.ROLE_FLAG_CAPTION, androidx.media3.common.C.ROLE_FLAG_SUBTITLE, androidx.media3.common.C.ROLE_FLAG_SIGN, androidx.media3.common.C.ROLE_FLAG_DESCRIBES_VIDEO, androidx.media3.common.C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND, androidx.media3.common.C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY, androidx.media3.common.C.ROLE_FLAG_TRANSCRIBES_DIALOG, androidx.media3.common.C.ROLE_FLAG_EASY_TO_READ, androidx.media3.common.C.ROLE_FLAG_TRICK_PLAY}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface C.RoleFlags { + } + + @IntDef(flag=true, value={androidx.media3.common.C.SELECTION_FLAG_DEFAULT, androidx.media3.common.C.SELECTION_FLAG_FORCED, androidx.media3.common.C.SELECTION_FLAG_AUTOSELECT}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface C.SelectionFlags { + } + + @IntDef({androidx.media3.common.C.SPATIALIZATION_BEHAVIOR_AUTO, androidx.media3.common.C.SPATIALIZATION_BEHAVIOR_NEVER}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface C.SpatializationBehavior { + } + + @IntDef(open=true, value={androidx.media3.common.C.TRACK_TYPE_UNKNOWN, androidx.media3.common.C.TRACK_TYPE_DEFAULT, androidx.media3.common.C.TRACK_TYPE_AUDIO, androidx.media3.common.C.TRACK_TYPE_VIDEO, androidx.media3.common.C.TRACK_TYPE_TEXT, androidx.media3.common.C.TRACK_TYPE_IMAGE, androidx.media3.common.C.TRACK_TYPE_METADATA, androidx.media3.common.C.TRACK_TYPE_CAMERA_MOTION, androidx.media3.common.C.TRACK_TYPE_NONE}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface C.TrackType { + } + + @IntDef({androidx.media3.common.C.WAKE_MODE_NONE, androidx.media3.common.C.WAKE_MODE_LOCAL, androidx.media3.common.C.WAKE_MODE_NETWORK}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface C.WakeMode { + } + + public final class DeviceInfo { + field public static final int PLAYBACK_TYPE_LOCAL = 0; // 0x0 + field public static final int PLAYBACK_TYPE_REMOTE = 1; // 0x1 + field public static final androidx.media3.common.DeviceInfo UNKNOWN; + field public final int maxVolume; + field public final int minVolume; + field @androidx.media3.common.DeviceInfo.PlaybackType public final int playbackType; + } + + @IntDef({androidx.media3.common.DeviceInfo.PLAYBACK_TYPE_LOCAL, androidx.media3.common.DeviceInfo.PLAYBACK_TYPE_REMOTE}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface DeviceInfo.PlaybackType { + } + + public interface ErrorMessageProvider { + method public android.util.Pair getErrorMessage(T); + } + + public final class Format { + field public static final int NO_VALUE = -1; // 0xffffffff + field public final int channelCount; + field @Nullable public final String codecs; + field @Nullable public final String containerMimeType; + field public final float frameRate; + field public final int height; + field @Nullable public final String id; + field @Nullable public final String label; + field @Nullable public final String language; + field public final float pixelWidthHeightRatio; + field @androidx.media3.common.C.RoleFlags public final int roleFlags; + field @Nullable public final String sampleMimeType; + field public final int sampleRate; + field @androidx.media3.common.C.SelectionFlags public final int selectionFlags; + field public final int width; + } + + public final class HeartRating extends androidx.media3.common.Rating { + ctor public HeartRating(); + ctor public HeartRating(boolean); + method public boolean isHeart(); + method public boolean isRated(); + } + + public final class MediaItem { + method public androidx.media3.common.MediaItem.Builder buildUpon(); + method public static androidx.media3.common.MediaItem fromUri(String); + method public static androidx.media3.common.MediaItem fromUri(android.net.Uri); + field public static final String DEFAULT_MEDIA_ID = ""; + field public static final androidx.media3.common.MediaItem EMPTY; + field public final androidx.media3.common.MediaItem.ClippingConfiguration clippingConfiguration; + field public final androidx.media3.common.MediaItem.LiveConfiguration liveConfiguration; + field @Nullable public final androidx.media3.common.MediaItem.LocalConfiguration localConfiguration; + field public final String mediaId; + field public final androidx.media3.common.MediaMetadata mediaMetadata; + field public final androidx.media3.common.MediaItem.RequestMetadata requestMetadata; + } + + public static final class MediaItem.AdsConfiguration { + method public androidx.media3.common.MediaItem.AdsConfiguration.Builder buildUpon(); + field public final android.net.Uri adTagUri; + field @Nullable public final Object adsId; + } + + public static final class MediaItem.AdsConfiguration.Builder { + ctor public MediaItem.AdsConfiguration.Builder(android.net.Uri); + method public androidx.media3.common.MediaItem.AdsConfiguration build(); + method public androidx.media3.common.MediaItem.AdsConfiguration.Builder setAdTagUri(android.net.Uri); + method public androidx.media3.common.MediaItem.AdsConfiguration.Builder setAdsId(@Nullable Object); + } + + public static final class MediaItem.Builder { + ctor public MediaItem.Builder(); + method public androidx.media3.common.MediaItem build(); + method public androidx.media3.common.MediaItem.Builder setAdsConfiguration(@Nullable androidx.media3.common.MediaItem.AdsConfiguration); + method public androidx.media3.common.MediaItem.Builder setClippingConfiguration(androidx.media3.common.MediaItem.ClippingConfiguration); + method public androidx.media3.common.MediaItem.Builder setDrmConfiguration(@Nullable androidx.media3.common.MediaItem.DrmConfiguration); + method public androidx.media3.common.MediaItem.Builder setLiveConfiguration(androidx.media3.common.MediaItem.LiveConfiguration); + method public androidx.media3.common.MediaItem.Builder setMediaId(String); + method public androidx.media3.common.MediaItem.Builder setMediaMetadata(androidx.media3.common.MediaMetadata); + method public androidx.media3.common.MediaItem.Builder setMimeType(@Nullable String); + method public androidx.media3.common.MediaItem.Builder setRequestMetadata(androidx.media3.common.MediaItem.RequestMetadata); + method public androidx.media3.common.MediaItem.Builder setSubtitleConfigurations(java.util.List); + method public androidx.media3.common.MediaItem.Builder setTag(@Nullable Object); + method public androidx.media3.common.MediaItem.Builder setUri(@Nullable String); + method public androidx.media3.common.MediaItem.Builder setUri(@Nullable android.net.Uri); + } + + public static class MediaItem.ClippingConfiguration { + method public androidx.media3.common.MediaItem.ClippingConfiguration.Builder buildUpon(); + field public static final androidx.media3.common.MediaItem.ClippingConfiguration UNSET; + field public final long endPositionMs; + field public final boolean relativeToDefaultPosition; + field public final boolean relativeToLiveWindow; + field @IntRange(from=0) public final long startPositionMs; + field public final boolean startsAtKeyFrame; + } + + public static final class MediaItem.ClippingConfiguration.Builder { + ctor public MediaItem.ClippingConfiguration.Builder(); + method public androidx.media3.common.MediaItem.ClippingConfiguration build(); + method public androidx.media3.common.MediaItem.ClippingConfiguration.Builder setEndPositionMs(long); + method public androidx.media3.common.MediaItem.ClippingConfiguration.Builder setRelativeToDefaultPosition(boolean); + method public androidx.media3.common.MediaItem.ClippingConfiguration.Builder setRelativeToLiveWindow(boolean); + method public androidx.media3.common.MediaItem.ClippingConfiguration.Builder setStartPositionMs(@IntRange(from=0) long); + method public androidx.media3.common.MediaItem.ClippingConfiguration.Builder setStartsAtKeyFrame(boolean); + } + + public static final class MediaItem.DrmConfiguration { + method public androidx.media3.common.MediaItem.DrmConfiguration.Builder buildUpon(); + method @Nullable public byte[] getKeySetId(); + field public final boolean forceDefaultLicenseUri; + field public final com.google.common.collect.ImmutableList forcedSessionTrackTypes; + field public final com.google.common.collect.ImmutableMap licenseRequestHeaders; + field @Nullable public final android.net.Uri licenseUri; + field public final boolean multiSession; + field public final boolean playClearContentWithoutKey; + field public final java.util.UUID scheme; + } + + public static final class MediaItem.DrmConfiguration.Builder { + ctor public MediaItem.DrmConfiguration.Builder(java.util.UUID); + method public androidx.media3.common.MediaItem.DrmConfiguration build(); + method public androidx.media3.common.MediaItem.DrmConfiguration.Builder setForceDefaultLicenseUri(boolean); + method public androidx.media3.common.MediaItem.DrmConfiguration.Builder setForceSessionsForAudioAndVideoTracks(boolean); + method public androidx.media3.common.MediaItem.DrmConfiguration.Builder setForcedSessionTrackTypes(java.util.List); + method public androidx.media3.common.MediaItem.DrmConfiguration.Builder setKeySetId(@Nullable byte[]); + method public androidx.media3.common.MediaItem.DrmConfiguration.Builder setLicenseRequestHeaders(java.util.Map); + method public androidx.media3.common.MediaItem.DrmConfiguration.Builder setLicenseUri(@Nullable android.net.Uri); + method public androidx.media3.common.MediaItem.DrmConfiguration.Builder setLicenseUri(@Nullable String); + method public androidx.media3.common.MediaItem.DrmConfiguration.Builder setMultiSession(boolean); + method public androidx.media3.common.MediaItem.DrmConfiguration.Builder setPlayClearContentWithoutKey(boolean); + method public androidx.media3.common.MediaItem.DrmConfiguration.Builder setScheme(java.util.UUID); + } + + public static final class MediaItem.LiveConfiguration { + method public androidx.media3.common.MediaItem.LiveConfiguration.Builder buildUpon(); + field public static final androidx.media3.common.MediaItem.LiveConfiguration UNSET; + field public final long maxOffsetMs; + field public final float maxPlaybackSpeed; + field public final long minOffsetMs; + field public final float minPlaybackSpeed; + field public final long targetOffsetMs; + } + + public static final class MediaItem.LiveConfiguration.Builder { + ctor public MediaItem.LiveConfiguration.Builder(); + method public androidx.media3.common.MediaItem.LiveConfiguration build(); + method public androidx.media3.common.MediaItem.LiveConfiguration.Builder setMaxOffsetMs(long); + method public androidx.media3.common.MediaItem.LiveConfiguration.Builder setMaxPlaybackSpeed(float); + method public androidx.media3.common.MediaItem.LiveConfiguration.Builder setMinOffsetMs(long); + method public androidx.media3.common.MediaItem.LiveConfiguration.Builder setMinPlaybackSpeed(float); + method public androidx.media3.common.MediaItem.LiveConfiguration.Builder setTargetOffsetMs(long); + } + + public static class MediaItem.LocalConfiguration { + field @Nullable public final androidx.media3.common.MediaItem.AdsConfiguration adsConfiguration; + field @Nullable public final androidx.media3.common.MediaItem.DrmConfiguration drmConfiguration; + field @Nullable public final String mimeType; + field public final com.google.common.collect.ImmutableList subtitleConfigurations; + field @Nullable public final Object tag; + field public final android.net.Uri uri; + } + + public static final class MediaItem.RequestMetadata { + method public androidx.media3.common.MediaItem.RequestMetadata.Builder buildUpon(); + field public static final androidx.media3.common.MediaItem.RequestMetadata EMPTY; + field @Nullable public final android.os.Bundle extras; + field @Nullable public final android.net.Uri mediaUri; + field @Nullable public final String searchQuery; + } + + public static final class MediaItem.RequestMetadata.Builder { + ctor public MediaItem.RequestMetadata.Builder(); + method public androidx.media3.common.MediaItem.RequestMetadata build(); + method public androidx.media3.common.MediaItem.RequestMetadata.Builder setExtras(@Nullable android.os.Bundle); + method public androidx.media3.common.MediaItem.RequestMetadata.Builder setMediaUri(@Nullable android.net.Uri); + method public androidx.media3.common.MediaItem.RequestMetadata.Builder setSearchQuery(@Nullable String); + } + + public static class MediaItem.SubtitleConfiguration { + method public androidx.media3.common.MediaItem.SubtitleConfiguration.Builder buildUpon(); + field @Nullable public final String id; + field @Nullable public final String label; + field @Nullable public final String language; + field @Nullable public final String mimeType; + field @androidx.media3.common.C.RoleFlags public final int roleFlags; + field @androidx.media3.common.C.SelectionFlags public final int selectionFlags; + field public final android.net.Uri uri; + } + + public static final class MediaItem.SubtitleConfiguration.Builder { + ctor public MediaItem.SubtitleConfiguration.Builder(android.net.Uri); + method public androidx.media3.common.MediaItem.SubtitleConfiguration build(); + method public androidx.media3.common.MediaItem.SubtitleConfiguration.Builder setId(@Nullable String); + method public androidx.media3.common.MediaItem.SubtitleConfiguration.Builder setLabel(@Nullable String); + method public androidx.media3.common.MediaItem.SubtitleConfiguration.Builder setLanguage(@Nullable String); + method public androidx.media3.common.MediaItem.SubtitleConfiguration.Builder setMimeType(@Nullable String); + method public androidx.media3.common.MediaItem.SubtitleConfiguration.Builder setRoleFlags(@androidx.media3.common.C.RoleFlags int); + method public androidx.media3.common.MediaItem.SubtitleConfiguration.Builder setSelectionFlags(@androidx.media3.common.C.SelectionFlags int); + method public androidx.media3.common.MediaItem.SubtitleConfiguration.Builder setUri(android.net.Uri); + } + + public final class MediaMetadata { + method public androidx.media3.common.MediaMetadata.Builder buildUpon(); + field public static final androidx.media3.common.MediaMetadata EMPTY; + field public static final int FOLDER_TYPE_ALBUMS = 2; // 0x2 + field public static final int FOLDER_TYPE_ARTISTS = 3; // 0x3 + field public static final int FOLDER_TYPE_GENRES = 4; // 0x4 + field public static final int FOLDER_TYPE_MIXED = 0; // 0x0 + field public static final int FOLDER_TYPE_NONE = -1; // 0xffffffff + field public static final int FOLDER_TYPE_PLAYLISTS = 5; // 0x5 + field public static final int FOLDER_TYPE_TITLES = 1; // 0x1 + field public static final int FOLDER_TYPE_YEARS = 6; // 0x6 + field public static final int PICTURE_TYPE_ARTIST_PERFORMER = 8; // 0x8 + field public static final int PICTURE_TYPE_A_BRIGHT_COLORED_FISH = 17; // 0x11 + field public static final int PICTURE_TYPE_BACK_COVER = 4; // 0x4 + field public static final int PICTURE_TYPE_BAND_ARTIST_LOGO = 19; // 0x13 + field public static final int PICTURE_TYPE_BAND_ORCHESTRA = 10; // 0xa + field public static final int PICTURE_TYPE_COMPOSER = 11; // 0xb + field public static final int PICTURE_TYPE_CONDUCTOR = 9; // 0x9 + field public static final int PICTURE_TYPE_DURING_PERFORMANCE = 15; // 0xf + field public static final int PICTURE_TYPE_DURING_RECORDING = 14; // 0xe + field public static final int PICTURE_TYPE_FILE_ICON = 1; // 0x1 + field public static final int PICTURE_TYPE_FILE_ICON_OTHER = 2; // 0x2 + field public static final int PICTURE_TYPE_FRONT_COVER = 3; // 0x3 + field public static final int PICTURE_TYPE_ILLUSTRATION = 18; // 0x12 + field public static final int PICTURE_TYPE_LEAD_ARTIST_PERFORMER = 7; // 0x7 + field public static final int PICTURE_TYPE_LEAFLET_PAGE = 5; // 0x5 + field public static final int PICTURE_TYPE_LYRICIST = 12; // 0xc + field public static final int PICTURE_TYPE_MEDIA = 6; // 0x6 + field public static final int PICTURE_TYPE_MOVIE_VIDEO_SCREEN_CAPTURE = 16; // 0x10 + field public static final int PICTURE_TYPE_OTHER = 0; // 0x0 + field public static final int PICTURE_TYPE_PUBLISHER_STUDIO_LOGO = 20; // 0x14 + field public static final int PICTURE_TYPE_RECORDING_LOCATION = 13; // 0xd + field @Nullable public final CharSequence albumArtist; + field @Nullable public final CharSequence albumTitle; + field @Nullable public final CharSequence artist; + field @Nullable public final byte[] artworkData; + field @Nullable @androidx.media3.common.MediaMetadata.PictureType public final Integer artworkDataType; + field @Nullable public final android.net.Uri artworkUri; + field @Nullable public final CharSequence compilation; + field @Nullable public final CharSequence composer; + field @Nullable public final CharSequence conductor; + field @Nullable public final CharSequence description; + field @Nullable public final Integer discNumber; + field @Nullable public final CharSequence displayTitle; + field @Nullable public final android.os.Bundle extras; + field @Nullable @androidx.media3.common.MediaMetadata.FolderType public final Integer folderType; + field @Nullable public final CharSequence genre; + field @Nullable public final Boolean isPlayable; + field @Nullable public final androidx.media3.common.Rating overallRating; + field @Nullable public final Integer recordingDay; + field @Nullable public final Integer recordingMonth; + field @Nullable public final Integer recordingYear; + field @Nullable public final Integer releaseDay; + field @Nullable public final Integer releaseMonth; + field @Nullable public final Integer releaseYear; + field @Nullable public final CharSequence station; + field @Nullable public final CharSequence subtitle; + field @Nullable public final CharSequence title; + field @Nullable public final Integer totalDiscCount; + field @Nullable public final Integer totalTrackCount; + field @Nullable public final Integer trackNumber; + field @Nullable public final androidx.media3.common.Rating userRating; + field @Nullable public final CharSequence writer; + } + + public static final class MediaMetadata.Builder { + ctor public MediaMetadata.Builder(); + method public androidx.media3.common.MediaMetadata build(); + method public androidx.media3.common.MediaMetadata.Builder maybeSetArtworkData(byte[], @androidx.media3.common.MediaMetadata.PictureType int); + method public androidx.media3.common.MediaMetadata.Builder setAlbumArtist(@Nullable CharSequence); + method public androidx.media3.common.MediaMetadata.Builder setAlbumTitle(@Nullable CharSequence); + method public androidx.media3.common.MediaMetadata.Builder setArtist(@Nullable CharSequence); + method public androidx.media3.common.MediaMetadata.Builder setArtworkData(@Nullable byte[], @Nullable @androidx.media3.common.MediaMetadata.PictureType Integer); + method public androidx.media3.common.MediaMetadata.Builder setArtworkUri(@Nullable android.net.Uri); + method public androidx.media3.common.MediaMetadata.Builder setCompilation(@Nullable CharSequence); + method public androidx.media3.common.MediaMetadata.Builder setComposer(@Nullable CharSequence); + method public androidx.media3.common.MediaMetadata.Builder setConductor(@Nullable CharSequence); + method public androidx.media3.common.MediaMetadata.Builder setDescription(@Nullable CharSequence); + method public androidx.media3.common.MediaMetadata.Builder setDiscNumber(@Nullable Integer); + method public androidx.media3.common.MediaMetadata.Builder setDisplayTitle(@Nullable CharSequence); + method public androidx.media3.common.MediaMetadata.Builder setExtras(@Nullable android.os.Bundle); + method public androidx.media3.common.MediaMetadata.Builder setFolderType(@Nullable @androidx.media3.common.MediaMetadata.FolderType Integer); + method public androidx.media3.common.MediaMetadata.Builder setGenre(@Nullable CharSequence); + method public androidx.media3.common.MediaMetadata.Builder setIsPlayable(@Nullable Boolean); + method public androidx.media3.common.MediaMetadata.Builder setOverallRating(@Nullable androidx.media3.common.Rating); + method public androidx.media3.common.MediaMetadata.Builder setRecordingDay(@IntRange(from=1, to=31) @Nullable Integer); + method public androidx.media3.common.MediaMetadata.Builder setRecordingMonth(@IntRange(from=1, to=12) @Nullable Integer); + method public androidx.media3.common.MediaMetadata.Builder setRecordingYear(@Nullable Integer); + method public androidx.media3.common.MediaMetadata.Builder setReleaseDay(@IntRange(from=1, to=31) @Nullable Integer); + method public androidx.media3.common.MediaMetadata.Builder setReleaseMonth(@IntRange(from=1, to=12) @Nullable Integer); + method public androidx.media3.common.MediaMetadata.Builder setReleaseYear(@Nullable Integer); + method public androidx.media3.common.MediaMetadata.Builder setStation(@Nullable CharSequence); + method public androidx.media3.common.MediaMetadata.Builder setSubtitle(@Nullable CharSequence); + method public androidx.media3.common.MediaMetadata.Builder setTitle(@Nullable CharSequence); + method public androidx.media3.common.MediaMetadata.Builder setTotalDiscCount(@Nullable Integer); + method public androidx.media3.common.MediaMetadata.Builder setTotalTrackCount(@Nullable Integer); + method public androidx.media3.common.MediaMetadata.Builder setTrackNumber(@Nullable Integer); + method public androidx.media3.common.MediaMetadata.Builder setUserRating(@Nullable androidx.media3.common.Rating); + method public androidx.media3.common.MediaMetadata.Builder setWriter(@Nullable CharSequence); + } + + @IntDef({androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE, androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED, androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES, androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS, androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS, androidx.media3.common.MediaMetadata.FOLDER_TYPE_GENRES, androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS, androidx.media3.common.MediaMetadata.FOLDER_TYPE_YEARS}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface MediaMetadata.FolderType { + } + + @IntDef({androidx.media3.common.MediaMetadata.PICTURE_TYPE_OTHER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_FILE_ICON, androidx.media3.common.MediaMetadata.PICTURE_TYPE_FILE_ICON_OTHER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_FRONT_COVER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_BACK_COVER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_LEAFLET_PAGE, androidx.media3.common.MediaMetadata.PICTURE_TYPE_MEDIA, androidx.media3.common.MediaMetadata.PICTURE_TYPE_LEAD_ARTIST_PERFORMER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_ARTIST_PERFORMER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_CONDUCTOR, androidx.media3.common.MediaMetadata.PICTURE_TYPE_BAND_ORCHESTRA, androidx.media3.common.MediaMetadata.PICTURE_TYPE_COMPOSER, androidx.media3.common.MediaMetadata.PICTURE_TYPE_LYRICIST, androidx.media3.common.MediaMetadata.PICTURE_TYPE_RECORDING_LOCATION, androidx.media3.common.MediaMetadata.PICTURE_TYPE_DURING_RECORDING, androidx.media3.common.MediaMetadata.PICTURE_TYPE_DURING_PERFORMANCE, androidx.media3.common.MediaMetadata.PICTURE_TYPE_MOVIE_VIDEO_SCREEN_CAPTURE, androidx.media3.common.MediaMetadata.PICTURE_TYPE_A_BRIGHT_COLORED_FISH, androidx.media3.common.MediaMetadata.PICTURE_TYPE_ILLUSTRATION, androidx.media3.common.MediaMetadata.PICTURE_TYPE_BAND_ARTIST_LOGO, androidx.media3.common.MediaMetadata.PICTURE_TYPE_PUBLISHER_STUDIO_LOGO}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface MediaMetadata.PictureType { + } + + public final class MimeTypes { + field public static final String APPLICATION_AIT = "application/vnd.dvb.ait"; + field public static final String APPLICATION_CEA608 = "application/cea-608"; + field public static final String APPLICATION_CEA708 = "application/cea-708"; + field public static final String APPLICATION_DVBSUBS = "application/dvbsubs"; + field public static final String APPLICATION_ID3 = "application/id3"; + field public static final String APPLICATION_M3U8 = "application/x-mpegURL"; + field public static final String APPLICATION_MATROSKA = "application/x-matroska"; + field public static final String APPLICATION_MP4 = "application/mp4"; + field public static final String APPLICATION_MP4CEA608 = "application/x-mp4-cea-608"; + field public static final String APPLICATION_MP4VTT = "application/x-mp4-vtt"; + field public static final String APPLICATION_MPD = "application/dash+xml"; + field public static final String APPLICATION_PGS = "application/pgs"; + field public static final String APPLICATION_RAWCC = "application/x-rawcc"; + field public static final String APPLICATION_RTSP = "application/x-rtsp"; + field public static final String APPLICATION_SS = "application/vnd.ms-sstr+xml"; + field public static final String APPLICATION_SUBRIP = "application/x-subrip"; + field public static final String APPLICATION_TTML = "application/ttml+xml"; + field public static final String APPLICATION_TX3G = "application/x-quicktime-tx3g"; + field public static final String APPLICATION_VOBSUB = "application/vobsub"; + field public static final String APPLICATION_WEBM = "application/webm"; + field public static final String AUDIO_AAC = "audio/mp4a-latm"; + field public static final String AUDIO_AC3 = "audio/ac3"; + field public static final String AUDIO_AC4 = "audio/ac4"; + field public static final String AUDIO_ALAC = "audio/alac"; + field public static final String AUDIO_ALAW = "audio/g711-alaw"; + field public static final String AUDIO_AMR = "audio/amr"; + field public static final String AUDIO_AMR_NB = "audio/3gpp"; + field public static final String AUDIO_AMR_WB = "audio/amr-wb"; + field public static final String AUDIO_DTS = "audio/vnd.dts"; + field public static final String AUDIO_DTS_EXPRESS = "audio/vnd.dts.hd;profile=lbr"; + field public static final String AUDIO_DTS_HD = "audio/vnd.dts.hd"; + field public static final String AUDIO_E_AC3 = "audio/eac3"; + field public static final String AUDIO_E_AC3_JOC = "audio/eac3-joc"; + field public static final String AUDIO_FLAC = "audio/flac"; + field public static final String AUDIO_MIDI = "audio/midi"; + field public static final String AUDIO_MLAW = "audio/g711-mlaw"; + field public static final String AUDIO_MP4 = "audio/mp4"; + field public static final String AUDIO_MPEG = "audio/mpeg"; + field public static final String AUDIO_MPEGH_MHA1 = "audio/mha1"; + field public static final String AUDIO_MPEGH_MHM1 = "audio/mhm1"; + field public static final String AUDIO_MPEG_L1 = "audio/mpeg-L1"; + field public static final String AUDIO_MPEG_L2 = "audio/mpeg-L2"; + field public static final String AUDIO_MSGSM = "audio/gsm"; + field public static final String AUDIO_OGG = "audio/ogg"; + field public static final String AUDIO_OPUS = "audio/opus"; + field public static final String AUDIO_RAW = "audio/raw"; + field public static final String AUDIO_TRUEHD = "audio/true-hd"; + field public static final String AUDIO_VORBIS = "audio/vorbis"; + field public static final String AUDIO_WAV = "audio/wav"; + field public static final String AUDIO_WEBM = "audio/webm"; + field public static final String IMAGE_JPEG = "image/jpeg"; + field public static final String TEXT_SSA = "text/x-ssa"; + field public static final String TEXT_VTT = "text/vtt"; + field public static final String VIDEO_AV1 = "video/av01"; + field public static final String VIDEO_AVI = "video/x-msvideo"; + field public static final String VIDEO_DIVX = "video/divx"; + field public static final String VIDEO_DOLBY_VISION = "video/dolby-vision"; + field public static final String VIDEO_H263 = "video/3gpp"; + field public static final String VIDEO_H264 = "video/avc"; + field public static final String VIDEO_H265 = "video/hevc"; + field public static final String VIDEO_MJPEG = "video/mjpeg"; + field public static final String VIDEO_MP2T = "video/mp2t"; + field public static final String VIDEO_MP4 = "video/mp4"; + field public static final String VIDEO_MP42 = "video/mp42"; + field public static final String VIDEO_MP43 = "video/mp43"; + field public static final String VIDEO_MP4V = "video/mp4v-es"; + field public static final String VIDEO_MPEG = "video/mpeg"; + field public static final String VIDEO_MPEG2 = "video/mpeg2"; + field public static final String VIDEO_OGG = "video/ogg"; + field public static final String VIDEO_PS = "video/mp2p"; + field public static final String VIDEO_VC1 = "video/wvc1"; + field public static final String VIDEO_WEBM = "video/webm"; + } + + public final class PercentageRating extends androidx.media3.common.Rating { + ctor public PercentageRating(); + ctor public PercentageRating(@FloatRange(from=0, to=100) float); + method public float getPercent(); + method public boolean isRated(); + } + + public class PlaybackException extends java.lang.Exception { + method @CallSuper public boolean errorInfoEquals(@Nullable androidx.media3.common.PlaybackException); + method public static String getErrorCodeName(@androidx.media3.common.PlaybackException.ErrorCode int); + method public final String getErrorCodeName(); + field public static final int CUSTOM_ERROR_CODE_BASE = 1000000; // 0xf4240 + field public static final int ERROR_CODE_AUDIO_TRACK_INIT_FAILED = 5001; // 0x1389 + field public static final int ERROR_CODE_AUDIO_TRACK_WRITE_FAILED = 5002; // 0x138a + field public static final int ERROR_CODE_BEHIND_LIVE_WINDOW = 1002; // 0x3ea + field public static final int ERROR_CODE_DECODER_INIT_FAILED = 4001; // 0xfa1 + field public static final int ERROR_CODE_DECODER_QUERY_FAILED = 4002; // 0xfa2 + field public static final int ERROR_CODE_DECODING_FAILED = 4003; // 0xfa3 + field public static final int ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES = 4004; // 0xfa4 + field public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 4005; // 0xfa5 + field public static final int ERROR_CODE_DRM_CONTENT_ERROR = 6003; // 0x1773 + field public static final int ERROR_CODE_DRM_DEVICE_REVOKED = 6007; // 0x1777 + field public static final int ERROR_CODE_DRM_DISALLOWED_OPERATION = 6005; // 0x1775 + field public static final int ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED = 6004; // 0x1774 + field public static final int ERROR_CODE_DRM_LICENSE_EXPIRED = 6008; // 0x1778 + field public static final int ERROR_CODE_DRM_PROVISIONING_FAILED = 6002; // 0x1772 + field public static final int ERROR_CODE_DRM_SCHEME_UNSUPPORTED = 6001; // 0x1771 + field public static final int ERROR_CODE_DRM_SYSTEM_ERROR = 6006; // 0x1776 + field public static final int ERROR_CODE_DRM_UNSPECIFIED = 6000; // 0x1770 + field public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 1004; // 0x3ec + field public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 2004; // 0x7d4 + field public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 2007; // 0x7d7 + field public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 2005; // 0x7d5 + field public static final int ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE = 2003; // 0x7d3 + field public static final int ERROR_CODE_IO_NETWORK_CONNECTION_FAILED = 2001; // 0x7d1 + field public static final int ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT = 2002; // 0x7d2 + field public static final int ERROR_CODE_IO_NO_PERMISSION = 2006; // 0x7d6 + field public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 2008; // 0x7d8 + field public static final int ERROR_CODE_IO_UNSPECIFIED = 2000; // 0x7d0 + field public static final int ERROR_CODE_PARSING_CONTAINER_MALFORMED = 3001; // 0xbb9 + field public static final int ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED = 3003; // 0xbbb + field public static final int ERROR_CODE_PARSING_MANIFEST_MALFORMED = 3002; // 0xbba + field public static final int ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED = 3004; // 0xbbc + field public static final int ERROR_CODE_REMOTE_ERROR = 1001; // 0x3e9 + field public static final int ERROR_CODE_TIMEOUT = 1003; // 0x3eb + field public static final int ERROR_CODE_UNSPECIFIED = 1000; // 0x3e8 + field @androidx.media3.common.PlaybackException.ErrorCode public final int errorCode; + field public final long timestampMs; + } + + @IntDef(open=true, value={androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_REMOTE_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW, androidx.media3.common.PlaybackException.ERROR_CODE_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK, androidx.media3.common.PlaybackException.ERROR_CODE_IO_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, androidx.media3.common.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NO_PERMISSION, androidx.media3.common.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SCHEME_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_EXPIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface PlaybackException.ErrorCode { + } + + public final class PlaybackParameters { + ctor public PlaybackParameters(float); + ctor public PlaybackParameters(@FloatRange(from=0, fromInclusive=false) float, @FloatRange(from=0, fromInclusive=false) float); + method @CheckResult public androidx.media3.common.PlaybackParameters withSpeed(@FloatRange(from=0, fromInclusive=false) float); + field public static final androidx.media3.common.PlaybackParameters DEFAULT; + field public final float pitch; + field public final float speed; + } + + public interface Player { + method public void addListener(androidx.media3.common.Player.Listener); + method public void addMediaItem(androidx.media3.common.MediaItem); + method public void addMediaItem(int, androidx.media3.common.MediaItem); + method public void addMediaItems(java.util.List); + method public void addMediaItems(int, java.util.List); + method public boolean canAdvertiseSession(); + method public void clearMediaItems(); + method public void clearVideoSurface(); + method public void clearVideoSurface(@Nullable android.view.Surface); + method public void clearVideoSurfaceHolder(@Nullable android.view.SurfaceHolder); + method public void clearVideoSurfaceView(@Nullable android.view.SurfaceView); + method public void clearVideoTextureView(@Nullable android.view.TextureView); + method public void decreaseDeviceVolume(); + method public android.os.Looper getApplicationLooper(); + method public androidx.media3.common.AudioAttributes getAudioAttributes(); + method public androidx.media3.common.Player.Commands getAvailableCommands(); + method @IntRange(from=0, to=100) public int getBufferedPercentage(); + method public long getBufferedPosition(); + method public long getContentBufferedPosition(); + method public long getContentDuration(); + method public long getContentPosition(); + method public int getCurrentAdGroupIndex(); + method public int getCurrentAdIndexInAdGroup(); + method public androidx.media3.common.text.CueGroup getCurrentCues(); + method public long getCurrentLiveOffset(); + method @Nullable public androidx.media3.common.MediaItem getCurrentMediaItem(); + method public int getCurrentMediaItemIndex(); + method public int getCurrentPeriodIndex(); + method public long getCurrentPosition(); + method public androidx.media3.common.Timeline getCurrentTimeline(); + method public androidx.media3.common.Tracks getCurrentTracks(); + method public androidx.media3.common.DeviceInfo getDeviceInfo(); + method @IntRange(from=0) public int getDeviceVolume(); + method public long getDuration(); + method public long getMaxSeekToPreviousPosition(); + method public androidx.media3.common.MediaItem getMediaItemAt(int); + method public int getMediaItemCount(); + method public androidx.media3.common.MediaMetadata getMediaMetadata(); + method public int getNextMediaItemIndex(); + method public boolean getPlayWhenReady(); + method public androidx.media3.common.PlaybackParameters getPlaybackParameters(); + method @androidx.media3.common.Player.State public int getPlaybackState(); + method @androidx.media3.common.Player.PlaybackSuppressionReason public int getPlaybackSuppressionReason(); + method @Nullable public androidx.media3.common.PlaybackException getPlayerError(); + method public androidx.media3.common.MediaMetadata getPlaylistMetadata(); + method public int getPreviousMediaItemIndex(); + method @androidx.media3.common.Player.RepeatMode public int getRepeatMode(); + method public long getSeekBackIncrement(); + method public long getSeekForwardIncrement(); + method public boolean getShuffleModeEnabled(); + method public long getTotalBufferedDuration(); + method public androidx.media3.common.TrackSelectionParameters getTrackSelectionParameters(); + method public androidx.media3.common.VideoSize getVideoSize(); + method @FloatRange(from=0, to=1.0) public float getVolume(); + method public boolean hasNextMediaItem(); + method public boolean hasPreviousMediaItem(); + method public void increaseDeviceVolume(); + method public boolean isCommandAvailable(@androidx.media3.common.Player.Command int); + method public boolean isCurrentMediaItemDynamic(); + method public boolean isCurrentMediaItemLive(); + method public boolean isCurrentMediaItemSeekable(); + method public boolean isDeviceMuted(); + method public boolean isLoading(); + method public boolean isPlaying(); + method public boolean isPlayingAd(); + method public void moveMediaItem(int, int); + method public void moveMediaItems(int, int, int); + method public void pause(); + method public void play(); + method public void prepare(); + method public void release(); + method public void removeListener(androidx.media3.common.Player.Listener); + method public void removeMediaItem(int); + method public void removeMediaItems(int, int); + method public void seekBack(); + method public void seekForward(); + method public void seekTo(long); + method public void seekTo(int, long); + method public void seekToDefaultPosition(); + method public void seekToDefaultPosition(int); + method public void seekToNext(); + method public void seekToNextMediaItem(); + method public void seekToPrevious(); + method public void seekToPreviousMediaItem(); + method public void setDeviceMuted(boolean); + method public void setDeviceVolume(@IntRange(from=0) int); + method public void setMediaItem(androidx.media3.common.MediaItem); + method public void setMediaItem(androidx.media3.common.MediaItem, long); + method public void setMediaItem(androidx.media3.common.MediaItem, boolean); + method public void setMediaItems(java.util.List); + method public void setMediaItems(java.util.List, boolean); + method public void setMediaItems(java.util.List, int, long); + method public void setPlayWhenReady(boolean); + method public void setPlaybackParameters(androidx.media3.common.PlaybackParameters); + method public void setPlaybackSpeed(@FloatRange(from=0, fromInclusive=false) float); + method public void setPlaylistMetadata(androidx.media3.common.MediaMetadata); + method public void setRepeatMode(@androidx.media3.common.Player.RepeatMode int); + method public void setShuffleModeEnabled(boolean); + method public void setTrackSelectionParameters(androidx.media3.common.TrackSelectionParameters); + method public void setVideoSurface(@Nullable android.view.Surface); + method public void setVideoSurfaceHolder(@Nullable android.view.SurfaceHolder); + method public void setVideoSurfaceView(@Nullable android.view.SurfaceView); + method public void setVideoTextureView(@Nullable android.view.TextureView); + method public void setVolume(@FloatRange(from=0, to=1.0) float); + method public void stop(); + field public static final int COMMAND_ADJUST_DEVICE_VOLUME = 26; // 0x1a + field public static final int COMMAND_CHANGE_MEDIA_ITEMS = 20; // 0x14 + field public static final int COMMAND_GET_AUDIO_ATTRIBUTES = 21; // 0x15 + field public static final int COMMAND_GET_CURRENT_MEDIA_ITEM = 16; // 0x10 + field public static final int COMMAND_GET_DEVICE_VOLUME = 23; // 0x17 + field public static final int COMMAND_GET_MEDIA_ITEMS_METADATA = 18; // 0x12 + field public static final int COMMAND_GET_TEXT = 28; // 0x1c + field public static final int COMMAND_GET_TIMELINE = 17; // 0x11 + field public static final int COMMAND_GET_TRACKS = 30; // 0x1e + field public static final int COMMAND_GET_VOLUME = 22; // 0x16 + field public static final int COMMAND_INVALID = -1; // 0xffffffff + field public static final int COMMAND_PLAY_PAUSE = 1; // 0x1 + field public static final int COMMAND_PREPARE = 2; // 0x2 + field public static final int COMMAND_SEEK_BACK = 11; // 0xb + field public static final int COMMAND_SEEK_FORWARD = 12; // 0xc + field public static final int COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM = 5; // 0x5 + field public static final int COMMAND_SEEK_TO_DEFAULT_POSITION = 4; // 0x4 + field public static final int COMMAND_SEEK_TO_MEDIA_ITEM = 10; // 0xa + field public static final int COMMAND_SEEK_TO_NEXT = 9; // 0x9 + field public static final int COMMAND_SEEK_TO_NEXT_MEDIA_ITEM = 8; // 0x8 + field public static final int COMMAND_SEEK_TO_PREVIOUS = 7; // 0x7 + field public static final int COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM = 6; // 0x6 + field public static final int COMMAND_SET_DEVICE_VOLUME = 25; // 0x19 + field public static final int COMMAND_SET_MEDIA_ITEM = 31; // 0x1f + field public static final int COMMAND_SET_MEDIA_ITEMS_METADATA = 19; // 0x13 + field public static final int COMMAND_SET_REPEAT_MODE = 15; // 0xf + field public static final int COMMAND_SET_SHUFFLE_MODE = 14; // 0xe + field public static final int COMMAND_SET_SPEED_AND_PITCH = 13; // 0xd + field public static final int COMMAND_SET_TRACK_SELECTION_PARAMETERS = 29; // 0x1d + field public static final int COMMAND_SET_VIDEO_SURFACE = 27; // 0x1b + field public static final int COMMAND_SET_VOLUME = 24; // 0x18 + field public static final int COMMAND_STOP = 3; // 0x3 + field public static final int DISCONTINUITY_REASON_AUTO_TRANSITION = 0; // 0x0 + field public static final int DISCONTINUITY_REASON_INTERNAL = 5; // 0x5 + field public static final int DISCONTINUITY_REASON_REMOVE = 4; // 0x4 + field public static final int DISCONTINUITY_REASON_SEEK = 1; // 0x1 + field public static final int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; // 0x2 + field public static final int DISCONTINUITY_REASON_SKIP = 3; // 0x3 + field public static final int EVENT_AUDIO_ATTRIBUTES_CHANGED = 20; // 0x14 + field public static final int EVENT_AUDIO_SESSION_ID = 21; // 0x15 + field public static final int EVENT_AVAILABLE_COMMANDS_CHANGED = 13; // 0xd + field public static final int EVENT_CUES = 27; // 0x1b + field public static final int EVENT_DEVICE_INFO_CHANGED = 29; // 0x1d + field public static final int EVENT_DEVICE_VOLUME_CHANGED = 30; // 0x1e + field public static final int EVENT_IS_LOADING_CHANGED = 3; // 0x3 + field public static final int EVENT_IS_PLAYING_CHANGED = 7; // 0x7 + field public static final int EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED = 18; // 0x12 + field public static final int EVENT_MEDIA_ITEM_TRANSITION = 1; // 0x1 + field public static final int EVENT_MEDIA_METADATA_CHANGED = 14; // 0xe + field public static final int EVENT_METADATA = 28; // 0x1c + field public static final int EVENT_PLAYBACK_PARAMETERS_CHANGED = 12; // 0xc + field public static final int EVENT_PLAYBACK_STATE_CHANGED = 4; // 0x4 + field public static final int EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED = 6; // 0x6 + field public static final int EVENT_PLAYER_ERROR = 10; // 0xa + field public static final int EVENT_PLAYLIST_METADATA_CHANGED = 15; // 0xf + field public static final int EVENT_PLAY_WHEN_READY_CHANGED = 5; // 0x5 + field public static final int EVENT_POSITION_DISCONTINUITY = 11; // 0xb + field public static final int EVENT_RENDERED_FIRST_FRAME = 26; // 0x1a + field public static final int EVENT_REPEAT_MODE_CHANGED = 8; // 0x8 + field public static final int EVENT_SEEK_BACK_INCREMENT_CHANGED = 16; // 0x10 + field public static final int EVENT_SEEK_FORWARD_INCREMENT_CHANGED = 17; // 0x11 + field public static final int EVENT_SHUFFLE_MODE_ENABLED_CHANGED = 9; // 0x9 + field public static final int EVENT_SKIP_SILENCE_ENABLED_CHANGED = 23; // 0x17 + field public static final int EVENT_SURFACE_SIZE_CHANGED = 24; // 0x18 + field public static final int EVENT_TIMELINE_CHANGED = 0; // 0x0 + field public static final int EVENT_TRACKS_CHANGED = 2; // 0x2 + field public static final int EVENT_TRACK_SELECTION_PARAMETERS_CHANGED = 19; // 0x13 + field public static final int EVENT_VIDEO_SIZE_CHANGED = 25; // 0x19 + field public static final int EVENT_VOLUME_CHANGED = 22; // 0x16 + field public static final int MEDIA_ITEM_TRANSITION_REASON_AUTO = 1; // 0x1 + field public static final int MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED = 3; // 0x3 + field public static final int MEDIA_ITEM_TRANSITION_REASON_REPEAT = 0; // 0x0 + field public static final int MEDIA_ITEM_TRANSITION_REASON_SEEK = 2; // 0x2 + field public static final int PLAYBACK_SUPPRESSION_REASON_NONE = 0; // 0x0 + field public static final int PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS = 1; // 0x1 + field public static final int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY = 3; // 0x3 + field public static final int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS = 2; // 0x2 + field public static final int PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM = 5; // 0x5 + field public static final int PLAY_WHEN_READY_CHANGE_REASON_REMOTE = 4; // 0x4 + field public static final int PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST = 1; // 0x1 + field public static final int REPEAT_MODE_ALL = 2; // 0x2 + field public static final int REPEAT_MODE_OFF = 0; // 0x0 + field public static final int REPEAT_MODE_ONE = 1; // 0x1 + field public static final int STATE_BUFFERING = 2; // 0x2 + field public static final int STATE_ENDED = 4; // 0x4 + field public static final int STATE_IDLE = 1; // 0x1 + field public static final int STATE_READY = 3; // 0x3 + field public static final int TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED = 0; // 0x0 + field public static final int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1; // 0x1 + } + + @IntDef({androidx.media3.common.Player.COMMAND_INVALID, androidx.media3.common.Player.COMMAND_PLAY_PAUSE, androidx.media3.common.Player.COMMAND_PREPARE, androidx.media3.common.Player.COMMAND_STOP, androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION, androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT, androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_BACK, androidx.media3.common.Player.COMMAND_SEEK_FORWARD, androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH, androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE, androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE, androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_GET_TIMELINE, androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS, androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES, androidx.media3.common.Player.COMMAND_GET_VOLUME, androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VOLUME, androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE, androidx.media3.common.Player.COMMAND_GET_TEXT, androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS, androidx.media3.common.Player.COMMAND_GET_TRACKS, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.Command { + } + + public static final class Player.Commands { + method public boolean contains(@androidx.media3.common.Player.Command int); + method public boolean containsAny(@androidx.media3.common.Player.Command int...); + method @androidx.media3.common.Player.Command public int get(int); + method public int size(); + field public static final androidx.media3.common.Player.Commands EMPTY; + } + + @IntDef({androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK, androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT, androidx.media3.common.Player.DISCONTINUITY_REASON_SKIP, androidx.media3.common.Player.DISCONTINUITY_REASON_REMOVE, androidx.media3.common.Player.DISCONTINUITY_REASON_INTERNAL}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.DiscontinuityReason { + } + + @IntDef({androidx.media3.common.Player.EVENT_TIMELINE_CHANGED, androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION, androidx.media3.common.Player.EVENT_TRACKS_CHANGED, androidx.media3.common.Player.EVENT_IS_LOADING_CHANGED, androidx.media3.common.Player.EVENT_PLAYBACK_STATE_CHANGED, androidx.media3.common.Player.EVENT_PLAY_WHEN_READY_CHANGED, androidx.media3.common.Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED, androidx.media3.common.Player.EVENT_REPEAT_MODE_CHANGED, androidx.media3.common.Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, androidx.media3.common.Player.EVENT_PLAYER_ERROR, androidx.media3.common.Player.EVENT_POSITION_DISCONTINUITY, androidx.media3.common.Player.EVENT_PLAYBACK_PARAMETERS_CHANGED, androidx.media3.common.Player.EVENT_AVAILABLE_COMMANDS_CHANGED, androidx.media3.common.Player.EVENT_MEDIA_METADATA_CHANGED, androidx.media3.common.Player.EVENT_PLAYLIST_METADATA_CHANGED, androidx.media3.common.Player.EVENT_SEEK_BACK_INCREMENT_CHANGED, androidx.media3.common.Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED, androidx.media3.common.Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED, androidx.media3.common.Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED, androidx.media3.common.Player.EVENT_AUDIO_ATTRIBUTES_CHANGED, androidx.media3.common.Player.EVENT_AUDIO_SESSION_ID, androidx.media3.common.Player.EVENT_VOLUME_CHANGED, androidx.media3.common.Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED, androidx.media3.common.Player.EVENT_SURFACE_SIZE_CHANGED, androidx.media3.common.Player.EVENT_VIDEO_SIZE_CHANGED, androidx.media3.common.Player.EVENT_RENDERED_FIRST_FRAME, androidx.media3.common.Player.EVENT_CUES, androidx.media3.common.Player.EVENT_METADATA, androidx.media3.common.Player.EVENT_DEVICE_INFO_CHANGED, androidx.media3.common.Player.EVENT_DEVICE_VOLUME_CHANGED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.Event { + } + + public static final class Player.Events { + method public boolean contains(@androidx.media3.common.Player.Event int); + method public boolean containsAny(@androidx.media3.common.Player.Event int...); + method @androidx.media3.common.Player.Event public int get(int); + method public int size(); + } + + public static interface Player.Listener { + method public default void onAudioAttributesChanged(androidx.media3.common.AudioAttributes); + method public default void onAvailableCommandsChanged(androidx.media3.common.Player.Commands); + method public default void onCues(androidx.media3.common.text.CueGroup); + method public default void onDeviceInfoChanged(androidx.media3.common.DeviceInfo); + method public default void onDeviceVolumeChanged(int, boolean); + method public default void onEvents(androidx.media3.common.Player, androidx.media3.common.Player.Events); + method public default void onIsLoadingChanged(boolean); + method public default void onIsPlayingChanged(boolean); + method public default void onMaxSeekToPreviousPositionChanged(long); + method public default void onMediaItemTransition(@Nullable androidx.media3.common.MediaItem, @androidx.media3.common.Player.MediaItemTransitionReason int); + method public default void onMediaMetadataChanged(androidx.media3.common.MediaMetadata); + method public default void onPlayWhenReadyChanged(boolean, @androidx.media3.common.Player.PlayWhenReadyChangeReason int); + method public default void onPlaybackParametersChanged(androidx.media3.common.PlaybackParameters); + method public default void onPlaybackStateChanged(@androidx.media3.common.Player.State int); + method public default void onPlaybackSuppressionReasonChanged(@androidx.media3.common.Player.PlaybackSuppressionReason int); + method public default void onPlayerError(androidx.media3.common.PlaybackException); + method public default void onPlayerErrorChanged(@Nullable androidx.media3.common.PlaybackException); + method public default void onPlaylistMetadataChanged(androidx.media3.common.MediaMetadata); + method public default void onPositionDiscontinuity(androidx.media3.common.Player.PositionInfo, androidx.media3.common.Player.PositionInfo, @androidx.media3.common.Player.DiscontinuityReason int); + method public default void onRenderedFirstFrame(); + method public default void onRepeatModeChanged(@androidx.media3.common.Player.RepeatMode int); + method public default void onSeekBackIncrementChanged(long); + method public default void onSeekForwardIncrementChanged(long); + method public default void onShuffleModeEnabledChanged(boolean); + method public default void onSkipSilenceEnabledChanged(boolean); + method public default void onSurfaceSizeChanged(int, int); + method public default void onTimelineChanged(androidx.media3.common.Timeline, @androidx.media3.common.Player.TimelineChangeReason int); + method public default void onTrackSelectionParametersChanged(androidx.media3.common.TrackSelectionParameters); + method public default void onTracksChanged(androidx.media3.common.Tracks); + method public default void onVideoSizeChanged(androidx.media3.common.VideoSize); + method public default void onVolumeChanged(float); + } + + @IntDef({androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT, androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO, androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_SEEK, androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.MediaItemTransitionReason { + } + + @IntDef({androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE, androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.PlayWhenReadyChangeReason { + } + + @IntDef({androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_NONE, androidx.media3.common.Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.PlaybackSuppressionReason { + } + + public static final class Player.PositionInfo { + field public final int adGroupIndex; + field public final int adIndexInAdGroup; + field public final long contentPositionMs; + field public final int mediaItemIndex; + field public final int periodIndex; + field @Nullable public final Object periodUid; + field public final long positionMs; + field @Nullable public final Object windowUid; + } + + @IntDef({androidx.media3.common.Player.REPEAT_MODE_OFF, androidx.media3.common.Player.REPEAT_MODE_ONE, androidx.media3.common.Player.REPEAT_MODE_ALL}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.RepeatMode { + } + + @IntDef({androidx.media3.common.Player.STATE_IDLE, androidx.media3.common.Player.STATE_BUFFERING, androidx.media3.common.Player.STATE_READY, androidx.media3.common.Player.STATE_ENDED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.State { + } + + @IntDef({androidx.media3.common.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, androidx.media3.common.Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.TimelineChangeReason { + } + + public abstract class Rating { + method public abstract boolean isRated(); + } + + public final class StarRating extends androidx.media3.common.Rating { + ctor public StarRating(@IntRange(from=1) int); + ctor public StarRating(@IntRange(from=1) int, @FloatRange(from=0.0) float); + method @IntRange(from=1) public int getMaxStars(); + method public float getStarRating(); + method public boolean isRated(); + } + + public final class ThumbRating extends androidx.media3.common.Rating { + ctor public ThumbRating(); + ctor public ThumbRating(boolean); + method public boolean isRated(); + method public boolean isThumbsUp(); + } + + public abstract class Timeline { + method public int getFirstWindowIndex(boolean); + method public abstract int getIndexOfPeriod(Object); + method public int getLastWindowIndex(boolean); + method public final int getNextPeriodIndex(int, androidx.media3.common.Timeline.Period, androidx.media3.common.Timeline.Window, @androidx.media3.common.Player.RepeatMode int, boolean); + method public int getNextWindowIndex(int, @androidx.media3.common.Player.RepeatMode int, boolean); + method public final androidx.media3.common.Timeline.Period getPeriod(int, androidx.media3.common.Timeline.Period); + method public abstract androidx.media3.common.Timeline.Period getPeriod(int, androidx.media3.common.Timeline.Period, boolean); + method public androidx.media3.common.Timeline.Period getPeriodByUid(Object, androidx.media3.common.Timeline.Period); + method public abstract int getPeriodCount(); + method public final android.util.Pair getPeriodPositionUs(androidx.media3.common.Timeline.Window, androidx.media3.common.Timeline.Period, int, long); + method @Nullable public final android.util.Pair getPeriodPositionUs(androidx.media3.common.Timeline.Window, androidx.media3.common.Timeline.Period, int, long, long); + method public int getPreviousWindowIndex(int, @androidx.media3.common.Player.RepeatMode int, boolean); + method public abstract Object getUidOfPeriod(int); + method public final androidx.media3.common.Timeline.Window getWindow(int, androidx.media3.common.Timeline.Window); + method public abstract androidx.media3.common.Timeline.Window getWindow(int, androidx.media3.common.Timeline.Window, long); + method public abstract int getWindowCount(); + method public final boolean isEmpty(); + method public final boolean isLastPeriod(int, androidx.media3.common.Timeline.Period, androidx.media3.common.Timeline.Window, @androidx.media3.common.Player.RepeatMode int, boolean); + field public static final androidx.media3.common.Timeline EMPTY; + } + + public static final class Timeline.Period { + ctor public Timeline.Period(); + method public int getAdCountInAdGroup(int); + method public long getAdDurationUs(int, int); + method public int getAdGroupCount(); + method public int getAdGroupIndexAfterPositionUs(long); + method public int getAdGroupIndexForPositionUs(long); + method public long getAdGroupTimeUs(int); + method public long getAdResumePositionUs(); + method @Nullable public Object getAdsId(); + method public long getDurationMs(); + method public long getDurationUs(); + method public int getFirstAdIndexToPlay(int); + method public int getNextAdIndexToPlay(int, int); + method public long getPositionInWindowMs(); + method public long getPositionInWindowUs(); + method public int getRemovedAdGroupCount(); + method public boolean hasPlayedAdGroup(int); + field @Nullable public Object id; + field public boolean isPlaceholder; + field @Nullable public Object uid; + field public int windowIndex; + } + + public static final class Timeline.Window { + ctor public Timeline.Window(); + method public long getCurrentUnixTimeMs(); + method public long getDefaultPositionMs(); + method public long getDefaultPositionUs(); + method public long getDurationMs(); + method public long getDurationUs(); + method public long getPositionInFirstPeriodMs(); + method public long getPositionInFirstPeriodUs(); + method public boolean isLive(); + field public static final Object SINGLE_WINDOW_UID; + field public long elapsedRealtimeEpochOffsetMs; + field public int firstPeriodIndex; + field public boolean isDynamic; + field public boolean isPlaceholder; + field public boolean isSeekable; + field public int lastPeriodIndex; + field @Nullable public androidx.media3.common.MediaItem.LiveConfiguration liveConfiguration; + field @Nullable public Object manifest; + field public androidx.media3.common.MediaItem mediaItem; + field public long presentationStartTimeMs; + field public Object uid; + field public long windowStartTimeMs; + } + + public final class TrackGroup { + } + + public final class TrackSelectionOverride { + ctor public TrackSelectionOverride(androidx.media3.common.TrackGroup, int); + ctor public TrackSelectionOverride(androidx.media3.common.TrackGroup, java.util.List); + method @androidx.media3.common.C.TrackType public int getType(); + field public final androidx.media3.common.TrackGroup mediaTrackGroup; + field public final com.google.common.collect.ImmutableList trackIndices; + } + + public class TrackSelectionParameters { + method public androidx.media3.common.TrackSelectionParameters.Builder buildUpon(); + method public static androidx.media3.common.TrackSelectionParameters fromBundle(android.os.Bundle); + method public static androidx.media3.common.TrackSelectionParameters getDefaults(android.content.Context); + method public android.os.Bundle toBundle(); + field public final com.google.common.collect.ImmutableSet disabledTrackTypes; + field public final boolean forceHighestSupportedBitrate; + field public final boolean forceLowestBitrate; + field @androidx.media3.common.C.SelectionFlags public final int ignoredTextSelectionFlags; + field public final int maxAudioBitrate; + field public final int maxAudioChannelCount; + field public final int maxVideoBitrate; + field public final int maxVideoFrameRate; + field public final int maxVideoHeight; + field public final int maxVideoWidth; + field public final int minVideoBitrate; + field public final int minVideoFrameRate; + field public final int minVideoHeight; + field public final int minVideoWidth; + field public final com.google.common.collect.ImmutableMap overrides; + field public final com.google.common.collect.ImmutableList preferredAudioLanguages; + field public final com.google.common.collect.ImmutableList preferredAudioMimeTypes; + field @androidx.media3.common.C.RoleFlags public final int preferredAudioRoleFlags; + field public final com.google.common.collect.ImmutableList preferredTextLanguages; + field @androidx.media3.common.C.RoleFlags public final int preferredTextRoleFlags; + field public final com.google.common.collect.ImmutableList preferredVideoMimeTypes; + field @androidx.media3.common.C.RoleFlags public final int preferredVideoRoleFlags; + field public final boolean selectUndeterminedTextLanguage; + field public final int viewportHeight; + field public final boolean viewportOrientationMayChange; + field public final int viewportWidth; + } + + public static class TrackSelectionParameters.Builder { + ctor public TrackSelectionParameters.Builder(android.content.Context); + method public androidx.media3.common.TrackSelectionParameters.Builder addOverride(androidx.media3.common.TrackSelectionOverride); + method public androidx.media3.common.TrackSelectionParameters build(); + method public androidx.media3.common.TrackSelectionParameters.Builder clearOverride(androidx.media3.common.TrackGroup); + method public androidx.media3.common.TrackSelectionParameters.Builder clearOverrides(); + method public androidx.media3.common.TrackSelectionParameters.Builder clearOverridesOfType(@androidx.media3.common.C.TrackType int); + method public androidx.media3.common.TrackSelectionParameters.Builder clearVideoSizeConstraints(); + method public androidx.media3.common.TrackSelectionParameters.Builder clearViewportSizeConstraints(); + method public androidx.media3.common.TrackSelectionParameters.Builder setForceHighestSupportedBitrate(boolean); + method public androidx.media3.common.TrackSelectionParameters.Builder setForceLowestBitrate(boolean); + method public androidx.media3.common.TrackSelectionParameters.Builder setIgnoredTextSelectionFlags(@androidx.media3.common.C.SelectionFlags int); + method public androidx.media3.common.TrackSelectionParameters.Builder setMaxAudioBitrate(int); + method public androidx.media3.common.TrackSelectionParameters.Builder setMaxAudioChannelCount(int); + method public androidx.media3.common.TrackSelectionParameters.Builder setMaxVideoBitrate(int); + method public androidx.media3.common.TrackSelectionParameters.Builder setMaxVideoFrameRate(int); + method public androidx.media3.common.TrackSelectionParameters.Builder setMaxVideoSize(int, int); + method public androidx.media3.common.TrackSelectionParameters.Builder setMaxVideoSizeSd(); + method public androidx.media3.common.TrackSelectionParameters.Builder setMinVideoBitrate(int); + method public androidx.media3.common.TrackSelectionParameters.Builder setMinVideoFrameRate(int); + method public androidx.media3.common.TrackSelectionParameters.Builder setMinVideoSize(int, int); + method public androidx.media3.common.TrackSelectionParameters.Builder setOverrideForType(androidx.media3.common.TrackSelectionOverride); + method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredAudioLanguage(@Nullable String); + method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredAudioLanguages(java.lang.String...); + method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredAudioMimeType(@Nullable String); + method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredAudioMimeTypes(java.lang.String...); + method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredAudioRoleFlags(@androidx.media3.common.C.RoleFlags int); + method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguage(@Nullable String); + method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(android.content.Context); + method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextLanguages(java.lang.String...); + method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredTextRoleFlags(@androidx.media3.common.C.RoleFlags int); + method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredVideoMimeType(@Nullable String); + method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredVideoMimeTypes(java.lang.String...); + method public androidx.media3.common.TrackSelectionParameters.Builder setPreferredVideoRoleFlags(@androidx.media3.common.C.RoleFlags int); + method public androidx.media3.common.TrackSelectionParameters.Builder setSelectUndeterminedTextLanguage(boolean); + method public androidx.media3.common.TrackSelectionParameters.Builder setTrackTypeDisabled(@androidx.media3.common.C.TrackType int, boolean); + method public androidx.media3.common.TrackSelectionParameters.Builder setViewportSize(int, int, boolean); + method public androidx.media3.common.TrackSelectionParameters.Builder setViewportSizeToPhysicalDisplaySize(android.content.Context, boolean); + } + + public final class Tracks { + method public boolean containsType(@androidx.media3.common.C.TrackType int); + method public com.google.common.collect.ImmutableList getGroups(); + method public boolean isEmpty(); + method public boolean isTypeSelected(@androidx.media3.common.C.TrackType int); + method public boolean isTypeSupported(@androidx.media3.common.C.TrackType int); + method public boolean isTypeSupported(@androidx.media3.common.C.TrackType int, boolean); + field public static final androidx.media3.common.Tracks EMPTY; + } + + public static final class Tracks.Group { + method public androidx.media3.common.TrackGroup getMediaTrackGroup(); + method public androidx.media3.common.Format getTrackFormat(int); + method @androidx.media3.common.C.TrackType public int getType(); + method public boolean isAdaptiveSupported(); + method public boolean isSelected(); + method public boolean isSupported(); + method public boolean isSupported(boolean); + method public boolean isTrackSelected(int); + method public boolean isTrackSupported(int); + method public boolean isTrackSupported(int, boolean); + method public android.os.Bundle toBundle(); + field public final int length; + } + + public final class VideoSize { + field public static final androidx.media3.common.VideoSize UNKNOWN; + field @IntRange(from=0) public final int height; + field @FloatRange(from=0, fromInclusive=false) public final float pixelWidthHeightRatio; + field @IntRange(from=0, to=359) public final int unappliedRotationDegrees; + field @IntRange(from=0) public final int width; + } + +} + +package androidx.media3.common.text { + + public final class Cue { + field public static final int ANCHOR_TYPE_END = 2; // 0x2 + field public static final int ANCHOR_TYPE_MIDDLE = 1; // 0x1 + field public static final int ANCHOR_TYPE_START = 0; // 0x0 + field public static final float DIMEN_UNSET = -3.4028235E38f; + field public static final androidx.media3.common.text.Cue EMPTY; + field public static final int LINE_TYPE_FRACTION = 0; // 0x0 + field public static final int LINE_TYPE_NUMBER = 1; // 0x1 + field public static final int TEXT_SIZE_TYPE_ABSOLUTE = 2; // 0x2 + field public static final int TEXT_SIZE_TYPE_FRACTIONAL = 0; // 0x0 + field public static final int TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING = 1; // 0x1 + field public static final int TYPE_UNSET = -2147483648; // 0x80000000 + field public static final int VERTICAL_TYPE_LR = 2; // 0x2 + field public static final int VERTICAL_TYPE_RL = 1; // 0x1 + field @Nullable public final android.graphics.Bitmap bitmap; + field public final float bitmapHeight; + field public final float line; + field @androidx.media3.common.text.Cue.AnchorType public final int lineAnchor; + field @androidx.media3.common.text.Cue.LineType public final int lineType; + field @Nullable public final android.text.Layout.Alignment multiRowAlignment; + field public final float position; + field @androidx.media3.common.text.Cue.AnchorType public final int positionAnchor; + field public final float shearDegrees; + field public final float size; + field @Nullable public final CharSequence text; + field @Nullable public final android.text.Layout.Alignment textAlignment; + field public final float textSize; + field @androidx.media3.common.text.Cue.TextSizeType public final int textSizeType; + field @androidx.media3.common.text.Cue.VerticalType public final int verticalType; + field public final int windowColor; + field public final boolean windowColorSet; + } + + @IntDef({androidx.media3.common.text.Cue.TYPE_UNSET, androidx.media3.common.text.Cue.ANCHOR_TYPE_START, androidx.media3.common.text.Cue.ANCHOR_TYPE_MIDDLE, androidx.media3.common.text.Cue.ANCHOR_TYPE_END}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Cue.AnchorType { + } + + @IntDef({androidx.media3.common.text.Cue.TYPE_UNSET, androidx.media3.common.text.Cue.LINE_TYPE_FRACTION, androidx.media3.common.text.Cue.LINE_TYPE_NUMBER}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Cue.LineType { + } + + @IntDef({androidx.media3.common.text.Cue.TYPE_UNSET, androidx.media3.common.text.Cue.TEXT_SIZE_TYPE_FRACTIONAL, androidx.media3.common.text.Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING, androidx.media3.common.text.Cue.TEXT_SIZE_TYPE_ABSOLUTE}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Cue.TextSizeType { + } + + @IntDef({androidx.media3.common.text.Cue.TYPE_UNSET, androidx.media3.common.text.Cue.VERTICAL_TYPE_RL, androidx.media3.common.text.Cue.VERTICAL_TYPE_LR}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Cue.VerticalType { + } + + public final class CueGroup { + field public final com.google.common.collect.ImmutableList cues; + } + +} + +package androidx.media3.common.util { + + public final class Util { + method public static boolean checkCleartextTrafficPermitted(androidx.media3.common.MediaItem...); + method @Nullable public static String getAdaptiveMimeTypeForContentType(@androidx.media3.common.C.ContentType int); + method @Nullable public static java.util.UUID getDrmUuid(String); + method @androidx.media3.common.C.ContentType public static int inferContentType(android.net.Uri); + method @androidx.media3.common.C.ContentType public static int inferContentTypeForExtension(String); + method @androidx.media3.common.C.ContentType public static int inferContentTypeForUriAndMimeType(android.net.Uri, @Nullable String); + method public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, android.net.Uri...); + method public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, androidx.media3.common.MediaItem...); + } + +} + +package androidx.media3.datasource { + + public interface DataSource { + } + + public static interface DataSource.Factory { + } + + public class DataSourceException extends java.io.IOException { + field @androidx.media3.common.PlaybackException.ErrorCode public final int reason; + } + + public final class DefaultDataSource implements androidx.media3.datasource.DataSource { + } + + public static final class DefaultDataSource.Factory implements androidx.media3.datasource.DataSource.Factory { + ctor public DefaultDataSource.Factory(android.content.Context); + ctor public DefaultDataSource.Factory(android.content.Context, androidx.media3.datasource.DataSource.Factory); + } + + public class DefaultHttpDataSource implements androidx.media3.datasource.DataSource androidx.media3.datasource.HttpDataSource { + } + + public static final class DefaultHttpDataSource.Factory implements androidx.media3.datasource.HttpDataSource.Factory { + ctor public DefaultHttpDataSource.Factory(); + } + + public interface HttpDataSource extends androidx.media3.datasource.DataSource { + } + + public static final class HttpDataSource.CleartextNotPermittedException extends androidx.media3.datasource.HttpDataSource.HttpDataSourceException { + } + + public static interface HttpDataSource.Factory extends androidx.media3.datasource.DataSource.Factory { + } + + public static class HttpDataSource.HttpDataSourceException extends androidx.media3.datasource.DataSourceException { + field public static final int TYPE_CLOSE = 3; // 0x3 + field public static final int TYPE_OPEN = 1; // 0x1 + field public static final int TYPE_READ = 2; // 0x2 + field @androidx.media3.datasource.HttpDataSource.HttpDataSourceException.Type public final int type; + } + + @IntDef({androidx.media3.datasource.HttpDataSource.HttpDataSourceException.TYPE_OPEN, androidx.media3.datasource.HttpDataSource.HttpDataSourceException.TYPE_READ, androidx.media3.datasource.HttpDataSource.HttpDataSourceException.TYPE_CLOSE}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface HttpDataSource.HttpDataSourceException.Type { + } + + public static final class HttpDataSource.InvalidContentTypeException extends androidx.media3.datasource.HttpDataSource.HttpDataSourceException { + field public final String contentType; + } + + public static final class HttpDataSource.InvalidResponseCodeException extends androidx.media3.datasource.HttpDataSource.HttpDataSourceException { + field public final byte[] responseBody; + field public final int responseCode; + field @Nullable public final String responseMessage; + } + +} + +package androidx.media3.datasource.cronet { + + public class CronetDataSource implements androidx.media3.datasource.DataSource androidx.media3.datasource.HttpDataSource { + } + + public static final class CronetDataSource.Factory implements androidx.media3.datasource.HttpDataSource.Factory { + ctor public CronetDataSource.Factory(org.chromium.net.CronetEngine, java.util.concurrent.Executor); + } + + public final class CronetUtil { + method @Nullable public static org.chromium.net.CronetEngine buildCronetEngine(android.content.Context); + } + +} + +package androidx.media3.datasource.okhttp { + + public class OkHttpDataSource implements androidx.media3.datasource.DataSource androidx.media3.datasource.HttpDataSource { + } + + public static final class OkHttpDataSource.Factory implements androidx.media3.datasource.HttpDataSource.Factory { + ctor public OkHttpDataSource.Factory(okhttp3.Call.Factory); + } + +} + +package androidx.media3.exoplayer { + + public final class ExoPlaybackException extends androidx.media3.common.PlaybackException { + } + + public interface ExoPlayer extends androidx.media3.common.Player { + method public void addAnalyticsListener(androidx.media3.exoplayer.analytics.AnalyticsListener); + method @Nullable public androidx.media3.exoplayer.ExoPlaybackException getPlayerError(); + method public void removeAnalyticsListener(androidx.media3.exoplayer.analytics.AnalyticsListener); + method public void setAudioAttributes(androidx.media3.common.AudioAttributes, boolean); + method public void setHandleAudioBecomingNoisy(boolean); + method public void setWakeMode(@androidx.media3.common.C.WakeMode int); + } + + public static final class ExoPlayer.Builder { + ctor public ExoPlayer.Builder(android.content.Context); + method public androidx.media3.exoplayer.ExoPlayer build(); + method public androidx.media3.exoplayer.ExoPlayer.Builder setAudioAttributes(androidx.media3.common.AudioAttributes, boolean); + method public androidx.media3.exoplayer.ExoPlayer.Builder setHandleAudioBecomingNoisy(boolean); + method public androidx.media3.exoplayer.ExoPlayer.Builder setMediaSourceFactory(androidx.media3.exoplayer.source.MediaSource.Factory); + method public androidx.media3.exoplayer.ExoPlayer.Builder setWakeMode(@androidx.media3.common.C.WakeMode int); + } + +} + +package androidx.media3.exoplayer.analytics { + + public interface AnalyticsListener { + } + +} + +package androidx.media3.exoplayer.drm { + + @RequiresApi(18) public final class FrameworkMediaDrm { + method public static boolean isCryptoSchemeSupported(java.util.UUID); + } + +} + +package androidx.media3.exoplayer.ima { + + public final class ImaAdsLoader implements androidx.media3.exoplayer.source.ads.AdsLoader { + method public void release(); + method public void setPlayer(@Nullable androidx.media3.common.Player); + } + + public static final class ImaAdsLoader.Builder { + ctor public ImaAdsLoader.Builder(android.content.Context); + method public androidx.media3.exoplayer.ima.ImaAdsLoader build(); + } + +} + +package androidx.media3.exoplayer.source { + + public final class DefaultMediaSourceFactory implements androidx.media3.exoplayer.source.MediaSource.Factory { + ctor public DefaultMediaSourceFactory(android.content.Context); + method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory clearLocalAdInsertionComponents(); + method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory setDataSourceFactory(androidx.media3.datasource.DataSource.Factory); + method public androidx.media3.exoplayer.source.DefaultMediaSourceFactory setLocalAdInsertionComponents(androidx.media3.exoplayer.source.ads.AdsLoader.Provider, androidx.media3.common.AdViewProvider); + } + + public interface MediaSource { + } + + public static interface MediaSource.Factory { + } + +} + +package androidx.media3.exoplayer.source.ads { + + public interface AdsLoader { + method public void release(); + method public void setPlayer(@Nullable androidx.media3.common.Player); + } + + public static interface AdsLoader.Provider { + method @Nullable public androidx.media3.exoplayer.source.ads.AdsLoader getAdsLoader(androidx.media3.common.MediaItem.AdsConfiguration); + } + +} + +package androidx.media3.exoplayer.util { + + public class DebugTextViewHelper { + ctor public DebugTextViewHelper(androidx.media3.exoplayer.ExoPlayer, android.widget.TextView); + method public final void start(); + method public final void stop(); + } + + public class EventLogger implements androidx.media3.exoplayer.analytics.AnalyticsListener { + ctor public EventLogger(); + ctor public EventLogger(String); + } + +} + +package androidx.media3.session { + + public final class CommandButton { + field public final CharSequence displayName; + field @DrawableRes public final int iconResId; + field public final boolean isEnabled; + field @androidx.media3.common.Player.Command public final int playerCommand; + field @Nullable public final androidx.media3.session.SessionCommand sessionCommand; + } + + public static final class CommandButton.Builder { + ctor public CommandButton.Builder(); + method public androidx.media3.session.CommandButton build(); + method public androidx.media3.session.CommandButton.Builder setDisplayName(CharSequence); + method public androidx.media3.session.CommandButton.Builder setEnabled(boolean); + method public androidx.media3.session.CommandButton.Builder setExtras(android.os.Bundle); + method public androidx.media3.session.CommandButton.Builder setIconResId(@DrawableRes int); + method public androidx.media3.session.CommandButton.Builder setPlayerCommand(@androidx.media3.common.Player.Command int); + method public androidx.media3.session.CommandButton.Builder setSessionCommand(androidx.media3.session.SessionCommand); + } + + public final class LibraryResult { + method public static androidx.media3.session.LibraryResult ofError(@androidx.media3.session.LibraryResult.Code int); + method public static androidx.media3.session.LibraryResult ofError(@androidx.media3.session.LibraryResult.Code int, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public static androidx.media3.session.LibraryResult ofItem(androidx.media3.common.MediaItem, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public static androidx.media3.session.LibraryResult> ofItemList(java.util.List, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public static androidx.media3.session.LibraryResult ofVoid(); + method public static androidx.media3.session.LibraryResult ofVoid(@Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + field public static final int RESULT_ERROR_BAD_VALUE = -3; // 0xfffffffd + field public static final int RESULT_ERROR_INVALID_STATE = -2; // 0xfffffffe + field public static final int RESULT_ERROR_IO = -5; // 0xfffffffb + field public static final int RESULT_ERROR_NOT_SUPPORTED = -6; // 0xfffffffa + field public static final int RESULT_ERROR_PERMISSION_DENIED = -4; // 0xfffffffc + field public static final int RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED = -102; // 0xffffff9a + field public static final int RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT = -104; // 0xffffff98 + field public static final int RESULT_ERROR_SESSION_DISCONNECTED = -100; // 0xffffff9c + field public static final int RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION = -106; // 0xffffff96 + field public static final int RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = -105; // 0xffffff97 + field public static final int RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = -103; // 0xffffff99 + field public static final int RESULT_ERROR_SESSION_SETUP_REQUIRED = -108; // 0xffffff94 + field public static final int RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED = -107; // 0xffffff95 + field public static final int RESULT_ERROR_UNKNOWN = -1; // 0xffffffff + field public static final int RESULT_INFO_SKIPPED = 1; // 0x1 + field public static final int RESULT_SUCCESS = 0; // 0x0 + field public final long completionTimeMs; + field @Nullable public final androidx.media3.session.MediaLibraryService.LibraryParams params; + field @androidx.media3.session.LibraryResult.Code public final int resultCode; + field @Nullable public final V value; + } + + @IntDef({androidx.media3.session.LibraryResult.RESULT_SUCCESS, androidx.media3.session.LibraryResult.RESULT_ERROR_UNKNOWN, androidx.media3.session.LibraryResult.RESULT_ERROR_INVALID_STATE, androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE, androidx.media3.session.LibraryResult.RESULT_ERROR_PERMISSION_DENIED, androidx.media3.session.LibraryResult.RESULT_ERROR_IO, androidx.media3.session.LibraryResult.RESULT_INFO_SKIPPED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_DISCONNECTED, androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface LibraryResult.Code { + } + + public final class MediaBrowser extends androidx.media3.session.MediaController { + method public com.google.common.util.concurrent.ListenableFuture>> getChildren(String, @IntRange(from=0) int, @IntRange(from=1) int, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public com.google.common.util.concurrent.ListenableFuture> getItem(String); + method public com.google.common.util.concurrent.ListenableFuture> getLibraryRoot(@Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public com.google.common.util.concurrent.ListenableFuture>> getSearchResult(String, @IntRange(from=0) int, @IntRange(from=1) int, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public com.google.common.util.concurrent.ListenableFuture> search(String, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public com.google.common.util.concurrent.ListenableFuture> subscribe(String, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public com.google.common.util.concurrent.ListenableFuture> unsubscribe(String); + } + + public static final class MediaBrowser.Builder { + ctor public MediaBrowser.Builder(android.content.Context, androidx.media3.session.SessionToken); + method public com.google.common.util.concurrent.ListenableFuture buildAsync(); + method public androidx.media3.session.MediaBrowser.Builder setApplicationLooper(android.os.Looper); + method public androidx.media3.session.MediaBrowser.Builder setConnectionHints(android.os.Bundle); + method public androidx.media3.session.MediaBrowser.Builder setListener(androidx.media3.session.MediaBrowser.Listener); + } + + public static interface MediaBrowser.Listener extends androidx.media3.session.MediaController.Listener { + method public default void onChildrenChanged(androidx.media3.session.MediaBrowser, String, @IntRange(from=0) int, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public default void onSearchResultChanged(androidx.media3.session.MediaBrowser, String, @IntRange(from=0) int, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + } + + public final class MediaConstants { + field public static final int ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT = 3; // 0x3 + field public static final String EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT = "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT"; + field public static final String EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT = "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL"; + field public static final String EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT = "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT"; + field public static final String EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV = "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS"; + } + + public class MediaController implements androidx.media3.common.Player { + method public void addListener(androidx.media3.common.Player.Listener); + method public void addMediaItem(androidx.media3.common.MediaItem); + method public void addMediaItem(int, androidx.media3.common.MediaItem); + method public void addMediaItems(java.util.List); + method public void addMediaItems(int, java.util.List); + method public boolean canAdvertiseSession(); + method public void clearMediaItems(); + method public void clearVideoSurface(); + method public void clearVideoSurface(@Nullable android.view.Surface); + method public void clearVideoSurfaceHolder(@Nullable android.view.SurfaceHolder); + method public void clearVideoSurfaceView(@Nullable android.view.SurfaceView); + method public void clearVideoTextureView(@Nullable android.view.TextureView); + method public void decreaseDeviceVolume(); + method public android.os.Looper getApplicationLooper(); + method public androidx.media3.common.AudioAttributes getAudioAttributes(); + method public androidx.media3.common.Player.Commands getAvailableCommands(); + method public androidx.media3.session.SessionCommands getAvailableSessionCommands(); + method @IntRange(from=0, to=100) public int getBufferedPercentage(); + method public long getBufferedPosition(); + method @Nullable public androidx.media3.session.SessionToken getConnectedToken(); + method public long getContentBufferedPosition(); + method public long getContentDuration(); + method public long getContentPosition(); + method public int getCurrentAdGroupIndex(); + method public int getCurrentAdIndexInAdGroup(); + method public androidx.media3.common.text.CueGroup getCurrentCues(); + method public long getCurrentLiveOffset(); + method @Nullable public androidx.media3.common.MediaItem getCurrentMediaItem(); + method public int getCurrentMediaItemIndex(); + method public int getCurrentPeriodIndex(); + method public long getCurrentPosition(); + method public androidx.media3.common.Timeline getCurrentTimeline(); + method public androidx.media3.common.Tracks getCurrentTracks(); + method public androidx.media3.common.DeviceInfo getDeviceInfo(); + method @IntRange(from=0) public int getDeviceVolume(); + method public long getDuration(); + method public long getMaxSeekToPreviousPosition(); + method public androidx.media3.common.MediaItem getMediaItemAt(int); + method public int getMediaItemCount(); + method public androidx.media3.common.MediaMetadata getMediaMetadata(); + method public int getNextMediaItemIndex(); + method public boolean getPlayWhenReady(); + method public androidx.media3.common.PlaybackParameters getPlaybackParameters(); + method @androidx.media3.common.Player.State public int getPlaybackState(); + method @androidx.media3.common.Player.PlaybackSuppressionReason public int getPlaybackSuppressionReason(); + method @Nullable public androidx.media3.common.PlaybackException getPlayerError(); + method public androidx.media3.common.MediaMetadata getPlaylistMetadata(); + method public int getPreviousMediaItemIndex(); + method @androidx.media3.common.Player.RepeatMode public int getRepeatMode(); + method public long getSeekBackIncrement(); + method public long getSeekForwardIncrement(); + method @Nullable public android.app.PendingIntent getSessionActivity(); + method public boolean getShuffleModeEnabled(); + method public long getTotalBufferedDuration(); + method public androidx.media3.common.TrackSelectionParameters getTrackSelectionParameters(); + method public androidx.media3.common.VideoSize getVideoSize(); + method @FloatRange(from=0, to=1) public float getVolume(); + method public boolean hasNextMediaItem(); + method public boolean hasPreviousMediaItem(); + method public void increaseDeviceVolume(); + method public boolean isCommandAvailable(@androidx.media3.common.Player.Command int); + method public boolean isConnected(); + method public boolean isCurrentMediaItemDynamic(); + method public boolean isCurrentMediaItemLive(); + method public boolean isCurrentMediaItemSeekable(); + method public boolean isDeviceMuted(); + method public boolean isLoading(); + method public boolean isPlaying(); + method public boolean isPlayingAd(); + method public boolean isSessionCommandAvailable(@androidx.media3.session.SessionCommand.CommandCode int); + method public boolean isSessionCommandAvailable(androidx.media3.session.SessionCommand); + method public void moveMediaItem(int, int); + method public void moveMediaItems(int, int, int); + method public void pause(); + method public void play(); + method public void prepare(); + method public void release(); + method public static void releaseFuture(java.util.concurrent.Future); + method public void removeListener(androidx.media3.common.Player.Listener); + method public void removeMediaItem(int); + method public void removeMediaItems(int, int); + method public void seekBack(); + method public void seekForward(); + method public void seekTo(long); + method public void seekTo(int, long); + method public void seekToDefaultPosition(); + method public void seekToDefaultPosition(int); + method public void seekToNext(); + method public void seekToNextMediaItem(); + method public void seekToPrevious(); + method public void seekToPreviousMediaItem(); + method public com.google.common.util.concurrent.ListenableFuture sendCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle); + method public void setDeviceMuted(boolean); + method public void setDeviceVolume(@IntRange(from=0) int); + method public void setMediaItem(androidx.media3.common.MediaItem); + method public void setMediaItem(androidx.media3.common.MediaItem, long); + method public void setMediaItem(androidx.media3.common.MediaItem, boolean); + method public void setMediaItems(java.util.List); + method public void setMediaItems(java.util.List, boolean); + method public void setMediaItems(java.util.List, int, long); + method public void setPlayWhenReady(boolean); + method public void setPlaybackParameters(androidx.media3.common.PlaybackParameters); + method public void setPlaybackSpeed(float); + method public void setPlaylistMetadata(androidx.media3.common.MediaMetadata); + method public com.google.common.util.concurrent.ListenableFuture setRating(String, androidx.media3.common.Rating); + method public com.google.common.util.concurrent.ListenableFuture setRating(androidx.media3.common.Rating); + method public void setRepeatMode(@androidx.media3.common.Player.RepeatMode int); + method public void setShuffleModeEnabled(boolean); + method public void setTrackSelectionParameters(androidx.media3.common.TrackSelectionParameters); + method public void setVideoSurface(@Nullable android.view.Surface); + method public void setVideoSurfaceHolder(@Nullable android.view.SurfaceHolder); + method public void setVideoSurfaceView(@Nullable android.view.SurfaceView); + method public void setVideoTextureView(@Nullable android.view.TextureView); + method public void setVolume(@FloatRange(from=0, to=1) float); + method public void stop(); + } + + public static final class MediaController.Builder { + ctor public MediaController.Builder(android.content.Context, androidx.media3.session.SessionToken); + method public com.google.common.util.concurrent.ListenableFuture buildAsync(); + method public androidx.media3.session.MediaController.Builder setApplicationLooper(android.os.Looper); + method public androidx.media3.session.MediaController.Builder setConnectionHints(android.os.Bundle); + method public androidx.media3.session.MediaController.Builder setListener(androidx.media3.session.MediaController.Listener); + } + + public static interface MediaController.Listener { + method public default void onAvailableSessionCommandsChanged(androidx.media3.session.MediaController, androidx.media3.session.SessionCommands); + method public default com.google.common.util.concurrent.ListenableFuture onCustomCommand(androidx.media3.session.MediaController, androidx.media3.session.SessionCommand, android.os.Bundle); + method public default void onDisconnected(androidx.media3.session.MediaController); + method public default void onExtrasChanged(androidx.media3.session.MediaController, android.os.Bundle); + method public default com.google.common.util.concurrent.ListenableFuture onSetCustomLayout(androidx.media3.session.MediaController, java.util.List); + } + + public abstract class MediaLibraryService extends androidx.media3.session.MediaSessionService { + ctor public MediaLibraryService(); + method @Nullable public abstract androidx.media3.session.MediaLibraryService.MediaLibrarySession onGetSession(androidx.media3.session.MediaSession.ControllerInfo); + field public static final String SERVICE_INTERFACE = "androidx.media3.session.MediaLibraryService"; + } + + public static final class MediaLibraryService.LibraryParams { + field public final boolean isOffline; + field public final boolean isRecent; + field public final boolean isSuggested; + } + + public static final class MediaLibraryService.LibraryParams.Builder { + ctor public MediaLibraryService.LibraryParams.Builder(); + method public androidx.media3.session.MediaLibraryService.LibraryParams build(); + method public androidx.media3.session.MediaLibraryService.LibraryParams.Builder setOffline(boolean); + method public androidx.media3.session.MediaLibraryService.LibraryParams.Builder setRecent(boolean); + method public androidx.media3.session.MediaLibraryService.LibraryParams.Builder setSuggested(boolean); + } + + public static final class MediaLibraryService.MediaLibrarySession extends androidx.media3.session.MediaSession { + method public void notifyChildrenChanged(androidx.media3.session.MediaSession.ControllerInfo, String, @IntRange(from=0) int, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public void notifyChildrenChanged(String, @IntRange(from=0) int, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public void notifySearchResultChanged(androidx.media3.session.MediaSession.ControllerInfo, String, @IntRange(from=0) int, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + } + + public static final class MediaLibraryService.MediaLibrarySession.Builder { + ctor public MediaLibraryService.MediaLibrarySession.Builder(androidx.media3.session.MediaLibraryService, androidx.media3.common.Player, androidx.media3.session.MediaLibraryService.MediaLibrarySession.Callback); + method public androidx.media3.session.MediaLibraryService.MediaLibrarySession build(); + method public androidx.media3.session.MediaLibraryService.MediaLibrarySession.Builder setExtras(android.os.Bundle); + method public androidx.media3.session.MediaLibraryService.MediaLibrarySession.Builder setId(String); + method public androidx.media3.session.MediaLibraryService.MediaLibrarySession.Builder setSessionActivity(android.app.PendingIntent); + } + + public static interface MediaLibraryService.MediaLibrarySession.Callback extends androidx.media3.session.MediaSession.Callback { + method public default com.google.common.util.concurrent.ListenableFuture>> onGetChildren(androidx.media3.session.MediaLibraryService.MediaLibrarySession, androidx.media3.session.MediaSession.ControllerInfo, String, @IntRange(from=0) int, @IntRange(from=1) int, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public default com.google.common.util.concurrent.ListenableFuture> onGetItem(androidx.media3.session.MediaLibraryService.MediaLibrarySession, androidx.media3.session.MediaSession.ControllerInfo, String); + method public default com.google.common.util.concurrent.ListenableFuture> onGetLibraryRoot(androidx.media3.session.MediaLibraryService.MediaLibrarySession, androidx.media3.session.MediaSession.ControllerInfo, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public default com.google.common.util.concurrent.ListenableFuture>> onGetSearchResult(androidx.media3.session.MediaLibraryService.MediaLibrarySession, androidx.media3.session.MediaSession.ControllerInfo, String, @IntRange(from=0) int, @IntRange(from=1) int, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public default com.google.common.util.concurrent.ListenableFuture> onSearch(androidx.media3.session.MediaLibraryService.MediaLibrarySession, androidx.media3.session.MediaSession.ControllerInfo, String, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public default com.google.common.util.concurrent.ListenableFuture> onSubscribe(androidx.media3.session.MediaLibraryService.MediaLibrarySession, androidx.media3.session.MediaSession.ControllerInfo, String, @Nullable androidx.media3.session.MediaLibraryService.LibraryParams); + method public default com.google.common.util.concurrent.ListenableFuture> onUnsubscribe(androidx.media3.session.MediaLibraryService.MediaLibrarySession, androidx.media3.session.MediaSession.ControllerInfo, String); + } + + public final class MediaNotification { + ctor public MediaNotification(@IntRange(from=1) int, android.app.Notification); + field public final android.app.Notification notification; + field @IntRange(from=1) public final int notificationId; + } + + public class MediaSession { + method public void broadcastCustomCommand(androidx.media3.session.SessionCommand, android.os.Bundle); + method public java.util.List getConnectedControllers(); + method public String getId(); + method public androidx.media3.common.Player getPlayer(); + method @Nullable public android.app.PendingIntent getSessionActivity(); + method public androidx.media3.session.SessionToken getToken(); + method public void release(); + method public com.google.common.util.concurrent.ListenableFuture sendCustomCommand(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommand, android.os.Bundle); + method public void setAvailableCommands(androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommands, androidx.media3.common.Player.Commands); + method public com.google.common.util.concurrent.ListenableFuture setCustomLayout(androidx.media3.session.MediaSession.ControllerInfo, java.util.List); + method public void setCustomLayout(java.util.List); + method public void setPlayer(androidx.media3.common.Player); + method public void setSessionExtras(android.os.Bundle); + method public void setSessionExtras(androidx.media3.session.MediaSession.ControllerInfo, android.os.Bundle); + } + + public static final class MediaSession.Builder { + ctor public MediaSession.Builder(android.content.Context, androidx.media3.common.Player); + method public androidx.media3.session.MediaSession build(); + method public androidx.media3.session.MediaSession.Builder setCallback(androidx.media3.session.MediaSession.Callback); + method public androidx.media3.session.MediaSession.Builder setExtras(android.os.Bundle); + method public androidx.media3.session.MediaSession.Builder setId(String); + method public androidx.media3.session.MediaSession.Builder setSessionActivity(android.app.PendingIntent); + } + + public static interface MediaSession.Callback { + method public default com.google.common.util.concurrent.ListenableFuture> onAddMediaItems(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, java.util.List); + method public default androidx.media3.session.MediaSession.ConnectionResult onConnect(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo); + method public default com.google.common.util.concurrent.ListenableFuture onCustomCommand(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.session.SessionCommand, android.os.Bundle); + method public default void onDisconnected(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo); + method @androidx.media3.session.SessionResult.Code public default int onPlayerCommandRequest(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, @androidx.media3.common.Player.Command int); + method public default void onPostConnect(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo); + method public default com.google.common.util.concurrent.ListenableFuture onSetRating(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, String, androidx.media3.common.Rating); + method public default com.google.common.util.concurrent.ListenableFuture onSetRating(androidx.media3.session.MediaSession, androidx.media3.session.MediaSession.ControllerInfo, androidx.media3.common.Rating); + } + + public static final class MediaSession.ConnectionResult { + method public static androidx.media3.session.MediaSession.ConnectionResult accept(androidx.media3.session.SessionCommands, androidx.media3.common.Player.Commands); + method public static androidx.media3.session.MediaSession.ConnectionResult reject(); + field public final androidx.media3.common.Player.Commands availablePlayerCommands; + field public final androidx.media3.session.SessionCommands availableSessionCommands; + field public final boolean isAccepted; + } + + public static final class MediaSession.ControllerInfo { + method public android.os.Bundle getConnectionHints(); + method public int getControllerVersion(); + method public String getPackageName(); + method public int getUid(); + field public static final int LEGACY_CONTROLLER_VERSION = 0; // 0x0 + } + + public abstract class MediaSessionService extends android.app.Service { + ctor public MediaSessionService(); + method public final void addSession(androidx.media3.session.MediaSession); + method public final java.util.List getSessions(); + method public final boolean isSessionAdded(androidx.media3.session.MediaSession); + method @CallSuper @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent); + method @Nullable public abstract androidx.media3.session.MediaSession onGetSession(androidx.media3.session.MediaSession.ControllerInfo); + method public void onUpdateNotification(androidx.media3.session.MediaSession); + method public final void removeSession(androidx.media3.session.MediaSession); + field public static final String SERVICE_INTERFACE = "androidx.media3.session.MediaSessionService"; + } + + public final class SessionCommand { + ctor public SessionCommand(@androidx.media3.session.SessionCommand.CommandCode int); + ctor public SessionCommand(String, android.os.Bundle); + field public static final int COMMAND_CODE_CUSTOM = 0; // 0x0 + field public static final int COMMAND_CODE_LIBRARY_GET_CHILDREN = 50003; // 0xc353 + field public static final int COMMAND_CODE_LIBRARY_GET_ITEM = 50004; // 0xc354 + field public static final int COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT = 50000; // 0xc350 + field public static final int COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT = 50006; // 0xc356 + field public static final int COMMAND_CODE_LIBRARY_SEARCH = 50005; // 0xc355 + field public static final int COMMAND_CODE_LIBRARY_SUBSCRIBE = 50001; // 0xc351 + field public static final int COMMAND_CODE_LIBRARY_UNSUBSCRIBE = 50002; // 0xc352 + field public static final int COMMAND_CODE_SESSION_SET_RATING = 40010; // 0x9c4a + field @androidx.media3.session.SessionCommand.CommandCode public final int commandCode; + field public final String customAction; + field public final android.os.Bundle customExtras; + } + + @IntDef({androidx.media3.session.SessionCommand.COMMAND_CODE_CUSTOM, androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_RATING, androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT, androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE, androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE, androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_CHILDREN, androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_ITEM, androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SEARCH, androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface SessionCommand.CommandCode { + } + + public final class SessionCommands { + method public androidx.media3.session.SessionCommands.Builder buildUpon(); + method public boolean contains(androidx.media3.session.SessionCommand); + method public boolean contains(@androidx.media3.session.SessionCommand.CommandCode int); + field public static final androidx.media3.session.SessionCommands EMPTY; + field public final com.google.common.collect.ImmutableSet commands; + } + + public static final class SessionCommands.Builder { + ctor public SessionCommands.Builder(); + method public androidx.media3.session.SessionCommands.Builder add(androidx.media3.session.SessionCommand); + method public androidx.media3.session.SessionCommands.Builder add(@androidx.media3.session.SessionCommand.CommandCode int); + method public androidx.media3.session.SessionCommands build(); + method public androidx.media3.session.SessionCommands.Builder remove(androidx.media3.session.SessionCommand); + method public androidx.media3.session.SessionCommands.Builder remove(@androidx.media3.session.SessionCommand.CommandCode int); + } + + public final class SessionResult { + ctor public SessionResult(@androidx.media3.session.SessionResult.Code int); + ctor public SessionResult(@androidx.media3.session.SessionResult.Code int, android.os.Bundle); + field public static final int RESULT_ERROR_BAD_VALUE = -3; // 0xfffffffd + field public static final int RESULT_ERROR_INVALID_STATE = -2; // 0xfffffffe + field public static final int RESULT_ERROR_IO = -5; // 0xfffffffb + field public static final int RESULT_ERROR_NOT_SUPPORTED = -6; // 0xfffffffa + field public static final int RESULT_ERROR_PERMISSION_DENIED = -4; // 0xfffffffc + field public static final int RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED = -102; // 0xffffff9a + field public static final int RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT = -104; // 0xffffff98 + field public static final int RESULT_ERROR_SESSION_DISCONNECTED = -100; // 0xffffff9c + field public static final int RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION = -106; // 0xffffff96 + field public static final int RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = -105; // 0xffffff97 + field public static final int RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = -103; // 0xffffff99 + field public static final int RESULT_ERROR_SESSION_SETUP_REQUIRED = -108; // 0xffffff94 + field public static final int RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED = -107; // 0xffffff95 + field public static final int RESULT_ERROR_UNKNOWN = -1; // 0xffffffff + field public static final int RESULT_INFO_SKIPPED = 1; // 0x1 + field public static final int RESULT_SUCCESS = 0; // 0x0 + field public final long completionTimeMs; + field public final android.os.Bundle extras; + field @androidx.media3.session.SessionResult.Code public final int resultCode; + } + + @IntDef({androidx.media3.session.SessionResult.RESULT_SUCCESS, androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN, androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE, androidx.media3.session.SessionResult.RESULT_ERROR_BAD_VALUE, androidx.media3.session.SessionResult.RESULT_ERROR_PERMISSION_DENIED, androidx.media3.session.SessionResult.RESULT_ERROR_IO, androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionResult.RESULT_ERROR_NOT_SUPPORTED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface SessionResult.Code { + } + + public final class SessionToken { + ctor public SessionToken(android.content.Context, android.content.ComponentName); + method public static com.google.common.collect.ImmutableSet getAllServiceTokens(android.content.Context); + method public android.os.Bundle getExtras(); + method public String getPackageName(); + method public String getServiceName(); + method public int getSessionVersion(); + method @androidx.media3.session.SessionToken.TokenType public int getType(); + method public int getUid(); + field public static final int TYPE_LIBRARY_SERVICE = 2; // 0x2 + field public static final int TYPE_SESSION = 0; // 0x0 + field public static final int TYPE_SESSION_SERVICE = 1; // 0x1 + } + + @IntDef({androidx.media3.session.SessionToken.TYPE_SESSION, androidx.media3.session.SessionToken.TYPE_SESSION_SERVICE, androidx.media3.session.SessionToken.TYPE_LIBRARY_SERVICE}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface SessionToken.TokenType { + } + +} + +package androidx.media3.ui { + + public class PlayerView extends android.widget.FrameLayout implements androidx.media3.common.AdViewProvider { + ctor public PlayerView(android.content.Context); + ctor public PlayerView(android.content.Context, @Nullable android.util.AttributeSet); + ctor public PlayerView(android.content.Context, @Nullable android.util.AttributeSet, int); + method public android.view.ViewGroup getAdViewGroup(); + method @Nullable public androidx.media3.common.Player getPlayer(); + method public boolean getUseController(); + method public void onPause(); + method public void onResume(); + method public void setControllerVisibilityListener(@Nullable androidx.media3.ui.PlayerView.ControllerVisibilityListener); + method public void setErrorMessageProvider(@Nullable androidx.media3.common.ErrorMessageProvider); + method public void setFullscreenButtonClickListener(@Nullable androidx.media3.ui.PlayerView.FullscreenButtonClickListener); + method public void setPlayer(@Nullable androidx.media3.common.Player); + method public void setUseController(boolean); + } + + public static interface PlayerView.ControllerVisibilityListener { + method public void onVisibilityChanged(int); + } + + public static interface PlayerView.FullscreenButtonClickListener { + method public void onFullscreenButtonClick(boolean); + } + +} + From 5f741bbe5a2401521a368846e83861864689116b Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Fri, 17 Jun 2022 10:08:37 +0100 Subject: [PATCH 45/45] Add lint baseline for spurious API-level warnings The API 32 SDK has incorrect versioning metadata for Spatializer. It reports the whole class has only been present since API 33 (which is surely impossible given it's present in the API 32 SDK): https://issuetracker.google.com/234009300 The metadata seems to be correct in the API 33 SDK, so this baseline will no longer be needed when we bump to `compileSdkVersion = 33`. --- libraries/exoplayer/build.gradle | 4 + libraries/exoplayer/lint-baseline.xml | 125 ++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 libraries/exoplayer/lint-baseline.xml diff --git a/libraries/exoplayer/build.gradle b/libraries/exoplayer/build.gradle index 6e4369a9e8..cffbbbb81b 100644 --- a/libraries/exoplayer/build.gradle +++ b/libraries/exoplayer/build.gradle @@ -28,6 +28,10 @@ android { } } + lint { + baseline = file("lint-baseline.xml") + } + sourceSets { androidTest.assets.srcDir '../test_data/src/test/assets' test.assets.srcDir '../test_data/src/test/assets/' diff --git a/libraries/exoplayer/lint-baseline.xml b/libraries/exoplayer/lint-baseline.xml new file mode 100644 index 0000000000..d05354ba10 --- /dev/null +++ b/libraries/exoplayer/lint-baseline.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +