diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0d62e8a798..8bb30fd4be 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -47,6 +47,8 @@ * Extractors: * Add support for `GContainer` and `GContainerItem` XMP namespace prefixes in JPEG motion photo parsing. + * Allow JFIF APP0 marker segment preceding Exif APP1 segment in + `JpegExtractor`. * Remove deprecated symbols: * Remove `Player.DefaultEventListener`. Use `Player.EventListener` instead. @@ -99,9 +101,8 @@ SmoothStreaming. * IMA extension: * Fix error caused by `AdPlaybackState` ad group times being cleared, - which can occur if the `ImaAdsLoader` is released while an ad is - pending loading - ([#8693](https://github.com/google/ExoPlayer/issues/8693)). + which can occur if the `ImaAdsLoader` is released while an ad is pending + loading ([#8693](https://github.com/google/ExoPlayer/issues/8693)). * Upgrade IMA SDK dependency to 3.22.3, fixing an issue with `NullPointerExceptions` within `WebView` callbacks ([#8447](https://github.com/google/ExoPlayer/issues/8447)). diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/JpegExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/JpegExtractor.java index 3dbbc85d84..777126905a 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/JpegExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/JpegExtractor.java @@ -60,10 +60,11 @@ public final class JpegExtractor implements Extractor { private static final int STATE_READING_MOTION_PHOTO_VIDEO = 5; private static final int STATE_ENDED = 6; - private static final int JPEG_EXIF_HEADER_LENGTH = 12; + private static final int EXIF_ID_CODE_LENGTH = 6; private static final long EXIF_HEADER = 0x45786966; // Exif private static final int MARKER_SOI = 0xFFD8; // Start of image marker private static final int MARKER_SOS = 0xFFDA; // Start of scan (image data) marker + private static final int MARKER_APP0 = 0xFFE0; // Application data 0 marker private static final int MARKER_APP1 = 0xFFE1; // Application data 1 marker private static final String HEADER_XMP_APP1 = "http://ns.adobe.com/xap/1.0/"; @@ -85,21 +86,33 @@ public final class JpegExtractor implements Extractor { @Nullable private MotionPhotoMetadata motionPhotoMetadata; private @MonotonicNonNull ExtractorInput lastExtractorInput; private @MonotonicNonNull StartOffsetExtractorInput mp4ExtractorStartOffsetExtractorInput; - private @MonotonicNonNull Mp4Extractor mp4Extractor; + @Nullable private Mp4Extractor mp4Extractor; public JpegExtractor() { - scratch = new ParsableByteArray(JPEG_EXIF_HEADER_LENGTH); + scratch = new ParsableByteArray(EXIF_ID_CODE_LENGTH); mp4StartPosition = C.POSITION_UNSET; } @Override public boolean sniff(ExtractorInput input) throws IOException { // See ITU-T.81 (1992) subsection B.1.1.3 and Exif version 2.2 (2002) subsection 4.5.4. - input.peekFully(scratch.getData(), /* offset= */ 0, JPEG_EXIF_HEADER_LENGTH); - if (scratch.readUnsignedShort() != MARKER_SOI || scratch.readUnsignedShort() != MARKER_APP1) { + if (peekMarker(input) != MARKER_SOI) { return false; } - scratch.skipBytes(2); // Unused segment length + marker = peekMarker(input); + // Even though JFIF and Exif standards are incompatible in theory, Exif files often contain a + // JFIF APP0 marker segment preceding the Exif APP1 marker segment. Skip the JFIF segment if + // present. + if (marker == MARKER_APP0) { + advancePeekPositionToNextSegment(input); + marker = peekMarker(input); + } + if (marker != MARKER_APP1) { + return false; + } + input.advancePeekPosition(2); // Unused segment length + scratch.reset(/* limit= */ EXIF_ID_CODE_LENGTH); + input.peekFully(scratch.getData(), /* offset= */ 0, EXIF_ID_CODE_LENGTH); return scratch.readUnsignedInt() == EXIF_HEADER && scratch.readUnsignedShort() == 0; // Exif\0\0 } @@ -152,6 +165,7 @@ public final class JpegExtractor implements Extractor { public void seek(long position, long timeUs) { if (position == 0) { state = STATE_READING_MARKER; + mp4Extractor = null; } else if (state == STATE_READING_MOTION_PHOTO_VIDEO) { checkNotNull(mp4Extractor).seek(position, timeUs); } @@ -164,6 +178,19 @@ public final class JpegExtractor implements Extractor { } } + private int peekMarker(ExtractorInput input) throws IOException { + scratch.reset(/* limit= */ 2); + input.peekFully(scratch.getData(), /* offset= */ 0, /* length= */ 2); + return scratch.readUnsignedShort(); + } + + private void advancePeekPositionToNextSegment(ExtractorInput input) throws IOException { + scratch.reset(/* limit= */ 2); + input.peekFully(scratch.getData(), /* offset= */ 0, /* length= */ 2); + int segmentLength = scratch.readUnsignedShort() - 2; + input.advancePeekPosition(segmentLength); + } + private void readMarker(ExtractorInput input) throws IOException { scratch.reset(/* limit= */ 2); input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 2); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/jpeg/JpegExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/jpeg/JpegExtractorTest.java index 9166f335a7..8e5556bf81 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/jpeg/JpegExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/jpeg/JpegExtractorTest.java @@ -45,6 +45,14 @@ public final class JpegExtractorTest { JpegExtractor::new, "media/jpeg/pixel-motion-photo-shortened.jpg", simulationConfig); } + @Test + public void samplePixelMotionPhotoJfifSegmentShortened() throws Exception { + ExtractorAsserts.assertBehavior( + JpegExtractor::new, + "media/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg", + simulationConfig); + } + @Test public void samplePixelMotionPhotoVideoRemovedShortened() throws Exception { ExtractorAsserts.assertBehavior( diff --git a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.0.dump b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.0.dump new file mode 100644 index 0000000000..80ac03b125 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.0.dump @@ -0,0 +1,32 @@ +seekMap: + isSeekable = true + duration = 867000 + getPosition(0) = [[timeUs=0, position=6425]] + getPosition(1) = [[timeUs=0, position=6425]] + getPosition(433500) = [[timeUs=0, position=6425]] + getPosition(867000) = [[timeUs=0, position=6425]] +numberOfTracks = 2 +track 0: + total output bytes = 3865 + sample count = 1 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64000A + maxInputSize = 3895 + width = 180 + height = 120 + pixelWidthHeightRatio = 0.5 + initializationData: + data = length 32, hash 1F3D6E87 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 536870913 + data = length 3865, hash 5B0DEEC7 +track 1024: + total output bytes = 0 + sample count = 0 + format 0: + metadata = entries=[Motion photo metadata: photoStartPosition=0, photoSize=6377, photoPresentationTimestampUs=1232840, videoStartPosition=6377, videoSize=4686] +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.1.dump b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.1.dump new file mode 100644 index 0000000000..80ac03b125 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.1.dump @@ -0,0 +1,32 @@ +seekMap: + isSeekable = true + duration = 867000 + getPosition(0) = [[timeUs=0, position=6425]] + getPosition(1) = [[timeUs=0, position=6425]] + getPosition(433500) = [[timeUs=0, position=6425]] + getPosition(867000) = [[timeUs=0, position=6425]] +numberOfTracks = 2 +track 0: + total output bytes = 3865 + sample count = 1 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64000A + maxInputSize = 3895 + width = 180 + height = 120 + pixelWidthHeightRatio = 0.5 + initializationData: + data = length 32, hash 1F3D6E87 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 536870913 + data = length 3865, hash 5B0DEEC7 +track 1024: + total output bytes = 0 + sample count = 0 + format 0: + metadata = entries=[Motion photo metadata: photoStartPosition=0, photoSize=6377, photoPresentationTimestampUs=1232840, videoStartPosition=6377, videoSize=4686] +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.2.dump b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.2.dump new file mode 100644 index 0000000000..80ac03b125 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.2.dump @@ -0,0 +1,32 @@ +seekMap: + isSeekable = true + duration = 867000 + getPosition(0) = [[timeUs=0, position=6425]] + getPosition(1) = [[timeUs=0, position=6425]] + getPosition(433500) = [[timeUs=0, position=6425]] + getPosition(867000) = [[timeUs=0, position=6425]] +numberOfTracks = 2 +track 0: + total output bytes = 3865 + sample count = 1 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64000A + maxInputSize = 3895 + width = 180 + height = 120 + pixelWidthHeightRatio = 0.5 + initializationData: + data = length 32, hash 1F3D6E87 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 536870913 + data = length 3865, hash 5B0DEEC7 +track 1024: + total output bytes = 0 + sample count = 0 + format 0: + metadata = entries=[Motion photo metadata: photoStartPosition=0, photoSize=6377, photoPresentationTimestampUs=1232840, videoStartPosition=6377, videoSize=4686] +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.3.dump b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.3.dump new file mode 100644 index 0000000000..80ac03b125 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.3.dump @@ -0,0 +1,32 @@ +seekMap: + isSeekable = true + duration = 867000 + getPosition(0) = [[timeUs=0, position=6425]] + getPosition(1) = [[timeUs=0, position=6425]] + getPosition(433500) = [[timeUs=0, position=6425]] + getPosition(867000) = [[timeUs=0, position=6425]] +numberOfTracks = 2 +track 0: + total output bytes = 3865 + sample count = 1 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64000A + maxInputSize = 3895 + width = 180 + height = 120 + pixelWidthHeightRatio = 0.5 + initializationData: + data = length 32, hash 1F3D6E87 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 536870913 + data = length 3865, hash 5B0DEEC7 +track 1024: + total output bytes = 0 + sample count = 0 + format 0: + metadata = entries=[Motion photo metadata: photoStartPosition=0, photoSize=6377, photoPresentationTimestampUs=1232840, videoStartPosition=6377, videoSize=4686] +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.unknown_length.dump b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.unknown_length.dump new file mode 100644 index 0000000000..bb201ef2ef --- /dev/null +++ b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.unknown_length.dump @@ -0,0 +1,11 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 1024: + total output bytes = 0 + sample count = 0 + format 0: + metadata = entries=[] +tracksEnded = true diff --git a/testdata/src/test/assets/media/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg b/testdata/src/test/assets/media/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg new file mode 100644 index 0000000000..23b27d420f Binary files /dev/null and b/testdata/src/test/assets/media/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg differ