Transformer: add api to cutomize image loading

PiperOrigin-RevId: 598793134
This commit is contained in:
tofunmi 2024-01-16 03:52:00 -08:00 committed by Copybara-Service
parent 2d89ffc682
commit 5488d33da8
5 changed files with 161 additions and 39 deletions

View File

@ -19,6 +19,7 @@ import static androidx.media3.common.MimeTypes.normalizeMimeType;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@ -29,6 +30,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern;
/** Defines common file type constants and helper methods. */ /** Defines common file type constants and helper methods. */
@UnstableApi @UnstableApi
@ -327,4 +329,30 @@ public final class FileTypes {
return FileTypes.UNKNOWN; return FileTypes.UNKNOWN;
} }
} }
/**
* Returns the file extension of the given {@link Uri} or an empty string if there is no
* extension.
*
* <p>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 "";
}
} }

View File

@ -16,18 +16,28 @@
package androidx.media3.transformer; 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.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.ColorSpace;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.FileTypes;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.BitmapLoader;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.UnstableApi; 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 androidx.media3.exoplayer.source.MediaSource;
import com.google.common.base.Ascii; 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; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** The default {@link AssetLoader.Factory} implementation. */ /** The default {@link AssetLoader.Factory} implementation. */
@ -39,6 +49,7 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory {
private final boolean forceInterpretHdrAsSdr; private final boolean forceInterpretHdrAsSdr;
private final Clock clock; private final Clock clock;
private final MediaSource.@MonotonicNonNull Factory mediaSourceFactory; private final MediaSource.@MonotonicNonNull Factory mediaSourceFactory;
private final BitmapLoader bitmapLoader;
private AssetLoader.@MonotonicNonNull Factory imageAssetLoaderFactory; private AssetLoader.@MonotonicNonNull Factory imageAssetLoaderFactory;
private AssetLoader.@MonotonicNonNull Factory exoPlayerAssetLoaderFactory; private AssetLoader.@MonotonicNonNull Factory exoPlayerAssetLoaderFactory;
@ -46,6 +57,10 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory {
/** /**
* Creates an instance. * Creates an instance.
* *
* <p>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 context The {@link Context}.
* @param decoderFactory The {@link Codec.DecoderFactory} to use to decode the samples (if * @param decoderFactory The {@link Codec.DecoderFactory} to use to decode the samples (if
* necessary). * necessary).
@ -64,6 +79,37 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory {
this.forceInterpretHdrAsSdr = forceInterpretHdrAsSdr; this.forceInterpretHdrAsSdr = forceInterpretHdrAsSdr;
this.clock = clock; this.clock = clock;
this.mediaSourceFactory = null; 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}.
*
* <p>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. * testing.
* @param mediaSourceFactory The {@link MediaSource.Factory} to use to retrieve the samples to * @param mediaSourceFactory The {@link MediaSource.Factory} to use to retrieve the samples to
* transform when an {@link ExoPlayerAssetLoader} is used. * transform when an {@link ExoPlayerAssetLoader} is used.
* @param bitmapLoader The {@link BitmapLoader} to use to load and decode images.
*/ */
public DefaultAssetLoaderFactory( public DefaultAssetLoaderFactory(
Context context, Context context,
Codec.DecoderFactory decoderFactory, Codec.DecoderFactory decoderFactory,
boolean forceInterpretHdrAsSdr, boolean forceInterpretHdrAsSdr,
Clock clock, Clock clock,
MediaSource.Factory mediaSourceFactory) { MediaSource.Factory mediaSourceFactory,
BitmapLoader bitmapLoader) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.decoderFactory = decoderFactory; this.decoderFactory = decoderFactory;
this.forceInterpretHdrAsSdr = forceInterpretHdrAsSdr; this.forceInterpretHdrAsSdr = forceInterpretHdrAsSdr;
this.clock = clock; this.clock = clock;
this.mediaSourceFactory = mediaSourceFactory; this.mediaSourceFactory = mediaSourceFactory;
this.bitmapLoader = bitmapLoader;
} }
@Override @Override
@ -98,7 +147,7 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory {
MediaItem mediaItem = editedMediaItem.mediaItem; MediaItem mediaItem = editedMediaItem.mediaItem;
if (isImage(mediaItem.localConfiguration)) { if (isImage(mediaItem.localConfiguration)) {
if (imageAssetLoaderFactory == null) { if (imageAssetLoaderFactory == null) {
imageAssetLoaderFactory = new ImageAssetLoader.Factory(context); imageAssetLoaderFactory = new ImageAssetLoader.Factory(bitmapLoader);
} }
return imageAssetLoaderFactory.createAssetLoader(editedMediaItem, looper, listener); return imageAssetLoaderFactory.createAssetLoader(editedMediaItem, looper, listener);
} }
@ -113,21 +162,71 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory {
return exoPlayerAssetLoaderFactory.createAssetLoader(editedMediaItem, looper, listener); return exoPlayerAssetLoaderFactory.createAssetLoader(editedMediaItem, looper, listener);
} }
private static boolean isImage(@Nullable MediaItem.LocalConfiguration localConfiguration) { private boolean isImage(@Nullable MediaItem.LocalConfiguration localConfiguration) {
if (localConfiguration == null) { if (localConfiguration == null) {
return false; return false;
} }
if (localConfiguration.mimeType != null) { @Nullable String mimeType = localConfiguration.mimeType;
return MimeTypes.isImage(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<String> supportedImageTypes = if (mimeType == null) {
ImmutableList.of(".png", ".webp", ".jpg", ".jpeg", ".heic", ".heif", ".bmp");
String uriPath = checkNotNull(localConfiguration.uri.getPath());
int fileExtensionStart = uriPath.lastIndexOf(".");
if (fileExtensionStart < 0) {
return false; return false;
} }
String extension = Ascii.toLowerCase(uriPath.substring(fileExtensionStart)); if (!MimeTypes.isImage(mimeType)) {
return supportedImageTypes.contains(extension); return false;
}
checkState(
bitmapLoader.supportsMimeType(mimeType),
"Image format not supported by given bitmapLoader");
return true;
}
@Nullable
private static String getCommonImageMimeTypeFromExtension(String extension) {
switch (extension) {
case "bmp":
case "dib":
return MimeTypes.IMAGE_BMP;
case "heif":
case "heic":
return MimeTypes.IMAGE_HEIF;
case "jpg":
case "jpeg":
case "jpe":
case "jif":
case "jfif":
case "jfi":
return MimeTypes.IMAGE_JPEG;
case "png":
return MimeTypes.IMAGE_PNG;
case "webp":
return MimeTypes.IMAGE_WEBP;
case "gif":
return "image/gif";
case "tiff":
case "tif":
return "image/tiff";
case "raw":
case "arw":
case "cr2":
case "k25":
return "image/raw";
case "svg":
case "svgz":
return "image/svg+xml";
case "ico":
return "image/x-icon";
case "avif":
return "image/avif";
default:
return null;
}
} }
} }

View File

@ -45,7 +45,9 @@ public final class EditedMediaItem {
* Creates an instance. * Creates an instance.
* *
* <p>For image inputs, the values passed into {@link #setRemoveAudio}, {@link #setRemoveVideo} * <p>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. * @param mediaItem The {@link MediaItem} on which transformations are applied.
*/ */

View File

@ -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 androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ColorSpace;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; 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.BitmapLoader;
import androidx.media3.common.util.ConstantRateTimestampIterator; import androidx.media3.common.util.ConstantRateTimestampIterator;
import androidx.media3.common.util.UnstableApi; 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 androidx.media3.transformer.SampleConsumer.InputResult;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@ -67,16 +59,21 @@ public final class ImageAssetLoader implements AssetLoader {
/** An {@link AssetLoader.Factory} for {@link ImageAssetLoader} instances. */ /** An {@link AssetLoader.Factory} for {@link ImageAssetLoader} instances. */
public static final class Factory implements AssetLoader.Factory { 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 @Override
public AssetLoader createAssetLoader( public AssetLoader createAssetLoader(
EditedMediaItem editedMediaItem, Looper looper, Listener listener) { 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 static final int QUEUE_BITMAP_INTERVAL_MS = 10;
private final EditedMediaItem editedMediaItem; private final EditedMediaItem editedMediaItem;
private final DataSource.Factory dataSourceFactory; private final BitmapLoader bitmapLoader;
private final Listener listener; private final Listener listener;
private final ScheduledExecutorService scheduledExecutorService; private final ScheduledExecutorService scheduledExecutorService;
@ -94,12 +91,13 @@ public final class ImageAssetLoader implements AssetLoader {
private volatile int progress; 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.durationUs != C.TIME_UNSET);
checkState(editedMediaItem.frameRate != C.RATE_UNSET_INT); checkState(editedMediaItem.frameRate != C.RATE_UNSET_INT);
this.editedMediaItem = editedMediaItem; this.editedMediaItem = editedMediaItem;
dataSourceFactory = new DefaultDataSource.Factory(context);
this.listener = listener; this.listener = listener;
this.bitmapLoader = bitmapLoader;
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
progressState = PROGRESS_STATE_NOT_STARTED; progressState = PROGRESS_STATE_NOT_STARTED;
} }
@ -112,18 +110,11 @@ public final class ImageAssetLoader implements AssetLoader {
progressState = PROGRESS_STATE_AVAILABLE; progressState = PROGRESS_STATE_AVAILABLE;
listener.onDurationUs(editedMediaItem.durationUs); listener.onDurationUs(editedMediaItem.durationUs);
listener.onTrackCount(1); 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 = MediaItem.LocalConfiguration localConfiguration =
checkNotNull(editedMediaItem.mediaItem.localConfiguration); checkNotNull(editedMediaItem.mediaItem.localConfiguration);
ListenableFuture<Bitmap> future = bitmapLoader.loadBitmap(localConfiguration.uri); ListenableFuture<Bitmap> future = bitmapLoader.loadBitmap(localConfiguration.uri);
Futures.addCallback( Futures.addCallback(
future, future,
new FutureCallback<Bitmap>() { new FutureCallback<Bitmap>() {

View File

@ -23,6 +23,7 @@ import android.os.Looper;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.util.TimestampIterator; import androidx.media3.common.util.TimestampIterator;
import androidx.media3.datasource.DataSourceBitmapLoader;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.time.Duration; import java.time.Duration;
@ -120,7 +121,8 @@ public class ImageAssetLoaderTest {
.setDurationUs(1_000_000) .setDurationUs(1_000_000)
.setFrameRate(30) .setFrameRate(30)
.build(); .build();
return new ImageAssetLoader.Factory(ApplicationProvider.getApplicationContext()) return new ImageAssetLoader.Factory(
new DataSourceBitmapLoader(ApplicationProvider.getApplicationContext()))
.createAssetLoader(editedMediaItem, Looper.myLooper(), listener); .createAssetLoader(editedMediaItem, Looper.myLooper(), listener);
} }