#1423 support for Samsung HEIC motion photos embedding video in sefd box
This commit is contained in:
parent
e2e0ee706f
commit
026cfebd49
7 changed files with 179 additions and 57 deletions
|
@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## <a id="unreleased"></a>[Unreleased]
|
## <a id="unreleased"></a>[Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- support for Samsung HEIC motion photos embedding video in sefd box
|
||||||
|
|
||||||
## <a id="v1.12.3"></a>[v1.12.3] - 2025-02-06
|
## <a id="v1.12.3"></a>[v1.12.3] - 2025-02-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -316,7 +316,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
if (mimeType == MimeTypes.MP4) {
|
if (mimeType == MimeTypes.MP4 || MimeTypes.isHeic(mimeType)) {
|
||||||
try {
|
try {
|
||||||
// we can skip uninteresting boxes with a seekable data source
|
// we can skip uninteresting boxes with a seekable data source
|
||||||
val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri")
|
val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri")
|
||||||
|
|
|
@ -186,7 +186,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
MultiPage.getMotionPhotoVideoSize(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes ->
|
MultiPage.getTrailerVideoSize(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes ->
|
||||||
val imageSizeBytes = sizeBytes - videoSizeBytes
|
val imageSizeBytes = sizeBytes - videoSizeBytes
|
||||||
StorageUtils.openInputStream(context, uri)?.let { input ->
|
StorageUtils.openInputStream(context, uri)?.let { input ->
|
||||||
copyEmbeddedBytes(result, mimeType, displayName, input, imageSizeBytes)
|
copyEmbeddedBytes(result, mimeType, displayName, input, imageSizeBytes)
|
||||||
|
@ -207,11 +207,10 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
MultiPage.getMotionPhotoVideoSize(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes ->
|
MultiPage.getMotionPhotoVideoSizing(context, uri, mimeType, sizeBytes)?.let { (videoOffset, videoSize) ->
|
||||||
val videoStartOffset = sizeBytes - videoSizeBytes
|
|
||||||
StorageUtils.openInputStream(context, uri)?.let { input ->
|
StorageUtils.openInputStream(context, uri)?.let { input ->
|
||||||
input.skip(videoStartOffset)
|
input.skip(videoOffset)
|
||||||
copyEmbeddedBytes(result, MimeTypes.MP4, displayName, input, videoSizeBytes)
|
copyEmbeddedBytes(result, MimeTypes.MP4, displayName, input, videoSize)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.adobe.internal.xmp.XMPException
|
import com.adobe.internal.xmp.XMPException
|
||||||
import com.adobe.internal.xmp.XMPMeta
|
import com.adobe.internal.xmp.XMPMeta
|
||||||
import com.adobe.internal.xmp.XMPMetaFactory
|
import com.adobe.internal.xmp.XMPMetaFactory
|
||||||
|
@ -13,6 +14,7 @@ import com.adobe.internal.xmp.options.SerializeOptions
|
||||||
import com.adobe.internal.xmp.properties.XMPPropertyInfo
|
import com.adobe.internal.xmp.properties.XMPPropertyInfo
|
||||||
import com.drew.lang.KeyValuePair
|
import com.drew.lang.KeyValuePair
|
||||||
import com.drew.lang.Rational
|
import com.drew.lang.Rational
|
||||||
|
import com.drew.lang.SequentialByteArrayReader
|
||||||
import com.drew.metadata.Tag
|
import com.drew.metadata.Tag
|
||||||
import com.drew.metadata.avi.AviDirectory
|
import com.drew.metadata.avi.AviDirectory
|
||||||
import com.drew.metadata.exif.ExifDirectoryBase
|
import com.drew.metadata.exif.ExifDirectoryBase
|
||||||
|
@ -107,7 +109,6 @@ import java.util.Locale
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
|
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
|
||||||
import androidx.core.net.toUri
|
|
||||||
|
|
||||||
class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
@ -470,6 +471,22 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
// Android's `MediaExtractor` and `MediaPlayer` cannot be used for details
|
// Android's `MediaExtractor` and `MediaPlayer` cannot be used for details
|
||||||
// about embedded images as they do not list them as separate tracks
|
// about embedded images as they do not list them as separate tracks
|
||||||
// and only identify at most one
|
// and only identify at most one
|
||||||
|
} else if (isHeic(mimeType)) {
|
||||||
|
Mp4ParserHelper.getSamsungSefd(context, uri)?.let { (_, bytes) ->
|
||||||
|
val dir = hashMapOf(
|
||||||
|
"Size" to bytes.size.toString(),
|
||||||
|
)
|
||||||
|
val reader = SequentialByteArrayReader(bytes).apply {
|
||||||
|
isMotorolaByteOrder = false
|
||||||
|
}
|
||||||
|
val start = reader.uInt16
|
||||||
|
val tag = reader.uInt16
|
||||||
|
if (start == 0 && tag == Mp4ParserHelper.SEFD_EMBEDDED_VIDEO_TAG) {
|
||||||
|
val nameSize = reader.uInt32
|
||||||
|
dir["Embedded Video Type"] = reader.getString(nameSize.toInt())
|
||||||
|
}
|
||||||
|
metadataMap[Mp4ParserHelper.SAMSUNG_MAKERNOTE_BOX_TYPE] = dir
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadataMap.isNotEmpty()) {
|
if (metadataMap.isNotEmpty()) {
|
||||||
|
@ -531,6 +548,13 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
getMultimediaCatalogMetadataByMediaMetadataRetriever(mimeType, uri, metadataMap)
|
getMultimediaCatalogMetadataByMediaMetadataRetriever(mimeType, uri, metadataMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isHeic(mimeType)) {
|
||||||
|
val flags = (metadataMap[KEY_FLAGS] ?: 0) as Int
|
||||||
|
if ((flags and MASK_IS_MOTION_PHOTO == 0) && MultiPage.isHeicSefdMotionPhoto(context, uri)) {
|
||||||
|
metadataMap[KEY_FLAGS] = flags or MASK_IS_MULTIPAGE or MASK_IS_MOTION_PHOTO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// report success even when empty
|
// report success even when empty
|
||||||
result.success(metadataMap)
|
result.success(metadataMap)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,16 @@ object Mp4ParserHelper {
|
||||||
// arbitrary size to detect boxes that may yield an OOM
|
// arbitrary size to detect boxes that may yield an OOM
|
||||||
private const val BOX_SIZE_DANGER_THRESHOLD = 3 * (1 shl 20) // MB
|
private const val BOX_SIZE_DANGER_THRESHOLD = 3 * (1 shl 20) // MB
|
||||||
|
|
||||||
|
const val SAMSUNG_MAKERNOTE_BOX_TYPE = "sefd"
|
||||||
|
const val SEFD_EMBEDDED_VIDEO_TAG = 0x0a30
|
||||||
|
const val SEFD_MOTION_PHOTO_NAME = "MotionPhoto_Data"
|
||||||
|
|
||||||
|
private val largerTypeWhitelist = listOf(
|
||||||
|
// HEIC motion photo may contain Samsung maker notes in `sefd` box,
|
||||||
|
// including a video larger than the danger threshold
|
||||||
|
SAMSUNG_MAKERNOTE_BOX_TYPE,
|
||||||
|
)
|
||||||
|
|
||||||
fun computeEdits(context: Context, uri: Uri, modifier: (isoFile: IsoFile) -> Unit): List<Pair<Long, ByteArray>> {
|
fun computeEdits(context: Context, uri: Uri, modifier: (isoFile: IsoFile) -> Unit): List<Pair<Long, ByteArray>> {
|
||||||
// we can skip uninteresting boxes with a seekable data source
|
// we can skip uninteresting boxes with a seekable data source
|
||||||
val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri")
|
val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri")
|
||||||
|
@ -133,6 +143,34 @@ object Mp4ParserHelper {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSamsungSefd(context: Context, uri: Uri): Pair<Long, ByteArray>? {
|
||||||
|
try {
|
||||||
|
// we can skip uninteresting boxes with a seekable data source
|
||||||
|
val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri")
|
||||||
|
pfd.use {
|
||||||
|
FileInputStream(it.fileDescriptor).use { stream ->
|
||||||
|
stream.channel.use { channel ->
|
||||||
|
IsoFile(channel, metadataBoxParser()).use { isoFile ->
|
||||||
|
var offset = 0L
|
||||||
|
for (box in isoFile.boxes) {
|
||||||
|
if (box is UnknownBox && box.type == SAMSUNG_MAKERNOTE_BOX_TYPE) {
|
||||||
|
if (!box.isParsed) {
|
||||||
|
box.parseDetails()
|
||||||
|
}
|
||||||
|
return Pair(offset + 8, box.data.toByteArray()) // skip 8 bytes for box header
|
||||||
|
}
|
||||||
|
offset += box.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to read sefd box", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// extensions
|
// extensions
|
||||||
|
|
||||||
fun IsoFile.updateLocation(locationIso6709: String?) {
|
fun IsoFile.updateLocation(locationIso6709: String?) {
|
||||||
|
@ -272,7 +310,7 @@ object Mp4ParserHelper {
|
||||||
)
|
)
|
||||||
setBoxSkipper { type, size ->
|
setBoxSkipper { type, size ->
|
||||||
if (skippedTypes.contains(type)) return@setBoxSkipper true
|
if (skippedTypes.contains(type)) return@setBoxSkipper true
|
||||||
if (size > BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large")
|
if (size > BOX_SIZE_DANGER_THRESHOLD && !largerTypeWhitelist.contains(type)) throw Exception("box (type=$type size=$size) is too large")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.adobe.internal.xmp.XMPMeta
|
import com.adobe.internal.xmp.XMPMeta
|
||||||
import com.drew.imaging.jpeg.JpegSegmentType
|
import com.drew.imaging.jpeg.JpegSegmentType
|
||||||
|
import com.drew.lang.SequentialByteArrayReader
|
||||||
import com.drew.metadata.exif.ExifDirectoryBase
|
import com.drew.metadata.exif.ExifDirectoryBase
|
||||||
import com.drew.metadata.exif.ExifIFD0Directory
|
import com.drew.metadata.exif.ExifIFD0Directory
|
||||||
import com.drew.metadata.xmp.XmpDirectory
|
import com.drew.metadata.xmp.XmpDirectory
|
||||||
|
@ -37,6 +38,8 @@ import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
|
||||||
object MultiPage {
|
object MultiPage {
|
||||||
private val LOG_TAG = LogUtils.createTag<MultiPage>()
|
private val LOG_TAG = LogUtils.createTag<MultiPage>()
|
||||||
|
|
||||||
|
// TODO TLAD more generic support, (e.g. 0x00000014 + `ftyp` + `qt `)
|
||||||
|
// atom length (variable, e.g. `0x00000018`) + atom type (`ftyp`) + type (variable, e.g. `mp42`, `qt`)
|
||||||
private val heicMotionPhotoVideoStartIndicator = byteArrayOf(0x00, 0x00, 0x00, 0x18) + "ftypmp42".toByteArray()
|
private val heicMotionPhotoVideoStartIndicator = byteArrayOf(0x00, 0x00, 0x00, 0x18) + "ftypmp42".toByteArray()
|
||||||
|
|
||||||
// page info
|
// page info
|
||||||
|
@ -84,6 +87,22 @@ object MultiPage {
|
||||||
return tracks
|
return tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isHeicSefdMotionPhoto(context: Context, uri: Uri): Boolean {
|
||||||
|
Mp4ParserHelper.getSamsungSefd(context, uri)?.let { (_, bytes) ->
|
||||||
|
val reader = SequentialByteArrayReader(bytes).apply {
|
||||||
|
isMotorolaByteOrder = false
|
||||||
|
}
|
||||||
|
val start = reader.uInt16
|
||||||
|
val tag = reader.uInt16
|
||||||
|
if (start == 0 && tag == Mp4ParserHelper.SEFD_EMBEDDED_VIDEO_TAG) {
|
||||||
|
val nameSize = reader.uInt32
|
||||||
|
val name = reader.getString(nameSize.toInt())
|
||||||
|
return name == Mp4ParserHelper.SEFD_MOTION_PHOTO_NAME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun getJpegMpfPrimaryRotation(context: Context, uri: Uri, sizeBytes: Long): Int {
|
private fun getJpegMpfPrimaryRotation(context: Context, uri: Uri, sizeBytes: Long): Int {
|
||||||
val mimeType = MimeTypes.JPEG
|
val mimeType = MimeTypes.JPEG
|
||||||
var rotationDegrees = 0
|
var rotationDegrees = 0
|
||||||
|
@ -245,8 +264,7 @@ object MultiPage {
|
||||||
|
|
||||||
fun getMotionPhotoPages(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): ArrayList<FieldMap> {
|
fun getMotionPhotoPages(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): ArrayList<FieldMap> {
|
||||||
val pages = ArrayList<FieldMap>()
|
val pages = ArrayList<FieldMap>()
|
||||||
getMotionPhotoVideoSize(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes ->
|
getMotionPhotoVideoInfo(context, uri, mimeType, sizeBytes)?.let { videoInfo ->
|
||||||
getTrailerVideoInfo(context, uri, fileSizeBytes = sizeBytes, videoSizeBytes = videoSizeBytes)?.let { videoInfo ->
|
|
||||||
// set the original image as the first and default track
|
// set the original image as the first and default track
|
||||||
var pageIndex = 0
|
var pageIndex = 0
|
||||||
pages.add(
|
pages.add(
|
||||||
|
@ -274,11 +292,10 @@ object MultiPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return pages
|
return pages
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMotionPhotoVideoSize(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): Long? {
|
fun getTrailerVideoSize(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): Long? {
|
||||||
if (MimeTypes.isHeic(mimeType)) {
|
if (MimeTypes.isHeic(mimeType)) {
|
||||||
// XMP in HEIC motion photos (as taken with a Samsung Camera v12.0.01.50) indicates an `Item:Length` of 68 bytes for the video.
|
// XMP in HEIC motion photos (as taken with a Samsung Camera v12.0.01.50) indicates an `Item:Length` of 68 bytes for the video.
|
||||||
// This item does not contain the video itself, but only some kind of metadata (no doc, no spec),
|
// This item does not contain the video itself, but only some kind of metadata (no doc, no spec),
|
||||||
|
@ -325,15 +342,25 @@ object MultiPage {
|
||||||
return offsetFromEnd
|
return offsetFromEnd
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTrailerVideoInfo(context: Context, uri: Uri, fileSizeBytes: Long, videoSizeBytes: Long): MediaFormat? {
|
private fun getMotionPhotoVideoInfo(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): MediaFormat? {
|
||||||
|
getMotionPhotoVideoSizing(context, uri, mimeType, sizeBytes)?.let { (videoOffset, videoSize) ->
|
||||||
|
return getEmbedVideoInfo(context, uri, videoOffset, videoSize)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTrailerVideoInfo(context: Context, uri: Uri, fileSize: Long, videoSize: Long): MediaFormat? {
|
||||||
|
return getEmbedVideoInfo(context, uri, videoOffset = fileSize - videoSize, videoSize = videoSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEmbedVideoInfo(context: Context, uri: Uri, videoOffset: Long, videoSize: Long): MediaFormat? {
|
||||||
var format: MediaFormat? = null
|
var format: MediaFormat? = null
|
||||||
val extractor = MediaExtractor()
|
val extractor = MediaExtractor()
|
||||||
var pfd: ParcelFileDescriptor? = null
|
var pfd: ParcelFileDescriptor? = null
|
||||||
try {
|
try {
|
||||||
val videoStartOffset = fileSizeBytes - videoSizeBytes
|
|
||||||
pfd = context.contentResolver.openFileDescriptor(uri, "r")
|
pfd = context.contentResolver.openFileDescriptor(uri, "r")
|
||||||
pfd?.fileDescriptor?.let { fd ->
|
pfd?.fileDescriptor?.let { fd ->
|
||||||
extractor.setDataSource(fd, videoStartOffset, videoSizeBytes)
|
extractor.setDataSource(fd, videoOffset, videoSize)
|
||||||
if (extractor.trackCount > 0) {
|
if (extractor.trackCount > 0) {
|
||||||
// only consider the first track to represent the appended video
|
// only consider the first track to represent the appended video
|
||||||
val trackIndex = 0
|
val trackIndex = 0
|
||||||
|
@ -353,6 +380,36 @@ object MultiPage {
|
||||||
return format
|
return format
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMotionPhotoVideoSizing(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): Pair<Long, Long>? {
|
||||||
|
// default to trailer videos
|
||||||
|
getTrailerVideoSize(context, uri, mimeType, sizeBytes)?.let { videoSize ->
|
||||||
|
val videoOffset = sizeBytes - videoSize
|
||||||
|
return Pair(videoOffset, videoSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MimeTypes.isHeic(mimeType)) {
|
||||||
|
// fallback to video within Samsung SEFD box
|
||||||
|
Mp4ParserHelper.getSamsungSefd(context, uri)?.let { (sefdOffset, bytes) ->
|
||||||
|
val reader = SequentialByteArrayReader(bytes).apply {
|
||||||
|
isMotorolaByteOrder = false
|
||||||
|
}
|
||||||
|
val start = reader.uInt16
|
||||||
|
val tag = reader.uInt16
|
||||||
|
if (start == 0 && tag == Mp4ParserHelper.SEFD_EMBEDDED_VIDEO_TAG) {
|
||||||
|
val nameSize = reader.uInt32
|
||||||
|
val name = reader.getString(nameSize.toInt())
|
||||||
|
if (name == Mp4ParserHelper.SEFD_MOTION_PHOTO_NAME) {
|
||||||
|
val videoOffset = sefdOffset + reader.position
|
||||||
|
val videoSize = reader.available().toLong()
|
||||||
|
return Pair(videoOffset, videoSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
fun getTiffPages(context: Context, uri: Uri): ArrayList<FieldMap> {
|
fun getTiffPages(context: Context, uri: Uri): ArrayList<FieldMap> {
|
||||||
fun toMap(pageIndex: Int, options: TiffBitmapFactory.Options): FieldMap {
|
fun toMap(pageIndex: Int, options: TiffBitmapFactory.Options): FieldMap {
|
||||||
return hashMapOf(
|
return hashMapOf(
|
||||||
|
|
|
@ -648,13 +648,13 @@ abstract class ImageProvider {
|
||||||
val originalFileSize = File(path).length()
|
val originalFileSize = File(path).length()
|
||||||
var trailerVideoBytes: ByteArray? = null
|
var trailerVideoBytes: ByteArray? = null
|
||||||
val editableFile = StorageUtils.createTempFile(context).apply {
|
val editableFile = StorageUtils.createTempFile(context).apply {
|
||||||
val videoSize = MultiPage.getMotionPhotoVideoSize(context, uri, mimeType, originalFileSize)?.let { it + trailerDiff }
|
val trailerVideoSize = MultiPage.getTrailerVideoSize(context, uri, mimeType, originalFileSize)?.let { it + trailerDiff }
|
||||||
val isTrailerVideoValid = videoSize != null && MultiPage.getTrailerVideoInfo(context, uri, originalFileSize, videoSize) != null
|
val isTrailerVideoValid = trailerVideoSize != null && MultiPage.getTrailerVideoInfo(context, uri, originalFileSize, trailerVideoSize) != null
|
||||||
try {
|
try {
|
||||||
if (videoSize != null && isTrailerVideoValid) {
|
if (trailerVideoSize != null && isTrailerVideoValid) {
|
||||||
// handle motion photo and embedded video separately
|
// handle motion photo and embedded video separately
|
||||||
val imageSize = (originalFileSize - videoSize).toInt()
|
val imageSize = (originalFileSize - trailerVideoSize).toInt()
|
||||||
val videoByteSize = videoSize.toInt()
|
val videoByteSize = trailerVideoSize.toInt()
|
||||||
trailerVideoBytes = ByteArray(videoByteSize)
|
trailerVideoBytes = ByteArray(videoByteSize)
|
||||||
|
|
||||||
StorageUtils.openInputStream(context, uri)?.let { input ->
|
StorageUtils.openInputStream(context, uri)?.let { input ->
|
||||||
|
@ -733,13 +733,13 @@ abstract class ImageProvider {
|
||||||
val originalFileSize = File(path).length()
|
val originalFileSize = File(path).length()
|
||||||
var trailerVideoBytes: ByteArray? = null
|
var trailerVideoBytes: ByteArray? = null
|
||||||
val editableFile = StorageUtils.createTempFile(context).apply {
|
val editableFile = StorageUtils.createTempFile(context).apply {
|
||||||
val videoSize = MultiPage.getMotionPhotoVideoSize(context, uri, mimeType, originalFileSize)?.let { it + trailerDiff }
|
val trailerVideoSize = MultiPage.getTrailerVideoSize(context, uri, mimeType, originalFileSize)?.let { it + trailerDiff }
|
||||||
val isTrailerVideoValid = videoSize != null && MultiPage.getTrailerVideoInfo(context, uri, originalFileSize, videoSize) != null
|
val isTrailerVideoValid = trailerVideoSize != null && MultiPage.getTrailerVideoInfo(context, uri, originalFileSize, trailerVideoSize) != null
|
||||||
try {
|
try {
|
||||||
if (videoSize != null && isTrailerVideoValid) {
|
if (trailerVideoSize != null && isTrailerVideoValid) {
|
||||||
// handle motion photo and embedded video separately
|
// handle motion photo and embedded video separately
|
||||||
val imageSize = (originalFileSize - videoSize).toInt()
|
val imageSize = (originalFileSize - trailerVideoSize).toInt()
|
||||||
val videoByteSize = videoSize.toInt()
|
val videoByteSize = trailerVideoSize.toInt()
|
||||||
trailerVideoBytes = ByteArray(videoByteSize)
|
trailerVideoBytes = ByteArray(videoByteSize)
|
||||||
|
|
||||||
StorageUtils.openInputStream(context, uri)?.let { input ->
|
StorageUtils.openInputStream(context, uri)?.let { input ->
|
||||||
|
@ -899,7 +899,7 @@ abstract class ImageProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
val originalFileSize = File(path).length()
|
val originalFileSize = File(path).length()
|
||||||
val videoSize = MultiPage.getMotionPhotoVideoSize(context, uri, mimeType, originalFileSize)?.let { it.toInt() + trailerDiff }
|
val trailerVideoSize = MultiPage.getTrailerVideoSize(context, uri, mimeType, originalFileSize)?.let { it.toInt() + trailerDiff }
|
||||||
val editableFile = StorageUtils.createTempFile(context).apply {
|
val editableFile = StorageUtils.createTempFile(context).apply {
|
||||||
try {
|
try {
|
||||||
editXmpWithPixy(
|
editXmpWithPixy(
|
||||||
|
@ -921,7 +921,7 @@ abstract class ImageProvider {
|
||||||
// copy the edited temporary file back to the original
|
// copy the edited temporary file back to the original
|
||||||
editableFile.transferTo(outputStream(context, mimeType, uri, path))
|
editableFile.transferTo(outputStream(context, mimeType, uri, path))
|
||||||
|
|
||||||
if (autoCorrectTrailerOffset && !checkTrailerOffset(context, path, uri, mimeType, videoSize, editableFile, callback)) {
|
if (autoCorrectTrailerOffset && !checkTrailerOffset(context, path, uri, mimeType, trailerVideoSize, editableFile, callback)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
editableFile.delete()
|
editableFile.delete()
|
||||||
|
@ -1262,15 +1262,15 @@ abstract class ImageProvider {
|
||||||
callback: ImageOpCallback,
|
callback: ImageOpCallback,
|
||||||
) {
|
) {
|
||||||
val originalFileSize = File(path).length()
|
val originalFileSize = File(path).length()
|
||||||
val videoSize = MultiPage.getMotionPhotoVideoSize(context, uri, mimeType, originalFileSize)
|
val trailerVideoSize = MultiPage.getTrailerVideoSize(context, uri, mimeType, originalFileSize)
|
||||||
if (videoSize == null) {
|
if (trailerVideoSize == null) {
|
||||||
callback.onFailure(Exception("failed to get trailer video size"))
|
callback.onFailure(Exception("failed to get trailer video size"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val isTrailerVideoValid = MultiPage.getTrailerVideoInfo(context, uri, fileSizeBytes = originalFileSize, videoSizeBytes = videoSize) != null
|
val isTrailerVideoValid = MultiPage.getTrailerVideoInfo(context, uri, fileSize = originalFileSize, videoSize = trailerVideoSize) != null
|
||||||
if (!isTrailerVideoValid) {
|
if (!isTrailerVideoValid) {
|
||||||
callback.onFailure(Exception("failed to open trailer video with size=$videoSize"))
|
callback.onFailure(Exception("failed to open trailer video with size=$trailerVideoSize"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1278,7 +1278,7 @@ abstract class ImageProvider {
|
||||||
try {
|
try {
|
||||||
val inputStream = StorageUtils.openInputStream(context, uri)
|
val inputStream = StorageUtils.openInputStream(context, uri)
|
||||||
// partial copy
|
// partial copy
|
||||||
transferFrom(inputStream, originalFileSize - videoSize)
|
transferFrom(inputStream, originalFileSize - trailerVideoSize)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(LOG_TAG, "failed to remove trailer video", e)
|
Log.d(LOG_TAG, "failed to remove trailer video", e)
|
||||||
callback.onFailure(e)
|
callback.onFailure(e)
|
||||||
|
@ -1313,8 +1313,8 @@ abstract class ImageProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
val originalFileSize = File(path).length()
|
val originalFileSize = File(path).length()
|
||||||
val videoSize = MultiPage.getMotionPhotoVideoSize(context, uri, mimeType, originalFileSize)
|
val trailerVideoSize = MultiPage.getTrailerVideoSize(context, uri, mimeType, originalFileSize)
|
||||||
val isTrailerVideoValid = videoSize != null && MultiPage.getTrailerVideoInfo(context, uri, originalFileSize, videoSize) != null
|
val isTrailerVideoValid = trailerVideoSize != null && MultiPage.getTrailerVideoInfo(context, uri, originalFileSize, trailerVideoSize) != null
|
||||||
val editableFile = StorageUtils.createTempFile(context).apply {
|
val editableFile = StorageUtils.createTempFile(context).apply {
|
||||||
try {
|
try {
|
||||||
outputStream().use { output ->
|
outputStream().use { output ->
|
||||||
|
@ -1334,7 +1334,7 @@ abstract class ImageProvider {
|
||||||
// copy the edited temporary file back to the original
|
// copy the edited temporary file back to the original
|
||||||
editableFile.transferTo(outputStream(context, mimeType, uri, path))
|
editableFile.transferTo(outputStream(context, mimeType, uri, path))
|
||||||
|
|
||||||
if (!types.contains(TYPE_XMP) && isTrailerVideoValid && !checkTrailerOffset(context, path, uri, mimeType, videoSize, editableFile, callback)) {
|
if (!types.contains(TYPE_XMP) && isTrailerVideoValid && !checkTrailerOffset(context, path, uri, mimeType, trailerVideoSize, editableFile, callback)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
editableFile.delete()
|
editableFile.delete()
|
||||||
|
|
Loading…
Reference in a new issue