- Implemented. The main database-backed player now understands provider URLs from lone-embed.php style handling.

What changed:
[includes/db.php (line 
227)](/Users/tyemeclifford/Documents/GH/mediaplayer/includes/db.php:227) 
now classifies YouTube, Vimeo, Dailymotion, Twitch, Facebook, TikTok, 
Instagram, X/Twitter, SoundCloud, Spotify, Google Drive, Wistia, 
Streamable, Loom, plus direct Dropbox/GitHub media rewrites.
[index.php (line 
235)](/Users/tyemeclifford/Documents/GH/mediaplayer/index.php:235) 
renders provider URLs as iframe embeds, while uploaded/direct files 
still use Video.js.
[embed.php (line 
198)](/Users/tyemeclifford/Documents/GH/mediaplayer/embed.php:198) now 
supports those provider embeds too.
[admin/add.php (line 
260)](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/add.php:260) 
and [admin/edit.php (line 
383)](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/edit.php:383) 
now describe external URLs as embed-capable and store recognized 
provider URLs as text/html.
[README.md (line 
63)](/Users/tyemeclifford/Documents/GH/mediaplayer/README.md:63) 
documents provider embed support.
This commit is contained in:
Ty Clifford
2026-06-24 12:15:03 -04:00
parent b24f0dc416
commit 75d5d39cda
6 changed files with 382 additions and 30 deletions
+3 -2
View File
@@ -65,7 +65,7 @@ Styled to match the existing `gate.php` / `index.php` dark theme exactly.
3. Upload a thumbnail (JPG/PNG/WebP)
4. Add one or more video sources. Each source can be either:
- an uploaded local video file, stored under `media/videos/`
- an external video URL, stored in SQLite and played directly by URL
- an external URL, stored in SQLite and played as a direct media source or provider embed
5. For uploaded files, supported formats include:
- **MP4** (H.264) — widest compatibility
- **WebM** (VP9) — smaller size, modern browsers
@@ -73,7 +73,7 @@ Styled to match the existing `gate.php` / `index.php` dark theme exactly.
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
External URLs must use `http://` or `https://`. Uploaded files and remote URLs can be mixed on the same video.
External URLs must use `http://` or `https://`. Uploaded files and remote URLs can be mixed on the same video. Recognized provider URLs such as YouTube, Vimeo, Dailymotion, Twitch, Facebook, TikTok, Instagram, X/Twitter, SoundCloud, Spotify, Google Drive, Wistia, Streamable, and Loom render through their embed players.
### Public Search
@@ -102,6 +102,7 @@ Use the **Sort Order** field — lower numbers appear first. Default is 0.
- Playback rate controls: 0.5×, 1×, 1.25×, 1.5×, 2×
- Multiple source fallback — browser picks best format automatically
- Local uploaded sources and remote URL sources
- Provider embeds for popular video and audio platforms
- Thumbnail posters
- Keyboard accessible
+4 -4
View File
@@ -83,7 +83,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$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, 'remote', '', $url, mime_from_ext($url), $quality, $sort]);
")->execute([$video_id, $label, 'remote', '', $url, source_storage_mime_from_url($url), $quality, $sort]);
$inserted_sources++;
continue;
}
@@ -259,7 +259,7 @@ render_head('Add Video — Admin', '
<div class="card">
<p class="section-heading">Video Sources</p>
<p class="hint" style="margin-bottom:1rem;">
Upload a video file or link to an external video URL. The database stores where each source lives, and the player uses that location at playback time.
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>.
</p>
@@ -270,7 +270,7 @@ render_head('Add Video — Admin', '
<label class="form-label">Source</label>
<select class="form-select source-type-select" name="source_type[]">
<option value="local" selected>Upload</option>
<option value="remote">External URL</option>
<option value="remote">External URL / Embed</option>
</select>
</div>
<div class="form-group source-location-panel source-location-local">
@@ -281,7 +281,7 @@ render_head('Add Video — Admin', '
</div>
<div class="form-group source-location-panel source-location-remote" hidden>
<label class="form-label">External URL</label>
<input class="form-input" type="url" name="source_url[]" placeholder="https://example.com/video.mp4">
<input class="form-input" type="url" name="source_url[]" placeholder="https://www.youtube.com/watch?v=...">
</div>
<div class="form-group">
<label class="form-label">Quality Label</label>
+6 -6
View File
@@ -103,7 +103,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($source_type === 'remote') {
$sql .= ", source_url=?, mime_type=?";
$params[] = $source_url;
$params[] = mime_from_ext($source_url);
$params[] = source_storage_mime_from_url($source_url);
}
$sql .= " WHERE id=? AND video_id=?";
$params[] = (int)$sid;
@@ -149,7 +149,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db->prepare("
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
VALUES (?,?,?,?,?,?,?,?)
")->execute([$id, $label, 'remote', '', $url, mime_from_ext($url), $quality, $sort]);
")->execute([$id, $label, 'remote', '', $url, source_storage_mime_from_url($url), $quality, $sort]);
$inserted_source_ids[] = (int)$db->lastInsertId();
continue;
}
@@ -340,7 +340,7 @@ render_head('Edit Video — Admin', '
<div class="source-item-header">
<div>
<span class="source-badge <?= source_is_remote($s) ? 'source-badge-remote' : 'source-badge-local' ?>">
<?= source_is_remote($s) ? 'External URL' : 'Uploaded File' ?>
<?= h(source_provider_label($s)) ?>
</span>
<div class="source-item-file"><?= h(source_location($s)) ?></div>
<div class="source-item-mime"><?= h($s['mime_type']) ?></div>
@@ -389,14 +389,14 @@ render_head('Edit Video — Admin', '
<!-- ── Add new sources ── -->
<div class="card" style="margin-bottom:1rem;">
<p class="section-heading">Add New Sources</p>
<p class="hint" style="margin-bottom:.85rem;">Upload another file or link an external URL. Remote URLs must use http:// or https://.</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="add-source-row" data-new-row="0">
<div class="form-group">
<label class="form-label">Source</label>
<select class="form-select source-type-select" name="new_source_type[]">
<option value="local" selected>Upload</option>
<option value="remote">External URL</option>
<option value="remote">External URL / Embed</option>
</select>
</div>
<div class="form-group source-location-panel source-location-local">
@@ -407,7 +407,7 @@ render_head('Edit Video — Admin', '
</div>
<div class="form-group source-location-panel source-location-remote" hidden>
<label class="form-label">External URL</label>
<input class="form-input" type="url" name="new_source_url[]" placeholder="https://example.com/video.mp4">
<input class="form-input" type="url" name="new_source_url[]" placeholder="https://www.youtube.com/watch?v=...">
</div>
<div class="form-group">
<label class="form-label">Quality</label>
+36 -5
View File
@@ -18,6 +18,19 @@ $script = dirname($_SERVER['SCRIPT_NAME'] ?? '/');
$script = rtrim($script, '/');
$base_url = $scheme . '://' . $host . $script . '/'; // e.g. https://tyclifford.com/live/
$player_sources = [];
if ($video && !empty($video['sources'])) {
foreach ($video['sources'] as $source) {
$player_source = source_player_data($source, $base_url . MEDIA_URL);
if ($player_source) $player_sources[] = $player_source;
}
}
$primary_player_source = $player_sources[0] ?? null;
$native_player_sources = [];
foreach ($player_sources as $player_source) {
if (($player_source['kind'] ?? '') !== 'iframe') $native_player_sources[] = $player_source;
}
// X-Frame-Options: allow embedding from any origin.
// If you want to restrict, change ALLOWALL to SAMEORIGIN or remove for CSP header instead.
header('X-Frame-Options: ALLOWALL');
@@ -111,6 +124,15 @@ header('Content-Security-Policy: frame-ancestors *');
.video-js .vjs-current-time,
.video-js .vjs-duration,
.video-js .vjs-time-divider { color: rgba(255,255,255,.8) !important; font-size: .7em !important; }
.provider-embed-frame {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
border: 0;
background: #000;
}
/* Title + branding overlay at top (fades out) */
.title-overlay {
@@ -173,7 +195,7 @@ header('Content-Security-Policy: frame-ancestors *');
<div class="player-wrap" id="player-wrap">
<div class="shimmer-bar"></div>
<?php if ($video && !empty($video['sources'])): ?>
<?php if ($video && !empty($player_sources)): ?>
<!-- Title + brand overlay -->
<div class="title-overlay" id="title-overlay">
@@ -182,19 +204,28 @@ header('Content-Security-Policy: frame-ancestors *');
target="_blank" rel="noopener">tyclifford.com</a>
</div>
<?php if (($primary_player_source['kind'] ?? '') === 'iframe'): ?>
<iframe class="provider-embed-frame"
src="<?= htmlspecialchars($primary_player_source['embed'], ENT_QUOTES) ?>"
title="<?= htmlspecialchars($video['title'], ENT_QUOTES) ?>"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share"
allowfullscreen
referrerpolicy="no-referrer-when-downgrade"></iframe>
<?php elseif (!empty($native_player_sources)): ?>
<video id="embed-player"
class="video-js vjs-default-skin vjs-big-play-centered"
controls preload="metadata"
<?php if ($video['thumbnail']): ?>
poster="<?= htmlspecialchars($base_url . MEDIA_URL . $video['thumbnail'], ENT_QUOTES) ?>"
<?php endif; ?>>
<?php foreach ($video['sources'] as $src): ?>
<?php foreach ($native_player_sources as $src): ?>
<source
src="<?= htmlspecialchars(source_public_url($src, $base_url . MEDIA_URL), ENT_QUOTES) ?>"
type="<?= htmlspecialchars($src['mime_type'], ENT_QUOTES) ?>">
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 else: ?>
@@ -211,7 +242,7 @@ header('Content-Security-Policy: frame-ancestors *');
<?php endif; ?>
</div>
<?php if ($video && !empty($video['sources'])): ?>
<?php if ($video && !empty($native_player_sources) && (!$primary_player_source || ($primary_player_source['kind'] ?? '') !== 'iframe')): ?>
<script>
document.addEventListener('DOMContentLoaded', function () {
var player = videojs('embed-player', {
+295
View File
@@ -130,6 +130,11 @@ function mime_from_ext(string $path): string {
'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',
default => 'video/mp4',
};
}
@@ -143,6 +148,249 @@ function normalize_media_url(string $url): string {
return in_array($scheme, ['http', 'https'], true) ? $url : '';
}
function media_host_matches(string $host, string $domain): bool {
$host = strtolower($host);
$domain = strtolower($domain);
return $host === $domain || substr($host, -strlen('.' . $domain)) === '.' . $domain;
}
function media_scalar_string($value): string {
return is_array($value) ? '' : trim((string)$value);
}
function media_query_values(string $url): array {
$query = parse_url($url, PHP_URL_QUERY);
if (!is_string($query) || $query === '') return [];
$values = [];
parse_str($query, $values);
return is_array($values) ? $values : [];
}
function media_path_segments(string $url): array {
$path = parse_url($url, PHP_URL_PATH);
if (!is_string($path)) return [];
$parts = array_values(array_filter(explode('/', trim($path, '/')), 'strlen'));
return array_map('rawurldecode', $parts);
}
function media_extension_from_url(string $url): string {
$path = parse_url($url, PHP_URL_PATH);
return is_string($path) ? strtolower(pathinfo($path, PATHINFO_EXTENSION)) : '';
}
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',
'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',
];
return $map[media_extension_from_url($url)] ?? '';
}
function media_append_query(string $base, array $params): string {
$query = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
if ($query === '') return $base;
return $base . (strpos($base, '?') === false ? '?' : '&') . $query;
}
function media_parse_youtube_time(string $value): int {
$value = trim($value);
if ($value === '') return 0;
if (ctype_digit($value)) return max(0, (int)$value);
$seconds = 0;
if (preg_match_all('/(\d+)\s*([hms])/i', $value, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$amount = (int)$match[1];
$unit = strtolower($match[2]);
$seconds += $unit === 'h' ? $amount * 3600 : ($unit === 'm' ? $amount * 60 : $amount);
}
}
return max(0, $seconds);
}
function media_clean_token(string $value, string $pattern = '/[^A-Za-z0-9_-]/'): string {
$clean = preg_replace($pattern, '', trim($value));
return is_string($clean) ? $clean : '';
}
function media_twitch_parent_host(): string {
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';
$host = strtolower(trim((string)$host));
$host = preg_replace('/:\d+$/', '', $host);
return is_string($host) && $host !== '' ? $host : 'localhost';
}
function media_provider_embed(string $url): ?array {
$parts = @parse_url($url);
if (!is_array($parts) || empty($parts['host'])) return null;
$host = strtolower((string)$parts['host']);
$path = isset($parts['path']) ? (string)$parts['path'] : '';
$segments = media_path_segments($url);
$query = media_query_values($url);
if (media_host_matches($host, 'youtu.be') || media_host_matches($host, 'youtube.com') || media_host_matches($host, 'youtube-nocookie.com')) {
$id = '';
if (media_host_matches($host, 'youtu.be') && isset($segments[0])) $id = $segments[0];
if ($id === '' && isset($query['v'])) $id = media_scalar_string($query['v']);
if ($id === '' && preg_match('#/(?:embed|shorts|live|v)/([^/?#]+)#', $path, $m)) $id = $m[1];
$id = media_clean_token($id);
if ($id !== '') {
$params = ['rel' => '0', 'modestbranding' => '1', 'playsinline' => '1'];
$start = isset($query['start'])
? media_parse_youtube_time(media_scalar_string($query['start']))
: (isset($query['t']) ? media_parse_youtube_time(media_scalar_string($query['t'])) : 0);
if ($start > 0) $params['start'] = (string)$start;
return ['provider' => 'youtube', 'embed' => media_append_query('https://www.youtube-nocookie.com/embed/' . rawurlencode($id), $params)];
}
if (isset($query['list'])) {
$list = media_clean_token(media_scalar_string($query['list']));
if ($list !== '') {
return ['provider' => 'youtube', 'embed' => media_append_query('https://www.youtube-nocookie.com/embed/videoseries', [
'list' => $list, 'rel' => '0', 'modestbranding' => '1', 'playsinline' => '1',
])];
}
}
}
if (media_host_matches($host, 'vimeo.com') && preg_match('#/(?:video/)?(\d+)#', $path, $m)) {
return ['provider' => 'vimeo', 'embed' => media_append_query('https://player.vimeo.com/video/' . rawurlencode($m[1]), [
'title' => '0', 'byline' => '0', 'portrait' => '0',
])];
}
if (media_host_matches($host, 'dailymotion.com') || media_host_matches($host, 'dai.ly')) {
$id = '';
if (media_host_matches($host, 'dai.ly') && isset($segments[0])) $id = $segments[0];
if ($id === '' && preg_match('#/(?:embed/)?video/([^/?#_]+)#', $path, $m)) $id = $m[1];
$id = media_clean_token($id);
if ($id !== '') return ['provider' => 'dailymotion', 'embed' => 'https://www.dailymotion.com/embed/video/' . rawurlencode($id)];
}
if (media_host_matches($host, 'twitch.tv')) {
$parent = media_twitch_parent_host();
if (preg_match('#/videos/(\d+)#', $path, $m)) {
return ['provider' => 'twitch', 'embed' => media_append_query('https://player.twitch.tv/', ['video' => $m[1], 'parent' => $parent, 'autoplay' => 'false'])];
}
if (preg_match('#/([^/]+)/clip/([^/?#]+)#', $path, $m)) {
return ['provider' => 'twitch', 'embed' => media_append_query('https://clips.twitch.tv/embed', ['clip' => media_clean_token($m[2]), 'parent' => $parent, 'autoplay' => 'false'])];
}
if (isset($segments[0]) && $segments[0] !== '') {
return ['provider' => 'twitch', 'embed' => media_append_query('https://player.twitch.tv/', ['channel' => media_clean_token($segments[0]), 'parent' => $parent, 'autoplay' => 'false'])];
}
}
if (media_host_matches($host, 'clips.twitch.tv') && isset($segments[0])) {
return ['provider' => 'twitch', 'embed' => media_append_query('https://clips.twitch.tv/embed', ['clip' => media_clean_token($segments[0]), 'parent' => media_twitch_parent_host(), 'autoplay' => 'false'])];
}
if (media_host_matches($host, 'facebook.com') || media_host_matches($host, 'fb.watch')) {
return ['provider' => 'facebook', 'embed' => media_append_query('https://www.facebook.com/plugins/video.php', ['href' => $url, 'show_text' => 'false', 'width' => '1280'])];
}
if (media_host_matches($host, 'tiktok.com') && preg_match('#/video/(\d+)#', $path, $m)) {
return ['provider' => 'tiktok', 'embed' => 'https://www.tiktok.com/embed/v2/' . rawurlencode($m[1])];
}
if (media_host_matches($host, 'instagram.com') && preg_match('#/(p|reel|tv)/([^/?#]+)#', $path, $m)) {
return ['provider' => 'instagram', 'embed' => 'https://www.instagram.com/' . rawurlencode($m[1]) . '/' . rawurlencode($m[2]) . '/embed'];
}
if ((media_host_matches($host, 'twitter.com') || media_host_matches($host, 'x.com')) && preg_match('#/status/(\d+)#', $path, $m)) {
return ['provider' => 'twitter', 'embed' => media_append_query('https://platform.twitter.com/embed/Tweet.html', ['id' => $m[1]])];
}
if (media_host_matches($host, 'soundcloud.com')) {
return ['provider' => 'soundcloud', 'embed' => media_append_query('https://w.soundcloud.com/player/', ['url' => $url, 'auto_play' => 'false', 'show_teaser' => 'true'])];
}
if (media_host_matches($host, 'open.spotify.com') && isset($segments[0], $segments[1])) {
$type = media_clean_token($segments[0]);
$id = media_clean_token($segments[1]);
if (in_array($type, ['album', 'artist', 'episode', 'playlist', 'show', 'track'], true) && $id !== '') {
return ['provider' => 'spotify', 'embed' => 'https://open.spotify.com/embed/' . rawurlencode($type) . '/' . rawurlencode($id)];
}
}
if (media_host_matches($host, 'drive.google.com')) {
$id = '';
if (preg_match('#/file/d/([^/]+)#', $path, $m)) $id = $m[1];
elseif (isset($query['id'])) $id = media_scalar_string($query['id']);
$id = media_clean_token($id);
if ($id !== '') return ['provider' => 'google-drive', 'embed' => 'https://drive.google.com/file/d/' . rawurlencode($id) . '/preview'];
}
if ((media_host_matches($host, 'wistia.com') || media_host_matches($host, 'wi.st')) && preg_match('#/(?:medias/)?([A-Za-z0-9]+)#', $path, $m)) {
return ['provider' => 'wistia', 'embed' => 'https://fast.wistia.net/embed/iframe/' . rawurlencode($m[1])];
}
if (media_host_matches($host, 'streamable.com') && isset($segments[0])) {
return ['provider' => 'streamable', 'embed' => 'https://streamable.com/e/' . rawurlencode(media_clean_token($segments[0]))];
}
if (media_host_matches($host, 'loom.com') && isset($segments[0], $segments[1]) && $segments[0] === 'share') {
return ['provider' => 'loom', 'embed' => 'https://www.loom.com/embed/' . rawurlencode(media_clean_token($segments[1]))];
}
return null;
}
function media_rewrite_direct_url(string $url): string {
$host = strtolower((string)(parse_url($url, PHP_URL_HOST) ?? ''));
$path = (string)(parse_url($url, PHP_URL_PATH) ?? '');
if (media_host_matches($host, 'dropbox.com')) {
$parts = @parse_url($url);
if (is_array($parts)) {
$query = [];
if (isset($parts['query'])) parse_str((string)$parts['query'], $query);
unset($query['dl'], $query['raw']);
$query['raw'] = '1';
$rebuilt = ($parts['scheme'] ?? 'https') . '://' . ($parts['host'] ?? 'www.dropbox.com');
$rebuilt .= $parts['path'] ?? '';
$rebuilt = media_append_query($rebuilt, $query);
if (isset($parts['fragment'])) $rebuilt .= '#' . $parts['fragment'];
return $rebuilt;
}
}
if (media_host_matches($host, 'github.com') && preg_match('#^/([^/]+)/([^/]+)/blob/([^/]+)/(.+)$#', $path, $m)) {
return 'https://raw.githubusercontent.com/' . rawurlencode($m[1]) . '/' . rawurlencode($m[2]) . '/' . rawurlencode($m[3]) . '/' . str_replace('%2F', '/', rawurlencode($m[4]));
}
return $url;
}
function media_classify_kind(string $url, string $mime, string $forced_mode = 'auto'): string {
$forced_mode = strtolower($forced_mode);
if (in_array($forced_mode, ['video', 'audio', 'hls', 'dash', 'iframe'], true)) return $forced_mode;
$ext = media_extension_from_url($url);
$mime_lc = strtolower($mime);
if ($ext === 'm3u8' || in_array($mime_lc, ['application/x-mpegurl', 'application/vnd.apple.mpegurl'], true)) return 'hls';
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';
return 'unknown';
}
function source_storage_mime_from_url(string $url): string {
$url = normalize_media_url($url);
if ($url === '') return 'video/mp4';
if (media_provider_embed($url)) return 'text/html';
$direct = media_rewrite_direct_url($url);
return media_guess_mime($direct) ?: 'text/html';
}
function source_is_remote(array $source): bool {
return ($source['source_type'] ?? 'local') === 'remote';
}
@@ -163,6 +411,53 @@ function source_public_url(array $source, string $media_base = MEDIA_URL): strin
return rtrim($media_base, '/') . '/' . ltrim($source['file_path'] ?? '', '/');
}
function source_player_data(array $source, string $media_base = MEDIA_URL): ?array {
$is_remote = source_is_remote($source);
$original_url = $is_remote ? normalize_media_url($source['source_url'] ?? '') : source_public_url($source, $media_base);
if ($original_url === '') return null;
if ($is_remote) {
$provider = media_provider_embed($original_url);
$play_url = $provider ? $original_url : media_rewrite_direct_url($original_url);
$mime = $provider ? 'text/html' : trim($source['mime_type'] ?? '');
if ($mime === '' || $mime === 'text/html') $mime = $provider ? 'text/html' : media_guess_mime($play_url);
$kind = $provider ? 'iframe' : media_classify_kind($play_url, $mime);
if ($kind === 'unknown') {
$kind = 'iframe';
$mime = 'text/html';
}
$embed = $provider['embed'] ?? $play_url;
$provider_name = $provider['provider'] ?? '';
} else {
$play_url = $original_url;
$mime = trim($source['mime_type'] ?? '') ?: mime_from_ext($play_url);
$kind = media_classify_kind($play_url, $mime);
$embed = $play_url;
$provider_name = '';
}
$label = trim($source['label'] ?? '') ?: trim($source['quality'] ?? '');
if ($label === '') $label = $provider_name ?: (media_extension_from_url($play_url) ?: $kind);
return [
'url' => $play_url,
'original_url' => $original_url,
'embed' => $embed,
'provider' => $provider_name,
'mime' => $mime,
'kind' => $kind,
'label' => $label,
];
}
function source_provider_label(array $source): string {
$player = source_player_data($source);
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';
return source_is_remote($source) ? 'External URL' : 'Uploaded File';
}
function format_duration(int $seconds): string {
if ($seconds <= 0) return '';
$h = intdiv($seconds, 3600);
+28 -3
View File
@@ -17,6 +17,19 @@ $per_page = (int)setting('catalogue_per_page', '10');
$page = max(1, (int)($_GET['page'] ?? 1));
$paged = get_videos_paginated($page, $per_page);
$player_sources = [];
if ($video && !empty($video['sources'])) {
foreach ($video['sources'] as $source) {
$player_source = source_player_data($source);
if ($player_source) $player_sources[] = $player_source;
}
}
$primary_player_source = $player_sources[0] ?? null;
$native_player_sources = [];
foreach ($player_sources as $player_source) {
if (($player_source['kind'] ?? '') !== 'iframe') $native_player_sources[] = $player_source;
}
// Build absolute base URL used for embed snippet generation
$_scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$_host = $_SERVER['HTTP_HOST'] ?? 'localhost';
@@ -117,6 +130,11 @@ render_head('Media — TyClifford.com', '
.video-js .vjs-current-time,
.video-js .vjs-duration,
.video-js .vjs-time-divider { color:rgba(255,255,255,.8) !important; font-size:.7em !important; }
.provider-embed-frame {
position:absolute; inset:0;
display:block; width:100%; height:100%;
border:0; background:#000;
}
/* ── Lower grid ── */
.lower-grid { display:grid; grid-template-columns:1fr; gap:1rem; }
@@ -214,7 +232,14 @@ render_head('Media — TyClifford.com', '
<!-- ═══════════════════════════ PLAYER CARD ════════════════════════════════ -->
<section class="stream-card" aria-label="Media player">
<div class="stream-ratio">
<?php if ($video && !empty($video['sources'])): ?>
<?php if ($primary_player_source && ($primary_player_source['kind'] ?? '') === 'iframe'): ?>
<iframe class="provider-embed-frame"
src="<?= h($primary_player_source['embed']) ?>"
title="<?= h($video['title']) ?>"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share"
allowfullscreen
referrerpolicy="no-referrer-when-downgrade"></iframe>
<?php elseif ($video && !empty($native_player_sources)): ?>
<video id="main-player"
class="video-js vjs-default-skin vjs-big-play-centered"
controls preload="metadata"
@@ -222,8 +247,8 @@ render_head('Media — TyClifford.com', '
poster="<?= h(MEDIA_URL . $video['thumbnail']) ?>"
<?php endif; ?>
data-setup='{"fluid":false,"responsive":true,"playbackRates":[0.5,1,1.25,1.5,2]}'>
<?php foreach ($video['sources'] as $src): ?>
<source src="<?= h(source_public_url($src)) ?>" type="<?= h($src['mime_type']) ?>" label="<?= h($src['quality'] ?: $src['label']) ?>">
<?php foreach ($native_player_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>