Skip to content

Commit

Permalink
Allow playback of MediaItems with LocalConfiguration
Browse files Browse the repository at this point in the history
When initiated by MediaController, it should be possible for `MediaSession` to pass `MediaItems` to the `Player` if they have `LocalConfiguration`. In such case, it is not required to override `MediaSession.Callback.onAddMediaItems`, because the new current default implementation will handle it.

However, in other cases, MediaItem.toBundle() will continue to strip the LocalConfiguration information.

Issue: #282

RELEASENOTES.md modified in cherrypick

PiperOrigin-RevId: 537993460
(cherry picked from commit d9764c1)
  • Loading branch information
oceanjules authored and tof-tof committed Jun 12, 2023
1 parent 4a90efa commit a5949d8
Show file tree
Hide file tree
Showing 8 changed files with 511 additions and 42 deletions.
79 changes: 73 additions & 6 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,78 @@
# Release notes

### Unreleased changes

* Common Library:
* Add a `@Nullable Throwable` parameter to the methods in the `Log.Logger`
interface. The `message` parameter to these methods no longer contains
any information about the `Throwable` passed to the `Log.{d,i,w,e}()`
methods, so implementations will need to manually append this
information if desired (possibly using
`Logger.appendThrowableString(String, Throwable)`).
* ExoPlayer:
* Transformer:
* Parse EXIF rotation data for image inputs.
* Track Selection:
* Extractors:
* Audio:
* Audio Offload:
* Add `AudioSink.getFormatOffloadSupport(Format)` that retrieves level of
offload support the sink can provide for the format through a
`DefaultAudioOffloadSupportProvider`. It returns the new
`AudioOffloadSupport` that contains `isFormatSupported`,
`isGaplessSupported`, and `isSpeedChangeSupported`.
* Add `AudioSink.setOffloadMode()` through which the offload configuration
on the audio sink is configured. Default is
`AudioSink.OFFLOAD_MODE_DISABLED`.
* Offload can be enabled through `setAudioOffloadPreference` in
`TrackSelectionParameters`. If the set preference is to enable, the
device supports offload for the format, and the track selection is a
single audio track, then audio offload will be enabled.
* If `audioOffloadModePreference` is set to
`AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED`, then the
`DefaultTrackSelector` will only select an audio track and only if that
track's format is supported in offload. If no audio track is supported
in offload, then no track will be selected.
* Remove parameter `enableOffload` from
`DefaultRenderersFactory.buildAudioSink` method signature.
* Remove method `DefaultAudioSink.Builder.setOffloadMode`.
* Remove intdef value
`DefaultAudioSink.OffloadMode.OFFLOAD_MODE_ENABLED_GAPLESS_DISABLED`.
* Video:
* Allow `MediaCodecVideoRenderer` to use a custom
`VideoFrameProcessor.Factory`.
* Text:
* Metadata:
* DRM:
* Effect:
* Muxers:
* IMA extension:
* Session:
* Add default implementation to `MediaSession.Callback.onAddMediaItems` to
allow requested `MediaItems` to be passed onto `Player` if they have
`LocalConfiguration` (e.g. URI)
([#282](https://github.com/androidx/media/issues/282)).
* UI:
* Downloads:
* OkHttp Extension:
* Cronet Extension:
* RTMP Extension:
* DASH Extension:
* HLS Extension:
* Smooth Streaming Extension:
* RTSP Extension:
* Decoder Extensions (FFmpeg, VP9, AV1, etc.):
* MIDI extension:
* Release the MIDI decoder module, which provides support for playback of
standard MIDI files using the Jsyn library to synthesize audio.
* Cast Extension:
* Test Utilities:
* Remove deprecated symbols:
* Remove deprecated `MediaItem.PlaybackProperties`, use
`MediaItem.LocalConfiguration` instead. Deprecated field
`MediaItem.playbackProperties` is now of type
`MediaItem.LocalConfiguration`.

## 1.1

### 1.1.0-beta01 (2023-06-07)
Expand Down Expand Up @@ -105,12 +178,6 @@ This release includes the following changes since
* Remove deprecated
`DefaultLoadControl.Builder.createDefaultLoadControl()`, use `build()`
instead.
* Remove deprecated `MediaItem.PlaybackProperties`, use
`MediaItem.LocalConfiguration` instead. Deprecated field
`MediaItem.playbackProperties` is now of type
`MediaItem.LocalConfiguration`.

## 1.1

### 1.1.0-alpha01 (2023-05-10)

Expand Down
121 changes: 111 additions & 10 deletions libraries/common/src/main/java/androidx/media3/common/MediaItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,7 @@ public Bundle toBundle() {
}

/** Properties for local playback. */
public static final class LocalConfiguration {
public static final class LocalConfiguration implements Bundleable {

/** The {@link Uri}. */
public final Uri uri;
Expand Down Expand Up @@ -1183,6 +1183,82 @@ public int hashCode() {
result = 31 * result + (tag == null ? 0 : tag.hashCode());
return result;
}

// Bundleable implementation.

private static final String FIELD_URI = Util.intToStringMaxRadix(0);
private static final String FIELD_MIME_TYPE = Util.intToStringMaxRadix(1);
private static final String FIELD_DRM_CONFIGURATION = Util.intToStringMaxRadix(2);
private static final String FIELD_ADS_CONFIGURATION = Util.intToStringMaxRadix(3);
private static final String FIELD_STREAM_KEYS = Util.intToStringMaxRadix(4);
private static final String FIELD_CUSTOM_CACHE_KEY = Util.intToStringMaxRadix(5);
private static final String FIELD_SUBTITLE_CONFIGURATION = Util.intToStringMaxRadix(6);

/**
* {@inheritDoc}
*
* <p>It omits the {@link #tag} field. The {@link #tag} of an instance restored from such a
* bundle by {@link #CREATOR} will be {@code null}.
*/
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelable(FIELD_URI, uri);
if (mimeType != null) {
bundle.putString(FIELD_MIME_TYPE, mimeType);
}
if (drmConfiguration != null) {
bundle.putBundle(FIELD_DRM_CONFIGURATION, drmConfiguration.toBundle());
}
if (adsConfiguration != null) {
bundle.putBundle(FIELD_ADS_CONFIGURATION, adsConfiguration.toBundle());
}
if (!streamKeys.isEmpty()) {
bundle.putParcelableArrayList(FIELD_STREAM_KEYS, new ArrayList<>(streamKeys));
}
if (customCacheKey != null) {
bundle.putString(FIELD_CUSTOM_CACHE_KEY, customCacheKey);
}
if (!subtitleConfigurations.isEmpty()) {
bundle.putParcelableArrayList(
FIELD_SUBTITLE_CONFIGURATION, BundleableUtil.toBundleArrayList(subtitleConfigurations));
}
return bundle;
}

/** Object that can restore {@link LocalConfiguration} from a {@link Bundle}. */
@UnstableApi
public static final Creator<LocalConfiguration> CREATOR = LocalConfiguration::fromBundle;

@UnstableApi
private static LocalConfiguration fromBundle(Bundle bundle) {
@Nullable Bundle drmBundle = bundle.getBundle(FIELD_DRM_CONFIGURATION);
DrmConfiguration drmConfiguration =
drmBundle == null ? null : DrmConfiguration.CREATOR.fromBundle(drmBundle);
@Nullable Bundle adsBundle = bundle.getBundle(FIELD_ADS_CONFIGURATION);
AdsConfiguration adsConfiguration =
adsBundle == null ? null : AdsConfiguration.CREATOR.fromBundle(adsBundle);
@Nullable List<StreamKey> streamKeysList = bundle.getParcelableArrayList(FIELD_STREAM_KEYS);
List<StreamKey> streamKeys =
streamKeysList == null ? ImmutableList.of() : ImmutableList.copyOf(streamKeysList);
@Nullable
List<Bundle> subtitleBundles = bundle.getParcelableArrayList(FIELD_SUBTITLE_CONFIGURATION);
ImmutableList<SubtitleConfiguration> subtitleConfiguration =
subtitleBundles == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(SubtitleConfiguration.CREATOR, subtitleBundles);

return new LocalConfiguration(
checkNotNull(bundle.getParcelable(FIELD_URI)),
bundle.getString(FIELD_MIME_TYPE),
drmConfiguration,
adsConfiguration,
streamKeys,
bundle.getString(FIELD_CUSTOM_CACHE_KEY),
subtitleConfiguration,
/* tag= */ null);
}
}

/** Live playback configuration. */
Expand Down Expand Up @@ -2167,16 +2243,10 @@ public int hashCode() {
private static final String FIELD_MEDIA_METADATA = Util.intToStringMaxRadix(2);
private static final String FIELD_CLIPPING_PROPERTIES = Util.intToStringMaxRadix(3);
private static final String FIELD_REQUEST_METADATA = Util.intToStringMaxRadix(4);
private static final String FIELD_LOCAL_CONFIGURATION = Util.intToStringMaxRadix(5);

/**
* {@inheritDoc}
*
* <p>It omits the {@link #localConfiguration} field. The {@link #localConfiguration} of an
* instance restored by {@link #CREATOR} will always be {@code null}.
*/
@UnstableApi
@Override
public Bundle toBundle() {
private Bundle toBundle(boolean includeLocalConfiguration) {
Bundle bundle = new Bundle();
if (!mediaId.equals(DEFAULT_MEDIA_ID)) {
bundle.putString(FIELD_MEDIA_ID, mediaId);
Expand All @@ -2193,9 +2263,33 @@ public Bundle toBundle() {
if (!requestMetadata.equals(RequestMetadata.EMPTY)) {
bundle.putBundle(FIELD_REQUEST_METADATA, requestMetadata.toBundle());
}
if (includeLocalConfiguration && localConfiguration != null) {
bundle.putBundle(FIELD_LOCAL_CONFIGURATION, localConfiguration.toBundle());
}
return bundle;
}

/**
* {@inheritDoc}
*
* <p>It omits the {@link #localConfiguration} field. The {@link #localConfiguration} of an
* instance restored from such a bundle by {@link #CREATOR} will be {@code null}.
*/
@UnstableApi
@Override
public Bundle toBundle() {
return toBundle(/* includeLocalConfiguration= */ false);
}

/**
* Returns a {@link Bundle} representing the information stored in this {@link #MediaItem} object,
* while including the {@link #localConfiguration} field if it is not null (otherwise skips it).
*/
@UnstableApi
public Bundle toBundleIncludeLocalConfiguration() {
return toBundle(/* includeLocalConfiguration= */ true);
}

/**
* An object that can restore {@link MediaItem} from a {@link Bundle}.
*
Expand Down Expand Up @@ -2234,10 +2328,17 @@ private static MediaItem fromBundle(Bundle bundle) {
} else {
requestMetadata = RequestMetadata.CREATOR.fromBundle(requestMetadataBundle);
}
@Nullable Bundle localConfigurationBundle = bundle.getBundle(FIELD_LOCAL_CONFIGURATION);
LocalConfiguration localConfiguration;
if (localConfigurationBundle == null) {
localConfiguration = null;
} else {
localConfiguration = LocalConfiguration.CREATOR.fromBundle(localConfigurationBundle);
}
return new MediaItem(
mediaId,
clippingConfiguration,
/* localConfiguration= */ null,
localConfiguration,
liveConfiguration,
mediaMetadata,
requestMetadata);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
Expand All @@ -36,10 +37,21 @@ public final class BundleableUtil {

/** Converts a list of {@link Bundleable} to a list {@link Bundle}. */
public static <T extends Bundleable> ImmutableList<Bundle> toBundleList(List<T> bundleableList) {
return toBundleList(bundleableList, Bundleable::toBundle);
}

/**
* Converts a list of {@link Bundleable} to a list {@link Bundle}
*
* @param bundleableList list of Bundleable items to be converted
* @param customToBundleFunc function that specifies how to bundle up each {@link Bundleable}
*/
public static <T extends Bundleable> ImmutableList<Bundle> toBundleList(
List<T> bundleableList, Function<T, Bundle> customToBundleFunc) {
ImmutableList.Builder<Bundle> builder = ImmutableList.builder();
for (int i = 0; i < bundleableList.size(); i++) {
Bundleable bundleable = bundleableList.get(i);
builder.add(bundleable.toBundle());
T bundleable = bundleableList.get(i);
builder.add(customToBundleFunc.apply(bundleable));
}
return builder.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,68 @@ public void createLiveConfigurationInstance_roundTripViaBundle_yieldsEqualInstan
assertThat(liveConfigurationFromBundle).isEqualTo(liveConfiguration);
}

@Test
public void
createDefaultLocalConfigurationInstance_toBundleSkipsDefaultValues_fromBundleRestoresThem() {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();

Bundle localConfigurationBundle = mediaItem.localConfiguration.toBundle();

// Check that default values are skipped when bundling, only Uri field (="0") is present
assertThat(localConfigurationBundle.keySet()).containsExactly("0");

MediaItem.LocalConfiguration restoredLocalConfiguration =
MediaItem.LocalConfiguration.CREATOR.fromBundle(localConfigurationBundle);

assertThat(restoredLocalConfiguration).isEqualTo(mediaItem.localConfiguration);
assertThat(restoredLocalConfiguration.streamKeys).isEmpty();
assertThat(restoredLocalConfiguration.subtitleConfigurations).isEmpty();
}

@Test
public void createLocalConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() {
Map<String, String> requestHeaders = new HashMap<>();
requestHeaders.put("Referer", "http://www.google.com");
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(URI_STRING)
.setMimeType(MimeTypes.APPLICATION_MP4)
.setCustomCacheKey("key")
.setSubtitleConfigurations(
ImmutableList.of(
new MediaItem.SubtitleConfiguration.Builder(Uri.parse(URI_STRING + "/en"))
.setMimeType(MimeTypes.APPLICATION_TTML)
.setLanguage("en")
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setRoleFlags(C.ROLE_FLAG_ALTERNATE)
.setLabel("label")
.setId("id")
.build()))
.setDrmConfiguration(
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
.setLicenseUri(Uri.parse(URI_STRING))
.setLicenseRequestHeaders(requestHeaders)
.setMultiSession(true)
.setForceDefaultLicenseUri(true)
.setPlayClearContentWithoutKey(true)
.setForcedSessionTrackTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO))
.setKeySetId(new byte[] {1, 2, 3})
.build())
.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(Uri.parse(URI_STRING)).build())
.build();

MediaItem.LocalConfiguration localConfiguration = mediaItem.localConfiguration;
MediaItem.LocalConfiguration localConfigurationFromBundle =
MediaItem.LocalConfiguration.CREATOR.fromBundle(localConfiguration.toBundle());
MediaItem.LocalConfiguration localConfigurationFromMediaItemBundle =
MediaItem.CREATOR.fromBundle(mediaItem.toBundleIncludeLocalConfiguration())
.localConfiguration;

assertThat(localConfigurationFromBundle).isEqualTo(localConfiguration);
assertThat(localConfigurationFromMediaItemBundle).isEqualTo(localConfiguration);
}

@Test
public void builderSetLiveConfiguration() {
MediaItem mediaItem =
Expand Down Expand Up @@ -892,13 +954,25 @@ public void roundTripViaBundle_withoutLocalConfiguration_yieldsEqualInstance() {
}

@Test
public void roundTripViaBundle_withLocalConfiguration_dropsLocalConfiguration() {
public void
roundTripViaDefaultBundle_mediaItemContainsLocalConfiguration_dropsLocalConfiguration() {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();

assertThat(mediaItem.localConfiguration).isNotNull();
assertThat(MediaItem.CREATOR.fromBundle(mediaItem.toBundle()).localConfiguration).isNull();
}

@Test
public void
roundTripViaBundleIncludeLocalConfiguration_mediaItemContainsLocalConfiguration_restoresLocalConfiguration() {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
MediaItem restoredMediaItem =
MediaItem.CREATOR.fromBundle(mediaItem.toBundleIncludeLocalConfiguration());

assertThat(mediaItem.localConfiguration).isNotNull();
assertThat(restoredMediaItem.localConfiguration).isEqualTo(mediaItem.localConfiguration);
}

@Test
public void createDefaultMediaItemInstance_checksDefaultValues() {
MediaItem mediaItem = new MediaItem.Builder().build();
Expand Down
Loading

0 comments on commit a5949d8

Please sign in to comment.