- Implemented audio support with Video.js.

What changed:
Added uploaded/direct audio playback paths in 
[index.php](/Users/tyemeclifford/Documents/GH/mediaplayer/index.php) and 
[embed.php](/Users/tyemeclifford/Documents/GH/mediaplayer/embed.php).
Added common audio upload support: MP3, M4A, AAC, WAV, FLAC, OGG, OGA, 
OPUS.
Added waveform poster generation for uploaded audio via ffmpeg, stored 
like normal thumbnails under media/thumbs/.
Updated add/edit forms to accept media files, not just video files.
Added audio-friendly labels like Audio, 320kbps, 256kbps, 128kbps.
Updated docs and changelog.
This commit is contained in:
Ty Clifford
2026-06-25 13:34:20 -04:00
parent 8a39bc02fa
commit 7deae1a399
10 changed files with 365 additions and 80 deletions
+122 -5
View File
@@ -321,6 +321,58 @@ function make_slug(string $title, int $id = 0): string {
return $slug ?: 'video-' . time();
}
function media_video_upload_extensions(): array {
return ['mp4', 'm4v', 'webm', 'ogv', 'mov', 'mkv', 'avi'];
}
function media_audio_upload_extensions(): array {
return ['mp3', 'm4a', 'aac', 'wav', 'wave', 'flac', 'ogg', 'oga', 'opus'];
}
function media_allowed_upload_extensions(): array {
return array_values(array_unique(array_merge(media_video_upload_extensions(), media_audio_upload_extensions())));
}
function media_upload_extension_allowed(string $ext): bool {
return in_array(strtolower(trim($ext)), media_allowed_upload_extensions(), true);
}
function media_upload_accept_attribute(): string {
return implode(',', [
'video/mp4',
'video/webm',
'video/ogg',
'video/quicktime',
'audio/mpeg',
'audio/mp4',
'audio/aac',
'audio/wav',
'audio/x-wav',
'audio/flac',
'audio/ogg',
'.mp4',
'.m4v',
'.webm',
'.ogv',
'.mov',
'.mkv',
'.avi',
'.mp3',
'.m4a',
'.aac',
'.wav',
'.wave',
'.flac',
'.ogg',
'.oga',
'.opus',
]);
}
function media_upload_format_summary(): string {
return 'MP4, M4V, WebM, OGV, MOV, MKV, AVI, MP3, M4A, AAC, WAV, FLAC, OGG, OPUS';
}
function mime_from_ext(string $path): string {
$url_path = parse_url($path, PHP_URL_PATH);
if (is_string($url_path) && $url_path !== '') {
@@ -332,7 +384,6 @@ function mime_from_ext(string $path): string {
'm4v' => 'video/mp4',
'webm' => 'video/webm',
'ogv' => 'video/ogg',
'ogg' => 'video/ogg',
'mov' => 'video/quicktime',
'mkv' => 'video/x-matroska',
'avi' => 'video/x-msvideo',
@@ -342,11 +393,41 @@ function mime_from_ext(string $path): string {
'm4a' => 'audio/mp4',
'aac' => 'audio/aac',
'wav' => 'audio/wav',
'wave' => 'audio/wav',
'flac' => 'audio/flac',
'ogg' => 'audio/ogg',
'oga' => 'audio/ogg',
'opus' => 'audio/ogg',
default => 'video/mp4',
};
}
function media_mime_from_local_file(string $path, string $fallback_name = ''): string {
$mime = '';
if (is_file($path) && function_exists('finfo_open')) {
$finfo = @finfo_open(FILEINFO_MIME_TYPE);
if ($finfo) {
$detected = @finfo_file($finfo, $path);
if (is_string($detected)) $mime = strtolower(trim($detected));
@finfo_close($finfo);
}
}
$fallback_source = $fallback_name !== '' ? $fallback_name : $path;
$fallback = mime_from_ext($fallback_source);
$ext = strtolower(pathinfo($fallback_source, PATHINFO_EXTENSION));
if ($mime === '' || in_array($mime, ['application/octet-stream', 'binary/octet-stream'], true)) return $fallback;
if ($mime === 'audio/x-wav' || $mime === 'audio/vnd.wave') return 'audio/wav';
if ($mime === 'audio/x-flac') return 'audio/flac';
if ($mime === 'video/x-msvideo') return 'video/x-msvideo';
if ($mime === 'application/ogg') {
return $ext === 'ogv' ? 'video/ogg' : 'audio/ogg';
}
if (in_array($ext, media_audio_upload_extensions(), true) && str_starts_with($mime, 'video/')) return $fallback;
if (str_starts_with($mime, 'audio/') || str_starts_with($mime, 'video/')) return $mime;
return $fallback;
}
function normalize_media_url(string $url): string {
$url = trim($url);
if ($url === '') return '';
@@ -389,11 +470,12 @@ function media_extension_from_url(string $url): string {
function media_guess_mime(string $url): string {
$map = [
'mp4' => 'video/mp4', 'm4v' => 'video/mp4', 'webm' => 'video/webm',
'ogv' => 'video/ogg', 'ogg' => 'video/ogg', 'mov' => 'video/quicktime',
'ogv' => 'video/ogg', 'mov' => 'video/quicktime',
'mkv' => 'video/x-matroska', 'avi' => 'video/x-msvideo',
'm3u8' => 'application/vnd.apple.mpegurl', 'mpd' => 'application/dash+xml',
'mp3' => 'audio/mpeg', 'm4a' => 'audio/mp4', 'aac' => 'audio/aac',
'wav' => 'audio/wav', 'flac' => 'audio/flac',
'wav' => 'audio/wav', 'wave' => 'audio/wav', 'flac' => 'audio/flac',
'ogg' => 'audio/ogg', 'oga' => 'audio/ogg', 'opus' => 'audio/ogg',
];
return $map[media_extension_from_url($url)] ?? '';
}
@@ -1182,6 +1264,37 @@ function media_thumbnail_from_local_video(string $file_path, string $slug): stri
return '';
}
function media_waveform_from_local_audio(string $file_path, string $slug): string {
$ffmpeg = media_ffmpeg_path();
if ($ffmpeg === '') return '';
$source = MEDIA_DIR . ltrim($file_path, '/');
if ($file_path === '' || !is_file($source) || !is_readable($source)) return '';
$mime = media_mime_from_local_file($source, $file_path);
if (media_classify_kind($file_path, $mime) !== 'audio') return '';
$thumb_dir = MEDIA_DIR . 'thumbs/';
if (!is_dir($thumb_dir)) mkdir($thumb_dir, 0755, true);
$fname = $slug . '_waveform_' . time() . '_' . substr(sha1($file_path), 0, 8) . '.png';
$target = $thumb_dir . $fname;
if (!function_exists('exec') || media_disabled_function('exec')) return '';
$filter = '[0:a]aformat=channel_layouts=mono,showwavespic=s=1280x720:colors=0x00e87a[wave];'
. 'color=c=0x09090f:s=1280x720[bg];'
. '[bg][wave]overlay=shortest=1,drawbox=x=0:y=0:w=iw:h=2:color=0xb060ff@0.9:t=fill';
$cmd = escapeshellarg($ffmpeg) . ' -y -i ' . escapeshellarg($source)
. ' -filter_complex ' . escapeshellarg($filter)
. ' -frames:v 1 ' . escapeshellarg($target) . ' 2>/dev/null';
@exec($cmd, $output, $code);
if ($code === 0 && is_file($target) && filesize($target) > 0 && (!function_exists('getimagesize') || @getimagesize($target))) {
return 'thumbs/' . $fname;
}
if (is_file($target)) unlink($target);
return '';
}
function media_auto_thumbnail_for_video(int $video_id, string $slug): string {
$stmt = get_db()->prepare("SELECT * FROM video_sources WHERE video_id=? ORDER BY sort_order ASC, id ASC");
$stmt->execute([$video_id]);
@@ -1196,6 +1309,8 @@ function media_auto_thumbnail_for_video(int $video_id, string $slug): string {
foreach ($sources as $source) {
if (!source_is_local($source)) continue;
$thumbnail = media_waveform_from_local_audio($source['file_path'] ?? '', $slug);
if ($thumbnail !== '') return $thumbnail;
$thumbnail = media_thumbnail_from_local_video($source['file_path'] ?? '', $slug);
if ($thumbnail !== '') return $thumbnail;
}
@@ -1352,8 +1467,8 @@ function media_classify_kind(string $url, string $mime, string $forced_mode = 'a
if ($ext === 'mpd' || $mime_lc === 'application/dash+xml') return 'dash';
if (str_starts_with($mime_lc, 'audio/')) return 'audio';
if (str_starts_with($mime_lc, 'video/')) return 'video';
if (in_array($ext, ['mp3', 'm4a', 'aac', 'wav', 'flac', 'oga', 'opus'], true)) return 'audio';
if (in_array($ext, ['mp4', 'm4v', 'webm', 'ogv', 'ogg', 'mov', 'mkv', 'avi', 'wmv', 'flv', 'ts', '3gp', '3g2'], true)) return 'video';
if (in_array($ext, ['mp3', 'm4a', 'aac', 'wav', 'wave', 'flac', 'ogg', 'oga', 'opus'], true)) return 'audio';
if (in_array($ext, ['mp4', 'm4v', 'webm', 'ogv', 'mov', 'mkv', 'avi', 'wmv', 'flv', 'ts', '3gp', '3g2'], true)) return 'video';
return 'unknown';
}
@@ -1429,6 +1544,8 @@ function source_provider_label(array $source): string {
if (!$player) return source_is_remote($source) ? 'External URL' : 'Uploaded File';
if (($player['provider'] ?? '') !== '') return ucwords(str_replace('-', ' ', $player['provider'])) . ' Embed';
if (($player['kind'] ?? '') === 'iframe') return 'External Embed';
if (($player['kind'] ?? '') === 'audio') return source_is_remote($source) ? 'External Audio URL' : 'Uploaded Audio';
if (($player['kind'] ?? '') === 'video') return source_is_remote($source) ? 'External Video URL' : 'Uploaded Video';
return source_is_remote($source) ? 'External URL' : 'Uploaded File';
}