From 2c76bc5a4cabac7f707a1d2d99a9dde148d789d3 Mon Sep 17 00:00:00 2001 From: kimvde Date: Tue, 23 Mar 2021 15:18:12 +0000 Subject: [PATCH] JpegExtractor: support JFIF segment preceding Exif segment #minor-release PiperOrigin-RevId: 364561115 --- RELEASENOTES.md | 7 ++-- .../extractor/jpeg/JpegExtractor.java | 39 +++++++++++++++--- .../extractor/jpeg/JpegExtractorTest.java | 8 ++++ ...on-photo-jfif-segment-shortened.jpg.0.dump | 32 ++++++++++++++ ...on-photo-jfif-segment-shortened.jpg.1.dump | 32 ++++++++++++++ ...on-photo-jfif-segment-shortened.jpg.2.dump | 32 ++++++++++++++ ...on-photo-jfif-segment-shortened.jpg.3.dump | 32 ++++++++++++++ ...-segment-shortened.jpg.unknown_length.dump | 11 +++++ ...el-motion-photo-jfif-segment-shortened.jpg | Bin 0 -> 11063 bytes 9 files changed, 184 insertions(+), 9 deletions(-) create mode 100644 testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.0.dump create mode 100644 testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.1.dump create mode 100644 testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.2.dump create mode 100644 testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.3.dump create mode 100644 testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg.unknown_length.dump create mode 100644 testdata/src/test/assets/media/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg 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 0000000000000000000000000000000000000000..23b27d420fb461af4cb8d06b8341d7359911cec0 GIT binary patch literal 11063 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 T6mlfS1ncN)XcIKRzdimxphGf} literal 0 HcmV?d00001