- 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
+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);