- Implemented.

Safari/iPhone recording now uses MP4/AAC when WebM is unavailable.
WebM remains the default when supported and FFmpeg is disabled.
Added optional FFmpeg profiles: recommended M4A/AAC-LC or requested 
MP4/H.264 + AAC.
Added MIME-aware, playsinline audio playback.
Added dashboard controls under Plans & Platform → Voice Transcoding / 
FFmpeg.
Added Apache/Nginx media MIME configuration and documentation.
Core implementation: voice_transcoder.php (line 72), messages.php (line 
86), and cyberchat-app.js (line 635).
This commit is contained in:
Ty Clifford
2026-06-09 02:21:42 -04:00
parent ae0fb45c48
commit 6217a216a7
13 changed files with 325 additions and 21 deletions
+28 -5
View File
@@ -1,6 +1,7 @@
<?php
define('CYBERCHAT_API', true);
require_once __DIR__ . '/../bootstrap.php';
require_once ROOT_DIR . '/lib/voice_transcoder.php';
sendCorsHeaders();
try { archiveYesterdayIfNeeded(); } catch (Throwable $e) { error_log($e->getMessage()); }
@@ -83,26 +84,48 @@ function sendVoiceMessage(): never {
}
$mime = class_exists('finfo') ? (string)(new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']) : '';
$header = (string)file_get_contents($file['tmp_name'], false, null, 0, 12);
$header = (string)file_get_contents($file['tmp_name'], false, null, 0, 16);
if (str_starts_with($header, "\x1A\x45\xDF\xA3")) $mime = 'audio/webm';
elseif (str_starts_with($header, 'RIFF') && substr($header, 8, 4) === 'WAVE') $mime = 'audio/wav';
elseif (strlen($header) >= 8 && substr($header, 4, 4) === 'ftyp') $mime = 'audio/mp4';
elseif (strlen($header) >= 2 && ord($header[0]) === 0xFF && (ord($header[1]) & 0xF0) === 0xF0) $mime = 'audio/aac';
$allowed = [
'audio/webm' => 'webm',
'video/webm' => 'webm',
'audio/wav' => 'wav',
'audio/x-wav' => 'wav',
'audio/wave' => 'wav',
'audio/mp4' => 'm4a',
'video/mp4' => 'm4a',
'audio/aac' => 'aac',
'audio/x-aac' => 'aac',
];
if (!isset($allowed[$mime])) jsonResponse(['error' => 'Voice clips must be WAV or WebM'], 400);
if (!isset($allowed[$mime])) jsonResponse(['error' => 'Voice clips must be WebM, MP4/M4A, AAC, or WAV'], 400);
$dir = voiceUploadDir();
ensureWritableDirectory($dir, 'Voice storage');
$filename = bin2hex(random_bytes(20)) . '.' . $allowed[$mime];
if (!move_uploaded_file($file['tmp_name'], $dir . '/' . $filename)) {
$token = bin2hex(random_bytes(20));
$sourceFilename = $token . '.' . $allowed[$mime];
$sourcePath = $dir . '/' . $sourceFilename;
if (!move_uploaded_file($file['tmp_name'], $sourcePath)) {
jsonResponse(['error' => 'Could not store voice clip'], 500);
}
$storedMime = $mime === 'video/webm' ? 'audio/webm' : $mime;
$sourceMime = match ($mime) {
'video/webm' => 'audio/webm',
'video/mp4' => 'audio/mp4',
'audio/x-wav', 'audio/wave' => 'audio/wav',
'audio/x-aac' => 'audio/aac',
default => $mime,
};
try {
$stored = transcodeVoiceRecording($sourcePath, $sourceFilename, $sourceMime);
} catch (Throwable $e) {
if (is_file($sourcePath)) @unlink($sourcePath);
throw $e;
}
$filename = $stored['filename'];
$storedMime = $stored['mime'];
$db = getDB();
try {
$db->prepare("INSERT INTO messages