added audio and video messages
Continuous integration / Check (push) Successful in 39s
Continuous integration / Rustfmt (push) Successful in 34s
Continuous integration / build (push) Successful in 37s
Continuous integration / build-release (push) Successful in 1m3s

This commit is contained in:
wt
2026-02-15 17:40:32 +07:00
parent 9016889a78
commit 78ca22e1b7
6 changed files with 492 additions and 7 deletions
Generated
+208
View File
@@ -47,6 +47,12 @@ dependencies = [
"libc",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -527,6 +533,12 @@ dependencies = [
"zune-inflate",
]
[[package]]
name = "extended"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -1262,6 +1274,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"symphonia",
"tokio",
"tower-http",
"tracing",
@@ -2188,6 +2201,201 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "symphonia"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039"
dependencies = [
"lazy_static",
"symphonia-bundle-flac",
"symphonia-bundle-mp3",
"symphonia-codec-aac",
"symphonia-codec-adpcm",
"symphonia-codec-alac",
"symphonia-codec-pcm",
"symphonia-codec-vorbis",
"symphonia-core",
"symphonia-format-caf",
"symphonia-format-isomp4",
"symphonia-format-mkv",
"symphonia-format-ogg",
"symphonia-format-riff",
"symphonia-metadata",
]
[[package]]
name = "symphonia-bundle-flac"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-bundle-mp3"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-codec-aac"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-adpcm"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f"
dependencies = [
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-alac"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8413fa754942ac16a73634c9dfd1500ed5c61430956b33728567f667fdd393ab"
dependencies = [
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-pcm"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95"
dependencies = [
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-vorbis"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73"
dependencies = [
"log",
"symphonia-core",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-core"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af"
dependencies = [
"arrayvec",
"bitflags 1.3.2",
"bytemuck",
"lazy_static",
"log",
]
[[package]]
name = "symphonia-format-caf"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8faf379316b6b6e6bbc274d00e7a592e0d63ff1a7e182ce8ba25e24edd3d096"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-format-isomp4"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5"
dependencies = [
"encoding_rs",
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-mkv"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-ogg"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-riff"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f"
dependencies = [
"extended",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-metadata"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16"
dependencies = [
"encoding_rs",
"lazy_static",
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-utils-xiph"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16"
dependencies = [
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "syn"
version = "1.0.109"
+2 -1
View File
@@ -18,4 +18,5 @@ rand = "0.8"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4"] }
image = "0.24"
config = "0.13"
config = "0.13"
symphonia = { version = "0.5", features = ["all"] }
+56 -6
View File
@@ -6,6 +6,9 @@ use axum::response::Json;
use image::GenericImageView;
use std::path::Path;
use std::sync::Arc;
use symphonia::core::formats::FormatOptions;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
use tokio::fs;
use tokio::io::AsyncWriteExt;
@@ -180,13 +183,14 @@ pub async fn upload_file_handler(
}))
}
FileType::Video => {
// For now, we'll just return basic info for videos
// In a real implementation, you might want to extract duration using ffmpeg
// Extract duration for video files
let duration = extract_duration(&filepath).await;
let video_info = VideoInfo {
filename: filename.clone(),
filepath: filepathweb.clone(),
size,
duration: None, // Duration extraction would require additional libraries
duration,
mime_type: content_type.clone(),
};
@@ -197,13 +201,14 @@ pub async fn upload_file_handler(
}))
}
FileType::Audio => {
// For now, we'll just return basic info for audio
// In a real implementation, you might want to extract duration using ffmpeg
// Extract duration for audio files
let duration = extract_duration(&filepath).await;
let audio_info = AudioInfo {
filename: filename.clone(),
filepath: filepathweb.clone(),
size,
duration: None, // Duration extraction would require additional libraries
duration,
mime_type: content_type.clone(),
};
@@ -242,6 +247,51 @@ enum FileType {
Generic,
}
// Function to extract duration from audio/video files
async fn extract_duration(filepath: &str) -> Option<f64> {
use symphonia::core::io::MediaSourceStream;
let file = match std::fs::File::open(filepath) {
Ok(file) => file,
Err(_) => return None,
};
let mss = MediaSourceStream::new(Box::new(file), Default::default());
let mut hint = Hint::new();
if let Some(ext) = Path::new(filepath).extension().and_then(|ext| ext.to_str()) {
hint.with_extension(ext);
}
let format_opts = FormatOptions::default();
let metadata_opts = MetadataOptions::default();
let probed =
match symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_opts) {
Ok(probed) => probed,
Err(_) => return None,
};
let track = match probed
.format
.tracks()
.iter()
.find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL)
{
Some(track) => track,
None => return None,
};
let duration = track.codec_params.time_base.and_then(|tb| {
track
.codec_params
.n_frames
.and_then(|frames| Some(tb.calc_time(frames).seconds as f64))
});
duration
}
fn determine_file_type(content_type: &str, filename: &str) -> Option<FileType> {
let lower_ct = content_type.to_lowercase();
let lower_fn = filename.to_lowercase();
+47
View File
@@ -662,6 +662,53 @@ body {
box-shadow: none;
}
.record-button {
padding: 12px 20px;
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
flex-shrink: 0;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.record-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(231, 76, 60, 0.4);
}
.record-button:disabled {
background: var(--bg-tertiary);
color: var(--text-secondary);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.record-button.recording {
background: linear-gradient(135deg, #ff6b6b, #ff5252);
animation: pulse 1s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(231, 76, 60, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(231, 76, 60, 0);
}
}
.system-message {
text-align: center;
color: var(--text-secondary);
+17
View File
@@ -206,6 +206,22 @@
>
&plus;
</button>
<button
id="recordVoiceBtn"
class="record-button"
title="Record voice message"
onclick="toggleVoiceRecording()"
>
●
</button>
<button
id="recordVideoBtn"
class="record-button"
title="Record video message"
onclick="toggleVideoRecording()"
>
◉
</button>
<button
onclick="sendMessage()"
class="send-button"
@@ -355,5 +371,6 @@
<script src="js/deletemodal.js"></script>
<script src="js/search.js"></script>
<script src="js/avatar.js"></script>
<script src="js/recording.js"></script>
</body>
</html>
+162
View File
@@ -0,0 +1,162 @@
// Voice and video recording functionality
let mediaRecorder;
let recordedChunks = [];
let isRecording = false;
let recordingType = null; // 'voice' or 'video'
// Toggle voice recording
async function toggleVoiceRecording() {
if (isRecording) {
stopRecording();
} else {
await startVoiceRecording();
}
}
// Toggle video recording
async function toggleVideoRecording() {
if (isRecording) {
stopRecording();
} else {
await startVideoRecording();
}
}
// Start voice recording
async function startVoiceRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
recordedChunks = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.onstop = async () => {
const blob = new Blob(recordedChunks, { type: 'audio/webm' });
// Create a temporary file object
const file = new File([blob], `voice_message_${Date.now()}.webm`, { type: 'audio/webm' });
// Send the voice message
await sendFileMessage(file);
// Stop the tracks
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
isRecording = true;
recordingType = 'voice';
// Update UI to show recording state
updateRecordingUI(true);
console.log('Voice recording started');
} catch (error) {
console.error('Error starting voice recording:', error);
showAlert('Microphone access denied. Please allow microphone access to record voice messages.', 'error');
}
}
// Start video recording
async function startVideoRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: { ideal: 640 }, height: { ideal: 480 } },
audio: true
});
mediaRecorder = new MediaRecorder(stream);
recordedChunks = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.onstop = async () => {
const blob = new Blob(recordedChunks, { type: 'video/webm' });
// Create a temporary file object
const file = new File([blob], `video_message_${Date.now()}.webm`, { type: 'video/webm' });
// Send the video message
await sendFileMessage(file);
// Stop the tracks
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
isRecording = true;
recordingType = 'video';
// Update UI to show recording state
updateRecordingUI(true);
console.log('Video recording started');
} catch (error) {
console.error('Error starting video recording:', error);
showAlert('Camera access denied. Please allow camera and microphone access to record video messages.', 'error');
}
}
// Stop recording
function stopRecording() {
if (mediaRecorder && isRecording) {
mediaRecorder.stop();
isRecording = false;
recordingType = null;
// Update UI to show stopped state
updateRecordingUI(false);
console.log('Recording stopped');
}
}
// Update UI based on recording state
function updateRecordingUI(isRecording) {
const voiceBtn = document.getElementById('recordVoiceBtn');
const videoBtn = document.getElementById('recordVideoBtn');
if (isRecording) {
// Add recording class to the active button
if (recordingType === 'voice') {
voiceBtn.classList.add('recording');
voiceBtn.textContent = '●'; // Solid circle to indicate recording
} else if (recordingType === 'video') {
videoBtn.classList.add('recording');
videoBtn.textContent = '●'; // Solid circle to indicate recording
}
} else {
// Remove recording class from both buttons
voiceBtn.classList.remove('recording');
videoBtn.classList.remove('recording');
// Reset button texts
voiceBtn.textContent = '●';
videoBtn.textContent = '◉';
}
}
// Handle window unload to clean up resources
window.addEventListener('beforeunload', () => {
if (isRecording) {
stopRecording();
}
});
// Also stop recording when leaving the page
window.addEventListener('pagehide', () => {
if (isRecording) {
stopRecording();
}
});