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 c31de70e68..d4dff3266b 100644 --- a/libraries/common/src/main/java/androidx/media3/common/FileTypes.java +++ b/libraries/common/src/main/java/androidx/media3/common/FileTypes.java @@ -19,6 +19,7 @@ import static androidx.media3.common.MimeTypes.normalizeMimeType; import static java.lang.annotation.ElementType.TYPE_USE; import android.net.Uri; +import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -29,6 +30,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; /** Defines common file type constants and helper methods. */ @UnstableApi @@ -327,4 +329,30 @@ public final class FileTypes { return FileTypes.UNKNOWN; } } + + /** + * Returns the file extension of the given {@link Uri} or an empty string if there is no + * extension. + * + *
This method is a convenience method for obtaining the extension of a url and has undefined + * results for other Strings. + */ + public static String getFileExtensionFromUri(Uri uri) { + String path = uri.getPath(); + if (TextUtils.isEmpty(path)) { + return ""; + } + int filenamePos = path.lastIndexOf('/'); + String filename = 0 <= filenamePos ? path.substring(filenamePos + 1) : path; + + // If the filename contains special characters, we don't consider it valid for our matching + // purposes. + if (!filename.isEmpty() && Pattern.matches("[a-zA-Z_0-9\\.\\-\\(\\)\\%]+", filename)) { + int dotPos = filename.lastIndexOf('.'); + if (0 <= dotPos) { + return filename.substring(dotPos + 1); + } + } + return ""; + } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultAssetLoaderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultAssetLoaderFactory.java index 36242a95d9..bfb4b5afd9 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultAssetLoaderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultAssetLoaderFactory.java @@ -16,18 +16,28 @@ package androidx.media3.transformer; -import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; +import android.content.ContentResolver; import android.content.Context; +import android.graphics.BitmapFactory; +import android.graphics.ColorSpace; import android.os.Looper; import androidx.annotation.Nullable; +import androidx.media3.common.FileTypes; import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.Clock; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import androidx.media3.datasource.DataSourceBitmapLoader; +import androidx.media3.datasource.DefaultDataSource; import androidx.media3.exoplayer.source.MediaSource; import com.google.common.base.Ascii; -import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.Objects; +import java.util.concurrent.Executors; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** The default {@link AssetLoader.Factory} implementation. */ @@ -39,6 +49,7 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory { private final boolean forceInterpretHdrAsSdr; private final Clock clock; private final MediaSource.@MonotonicNonNull Factory mediaSourceFactory; + private final BitmapLoader bitmapLoader; private AssetLoader.@MonotonicNonNull Factory imageAssetLoaderFactory; private AssetLoader.@MonotonicNonNull Factory exoPlayerAssetLoaderFactory; @@ -46,6 +57,10 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory { /** * Creates an instance. * + *
Uses {@link DataSourceBitmapLoader} to load images, setting the {@link + * android.graphics.BitmapFactory.Options#inPreferredColorSpace} to {@link + * android.graphics.ColorSpace.Named#SRGB} when possible. + * * @param context The {@link Context}. * @param decoderFactory The {@link Codec.DecoderFactory} to use to decode the samples (if * necessary). @@ -64,6 +79,37 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory { this.forceInterpretHdrAsSdr = forceInterpretHdrAsSdr; this.clock = clock; this.mediaSourceFactory = null; + @Nullable BitmapFactory.Options options = null; + if (Util.SDK_INT >= 26) { + options = new BitmapFactory.Options(); + options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB); + } + this.bitmapLoader = + new DataSourceBitmapLoader( + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()), + new DefaultDataSource.Factory(context), + options); + } + + /** + * Creates an instance with the default {@link Clock} and {@link Codec.DecoderFactory}. + * + *
For multi-picture formats (e.g. gifs), a single image frame from the container is loaded.
+ * The frame loaded is determined by the {@link BitmapLoader} implementation.
+ *
+ * @param context The {@link Context}.
+ * @param forceInterpretHdrAsSdr Whether to apply {@link
+ * Composition#HDR_MODE_EXPERIMENTAL_FORCE_INTERPRET_HDR_AS_SDR}.
+ * @param bitmapLoader The {@link BitmapLoader} to use to load and decode images.
+ */
+ public DefaultAssetLoaderFactory(
+ Context context, boolean forceInterpretHdrAsSdr, BitmapLoader bitmapLoader) {
+ this.context = context.getApplicationContext();
+ this.decoderFactory = new DefaultDecoderFactory(context);
+ this.forceInterpretHdrAsSdr = forceInterpretHdrAsSdr;
+ this.clock = Clock.DEFAULT;
+ this.mediaSourceFactory = null;
+ this.bitmapLoader = bitmapLoader;
}
/**
@@ -78,18 +124,21 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory {
* testing.
* @param mediaSourceFactory The {@link MediaSource.Factory} to use to retrieve the samples to
* transform when an {@link ExoPlayerAssetLoader} is used.
+ * @param bitmapLoader The {@link BitmapLoader} to use to load and decode images.
*/
public DefaultAssetLoaderFactory(
Context context,
Codec.DecoderFactory decoderFactory,
boolean forceInterpretHdrAsSdr,
Clock clock,
- MediaSource.Factory mediaSourceFactory) {
+ MediaSource.Factory mediaSourceFactory,
+ BitmapLoader bitmapLoader) {
this.context = context.getApplicationContext();
this.decoderFactory = decoderFactory;
this.forceInterpretHdrAsSdr = forceInterpretHdrAsSdr;
this.clock = clock;
this.mediaSourceFactory = mediaSourceFactory;
+ this.bitmapLoader = bitmapLoader;
}
@Override
@@ -98,7 +147,7 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory {
MediaItem mediaItem = editedMediaItem.mediaItem;
if (isImage(mediaItem.localConfiguration)) {
if (imageAssetLoaderFactory == null) {
- imageAssetLoaderFactory = new ImageAssetLoader.Factory(context);
+ imageAssetLoaderFactory = new ImageAssetLoader.Factory(bitmapLoader);
}
return imageAssetLoaderFactory.createAssetLoader(editedMediaItem, looper, listener);
}
@@ -113,21 +162,71 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory {
return exoPlayerAssetLoaderFactory.createAssetLoader(editedMediaItem, looper, listener);
}
- private static boolean isImage(@Nullable MediaItem.LocalConfiguration localConfiguration) {
+ private boolean isImage(@Nullable MediaItem.LocalConfiguration localConfiguration) {
if (localConfiguration == null) {
return false;
}
- if (localConfiguration.mimeType != null) {
- return MimeTypes.isImage(localConfiguration.mimeType);
+ @Nullable String mimeType = localConfiguration.mimeType;
+ if (mimeType == null) {
+ if (Objects.equals(localConfiguration.uri.getScheme(), ContentResolver.SCHEME_CONTENT)) {
+ ContentResolver cr = context.getContentResolver();
+ mimeType = cr.getType(localConfiguration.uri);
+ } else {
+ String fileExtension = FileTypes.getFileExtensionFromUri(localConfiguration.uri);
+ mimeType = getCommonImageMimeTypeFromExtension(Ascii.toLowerCase(fileExtension));
+ }
}
- ImmutableList For image inputs, the values passed into {@link #setRemoveAudio}, {@link #setRemoveVideo}
- * and {@link #setFlattenForSlowMotion} will be ignored.
+ * and {@link #setFlattenForSlowMotion} will be ignored. For multi-picture formats (e.g. gifs),
+ * a single image frame from the container is displayed if the {@link DefaultAssetLoaderFactory}
+ * is used.
*
* @param mediaItem The {@link MediaItem} on which transformations are applied.
*/
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java
index b621123ad5..ce81d8371a 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java
@@ -27,10 +27,7 @@ import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import android.content.Context;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.ColorSpace;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
@@ -41,16 +38,11 @@ import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.BitmapLoader;
import androidx.media3.common.util.ConstantRateTimestampIterator;
import androidx.media3.common.util.UnstableApi;
-import androidx.media3.common.util.Util;
-import androidx.media3.datasource.DataSource;
-import androidx.media3.datasource.DataSourceBitmapLoader;
-import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.transformer.SampleConsumer.InputResult;
import com.google.common.collect.ImmutableMap;
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.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -67,16 +59,21 @@ public final class ImageAssetLoader implements AssetLoader {
/** An {@link AssetLoader.Factory} for {@link ImageAssetLoader} instances. */
public static final class Factory implements AssetLoader.Factory {
- private final Context context;
+ private final BitmapLoader bitmapLoader;
- public Factory(Context context) {
- this.context = context.getApplicationContext();
+ /**
+ * Creates an instance.
+ *
+ * @param bitmapLoader The {@link BitmapLoader} to use to load and decode images.
+ */
+ public Factory(BitmapLoader bitmapLoader) {
+ this.bitmapLoader = bitmapLoader;
}
@Override
public AssetLoader createAssetLoader(
EditedMediaItem editedMediaItem, Looper looper, Listener listener) {
- return new ImageAssetLoader(context, editedMediaItem, listener);
+ return new ImageAssetLoader(editedMediaItem, listener, bitmapLoader);
}
}
@@ -85,7 +82,7 @@ public final class ImageAssetLoader implements AssetLoader {
private static final int QUEUE_BITMAP_INTERVAL_MS = 10;
private final EditedMediaItem editedMediaItem;
- private final DataSource.Factory dataSourceFactory;
+ private final BitmapLoader bitmapLoader;
private final Listener listener;
private final ScheduledExecutorService scheduledExecutorService;
@@ -94,12 +91,13 @@ public final class ImageAssetLoader implements AssetLoader {
private volatile int progress;
- private ImageAssetLoader(Context context, EditedMediaItem editedMediaItem, Listener listener) {
+ private ImageAssetLoader(
+ EditedMediaItem editedMediaItem, Listener listener, BitmapLoader bitmapLoader) {
checkState(editedMediaItem.durationUs != C.TIME_UNSET);
checkState(editedMediaItem.frameRate != C.RATE_UNSET_INT);
this.editedMediaItem = editedMediaItem;
- dataSourceFactory = new DefaultDataSource.Factory(context);
this.listener = listener;
+ this.bitmapLoader = bitmapLoader;
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
progressState = PROGRESS_STATE_NOT_STARTED;
}
@@ -112,18 +110,11 @@ public final class ImageAssetLoader implements AssetLoader {
progressState = PROGRESS_STATE_AVAILABLE;
listener.onDurationUs(editedMediaItem.durationUs);
listener.onTrackCount(1);
- @Nullable BitmapFactory.Options options = null;
- if (Util.SDK_INT >= 26) {
- options = new BitmapFactory.Options();
- options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
- }
- BitmapLoader bitmapLoader =
- new DataSourceBitmapLoader(
- MoreExecutors.listeningDecorator(scheduledExecutorService), dataSourceFactory, options);
MediaItem.LocalConfiguration localConfiguration =
checkNotNull(editedMediaItem.mediaItem.localConfiguration);
ListenableFuture