Compare commits

...

5 commits

Author SHA1 Message Date
ivanbuper
6604afcb3a Add Kotlin dependencies to missing_aar_type_workaround.gradle
#cherrypick

PiperOrigin-RevId: 671736852
(cherry picked from commit a1357befff)
2024-09-06 15:07:08 +01:00
aquilescanta
afc72880e9 Populate DeviceInfo in CastPlayer using MediaRouter2 info
This enables linking the media session to a routing session.

Issue: androidx/media#1056
PiperOrigin-RevId: 671425490
(cherry picked from commit 4ea58a133e)
2024-09-05 19:04:22 +01:00
aquilescanta
4e8f17a7cb Do not clear the timeline after the Cast receiver disconnects
The goal is to enable the app to fetch the timeline after a
disconnection in order to prepare and resume local playback.

PiperOrigin-RevId: 671022044
(cherry picked from commit a00c446529)
2024-09-05 19:02:28 +01:00
ivanbuper
4e305729a0 Bump Media3 version to 1.5.0-alpha01
PiperOrigin-RevId: 670535221
(cherry picked from commit 9562c976a9)
2024-09-03 14:55:00 +01:00
ivanbuper
9c13301b0a Update release notes for Media3 1.5.0-alpha01 release
PiperOrigin-RevId: 670523759
(cherry picked from commit e16b4fff8d)
2024-09-03 14:31:59 +01:00
6 changed files with 167 additions and 31 deletions

View file

@ -1,6 +1,11 @@
# Release notes
### Unreleased changes
## 1.5
### 1.5.0-alpha01 (2024-09-06)
This release includes the following changes since the
[1.4.1 release](#141-2024-08-23):
* Common Library:
* Add `ForwardingSimpleBasePlayer` that allows forwarding to another
@ -46,7 +51,6 @@
Transformer via a `Surface`.
* `ImageAssetLoader` reports unsupported input via `AssetLoader.onError`
instead of throwing an `IllegalStateException`.
* Track Selection:
* Extractors:
* Allow `Mp4Extractor` and `FragmentedMp4Extractor` to identify H264
samples that are not used as reference by subsequent samples.
@ -79,7 +83,6 @@
* Add a custom `VoiceSpan` and populate it for
[WebVTT voice spans](https://www.w3.org/TR/webvtt1/#webvtt-cue-voice-span)
([#1632](https://github.com/androidx/media/issues/1632)).
* Metadata:
* Image:
* Add `ExternallyLoadedImageDecoder` for simplified integration with
external image loading libraries like Glide or Coil.
@ -87,7 +90,6 @@
* Add `FileDescriptorDataSource`, a new `DataSource` that can be used to
read from a `FileDescriptor`
([#3757](https://github.com/google/ExoPlayer/issues/3757)).
* DRM:
* Effect:
* Add `DefaultVideoFrameProcessor` workaround for minor `SurfaceTexture`
scaling. `SurfaceTexture` may include a small scaling that cuts off a
@ -95,7 +97,6 @@
such that output is closer to expected.
* Speed up `DefaultVideoFrameProcessor.queueInputBitmap()`. As a result,
exporting images to videos with `Transformer` is faster.
* Muxers:
* IMA extension:
* Fix bug where clearing the playlist may cause an
`ArrayIndexOutOfBoundsException` in
@ -107,17 +108,9 @@
playback can't be suppressed without the system crashing the service
with a `ForegroundServiceDidNotStartInTimeException`
([#1528](https://github.com/google/ExoPlayer/issues/1528)).
* UI:
* Downloads:
* OkHttp Extension:
* Cronet Extension:
* RTMP Extension:
* HLS Extension:
* DASH Extension:
* Add support for periods starting in the middle of a segment
([#1440](https://github.com/androidx/media/issues/1440)).
* Smooth Streaming Extension:
* RTSP Extension:
* Decoder Extensions (FFmpeg, VP9, AV1, etc.):
* Add the IAMF decoder module, which provides support for playback of MP4
files containing IAMF tracks using the libiamf native library to
@ -125,14 +118,17 @@
* Playback is enabled with a stereo layout as well as 5.1 with
spatialization together with optional head tracking enabled, but
binaural playback support is currently not available.
* MIDI extension:
* Leanback extension:
* Cast Extension:
* Stop clearning the timeline after the CastSession disconnects, which
enables the sender app to resume playback locally after a disconnection.
* Populate CastPlayer's `DeviceInfo` when a `Context` is provided. This
enables linking the `MediaSession` to a `RoutingSession`, which is
necessary for integrating Output Switcher
([#1056](https://github.com/androidx/media/issues/1056)).
* Test Utilities:
* `DataSourceContractTest` now includes tests to verify:
* Input stream `read position` is updated.
* Output buffer `offset` is applied correctly.
* Demo app:
* Remove deprecated symbols:
* Remove deprecated `Player.hasPrevious`, `Player.hasPreviousWindow()`.
Use `Player.hasPreviousMediaItem()` instead.

View file

@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
project.ext {
releaseVersion = '1.4.1'
releaseVersionCode = 1_004_001_3_00
releaseVersion = '1.5.0-alpha01'
releaseVersionCode = 1_005_000_0_01
minSdkVersion = 21
// See https://developer.android.com/training/cars/media/automotive-os#automotive-module
automotiveMinSdkVersion = 28

View file

@ -17,16 +17,26 @@ package androidx.media3.cast;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Util.SDK_INT;
import static androidx.media3.common.util.Util.castNonNull;
import static java.lang.Math.min;
import android.content.Context;
import android.media.MediaRouter2;
import android.media.MediaRouter2.RouteCallback;
import android.media.MediaRouter2.RoutingController;
import android.media.MediaRouter2.TransferCallback;
import android.media.RouteDiscoveryPreference;
import android.os.Handler;
import android.os.Looper;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.BasePlayer;
@ -83,8 +93,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@UnstableApi
public final class CastPlayer extends BasePlayer {
/** The {@link DeviceInfo} returned by {@link #getDeviceInfo() this player}. */
public static final DeviceInfo DEVICE_INFO =
/**
* A {@link DeviceInfo#PLAYBACK_TYPE_REMOTE remote} {@link DeviceInfo} with a null {@link
* DeviceInfo#routingControllerId}.
*/
public static final DeviceInfo DEVICE_INFO_REMOTE_EMPTY =
new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).build();
static {
@ -128,6 +141,7 @@ public final class CastPlayer extends BasePlayer {
// TODO: Allow custom implementations of CastTimelineTracker.
private final CastTimelineTracker timelineTracker;
private final Timeline.Period period;
@Nullable private final Api30Impl api30Impl;
// Result callbacks.
private final StatusListener statusListener;
@ -153,6 +167,7 @@ public final class CastPlayer extends BasePlayer {
private long pendingSeekPositionMs;
@Nullable private PositionInfo pendingMediaItemRemovalPosition;
private MediaMetadata mediaMetadata;
private DeviceInfo deviceInfo;
/**
* Creates a new cast player.
@ -202,6 +217,7 @@ public final class CastPlayer extends BasePlayer {
@IntRange(from = 1) long seekBackIncrementMs,
@IntRange(from = 1) long seekForwardIncrementMs) {
this(
/* context= */ null,
castContext,
mediaItemConverter,
seekBackIncrementMs,
@ -212,6 +228,8 @@ public final class CastPlayer extends BasePlayer {
/**
* Creates a new cast player.
*
* @param context A {@link Context} used to populate {@link #getDeviceInfo()}. If null, {@link
* #getDeviceInfo()} will always return {@link #DEVICE_INFO_REMOTE_EMPTY}.
* @param castContext The context from which the cast session is obtained.
* @param mediaItemConverter The {@link MediaItemConverter} to use.
* @param seekBackIncrementMs The {@link #seekBack()} increment, in milliseconds.
@ -223,6 +241,7 @@ public final class CastPlayer extends BasePlayer {
* negative.
*/
public CastPlayer(
@Nullable Context context,
CastContext castContext,
MediaItemConverter mediaItemConverter,
@IntRange(from = 1) long seekBackIncrementMs,
@ -260,6 +279,14 @@ public final class CastPlayer extends BasePlayer {
CastSession session = sessionManager.getCurrentCastSession();
setRemoteMediaClient(session != null ? session.getRemoteMediaClient() : null);
updateInternalStateAndNotifyIfChanged();
if (SDK_INT >= 30 && context != null) {
api30Impl = new Api30Impl(context);
api30Impl.initialize();
deviceInfo = api30Impl.fetchDeviceInfo();
} else {
api30Impl = null;
deviceInfo = DEVICE_INFO_REMOTE_EMPTY;
}
}
/**
@ -530,6 +557,10 @@ public final class CastPlayer extends BasePlayer {
@Override
public void release() {
// The SDK_INT check is not necessary, but it prevents a lint error for the release call.
if (SDK_INT >= 30 && api30Impl != null) {
api30Impl.release();
}
SessionManager sessionManager = castContext.getSessionManager();
sessionManager.removeSessionManagerListener(statusListener, CastSession.class);
sessionManager.endCurrentSession(false);
@ -782,10 +813,14 @@ public final class CastPlayer extends BasePlayer {
return CueGroup.EMPTY_TIME_ZERO;
}
/** This method always returns {@link CastPlayer#DEVICE_INFO}. */
/**
* Returns a {@link DeviceInfo} describing the receiver device. Returns {@link
* #DEVICE_INFO_REMOTE_EMPTY} if no {@link Context} was provided at construction, or if the Cast
* {@link RoutingController} could not be identified.
*/
@Override
public DeviceInfo getDeviceInfo() {
return DEVICE_INFO;
return deviceInfo;
}
/** This method is not supported and always returns {@code 0}. */
@ -1283,11 +1318,8 @@ public final class CastPlayer extends BasePlayer {
remoteMediaClient.registerCallback(statusListener);
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
updateInternalStateAndNotifyIfChanged();
} else {
updateTimelineAndNotifyIfChanged();
if (sessionAvailabilityListener != null) {
sessionAvailabilityListener.onCastSessionUnavailable();
}
} else if (sessionAvailabilityListener != null) {
sessionAvailabilityListener.onCastSessionUnavailable();
}
}
@ -1537,4 +1569,109 @@ public final class CastPlayer extends BasePlayer {
return pendingResultCallback == resultCallback;
}
}
@RequiresApi(30)
private final class Api30Impl {
private final MediaRouter2 mediaRouter2;
private final TransferCallback transferCallback;
private final RouteCallback emptyRouteCallback;
private final Handler handler;
public Api30Impl(Context context) {
mediaRouter2 = MediaRouter2.getInstance(context);
transferCallback = new MediaRouter2TransferCallbackImpl();
emptyRouteCallback = new MediaRouter2RouteCallbackImpl();
handler = new Handler(Looper.getMainLooper());
}
/** Acquires necessary resources and registers callbacks. */
@DoNotInline
public void initialize() {
mediaRouter2.registerTransferCallback(handler::post, transferCallback);
// We need at least one route callback registered in order to get transfer callback updates.
mediaRouter2.registerRouteCallback(
handler::post,
emptyRouteCallback,
new RouteDiscoveryPreference.Builder(ImmutableList.of(), /* activeScan= */ false)
.build());
}
/**
* Releases any resources acquired in {@link #initialize()} and unregisters any registered
* callbacks.
*/
@DoNotInline
public void release() {
mediaRouter2.unregisterTransferCallback(transferCallback);
mediaRouter2.unregisterRouteCallback(emptyRouteCallback);
handler.removeCallbacksAndMessages(/* token= */ null);
}
/** Updates the device info with an up-to-date value and notifies the listeners. */
@DoNotInline
private void updateDeviceInfo() {
DeviceInfo oldDeviceInfo = deviceInfo;
DeviceInfo newDeviceInfo = fetchDeviceInfo();
deviceInfo = newDeviceInfo;
if (!deviceInfo.equals(oldDeviceInfo)) {
listeners.sendEvent(
EVENT_DEVICE_INFO_CHANGED, listener -> listener.onDeviceInfoChanged(newDeviceInfo));
}
}
/**
* Returns a {@link DeviceInfo} with the {@link RoutingController#getId() id} that corresponds
* to the Cast session, or {@link #DEVICE_INFO_REMOTE_EMPTY} if not available.
*/
@DoNotInline
public DeviceInfo fetchDeviceInfo() {
// TODO: b/364833997 - Fetch this information from the AndroidX MediaRouter selected route
// once the selected route id matches the controller id.
List<RoutingController> controllers = mediaRouter2.getControllers();
// The controller at position zero is always the system controller (local playback). All other
// controllers are for remote playback, and could be the Cast one.
if (controllers.size() != 2) {
// There's either no remote routing controller, or there's more than one. In either case we
// don't populate the device info because either there's no Cast routing controller, or we
// cannot safely identify the Cast routing controller.
return DEVICE_INFO_REMOTE_EMPTY;
} else {
// There's only one remote routing controller. It's safe to assume it's the Cast routing
// controller.
RoutingController remoteController = controllers.get(1);
// TODO b/364580007 - Populate volume information, and implement Player volume-related
// methods.
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
.setRoutingControllerId(remoteController.getId())
.build();
}
}
/**
* Empty {@link RouteCallback} implementation necessary for registering the {@link MediaRouter2}
* instance with the system_server.
*
* <p>This callback must be registered so that the media router service notifies the {@link
* MediaRouter2TransferCallbackImpl} of transfer events.
*/
private final class MediaRouter2RouteCallbackImpl extends RouteCallback {}
/**
* {@link TransferCallback} implementation to listen for {@link RoutingController} creation and
* releases.
*/
private final class MediaRouter2TransferCallbackImpl extends TransferCallback {
@Override
public void onTransfer(RoutingController oldController, RoutingController newController) {
updateDeviceInfo();
}
@Override
public void onStop(RoutingController controller) {
updateDeviceInfo();
}
}
}
}

View file

@ -1902,7 +1902,7 @@ public class CastPlayerTest {
public void getDeviceInfo_returnsCorrectDeviceInfoWithPlaybackTypeRemote() {
DeviceInfo deviceInfo = castPlayer.getDeviceInfo();
assertThat(deviceInfo).isEqualTo(CastPlayer.DEVICE_INFO);
assertThat(deviceInfo).isEqualTo(CastPlayer.DEVICE_INFO_REMOTE_EMPTY);
assertThat(deviceInfo.playbackType).isEqualTo(DeviceInfo.PLAYBACK_TYPE_REMOTE);
}

View file

@ -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.0-beta01". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "1.4.1";
public static final String VERSION = "1.5.0-alpha01";
/** 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.4.1";
public static final String VERSION_SLASHY = "AndroidXMedia3/1.5.0-alpha01";
/**
* 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_004_001_3_00;
public static final int VERSION_INT = 1_005_000_0_01;
/** Whether the library was compiled with {@link Assertions} checks enabled. */
public static final boolean ASSERTIONS_ENABLED = true;

View file

@ -30,6 +30,9 @@ def addMissingAarTypeToXml(xml) {
"org.mockito:mockito-core",
"org.robolectric:robolectric",
"com.github.philburk:jsyn",
"org.jetbrains.kotlin:kotlin-stdlib-jdk8",
"org.jetbrains.kotlinx:kotlinx-coroutines-core",
"org.jetbrains.kotlinx:kotlinx-coroutines-android",
]
// Dependencies that have AAR files.
def aar_dependencies = [