Commit 72a4fb08 by Ian Baker Committed by GitHub

Merge pull request #52 from androidx/release-1.0

r1.0.0-alpha03
parents 850bd69d 58324934
Showing with 374 additions and 296 deletions
......@@ -94,15 +94,13 @@ to prevent build errors.
Cloning the repository and depending on the modules locally is required when
using some libraries. It's also a suitable approach if you want to make local
changes, or if you want to use the main branch.
changes, or if you want to use the `main` branch.
First, clone the repository into a local directory and checkout the desired
branch:
First, clone the repository into a local directory:
```sh
git clone https://github.com/androidx/media.git
cd media
git checkout main
```
Next, add the following to your project's `settings.gradle` file, replacing
......@@ -129,7 +127,7 @@ implementation project(':media-lib-ui')
Development work happens on the `main` branch. Pull requests should normally be
made to this branch.
We plan to add a release branch soon.
The `release` branch holds the most recent stable release.
#### Using Android Studio
......
# Release notes
### 1.0.0-alpha02 (2022-03-09)
### 1.0.0-alpha03 (2022-03-14)
This release corresponds to the
[ExoPlayer 2.17.1 release](https://github.com/google/ExoPlayer/releases/tag/r2.17.1).
* Audio:
* Fix error checking audio capabilities for Dolby Atmos (E-AC3-JOC) in
HLS.
* Extractors:
* FMP4: Fix issue where emsg sample metadata could be output in the wrong
order for streams containing both v0 and v1 emsg atoms
([#9996](https://github.com/google/ExoPlayer/issues/9996)).
* Text:
* Fix the interaction of `SingleSampleMediaSource.Factory.setTrackId` and
`MediaItem.SubtitleConfiguration.Builder.setId` to prioritise the
`SubtitleConfiguration` field and fall back to the `Factory` value if
it's not set
([#10016](https://github.com/google/ExoPlayer/issues/10016)).
* Ad playback:
* Fix audio underruns between ad periods in live HLS SSAI streams.
### 1.0.0-alpha02 (2022-03-02)
This release corresponds to the
[ExoPlayer 2.17.0 release](https://github.com/google/ExoPlayer/releases/tag/r2.17.0).
* Core Library:
* Add protected method `DefaultRenderersFactory.getCodecAdapterFactory()`
......@@ -18,7 +42,7 @@
from a secure codec to another codec
([#8696](https://github.com/google/ExoPlayer/issues/8696)).
* Add `MediaCodecAdapter.getMetrics()` to allow users obtain metrics data
from `MediaCodec`.
from `MediaCodec`
([#9766](https://github.com/google/ExoPlayer/issues/9766)).
* Fix Maven dependency resolution
([#8353](https://github.com/google/ExoPlayer/issues/8353)).
......@@ -63,17 +87,17 @@
under sufficient network bandwidth even if playback is very close to the
live edge ([#9784](https://github.com/google/ExoPlayer/issues/9784)).
* Video:
* Fix decoder fallback logic for Dolby Vision
to use a compatible H264/H265 decoder if needed.
* Fix decoder fallback logic for Dolby Vision to use a compatible
H264/H265 decoder if needed.
* Audio:
* Fix decoder fallback logic for Dolby Atmos (E-AC3-JOC)
to use a compatible E-AC3 decoder if needed.
* Fix decoder fallback logic for Dolby Atmos (E-AC3-JOC) to use a
compatible E-AC3 decoder if needed.
* Change `AudioCapabilities` APIs to require passing explicitly
`AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES` instead of `null`.
* Allow customization of the `AudioTrack` buffer size calculation by
injecting an `AudioTrackBufferSizeProvider` to `DefaultAudioSink`.
injecting an `AudioTrackBufferSizeProvider` to `DefaultAudioSink`
([#8891](https://github.com/google/ExoPlayer/issues/8891)).
* Retry `AudioTrack` creation if the requested buffer size was > 1MB.
* Retry `AudioTrack` creation if the requested buffer size was > 1MB
([#9712](https://github.com/google/ExoPlayer/issues/9712)).
* Extractors:
* WAV: Add support for RF64 streams
......@@ -120,7 +144,8 @@
* Support the `forced-subtitle` track role
([#9727](https://github.com/google/ExoPlayer/issues/9727)).
* Stop interpreting the `main` track role as `C.SELECTION_FLAG_DEFAULT`.
* Fix base URL exclusion logic for manifests that do not declare the DVB namespace ([#9856](https://github.com/google/ExoPlayer/issues/9856)).
* Fix base URL exclusion logic for manifests that do not declare the DVB
namespace ([#9856](https://github.com/google/ExoPlayer/issues/9856)).
* Support relative `MPD.Location` URLs
([#9939](https://github.com/google/ExoPlayer/issues/9939)).
* HLS:
......@@ -133,8 +158,6 @@
`HlsMediaSource.Factory.setAllowChunklessPreparation(false)`.
* Support key-frame accurate seeking in HLS
([#2882](https://github.com/google/ExoPlayer/issues/2882)).
* Correctly populate `Format.label` for audio only HLS streams
([#9608](https://github.com/google/ExoPlayer/issues/9608)).
* RTSP:
* Provide a client API to override the `SocketFactory` used for any server
connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)).
......@@ -154,12 +177,10 @@
* Fix potential NPE in `Transformer.getProgress` when releasing the muxer
throws.
* Add a demo app for applying transformations.
* The transformer module is no longer included by depending on
`com.google.android.exoplayer:exoplayer`. To continue using transformer,
add an additional dependency on
`com.google.android.exoplayer:exoplayer-transformer`.
* MediaSession extension:
* By default, `MediaSessionConnector` now clears the playlist on stop. Apps that want the playlist to be retained can call `setClearMediaItemsOnStop(false)` on the connector.
* By default, `MediaSessionConnector` now clears the playlist on stop.
Apps that want the playlist to be retained can call
`setClearMediaItemsOnStop(false)` on the connector.
* Cast extension:
* Fix bug that prevented `CastPlayer` from calling `onIsPlayingChanged`
correctly ([#9792](https://github.com/google/ExoPlayer/issues/9792)).
......@@ -178,38 +199,38 @@
([#9528](https://github.com/google/ExoPlayer/issues/9528)).
* Remove deprecated symbols:
* Remove `Player.EventLister`. Use `Player.Listener` instead.
* Remove `MediaSourceFactory#setDrmSessionManager`,
`MediaSourceFactory#setDrmHttpDataSourceFactory`, and
`MediaSourceFactory#setDrmUserAgent`. Use
`MediaSourceFactory#setDrmSessionManagerProvider` instead.
* Remove `MediaSourceFactory#setStreamKeys`. Use
`MediaItem.Builder#setStreamKeys` instead.
* Remove `MediaSourceFactory#createMediaSource(Uri)`. Use
`MediaSourceFactory#createMediaSource(MediaItem)` instead.
* Remove `MediaSourceFactory.setDrmSessionManager`,
`MediaSourceFactory.setDrmHttpDataSourceFactory`, and
`MediaSourceFactory.setDrmUserAgent`. Use
`MediaSourceFactory.setDrmSessionManagerProvider` instead.
* Remove `MediaSourceFactory.setStreamKeys`. Use
`MediaItem.Builder.setStreamKeys` instead.
* Remove `MediaSourceFactory.createMediaSource(Uri)`. Use
`MediaSourceFactory.createMediaSource(MediaItem)` instead.
* Remove `setTag` from `DashMediaSource`, `HlsMediaSource` and
`SsMediaSource`. Use `MediaItem.Builder#setTag` instead.
* Remove `DashMediaSource#setLivePresentationDelayMs(long, boolean)`. Use
`MediaItem.Builder#setLiveConfiguration` and
`MediaItem.LiveConfiguration.Builder#setTargetOffsetMs` to override the
manifest, or `DashMediaSource#setFallbackTargetLiveOffsetMs` to provide
`SsMediaSource`. Use `MediaItem.Builder.setTag` instead.
* Remove `DashMediaSource.setLivePresentationDelayMs(long, boolean)`. Use
`MediaItem.Builder.setLiveConfiguration` and
`MediaItem.LiveConfiguration.Builder.setTargetOffsetMs` to override the
manifest, or `DashMediaSource.setFallbackTargetLiveOffsetMs` to provide
a fallback value.
* Remove `(Simple)ExoPlayer.setThrowsWhenUsingWrongThread`. Opting out of
the thread enforcement is no longer possible.
* Remove `ActionFile` and `ActionFileUpgradeUtil`. Use ExoPlayer 2.16.1 or
before to use `ActionFileUpgradeUtil` to merge legacy action files into
`DefaultDownloadIndex`.
* Remove `ProgressiveMediaSource#setExtractorsFactory`. Use
* Remove `ProgressiveMediaSource.setExtractorsFactory`. Use
`ProgressiveMediaSource.Factory(DataSource.Factory, ExtractorsFactory)`
constructor instead.
* Remove `ProgressiveMediaSource.Factory#setTag` and, and
`ProgressiveMediaSource.Factory#setCustomCacheKey`. Use
`MediaItem.Builder#setTag` and `MediaItem.Builder#setCustomCacheKey`
* Remove `ProgressiveMediaSource.Factory.setTag` and
`ProgressiveMediaSource.Factory.setCustomCacheKey`. Use
`MediaItem.Builder.setTag` and `MediaItem.Builder.setCustomCacheKey`
instead.
* Remove `DefaultRenderersFactory(Context, @ExtensionRendererMode int)`
and `DefaultRenderersFactory(Context, @ExtensionRendererMode int, long)`
constructors. Use the `DefaultRenderersFactory(Context)` constructor,
`DefaultRenderersFactory#setExtensionRendererMode`, and
`DefaultRenderersFactory#setAllowedVideoJoiningTimeMs` instead.
`DefaultRenderersFactory.setExtensionRendererMode`, and
`DefaultRenderersFactory.setAllowedVideoJoiningTimeMs` instead.
* Remove all public `CronetDataSource` constructors. Use
`CronetDataSource.Factory` instead.
* Change the following `IntDefs` to `@Target(TYPE_USE)` only. This may break
......
......@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
project.ext {
releaseVersion = '1.0.0-alpha02'
releaseVersionCode = 1_000_000_0_02
releaseVersion = '1.0.0-alpha03'
releaseVersionCode = 1_000_000_0_03
minSdkVersion = 16
appTargetSdkVersion = 29
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
......
......@@ -30,6 +30,7 @@ import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.session.MediaBrowser
import androidx.media3.session.SessionToken
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
......@@ -179,6 +180,9 @@ class PlayableFolderActivity : AppCompatActivity() {
returnConvertView.findViewById<TextView>(R.id.add_button).setOnClickListener {
val browser = this@PlayableFolderActivity.browser ?: return@setOnClickListener
browser.addMediaItem(mediaItem)
if (browser.playbackState == Player.STATE_IDLE) {
browser.prepare()
}
Snackbar.make(
findViewById<LinearLayout>(R.id.linear_layout),
getString(R.string.added_media_item_format, mediaItem.mediaMetadata.title),
......
......@@ -96,7 +96,6 @@ class PlaybackService : MediaLibraryService() {
val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem()
player.setMediaItem(item)
player.prepare()
}
override fun onSetMediaUri(
......
#Wed Mar 04 12:41:50 GMT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https://services.gradle.org/distributions/gradle-7.3.3-all.zip
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
......@@ -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-alpha02";
public static final String VERSION = "1.0.0-alpha03";
/** 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-alpha02";
public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-alpha03";
/**
* 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_02;
public static final int VERSION_INT = 1_000_000_0_03;
/** Whether the library was compiled with {@link Assertions} checks enabled. */
public static final boolean ASSERTIONS_ENABLED = true;
......
......@@ -56,6 +56,7 @@ import androidx.media3.exoplayer.DefaultMediaClock.PlaybackParametersListener;
import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.metadata.MetadataRenderer;
import androidx.media3.exoplayer.source.BehindLiveWindowException;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
......@@ -2228,6 +2229,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
return reading.info.isFollowedByTransitionToSameStream
&& nextPeriod.prepared
&& (renderer instanceof TextRenderer // [internal: b/181312195]
|| renderer instanceof MetadataRenderer
|| renderer.getReadingPositionUs() >= nextPeriod.getStartPositionRendererTime());
}
......
......@@ -1739,8 +1739,11 @@ public final class DefaultAudioSink implements AudioSink {
// 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, format.sampleRate);
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;
......
......@@ -24,6 +24,8 @@ import static java.lang.annotation.ElementType.TYPE_USE;
import androidx.annotation.IntDef;
import androidx.media3.common.util.UnstableApi;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** Thrown when the requested DRM scheme is not supported. */
......@@ -35,8 +37,9 @@ public final class UnsupportedDrmException extends Exception {
* #REASON_INSTANTIATION_ERROR}.
*/
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
// with Kotlin usages from before TYPE_USE was added. @Retention(RetentionPolicy.SOURCE)
// with Kotlin usages from before TYPE_USE was added.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@IntDef({REASON_UNSUPPORTED_SCHEME, REASON_INSTANTIATION_ERROR})
public @interface Reason {}
......
......@@ -74,11 +74,12 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
}
/**
* Sets an optional track id to be used.
*
* @param trackId An optional track id.
* @return This factory, for convenience.
* @deprecated Use {@link MediaItem.SubtitleConfiguration.Builder#setId(String)} instead (on the
* {@link MediaItem.SubtitleConfiguration} passed to {@link
* #createMediaSource(MediaItem.SubtitleConfiguration, long)}). {@code trackId} will only be
* used if {@link MediaItem.SubtitleConfiguration#id} is {@code null}.
*/
@Deprecated
public Factory setTrackId(@Nullable String trackId) {
this.trackId = trackId;
return this;
......@@ -157,29 +158,28 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
this.durationUs = durationUs;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
mediaItem =
this.mediaItem =
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setMediaId(subtitleConfiguration.uri.toString())
.setSubtitleConfigurations(ImmutableList.of(subtitleConfiguration))
.setTag(tag)
.build();
format =
this.format =
new Format.Builder()
.setId(trackId)
.setSampleMimeType(firstNonNull(subtitleConfiguration.mimeType, MimeTypes.TEXT_UNKNOWN))
.setLanguage(subtitleConfiguration.language)
.setSelectionFlags(subtitleConfiguration.selectionFlags)
.setRoleFlags(subtitleConfiguration.roleFlags)
.setLabel(subtitleConfiguration.label)
.setId(subtitleConfiguration.id)
.setId(subtitleConfiguration.id != null ? subtitleConfiguration.id : trackId)
.build();
dataSpec =
this.dataSpec =
new DataSpec.Builder()
.setUri(subtitleConfiguration.uri)
.setFlags(DataSpec.FLAG_ALLOW_GZIP)
.build();
timeline =
this.timeline =
new SinglePeriodTimeline(
durationUs,
/* isSeekable= */ true,
......
......@@ -105,11 +105,7 @@ import java.util.Map;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* MediaSource for IMA server side inserted ad streams.
*
* <p>TODO(bachinger) add code snippet from PlayerActivity
*/
/** MediaSource for IMA server side inserted ad streams. */
@UnstableApi
public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSource<Void> {
......@@ -119,8 +115,6 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
*
* <p>Apps can use the {@link ImaServerSideAdInsertionMediaSource.Factory} to customized the
* {@link DefaultMediaSourceFactory} that is used to build a player:
*
* <p>TODO(bachinger) add code snippet from PlayerActivity
*/
public static final class Factory implements MediaSource.Factory {
......@@ -461,6 +455,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Nullable private IOException loadError;
private @MonotonicNonNull Timeline contentTimeline;
private AdPlaybackState adPlaybackState;
private int firstSeenAdIndexInAdGroup;
private ImaServerSideAdInsertionMediaSource(
MediaItem mediaItem,
......@@ -698,18 +693,21 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
return adPlaybackState;
}
private static AdPlaybackState addLiveAdBreak(
private AdPlaybackState addLiveAdBreak(
Ad ad, long currentPeriodPositionUs, AdPlaybackState adPlaybackState) {
AdPodInfo adPodInfo = ad.getAdPodInfo();
long adDurationUs = secToUs(ad.getDuration());
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
// TODO(b/208398934) Support seeking backwards.
if (adIndexInAdGroup == 0 || adPlaybackState.adGroupCount == 1) {
firstSeenAdIndexInAdGroup = adIndexInAdGroup;
// Adjust count and ad index in case we joined the live stream within an ad group.
int adCount = adPodInfo.getTotalAds() - firstSeenAdIndexInAdGroup;
adIndexInAdGroup -= firstSeenAdIndexInAdGroup;
// First ad of group. Create a new group with all ads.
long[] adDurationsUs =
updateAdDurationAndPropagate(
new long[adPodInfo.getTotalAds()],
new long[adCount],
adIndexInAdGroup,
adDurationUs,
secToUs(adPodInfo.getMaxDuration()));
......@@ -721,6 +719,11 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
/* adDurationsUs...= */ adDurationsUs);
} else {
int adGroupIndex = adPlaybackState.adGroupCount - 2;
adIndexInAdGroup -= firstSeenAdIndexInAdGroup;
if (adPodInfo.getTotalAds() == adPodInfo.getAdPosition()) {
// Reset the ad index whe we are at the last ad in the group.
firstSeenAdIndexInAdGroup = 0;
}
adPlaybackState =
updateAdDurationInAdGroup(adGroupIndex, adIndexInAdGroup, adDurationUs, adPlaybackState);
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
......@@ -857,7 +860,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
long positionInWindowUs =
timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period())
.positionInWindowUs;
long currentPeriodPosition = msToUs(player.getCurrentPosition()) - positionInWindowUs;
long currentPeriodPosition = msToUs(player.getContentPosition()) - positionInWindowUs;
newAdPlaybackState =
addLiveAdBreak(
event.getAd(),
......
......@@ -666,14 +666,23 @@ public class FragmentedMp4Extractor implements Extractor {
emsgTrackOutput.sampleData(encodedEventMessage, sampleSize);
}
// Output the sample metadata. This is made a little complicated because emsg-v0 atoms
// have presentation time *delta* while v1 atoms have absolute presentation time.
// Output the sample metadata.
if (sampleTimeUs == C.TIME_UNSET) {
// We need the first sample timestamp in the segment before we can output the metadata.
// We're processing a v0 emsg atom, which contains a presentation time delta, and cannot yet
// calculate its absolute sample timestamp. Defer outputting the metadata until we can.
pendingMetadataSampleInfos.addLast(
new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize));
new MetadataSampleInfo(
presentationTimeDeltaUs, /* sampleTimeIsRelative= */ true, sampleSize));
pendingMetadataSampleBytes += sampleSize;
} else if (!pendingMetadataSampleInfos.isEmpty()) {
// We also need to defer outputting metadata if pendingMetadataSampleInfos is non-empty, else
// we will output metadata for samples in the wrong order. See:
// https://github.com/google/ExoPlayer/issues/9996.
pendingMetadataSampleInfos.addLast(
new MetadataSampleInfo(sampleTimeUs, /* sampleTimeIsRelative= */ false, sampleSize));
pendingMetadataSampleBytes += sampleSize;
} else {
// We can output the sample metadata immediately.
if (timestampAdjuster != null) {
sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
}
......@@ -1459,19 +1468,30 @@ public class FragmentedMp4Extractor implements Extractor {
return true;
}
/**
* Called immediately after outputting a non-metadata sample, to output any pending metadata
* samples.
*
* @param sampleTimeUs The timestamp of the non-metadata sample that was just output.
*/
private void outputPendingMetadataSamples(long sampleTimeUs) {
while (!pendingMetadataSampleInfos.isEmpty()) {
MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();
pendingMetadataSampleBytes -= sampleInfo.size;
long metadataTimeUs = sampleTimeUs + sampleInfo.presentationTimeDeltaUs;
MetadataSampleInfo metadataSampleInfo = pendingMetadataSampleInfos.removeFirst();
pendingMetadataSampleBytes -= metadataSampleInfo.size;
long metadataSampleTimeUs = metadataSampleInfo.sampleTimeUs;
if (metadataSampleInfo.sampleTimeIsRelative) {
// The metadata sample timestamp is relative to the timestamp of the non-metadata sample
// that was just output. Make it absolute.
metadataSampleTimeUs += sampleTimeUs;
}
if (timestampAdjuster != null) {
metadataTimeUs = timestampAdjuster.adjustSampleTimestamp(metadataTimeUs);
metadataSampleTimeUs = timestampAdjuster.adjustSampleTimestamp(metadataSampleTimeUs);
}
for (TrackOutput emsgTrackOutput : emsgTrackOutputs) {
emsgTrackOutput.sampleMetadata(
metadataTimeUs,
metadataSampleTimeUs,
C.BUFFER_FLAG_KEY_FRAME,
sampleInfo.size,
metadataSampleInfo.size,
pendingMetadataSampleBytes,
null);
}
......@@ -1577,11 +1597,13 @@ public class FragmentedMp4Extractor implements Extractor {
/** Holds data corresponding to a metadata sample. */
private static final class MetadataSampleInfo {
public final long presentationTimeDeltaUs;
public final long sampleTimeUs;
public final boolean sampleTimeIsRelative;
public final int size;
public MetadataSampleInfo(long presentationTimeDeltaUs, int size) {
this.presentationTimeDeltaUs = presentationTimeDeltaUs;
public MetadataSampleInfo(long sampleTimeUs, boolean sampleTimeIsRelative, int size) {
this.sampleTimeUs = sampleTimeUs;
this.sampleTimeIsRelative = sampleTimeIsRelative;
this.size = size;
}
}
......
......@@ -27,6 +27,7 @@ import android.os.Bundle;
import androidx.core.app.NotificationCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
......@@ -93,20 +94,21 @@ import androidx.media3.common.util.Util;
IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_previous),
context.getString(R.string.media3_controls_seek_to_previous_description),
MediaNotification.ActionFactory.COMMAND_SKIP_TO_PREVIOUS));
if (mediaController.getPlayWhenReady()) {
// Pause action.
builder.addAction(
actionFactory.createMediaAction(
IconCompat.createWithResource(context, R.drawable.media3_notification_pause),
context.getString(R.string.media3_controls_pause_description),
MediaNotification.ActionFactory.COMMAND_PAUSE));
} else {
if (mediaController.getPlaybackState() == Player.STATE_ENDED
|| !mediaController.getPlayWhenReady()) {
// Play action.
builder.addAction(
actionFactory.createMediaAction(
IconCompat.createWithResource(context, R.drawable.media3_notification_play),
context.getString(R.string.media3_controls_play_description),
MediaNotification.ActionFactory.COMMAND_PLAY));
} else {
// Pause action.
builder.addAction(
actionFactory.createMediaAction(
IconCompat.createWithResource(context, R.drawable.media3_notification_pause),
context.getString(R.string.media3_controls_pause_description),
MediaNotification.ActionFactory.COMMAND_PAUSE));
}
// Skip to next action.
builder.addAction(
......
......@@ -198,7 +198,7 @@ import java.util.concurrent.atomic.AtomicReference;
try {
int page = options.getInt(EXTRA_PAGE);
int pageSize = options.getInt(EXTRA_PAGE_SIZE);
if (page > 0 && pageSize > 0) {
if (page >= 0 && pageSize > 0) {
// Requesting the list of children through pagination.
@Nullable
LibraryParams params =
......@@ -223,7 +223,7 @@ import java.util.concurrent.atomic.AtomicReference;
parentId,
/* page= */ 0,
/* pageSize= */ Integer.MAX_VALUE,
/* extras= */ null);
/* params= */ null);
sendLibraryResultWithMediaItemsWhenReady(result, future);
});
}
......
......@@ -196,7 +196,9 @@ import java.util.concurrent.TimeoutException;
@Override
public void onEvents(Player player, Player.Events events) {
if (events.containsAny(
Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_MEDIA_METADATA_CHANGED)) {
Player.EVENT_PLAYBACK_STATE_CHANGED,
Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_MEDIA_METADATA_CHANGED)) {
updateNotification(session);
}
}
......
......@@ -28,6 +28,8 @@ 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;
import static androidx.media3.common.Player.COMMAND_STOP;
import static androidx.media3.common.Player.STATE_ENDED;
import static androidx.media3.common.Player.STATE_IDLE;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.postOrRun;
......@@ -231,7 +233,17 @@ import org.checkerframework.checker.initialization.qual.Initialized;
} else {
dispatchSessionTaskWithPlayerCommand(
COMMAND_PLAY_PAUSE,
(controller) -> sessionImpl.getPlayerWrapper().play(),
(controller) -> {
PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper();
@Player.State int playbackState = playerWrapper.getPlaybackState();
if (playbackState == STATE_IDLE) {
playerWrapper.prepare();
} else if (playbackState == STATE_ENDED) {
playerWrapper.seekTo(
playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET);
}
playerWrapper.play();
},
remoteUserInfo);
}
}
......@@ -285,7 +297,17 @@ import org.checkerframework.checker.initialization.qual.Initialized;
public void onPlay() {
dispatchSessionTaskWithPlayerCommand(
COMMAND_PLAY_PAUSE,
controller -> sessionImpl.getPlayerWrapper().play(),
controller -> {
PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper();
@Player.State int playbackState = playerWrapper.getPlaybackState();
if (playbackState == Player.STATE_IDLE) {
playerWrapper.prepare();
} else if (playbackState == Player.STATE_ENDED) {
playerWrapper.seekTo(
playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET);
}
playerWrapper.play();
},
sessionCompat.getCurrentControllerInfo());
}
......@@ -321,7 +343,15 @@ import org.checkerframework.checker.initialization.qual.Initialized;
if (sessionImpl.onSetMediaUriOnHandler(
controller, mediaUri, extras == null ? Bundle.EMPTY : extras)
== RESULT_SUCCESS) {
sessionImpl.getPlayerWrapper().play();
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();
}
});
}
......
......@@ -181,7 +181,6 @@ public class MediaSessionAndControllerTest {
MockPlayer player =
new MockPlayer.Builder()
.setApplicationLooper(threadTestRule.getHandler().getLooper())
.setLatchCount(1)
.build();
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(
......@@ -190,8 +189,7 @@ public class MediaSessionAndControllerTest {
threadTestRule.getHandler().postAndSync(controller::play);
assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(player.playCalled).isTrue();
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
}
@Test
......
......@@ -74,7 +74,6 @@ public class MediaSessionCallbackTest {
context = ApplicationProvider.getApplicationContext();
player =
new MockPlayer.Builder()
.setLatchCount(1)
.setApplicationLooper(threadTestRule.getHandler().getLooper())
.build();
}
......@@ -157,15 +156,14 @@ public class MediaSessionCallbackTest {
controllerTestRule.createRemoteController(session.getToken());
controller.prepare();
assertThat(player.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse();
assertThat(player.prepareCalled).isFalse();
Thread.sleep(NO_RESPONSE_TIMEOUT_MS);
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse();
assertThat(commands).hasSize(1);
assertThat(commands.get(0)).isEqualTo(Player.COMMAND_PREPARE);
controller.play();
assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(player.playCalled).isTrue();
assertThat(player.prepareCalled).isFalse();
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse();
assertThat(commands).hasSize(2);
assertThat(commands.get(1)).isEqualTo(Player.COMMAND_PLAY_PAUSE);
}
......
......@@ -89,8 +89,7 @@ public class MediaSessionKeyEventTest {
Context context = ApplicationProvider.getApplicationContext();
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
handler = threadTestRule.getHandler();
player =
new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build();
player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build();
sessionCallback = new TestSessionCallback();
session = new MediaSession.Builder(context, player).setSessionCallback(sessionCallback).build();
......@@ -120,7 +119,7 @@ public class MediaSessionKeyEventTest {
}
@After
public void cleanUp() throws Exception {
public void tearDown() throws Exception {
handler.postAndSync(
() -> {
if (mediaPlayer != null) {
......@@ -131,55 +130,46 @@ public class MediaSessionKeyEventTest {
session.release();
}
private void dispatchMediaKeyEvent(int keyCode, boolean doubleTap) {
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
if (doubleTap) {
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
}
}
@Test
public void playKeyEvent() throws Exception {
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY, false);
assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(player.playCalled).isTrue();
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
}
@Test
public void pauseKeyEvent() throws Exception {
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PAUSE, false);
assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(player.pauseCalled).isTrue();
player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS);
}
@Test
public void nextKeyEvent() throws Exception {
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT, false);
assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(player.seekToNextCalled).isTrue();
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS);
}
@Test
public void previousKeyEvent() throws Exception {
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PREVIOUS, false);
assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(player.seekToPreviousCalled).isTrue();
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS);
}
@Test
public void stopKeyEvent() throws Exception {
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_STOP, false);
assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(player.stopCalled).isTrue();
player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS);
}
@Test
public void playPauseKeyEvent_play() throws Exception {
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false);
assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(player.playCalled).isTrue();
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
}
@Test
......@@ -188,18 +178,28 @@ public class MediaSessionKeyEventTest {
() -> {
player.playWhenReady = true;
});
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false);
assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(player.pauseCalled).isTrue();
player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS);
}
@Test
public void playPauseKeyEvent_doubleTapIsTranslatedToSkipToNext() throws Exception {
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, true);
assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(player.seekToNextCalled).isTrue();
assertThat(player.playCalled).isFalse();
assertThat(player.pauseCalled).isFalse();
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS);
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isFalse();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PAUSE)).isFalse();
}
private void dispatchMediaKeyEvent(int keyCode, boolean doubleTap) {
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
if (doubleTap) {
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
}
}
private static class TestSessionCallback implements MediaSession.SessionCallback {
......
......@@ -88,7 +88,7 @@ public class MediaSessionPermissionTest {
}
@After
public void cleanUp() {
public void tearDown() {
if (session != null) {
session.release();
session = null;
......@@ -97,55 +97,6 @@ public class MediaSessionPermissionTest {
callback = null;
}
private void createSessionWithAvailableCommands(
SessionCommands sessionCommands, Player.Commands playerCommands) {
player =
new MockPlayer.Builder()
.setLatchCount(1)
.setApplicationLooper(threadTestRule.getHandler().getLooper())
.build();
callback =
new MySessionCallback() {
@Override
public MediaSession.ConnectionResult onConnect(
MediaSession session, ControllerInfo controller) {
if (!TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controller.getPackageName())) {
return MediaSession.ConnectionResult.reject();
}
return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands);
}
};
if (this.session != null) {
this.session.release();
}
this.session =
new MediaSession.Builder(context, player)
.setId(SESSION_ID)
.setSessionCallback(callback)
.build();
}
private SessionCommands createSessionCommandsWith(SessionCommand command) {
return new SessionCommands.Builder().add(command).build();
}
private void testOnCommandRequest(int commandCode, PermissionTestTask runnable) throws Exception {
createSessionWithAvailableCommands(
SessionCommands.EMPTY, createPlayerCommandsWith(commandCode));
runnable.run(controllerTestRule.createRemoteController(session.getToken()));
assertThat(callback.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(callback.onCommandRequestCalled).isTrue();
assertThat(callback.command).isEqualTo(commandCode);
createSessionWithAvailableCommands(
SessionCommands.EMPTY, createPlayerCommandsWithout(commandCode));
runnable.run(controllerTestRule.createRemoteController(session.getToken()));
assertThat(callback.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse();
assertThat(callback.onCommandRequestCalled).isFalse();
}
@Test
public void play() throws Exception {
testOnCommandRequest(COMMAND_PLAY_PAUSE, RemoteMediaController::play);
......@@ -409,4 +360,52 @@ public class MediaSessionPermissionTest {
return Futures.immediateFuture(new SessionResult(RESULT_SUCCESS));
}
}
private void createSessionWithAvailableCommands(
SessionCommands sessionCommands, Player.Commands playerCommands) {
player =
new MockPlayer.Builder()
.setApplicationLooper(threadTestRule.getHandler().getLooper())
.build();
callback =
new MySessionCallback() {
@Override
public MediaSession.ConnectionResult onConnect(
MediaSession session, ControllerInfo controller) {
if (!TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controller.getPackageName())) {
return MediaSession.ConnectionResult.reject();
}
return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands);
}
};
if (this.session != null) {
this.session.release();
}
this.session =
new MediaSession.Builder(context, player)
.setId(SESSION_ID)
.setSessionCallback(callback)
.build();
}
private SessionCommands createSessionCommandsWith(SessionCommand command) {
return new SessionCommands.Builder().add(command).build();
}
private void testOnCommandRequest(int commandCode, PermissionTestTask runnable) throws Exception {
createSessionWithAvailableCommands(
SessionCommands.EMPTY, createPlayerCommandsWith(commandCode));
runnable.run(controllerTestRule.createRemoteController(session.getToken()));
assertThat(callback.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(callback.onCommandRequestCalled).isTrue();
assertThat(callback.command).isEqualTo(commandCode);
createSessionWithAvailableCommands(
SessionCommands.EMPTY, createPlayerCommandsWithout(commandCode));
runnable.run(controllerTestRule.createRemoteController(session.getToken()));
assertThat(callback.countDownLatch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse();
assertThat(callback.onCommandRequestCalled).isFalse();
}
}
......@@ -47,6 +47,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
......@@ -78,8 +79,7 @@ public class MediaSessionTest {
public void setUp() throws Exception {
context = ApplicationProvider.getApplicationContext();
handler = threadTestRule.getHandler();
player =
new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build();
player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build();
session =
sessionTestRule.ensureReleaseAfterTest(
......@@ -107,6 +107,16 @@ public class MediaSessionTest {
.get(TIMEOUT_MS, MILLISECONDS);
}
@After
public void tearDown() throws Exception {
if ((controller != null)) {
threadTestRule.getHandler().postAndSync(() -> controller.release());
}
if (session != null) {
session.release();
}
}
@Test
public void builder() {
MediaSession.Builder builder;
......@@ -394,8 +404,7 @@ public class MediaSessionTest {
long testSeekPositionMs = 1234;
controllerCompat.getTransportControls().seekTo(testSeekPositionMs);
assertThat(player.countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(player.seekToCalled).isTrue();
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO, TIMEOUT_MS);
assertThat(player.seekPositionMs).isEqualTo(testSeekPositionMs);
}
......
......@@ -134,8 +134,7 @@ public class MockMediaLibraryService extends MediaLibraryService {
return (MediaLibrarySession) onGetSessionHandler.onGetSession(controllerInfo);
}
MockPlayer player =
new MockPlayer.Builder().setLatchCount(1).setApplicationLooper(handler.getLooper()).build();
MockPlayer player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build();
MediaLibrarySessionCallback callback = registry.getSessionCallback();
session =
......
......@@ -156,22 +156,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* <li>Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)}
* <li>Default: {@code false}
* </ul>
* <li><b>{@code player_layout_id}</b> - Specifies the id of the layout to be inflated. See below
* for more details.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code R.layout.exo_player_view}
* </ul>
* <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout resource to be
* inflated by the child {@link PlayerControlView}. See below for more details.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code R.layout.exo_player_control_view}
* </ul>
* <li>All attributes that can be set on {@link PlayerControlView} and {@link DefaultTimeBar} can
* also be set on a PlayerView, and will be propagated to the inflated {@link
* PlayerControlView} unless the layout is overridden to specify a custom {@code
* exo_controller} (see below).
* exo_controller}.
* </ul>
*
* <h2>Overriding drawables</h2>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment