mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Add support for JPEG motion photo extraction
PiperOrigin-RevId: 351752989
This commit is contained in:
parent
789a211d53
commit
9b062053fa
25 changed files with 1075 additions and 6 deletions
|
|
@ -164,10 +164,10 @@
|
||||||
containers.
|
containers.
|
||||||
* Fix CEA-708 anchor positioning
|
* Fix CEA-708 anchor positioning
|
||||||
([#1807](https://github.com/google/ExoPlayer/issues/1807)).
|
([#1807](https://github.com/google/ExoPlayer/issues/1807)).
|
||||||
* Metadata retriever:
|
|
||||||
* Parse Google Photos HEIC motion photos metadata.
|
|
||||||
* Data sources:
|
* Data sources:
|
||||||
* Use the user agent of the underlying network stack by default.
|
* Use the user agent of the underlying network stack by default.
|
||||||
|
* Metadata retriever:
|
||||||
|
* Parse Google Photos HEIC and JPEG motion photo metadata.
|
||||||
* IMA extension:
|
* IMA extension:
|
||||||
* Add support for playback of ads in playlists
|
* Add support for playback of ads in playlists
|
||||||
([#3750](https://github.com/google/ExoPlayer/issues/3750)).
|
([#3750](https://github.com/google/ExoPlayer/issues/3750)).
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,13 @@ public final class FileTypes {
|
||||||
/**
|
/**
|
||||||
* File types. One of {@link #UNKNOWN}, {@link #AC3}, {@link #AC4}, {@link #ADTS}, {@link #AMR},
|
* File types. One of {@link #UNKNOWN}, {@link #AC3}, {@link #AC4}, {@link #ADTS}, {@link #AMR},
|
||||||
* {@link #FLAC}, {@link #FLV}, {@link #MATROSKA}, {@link #MP3}, {@link #MP4}, {@link #OGG},
|
* {@link #FLAC}, {@link #FLV}, {@link #MATROSKA}, {@link #MP3}, {@link #MP4}, {@link #OGG},
|
||||||
* {@link #PS}, {@link #TS}, {@link #WAV} and {@link #WEBVTT}.
|
* {@link #PS}, {@link #TS}, {@link #WAV}, {@link #WEBVTT} and {@link #JPEG}.
|
||||||
*/
|
*/
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT})
|
@IntDef({
|
||||||
|
UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG
|
||||||
|
})
|
||||||
public @interface Type {}
|
public @interface Type {}
|
||||||
/** Unknown file type. */
|
/** Unknown file type. */
|
||||||
public static final int UNKNOWN = -1;
|
public static final int UNKNOWN = -1;
|
||||||
|
|
@ -69,6 +71,8 @@ public final class FileTypes {
|
||||||
public static final int WAV = 12;
|
public static final int WAV = 12;
|
||||||
/** File type for the WebVTT format. */
|
/** File type for the WebVTT format. */
|
||||||
public static final int WEBVTT = 13;
|
public static final int WEBVTT = 13;
|
||||||
|
/** File type for the JPEG format. */
|
||||||
|
public static final int JPEG = 14;
|
||||||
|
|
||||||
@VisibleForTesting /* package */ static final String HEADER_CONTENT_TYPE = "Content-Type";
|
@VisibleForTesting /* package */ static final String HEADER_CONTENT_TYPE = "Content-Type";
|
||||||
|
|
||||||
|
|
@ -99,6 +103,8 @@ public final class FileTypes {
|
||||||
private static final String EXTENSION_WAVE = ".wave";
|
private static final String EXTENSION_WAVE = ".wave";
|
||||||
private static final String EXTENSION_VTT = ".vtt";
|
private static final String EXTENSION_VTT = ".vtt";
|
||||||
private static final String EXTENSION_WEBVTT = ".webvtt";
|
private static final String EXTENSION_WEBVTT = ".webvtt";
|
||||||
|
private static final String EXTENSION_JPG = ".jpg";
|
||||||
|
private static final String EXTENSION_JPEG = ".jpeg";
|
||||||
|
|
||||||
private FileTypes() {}
|
private FileTypes() {}
|
||||||
|
|
||||||
|
|
@ -159,6 +165,8 @@ public final class FileTypes {
|
||||||
return FileTypes.WAV;
|
return FileTypes.WAV;
|
||||||
case MimeTypes.TEXT_VTT:
|
case MimeTypes.TEXT_VTT:
|
||||||
return FileTypes.WEBVTT;
|
return FileTypes.WEBVTT;
|
||||||
|
case MimeTypes.IMAGE_JPEG:
|
||||||
|
return FileTypes.JPEG;
|
||||||
default:
|
default:
|
||||||
return FileTypes.UNKNOWN;
|
return FileTypes.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
@ -219,6 +227,8 @@ public final class FileTypes {
|
||||||
return FileTypes.WAV;
|
return FileTypes.WAV;
|
||||||
} else if (filename.endsWith(EXTENSION_VTT) || filename.endsWith(EXTENSION_WEBVTT)) {
|
} else if (filename.endsWith(EXTENSION_VTT) || filename.endsWith(EXTENSION_WEBVTT)) {
|
||||||
return FileTypes.WEBVTT;
|
return FileTypes.WEBVTT;
|
||||||
|
} else if (filename.endsWith(EXTENSION_JPG) || filename.endsWith(EXTENSION_JPEG)) {
|
||||||
|
return FileTypes.JPEG;
|
||||||
} else {
|
} else {
|
||||||
return FileTypes.UNKNOWN;
|
return FileTypes.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ public final class MimeTypes {
|
||||||
public static final String BASE_TYPE_VIDEO = "video";
|
public static final String BASE_TYPE_VIDEO = "video";
|
||||||
public static final String BASE_TYPE_AUDIO = "audio";
|
public static final String BASE_TYPE_AUDIO = "audio";
|
||||||
public static final String BASE_TYPE_TEXT = "text";
|
public static final String BASE_TYPE_TEXT = "text";
|
||||||
|
public static final String BASE_TYPE_IMAGE = "image";
|
||||||
public static final String BASE_TYPE_APPLICATION = "application";
|
public static final String BASE_TYPE_APPLICATION = "application";
|
||||||
|
|
||||||
public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4";
|
public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4";
|
||||||
|
|
@ -113,6 +114,8 @@ public final class MimeTypes {
|
||||||
public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy";
|
public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy";
|
||||||
public static final String APPLICATION_AIT = BASE_TYPE_APPLICATION + "/vnd.dvb.ait";
|
public static final String APPLICATION_AIT = BASE_TYPE_APPLICATION + "/vnd.dvb.ait";
|
||||||
|
|
||||||
|
public static final String IMAGE_JPEG = BASE_TYPE_IMAGE + "/jpeg";
|
||||||
|
|
||||||
private static final ArrayList<CustomMimeType> customMimeTypes = new ArrayList<>();
|
private static final ArrayList<CustomMimeType> customMimeTypes = new ArrayList<>();
|
||||||
|
|
||||||
private static final Pattern MP4A_RFC_6381_CODEC_PATTERN =
|
private static final Pattern MP4A_RFC_6381_CODEC_PATTERN =
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
|
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
|
import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
|
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.jpeg.JpegExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
|
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
||||||
|
|
@ -69,12 +70,15 @@ import java.util.Map;
|
||||||
* generally include a FLAC decoder before API 27. This can be worked around by using
|
* generally include a FLAC decoder before API 27. This can be worked around by using
|
||||||
* the FLAC extension or the FFmpeg extension.
|
* the FLAC extension or the FFmpeg extension.
|
||||||
* </ul>
|
* </ul>
|
||||||
|
* <li>JPEG ({@link JpegExtractor})
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public final class DefaultExtractorsFactory implements ExtractorsFactory {
|
public final class DefaultExtractorsFactory implements ExtractorsFactory {
|
||||||
|
|
||||||
// Extractors order is optimized according to
|
// Extractors order is optimized according to
|
||||||
// https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ.
|
// https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ.
|
||||||
|
// The JPEG extractor appears after audio/video extractors because we expect audio/video input to
|
||||||
|
// be more common.
|
||||||
private static final int[] DEFAULT_EXTRACTOR_ORDER =
|
private static final int[] DEFAULT_EXTRACTOR_ORDER =
|
||||||
new int[] {
|
new int[] {
|
||||||
FileTypes.FLV,
|
FileTypes.FLV,
|
||||||
|
|
@ -90,6 +94,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
|
||||||
FileTypes.AC3,
|
FileTypes.AC3,
|
||||||
FileTypes.AC4,
|
FileTypes.AC4,
|
||||||
FileTypes.MP3,
|
FileTypes.MP3,
|
||||||
|
FileTypes.JPEG,
|
||||||
};
|
};
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
@ -382,6 +387,11 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
|
||||||
case FileTypes.WAV:
|
case FileTypes.WAV:
|
||||||
extractors.add(new WavExtractor());
|
extractors.add(new WavExtractor());
|
||||||
break;
|
break;
|
||||||
|
case FileTypes.JPEG:
|
||||||
|
extractors.add(new JpegExtractor());
|
||||||
|
break;
|
||||||
|
case FileTypes.WEBVTT:
|
||||||
|
case FileTypes.UNKNOWN:
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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 com.google.android.exoplayer2.extractor;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/** An overridable {@link ExtractorInput} implementation forwarding all methods to another input. */
|
||||||
|
public class ForwardingExtractorInput implements ExtractorInput {
|
||||||
|
|
||||||
|
private final ExtractorInput input;
|
||||||
|
|
||||||
|
public ForwardingExtractorInput(ExtractorInput input) {
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] target, int offset, int length) throws IOException {
|
||||||
|
return input.read(target, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
|
||||||
|
throws IOException {
|
||||||
|
return input.readFully(target, offset, length, allowEndOfInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFully(byte[] target, int offset, int length) throws IOException {
|
||||||
|
input.readFully(target, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int skip(int length) throws IOException {
|
||||||
|
return input.skip(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean skipFully(int length, boolean allowEndOfInput) throws IOException {
|
||||||
|
return input.skipFully(length, allowEndOfInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void skipFully(int length) throws IOException {
|
||||||
|
input.skipFully(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int peek(byte[] target, int offset, int length) throws IOException {
|
||||||
|
return input.peek(target, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
|
||||||
|
throws IOException {
|
||||||
|
return input.peekFully(target, offset, length, allowEndOfInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void peekFully(byte[] target, int offset, int length) throws IOException {
|
||||||
|
input.peekFully(target, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean advancePeekPosition(int length, boolean allowEndOfInput) throws IOException {
|
||||||
|
return input.advancePeekPosition(length, allowEndOfInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void advancePeekPosition(int length) throws IOException {
|
||||||
|
input.advancePeekPosition(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resetPeekPosition() {
|
||||||
|
input.resetPeekPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getPeekPosition() {
|
||||||
|
return input.getPeekPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getPosition() {
|
||||||
|
return input.getPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLength() {
|
||||||
|
return input.getLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Throwable> void setRetryPosition(long position, E e) throws E {
|
||||||
|
input.setRetryPosition(position, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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 com.google.android.exoplayer2.extractor.jpeg;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
|
import com.google.android.exoplayer2.metadata.mp4.MotionPhotoMetadata;
|
||||||
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
|
/** Extracts JPEG image using the Exif format. */
|
||||||
|
public final class JpegExtractor implements Extractor {
|
||||||
|
|
||||||
|
/** Parser states. */
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@IntDef({
|
||||||
|
STATE_READING_MARKER,
|
||||||
|
STATE_READING_SEGMENT_LENGTH,
|
||||||
|
STATE_READING_SEGMENT,
|
||||||
|
STATE_SNIFFING_MOTION_PHOTO_VIDEO,
|
||||||
|
STATE_ENDED,
|
||||||
|
})
|
||||||
|
private @interface State {}
|
||||||
|
|
||||||
|
private static final int STATE_READING_MARKER = 0;
|
||||||
|
private static final int STATE_READING_SEGMENT_LENGTH = 1;
|
||||||
|
private static final int STATE_READING_SEGMENT = 2;
|
||||||
|
private static final int STATE_SNIFFING_MOTION_PHOTO_VIDEO = 4;
|
||||||
|
private static final int STATE_ENDED = 5;
|
||||||
|
|
||||||
|
private static final int JPEG_EXIF_HEADER_LENGTH = 12;
|
||||||
|
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_APP1 = 0xFFE1; // Application data 1 marker
|
||||||
|
private static final String HEADER_XMP_APP1 = "http://ns.adobe.com/xap/1.0/";
|
||||||
|
|
||||||
|
private final ParsableByteArray scratch;
|
||||||
|
|
||||||
|
private @MonotonicNonNull ExtractorOutput extractorOutput;
|
||||||
|
|
||||||
|
@State private int state;
|
||||||
|
private int marker;
|
||||||
|
private int segmentLength;
|
||||||
|
|
||||||
|
@Nullable private MotionPhotoMetadata motionPhotoMetadata;
|
||||||
|
|
||||||
|
public JpegExtractor() {
|
||||||
|
scratch = new ParsableByteArray(JPEG_EXIF_HEADER_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
scratch.skipBytes(2); // Unused segment length
|
||||||
|
return scratch.readUnsignedInt() == EXIF_HEADER && scratch.readUnsignedShort() == 0; // Exif\0\0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ExtractorOutput output) {
|
||||||
|
extractorOutput = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@ReadResult
|
||||||
|
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
|
||||||
|
switch (state) {
|
||||||
|
case STATE_READING_MARKER:
|
||||||
|
readMarker(input);
|
||||||
|
return RESULT_CONTINUE;
|
||||||
|
case STATE_READING_SEGMENT_LENGTH:
|
||||||
|
readSegmentLength(input);
|
||||||
|
return RESULT_CONTINUE;
|
||||||
|
case STATE_READING_SEGMENT:
|
||||||
|
readSegment(input);
|
||||||
|
return RESULT_CONTINUE;
|
||||||
|
case STATE_SNIFFING_MOTION_PHOTO_VIDEO:
|
||||||
|
if (input.getPosition() != checkNotNull(motionPhotoMetadata).videoStartPosition) {
|
||||||
|
seekPosition.position = motionPhotoMetadata.videoStartPosition;
|
||||||
|
return RESULT_SEEK;
|
||||||
|
}
|
||||||
|
sniffMotionPhotoVideo(input);
|
||||||
|
return RESULT_CONTINUE;
|
||||||
|
case STATE_ENDED:
|
||||||
|
return RESULT_END_OF_INPUT;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void seek(long position, long timeUs) {
|
||||||
|
state = STATE_READING_MARKER;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void release() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readMarker(ExtractorInput input) throws IOException {
|
||||||
|
scratch.reset(2);
|
||||||
|
input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 2);
|
||||||
|
marker = scratch.readUnsignedShort();
|
||||||
|
if (marker == MARKER_SOS) { // Start of scan.
|
||||||
|
if (motionPhotoMetadata != null) {
|
||||||
|
state = STATE_SNIFFING_MOTION_PHOTO_VIDEO;
|
||||||
|
} else {
|
||||||
|
outputTracks();
|
||||||
|
state = STATE_ENDED;
|
||||||
|
}
|
||||||
|
} else if ((marker < 0xFFD0 || marker > 0xFFD9) && marker != 0xFF01) {
|
||||||
|
state = STATE_READING_SEGMENT_LENGTH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readSegmentLength(ExtractorInput input) throws IOException {
|
||||||
|
scratch.reset(2);
|
||||||
|
input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 2);
|
||||||
|
segmentLength = scratch.readUnsignedShort() - 2;
|
||||||
|
state = STATE_READING_SEGMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readSegment(ExtractorInput input) throws IOException {
|
||||||
|
if (marker == MARKER_APP1) {
|
||||||
|
ParsableByteArray payload = new ParsableByteArray(segmentLength);
|
||||||
|
input.readFully(payload.getData(), /* offset= */ 0, /* length= */ segmentLength);
|
||||||
|
if (motionPhotoMetadata == null
|
||||||
|
&& HEADER_XMP_APP1.equals(payload.readNullTerminatedString())) {
|
||||||
|
@Nullable String xmpString = payload.readNullTerminatedString();
|
||||||
|
if (xmpString != null) {
|
||||||
|
motionPhotoMetadata = getMotionPhotoMetadata(xmpString, input.getLength());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input.skipFully(segmentLength);
|
||||||
|
}
|
||||||
|
state = STATE_READING_MARKER;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sniffMotionPhotoVideo(ExtractorInput input) throws IOException {
|
||||||
|
// Check if the file is truncated.
|
||||||
|
boolean peekedData =
|
||||||
|
input.peekFully(
|
||||||
|
scratch.getData(), /* offset= */ 0, /* length= */ 1, /* allowEndOfInput= */ true);
|
||||||
|
if (!peekedData) {
|
||||||
|
outputTracks();
|
||||||
|
} else {
|
||||||
|
input.resetPeekPosition();
|
||||||
|
long mp4StartPosition = input.getPosition();
|
||||||
|
StartOffsetExtractorInput mp4ExtractorInput =
|
||||||
|
new StartOffsetExtractorInput(input, mp4StartPosition);
|
||||||
|
Mp4Extractor mp4Extractor = new Mp4Extractor();
|
||||||
|
if (mp4Extractor.sniff(mp4ExtractorInput)) {
|
||||||
|
outputTracks(checkNotNull(motionPhotoMetadata));
|
||||||
|
} else {
|
||||||
|
outputTracks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state = STATE_ENDED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void outputTracks(Metadata.Entry... metadataEntries) {
|
||||||
|
TrackOutput imageTrackOutput =
|
||||||
|
checkNotNull(extractorOutput).track(/* id= */ 0, C.TRACK_TYPE_IMAGE);
|
||||||
|
imageTrackOutput.format(
|
||||||
|
new Format.Builder().setMetadata(new Metadata(metadataEntries)).build());
|
||||||
|
extractorOutput.endTracks();
|
||||||
|
extractorOutput.seekMap(new SeekMap.Unseekable(/* durationUs= */ C.TIME_UNSET));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to parse the specified XMP data describing the motion photo, returning the resulting
|
||||||
|
* {@link MotionPhotoMetadata} or {@code null} if it wasn't possible to derive motion photo
|
||||||
|
* metadata.
|
||||||
|
*
|
||||||
|
* @param xmpString A string of XML containing XMP motion photo metadata to attempt to parse.
|
||||||
|
* @param inputLength The length of the input stream in bytes, or {@link C#LENGTH_UNSET} if
|
||||||
|
* unknown.
|
||||||
|
* @return The {@link MotionPhotoMetadata}, or {@code null} if it wasn't possible to derive motion
|
||||||
|
* photo metadata.
|
||||||
|
* @throws IOException If an error occurs parsing the XMP string.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static MotionPhotoMetadata getMotionPhotoMetadata(String xmpString, long inputLength)
|
||||||
|
throws IOException {
|
||||||
|
// Metadata defines offsets from the end of the stream, so we need the stream length to
|
||||||
|
// determine start offsets.
|
||||||
|
if (inputLength == C.LENGTH_UNSET) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Motion photos have (at least) a primary image media item and a secondary video media item.
|
||||||
|
@Nullable
|
||||||
|
MotionPhotoDescription motionPhotoDescription =
|
||||||
|
XmpMotionPhotoDescriptionParser.parse(xmpString);
|
||||||
|
if (motionPhotoDescription == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return motionPhotoDescription.getMotionPhotoMetadata(inputLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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 com.google.android.exoplayer2.extractor.jpeg;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.metadata.mp4.MotionPhotoMetadata;
|
||||||
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Describes the layout and metadata of a motion photo file. */
|
||||||
|
/* package */ final class MotionPhotoDescription {
|
||||||
|
|
||||||
|
/** Describes a media item in the motion photo. */
|
||||||
|
public static final class ContainerItem {
|
||||||
|
/** The MIME type of the media item. */
|
||||||
|
public final String mime;
|
||||||
|
/** The application-specific meaning of the media item. */
|
||||||
|
public final String semantic;
|
||||||
|
/**
|
||||||
|
* The positive integer length in bytes of the media item, or 0 for primary media items and
|
||||||
|
* secondary media items that share their resource with the preceding media item.
|
||||||
|
*/
|
||||||
|
public final long length;
|
||||||
|
/**
|
||||||
|
* The number of bytes of additional padding between the end of the primary media item and the
|
||||||
|
* start of the next media item. 0 for secondary media items.
|
||||||
|
*/
|
||||||
|
public final long padding;
|
||||||
|
|
||||||
|
public ContainerItem(String mime, String semantic, long length, long padding) {
|
||||||
|
this.mime = mime;
|
||||||
|
this.semantic = semantic;
|
||||||
|
this.length = length;
|
||||||
|
this.padding = padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The presentation timestamp of the primary media item, in microseconds, or {@link C#TIME_UNSET}
|
||||||
|
* if unknown.
|
||||||
|
*/
|
||||||
|
public final long photoPresentationTimestampUs;
|
||||||
|
/**
|
||||||
|
* The media items represented by the motion photo file, in order. The primary media item is
|
||||||
|
* listed first, followed by any secondary media items.
|
||||||
|
*/
|
||||||
|
public final List<ContainerItem> items;
|
||||||
|
|
||||||
|
public MotionPhotoDescription(long photoPresentationTimestampUs, List<ContainerItem> items) {
|
||||||
|
this.photoPresentationTimestampUs = photoPresentationTimestampUs;
|
||||||
|
this.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link MotionPhotoMetadata} for the motion photo represented by this instance, or
|
||||||
|
* {@code null} if there wasn't enough information to derive the metadata.
|
||||||
|
*
|
||||||
|
* @param motionPhotoLength The length of the motion photo file, in bytes.
|
||||||
|
* @return The motion photo metadata, or {@code null}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public MotionPhotoMetadata getMotionPhotoMetadata(long motionPhotoLength) {
|
||||||
|
if (items.size() < 2) {
|
||||||
|
// We need a primary item (photo) and at least one secondary item (video).
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Iterate backwards through the items to find the earlier video in the list. If we find a video
|
||||||
|
// item with length zero, we need to keep scanning backwards to find the preceding item with
|
||||||
|
// non-zero length, which is the item that contains the video data.
|
||||||
|
long photoStartPosition = C.POSITION_UNSET;
|
||||||
|
long photoLength = C.LENGTH_UNSET;
|
||||||
|
long mp4StartPosition = C.POSITION_UNSET;
|
||||||
|
long mp4Length = C.LENGTH_UNSET;
|
||||||
|
boolean itemContainsMp4 = false;
|
||||||
|
long itemStartPosition = motionPhotoLength;
|
||||||
|
long itemEndPosition = motionPhotoLength;
|
||||||
|
for (int i = items.size() - 1; i >= 0; i--) {
|
||||||
|
MotionPhotoDescription.ContainerItem item = items.get(i);
|
||||||
|
itemContainsMp4 |= MimeTypes.VIDEO_MP4.equals(item.mime);
|
||||||
|
itemEndPosition = itemStartPosition;
|
||||||
|
if (i == 0) {
|
||||||
|
// Padding is only applied for the primary item.
|
||||||
|
itemStartPosition = 0;
|
||||||
|
itemEndPosition -= item.padding;
|
||||||
|
} else {
|
||||||
|
itemStartPosition -= item.length;
|
||||||
|
}
|
||||||
|
if (itemContainsMp4 && itemStartPosition != itemEndPosition) {
|
||||||
|
mp4StartPosition = itemStartPosition;
|
||||||
|
mp4Length = itemEndPosition - itemStartPosition;
|
||||||
|
// Reset in case there's another video earlier in the list.
|
||||||
|
itemContainsMp4 = false;
|
||||||
|
}
|
||||||
|
if (i == 0) {
|
||||||
|
photoStartPosition = itemStartPosition;
|
||||||
|
photoLength = itemEndPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mp4StartPosition == C.POSITION_UNSET
|
||||||
|
|| mp4Length == C.LENGTH_UNSET
|
||||||
|
|| photoStartPosition == C.POSITION_UNSET
|
||||||
|
|| photoLength == C.LENGTH_UNSET) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new MotionPhotoMetadata(
|
||||||
|
photoStartPosition, photoLength, photoPresentationTimestampUs, mp4StartPosition, mp4Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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 com.google.android.exoplayer2.extractor.jpeg;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
|
import com.google.android.exoplayer2.extractor.ForwardingExtractorInput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extractor input that wraps another extractor input and exposes data starting at a given start
|
||||||
|
* byte offset.
|
||||||
|
*
|
||||||
|
* <p>This is useful for reading data from a container that's concatenated after some prefix data
|
||||||
|
* but where the container's extractor doesn't handle a non-zero start offset (for example, because
|
||||||
|
* it seeks to absolute positions read from the container data).
|
||||||
|
*/
|
||||||
|
/* package */ final class StartOffsetExtractorInput extends ForwardingExtractorInput {
|
||||||
|
|
||||||
|
private final long startOffset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new wrapper reading from the given start byte offset.
|
||||||
|
*
|
||||||
|
* @param input The extractor input to wrap. The reading position must be at or after the start
|
||||||
|
* offset, otherwise data could be read from before the start offset.
|
||||||
|
* @param startOffset The offset from which this extractor input provides data, in bytes.
|
||||||
|
*/
|
||||||
|
public StartOffsetExtractorInput(ExtractorInput input, long startOffset) {
|
||||||
|
super(input);
|
||||||
|
checkState(input.getPosition() >= startOffset);
|
||||||
|
this.startOffset = startOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getPosition() {
|
||||||
|
return super.getPosition() - startOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getPeekPosition() {
|
||||||
|
return super.getPeekPosition() - startOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getLength() {
|
||||||
|
return super.getLength() - startOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Throwable> void setRetryPosition(long position, E e) throws E {
|
||||||
|
super.setRetryPosition(position + startOffset, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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 com.google.android.exoplayer2.extractor.jpeg;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import com.google.android.exoplayer2.util.XmlPullParserUtil;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.util.List;
|
||||||
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
import org.xmlpull.v1.XmlPullParserFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for motion photo metadata, handling XMP following the Motion Photo V1 and Micro Video V1b
|
||||||
|
* specifications.
|
||||||
|
*/
|
||||||
|
/* package */ final class XmpMotionPhotoDescriptionParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to parse the specified XMP data describing the motion photo, returning the resulting
|
||||||
|
* {@link MotionPhotoDescription} or {@code null} if it wasn't possible to derive a motion photo
|
||||||
|
* description.
|
||||||
|
*
|
||||||
|
* @param xmpString A string of XML containing XMP motion photo metadata to attempt to parse.
|
||||||
|
* @return The {@link MotionPhotoDescription}, or {@code null} if it wasn't possible to derive a
|
||||||
|
* motion photo description.
|
||||||
|
* @throws IOException If an error occurs reading data from the stream.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static MotionPhotoDescription parse(String xmpString) throws IOException {
|
||||||
|
try {
|
||||||
|
return parseInternal(xmpString);
|
||||||
|
} catch (XmlPullParserException | ParserException | NumberFormatException e) {
|
||||||
|
Log.w(TAG, "Ignoring unexpected XMP metadata");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String TAG = "MotionPhotoXmpParser";
|
||||||
|
|
||||||
|
private static final String[] MOTION_PHOTO_ATTRIBUTE_NAMES =
|
||||||
|
new String[] {
|
||||||
|
"Camera:MotionPhoto", // Motion Photo V1
|
||||||
|
"GCamera:MotionPhoto", // Motion Photo V1 (legacy element naming)
|
||||||
|
"Camera:MicroVideo", // Micro Video V1b
|
||||||
|
"GCamera:MicroVideo", // Micro Video V1b (legacy element naming)
|
||||||
|
};
|
||||||
|
private static final String[] DESCRIPTION_MOTION_PHOTO_PRESENTATION_TIMESTAMP_ATTRIBUTE_NAMES =
|
||||||
|
new String[] {
|
||||||
|
"Camera:MotionPhotoPresentationTimestampUs", // Motion Photo V1
|
||||||
|
"GCamera:MotionPhotoPresentationTimestampUs", // Motion Photo V1 (legacy element naming)
|
||||||
|
"Camera:MicroVideoPresentationTimestampUs", // Micro Video V1b
|
||||||
|
"GCamera:MicroVideoPresentationTimestampUs", // Micro Video V1b (legacy element naming)
|
||||||
|
};
|
||||||
|
private static final String[] DESCRIPTION_MICRO_VIDEO_OFFSET_ATTRIBUTE_NAMES =
|
||||||
|
new String[] {
|
||||||
|
"Camera:MicroVideoOffset", // Micro Video V1b
|
||||||
|
"GCamera:MicroVideoOffset", // Micro Video V1b (legacy element naming)
|
||||||
|
};
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static MotionPhotoDescription parseInternal(String xmpString)
|
||||||
|
throws XmlPullParserException, IOException {
|
||||||
|
XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();
|
||||||
|
XmlPullParser xpp = xmlPullParserFactory.newPullParser();
|
||||||
|
xpp.setInput(new StringReader(xmpString));
|
||||||
|
xpp.next();
|
||||||
|
if (!XmlPullParserUtil.isStartTag(xpp, "x:xmpmeta")) {
|
||||||
|
throw new ParserException("Couldn't find xmp metadata");
|
||||||
|
}
|
||||||
|
long motionPhotoPresentationTimestampUs = C.TIME_UNSET;
|
||||||
|
List<MotionPhotoDescription.ContainerItem> containerItems = ImmutableList.of();
|
||||||
|
do {
|
||||||
|
xpp.next();
|
||||||
|
if (XmlPullParserUtil.isStartTag(xpp, "rdf:Description")) {
|
||||||
|
if (!parseMotionPhotoFlagFromDescription(xpp)) {
|
||||||
|
// The motion photo flag is not set, so the file should not be treated as a motion photo.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
motionPhotoPresentationTimestampUs =
|
||||||
|
parseMotionPhotoPresentationTimestampUsFromDescription(xpp);
|
||||||
|
containerItems = parseMicroVideoOffsetFromDescription(xpp);
|
||||||
|
} else if (XmlPullParserUtil.isStartTag(xpp, "Container:Directory")) {
|
||||||
|
containerItems = parseMotionPhotoV1Directory(xpp);
|
||||||
|
}
|
||||||
|
} while (!XmlPullParserUtil.isEndTag(xpp, "x:xmpmeta"));
|
||||||
|
if (containerItems.isEmpty()) {
|
||||||
|
// No motion photo information was parsed.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new MotionPhotoDescription(motionPhotoPresentationTimestampUs, containerItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean parseMotionPhotoFlagFromDescription(XmlPullParser xpp) {
|
||||||
|
for (String attributeName : MOTION_PHOTO_ATTRIBUTE_NAMES) {
|
||||||
|
@Nullable String attributeValue = XmlPullParserUtil.getAttributeValue(xpp, attributeName);
|
||||||
|
if (attributeValue != null) {
|
||||||
|
int motionPhotoFlag = Integer.parseInt(attributeValue);
|
||||||
|
return motionPhotoFlag == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long parseMotionPhotoPresentationTimestampUsFromDescription(XmlPullParser xpp) {
|
||||||
|
for (String attributeName : DESCRIPTION_MOTION_PHOTO_PRESENTATION_TIMESTAMP_ATTRIBUTE_NAMES) {
|
||||||
|
@Nullable String attributeValue = XmlPullParserUtil.getAttributeValue(xpp, attributeName);
|
||||||
|
if (attributeValue != null) {
|
||||||
|
long presentationTimestampUs = Long.parseLong(attributeValue);
|
||||||
|
return presentationTimestampUs == -1 ? C.TIME_UNSET : presentationTimestampUs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return C.TIME_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableList<MotionPhotoDescription.ContainerItem>
|
||||||
|
parseMicroVideoOffsetFromDescription(XmlPullParser xpp) {
|
||||||
|
// We store a new Motion Photo item list based on the MicroVideo offset, so that the same
|
||||||
|
// representation is used for both specifications.
|
||||||
|
for (String attributeName : DESCRIPTION_MICRO_VIDEO_OFFSET_ATTRIBUTE_NAMES) {
|
||||||
|
@Nullable String attributeValue = XmlPullParserUtil.getAttributeValue(xpp, attributeName);
|
||||||
|
if (attributeValue != null) {
|
||||||
|
long microVideoOffset = Long.parseLong(attributeValue);
|
||||||
|
return ImmutableList.of(
|
||||||
|
new MotionPhotoDescription.ContainerItem(
|
||||||
|
MimeTypes.IMAGE_JPEG, "Primary", /* length= */ 0, /* padding= */ 0),
|
||||||
|
new MotionPhotoDescription.ContainerItem(
|
||||||
|
MimeTypes.VIDEO_MP4,
|
||||||
|
"MotionPhoto",
|
||||||
|
/* length= */ microVideoOffset,
|
||||||
|
/* padding= */ 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ImmutableList.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableList<MotionPhotoDescription.ContainerItem> parseMotionPhotoV1Directory(
|
||||||
|
XmlPullParser xpp) throws XmlPullParserException, IOException {
|
||||||
|
ImmutableList.Builder<MotionPhotoDescription.ContainerItem> containerItems =
|
||||||
|
ImmutableList.builder();
|
||||||
|
do {
|
||||||
|
xpp.next();
|
||||||
|
if (XmlPullParserUtil.isStartTag(xpp, "Container:Item")) {
|
||||||
|
@Nullable String mime = XmlPullParserUtil.getAttributeValue(xpp, "Item:Mime");
|
||||||
|
@Nullable String semantic = XmlPullParserUtil.getAttributeValue(xpp, "Item:Semantic");
|
||||||
|
@Nullable String length = XmlPullParserUtil.getAttributeValue(xpp, "Item:Length");
|
||||||
|
@Nullable String padding = XmlPullParserUtil.getAttributeValue(xpp, "Item:Padding");
|
||||||
|
if (mime == null || semantic == null) {
|
||||||
|
// Required values are missing.
|
||||||
|
return ImmutableList.of();
|
||||||
|
}
|
||||||
|
containerItems.add(
|
||||||
|
new MotionPhotoDescription.ContainerItem(
|
||||||
|
mime,
|
||||||
|
semantic,
|
||||||
|
length != null ? Long.parseLong(length) : 0,
|
||||||
|
padding != null ? Long.parseLong(padding) : 0));
|
||||||
|
}
|
||||||
|
} while (!XmlPullParserUtil.isEndTag(xpp, "Container:Directory"));
|
||||||
|
return containerItems.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private XmpMotionPhotoDescriptionParser() {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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 com.google.android.exoplayer2.extractor.jpeg;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.util.NonNullApi;
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
|
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
|
import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
|
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.jpeg.JpegExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||||
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
|
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
||||||
|
|
@ -68,7 +69,8 @@ public final class DefaultExtractorsFactoryTest {
|
||||||
AdtsExtractor.class,
|
AdtsExtractor.class,
|
||||||
Ac3Extractor.class,
|
Ac3Extractor.class,
|
||||||
Ac4Extractor.class,
|
Ac4Extractor.class,
|
||||||
Mp3Extractor.class)
|
Mp3Extractor.class,
|
||||||
|
JpegExtractor.class)
|
||||||
.inOrder();
|
.inOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +111,8 @@ public final class DefaultExtractorsFactoryTest {
|
||||||
MatroskaExtractor.class,
|
MatroskaExtractor.class,
|
||||||
AdtsExtractor.class,
|
AdtsExtractor.class,
|
||||||
Ac3Extractor.class,
|
Ac3Extractor.class,
|
||||||
Ac4Extractor.class)
|
Ac4Extractor.class,
|
||||||
|
JpegExtractor.class)
|
||||||
.inOrder();
|
.inOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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 com.google.android.exoplayer2.extractor.jpeg;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.ParameterizedRobolectricTestRunner;
|
||||||
|
|
||||||
|
/** Unit tests for {@link JpegExtractor}. */
|
||||||
|
@RunWith(ParameterizedRobolectricTestRunner.class)
|
||||||
|
public final class JpegExtractorTest {
|
||||||
|
|
||||||
|
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
|
||||||
|
public static ImmutableList<ExtractorAsserts.SimulationConfig> params() {
|
||||||
|
return ExtractorAsserts.configs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedRobolectricTestRunner.Parameter
|
||||||
|
public ExtractorAsserts.SimulationConfig simulationConfig;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sampleNonMotionPhotoShortened() throws Exception {
|
||||||
|
ExtractorAsserts.assertBehavior(
|
||||||
|
JpegExtractor::new, "media/jpeg/non-motion-photo-shortened.jpg", simulationConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void samplePixelMotionPhotoShortened() throws Exception {
|
||||||
|
ExtractorAsserts.assertBehavior(
|
||||||
|
JpegExtractor::new, "media/jpeg/pixel-motion-photo-shortened.jpg", simulationConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void samplePixelMotionPhotoVideoRemovedShortened() throws Exception {
|
||||||
|
ExtractorAsserts.assertBehavior(
|
||||||
|
JpegExtractor::new,
|
||||||
|
"media/jpeg/pixel-motion-photo-video-removed-shortened.jpg",
|
||||||
|
simulationConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sampleSsMotionPhotoShortened() throws Exception {
|
||||||
|
ExtractorAsserts.assertBehavior(
|
||||||
|
JpegExtractor::new, "media/jpeg/ss-motion-photo-shortened.jpg", simulationConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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 com.google.android.exoplayer2.extractor.jpeg;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.android.exoplayer2.metadata.mp4.MotionPhotoMetadata;
|
||||||
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Unit test for {@link MotionPhotoDescription}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public final class MotionPhotoDescriptionTest {
|
||||||
|
|
||||||
|
private static final long TEST_PRESENTATION_TIMESTAMP_US = 5L;
|
||||||
|
private static final long TEST_MOTION_PHOTO_LENGTH_BYTES = 20;
|
||||||
|
private static final long TEST_MOTION_PHOTO_VIDEO_LENGTH_BYTES = 7;
|
||||||
|
private static final long TEST_MOTION_PHOTO_PHOTO_PADDING_BYTES = 1;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getMotionPhotoMetadata_withPrimaryAndSecondaryMediaItems() {
|
||||||
|
MotionPhotoDescription motionPhotoDescription =
|
||||||
|
new MotionPhotoDescription(
|
||||||
|
TEST_PRESENTATION_TIMESTAMP_US,
|
||||||
|
ImmutableList.of(
|
||||||
|
new MotionPhotoDescription.ContainerItem(
|
||||||
|
MimeTypes.IMAGE_JPEG,
|
||||||
|
"Primary",
|
||||||
|
/* length= */ 0,
|
||||||
|
TEST_MOTION_PHOTO_PHOTO_PADDING_BYTES),
|
||||||
|
new MotionPhotoDescription.ContainerItem(
|
||||||
|
MimeTypes.VIDEO_MP4,
|
||||||
|
"MotionPhoto",
|
||||||
|
TEST_MOTION_PHOTO_VIDEO_LENGTH_BYTES,
|
||||||
|
/* padding= */ 0)));
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
MotionPhotoMetadata metadata =
|
||||||
|
motionPhotoDescription.getMotionPhotoMetadata(TEST_MOTION_PHOTO_LENGTH_BYTES);
|
||||||
|
|
||||||
|
assertThat(metadata.photoStartPosition).isEqualTo(0);
|
||||||
|
assertThat(metadata.photoSize)
|
||||||
|
.isEqualTo(
|
||||||
|
TEST_MOTION_PHOTO_LENGTH_BYTES
|
||||||
|
- TEST_MOTION_PHOTO_VIDEO_LENGTH_BYTES
|
||||||
|
- TEST_MOTION_PHOTO_PHOTO_PADDING_BYTES);
|
||||||
|
assertThat(metadata.photoPresentationTimestampUs).isEqualTo(TEST_PRESENTATION_TIMESTAMP_US);
|
||||||
|
assertThat(metadata.videoStartPosition)
|
||||||
|
.isEqualTo(TEST_MOTION_PHOTO_LENGTH_BYTES - TEST_MOTION_PHOTO_VIDEO_LENGTH_BYTES);
|
||||||
|
assertThat(metadata.videoSize).isEqualTo(TEST_MOTION_PHOTO_VIDEO_LENGTH_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
getMotionPhotoMetadata_withPrimaryAndMultipleSecondaryMediaItems_returnsSecondMediaItemAsVideo() {
|
||||||
|
MotionPhotoDescription motionPhotoDescription =
|
||||||
|
new MotionPhotoDescription(
|
||||||
|
TEST_PRESENTATION_TIMESTAMP_US,
|
||||||
|
ImmutableList.of(
|
||||||
|
new MotionPhotoDescription.ContainerItem(
|
||||||
|
MimeTypes.IMAGE_JPEG,
|
||||||
|
"Primary",
|
||||||
|
/* length= */ 0,
|
||||||
|
TEST_MOTION_PHOTO_PHOTO_PADDING_BYTES),
|
||||||
|
new MotionPhotoDescription.ContainerItem(
|
||||||
|
MimeTypes.VIDEO_MP4,
|
||||||
|
"MotionPhoto",
|
||||||
|
TEST_MOTION_PHOTO_VIDEO_LENGTH_BYTES,
|
||||||
|
/* padding= */ 0),
|
||||||
|
new MotionPhotoDescription.ContainerItem(
|
||||||
|
MimeTypes.VIDEO_MP4,
|
||||||
|
"MotionPhoto",
|
||||||
|
TEST_MOTION_PHOTO_VIDEO_LENGTH_BYTES,
|
||||||
|
/* padding= */ 0)));
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
MotionPhotoMetadata metadata =
|
||||||
|
motionPhotoDescription.getMotionPhotoMetadata(TEST_MOTION_PHOTO_LENGTH_BYTES);
|
||||||
|
|
||||||
|
assertThat(metadata.photoStartPosition).isEqualTo(0);
|
||||||
|
assertThat(metadata.photoSize)
|
||||||
|
.isEqualTo(
|
||||||
|
TEST_MOTION_PHOTO_LENGTH_BYTES
|
||||||
|
- TEST_MOTION_PHOTO_VIDEO_LENGTH_BYTES * 2
|
||||||
|
- TEST_MOTION_PHOTO_PHOTO_PADDING_BYTES);
|
||||||
|
assertThat(metadata.photoPresentationTimestampUs).isEqualTo(TEST_PRESENTATION_TIMESTAMP_US);
|
||||||
|
assertThat(metadata.videoStartPosition)
|
||||||
|
.isEqualTo(TEST_MOTION_PHOTO_LENGTH_BYTES - TEST_MOTION_PHOTO_VIDEO_LENGTH_BYTES * 2);
|
||||||
|
assertThat(metadata.videoSize).isEqualTo(TEST_MOTION_PHOTO_VIDEO_LENGTH_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
getMotionPhotoMetadata_withPrimaryAndSecondaryItemSharingData_returnsPrimaryItemAsPhotoAndVideo() {
|
||||||
|
// Theoretical example of an HEIF file that has both an image and a video represented in the
|
||||||
|
// same file, which looks like an MP4.
|
||||||
|
MotionPhotoDescription motionPhotoDescription =
|
||||||
|
new MotionPhotoDescription(
|
||||||
|
TEST_PRESENTATION_TIMESTAMP_US,
|
||||||
|
ImmutableList.of(
|
||||||
|
new MotionPhotoDescription.ContainerItem(
|
||||||
|
MimeTypes.VIDEO_MP4,
|
||||||
|
"Primary",
|
||||||
|
/* length= */ 0,
|
||||||
|
TEST_MOTION_PHOTO_PHOTO_PADDING_BYTES),
|
||||||
|
new MotionPhotoDescription.ContainerItem(
|
||||||
|
MimeTypes.VIDEO_MP4, "MotionPhoto", /* length= */ 0, /* padding= */ 0)));
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
MotionPhotoMetadata metadata =
|
||||||
|
motionPhotoDescription.getMotionPhotoMetadata(TEST_MOTION_PHOTO_LENGTH_BYTES);
|
||||||
|
|
||||||
|
assertThat(metadata.photoStartPosition).isEqualTo(0);
|
||||||
|
assertThat(metadata.photoSize)
|
||||||
|
.isEqualTo(TEST_MOTION_PHOTO_LENGTH_BYTES - TEST_MOTION_PHOTO_PHOTO_PADDING_BYTES);
|
||||||
|
assertThat(metadata.photoPresentationTimestampUs).isEqualTo(TEST_PRESENTATION_TIMESTAMP_US);
|
||||||
|
assertThat(metadata.videoStartPosition).isEqualTo(0);
|
||||||
|
assertThat(metadata.videoSize)
|
||||||
|
.isEqualTo(TEST_MOTION_PHOTO_LENGTH_BYTES - TEST_MOTION_PHOTO_PHOTO_PADDING_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getMotionPhotoMetadata_withOnlyPrimaryItem_returnsNull() {
|
||||||
|
MotionPhotoDescription motionPhotoDescription =
|
||||||
|
new MotionPhotoDescription(
|
||||||
|
TEST_PRESENTATION_TIMESTAMP_US,
|
||||||
|
ImmutableList.of(
|
||||||
|
new MotionPhotoDescription.ContainerItem(
|
||||||
|
MimeTypes.VIDEO_MP4,
|
||||||
|
"Primary",
|
||||||
|
/* length= */ 0,
|
||||||
|
TEST_MOTION_PHOTO_PHOTO_PADDING_BYTES)));
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
MotionPhotoMetadata metadata =
|
||||||
|
motionPhotoDescription.getMotionPhotoMetadata(TEST_MOTION_PHOTO_LENGTH_BYTES);
|
||||||
|
|
||||||
|
assertThat(metadata).isNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
11
testdata/src/test/assets/extractordumps/jpeg/non-motion-photo-shortened.jpg.0.dump
vendored
Normal file
11
testdata/src/test/assets/extractordumps/jpeg/non-motion-photo-shortened.jpg.0.dump
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
seekMap:
|
||||||
|
isSeekable = false
|
||||||
|
duration = UNSET TIME
|
||||||
|
getPosition(0) = [[timeUs=0, position=0]]
|
||||||
|
numberOfTracks = 1
|
||||||
|
track 0:
|
||||||
|
total output bytes = 0
|
||||||
|
sample count = 0
|
||||||
|
format 0:
|
||||||
|
metadata = entries=[]
|
||||||
|
tracksEnded = true
|
||||||
11
testdata/src/test/assets/extractordumps/jpeg/non-motion-photo-shortened.jpg.unknown_length.dump
vendored
Normal file
11
testdata/src/test/assets/extractordumps/jpeg/non-motion-photo-shortened.jpg.unknown_length.dump
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
seekMap:
|
||||||
|
isSeekable = false
|
||||||
|
duration = UNSET TIME
|
||||||
|
getPosition(0) = [[timeUs=0, position=0]]
|
||||||
|
numberOfTracks = 1
|
||||||
|
track 0:
|
||||||
|
total output bytes = 0
|
||||||
|
sample count = 0
|
||||||
|
format 0:
|
||||||
|
metadata = entries=[]
|
||||||
|
tracksEnded = true
|
||||||
11
testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-shortened.jpg.0.dump
vendored
Normal file
11
testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-shortened.jpg.0.dump
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
seekMap:
|
||||||
|
isSeekable = false
|
||||||
|
duration = UNSET TIME
|
||||||
|
getPosition(0) = [[timeUs=0, position=0]]
|
||||||
|
numberOfTracks = 1
|
||||||
|
track 0:
|
||||||
|
total output bytes = 0
|
||||||
|
sample count = 0
|
||||||
|
format 0:
|
||||||
|
metadata = entries=[Motion photo metadata: photoStartPosition=0, photoSize=131582, photoPresentationTimestampUs=0, videoStartPosition=131582, videoSize=8730]
|
||||||
|
tracksEnded = true
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
seekMap:
|
||||||
|
isSeekable = false
|
||||||
|
duration = UNSET TIME
|
||||||
|
getPosition(0) = [[timeUs=0, position=0]]
|
||||||
|
numberOfTracks = 1
|
||||||
|
track 0:
|
||||||
|
total output bytes = 0
|
||||||
|
sample count = 0
|
||||||
|
format 0:
|
||||||
|
metadata = entries=[]
|
||||||
|
tracksEnded = true
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
seekMap:
|
||||||
|
isSeekable = false
|
||||||
|
duration = UNSET TIME
|
||||||
|
getPosition(0) = [[timeUs=0, position=0]]
|
||||||
|
numberOfTracks = 1
|
||||||
|
track 0:
|
||||||
|
total output bytes = 0
|
||||||
|
sample count = 0
|
||||||
|
format 0:
|
||||||
|
metadata = entries=[]
|
||||||
|
tracksEnded = true
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
seekMap:
|
||||||
|
isSeekable = false
|
||||||
|
duration = UNSET TIME
|
||||||
|
getPosition(0) = [[timeUs=0, position=0]]
|
||||||
|
numberOfTracks = 1
|
||||||
|
track 0:
|
||||||
|
total output bytes = 0
|
||||||
|
sample count = 0
|
||||||
|
format 0:
|
||||||
|
metadata = entries=[]
|
||||||
|
tracksEnded = true
|
||||||
11
testdata/src/test/assets/extractordumps/jpeg/ss-motion-photo-shortened.jpg.0.dump
vendored
Normal file
11
testdata/src/test/assets/extractordumps/jpeg/ss-motion-photo-shortened.jpg.0.dump
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
seekMap:
|
||||||
|
isSeekable = false
|
||||||
|
duration = UNSET TIME
|
||||||
|
getPosition(0) = [[timeUs=0, position=0]]
|
||||||
|
numberOfTracks = 1
|
||||||
|
track 0:
|
||||||
|
total output bytes = 0
|
||||||
|
sample count = 0
|
||||||
|
format 0:
|
||||||
|
metadata = entries=[Motion photo metadata: photoStartPosition=0, photoSize=20345, photoPresentationTimestampUs=-9223372036854775807, videoStartPosition=20345, videoSize=2582]
|
||||||
|
tracksEnded = true
|
||||||
11
testdata/src/test/assets/extractordumps/jpeg/ss-motion-photo-shortened.jpg.unknown_length.dump
vendored
Normal file
11
testdata/src/test/assets/extractordumps/jpeg/ss-motion-photo-shortened.jpg.unknown_length.dump
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
seekMap:
|
||||||
|
isSeekable = false
|
||||||
|
duration = UNSET TIME
|
||||||
|
getPosition(0) = [[timeUs=0, position=0]]
|
||||||
|
numberOfTracks = 1
|
||||||
|
track 0:
|
||||||
|
total output bytes = 0
|
||||||
|
sample count = 0
|
||||||
|
format 0:
|
||||||
|
metadata = entries=[]
|
||||||
|
tracksEnded = true
|
||||||
BIN
testdata/src/test/assets/media/jpeg/non-motion-photo-shortened.jpg
vendored
Normal file
BIN
testdata/src/test/assets/media/jpeg/non-motion-photo-shortened.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
testdata/src/test/assets/media/jpeg/pixel-motion-photo-shortened.jpg
vendored
Normal file
BIN
testdata/src/test/assets/media/jpeg/pixel-motion-photo-shortened.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
BIN
testdata/src/test/assets/media/jpeg/pixel-motion-photo-video-removed-shortened.jpg
vendored
Normal file
BIN
testdata/src/test/assets/media/jpeg/pixel-motion-photo-video-removed-shortened.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
BIN
testdata/src/test/assets/media/jpeg/ss-motion-photo-shortened.jpg
vendored
Normal file
BIN
testdata/src/test/assets/media/jpeg/ss-motion-photo-shortened.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Loading…
Reference in a new issue