Commit aa250376 by kimvde Committed by marcbaechinger

JpegExtractor: support JFIF segment preceding Exif segment

#minor-release

PiperOrigin-RevId: 364561115
parent 65ab0085
...@@ -43,6 +43,8 @@ ...@@ -43,6 +43,8 @@
* Extractors: * Extractors:
* Add support for `GContainer` and `GContainerItem` XMP namespace prefixes * Add support for `GContainer` and `GContainerItem` XMP namespace prefixes
in JPEG motion photo parsing. in JPEG motion photo parsing.
* Allow JFIF APP0 marker segment preceding Exif APP1 segment in
`JpegExtractor`.
* Remove deprecated symbols: * Remove deprecated symbols:
* Remove `Player.DefaultEventListener`. Use `Player.EventListener` * Remove `Player.DefaultEventListener`. Use `Player.EventListener`
instead. instead.
...@@ -77,9 +79,8 @@ ...@@ -77,9 +79,8 @@
media item and so that it is not triggered after a timeline change. media item and so that it is not triggered after a timeline change.
* IMA extension: * IMA extension:
* Fix error caused by `AdPlaybackState` ad group times being cleared, * Fix error caused by `AdPlaybackState` ad group times being cleared,
which can occur if the `ImaAdsLoader` is released while an ad is which can occur if the `ImaAdsLoader` is released while an ad is pending
pending loading loading ([#8693](https://github.com/google/ExoPlayer/issues/8693)).
([#8693](https://github.com/google/ExoPlayer/issues/8693)).
* Upgrade IMA SDK dependency to 3.22.3, fixing an issue with * Upgrade IMA SDK dependency to 3.22.3, fixing an issue with
`NullPointerExceptions` within `WebView` callbacks `NullPointerExceptions` within `WebView` callbacks
([#8447](https://github.com/google/ExoPlayer/issues/8447)). ([#8447](https://github.com/google/ExoPlayer/issues/8447)).
......
...@@ -60,10 +60,11 @@ public final class JpegExtractor implements Extractor { ...@@ -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_READING_MOTION_PHOTO_VIDEO = 5;
private static final int STATE_ENDED = 6; 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 long EXIF_HEADER = 0x45786966; // Exif
private static final int MARKER_SOI = 0xFFD8; // Start of image marker 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_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 int MARKER_APP1 = 0xFFE1; // Application data 1 marker
private static final String HEADER_XMP_APP1 = "http://ns.adobe.com/xap/1.0/"; private static final String HEADER_XMP_APP1 = "http://ns.adobe.com/xap/1.0/";
...@@ -85,21 +86,33 @@ public final class JpegExtractor implements Extractor { ...@@ -85,21 +86,33 @@ public final class JpegExtractor implements Extractor {
@Nullable private MotionPhotoMetadata motionPhotoMetadata; @Nullable private MotionPhotoMetadata motionPhotoMetadata;
private @MonotonicNonNull ExtractorInput lastExtractorInput; private @MonotonicNonNull ExtractorInput lastExtractorInput;
private @MonotonicNonNull StartOffsetExtractorInput mp4ExtractorStartOffsetExtractorInput; private @MonotonicNonNull StartOffsetExtractorInput mp4ExtractorStartOffsetExtractorInput;
private @MonotonicNonNull Mp4Extractor mp4Extractor; @Nullable private Mp4Extractor mp4Extractor;
public JpegExtractor() { public JpegExtractor() {
scratch = new ParsableByteArray(JPEG_EXIF_HEADER_LENGTH); scratch = new ParsableByteArray(EXIF_ID_CODE_LENGTH);
mp4StartPosition = C.POSITION_UNSET; mp4StartPosition = C.POSITION_UNSET;
} }
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException { 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. // 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 (peekMarker(input) != MARKER_SOI) {
if (scratch.readUnsignedShort() != MARKER_SOI || scratch.readUnsignedShort() != MARKER_APP1) {
return false; 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 return scratch.readUnsignedInt() == EXIF_HEADER && scratch.readUnsignedShort() == 0; // Exif\0\0
} }
...@@ -152,6 +165,7 @@ public final class JpegExtractor implements Extractor { ...@@ -152,6 +165,7 @@ public final class JpegExtractor implements Extractor {
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
if (position == 0) { if (position == 0) {
state = STATE_READING_MARKER; state = STATE_READING_MARKER;
mp4Extractor = null;
} else if (state == STATE_READING_MOTION_PHOTO_VIDEO) { } else if (state == STATE_READING_MOTION_PHOTO_VIDEO) {
checkNotNull(mp4Extractor).seek(position, timeUs); checkNotNull(mp4Extractor).seek(position, timeUs);
} }
...@@ -164,6 +178,19 @@ public final class JpegExtractor implements Extractor { ...@@ -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 { private void readMarker(ExtractorInput input) throws IOException {
scratch.reset(/* limit= */ 2); scratch.reset(/* limit= */ 2);
input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 2); input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 2);
......
...@@ -46,6 +46,14 @@ public final class JpegExtractorTest { ...@@ -46,6 +46,14 @@ public final class JpegExtractorTest {
} }
@Test @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 { public void samplePixelMotionPhotoVideoRemovedShortened() throws Exception {
ExtractorAsserts.assertBehavior( ExtractorAsserts.assertBehavior(
JpegExtractor::new, JpegExtractor::new,
......
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
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
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
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
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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment