- Removed the audio additions and reverted the app back to the prior video-focused behavior.

Audio upload support, waveform generation, Video.js audio rendering 
branches, audio quality labels, and “Add Media” wording were removed 
from:
[includes/db.php](/Users/tyemeclifford/Documents/GH/mediaplayer/includes/db.php), 
[admin/add.php](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/add.php), 
[admin/edit.php](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/edit.php), 
[index.php](/Users/tyemeclifford/Documents/GH/mediaplayer/index.php), 
[embed.php](/Users/tyemeclifford/Documents/GH/mediaplayer/embed.php), 
README, and changelog.
This commit is contained in:
Ty Clifford
2026-06-25 14:13:45 -04:00
parent 7deae1a399
commit 57d3c81dad
10 changed files with 80 additions and 365 deletions
+8 -11
View File
@@ -44,16 +44,16 @@ Styled to match the existing `gate.php` / `index.php` dark theme exactly.
│ └── layout.php ← Shared HTML head, topbar, footer, pagination │ └── layout.php ← Shared HTML head, topbar, footer, pagination
├── admin/ ├── admin/
│ ├── index.php ← Video list with search + pagination │ ├── index.php ← Video list with search + pagination
│ ├── add.php ← Add new media with uploaded or external URL sources │ ├── add.php ← Add new video with uploaded or external URL sources
│ ├── edit.php ← Edit media metadata + manage local/remote sources │ ├── edit.php ← Edit video metadata + manage local/remote sources
│ ├── youtube_import.php ← Scrape a YouTube channel and import videos │ ├── youtube_import.php ← Scrape a YouTube channel and import videos
│ ├── settings.php ← Site settings, per-page count, password │ ├── settings.php ← Site settings, per-page count, password
│ ├── login.php ← Admin login │ ├── login.php ← Admin login
│ ├── logout.php ← Session destroy + redirect │ ├── logout.php ← Session destroy + redirect
│ └── auth.php ← Session auth helpers │ └── auth.php ← Session auth helpers
├── media/ ← All media files (created automatically) ├── media/ ← All media files (created automatically)
│ ├── videos/ ← Uploaded media files │ ├── videos/ ← Uploaded video files
│ └── thumbs/ ← Uploaded thumbnails and generated waveforms │ └── thumbs/ ← Uploaded thumbnail images
└── data/ └── data/
└── media.db ← SQLite database (auto-created) └── media.db ← SQLite database (auto-created)
``` ```
@@ -62,19 +62,18 @@ Styled to match the existing `gate.php` / `index.php` dark theme exactly.
## Usage ## Usage
### Adding Media (Admin) ### Adding Videos (Admin)
1. Go to `admin/add.php` 1. Go to `admin/add.php`
2. Fill in title, description, duration, sort order, or paste an external URL and click **Fetch Info** to auto-fill readable metadata 2. Fill in title, description, duration, sort order, or paste an external URL and click **Fetch Info** to auto-fill readable metadata
3. Upload a thumbnail (JPG/PNG/WebP) 3. Upload a thumbnail (JPG/PNG/WebP)
4. Add one or more media sources. Each source can be either: 4. Add one or more video sources. Each source can be either:
- an uploaded local video or audio file, stored under `media/videos/` - an uploaded local video file, stored under `media/videos/`
- an external URL, stored in SQLite and played as a direct media source or provider embed - an external URL, stored in SQLite and played as a direct media source or provider embed
5. For uploaded files, supported formats include: 5. For uploaded files, supported formats include:
- **MP4** (H.264) — widest compatibility - **MP4** (H.264) — widest compatibility
- **WebM** (VP9) — smaller size, modern browsers - **WebM** (VP9) — smaller size, modern browsers
- **OGV** — Firefox fallback (legacy) - **OGV** — Firefox fallback (legacy)
- **MP3 / M4A / AAC / WAV / FLAC / OGG / OPUS** — common audio formats
6. Set quality labels (1080p, 720p, 480p, etc.) 6. Set quality labels (1080p, 720p, 480p, etc.)
7. Save — Video.js will use the stored source location and auto-select the best format the browser supports 7. Save — Video.js will use the stored source location and auto-select the best format the browser supports
@@ -82,7 +81,7 @@ External URLs must use `http://` or `https://`. Uploaded files and remote URLs c
When adding an external URL, the admin form can fetch title, description, duration, thumbnail metadata, and readable provider view counts through oEmbed, page metadata, and JSON-LD. The save action repeats the metadata lookup as a fallback, so blank titles can be filled from supported external pages even if the browser autofill was not used. When adding an external URL, the admin form can fetch title, description, duration, thumbnail metadata, and readable provider view counts through oEmbed, page metadata, and JSON-LD. The save action repeats the metadata lookup as a fallback, so blank titles can be filled from supported external pages even if the browser autofill was not used.
If no thumbnail image is uploaded, the add/edit forms can auto-grab one from the first supported source. YouTube and Dailymotion use direct thumbnail URLs; Vimeo, Wistia, TikTok, SoundCloud, and Spotify use oEmbed when available. Local uploaded videos can generate a first-frame thumbnail when `ffmpeg` is installed on the server, and local uploaded audio can generate a waveform poster image. If no thumbnail image is uploaded, the add/edit forms can auto-grab one from the first supported source. YouTube and Dailymotion use direct thumbnail URLs; Vimeo, Wistia, TikTok, SoundCloud, and Spotify use oEmbed when available. Local uploaded videos can generate a first-frame thumbnail when `ffmpeg` is installed on the server.
### Live Homepage Option ### Live Homepage Option
@@ -143,8 +142,6 @@ Use the **Sort Order** field — lower numbers appear first. Default is 0. The *
- Playback rate controls: 0.5×, 1×, 1.25×, 1.5×, 2× - Playback rate controls: 0.5×, 1×, 1.25×, 1.5×, 2×
- Multiple source fallback — browser picks best format automatically - Multiple source fallback — browser picks best format automatically
- Local uploaded sources and remote URL sources - Local uploaded sources and remote URL sources
- Audio playback for uploaded or direct linked audio sources
- Generated waveform poster images for uploaded audio when `ffmpeg` is available
- Provider embeds for popular video and audio platforms - Provider embeds for popular video and audio platforms
- Thumbnail posters - Thumbnail posters
- Per-video view counts and optional public library statistics - Per-video view counts and optional public library statistics
+19 -27
View File
@@ -88,7 +88,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$inserted_sources = 0; $inserted_sources = 0;
$moved_files = []; $moved_files = [];
$allowed_media = media_allowed_upload_extensions(); $allowed_video = ['mp4','webm','ogv','ogg','mov','mkv','avi'];
for ($i = 0; $i < $source_count; $i++) { for ($i = 0; $i < $source_count; $i++) {
$type = ($source_types[$i] ?? 'local') === 'remote' ? 'remote' : 'local'; $type = ($source_types[$i] ?? 'local') === 'remote' ? 'remote' : 'local';
@@ -99,7 +99,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($type === 'remote') { if ($type === 'remote') {
$url = normalize_media_url($source_urls[$i] ?? ''); $url = normalize_media_url($source_urls[$i] ?? '');
if ($url === '') { if ($url === '') {
if (trim($source_urls[$i] ?? '') !== '') $error = 'External media URLs must start with http:// or https://.'; if (trim($source_urls[$i] ?? '') !== '') $error = 'External video URLs must start with http:// or https://.';
continue; continue;
} }
$metadata = $source_metadata[$url] ?? []; $metadata = $source_metadata[$url] ?? [];
@@ -119,19 +119,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (empty($tmp) || !is_uploaded_file($tmp)) continue; if (empty($tmp) || !is_uploaded_file($tmp)) continue;
$orig_name = $source_files['name'][$i] ?? ''; $orig_name = $source_files['name'][$i] ?? '';
$ext = strtolower(pathinfo($orig_name, PATHINFO_EXTENSION)); $ext = strtolower(pathinfo($orig_name, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_media, true)) { if (!in_array($ext, $allowed_video, true)) continue;
$error = 'Uploaded media files must be one of: ' . media_upload_format_summary() . '.';
break;
}
$fname = $slug . '_' . ($i+1) . '_' . time() . '.' . $ext; $fname = $slug . '_' . ($i+1) . '_' . time() . '.' . $ext;
if (move_uploaded_file($tmp, $videos_dir . $fname)) { if (move_uploaded_file($tmp, $videos_dir . $fname)) {
$target = $videos_dir . $fname; $moved_files[] = $videos_dir . $fname;
$moved_files[] = $target;
$db->prepare(" $db->prepare("
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order) INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
VALUES (?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?)
")->execute([$video_id, $label, 'local', 'videos/' . $fname, '', media_mime_from_local_file($target, $fname), $quality, $sort]); ")->execute([$video_id, $label, 'local', 'videos/' . $fname, '', mime_from_ext($fname), $quality, $sort]);
$inserted_sources++; $inserted_sources++;
} }
} }
@@ -158,7 +154,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
} }
render_head('Add Media — Admin', ' render_head('Add Video — Admin', '
.form-grid { display:grid; grid-template-columns:1fr; gap:1.2rem; } .form-grid { display:grid; grid-template-columns:1fr; gap:1.2rem; }
@media(min-width:700px) { .form-grid { grid-template-columns:1fr 1fr; } } @media(min-width:700px) { .form-grid { grid-template-columns:1fr 1fr; } }
.form-grid .span2 { grid-column:1/-1; } .form-grid .span2 { grid-column:1/-1; }
@@ -210,7 +206,7 @@ render_head('Add Media — Admin', '
<div class="topbar-brand"> <div class="topbar-brand">
<div> <div>
<div class="brand-name">Ty Clifford</div> <div class="brand-name">Ty Clifford</div>
<div class="brand-sub">admin / add media</div> <div class="brand-sub">admin / add video</div>
</div> </div>
</div> </div>
<nav class="topbar-social"> <nav class="topbar-social">
@@ -238,7 +234,7 @@ render_head('Add Media — Admin', '
</a> </a>
<a href="add.php" class="active"> <a href="add.php" class="active">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Add Media Add Video
</a> </a>
<a href="youtube_import.php"> <a href="youtube_import.php">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22.54 6.42a2.78 2.78 0 00-1.95-2C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 00-1.95 2A29 29 0 001 12a29 29 0 00.46 5.58 2.78 2.78 0 001.95 2C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 001.95-2A29 29 0 0023 12a29 29 0 00-.46-5.58z"/><polygon points="10,15 15,12 10,9"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22.54 6.42a2.78 2.78 0 00-1.95-2C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 00-1.95 2A29 29 0 001 12a29 29 0 00.46 5.58 2.78 2.78 0 001.95 2C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 001.95-2A29 29 0 0023 12a29 29 0 00-.46-5.58z"/><polygon points="10,15 15,12 10,9"/></svg>
@@ -255,7 +251,7 @@ render_head('Add Media — Admin', '
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem;flex-wrap:wrap;gap:.75rem;"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem;flex-wrap:wrap;gap:.75rem;">
<div> <div>
<p class="eyebrow eyebrow-purple" style="margin-bottom:.3rem">New Entry</p> <p class="eyebrow eyebrow-purple" style="margin-bottom:.3rem">New Entry</p>
<h1 style="font-size:1.05rem;font-weight:700;color:var(--text);">Add Media</h1> <h1 style="font-size:1.05rem;font-weight:700;color:var(--text);">Add Video</h1>
</div> </div>
<a href="index.php" class="btn btn-secondary btn-sm">← Back</a> <a href="index.php" class="btn btn-secondary btn-sm">← Back</a>
</div> </div>
@@ -264,7 +260,7 @@ render_head('Add Media — Admin', '
<!-- ── Video details ── --> <!-- ── Video details ── -->
<div class="card" style="margin-bottom:1rem;"> <div class="card" style="margin-bottom:1rem;">
<p class="section-heading">Media Details</p> <p class="section-heading">Video Details</p>
<div class="form-grid"> <div class="form-grid">
<div class="form-group span2"> <div class="form-group span2">
<label class="form-label" for="title">Title</label> <label class="form-label" for="title">Title</label>
@@ -294,9 +290,9 @@ render_head('Add Media — Admin', '
accept="image/jpeg,image/png,image/webp,image/gif" style="padding:.45rem .6rem;"> accept="image/jpeg,image/png,image/webp,image/gif" style="padding:.45rem .6rem;">
<label style="display:flex;align-items:center;gap:.55rem;margin-top:.55rem;cursor:pointer;"> <label style="display:flex;align-items:center;gap:.55rem;margin-top:.55rem;cursor:pointer;">
<input type="checkbox" name="auto_thumbnail" value="1" <?= isset($_POST['auto_thumbnail']) || $_SERVER['REQUEST_METHOD'] !== 'POST' ? 'checked' : '' ?>> <input type="checkbox" name="auto_thumbnail" value="1" <?= isset($_POST['auto_thumbnail']) || $_SERVER['REQUEST_METHOD'] !== 'POST' ? 'checked' : '' ?>>
<span style="font-size:.78rem;color:var(--text-2);">Auto-grab thumbnail or waveform from source if no image is uploaded</span> <span style="font-size:.78rem;color:var(--text-2);">Auto-grab thumbnail from source if no image is uploaded</span>
</label> </label>
<span class="hint">JPG, PNG, WebP or GIF. Auto-grab supports providers, local video first frames, and local audio waveforms when ffmpeg is available.</span> <span class="hint">JPG, PNG, WebP or GIF. Auto-grab supports YouTube, Vimeo, Dailymotion and other oEmbed providers; local uploads use ffmpeg when available.</span>
</div> </div>
<div class="form-group" style="justify-content:flex-end;padding-bottom:.2rem;"> <div class="form-group" style="justify-content:flex-end;padding-bottom:.2rem;">
<label style="display:flex;align-items:center;gap:.6rem;cursor:pointer;"> <label style="display:flex;align-items:center;gap:.6rem;cursor:pointer;">
@@ -308,12 +304,12 @@ render_head('Add Media — Admin', '
</div> </div>
</div> </div>
<!-- ── Media sources ── --> <!-- ── Video sources ── -->
<div class="card"> <div class="card">
<p class="section-heading">Media Sources</p> <p class="section-heading">Video Sources</p>
<p class="hint" style="margin-bottom:1rem;"> <p class="hint" style="margin-bottom:1rem;">
Upload a video or audio file, link a direct media file, or paste a provider page URL such as YouTube or Vimeo. The player will use a provider embed when the URL supports it. Upload a video file, link a direct media file, or paste a provider page URL such as YouTube or Vimeo. The player will use a provider embed when the URL supports it.
Uploaded files: <strong><?= h(media_upload_format_summary()) ?></strong>. Remote URLs must use <strong>http://</strong> or <strong>https://</strong>. Uploaded files: <strong>MP4, WebM, OGV, MOV, MKV, AVI</strong>. Remote URLs must use <strong>http://</strong> or <strong>https://</strong>.
</p> </p>
<div class="sources-section" id="sources-container"> <div class="sources-section" id="sources-container">
@@ -327,9 +323,9 @@ render_head('Add Media — Admin', '
</select> </select>
</div> </div>
<div class="form-group source-location-panel source-location-local"> <div class="form-group source-location-panel source-location-local">
<label class="form-label">Media File</label> <label class="form-label">Video File</label>
<input class="form-input" type="file" name="source_file[]" <input class="form-input" type="file" name="source_file[]"
accept="<?= h(media_upload_accept_attribute()) ?>" accept="video/mp4,video/webm,video/ogg,.mp4,.webm,.ogv,.mov,.mkv,.avi"
style="padding:.45rem .6rem;"> style="padding:.45rem .6rem;">
</div> </div>
<div class="form-group source-location-panel source-location-remote" hidden> <div class="form-group source-location-panel source-location-remote" hidden>
@@ -348,10 +344,6 @@ render_head('Add Media — Admin', '
<option value="480p">480p</option> <option value="480p">480p</option>
<option value="360p">360p</option> <option value="360p">360p</option>
<option value="4K">4K</option> <option value="4K">4K</option>
<option value="Audio">Audio</option>
<option value="320kbps">320kbps</option>
<option value="256kbps">256kbps</option>
<option value="128kbps">128kbps</option>
<option value="">—</option> <option value="">—</option>
</select> </select>
</div> </div>
@@ -375,7 +367,7 @@ render_head('Add Media — Admin', '
<div style="display:flex;gap:.75rem;margin-top:1rem;flex-wrap:wrap;"> <div style="display:flex;gap:.75rem;margin-top:1rem;flex-wrap:wrap;">
<button class="btn btn-primary" type="submit"> <button class="btn btn-primary" type="submit">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17,21 17,13 7,13 7,21"/><polyline points="7,3 7,8 15,8"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17,21 17,13 7,13 7,21"/><polyline points="7,3 7,8 15,8"/></svg>
Save Media Save Video
</button> </button>
<a href="index.php" class="btn btn-secondary">Cancel</a> <a href="index.php" class="btn btn-secondary">Cancel</a>
</div> </div>
+14 -22
View File
@@ -94,7 +94,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($source_type === 'remote') { if ($source_type === 'remote') {
$source_url = normalize_media_url($upd_urls[$i] ?? ''); $source_url = normalize_media_url($upd_urls[$i] ?? '');
if ($source_url === '') { if ($source_url === '') {
$error = 'External media URLs must start with http:// or https://.'; $error = 'External video URLs must start with http:// or https://.';
break; break;
} }
} }
@@ -127,7 +127,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$new_files = $_FILES['new_source_file'] ?? []; $new_files = $_FILES['new_source_file'] ?? [];
$videos_dir = MEDIA_DIR . 'videos/'; $videos_dir = MEDIA_DIR . 'videos/';
if (!is_dir($videos_dir)) mkdir($videos_dir, 0755, true); if (!is_dir($videos_dir)) mkdir($videos_dir, 0755, true);
$allowed_media = media_allowed_upload_extensions(); $allowed_video = ['mp4','webm','ogv','ogg','mov','mkv','avi'];
$new_count = max( $new_count = max(
count($new_types), count($new_types),
@@ -147,7 +147,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($type === 'remote') { if ($type === 'remote') {
$url = normalize_media_url($new_urls[$i] ?? ''); $url = normalize_media_url($new_urls[$i] ?? '');
if ($url === '') { if ($url === '') {
if (trim($new_urls[$i] ?? '') !== '') $error = 'External media URLs must start with http:// or https://.'; if (trim($new_urls[$i] ?? '') !== '') $error = 'External video URLs must start with http:// or https://.';
continue; continue;
} }
@@ -163,18 +163,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (empty($tmp) || !is_uploaded_file($tmp)) continue; if (empty($tmp) || !is_uploaded_file($tmp)) continue;
$orig = $new_files['name'][$i] ?? ''; $orig = $new_files['name'][$i] ?? '';
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION)); $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_media, true)) { if (!in_array($ext, $allowed_video, true)) continue;
$error = 'Uploaded media files must be one of: ' . media_upload_format_summary() . '.';
break;
}
$fname = $slug . '_' . time() . '_' . $i . '.' . $ext; $fname = $slug . '_' . time() . '_' . $i . '.' . $ext;
if (move_uploaded_file($tmp, $videos_dir . $fname)) { if (move_uploaded_file($tmp, $videos_dir . $fname)) {
$target = $videos_dir . $fname; $moved_files[] = $videos_dir . $fname;
$moved_files[] = $target;
$db->prepare(" $db->prepare("
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order) INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
VALUES (?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?)
")->execute([$id, $label, 'local', 'videos/' . $fname, '', media_mime_from_local_file($target, $fname), $quality, $sort]); ")->execute([$id, $label, 'local', 'videos/' . $fname, '', mime_from_ext($fname), $quality, $sort]);
$inserted_source_ids[] = (int)$db->lastInsertId(); $inserted_source_ids[] = (int)$db->lastInsertId();
} }
} }
@@ -282,7 +278,7 @@ render_head('Edit Video — Admin', '
</a> </a>
<a href="add.php"> <a href="add.php">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Add Media Add Video
</a> </a>
<a href="youtube_import.php"> <a href="youtube_import.php">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22.54 6.42a2.78 2.78 0 00-1.95-2C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 00-1.95 2A29 29 0 001 12a29 29 0 00.46 5.58 2.78 2.78 0 001.95 2C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 001.95-2A29 29 0 0023 12a29 29 0 00-.46-5.58z"/><polygon points="10,15 15,12 10,9"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22.54 6.42a2.78 2.78 0 00-1.95-2C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 00-1.95 2A29 29 0 001 12a29 29 0 00.46 5.58 2.78 2.78 0 001.95 2C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 001.95-2A29 29 0 0023 12a29 29 0 00-.46-5.58z"/><polygon points="10,15 15,12 10,9"/></svg>
@@ -309,7 +305,7 @@ render_head('Edit Video — Admin', '
<!-- ── Details ── --> <!-- ── Details ── -->
<div class="card" style="margin-bottom:1rem;"> <div class="card" style="margin-bottom:1rem;">
<p class="section-heading">Media Details</p> <p class="section-heading">Video Details</p>
<div class="form-grid"> <div class="form-grid">
<div class="form-group span2"> <div class="form-group span2">
<label class="form-label" for="title">Title *</label> <label class="form-label" for="title">Title *</label>
@@ -363,9 +359,9 @@ render_head('Edit Video — Admin', '
accept="image/jpeg,image/png,image/webp,image/gif" style="padding:.45rem .6rem;"> accept="image/jpeg,image/png,image/webp,image/gif" style="padding:.45rem .6rem;">
<label style="display:flex;align-items:center;gap:.55rem;margin-top:.55rem;cursor:pointer;"> <label style="display:flex;align-items:center;gap:.55rem;margin-top:.55rem;cursor:pointer;">
<input type="checkbox" name="auto_thumbnail" value="1" <?= isset($_POST['auto_thumbnail']) || ($_SERVER['REQUEST_METHOD'] !== 'POST' && !$video['thumbnail']) ? 'checked' : '' ?>> <input type="checkbox" name="auto_thumbnail" value="1" <?= isset($_POST['auto_thumbnail']) || ($_SERVER['REQUEST_METHOD'] !== 'POST' && !$video['thumbnail']) ? 'checked' : '' ?>>
<span style="font-size:.78rem;color:var(--text-2);">Auto-grab/refresh thumbnail or waveform from source on save</span> <span style="font-size:.78rem;color:var(--text-2);">Auto-grab/refresh thumbnail from source on save</span>
</label> </label>
<span class="hint">Upload a new image to override auto-grab. Auto-grab supports providers, local video first frames, and local audio waveforms when ffmpeg is available.</span> <span class="hint">Upload a new image to override auto-grab. Auto-grab supports YouTube, Vimeo, Dailymotion and other oEmbed providers; local uploads use ffmpeg when available.</span>
</div> </div>
</div> </div>
</div> </div>
@@ -406,7 +402,7 @@ render_head('Edit Video — Admin', '
<div class="form-group"> <div class="form-group">
<label class="form-label">Quality</label> <label class="form-label">Quality</label>
<select class="form-select" name="src_quality[]"> <select class="form-select" name="src_quality[]">
<?php foreach (['1080p','720p','480p','360p','4K','Audio','320kbps','256kbps','128kbps',''] as $q): ?> <?php foreach (['1080p','720p','480p','360p','4K',''] as $q): ?>
<option value="<?= $q ?>" <?= $s['quality']===$q ? 'selected' : '' ?>><?= $q ?: '—' ?></option> <option value="<?= $q ?>" <?= $s['quality']===$q ? 'selected' : '' ?>><?= $q ?: '—' ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
@@ -429,7 +425,7 @@ render_head('Edit Video — Admin', '
<!-- ── Add new sources ── --> <!-- ── Add new sources ── -->
<div class="card" style="margin-bottom:1rem;"> <div class="card" style="margin-bottom:1rem;">
<p class="section-heading">Add New Sources</p> <p class="section-heading">Add New Sources</p>
<p class="hint" style="margin-bottom:.85rem;">Upload another video or audio file, link a direct media file, or paste a provider page URL such as YouTube or Vimeo. Uploaded files: <strong><?= h(media_upload_format_summary()) ?></strong>.</p> <p class="hint" style="margin-bottom:.85rem;">Upload another file, link a direct media file, or paste a provider page URL such as YouTube or Vimeo.</p>
<div class="source-list" id="new-sources-container"> <div class="source-list" id="new-sources-container">
<div class="add-source-row" data-new-row="0"> <div class="add-source-row" data-new-row="0">
<div class="form-group"> <div class="form-group">
@@ -440,9 +436,9 @@ render_head('Edit Video — Admin', '
</select> </select>
</div> </div>
<div class="form-group source-location-panel source-location-local"> <div class="form-group source-location-panel source-location-local">
<label class="form-label">Media File</label> <label class="form-label">Video File</label>
<input class="form-input" type="file" name="new_source_file[]" <input class="form-input" type="file" name="new_source_file[]"
accept="<?= h(media_upload_accept_attribute()) ?>" accept="video/mp4,video/webm,video/ogg,.mp4,.webm,.ogv,.mov,.mkv,.avi"
style="padding:.45rem .6rem;"> style="padding:.45rem .6rem;">
</div> </div>
<div class="form-group source-location-panel source-location-remote" hidden> <div class="form-group source-location-panel source-location-remote" hidden>
@@ -457,10 +453,6 @@ render_head('Edit Video — Admin', '
<option value="480p">480p</option> <option value="480p">480p</option>
<option value="360p">360p</option> <option value="360p">360p</option>
<option value="4K">4K</option> <option value="4K">4K</option>
<option value="Audio">Audio</option>
<option value="320kbps">320kbps</option>
<option value="256kbps">256kbps</option>
<option value="128kbps">128kbps</option>
<option value="">—</option> <option value="">—</option>
</select> </select>
</div> </div>
+2 -2
View File
@@ -205,7 +205,7 @@ render_head('Admin — TyClifford.com', '
</a> </a>
<a href="add.php"> <a href="add.php">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Add Media Add Video
</a> </a>
<a href="youtube_import.php"> <a href="youtube_import.php">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22.54 6.42a2.78 2.78 0 00-1.95-2C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 00-1.95 2A29 29 0 001 12a29 29 0 00.46 5.58 2.78 2.78 0 001.95 2C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 001.95-2A29 29 0 0023 12a29 29 0 00-.46-5.58z"/><polygon points="10,15 15,12 10,9"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22.54 6.42a2.78 2.78 0 00-1.95-2C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 00-1.95 2A29 29 0 001 12a29 29 0 00.46 5.58 2.78 2.78 0 001.95 2C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 001.95-2A29 29 0 0023 12a29 29 0 00-.46-5.58z"/><polygon points="10,15 15,12 10,9"/></svg>
@@ -295,7 +295,7 @@ render_head('Admin — TyClifford.com', '
</form> </form>
<a href="add.php" class="btn btn-primary btn-sm"> <a href="add.php" class="btn btn-primary btn-sm">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Add Media Add Video
</a> </a>
<a href="youtube_import.php" class="btn btn-secondary btn-sm"> <a href="youtube_import.php" class="btn btn-secondary btn-sm">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22.54 6.42a2.78 2.78 0 00-1.95-2C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 00-1.95 2A29 29 0 001 12a29 29 0 00.46 5.58 2.78 2.78 0 001.95 2C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 001.95-2A29 29 0 0023 12a29 29 0 00-.46-5.58z"/><polygon points="10,15 15,12 10,9"/></svg> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22.54 6.42a2.78 2.78 0 00-1.95-2C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 00-1.95 2A29 29 0 001 12a29 29 0 00.46 5.58 2.78 2.78 0 001.95 2C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 001.95-2A29 29 0 0023 12a29 29 0 00-.46-5.58z"/><polygon points="10,15 15,12 10,9"/></svg>
+1 -1
View File
@@ -132,7 +132,7 @@ render_head('Settings — Admin', '
</a> </a>
<a href="add.php"> <a href="add.php">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Add Media Add Video
</a> </a>
<a href="youtube_import.php"> <a href="youtube_import.php">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22.54 6.42a2.78 2.78 0 00-1.95-2C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 00-1.95 2A29 29 0 001 12a29 29 0 00.46 5.58 2.78 2.78 0 001.95 2C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 001.95-2A29 29 0 0023 12a29 29 0 00-.46-5.58z"/><polygon points="10,15 15,12 10,9"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22.54 6.42a2.78 2.78 0 00-1.95-2C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 00-1.95 2A29 29 0 001 12a29 29 0 00.46 5.58 2.78 2.78 0 001.95 2C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 001.95-2A29 29 0 0023 12a29 29 0 00-.46-5.58z"/><polygon points="10,15 15,12 10,9"/></svg>
+1 -1
View File
@@ -600,7 +600,7 @@ render_head('YouTube Importer — Admin', '
<aside class="admin-nav"> <aside class="admin-nav">
<div class="admin-nav-label">Navigation</div> <div class="admin-nav-label">Navigation</div>
<a href="index.php">Videos</a> <a href="index.php">Videos</a>
<a href="add.php">Add Media</a> <a href="add.php">Add Video</a>
<a href="youtube_import.php" class="active">YouTube Importer</a> <a href="youtube_import.php" class="active">YouTube Importer</a>
<hr class="nav-divider"> <hr class="nav-divider">
<a href="settings.php">Settings</a> <a href="settings.php">Settings</a>
-2
View File
@@ -11,8 +11,6 @@ All notable changes to this media player project.
- Added an admin upload flow that supports either direct uploads or external provider/page URLs. - Added an admin upload flow that supports either direct uploads or external provider/page URLs.
- Added metadata fetching for external URLs to populate title, description, duration, thumbnails, publish dates, and readable provider view counts where available. - Added metadata fetching for external URLs to populate title, description, duration, thumbnails, publish dates, and readable provider view counts where available.
- Added automatic thumbnail grabbing for external URLs, plus first-frame thumbnail generation for local videos when `ffmpeg` is available. - Added automatic thumbnail grabbing for external URLs, plus first-frame thumbnail generation for local videos when `ffmpeg` is available.
- Added Video.js audio playback for uploaded and direct linked audio sources, including WAV and other common audio formats.
- Added generated waveform poster images for uploaded audio files when `ffmpeg` is available.
- Added a YouTube channel importer that scrapes channel videos, previews imports, skips duplicates, and stores imported videos as embedded YouTube sources. - Added a YouTube channel importer that scrapes channel videos, previews imports, skips duplicates, and stores imported videos as embedded YouTube sources.
- Added YouTube timestamp correction so imported and existing YouTube videos can use their original publish dates. - Added YouTube timestamp correction so imported and existing YouTube videos can use their original publish dates.
- Added an **Upkeeping** section to the admin dashboard for batch maintenance actions. - Added an **Upkeeping** section to the admin dashboard for batch maintenance actions.
+16 -96
View File
@@ -33,13 +33,6 @@ $native_player_sources = [];
foreach ($player_sources as $player_source) { foreach ($player_sources as $player_source) {
if (($player_source['kind'] ?? '') !== 'iframe') $native_player_sources[] = $player_source; if (($player_source['kind'] ?? '') !== 'iframe') $native_player_sources[] = $player_source;
} }
$native_player_mode = (($primary_player_source['kind'] ?? '') === 'audio') ? 'audio' : 'video';
$active_native_sources = [];
foreach ($native_player_sources as $player_source) {
$kind = $player_source['kind'] ?? '';
if ($native_player_mode === 'audio' && $kind === 'audio') $active_native_sources[] = $player_source;
if ($native_player_mode === 'video' && $kind !== 'audio') $active_native_sources[] = $player_source;
}
$meta_title = $video ? $video['title'] . ' — TyClifford.com' : 'Video Player'; $meta_title = $video ? $video['title'] . ' — TyClifford.com' : 'Video Player';
$meta_description = $video && trim($video['description'] ?? '') !== '' ? trim($video['description']) : 'TyClifford.com media player'; $meta_description = $video && trim($video['description'] ?? '') !== '' ? trim($video['description']) : 'TyClifford.com media player';
@@ -160,57 +153,6 @@ header('Content-Security-Policy: frame-ancestors *');
.video-js .vjs-current-time, .video-js .vjs-current-time,
.video-js .vjs-duration, .video-js .vjs-duration,
.video-js .vjs-time-divider { color: rgba(255,255,255,.8) !important; font-size: .7em !important; } .video-js .vjs-time-divider { color: rgba(255,255,255,.8) !important; font-size: .7em !important; }
.audio-player-frame {
position: absolute;
inset: 0;
display: flex;
align-items: flex-end;
justify-content: center;
padding: 1rem;
background: radial-gradient(circle at 50% 35%, rgba(176,96,255,.12), transparent 55%), #09090f;
}
.audio-waveform-art {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, rgba(9,9,15,.2), rgba(9,9,15,.82));
overflow: hidden;
}
.audio-waveform-art img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: .92;
filter: saturate(1.15) contrast(1.05);
}
.audio-waveform-fallback {
position: absolute;
inset: 0;
background:
linear-gradient(90deg, transparent 0 4%, rgba(0,232,122,.34) 4% 5%, transparent 5% 9%),
radial-gradient(circle at 50% 35%, rgba(0,232,122,.12), transparent 55%),
#09090f;
background-size: 90px 100%, 100% 100%, 100% 100%;
opacity: .9;
}
.audio-player-frame .video-js {
position: relative !important;
inset: auto !important;
width: 100% !important;
max-width: calc(100% - 1rem) !important;
height: 4.2rem !important;
background: rgba(9,9,15,.86) !important;
border: 1px solid rgba(255,255,255,.08);
border-radius: 6px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,.35);
}
.audio-player-frame .video-js .vjs-control-bar {
background: rgba(9,9,15,.92) !important;
height: 4.2rem !important;
}
.provider-embed-frame { .provider-embed-frame {
position: absolute; position: absolute;
inset: 0; inset: 0;
@@ -298,42 +240,20 @@ header('Content-Security-Policy: frame-ancestors *');
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share" allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share"
allowfullscreen allowfullscreen
referrerpolicy="no-referrer-when-downgrade"></iframe> referrerpolicy="no-referrer-when-downgrade"></iframe>
<?php elseif (!empty($active_native_sources)): ?> <?php elseif (!empty($native_player_sources)): ?>
<?php if ($native_player_mode === 'audio'): ?> <video id="embed-player"
<div class="audio-player-frame"> class="video-js vjs-default-skin vjs-big-play-centered"
<div class="audio-waveform-art" aria-hidden="true"> controls preload="metadata"
<?php if ($video['thumbnail']): ?> <?php if ($video['thumbnail']): ?>
<img src="<?= h(public_absolute_url(public_media_url($video['thumbnail']))) ?>" alt=""> poster="<?= htmlspecialchars(public_absolute_url(public_media_url($video['thumbnail'])), ENT_QUOTES) ?>"
<?php else: ?> <?php endif; ?>>
<div class="audio-waveform-fallback"></div> <?php foreach ($native_player_sources as $src): ?>
<?php endif; ?> <source
</div> src="<?= htmlspecialchars($src['url'], ENT_QUOTES) ?>"
<audio id="embed-player" type="<?= htmlspecialchars($src['mime'], ENT_QUOTES) ?>">
class="video-js vjs-default-skin" <?php endforeach; ?>
controls preload="metadata"> <p class="vjs-no-js">Your browser does not support HTML5 video.</p>
<?php foreach ($active_native_sources as $src): ?> </video>
<source
src="<?= htmlspecialchars($src['url'], ENT_QUOTES) ?>"
type="<?= htmlspecialchars($src['mime'], ENT_QUOTES) ?>">
<?php endforeach; ?>
<p class="vjs-no-js">Your browser does not support HTML5 audio.</p>
</audio>
</div>
<?php else: ?>
<video id="embed-player"
class="video-js vjs-default-skin vjs-big-play-centered"
controls preload="metadata"
<?php if ($video['thumbnail']): ?>
poster="<?= htmlspecialchars(public_absolute_url(public_media_url($video['thumbnail'])), ENT_QUOTES) ?>"
<?php endif; ?>>
<?php foreach ($active_native_sources as $src): ?>
<source
src="<?= htmlspecialchars($src['url'], ENT_QUOTES) ?>"
type="<?= htmlspecialchars($src['mime'], ENT_QUOTES) ?>">
<?php endforeach; ?>
<p class="vjs-no-js">Your browser does not support HTML5 video.</p>
</video>
<?php endif; ?>
<?php endif; ?> <?php endif; ?>
<?php else: ?> <?php else: ?>
@@ -345,13 +265,13 @@ header('Content-Security-Policy: frame-ancestors *');
<line x1="8" y1="21" x2="16" y2="21"/> <line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/> <line x1="12" y1="17" x2="12" y2="21"/>
</svg> </svg>
<p><?= $slug ? 'media not found' : 'no media specified' ?></p> <p><?= $slug ? 'video not found' : 'no video specified' ?></p>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php if ($video && !empty($active_native_sources) && (!$primary_player_source || ($primary_player_source['kind'] ?? '') !== 'iframe')): ?> <?php if ($video && !empty($native_player_sources) && (!$primary_player_source || ($primary_player_source['kind'] ?? '') !== 'iframe')): ?>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
var player = videojs('embed-player', { var player = videojs('embed-player', {
+5 -122
View File
@@ -321,58 +321,6 @@ function make_slug(string $title, int $id = 0): string {
return $slug ?: 'video-' . time(); 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 { function mime_from_ext(string $path): string {
$url_path = parse_url($path, PHP_URL_PATH); $url_path = parse_url($path, PHP_URL_PATH);
if (is_string($url_path) && $url_path !== '') { if (is_string($url_path) && $url_path !== '') {
@@ -384,6 +332,7 @@ function mime_from_ext(string $path): string {
'm4v' => 'video/mp4', 'm4v' => 'video/mp4',
'webm' => 'video/webm', 'webm' => 'video/webm',
'ogv' => 'video/ogg', 'ogv' => 'video/ogg',
'ogg' => 'video/ogg',
'mov' => 'video/quicktime', 'mov' => 'video/quicktime',
'mkv' => 'video/x-matroska', 'mkv' => 'video/x-matroska',
'avi' => 'video/x-msvideo', 'avi' => 'video/x-msvideo',
@@ -393,41 +342,11 @@ function mime_from_ext(string $path): string {
'm4a' => 'audio/mp4', 'm4a' => 'audio/mp4',
'aac' => 'audio/aac', 'aac' => 'audio/aac',
'wav' => 'audio/wav', 'wav' => 'audio/wav',
'wave' => 'audio/wav',
'flac' => 'audio/flac', 'flac' => 'audio/flac',
'ogg' => 'audio/ogg',
'oga' => 'audio/ogg',
'opus' => 'audio/ogg',
default => 'video/mp4', 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 { function normalize_media_url(string $url): string {
$url = trim($url); $url = trim($url);
if ($url === '') return ''; if ($url === '') return '';
@@ -470,12 +389,11 @@ function media_extension_from_url(string $url): string {
function media_guess_mime(string $url): string { function media_guess_mime(string $url): string {
$map = [ $map = [
'mp4' => 'video/mp4', 'm4v' => 'video/mp4', 'webm' => 'video/webm', 'mp4' => 'video/mp4', 'm4v' => 'video/mp4', 'webm' => 'video/webm',
'ogv' => 'video/ogg', 'mov' => 'video/quicktime', 'ogv' => 'video/ogg', 'ogg' => 'video/ogg', 'mov' => 'video/quicktime',
'mkv' => 'video/x-matroska', 'avi' => 'video/x-msvideo', 'mkv' => 'video/x-matroska', 'avi' => 'video/x-msvideo',
'm3u8' => 'application/vnd.apple.mpegurl', 'mpd' => 'application/dash+xml', 'm3u8' => 'application/vnd.apple.mpegurl', 'mpd' => 'application/dash+xml',
'mp3' => 'audio/mpeg', 'm4a' => 'audio/mp4', 'aac' => 'audio/aac', 'mp3' => 'audio/mpeg', 'm4a' => 'audio/mp4', 'aac' => 'audio/aac',
'wav' => 'audio/wav', 'wave' => 'audio/wav', 'flac' => 'audio/flac', 'wav' => 'audio/wav', 'flac' => 'audio/flac',
'ogg' => 'audio/ogg', 'oga' => 'audio/ogg', 'opus' => 'audio/ogg',
]; ];
return $map[media_extension_from_url($url)] ?? ''; return $map[media_extension_from_url($url)] ?? '';
} }
@@ -1264,37 +1182,6 @@ function media_thumbnail_from_local_video(string $file_path, string $slug): stri
return ''; 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 { 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 = get_db()->prepare("SELECT * FROM video_sources WHERE video_id=? ORDER BY sort_order ASC, id ASC");
$stmt->execute([$video_id]); $stmt->execute([$video_id]);
@@ -1309,8 +1196,6 @@ function media_auto_thumbnail_for_video(int $video_id, string $slug): string {
foreach ($sources as $source) { foreach ($sources as $source) {
if (!source_is_local($source)) continue; 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); $thumbnail = media_thumbnail_from_local_video($source['file_path'] ?? '', $slug);
if ($thumbnail !== '') return $thumbnail; if ($thumbnail !== '') return $thumbnail;
} }
@@ -1467,8 +1352,8 @@ function media_classify_kind(string $url, string $mime, string $forced_mode = 'a
if ($ext === 'mpd' || $mime_lc === 'application/dash+xml') return 'dash'; 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, 'audio/')) return 'audio';
if (str_starts_with($mime_lc, 'video/')) return 'video'; if (str_starts_with($mime_lc, 'video/')) return 'video';
if (in_array($ext, ['mp3', 'm4a', 'aac', 'wav', 'wave', 'flac', 'ogg', 'oga', 'opus'], true)) return 'audio'; if (in_array($ext, ['mp3', 'm4a', 'aac', 'wav', 'flac', 'oga', 'opus'], true)) return 'audio';
if (in_array($ext, ['mp4', 'm4v', 'webm', 'ogv', 'mov', 'mkv', 'avi', 'wmv', 'flv', 'ts', '3gp', '3g2'], true)) return 'video'; if (in_array($ext, ['mp4', 'm4v', 'webm', 'ogv', 'ogg', 'mov', 'mkv', 'avi', 'wmv', 'flv', 'ts', '3gp', '3g2'], true)) return 'video';
return 'unknown'; return 'unknown';
} }
@@ -1544,8 +1429,6 @@ function source_provider_label(array $source): string {
if (!$player) return source_is_remote($source) ? 'External URL' : 'Uploaded File'; if (!$player) return source_is_remote($source) ? 'External URL' : 'Uploaded File';
if (($player['provider'] ?? '') !== '') return ucwords(str_replace('-', ' ', $player['provider'])) . ' Embed'; if (($player['provider'] ?? '') !== '') return ucwords(str_replace('-', ' ', $player['provider'])) . ' Embed';
if (($player['kind'] ?? '') === 'iframe') return 'External 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'; return source_is_remote($source) ? 'External URL' : 'Uploaded File';
} }
+14 -81
View File
@@ -58,13 +58,6 @@ $native_player_sources = [];
foreach ($player_sources as $player_source) { foreach ($player_sources as $player_source) {
if (($player_source['kind'] ?? '') !== 'iframe') $native_player_sources[] = $player_source; if (($player_source['kind'] ?? '') !== 'iframe') $native_player_sources[] = $player_source;
} }
$native_player_mode = (($primary_player_source['kind'] ?? '') === 'audio') ? 'audio' : 'video';
$active_native_sources = [];
foreach ($native_player_sources as $player_source) {
$kind = $player_source['kind'] ?? '';
if ($native_player_mode === 'audio' && $kind === 'audio') $active_native_sources[] = $player_source;
if ($native_player_mode === 'video' && $kind !== 'audio') $active_native_sources[] = $player_source;
}
$site_title = setting('site_title', 'Ty Clifford'); $site_title = setting('site_title', 'Ty Clifford');
if ($home_live_first) { if ($home_live_first) {
@@ -196,45 +189,6 @@ render_head($share_title, '
.video-js .vjs-current-time, .video-js .vjs-current-time,
.video-js .vjs-duration, .video-js .vjs-duration,
.video-js .vjs-time-divider { color:rgba(255,255,255,.8) !important; font-size:.7em !important; } .video-js .vjs-time-divider { color:rgba(255,255,255,.8) !important; font-size:.7em !important; }
.audio-player-frame {
position:absolute; inset:0;
display:flex; align-items:flex-end; justify-content:center;
padding:1rem;
background:radial-gradient(circle at 50% 35%, rgba(176,96,255,.12), transparent 55%), #09090f;
}
.audio-waveform-art {
position:absolute; inset:0;
display:flex; align-items:center; justify-content:center;
background:linear-gradient(180deg,rgba(9,9,15,.2),rgba(9,9,15,.82));
overflow:hidden;
}
.audio-waveform-art img {
width:100%; height:100%; object-fit:cover;
opacity:.92; filter:saturate(1.15) contrast(1.05);
}
.audio-waveform-fallback {
position:absolute; inset:0;
background:
linear-gradient(90deg, transparent 0 4%, rgba(0,232,122,.34) 4% 5%, transparent 5% 9%),
radial-gradient(circle at 50% 35%, rgba(0,232,122,.12), transparent 55%),
#09090f;
background-size:90px 100%, 100% 100%, 100% 100%;
opacity:.9;
}
.audio-player-frame .video-js {
position:relative !important; inset:auto !important;
width:100% !important; max-width:calc(100% - 1rem) !important;
height:4.2rem !important;
background:rgba(9,9,15,.86) !important;
border:1px solid rgba(255,255,255,.08);
border-radius:var(--r);
overflow:hidden;
box-shadow:0 10px 30px rgba(0,0,0,.35);
}
.audio-player-frame .video-js .vjs-control-bar {
background:rgba(9,9,15,.92) !important;
height:4.2rem !important;
}
.provider-embed-frame { .provider-embed-frame {
position:absolute; inset:0; position:absolute; inset:0;
display:block; width:100%; height:100%; display:block; width:100%; height:100%;
@@ -392,44 +346,23 @@ render_head($share_title, '
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share" allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share"
allowfullscreen allowfullscreen
referrerpolicy="no-referrer-when-downgrade"></iframe> referrerpolicy="no-referrer-when-downgrade"></iframe>
<?php elseif ($video && !empty($active_native_sources)): ?> <?php elseif ($video && !empty($native_player_sources)): ?>
<?php if ($native_player_mode === 'audio'): ?> <video id="main-player"
<div class="audio-player-frame"> class="video-js vjs-default-skin vjs-big-play-centered"
<div class="audio-waveform-art" aria-hidden="true"> controls preload="metadata"
<?php if ($video['thumbnail']): ?> <?php if ($video['thumbnail']): ?>
<img src="<?= h(public_media_url($video['thumbnail'])) ?>" alt=""> poster="<?= h(public_media_url($video['thumbnail'])) ?>"
<?php else: ?> <?php endif; ?>
<div class="audio-waveform-fallback"></div> data-setup='{"fluid":false,"responsive":true,"playbackRates":[0.5,1,1.25,1.5,2]}'>
<?php endif; ?> <?php foreach ($native_player_sources as $src): ?>
</div> <source src="<?= h($src['url']) ?>" type="<?= h($src['mime']) ?>" label="<?= h($src['label']) ?>">
<audio id="main-player" <?php endforeach; ?>
class="video-js vjs-default-skin" <p class="vjs-no-js">Your browser does not support HTML video.</p>
controls preload="metadata" </video>
data-setup='{"fluid":false,"responsive":true,"playbackRates":[0.5,1,1.25,1.5,2]}'>
<?php foreach ($active_native_sources as $src): ?>
<source src="<?= h($src['url']) ?>" type="<?= h($src['mime']) ?>" label="<?= h($src['label']) ?>">
<?php endforeach; ?>
<p class="vjs-no-js">Your browser does not support HTML audio.</p>
</audio>
</div>
<?php else: ?>
<video id="main-player"
class="video-js vjs-default-skin vjs-big-play-centered"
controls preload="metadata"
<?php if ($video['thumbnail']): ?>
poster="<?= h(public_media_url($video['thumbnail'])) ?>"
<?php endif; ?>
data-setup='{"fluid":false,"responsive":true,"playbackRates":[0.5,1,1.25,1.5,2]}'>
<?php foreach ($active_native_sources as $src): ?>
<source src="<?= h($src['url']) ?>" type="<?= h($src['mime']) ?>" label="<?= h($src['label']) ?>">
<?php endforeach; ?>
<p class="vjs-no-js">Your browser does not support HTML video.</p>
</video>
<?php endif; ?>
<?php else: ?> <?php else: ?>
<div class="no-video"> <div class="no-video">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity=".25"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg> <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity=".25"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
<p>no media selected</p> <p>no video selected</p>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>