mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Downscale bitmaps during decoding in Transformer
Limit input image size in Transformer to be less than 4096x4096. For very large images, this can reduce memory usage substantially, and stays away from `GL_MAX_TEXTURE_SIZE` - often 4096 PiperOrigin-RevId: 670555939
This commit is contained in:
parent
9562c976a9
commit
1c61fbadf7
@ -31,6 +31,7 @@ import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
@ -215,6 +216,28 @@ public class DataSourceBitmapLoaderTest {
|
||||
assertThat(bitmap.isMutable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadBitmap_withFileUriAndMaxOutputDimension_loadsDataWithSmallerSize()
|
||||
throws Exception {
|
||||
byte[] imageData =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TEST_IMAGE_PATH);
|
||||
File file = tempFolder.newFile();
|
||||
Files.write(Path.of(file.getAbsolutePath()), imageData);
|
||||
Uri uri = Uri.fromFile(file);
|
||||
int maximumOutputDimension = 2000;
|
||||
DataSourceBitmapLoader bitmapLoader =
|
||||
new DataSourceBitmapLoader(
|
||||
MoreExecutors.newDirectExecutorService(),
|
||||
dataSourceFactory,
|
||||
/* options= */ null,
|
||||
maximumOutputDimension);
|
||||
|
||||
Bitmap bitmap = bitmapLoader.loadBitmap(uri).get();
|
||||
|
||||
assertThat(bitmap.getWidth()).isAtMost(maximumOutputDimension);
|
||||
assertThat(bitmap.getHeight()).isAtMost(maximumOutputDimension);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadBitmap_fileUriWithFileNotExisting_throws() {
|
||||
DataSourceBitmapLoader bitmapLoader =
|
||||
|
@ -15,11 +15,14 @@
|
||||
*/
|
||||
package androidx.media3.datasource;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Matrix;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.ParserException;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import java.io.ByteArrayInputStream;
|
||||
@ -38,14 +41,37 @@ public final class BitmapUtil {
|
||||
*
|
||||
* @param data Byte array of compressed image data.
|
||||
* @param length The number of bytes to parse.
|
||||
* @param options the {@link BitmapFactory.Options} to decode the {@code data} with.
|
||||
* @param options The {@link BitmapFactory.Options} to decode the {@code data} with.
|
||||
* @param maximumOutputDimension The largest output Bitmap dimension that can be returned by this
|
||||
* method, or {@link C#LENGTH_UNSET} if no limits are enforced.
|
||||
* @throws ParserException if the {@code data} could not be decoded.
|
||||
*/
|
||||
// BitmapFactory's options parameter is null-ok.
|
||||
@SuppressWarnings("nullness:argument.type.incompatible")
|
||||
public static Bitmap decode(byte[] data, int length, @Nullable BitmapFactory.Options options)
|
||||
public static Bitmap decode(
|
||||
byte[] data, int length, @Nullable BitmapFactory.Options options, int maximumOutputDimension)
|
||||
throws IOException {
|
||||
if (maximumOutputDimension != C.LENGTH_UNSET) {
|
||||
if (options == null) {
|
||||
options = new BitmapFactory.Options();
|
||||
}
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeByteArray(data, /* offset= */ 0, length, options);
|
||||
int largerDimensions = max(options.outWidth, options.outHeight);
|
||||
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = 1;
|
||||
// Only scaling by 2x is supported.
|
||||
while (largerDimensions > maximumOutputDimension) {
|
||||
options.inSampleSize *= 2;
|
||||
largerDimensions /= 2;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, length, options);
|
||||
if (options != null) {
|
||||
options.inSampleSize = 1;
|
||||
}
|
||||
if (bitmap == null) {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"Could not decode image data", new IllegalStateException());
|
||||
|
@ -23,6 +23,7 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.BitmapLoader;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.common.base.Supplier;
|
||||
@ -51,6 +52,7 @@ public final class DataSourceBitmapLoader implements BitmapLoader {
|
||||
private final ListeningExecutorService listeningExecutorService;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
@Nullable private final BitmapFactory.Options options;
|
||||
private final int maximumOutputDimension;
|
||||
|
||||
/**
|
||||
* Creates an instance that uses a {@link DefaultHttpDataSource} for image loading and delegates
|
||||
@ -84,9 +86,29 @@ public final class DataSourceBitmapLoader implements BitmapLoader {
|
||||
ListeningExecutorService listeningExecutorService,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
@Nullable BitmapFactory.Options options) {
|
||||
this(listeningExecutorService, dataSourceFactory, options, C.LENGTH_UNSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance that delegates loading tasks to the {@link ListeningExecutorService}.
|
||||
*
|
||||
* <p>Use {@code maximumOutputDimension} to limit memory usage when loading large Bitmaps.
|
||||
*
|
||||
* @param listeningExecutorService The {@link ListeningExecutorService}.
|
||||
* @param dataSourceFactory The {@link DataSource.Factory} that creates the {@link DataSource}
|
||||
* used to load the image.
|
||||
* @param options The {@link BitmapFactory.Options} the image should be loaded with.
|
||||
* @param maximumOutputDimension The maximum dimension of the output Bitmap.
|
||||
*/
|
||||
public DataSourceBitmapLoader(
|
||||
ListeningExecutorService listeningExecutorService,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
@Nullable BitmapFactory.Options options,
|
||||
int maximumOutputDimension) {
|
||||
this.listeningExecutorService = listeningExecutorService;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.options = options;
|
||||
this.maximumOutputDimension = maximumOutputDimension;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -96,22 +118,27 @@ public final class DataSourceBitmapLoader implements BitmapLoader {
|
||||
|
||||
@Override
|
||||
public ListenableFuture<Bitmap> decodeBitmap(byte[] data) {
|
||||
return listeningExecutorService.submit(() -> BitmapUtil.decode(data, data.length, options));
|
||||
return listeningExecutorService.submit(
|
||||
() -> BitmapUtil.decode(data, data.length, options, maximumOutputDimension));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<Bitmap> loadBitmap(Uri uri) {
|
||||
return listeningExecutorService.submit(
|
||||
() -> load(dataSourceFactory.createDataSource(), uri, options));
|
||||
() -> load(dataSourceFactory.createDataSource(), uri, options, maximumOutputDimension));
|
||||
}
|
||||
|
||||
private static Bitmap load(
|
||||
DataSource dataSource, Uri uri, @Nullable BitmapFactory.Options options) throws IOException {
|
||||
DataSource dataSource,
|
||||
Uri uri,
|
||||
@Nullable BitmapFactory.Options options,
|
||||
int maximumOutputDimension)
|
||||
throws IOException {
|
||||
try {
|
||||
DataSpec dataSpec = new DataSpec(uri);
|
||||
dataSource.open(dataSpec);
|
||||
byte[] readData = DataSourceUtil.readToEnd(dataSource);
|
||||
return BitmapUtil.decode(readData, readData.length, options);
|
||||
return BitmapUtil.decode(readData, readData.length, options, maximumOutputDimension);
|
||||
} finally {
|
||||
dataSource.close();
|
||||
}
|
||||
|
@ -159,7 +159,8 @@ public final class BitmapFactoryImageDecoder
|
||||
*/
|
||||
private static Bitmap decode(byte[] data, int length) throws ImageDecoderException {
|
||||
try {
|
||||
return BitmapUtil.decode(data, length, /* options= */ null);
|
||||
return BitmapUtil.decode(
|
||||
data, length, /* options= */ null, /* maximumOutputDimension= */ C.LENGTH_UNSET);
|
||||
} catch (ParserException e) {
|
||||
throw new ImageDecoderException(
|
||||
"Could not decode image data with BitmapFactory. (data.length = "
|
||||
|
BIN
libraries/test_data/src/test/assets/media/webp/black_large.webp
Normal file
BIN
libraries/test_data/src/test/assets/media/webp/black_large.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 250 KiB |
@ -17,6 +17,7 @@ package androidx.media3.transformer;
|
||||
|
||||
import static androidx.media3.common.MimeTypes.IMAGE_JPEG;
|
||||
import static androidx.media3.common.MimeTypes.IMAGE_PNG;
|
||||
import static androidx.media3.common.MimeTypes.IMAGE_WEBP;
|
||||
import static androidx.media3.common.MimeTypes.VIDEO_AV1;
|
||||
import static androidx.media3.common.MimeTypes.VIDEO_DOLBY_VISION;
|
||||
import static androidx.media3.common.MimeTypes.VIDEO_H264;
|
||||
@ -222,6 +223,16 @@ public final class AndroidTestUtil {
|
||||
.build())
|
||||
.build();
|
||||
|
||||
public static final AssetInfo WEBP_LARGE =
|
||||
new AssetInfo.Builder("asset:///media/webp/black_large.webp")
|
||||
.setVideoFormat(
|
||||
new Format.Builder()
|
||||
.setSampleMimeType(IMAGE_WEBP)
|
||||
.setWidth(16000)
|
||||
.setHeight(9000)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
public static final AssetInfo MP4_TRIM_OPTIMIZATION =
|
||||
new AssetInfo.Builder("asset:///media/mp4/internal_emulator_transformer_output.mp4")
|
||||
.setVideoFormat(
|
||||
|
@ -29,6 +29,7 @@ import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION_180;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION_270;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.PNG_ASSET;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.WEBP_LARGE;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.createFrameCountingEffect;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.createOpenGlObjects;
|
||||
@ -288,6 +289,31 @@ public class TransformerEndToEndTest {
|
||||
assertThat(new File(result.filePath).length()).isGreaterThan(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void videoEditing_withLargeImageInput_completesWithCorrectFrameCountAndDuration()
|
||||
throws Exception {
|
||||
Transformer transformer = new Transformer.Builder(context).build();
|
||||
ImmutableList<Effect> videoEffects = ImmutableList.of(Presentation.createForHeight(480));
|
||||
Effects effects = new Effects(/* audioProcessors= */ ImmutableList.of(), videoEffects);
|
||||
int expectedFrameCount = 40;
|
||||
EditedMediaItem editedMediaItem =
|
||||
new EditedMediaItem.Builder(MediaItem.fromUri(WEBP_LARGE.uri))
|
||||
.setDurationUs(C.MICROS_PER_SECOND)
|
||||
.setFrameRate(expectedFrameCount)
|
||||
.setEffects(effects)
|
||||
.build();
|
||||
ExportTestResult result =
|
||||
new TransformerAndroidTestRunner.Builder(context, transformer)
|
||||
.build()
|
||||
.run(testId, editedMediaItem);
|
||||
|
||||
assertThat(result.exportResult.videoFrameCount).isEqualTo(expectedFrameCount);
|
||||
// Expected timestamp of the last frame.
|
||||
assertThat(result.exportResult.durationMs)
|
||||
.isEqualTo((C.MILLIS_PER_SECOND / expectedFrameCount) * (expectedFrameCount - 1));
|
||||
assertThat(new File(result.filePath).length()).isGreaterThan(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void videoEditing_withTextureInput_completesWithCorrectFrameCountAndDuration()
|
||||
throws Exception {
|
||||
|
@ -39,6 +39,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
@UnstableApi
|
||||
public final class DefaultAssetLoaderFactory implements AssetLoader.Factory {
|
||||
|
||||
// Limit decoded images to 4096x4096 - should be large enough for most image to video
|
||||
// transcode operations, and smaller than GL_MAX_TEXTURE_SIZE for most devices.
|
||||
// TODO: b/356072337 - consider reading this from GL_MAX_TEXTURE_SIZE. This requires an
|
||||
// active OpenGL context.
|
||||
private static final int MAXIMUM_BITMAP_OUTPUT_DIMENSION = 4096;
|
||||
|
||||
private final Context context;
|
||||
private final Codec.DecoderFactory decoderFactory;
|
||||
private final Clock clock;
|
||||
@ -76,7 +82,8 @@ public final class DefaultAssetLoaderFactory implements AssetLoader.Factory {
|
||||
new DataSourceBitmapLoader(
|
||||
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()),
|
||||
new DefaultDataSource.Factory(context),
|
||||
options);
|
||||
options,
|
||||
MAXIMUM_BITMAP_OUTPUT_DIMENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user