diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 581eb4fd10..f27f499dcb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -19,6 +19,7 @@ `SampleConsumer.queueInputBitmap` to `TimestampIterator`. * Track Selection: * Extractors: + * Add `BmpExtractor`. * Audio: * Add support for Opus gapless metadata during offload playback. * Video: diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/SingleSampleExtractorHelper.java b/libraries/extractor/src/main/java/androidx/media3/extractor/SingleSampleExtractorHelper.java new file mode 100644 index 0000000000..4bca96713a --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/SingleSampleExtractorHelper.java @@ -0,0 +1,128 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.extractor; + +import static androidx.media3.common.C.BUFFER_FLAG_KEY_FRAME; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.extractor.Extractor.RESULT_CONTINUE; +import static androidx.media3.extractor.Extractor.RESULT_END_OF_INPUT; +import static java.lang.annotation.ElementType.TYPE_USE; + +import androidx.annotation.IntDef; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.extractor.mp4.Mp4Extractor; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +/** + * Extracts data by loading all the bytes into one sample. + * + *

Used as a component in other extractors. + */ +@UnstableApi +/* package */ public final class SingleSampleExtractorHelper { + + /** Parser states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({STATE_READING, STATE_ENDED}) + private @interface State {} + + private static final int STATE_READING = 1; + private static final int STATE_ENDED = 2; + + /** + * The identifier to use for the image track. Chosen to avoid colliding with track IDs used by + * {@link Mp4Extractor} for motion photos. + */ + public static final int IMAGE_TRACK_ID = 1024; + + private static final int FIXED_READ_LENGTH = 1024; + + private int size; + private @State int state; + private @MonotonicNonNull ExtractorOutput extractorOutput; + private @MonotonicNonNull TrackOutput trackOutput; + + public boolean sniff(ExtractorInput input, int fileSignature, int fileSignatureLength) + throws IOException { + ParsableByteArray scratch = new ParsableByteArray(fileSignatureLength); + input.peekFully(scratch.getData(), /* offset= */ 0, fileSignatureLength); + return scratch.readUnsignedShort() == fileSignature; + } + + public void init(ExtractorOutput output, String containerMimeType) { + extractorOutput = output; + outputImageTrackAndSeekMap(containerMimeType); + } + + public @Extractor.ReadResult int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException { + switch (state) { + case STATE_READING: + readSegment(input); + return RESULT_CONTINUE; + case STATE_ENDED: + return RESULT_END_OF_INPUT; + default: + throw new IllegalStateException(); + } + } + + private void readSegment(ExtractorInput input) throws IOException { + int result = + checkNotNull(trackOutput).sampleData(input, FIXED_READ_LENGTH, /* allowEndOfInput= */ true); + if (result == C.RESULT_END_OF_INPUT) { + state = STATE_ENDED; + @C.BufferFlags int flags = BUFFER_FLAG_KEY_FRAME; + trackOutput.sampleMetadata( + /* timeUs= */ 0, flags, size, /* offset= */ 0, /* cryptoData= */ null); + size = 0; + } else { + size += result; + } + } + + public void seek(long position) { + if (position == 0 || state == STATE_READING) { + state = STATE_READING; + size = 0; + } + } + + @RequiresNonNull("this.extractorOutput") + private void outputImageTrackAndSeekMap(String containerMimeType) { + trackOutput = extractorOutput.track(IMAGE_TRACK_ID, C.TRACK_TYPE_IMAGE); + trackOutput.format( + new Format.Builder() + .setContainerMimeType(containerMimeType) + .setTileCountHorizontal(1) + .setTileCountVertical(1) + .build()); + extractorOutput.endTracks(); + extractorOutput.seekMap(new SingleSampleSeekMap(/* durationUs= */ C.TIME_UNSET)); + state = STATE_READING; + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/bmp/BmpExtractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/bmp/BmpExtractor.java new file mode 100644 index 0000000000..cac98de6ee --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/bmp/BmpExtractor.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.extractor.bmp; + +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.extractor.Extractor; +import androidx.media3.extractor.ExtractorInput; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.PositionHolder; +import androidx.media3.extractor.SingleSampleExtractorHelper; +import java.io.IOException; + +/** Extracts data from the BMP container format. */ +@UnstableApi +public final class BmpExtractor implements Extractor { + private static final int BMP_FILE_SIGNATURE_LENGTH = 2; + private static final int BMP_FILE_SIGNATURE = 0x424D; + + private final SingleSampleExtractorHelper imageExtractor; + + /** Creates an instance. */ + public BmpExtractor() { + imageExtractor = new SingleSampleExtractorHelper(); + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException { + return imageExtractor.sniff(input, BMP_FILE_SIGNATURE, BMP_FILE_SIGNATURE_LENGTH); + } + + @Override + public void init(ExtractorOutput output) { + imageExtractor.init(output, MimeTypes.IMAGE_BMP); + } + + @Override + public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException { + return imageExtractor.read(input, seekPosition); + } + + @Override + public void seek(long position, long timeUs) { + imageExtractor.seek(position); + } + + @Override + public void release() { + // Do nothing. + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ImageExtractorUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/bmp/package-info.java similarity index 57% rename from libraries/extractor/src/main/java/androidx/media3/extractor/ImageExtractorUtil.java rename to libraries/extractor/src/main/java/androidx/media3/extractor/bmp/package-info.java index 744820956d..95d2d61840 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ImageExtractorUtil.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/bmp/package-info.java @@ -13,20 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.media3.extractor; +@NonNullApi +package androidx.media3.extractor.bmp; -import androidx.media3.common.util.UnstableApi; -import androidx.media3.extractor.mp4.Mp4Extractor; - -/** Utilities for image extractors. */ -@UnstableApi -public class ImageExtractorUtil { - - /** - * The identifier to use for the image track. Chosen to avoid colliding with track IDs used by - * {@link Mp4Extractor} for motion photos. - */ - public static final int IMAGE_TRACK_ID = 1024; - - private ImageExtractorUtil() {} -} +import androidx.media3.common.util.NonNullApi; diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/JpegExtractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/JpegExtractor.java index f9fc1d98b9..142ea71e6c 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/JpegExtractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/jpeg/JpegExtractor.java @@ -16,7 +16,7 @@ package androidx.media3.extractor.jpeg; import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.extractor.ImageExtractorUtil.IMAGE_TRACK_ID; +import static androidx.media3.extractor.SingleSampleExtractorHelper.IMAGE_TRACK_ID; import static java.lang.annotation.ElementType.TYPE_USE; import androidx.annotation.IntDef; diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/png/PngExtractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/png/PngExtractor.java index afc645362d..45c56ef042 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/png/PngExtractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/png/PngExtractor.java @@ -15,127 +15,49 @@ */ package androidx.media3.extractor.png; -import static androidx.media3.common.C.BUFFER_FLAG_KEY_FRAME; -import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.extractor.ImageExtractorUtil.IMAGE_TRACK_ID; -import static java.lang.annotation.ElementType.TYPE_USE; - -import androidx.annotation.IntDef; -import androidx.media3.common.C; -import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; -import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.extractor.Extractor; import androidx.media3.extractor.ExtractorInput; import androidx.media3.extractor.ExtractorOutput; import androidx.media3.extractor.PositionHolder; -import androidx.media3.extractor.SingleSampleSeekMap; -import androidx.media3.extractor.TrackOutput; +import androidx.media3.extractor.SingleSampleExtractorHelper; import java.io.IOException; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** Extracts data from the PNG container format. */ @UnstableApi -// TODO: b/289989902 - Move methods of this class into ImageExtractorUtil once there are multiple -// image extractors. public final class PngExtractor implements Extractor { - /** Parser states. */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({STATE_READING_IMAGE, STATE_ENDED}) - private @interface State {} - - private static final int STATE_READING_IMAGE = 1; - private static final int STATE_ENDED = 2; - - private static final int PNG_FILE_SIGNATURE_LENGTH = 2; // See PNG (Portable Network Graphics) Specification, Version 1.2, Section 12.12 and Section 3.1. private static final int PNG_FILE_SIGNATURE = 0x8950; - private static final int FIXED_READ_LENGTH = 1024; + private static final int PNG_FILE_SIGNATURE_LENGTH = 2; - private final ParsableByteArray scratch; - - private int size; - private @State int state; - private @MonotonicNonNull ExtractorOutput extractorOutput; - private @MonotonicNonNull TrackOutput trackOutput; + private final SingleSampleExtractorHelper imageExtractor; /** Creates an instance. */ public PngExtractor() { - scratch = new ParsableByteArray(PNG_FILE_SIGNATURE_LENGTH); + imageExtractor = new SingleSampleExtractorHelper(); } @Override public boolean sniff(ExtractorInput input) throws IOException { - scratch.reset(/* limit= */ PNG_FILE_SIGNATURE_LENGTH); - input.peekFully(scratch.getData(), /* offset= */ 0, PNG_FILE_SIGNATURE_LENGTH); - return scratch.readUnsignedShort() == PNG_FILE_SIGNATURE; + return imageExtractor.sniff(input, PNG_FILE_SIGNATURE, PNG_FILE_SIGNATURE_LENGTH); } @Override public void init(ExtractorOutput output) { - extractorOutput = output; - outputImageTrackAndSeekMap(); + imageExtractor.init(output, MimeTypes.IMAGE_PNG); } @Override public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { - switch (state) { - case STATE_READING_IMAGE: - readSegment(input); - return RESULT_CONTINUE; - case STATE_ENDED: - return RESULT_END_OF_INPUT; - default: - throw new IllegalStateException(); - } - } - - private void readSegment(ExtractorInput input) throws IOException { - int result = - checkNotNull(trackOutput).sampleData(input, FIXED_READ_LENGTH, /* allowEndOfInput= */ true); - if (result == C.RESULT_END_OF_INPUT) { - state = STATE_ENDED; - @C.BufferFlags int flags = BUFFER_FLAG_KEY_FRAME; - trackOutput.sampleMetadata( - /* timeUs= */ 0, flags, size, /* offset= */ 0, /* cryptoData= */ null); - size = 0; - } else { - size += result; - } - } - - @RequiresNonNull("this.extractorOutput") - private void outputImageTrackAndSeekMap() { - trackOutput = extractorOutput.track(IMAGE_TRACK_ID, C.TRACK_TYPE_IMAGE); - trackOutput.format( - new Format.Builder() - .setContainerMimeType(MimeTypes.IMAGE_PNG) - .setTileCountHorizontal(1) - .setTileCountVertical(1) - .build()); - extractorOutput.endTracks(); - extractorOutput.seekMap(new SingleSampleSeekMap(/* durationUs= */ C.TIME_UNSET)); - state = STATE_READING_IMAGE; + return imageExtractor.read(input, seekPosition); } @Override public void seek(long position, long timeUs) { - if (position == 0) { - state = STATE_READING_IMAGE; - } - if (state == STATE_READING_IMAGE) { - size = 0; - } + imageExtractor.seek(position); } @Override diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/bmp/BmpExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/bmp/BmpExtractorTest.java new file mode 100644 index 0000000000..c5b410ad13 --- /dev/null +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/bmp/BmpExtractorTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.extractor.bmp; + +import androidx.media3.test.utils.ExtractorAsserts; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; + +/** Unit tests for {@link BmpExtractor}. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public final class BmpExtractorTest { + + @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") + public static ImmutableList params() { + return ExtractorAsserts.configs(); + } + + @ParameterizedRobolectricTestRunner.Parameter + public ExtractorAsserts.SimulationConfig simulationConfig; + + @Test + public void sampleBmp() throws Exception { + ExtractorAsserts.assertBehavior( + BmpExtractor::new, "media/bmp/non-motion-photo-shortened-cropped.bmp", simulationConfig); + } +} diff --git a/libraries/test_data/src/test/assets/extractordumps/bmp/non-motion-photo-shortened-cropped.bmp.0.dump b/libraries/test_data/src/test/assets/extractordumps/bmp/non-motion-photo-shortened-cropped.bmp.0.dump new file mode 100644 index 0000000000..b5af669ca9 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/bmp/non-motion-photo-shortened-cropped.bmp.0.dump @@ -0,0 +1,16 @@ +seekMap: + isSeekable = true + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] +numberOfTracks = 1 +track 1024: + total output bytes = 69130 + sample count = 1 + format 0: + containerMimeType = image/bmp + sample 0: + time = 0 + flags = 1 + data = length 69130, hash D9768D79 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/bmp/non-motion-photo-shortened-cropped.bmp.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/bmp/non-motion-photo-shortened-cropped.bmp.unknown_length.dump new file mode 100644 index 0000000000..b5af669ca9 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/bmp/non-motion-photo-shortened-cropped.bmp.unknown_length.dump @@ -0,0 +1,16 @@ +seekMap: + isSeekable = true + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] +numberOfTracks = 1 +track 1024: + total output bytes = 69130 + sample count = 1 + format 0: + containerMimeType = image/bmp + sample 0: + time = 0 + flags = 1 + data = length 69130, hash D9768D79 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/media/bmp/non-motion-photo-shortened-cropped.bmp b/libraries/test_data/src/test/assets/media/bmp/non-motion-photo-shortened-cropped.bmp new file mode 100644 index 0000000000..344c4cdb38 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/bmp/non-motion-photo-shortened-cropped.bmp differ