desktop: ability to send a video (#3102)

This commit is contained in:
Stanislav Dmitrenko 2023-09-22 19:43:45 +08:00 committed by GitHub
parent 20f90ee865
commit 08ea5dc2e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 32 additions and 27 deletions

View File

@ -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() val mmr = MediaMetadataRetriever()
mmr.setDataSource(androidAppContext, uri.toUri()) mmr.setDataSource(androidAppContext, uri.toUri())
val durationMs = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() val durationMs = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()

View File

@ -449,7 +449,7 @@ fun ChatLayout(
val images = groups[true] ?: emptyList() val images = groups[true] ?: emptyList()
val files = groups[false] ?: emptyList() val files = groups[false] ?: emptyList()
if (images.isNotEmpty()) { if (images.isNotEmpty()) {
composeState.processPickedMedia(images, null) CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(images, null) }
} else if (files.isNotEmpty()) { } else if (files.isNotEmpty()) {
composeState.processPickedFile(uris.first(), null) composeState.processPickedFile(uris.first(), null)
} }
@ -459,7 +459,7 @@ fun ChatLayout(
tmpFile.deleteOnExit() tmpFile.deleteOnExit()
chatModel.filesToDelete.add(tmpFile) chatModel.filesToDelete.add(tmpFile)
val uri = tmpFile.toURI() val uri = tmpFile.toURI()
composeState.processPickedMedia(listOf(uri), null) CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(uri), null) }
}, },
onText = { onText = {
// Need to parse HTML in order to correctly display the content // Need to parse HTML in order to correctly display the content

View File

@ -176,7 +176,7 @@ fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) {
} }
} }
fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text: String?) { suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text: String?) {
val content = ArrayList<UploadContent>() val content = ArrayList<UploadContent>()
val imagesPreview = ArrayList<String>() val imagesPreview = ArrayList<String>()
uris.forEach { uri -> uris.forEach { uri ->
@ -237,7 +237,7 @@ fun ComposeView(
val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) } val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) }
val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) } val recState: MutableState<RecordingState> = 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 = fun isSimplexLink(link: String): Boolean =
link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true) link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true)

View File

@ -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 = 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) Color(max(red * (1 - factor), 0f), max(green * (1 - factor), 0f), max(blue * (1 - factor), 0f), alpha)

View File

@ -174,7 +174,7 @@ actual class VideoPlayer actual constructor(
private suspend fun setPreviewAndDuration() { private suspend fun setPreviewAndDuration() {
// It freezes main thread, doing it in IO thread // It freezes main thread, doing it in IO thread
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo() } val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(defaultPreview, uri) }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
preview.value = previewAndDuration.preview ?: defaultPreview preview.value = previewAndDuration.preview ?: defaultPreview
duration.value = (previewAndDuration.duration ?: 0) 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 { private fun initializeMediaPlayerComponent(): Component {
return if (desktopPlatform.isMac()) { return if (desktopPlatform.isMac()) {
CallbackMediaPlayerComponent() CallbackMediaPlayerComponent()
@ -212,4 +195,22 @@ actual class VideoPlayer actual constructor(
is EmbeddedMediaPlayerComponent -> mediaPlayer() is EmbeddedMediaPlayerComponent -> mediaPlayer()
else -> error("mediaPlayer() can only be called on vlcj player components") 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)
}
}
} }

View File

@ -11,10 +11,14 @@ import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
actual fun ChooseAttachmentButtons(attachmentOption: MutableState<AttachmentOption?>, hide: () -> Unit) { actual fun ChooseAttachmentButtons(attachmentOption: MutableState<AttachmentOption?>, 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 attachmentOption.value = AttachmentOption.GalleryImage
hide() 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)) { ActionButton(Modifier.fillMaxWidth(1f), null, stringResource(MR.strings.choose_file), icon = painterResource(MR.images.ic_note_add)) {
attachmentOption.value = AttachmentOption.File attachmentOption.value = AttachmentOption.File
hide() hide()

View File

@ -132,8 +132,8 @@ actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean)
} else null } else null
} }
actual fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration { actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration {
return VideoPlayerInterface.PreviewAndDuration(preview = null, timestamp = 0L, duration = 0L) return VideoPlayer.getBitmapFromVideo(null, uri)
} }
@OptIn(ExperimentalEncodingApi::class) @OptIn(ExperimentalEncodingApi::class)