Merge pull request #1443 from davotoula/capture-and-post-video

Capture and post video
This commit is contained in:
Vitor Pamplona
2025-08-30 09:34:14 -04:00
committed by GitHub
13 changed files with 195 additions and 9 deletions
@@ -84,14 +84,15 @@ class MediaCompressor {
val videoQuality =
when (mediaQuality) {
CompressorQuality.VERY_LOW -> VideoQuality.VERY_LOW
CompressorQuality.LOW -> VideoQuality.LOW
// Override user selection LOW to use VERY_LOW for better video streaming experience
CompressorQuality.LOW -> VideoQuality.VERY_LOW
CompressorQuality.MEDIUM -> VideoQuality.MEDIUM
CompressorQuality.HIGH -> VideoQuality.HIGH
CompressorQuality.VERY_HIGH -> VideoQuality.VERY_HIGH
else -> VideoQuality.MEDIUM
}
Log.d("MediaCompressor", "Using video compression $mediaQuality")
Log.d("MediaCompressor", "Using video compression $videoQuality")
val result =
withTimeoutOrNull(30000) {
@@ -29,6 +29,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -136,14 +137,114 @@ fun PictureButton(onClick: () -> Unit) {
}
}
fun getPhotoUri(context: Context): Uri {
fun getPhotoUri(context: Context): Uri =
getMediaUri(
context = context,
filePrefix = "JPEG",
fileExtension = ".jpg",
storageDir = Environment.DIRECTORY_PICTURES,
)
@Composable
fun TakeVideoButton(onVideoTaken: (ImmutableList<SelectedMedia>) -> Unit) {
var showCamera by remember { mutableStateOf(false) }
if (showCamera) {
TakeVideo(
onVideoTaken = { uri ->
showCamera = false
if (uri.isNotEmpty()) {
onVideoTaken(uri)
}
},
)
}
VideoButton { showCamera = true }
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun TakeVideo(onVideoTaken: (ImmutableList<SelectedMedia>) -> Unit) {
val context = LocalContext.current
var videoUri by remember { mutableStateOf<Uri?>(null) }
val scope = rememberCoroutineScope()
val launcher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.CaptureVideo(),
) { success ->
if (success) {
videoUri?.let {
onVideoTaken(persistentListOf(SelectedMedia(it, "video/mp4")))
}
} else {
onVideoTaken(persistentListOf())
}
videoUri = null
}
val cameraPermissionState =
rememberPermissionState(
Manifest.permission.CAMERA,
onPermissionResult = {
if (it) {
scope.launch(Dispatchers.IO) {
videoUri = getVideoUri(context)
videoUri?.let { launcher.launch(it) }
}
}
},
)
if (cameraPermissionState.status.isGranted) {
LaunchedEffect(key1 = Unit) {
launch(Dispatchers.IO) {
videoUri = getVideoUri(context)
videoUri?.let { launcher.launch(it) }
}
}
} else {
LaunchedEffect(key1 = Unit) {
cameraPermissionState.launchPermissionRequest()
}
}
}
@Composable
fun VideoButton(onClick: () -> Unit) {
IconButton(
onClick = onClick,
) {
Icon(
imageVector = Icons.Default.Videocam,
contentDescription = stringRes(id = R.string.record_a_video),
modifier = Modifier.height(22.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
}
}
fun getVideoUri(context: Context): Uri =
getMediaUri(
context = context,
filePrefix = "MP4",
fileExtension = ".mp4",
storageDir = Environment.DIRECTORY_MOVIES,
)
private fun getMediaUri(
context: Context,
filePrefix: String,
fileExtension: String,
storageDir: String,
): Uri {
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val storageDirectory: File? = context.getExternalFilesDir(storageDir)
return File
.createTempFile(
"JPEG_${timeStamp}_",
".jpg",
storageDir,
"${filePrefix}_${timeStamp}_",
fileExtension,
storageDirectory,
).let {
FileProvider.getUriForFile(
context,
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
@@ -390,6 +391,12 @@ private fun BottomRowActions(postViewModel: CommentPostViewModel) {
},
)
TakeVideoButton(
onVideoTaken = {
postViewModel.selectImage(it)
},
)
ForwardZapToButton(postViewModel.wantsForwardZapTo) {
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
}
@@ -41,6 +41,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddPhotoAlternate
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material.icons.outlined.CameraAlt
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
@@ -85,6 +86,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -408,7 +410,26 @@ private fun BottomRowActions(
) {
Icon(
imageVector = Icons.Outlined.CameraAlt,
contentDescription = stringRes(id = R.string.upload_image),
contentDescription = stringRes(id = R.string.take_a_picture),
modifier = Modifier.height(22.dp),
tint = MaterialTheme.colorScheme.placeholderText,
)
}
}
if (postViewModel.room != null) {
TakeVideoButton(
onVideoTaken = { postViewModel.pickedMedia(it) },
)
} else {
IconButton(
onClick = {
accountViewModel.toastManager.toast(R.string.messages_cant_upload_title, R.string.messages_cant_upload_explainer)
},
) {
Icon(
imageVector = Icons.Default.Videocam,
contentDescription = stringRes(id = R.string.record_a_video),
modifier = Modifier.height(22.dp),
tint = MaterialTheme.colorScheme.placeholderText,
)
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
@@ -370,6 +371,12 @@ private fun BottomRowActions(postViewModel: NewProductViewModel) {
},
)
TakeVideoButton(
onVideoTaken = {
postViewModel.selectImage(it)
},
)
ForwardZapToButton(postViewModel.wantsForwardZapTo) {
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
}
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.components.getActivity
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
@@ -431,6 +432,12 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) {
},
)
TakeVideoButton(
onVideoTaken = {
postViewModel.selectImage(it)
},
)
if (postViewModel.canUsePoll) {
// These should be hashtag recommendations the user selects in the future.
// val hashtag = stringRes(R.string.poll_hashtag)
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
@@ -320,6 +321,12 @@ private fun BottomRowActions(
},
)
TakeVideoButton(
onVideoTaken = {
postViewModel.selectImage(it)
},
)
ForwardZapToButton(postViewModel.wantsForwardZapTo) {
postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo
}
@@ -33,6 +33,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddPhotoAlternate
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
@@ -53,6 +54,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewMediaView
import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePicture
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideo
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -78,6 +80,8 @@ fun NewImageButton(
var wantsToPostFromCamera by remember { mutableStateOf(false) }
var wantsToPostFromVideo by remember { mutableStateOf(false) }
var pickedURIs by remember { mutableStateOf<ImmutableList<SelectedMedia>>(persistentListOf()) }
val scope = rememberCoroutineScope()
@@ -97,6 +101,13 @@ fun NewImageButton(
}
}
if (wantsToPostFromVideo) {
TakeVideo { uri ->
wantsToPostFromVideo = false
pickedURIs = uri
}
}
if (wantsToPostFromGallery) {
GallerySelect(
onImageUri = { uri ->
@@ -134,7 +145,26 @@ fun NewImageButton(
) {
Icon(
imageVector = Icons.Default.CameraAlt,
contentDescription = stringRes(id = R.string.upload_image),
contentDescription = stringRes(id = R.string.take_a_picture),
modifier = Modifier.size(26.dp),
tint = Color.White,
)
}
Spacer(modifier = Modifier.height(20.dp))
FloatingActionButton(
onClick = {
wantsToPostFromVideo = true
isOpen = false
},
modifier = Size55Modifier,
shape = CircleShape,
containerColor = MaterialTheme.colorScheme.primary,
) {
Icon(
imageVector = Icons.Default.Videocam,
contentDescription = stringRes(id = R.string.record_a_video),
modifier = Modifier.size(26.dp),
tint = Color.White,
)
@@ -148,6 +148,7 @@
<string name="failed_to_save_the_video">Nepodařilo se uložit video</string>
<string name="upload_image">Nahrát obrázek</string>
<string name="take_a_picture">Pořiďte fotografii</string>
<string name="record_a_video">Nahrát video</string>
<string name="record_a_message">Nahrajte zprávu</string>
<string name="record_a_message_title">Nahrajte zprávu</string>
<string name="record_a_message_description">Stiskněte a podržte pro nahrání zprávy</string>
@@ -947,6 +947,7 @@ anz der Bedingungen ist erforderlich</string>
<string name="signer_not_found_exception_description">Wurde die Signierer-App deinstalliert? Überprüfe, ob sie installiert ist und dieses Konto enthält. Melde dich ab und wieder an, falls sich die App geändert hat.</string>
<string name="no_description">Keine Beschreibung gefunden</string>
<string name="take_a_picture">Ein Foto aufnehmen</string>
<string name="record_a_video">Video aufnehmen</string>
<string name="record_a_message">Eine Nachricht aufnehmen</string>
<string name="record_a_message_title">Eine Nachricht aufnehmen</string>
<string name="record_a_message_description">Zum Aufnehmen einer Nachricht gedrückt halten</string>
@@ -148,6 +148,7 @@
<string name="failed_to_save_the_video">Falha ao salvar o vídeo</string>
<string name="upload_image">Enviar Imagem</string>
<string name="take_a_picture">Tirar uma foto</string>
<string name="record_a_video">Gravar um vídeo</string>
<string name="record_a_message">Gravar uma mensagem</string>
<string name="record_a_message_title">Gravar uma mensagem</string>
<string name="record_a_message_description">Clique e segure para gravar uma mensagem</string>
@@ -148,6 +148,7 @@
<string name="failed_to_save_the_video">Det gick inte att spara videon</string>
<string name="upload_image">Ladda upp bilden</string>
<string name="take_a_picture">Ta en bild</string>
<string name="record_a_video">Spela in video</string>
<string name="record_a_message">Spela in ett meddelande</string>
<string name="record_a_message_title">Spela in ett meddelande</string>
<string name="record_a_message_description">Tryck och håll in för att spela in ett meddelande</string>
+1
View File
@@ -160,6 +160,7 @@
<string name="failed_to_save_the_video">Failed to save the video</string>
<string name="upload_image">Upload Image</string>
<string name="take_a_picture">Take a picture</string>
<string name="record_a_video">Record a video</string>
<string name="record_a_message">Record a message</string>
<string name="record_a_message_title">Record a message</string>
<string name="record_a_message_description">Click and hold to record a message</string>