Support AVIF in exoplayer

https://developer.android.com/media/platform/supported-formats#image-formats was updated to include AVIF support in API 34+, so <unknown commit> updated our associated Util's to reflect this. After that change, ExoPlayer's BitmapFactoryImageDecoder will be able to decode AVIF, but the player won't be able to detect or extract it. Add this support for completeness, so that ExoPlayer can continue to say it supports all formats in https://developer.android.com/media/platform/supported-formats#image-formats.

PiperOrigin-RevId: 633956245
This commit is contained in:
tofunmi 2024-05-15 08:02:37 -07:00 committed by Copybara-Service
parent 0e5a5e0294
commit 7b357337d2
27 changed files with 329 additions and 6 deletions

View File

@ -79,7 +79,7 @@
* Image:
* Add support for non-square DASH thumbnail grids
([#1300](https://github.com/androidx/media/pull/1300)).
* Add AVIF decoding support for API 34+.
* Add support for AVIF for API 34+.
* DRM:
* Allow setting a `LoadErrorHandlingPolicy` on
`DefaultDrmSessionManagerProvider`

View File

@ -58,6 +58,7 @@ public final class FileTypes {
* <li>{@link #WEBP}
* <li>{@link #BMP}
* <li>{@link #HEIF}
* <li>{@link #AVIF}
* </ul>
*/
@Documented
@ -65,7 +66,7 @@ public final class FileTypes {
@Target(TYPE_USE)
@IntDef({
UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG,
MIDI, AVI, PNG, WEBP, BMP, HEIF
MIDI, AVI, PNG, WEBP, BMP, HEIF, AVIF
})
public @interface Type {}
@ -135,6 +136,9 @@ public final class FileTypes {
/** File type for the HEIF format. */
public static final int HEIF = 20;
/** File type for the AVIF format. */
public static final int AVIF = 21;
@VisibleForTesting /* package */ static final String HEADER_CONTENT_TYPE = "Content-Type";
private static final String EXTENSION_AC3 = ".ac3";
@ -175,6 +179,7 @@ public final class FileTypes {
private static final String EXTENSION_BMP = ".bmp";
private static final String EXTENSION_DIB = ".dib";
private static final String EXTENSION_HEIC = ".heic";
private static final String EXTENSION_AVIF = ".avif";
private FileTypes() {}
@ -247,7 +252,10 @@ public final class FileTypes {
case MimeTypes.IMAGE_BMP:
return FileTypes.BMP;
case MimeTypes.IMAGE_HEIF:
case MimeTypes.IMAGE_HEIC:
return FileTypes.HEIF;
case MimeTypes.IMAGE_AVIF:
return FileTypes.AVIF;
default:
return FileTypes.UNKNOWN;
}
@ -323,6 +331,8 @@ public final class FileTypes {
return FileTypes.BMP;
} else if (filename.endsWith(EXTENSION_HEIC)) {
return FileTypes.HEIF;
} else if (filename.endsWith(EXTENSION_AVIF)) {
return FileTypes.AVIF;
} else {
return FileTypes.UNKNOWN;
}

View File

@ -63,7 +63,8 @@ public class ParameterizedImagePlaybackTest {
"png/media3test.png",
"bmp/non-motion-photo-shortened-cropped.bmp",
"png/non-motion-photo-shortened.png",
"webp/ic_launcher_round.webp")),
"webp/ic_launcher_round.webp",
"avif/white-1x1.avif")),
/* predicate= */ input -> !input.isEmpty()));
}

View File

@ -31,6 +31,7 @@ import androidx.media3.common.util.TimestampAdjuster;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.extractor.amr.AmrExtractor;
import androidx.media3.extractor.avi.AviExtractor;
import androidx.media3.extractor.avif.AvifExtractor;
import androidx.media3.extractor.bmp.BmpExtractor;
import androidx.media3.extractor.flac.FlacExtractor;
import androidx.media3.extractor.flv.FlvExtractor;
@ -94,6 +95,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* <li>WEBP ({@link WebpExtractor})
* <li>BMP ({@link BmpExtractor})
* <li>HEIF ({@link HeifExtractor})
* <li>AVIF ({@link AvifExtractor})
* <li>MIDI, if available, the MIDI extension's {@code androidx.media3.decoder.midi.MidiExtractor}
* is used.
* </ul>
@ -128,7 +130,8 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
FileTypes.PNG,
FileTypes.WEBP,
FileTypes.BMP,
FileTypes.HEIF
FileTypes.HEIF,
FileTypes.AVIF
};
private static final ExtensionLoader FLAC_EXTENSION_LOADER =
@ -569,6 +572,9 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
extractors.add(new HeifExtractor());
}
break;
case FileTypes.AVIF:
extractors.add(new AvifExtractor());
break;
case FileTypes.WEBVTT:
case FileTypes.UNKNOWN:
default:

View File

@ -0,0 +1,85 @@
/*
* Copyright 2024 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.avif;
import androidx.media3.common.C;
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.SingleSampleExtractor;
import java.io.IOException;
/** Extracts data from the AVIF (.avif) container format. */
@UnstableApi
public final class AvifExtractor implements Extractor {
// Specification reference: ISO/IEC 23008-12:2022
private static final int AVIF_FILE_SIGNATURE_PART_1 = 0x66747970;
private static final int AVIF_FILE_SIGNATURE_PART_2 = 0x61766966;
private static final int FILE_SIGNATURE_SEGMENT_LENGTH = 4;
private final ParsableByteArray scratch;
private final SingleSampleExtractor imageExtractor;
/** Creates an instance. */
public AvifExtractor() {
scratch = new ParsableByteArray(FILE_SIGNATURE_SEGMENT_LENGTH);
imageExtractor =
new SingleSampleExtractor(
/* fileSignature= */ C.INDEX_UNSET,
/* fileSignatureLength= */ C.LENGTH_UNSET,
MimeTypes.IMAGE_AVIF);
}
@Override
public boolean sniff(ExtractorInput input) throws IOException {
input.advancePeekPosition(4);
return readAndCompareFourBytes(input, AVIF_FILE_SIGNATURE_PART_1)
&& readAndCompareFourBytes(input, AVIF_FILE_SIGNATURE_PART_2);
}
@Override
public void init(ExtractorOutput output) {
imageExtractor.init(output);
}
@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, timeUs);
}
@Override
public void release() {
// Do nothing.
}
private boolean readAndCompareFourBytes(ExtractorInput input, int bytesToCompare)
throws IOException {
scratch.reset(/* limit= */ FILE_SIGNATURE_SEGMENT_LENGTH);
input.peekFully(scratch.getData(), /* offset= */ 0, FILE_SIGNATURE_SEGMENT_LENGTH);
return scratch.readUnsignedInt() == bytesToCompare;
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2024 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.avif;
import androidx.media3.common.util.NonNullApi;

View File

@ -22,6 +22,7 @@ import android.net.Uri;
import androidx.media3.common.MimeTypes;
import androidx.media3.extractor.amr.AmrExtractor;
import androidx.media3.extractor.avi.AviExtractor;
import androidx.media3.extractor.avif.AvifExtractor;
import androidx.media3.extractor.bmp.BmpExtractor;
import androidx.media3.extractor.flac.FlacExtractor;
import androidx.media3.extractor.flv.FlvExtractor;
@ -82,7 +83,8 @@ public final class DefaultExtractorsFactoryTest {
PngExtractor.class,
WebpExtractor.class,
BmpExtractor.class,
HeifExtractor.class)
HeifExtractor.class,
AvifExtractor.class)
.inOrder();
}
@ -129,7 +131,8 @@ public final class DefaultExtractorsFactoryTest {
PngExtractor.class,
WebpExtractor.class,
BmpExtractor.class,
HeifExtractor.class)
HeifExtractor.class,
AvifExtractor.class)
.inOrder();
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2024 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.avif;
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 AvifExtractor}. */
@RunWith(ParameterizedRobolectricTestRunner.class)
public final class AvifExtractorTest {
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
public static ImmutableList<ExtractorAsserts.SimulationConfig> params() {
return ExtractorAsserts.configs();
}
@ParameterizedRobolectricTestRunner.Parameter
public ExtractorAsserts.SimulationConfig simulationConfig;
@Test
public void sampleAvif() throws Exception {
ExtractorAsserts.assertBehavior(
AvifExtractor::new, "media/avif/white-1x1.avif", simulationConfig);
}
}

View File

@ -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 = 295
sample count = 1
format 0:
sampleMimeType = image/avif
sample 0:
time = 0
flags = 1
data = length 295, hash 24CB12
tracksEnded = true

View File

@ -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 = 295
sample count = 1
format 0:
sampleMimeType = image/avif
sample 0:
time = 0
flags = 1
data = length 295, hash 24CB12
tracksEnded = true

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

View File

@ -0,0 +1,5 @@
ImageOutput:
rendered image count = 1
image output #1:
presentationTimeUs = 0
bitmap hash = 443865884

View File

@ -0,0 +1,8 @@
ImageOutput:
rendered image count = 2
image output #1:
presentationTimeUs = 0
bitmap hash = -389047680
image output #2:
presentationTimeUs = 0
bitmap hash = 443865884

View File

@ -0,0 +1,11 @@
ImageOutput:
rendered image count = 3
image output #1:
presentationTimeUs = 0
bitmap hash = -389047680
image output #2:
presentationTimeUs = 0
bitmap hash = 1367007828
image output #3:
presentationTimeUs = 0
bitmap hash = 443865884

View File

@ -0,0 +1,8 @@
ImageOutput:
rendered image count = 2
image output #1:
presentationTimeUs = 0
bitmap hash = -389047680
image output #2:
presentationTimeUs = 0
bitmap hash = 1367007828

View File

@ -0,0 +1,5 @@
ImageOutput:
rendered image count = 1
image output #1:
presentationTimeUs = 0
bitmap hash = -389047680

View File

@ -0,0 +1,8 @@
ImageOutput:
rendered image count = 2
image output #1:
presentationTimeUs = 0
bitmap hash = -1851534335
image output #2:
presentationTimeUs = 0
bitmap hash = 443865884

View File

@ -0,0 +1,11 @@
ImageOutput:
rendered image count = 3
image output #1:
presentationTimeUs = 0
bitmap hash = -1851534335
image output #2:
presentationTimeUs = 0
bitmap hash = -389047680
image output #3:
presentationTimeUs = 0
bitmap hash = 443865884

View File

@ -0,0 +1,14 @@
ImageOutput:
rendered image count = 4
image output #1:
presentationTimeUs = 0
bitmap hash = -1851534335
image output #2:
presentationTimeUs = 0
bitmap hash = -389047680
image output #3:
presentationTimeUs = 0
bitmap hash = 1367007828
image output #4:
presentationTimeUs = 0
bitmap hash = 443865884

View File

@ -0,0 +1,11 @@
ImageOutput:
rendered image count = 3
image output #1:
presentationTimeUs = 0
bitmap hash = -1851534335
image output #2:
presentationTimeUs = 0
bitmap hash = -389047680
image output #3:
presentationTimeUs = 0
bitmap hash = 1367007828

View File

@ -0,0 +1,8 @@
ImageOutput:
rendered image count = 2
image output #1:
presentationTimeUs = 0
bitmap hash = -1851534335
image output #2:
presentationTimeUs = 0
bitmap hash = -389047680

View File

@ -0,0 +1,11 @@
ImageOutput:
rendered image count = 3
image output #1:
presentationTimeUs = 0
bitmap hash = -1851534335
image output #2:
presentationTimeUs = 0
bitmap hash = 1367007828
image output #3:
presentationTimeUs = 0
bitmap hash = 443865884

View File

@ -0,0 +1,8 @@
ImageOutput:
rendered image count = 2
image output #1:
presentationTimeUs = 0
bitmap hash = -1851534335
image output #2:
presentationTimeUs = 0
bitmap hash = 1367007828

View File

@ -0,0 +1,5 @@
ImageOutput:
rendered image count = 1
image output #1:
presentationTimeUs = 0
bitmap hash = -1851534335

View File

@ -0,0 +1,8 @@
ImageOutput:
rendered image count = 2
image output #1:
presentationTimeUs = 0
bitmap hash = 1367007828
image output #2:
presentationTimeUs = 0
bitmap hash = 443865884

View File

@ -0,0 +1,5 @@
ImageOutput:
rendered image count = 1
image output #1:
presentationTimeUs = 0
bitmap hash = 1367007828