shawshuai
shawshuai

Reputation: 75

Cannot create MotionPhoto, metadata not rightly wrote or the process creating file wrong

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

Answers (0)

Related Questions