mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
JpegExtractor: support JFIF segment preceding Exif segment
#minor-release PiperOrigin-RevId: 364561115
This commit is contained in:
parent
3fbfc0ec0e
commit
2c76bc5a4c
9 changed files with 184 additions and 9 deletions
|
|
@ -47,6 +47,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.
|
||||||
|
|
@ -99,9 +101,8 @@
|
||||||
SmoothStreaming.
|
SmoothStreaming.
|
||||||
* 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 {
|
||||||
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 {
|
||||||
@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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,14 @@ public final class JpegExtractorTest {
|
||||||
JpegExtractor::new, "media/jpeg/pixel-motion-photo-shortened.jpg", simulationConfig);
|
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
|
@Test
|
||||||
public void samplePixelMotionPhotoVideoRemovedShortened() throws Exception {
|
public void samplePixelMotionPhotoVideoRemovedShortened() throws Exception {
|
||||||
ExtractorAsserts.assertBehavior(
|
ExtractorAsserts.assertBehavior(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
BIN
testdata/src/test/assets/media/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg
vendored
Normal file
BIN
testdata/src/test/assets/media/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Loading…
Reference in a new issue