Reputation: 75
I create a Android app mean to combine a image file and a video file to a MotionPhoto. I wrote the UI and logic into one file for proof the concept. The code is below.
The problem is: the created file by this code saved to Android phone cannot be recognized as a Image file or a MotionPhoto by Google Photo app. But with File app, can view this image as a static photo.
import android.media.MediaScannerConnection
import android.net.Uri
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.exifinterface.media.ExifInterface
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import coil3.compose.AsyncImage
import java.io.File
import java.io.InputStream
@Composable
fun ImageItem(
getInputStream: (Uri) -> InputStream?
) {
var imageUri by remember { mutableStateOf<Uri?>(null) }
var videoUri by remember { mutableStateOf<Uri?>(null) }
var imageInputStream by remember { mutableStateOf<InputStream?>(null) }
var videoInputStream by remember { mutableStateOf<InputStream?>(null) }
val imagePicker =
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri != null) {
imageUri = uri
imageInputStream = getInputStream(uri)
} else {
Log.d("PhotoPicker", "No media selected")
}
}
val videoPicker =
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri != null) {
videoUri = uri
videoInputStream = getInputStream(uri)
} else {
Log.d("PhotoPicker", "No media selected")
}
}
Card {
Row(
modifier = Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.clip(
RoundedCornerShape(10.dp)
)
.border(
1.dp,
MaterialTheme.colorScheme.surfaceTint,
RoundedCornerShape(10.dp)
)
.clickable {
imagePicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
.size(75.dp),
contentAlignment = Alignment.Center
) {
when (imageUri) {
null -> Image(
painter = painterResource(R.drawable.ic_image),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.surfaceTint)
)
else -> AsyncImage(
model = imageUri,
contentDescription = null,
contentScale = ContentScale.Crop
)
}
}
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.clip(
RoundedCornerShape(10.dp)
)
.border(
1.dp,
MaterialTheme.colorScheme.surfaceTint,
RoundedCornerShape(10.dp)
)
.clickable {
videoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly))
}
.size(75.dp),
contentAlignment = Alignment.Center
) {
when (videoUri) {
null -> Image(
painter = painterResource(R.drawable.ic_video),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.surfaceTint)
)
else -> {
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(videoUri!!))
prepare()
playWhenReady = true
repeatMode = Player.REPEAT_MODE_ONE
}
}
PlayerSurface(
player = exoPlayer,
surfaceType = SURFACE_TYPE_SURFACE_VIEW
)
}
}
}
Spacer(modifier = Modifier.weight(1f))
val context = LocalContext.current
Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
contentDescription = null,
modifier = Modifier.clickable {
if (imageInputStream != null && videoInputStream != null) {
// Create temp JPEG file to store image data and EXIF data
val tempImageFile =
File.createTempFile("temp_image", ".jpg", context.cacheDir)
val motionPhotoFile = File(
context.filesDir,
"dynamic_photo_${System.currentTimeMillis()}.MP.jpg"
)
try {
// Write the image to the temp file
tempImageFile.outputStream().use { imageOutput ->
imageInputStream?.copyTo(imageOutput)
}
// Write image and video to final file
var videoLength: Int
motionPhotoFile.outputStream().use { outputStream ->
// Write image data
tempImageFile.inputStream().use { it.copyTo(outputStream) }
// Record video offset
val videoStartOffset = outputStream.channel.position()
// Write video data
videoInputStream?.copyTo(outputStream)
// Record video length
videoLength = (outputStream.channel.size() - videoStartOffset).toInt()
}
android.util.Log.e("MotionPhoto", "stream size: ${videoInputStream?.readBytes()?.size}, length: $videoLength")
// Add EXIF and XMP metadata to temp file
val exifInterface = ExifInterface(motionPhotoFile)
exifInterface.setAttribute(ExifInterface.TAG_XMP, getXmpMetadata(videoLength))
exifInterface.saveAttributes()
MediaScannerConnection.scanFile(
context,
arrayOf(motionPhotoFile.absolutePath),
arrayOf("image/jpeg", "image/heic")
) { _, uri ->
Log.d("MotionPhoto", "Save success, URI: $uri")
}
} catch (e: Exception) {
Log.e("MotionPhoto", "MotionPhoto save failed: ${e.message}")
} finally {
// delete temp file
tempImageFile.delete()
}
} else {
Log.e("MotionPhoto", "Image or video input stream is null")
}
}
)
Spacer(modifier = Modifier.weight(1f))
Box(
modifier = Modifier
.border(
1.dp,
MaterialTheme.colorScheme.surfaceTint,
RoundedCornerShape(10.dp)
)
.size(75.dp),
contentAlignment = Alignment.Center
) {
when (videoUri) {
else -> Image(
painter = painterResource(R.drawable.ic_image),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.surfaceTint)
)
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun ImageItemPreview() {
ImageItem { null }
}
fun getXmpMetadata(videoLengthInBytes: Int): String {
return """
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.1.0-jc003">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmpNote="http://ns.adobe.com/xmp/note/"
xmlns:Container="http://ns.google.com/photos/1.0/container/"
xmlns:Item="http://ns.google.com/photos/1.0/container/item/"
xmlns:GCamera="http://ns.google.com/photos/1.0/camera/"
xmpNote:HasExtendedXMP="EBED0DCEA400D16C9CA3EEE1FCA3FEB5"
GCamera:MotionPhoto="1"
GCamera:MotionPhotoVersion="1">
<Container:Directory>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<Container:Item
Item:Mime="image/jpeg"
Item:Semantic="Primary" />
</rdf:li>
<rdf:li rdf:parseType="Resource">
<Container:Item
Item:Mime="video/mp4"
Item:Semantic="MotionPhoto"
Item:Length="$videoLengthInBytes"
Item:Padding="0" />
</rdf:li>
</rdf:Seq>
</Container:Directory>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
""".trimIndent()
}
I refered the doc about MotionPhoto format version 1.0 https://developer.android.com/media/platform/motion-photo-format
But there're not sample about create a MotionPhoto.
I read some MotionPhoto file EXIF XMP data known about how the XMP metadata of a MotionPhoto looks like. But I have no idea about how to modify my code above to write them in. Or the wrong place I trying to write the metadata.
The expected is combine a image and a video file to a MotionPhoto, can be recognized by Google Photos.
Upvotes: 0
Views: 29