- .m4a uploads are now accepted in add/edit, and M4A handling now keeps the original source while adding a generated black-frame H.264 MP4 companion source when ffmpeg can run.
The core conversion helper is in [includes/db.php (line 1161)](/Users/tyemeclifford/Documents/GH/mediaplayer/includes/db.php:1161). The upload flows insert the converted MP4 first for playback and retain the original M4A as a separate source in [admin/add.php (line 129)](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/add.php:129) and [admin/edit.php (line 172)](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/edit.php:172). I also updated the admin upload hints, [README.md (line 77)](/Users/tyemeclifford/Documents/GH/mediaplayer/README.md:77), and [changelog.md (line 14)](/Users/tyemeclifford/Documents/GH/mediaplayer/changelog.md:14).
This commit is contained in:
@@ -74,6 +74,7 @@ Styled to match the existing `gate.php` / `index.php` dark theme exactly.
|
|||||||
- **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)
|
||||||
|
- **M4A** — kept as an original source and converted to a black-frame H.264 MP4 companion source when `ffmpeg` is installed
|
||||||
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
|
||||||
|
|
||||||
@@ -81,7 +82,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.
|
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. Uploaded M4A files also use `ffmpeg` to create a black-frame H.264 MP4 playback source while keeping the original M4A source.
|
||||||
|
|
||||||
### Live Homepage Option
|
### Live Homepage Option
|
||||||
|
|
||||||
@@ -142,6 +143,7 @@ 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
|
||||||
|
- M4A uploads can add a generated black-frame H.264 MP4 companion source
|
||||||
- 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
|
||||||
|
|||||||
+30
-8
@@ -88,7 +88,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
$inserted_sources = 0;
|
$inserted_sources = 0;
|
||||||
$moved_files = [];
|
$moved_files = [];
|
||||||
$allowed_video = ['mp4','webm','ogv','ogg','mov','mkv','avi'];
|
$allowed_video = ['mp4','webm','ogv','ogg','mov','mkv','avi','m4a'];
|
||||||
|
|
||||||
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';
|
||||||
@@ -123,12 +123,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
$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)) {
|
||||||
|
$file_path = 'videos/' . $fname;
|
||||||
$moved_files[] = $videos_dir . $fname;
|
$moved_files[] = $videos_dir . $fname;
|
||||||
$db->prepare("
|
|
||||||
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
|
if ($ext === 'm4a') {
|
||||||
VALUES (?,?,?,?,?,?,?,?)
|
$converted_path = media_black_video_from_m4a($file_path, $slug, $i);
|
||||||
")->execute([$video_id, $label, 'local', 'videos/' . $fname, '', mime_from_ext($fname), $quality, $sort]);
|
if ($converted_path !== '') {
|
||||||
$inserted_sources++;
|
$moved_files[] = MEDIA_DIR . $converted_path;
|
||||||
|
$converted_label = $label !== '' ? $label . ' (MP4)' : 'Converted MP4';
|
||||||
|
$db->prepare("
|
||||||
|
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?)
|
||||||
|
")->execute([$video_id, $converted_label, 'local', $converted_path, '', 'video/mp4', $quality, $sort]);
|
||||||
|
$inserted_sources++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$original_label = $label !== '' ? $label . ' (M4A)' : 'Original M4A';
|
||||||
|
$db->prepare("
|
||||||
|
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?)
|
||||||
|
")->execute([$video_id, $original_label, 'local', $file_path, '', mime_from_ext($fname), '', $converted_path !== '' ? $sort + 1 : $sort]);
|
||||||
|
$inserted_sources++;
|
||||||
|
} else {
|
||||||
|
$db->prepare("
|
||||||
|
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?)
|
||||||
|
")->execute([$video_id, $label, 'local', $file_path, '', mime_from_ext($fname), $quality, $sort]);
|
||||||
|
$inserted_sources++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +331,7 @@ render_head('Add Video — Admin', '
|
|||||||
<p class="section-heading">Video 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 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>MP4, WebM, OGV, MOV, MKV, AVI</strong>. Remote URLs must use <strong>http://</strong> or <strong>https://</strong>.
|
Uploaded files: <strong>MP4, WebM, OGV, MOV, MKV, AVI, M4A</strong>. M4A uploads keep the original source and add a black-frame H.264 MP4 source when ffmpeg is available. 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">
|
||||||
@@ -325,7 +347,7 @@ render_head('Add Video — Admin', '
|
|||||||
<div class="form-group source-location-panel source-location-local">
|
<div class="form-group source-location-panel source-location-local">
|
||||||
<label class="form-label">Video 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="video/mp4,video/webm,video/ogg,.mp4,.webm,.ogv,.mov,.mkv,.avi"
|
accept="video/mp4,video/webm,video/ogg,audio/mp4,.mp4,.webm,.ogv,.mov,.mkv,.avi,.m4a"
|
||||||
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>
|
||||||
|
|||||||
+30
-8
@@ -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_video = ['mp4','webm','ogv','ogg','mov','mkv','avi'];
|
$allowed_video = ['mp4','webm','ogv','ogg','mov','mkv','avi','m4a'];
|
||||||
|
|
||||||
$new_count = max(
|
$new_count = max(
|
||||||
count($new_types),
|
count($new_types),
|
||||||
@@ -166,12 +166,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (!in_array($ext, $allowed_video, true)) continue;
|
if (!in_array($ext, $allowed_video, true)) continue;
|
||||||
$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)) {
|
||||||
|
$file_path = 'videos/' . $fname;
|
||||||
$moved_files[] = $videos_dir . $fname;
|
$moved_files[] = $videos_dir . $fname;
|
||||||
$db->prepare("
|
|
||||||
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
|
if ($ext === 'm4a') {
|
||||||
VALUES (?,?,?,?,?,?,?,?)
|
$converted_path = media_black_video_from_m4a($file_path, $slug, $i);
|
||||||
")->execute([$id, $label, 'local', 'videos/' . $fname, '', mime_from_ext($fname), $quality, $sort]);
|
if ($converted_path !== '') {
|
||||||
$inserted_source_ids[] = (int)$db->lastInsertId();
|
$moved_files[] = MEDIA_DIR . $converted_path;
|
||||||
|
$converted_label = $label !== '' ? $label . ' (MP4)' : 'Converted MP4';
|
||||||
|
$db->prepare("
|
||||||
|
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?)
|
||||||
|
")->execute([$id, $converted_label, 'local', $converted_path, '', 'video/mp4', $quality, $sort]);
|
||||||
|
$inserted_source_ids[] = (int)$db->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
$original_label = $label !== '' ? $label . ' (M4A)' : 'Original M4A';
|
||||||
|
$db->prepare("
|
||||||
|
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?)
|
||||||
|
")->execute([$id, $original_label, 'local', $file_path, '', mime_from_ext($fname), '', $converted_path !== '' ? $sort + 1 : $sort]);
|
||||||
|
$inserted_source_ids[] = (int)$db->lastInsertId();
|
||||||
|
} else {
|
||||||
|
$db->prepare("
|
||||||
|
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?)
|
||||||
|
")->execute([$id, $label, 'local', $file_path, '', mime_from_ext($fname), $quality, $sort]);
|
||||||
|
$inserted_source_ids[] = (int)$db->lastInsertId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,7 +447,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 file, link a direct media file, or paste a provider page URL such as YouTube or Vimeo.</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. M4A uploads keep the original source and add a black-frame H.264 MP4 source when ffmpeg is available.</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">
|
||||||
@@ -438,7 +460,7 @@ render_head('Edit Video — Admin', '
|
|||||||
<div class="form-group source-location-panel source-location-local">
|
<div class="form-group source-location-panel source-location-local">
|
||||||
<label class="form-label">Video 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="video/mp4,video/webm,video/ogg,.mp4,.webm,.ogv,.mov,.mkv,.avi"
|
accept="video/mp4,video/webm,video/ogg,audio/mp4,.mp4,.webm,.ogv,.mov,.mkv,.avi,.m4a"
|
||||||
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>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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 M4A upload support that keeps the original M4A source and adds a generated black-frame H.264 MP4 source 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.
|
||||||
|
|||||||
@@ -1158,6 +1158,40 @@ function media_ffmpeg_path(): string {
|
|||||||
return $path !== '' && is_executable($path) ? $path : '';
|
return $path !== '' && is_executable($path) ? $path : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function media_black_video_from_m4a(string $file_path, string $slug, int $index = 0): string {
|
||||||
|
$ffmpeg = media_ffmpeg_path();
|
||||||
|
if ($ffmpeg === '') return '';
|
||||||
|
|
||||||
|
$source = MEDIA_DIR . ltrim($file_path, '/');
|
||||||
|
if ($file_path === '' || !is_file($source) || !is_readable($source)) return '';
|
||||||
|
if (strtolower(pathinfo($source, PATHINFO_EXTENSION)) !== 'm4a') return '';
|
||||||
|
|
||||||
|
$videos_dir = MEDIA_DIR . 'videos/';
|
||||||
|
if (!is_dir($videos_dir)) mkdir($videos_dir, 0755, true);
|
||||||
|
if (!function_exists('exec') || media_disabled_function('exec')) return '';
|
||||||
|
|
||||||
|
$label = media_clean_token($slug);
|
||||||
|
if ($label === '') $label = 'video';
|
||||||
|
$fname = $label . '_m4a_black_' . $index . '_' . time() . '_' . substr(sha1($file_path), 0, 8) . '.mp4';
|
||||||
|
$target = $videos_dir . $fname;
|
||||||
|
|
||||||
|
$cmd = escapeshellarg($ffmpeg)
|
||||||
|
. ' -y -f lavfi -i ' . escapeshellarg('color=c=black:s=1280x720:r=30')
|
||||||
|
. ' -i ' . escapeshellarg($source)
|
||||||
|
. ' -shortest -map 0:v:0 -map 1:a:0'
|
||||||
|
. ' -c:v libx264 -preset veryfast -tune stillimage -pix_fmt yuv420p'
|
||||||
|
. ' -c:a aac -b:a 192k -movflags +faststart '
|
||||||
|
. escapeshellarg($target) . ' 2>/dev/null';
|
||||||
|
@exec($cmd, $output, $code);
|
||||||
|
|
||||||
|
if ($code === 0 && is_file($target) && filesize($target) > 0) {
|
||||||
|
return 'videos/' . $fname;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_file($target)) unlink($target);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function media_thumbnail_from_local_video(string $file_path, string $slug): string {
|
function media_thumbnail_from_local_video(string $file_path, string $slug): string {
|
||||||
$ffmpeg = media_ffmpeg_path();
|
$ffmpeg = media_ffmpeg_path();
|
||||||
if ($ffmpeg === '') return '';
|
if ($ffmpeg === '') return '';
|
||||||
|
|||||||
Reference in New Issue
Block a user