added audio and video messages
This commit is contained in:
Generated
+208
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -206,6 +206,22 @@
|
||||
>
|
||||
+
|
||||
</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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user