diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f27f499dcb..ba16f5bd3d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,7 @@ * Track Selection: * Extractors: * Add `BmpExtractor`. + * Add `WebpExtractor`. * Audio: * Add support for Opus gapless metadata during offload playback. * Video: diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/webp/WebpExtractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/webp/WebpExtractor.java new file mode 100644 index 0000000000..a99ccbebf7 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/webp/WebpExtractor.java @@ -0,0 +1,82 @@ +/* + * 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.webp; + +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.SingleSampleExtractorHelper; +import java.io.IOException; + +/** Extracts data from the WEBP container format. */ +@UnstableApi +public final class WebpExtractor implements Extractor { + + // Documentation Reference: + // https://developers.google.com/speed/webp/docs/riff_container#webp_file_header + private static final int FILE_SIGNATURE_SEGMENT_LENGTH = 4; + private static final int RIFF_FILE_SIGNATURE = 0x52494646; + private static final int WEBP_FILE_SIGNATURE = 0x57454250; + + private final SingleSampleExtractorHelper imageExtractor; + + /** Creates an instance. */ + public WebpExtractor() { + imageExtractor = new SingleSampleExtractorHelper(); + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException { + // The full file signature for the format webp is RIFF????WEBP. + ParsableByteArray scratch = new ParsableByteArray(FILE_SIGNATURE_SEGMENT_LENGTH); + + input.peekFully(scratch.getData(), /* offset= */ 0, FILE_SIGNATURE_SEGMENT_LENGTH); + if (scratch.readUnsignedInt() != RIFF_FILE_SIGNATURE) { + return false; + } + + input.advancePeekPosition(FILE_SIGNATURE_SEGMENT_LENGTH); + scratch.reset(/* limit= */ FILE_SIGNATURE_SEGMENT_LENGTH); + + input.peekFully(scratch.getData(), /* offset= */ 0, FILE_SIGNATURE_SEGMENT_LENGTH); + return scratch.readUnsignedInt() == WEBP_FILE_SIGNATURE; + } + + @Override + public void init(ExtractorOutput output) { + imageExtractor.init(output, MimeTypes.IMAGE_WEBP); + } + + @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/webp/package-info.java b/libraries/extractor/src/main/java/androidx/media3/extractor/webp/package-info.java new file mode 100644 index 0000000000..06136904bf --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/webp/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package androidx.media3.extractor.webp; + +import androidx.media3.common.util.NonNullApi; diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/webp/WebpExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/webp/WebpExtractorTest.java new file mode 100644 index 0000000000..bce4dade47 --- /dev/null +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/webp/WebpExtractorTest.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.webp; + +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 WebpExtractor}. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public final class WebpExtractorTest { + + @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") + public static ImmutableList params() { + return ExtractorAsserts.configs(); + } + + @ParameterizedRobolectricTestRunner.Parameter + public ExtractorAsserts.SimulationConfig simulationConfig; + + @Test + public void sampleWebp() throws Exception { + ExtractorAsserts.assertBehavior( + WebpExtractor::new, "media/webp/ic_launcher_round.webp", simulationConfig); + } +} diff --git a/libraries/test_data/src/test/assets/extractordumps/webp/ic_launcher_round.webp.0.dump b/libraries/test_data/src/test/assets/extractordumps/webp/ic_launcher_round.webp.0.dump new file mode 100644 index 0000000000..7a4917504e --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/webp/ic_launcher_round.webp.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 = 7778 + sample count = 1 + format 0: + containerMimeType = image/webp + sample 0: + time = 0 + flags = 1 + data = length 7778, hash 471EBEE1 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/webp/ic_launcher_round.webp.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/webp/ic_launcher_round.webp.unknown_length.dump new file mode 100644 index 0000000000..7a4917504e --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/webp/ic_launcher_round.webp.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 = 7778 + sample count = 1 + format 0: + containerMimeType = image/webp + sample 0: + time = 0 + flags = 1 + data = length 7778, hash 471EBEE1 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/media/webp/ic_launcher_round.webp b/libraries/test_data/src/test/assets/media/webp/ic_launcher_round.webp new file mode 100644 index 0000000000..9126ae37cb Binary files /dev/null and b/libraries/test_data/src/test/assets/media/webp/ic_launcher_round.webp differ