From 08ea5dc2e777459259e793763a010c64ff1c4c8e Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 22 Sep 2023 19:43:45 +0800 Subject: [PATCH] desktop: ability to send a video (#3102) --- .../common/views/helpers/Utils.android.kt | 2 +- .../simplex/common/views/chat/ChatView.kt | 4 +- .../simplex/common/views/chat/ComposeView.kt | 4 +- .../simplex/common/views/helpers/Utils.kt | 2 +- .../common/platform/VideoPlayer.desktop.kt | 37 ++++++++++--------- .../helpers/ChooseAttachmentView.desktop.kt | 6 ++- .../common/views/helpers/Utils.desktop.kt | 4 +- 7 files changed, 32 insertions(+), 27 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt index e3c857716..127f13cd5 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt @@ -309,7 +309,7 @@ actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean) } } -actual fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration { +actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration { val mmr = MediaMetadataRetriever() mmr.setDataSource(androidAppContext, uri.toUri()) val durationMs = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 3a5e27ed2..e13439e18 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -449,7 +449,7 @@ fun ChatLayout( val images = groups[true] ?: emptyList() val files = groups[false] ?: emptyList() if (images.isNotEmpty()) { - composeState.processPickedMedia(images, null) + CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(images, null) } } else if (files.isNotEmpty()) { composeState.processPickedFile(uris.first(), null) } @@ -459,7 +459,7 @@ fun ChatLayout( tmpFile.deleteOnExit() chatModel.filesToDelete.add(tmpFile) val uri = tmpFile.toURI() - composeState.processPickedMedia(listOf(uri), null) + CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(uri), null) } }, onText = { // Need to parse HTML in order to correctly display the content diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index f26ce0a7a..c6e6ca7b7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -176,7 +176,7 @@ fun MutableState.processPickedFile(uri: URI?, text: String?) { } } -fun MutableState.processPickedMedia(uris: List, text: String?) { +suspend fun MutableState.processPickedMedia(uris: List, text: String?) { val content = ArrayList() val imagesPreview = ArrayList() uris.forEach { uri -> @@ -237,7 +237,7 @@ fun ComposeView( val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) } val recState: MutableState = remember { mutableStateOf(RecordingState.NotStarted) } - AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile, composeState::processPickedMedia) + AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile) { uris, text -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(uris, text) } } fun isSimplexLink(link: String): Boolean = link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 6aaf7a9fd..2aad0bc3d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -267,7 +267,7 @@ fun getMaxFileSize(fileProtocol: FileProtocol): Long { } } -expect fun getBitmapFromVideo(uri: URI, timestamp: Long? = null, random: Boolean = true): VideoPlayerInterface.PreviewAndDuration +expect suspend fun getBitmapFromVideo(uri: URI, timestamp: Long? = null, random: Boolean = true): VideoPlayerInterface.PreviewAndDuration fun Color.darker(factor: Float = 0.1f): Color = Color(max(red * (1 - factor), 0f), max(green * (1 - factor), 0f), max(blue * (1 - factor), 0f), alpha) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt index 1d98c6497..e590c2e20 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt @@ -174,7 +174,7 @@ actual class VideoPlayer actual constructor( private suspend fun setPreviewAndDuration() { // It freezes main thread, doing it in IO thread CoroutineScope(Dispatchers.IO).launch { - val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo() } + val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(defaultPreview, uri) } withContext(Dispatchers.Main) { preview.value = previewAndDuration.preview ?: defaultPreview duration.value = (previewAndDuration.duration ?: 0) @@ -182,23 +182,6 @@ actual class VideoPlayer actual constructor( } } - private suspend fun getBitmapFromVideo(): VideoPlayerInterface.PreviewAndDuration { - val player = CallbackMediaPlayerComponent().mediaPlayer() - val filepath = getAppFilePath(uri) - if (filepath == null || !File(filepath).exists()) { - return VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L) - } - player.media().startPaused(filepath) - val start = System.currentTimeMillis() - while (player.snapshots()?.get() == null && start + 5000 > System.currentTimeMillis()) { - delay(10) - } - val preview = player.snapshots()?.get()?.toComposeImageBitmap() - val duration = player.duration.toLong() - CoroutineScope(Dispatchers.IO).launch { player.release() } - return VideoPlayerInterface.PreviewAndDuration(preview = preview, timestamp = 0L, duration = duration) - } - private fun initializeMediaPlayerComponent(): Component { return if (desktopPlatform.isMac()) { CallbackMediaPlayerComponent() @@ -212,4 +195,22 @@ actual class VideoPlayer actual constructor( is EmbeddedMediaPlayerComponent -> mediaPlayer() else -> error("mediaPlayer() can only be called on vlcj player components") } + + companion object { + suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?): VideoPlayerInterface.PreviewAndDuration { + val player = CallbackMediaPlayerComponent().mediaPlayer() + if (uri == null || !File(uri.rawPath).exists()) { + return VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L) + } + player.media().startPaused(uri.toString().replaceFirst("file:", "file://")) + val start = System.currentTimeMillis() + while (player.snapshots()?.get() == null && start + 5000 > System.currentTimeMillis()) { + delay(10) + } + val preview = player.snapshots()?.get()?.toComposeImageBitmap() + val duration = player.duration.toLong() + CoroutineScope(Dispatchers.IO).launch { player.release() } + return VideoPlayerInterface.PreviewAndDuration(preview = preview, timestamp = 0L, duration = duration) + } + } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.desktop.kt index 32f5a0a80..d125f94e5 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.desktop.kt @@ -11,10 +11,14 @@ import dev.icerock.moko.resources.compose.stringResource @Composable actual fun ChooseAttachmentButtons(attachmentOption: MutableState, hide: () -> Unit) { - ActionButton(Modifier.fillMaxWidth(0.5f), null, stringResource(MR.strings.gallery_image_button), icon = painterResource(MR.images.ic_add_photo)) { + ActionButton(Modifier.fillMaxWidth(0.33f), null, stringResource(MR.strings.gallery_image_button), icon = painterResource(MR.images.ic_add_photo)) { attachmentOption.value = AttachmentOption.GalleryImage hide() } + ActionButton(Modifier.fillMaxWidth(0.5f), null, stringResource(MR.strings.gallery_video_button), icon = painterResource(MR.images.ic_smart_display)) { + attachmentOption.value = AttachmentOption.GalleryVideo + hide() + } ActionButton(Modifier.fillMaxWidth(1f), null, stringResource(MR.strings.choose_file), icon = painterResource(MR.images.ic_note_add)) { attachmentOption.value = AttachmentOption.File hide() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt index cc84e9ac0..21b2cfa6e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt @@ -132,8 +132,8 @@ actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean) } else null } -actual fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration { - return VideoPlayerInterface.PreviewAndDuration(preview = null, timestamp = 0L, duration = 0L) +actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration { + return VideoPlayer.getBitmapFromVideo(null, uri) } @OptIn(ExperimentalEncodingApi::class)