1323 lines
40 KiB
PHP
1323 lines
40 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* embed.php - Standalone embeddable player, URL based and database free.
|
|
*
|
|
* Accepted query parameters:
|
|
* video, url, src, v, slug Primary URL.
|
|
* video2-video10 Additional fallback URLs.
|
|
* url2-url10 Additional fallback URLs.
|
|
* title Optional display title.
|
|
* poster Optional poster/cover image URL.
|
|
* mode auto, video, audio, hls, dash, iframe.
|
|
* autoplay 1/0.
|
|
* muted 1/0.
|
|
* loop 1/0.
|
|
* controls 1/0, default 1.
|
|
* iframe_fallback 1/0, default 1.
|
|
* crossorigin anonymous or use-credentials.
|
|
* with_credentials 1/0, for HLS segment requests.
|
|
*
|
|
* MIME override:
|
|
* Append |mime/type to a source URL:
|
|
* embed.php?video=https%3A%2F%2Fexample.com%2Fstream%7Capplication%2Fx-mpegURL
|
|
*
|
|
* Important:
|
|
* URL-encode media URLs that contain their own ? or & characters.
|
|
*/
|
|
|
|
function h(string $value): string
|
|
{
|
|
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
}
|
|
|
|
function starts_with(string $value, string $prefix): bool
|
|
{
|
|
return $prefix === '' || strncmp($value, $prefix, strlen($prefix)) === 0;
|
|
}
|
|
|
|
function host_matches(string $host, string $domain): bool
|
|
{
|
|
$host = strtolower($host);
|
|
$domain = strtolower($domain);
|
|
return $host === $domain || substr($host, -strlen('.' . $domain)) === '.' . $domain;
|
|
}
|
|
|
|
function scalar_string($value): string
|
|
{
|
|
if (is_array($value)) {
|
|
return '';
|
|
}
|
|
|
|
return trim((string) $value);
|
|
}
|
|
|
|
function get_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 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 extension_from_url(string $url): string
|
|
{
|
|
$path = parse_url($url, PHP_URL_PATH);
|
|
if (!is_string($path)) {
|
|
return '';
|
|
}
|
|
|
|
return strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
|
}
|
|
|
|
function 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',
|
|
'wmv' => 'video/x-ms-wmv',
|
|
'flv' => 'video/x-flv',
|
|
'ts' => 'video/mp2t',
|
|
'3gp' => 'video/3gpp',
|
|
'3g2' => 'video/3gpp2',
|
|
'm3u8' => 'application/vnd.apple.mpegurl',
|
|
'mpd' => 'application/dash+xml',
|
|
'mp3' => 'audio/mpeg',
|
|
'm4a' => 'audio/mp4',
|
|
'aac' => 'audio/aac',
|
|
'wav' => 'audio/wav',
|
|
'flac' => 'audio/flac',
|
|
'oga' => 'audio/ogg',
|
|
'opus' => 'audio/ogg',
|
|
];
|
|
|
|
$ext = extension_from_url($url);
|
|
return $map[$ext] ?? '';
|
|
}
|
|
|
|
function classify_kind(string $url, string $mime, string $forced_mode): string
|
|
{
|
|
$forced = strtolower($forced_mode);
|
|
if (in_array($forced, ['video', 'audio', 'hls', 'dash', 'iframe'], true)) {
|
|
return $forced;
|
|
}
|
|
|
|
$ext = 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 (starts_with($mime_lc, 'audio/')) {
|
|
return 'audio';
|
|
}
|
|
|
|
if (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 normalize_url(string $url): string
|
|
{
|
|
$url = trim(html_entity_decode($url, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
|
if ($url === '') {
|
|
return '';
|
|
}
|
|
|
|
if (starts_with($url, '//')) {
|
|
$url = 'https:' . $url;
|
|
}
|
|
|
|
$scheme = parse_url($url, PHP_URL_SCHEME);
|
|
if (!is_string($scheme) || $scheme === '') {
|
|
return $url;
|
|
}
|
|
|
|
$scheme = strtolower($scheme);
|
|
if (!in_array($scheme, ['http', 'https'], true)) {
|
|
return '';
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
function 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 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]);
|
|
if ($unit === 'h') {
|
|
$seconds += $amount * 3600;
|
|
} elseif ($unit === 'm') {
|
|
$seconds += $amount * 60;
|
|
} else {
|
|
$seconds += $amount;
|
|
}
|
|
}
|
|
}
|
|
|
|
return max(0, $seconds);
|
|
}
|
|
|
|
function clean_token(string $value, string $pattern = '/[^A-Za-z0-9_-]/'): string
|
|
{
|
|
$clean = preg_replace($pattern, '', trim($value));
|
|
return is_string($clean) ? $clean : '';
|
|
}
|
|
|
|
function twitch_parent_host(): string
|
|
{
|
|
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';
|
|
$host = strtolower(trim((string) $host));
|
|
$host = preg_replace('/:\d+$/', '', $host);
|
|
$host = is_string($host) ? $host : 'localhost';
|
|
return $host !== '' ? $host : 'localhost';
|
|
}
|
|
|
|
function 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 = path_segments($url);
|
|
$query = get_query_values($url);
|
|
|
|
if (host_matches($host, 'youtu.be') || host_matches($host, 'youtube.com') || host_matches($host, 'youtube-nocookie.com')) {
|
|
$id = '';
|
|
if (host_matches($host, 'youtu.be') && isset($segments[0])) {
|
|
$id = $segments[0];
|
|
}
|
|
if ($id === '' && isset($query['v'])) {
|
|
$id = scalar_string($query['v']);
|
|
}
|
|
if ($id === '' && preg_match('#/(?:embed|shorts|live|v)/([^/?#]+)#', $path, $m)) {
|
|
$id = $m[1];
|
|
}
|
|
|
|
$id = clean_token($id);
|
|
if ($id !== '') {
|
|
$params = ['rel' => '0', 'modestbranding' => '1', 'playsinline' => '1'];
|
|
$start = 0;
|
|
if (isset($query['start'])) {
|
|
$start = parse_youtube_time(scalar_string($query['start']));
|
|
} elseif (isset($query['t'])) {
|
|
$start = parse_youtube_time(scalar_string($query['t']));
|
|
}
|
|
if ($start > 0) {
|
|
$params['start'] = (string) $start;
|
|
}
|
|
|
|
return [
|
|
'provider' => 'youtube',
|
|
'embed' => append_query('https://www.youtube-nocookie.com/embed/' . rawurlencode($id), $params),
|
|
];
|
|
}
|
|
|
|
if (isset($query['list'])) {
|
|
$list = clean_token(scalar_string($query['list']));
|
|
if ($list !== '') {
|
|
return [
|
|
'provider' => 'youtube',
|
|
'embed' => append_query('https://www.youtube-nocookie.com/embed/videoseries', [
|
|
'list' => $list,
|
|
'rel' => '0',
|
|
'modestbranding' => '1',
|
|
'playsinline' => '1',
|
|
]),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (host_matches($host, 'vimeo.com')) {
|
|
$id = '';
|
|
if (preg_match('#/(?:video/)?(\d+)#', $path, $m)) {
|
|
$id = $m[1];
|
|
}
|
|
|
|
if ($id !== '') {
|
|
return [
|
|
'provider' => 'vimeo',
|
|
'embed' => append_query('https://player.vimeo.com/video/' . rawurlencode($id), [
|
|
'title' => '0',
|
|
'byline' => '0',
|
|
'portrait' => '0',
|
|
]),
|
|
];
|
|
}
|
|
}
|
|
|
|
if (host_matches($host, 'dailymotion.com') || host_matches($host, 'dai.ly')) {
|
|
$id = '';
|
|
if (host_matches($host, 'dai.ly') && isset($segments[0])) {
|
|
$id = $segments[0];
|
|
}
|
|
if ($id === '' && preg_match('#/(?:embed/)?video/([^/?#_]+)#', $path, $m)) {
|
|
$id = $m[1];
|
|
}
|
|
|
|
$id = clean_token($id);
|
|
if ($id !== '') {
|
|
return [
|
|
'provider' => 'dailymotion',
|
|
'embed' => 'https://www.dailymotion.com/embed/video/' . rawurlencode($id),
|
|
];
|
|
}
|
|
}
|
|
|
|
if (host_matches($host, 'twitch.tv')) {
|
|
$parent = twitch_parent_host();
|
|
|
|
if (preg_match('#/videos/(\d+)#', $path, $m)) {
|
|
return [
|
|
'provider' => 'twitch',
|
|
'embed' => append_query('https://player.twitch.tv/', [
|
|
'video' => $m[1],
|
|
'parent' => $parent,
|
|
'autoplay' => 'false',
|
|
]),
|
|
];
|
|
}
|
|
|
|
if (preg_match('#/([^/]+)/clip/([^/?#]+)#', $path, $m)) {
|
|
return [
|
|
'provider' => 'twitch',
|
|
'embed' => append_query('https://clips.twitch.tv/embed', [
|
|
'clip' => clean_token($m[2]),
|
|
'parent' => $parent,
|
|
'autoplay' => 'false',
|
|
]),
|
|
];
|
|
}
|
|
|
|
if (isset($segments[0]) && $segments[0] !== '') {
|
|
return [
|
|
'provider' => 'twitch',
|
|
'embed' => append_query('https://player.twitch.tv/', [
|
|
'channel' => clean_token($segments[0]),
|
|
'parent' => $parent,
|
|
'autoplay' => 'false',
|
|
]),
|
|
];
|
|
}
|
|
}
|
|
|
|
if (host_matches($host, 'clips.twitch.tv') && isset($segments[0])) {
|
|
return [
|
|
'provider' => 'twitch',
|
|
'embed' => append_query('https://clips.twitch.tv/embed', [
|
|
'clip' => clean_token($segments[0]),
|
|
'parent' => twitch_parent_host(),
|
|
'autoplay' => 'false',
|
|
]),
|
|
];
|
|
}
|
|
|
|
if (host_matches($host, 'facebook.com') || host_matches($host, 'fb.watch')) {
|
|
return [
|
|
'provider' => 'facebook',
|
|
'embed' => append_query('https://www.facebook.com/plugins/video.php', [
|
|
'href' => $url,
|
|
'show_text' => 'false',
|
|
'width' => '1280',
|
|
]),
|
|
];
|
|
}
|
|
|
|
if (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 (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 ((host_matches($host, 'twitter.com') || host_matches($host, 'x.com')) && preg_match('#/status/(\d+)#', $path, $m)) {
|
|
return [
|
|
'provider' => 'twitter',
|
|
'embed' => append_query('https://platform.twitter.com/embed/Tweet.html', ['id' => $m[1]]),
|
|
];
|
|
}
|
|
|
|
if (host_matches($host, 'soundcloud.com')) {
|
|
return [
|
|
'provider' => 'soundcloud',
|
|
'embed' => append_query('https://w.soundcloud.com/player/', [
|
|
'url' => $url,
|
|
'auto_play' => 'false',
|
|
'show_teaser' => 'true',
|
|
]),
|
|
];
|
|
}
|
|
|
|
if (host_matches($host, 'open.spotify.com') && isset($segments[0], $segments[1])) {
|
|
$type = clean_token($segments[0]);
|
|
$id = 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 (host_matches($host, 'drive.google.com')) {
|
|
$id = '';
|
|
if (preg_match('#/file/d/([^/]+)#', $path, $m)) {
|
|
$id = $m[1];
|
|
} elseif (isset($query['id'])) {
|
|
$id = scalar_string($query['id']);
|
|
}
|
|
|
|
$id = clean_token($id);
|
|
if ($id !== '') {
|
|
return [
|
|
'provider' => 'google-drive',
|
|
'embed' => 'https://drive.google.com/file/d/' . rawurlencode($id) . '/preview',
|
|
];
|
|
}
|
|
}
|
|
|
|
if ((host_matches($host, 'wistia.com') || 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 (host_matches($host, 'streamable.com') && isset($segments[0])) {
|
|
return [
|
|
'provider' => 'streamable',
|
|
'embed' => 'https://streamable.com/e/' . rawurlencode(clean_token($segments[0])),
|
|
];
|
|
}
|
|
|
|
if (host_matches($host, 'loom.com') && isset($segments[0], $segments[1]) && $segments[0] === 'share') {
|
|
return [
|
|
'provider' => 'loom',
|
|
'embed' => 'https://www.loom.com/embed/' . rawurlencode(clean_token($segments[1])),
|
|
];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function rewrite_direct_url(string $url): string
|
|
{
|
|
$host = strtolower((string) (parse_url($url, PHP_URL_HOST) ?? ''));
|
|
$path = (string) (parse_url($url, PHP_URL_PATH) ?? '');
|
|
|
|
if (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 = append_query($rebuilt, $query);
|
|
if (isset($parts['fragment'])) {
|
|
$rebuilt .= '#' . $parts['fragment'];
|
|
}
|
|
return $rebuilt;
|
|
}
|
|
}
|
|
|
|
if (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 source_from_raw(string $raw, string $forced_mode): ?array
|
|
{
|
|
$raw = trim($raw);
|
|
if ($raw === '') {
|
|
return null;
|
|
}
|
|
|
|
$mime = '';
|
|
if (strpos($raw, '|') !== false) {
|
|
[$raw, $mime] = explode('|', $raw, 2);
|
|
$mime = trim($mime);
|
|
}
|
|
|
|
$original_url = normalize_url($raw);
|
|
if ($original_url === '') {
|
|
return null;
|
|
}
|
|
|
|
$provider = provider_embed($original_url);
|
|
$url = $provider ? $original_url : rewrite_direct_url($original_url);
|
|
if ($mime === '') {
|
|
$mime = guess_mime($url);
|
|
}
|
|
|
|
$kind = classify_kind($url, $mime, $forced_mode);
|
|
if ($provider && strtolower($forced_mode) === 'auto') {
|
|
$kind = 'iframe';
|
|
}
|
|
|
|
if (strtolower($forced_mode) === 'iframe') {
|
|
$kind = 'iframe';
|
|
}
|
|
|
|
$ext = extension_from_url($url);
|
|
$label = $provider['provider'] ?? ($ext !== '' ? $ext : ($kind === 'unknown' ? 'url' : $kind));
|
|
|
|
return [
|
|
'url' => $url,
|
|
'originalUrl' => $original_url,
|
|
'embed' => $provider['embed'] ?? $url,
|
|
'provider' => $provider['provider'] ?? '',
|
|
'mime' => $mime,
|
|
'kind' => $kind,
|
|
'label' => $label,
|
|
];
|
|
}
|
|
|
|
function query_bool(string $name, bool $default = false): bool
|
|
{
|
|
if (!array_key_exists($name, $_GET)) {
|
|
return $default;
|
|
}
|
|
|
|
$value = strtolower(trim((string) $_GET[$name]));
|
|
if (in_array($value, ['1', 'true', 'yes', 'on'], true)) {
|
|
return true;
|
|
}
|
|
if (in_array($value, ['0', 'false', 'no', 'off'], true)) {
|
|
return false;
|
|
}
|
|
|
|
return $default;
|
|
}
|
|
|
|
function media_type_attr(array $source): string
|
|
{
|
|
$mime = isset($source['mime']) ? trim((string) $source['mime']) : '';
|
|
return $mime !== '' ? ' type="' . h($mime) . '"' : '';
|
|
}
|
|
|
|
function media_common_attrs(array $config): string
|
|
{
|
|
$attrs = [];
|
|
|
|
if (($config['controls'] ?? true) !== false) {
|
|
$attrs[] = 'controls';
|
|
}
|
|
if (($config['autoplay'] ?? false) === true) {
|
|
$attrs[] = 'autoplay';
|
|
}
|
|
if (($config['muted'] ?? false) === true || ($config['autoplay'] ?? false) === true) {
|
|
$attrs[] = 'muted';
|
|
}
|
|
if (($config['loop'] ?? false) === true) {
|
|
$attrs[] = 'loop';
|
|
}
|
|
|
|
$attrs[] = 'preload="metadata"';
|
|
|
|
$crossorigin = trim((string) ($config['crossorigin'] ?? ''));
|
|
if ($crossorigin !== '') {
|
|
$attrs[] = 'crossorigin="' . h($crossorigin) . '"';
|
|
}
|
|
|
|
return implode(' ', $attrs);
|
|
}
|
|
|
|
$title = trim((string) ($_GET['title'] ?? ''));
|
|
$poster = normalize_url((string) ($_GET['poster'] ?? ''));
|
|
$mode = strtolower(trim((string) ($_GET['mode'] ?? 'auto')));
|
|
if (!in_array($mode, ['auto', 'video', 'audio', 'hls', 'dash', 'iframe'], true)) {
|
|
$mode = 'auto';
|
|
}
|
|
|
|
$primary_raw = '';
|
|
foreach (['video', 'url', 'src', 'v', 'slug'] as $key) {
|
|
if (isset($_GET[$key]) && trim((string) $_GET[$key]) !== '') {
|
|
$primary_raw = trim((string) $_GET[$key]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
$sources = [];
|
|
if ($primary_raw !== '') {
|
|
$source = source_from_raw($primary_raw, $mode);
|
|
if ($source !== null) {
|
|
$sources[] = $source;
|
|
}
|
|
}
|
|
|
|
for ($i = 2; $i <= 10; $i++) {
|
|
foreach (["video{$i}", "url{$i}", "src{$i}"] as $key) {
|
|
if (isset($_GET[$key]) && trim((string) $_GET[$key]) !== '') {
|
|
$source = source_from_raw(trim((string) $_GET[$key]), $mode);
|
|
if ($source !== null) {
|
|
$sources[] = $source;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$crossorigin = strtolower(trim((string) ($_GET['crossorigin'] ?? '')));
|
|
if (!in_array($crossorigin, ['anonymous', 'use-credentials'], true)) {
|
|
$crossorigin = '';
|
|
}
|
|
|
|
$config = [
|
|
'sources' => $sources,
|
|
'title' => $title,
|
|
'poster' => $poster,
|
|
'autoplay' => query_bool('autoplay', false),
|
|
'muted' => query_bool('muted', false),
|
|
'loop' => query_bool('loop', false),
|
|
'controls' => query_bool('controls', true),
|
|
'playsinline' => true,
|
|
'iframeFallback' => query_bool('iframe_fallback', true),
|
|
'crossorigin' => $crossorigin,
|
|
'withCredentials' => query_bool('with_credentials', false),
|
|
];
|
|
|
|
$primary_source = $sources[0] ?? null;
|
|
|
|
$json_config = json_encode(
|
|
$config,
|
|
JSON_UNESCAPED_SLASHES
|
|
| JSON_UNESCAPED_UNICODE
|
|
| JSON_HEX_TAG
|
|
| JSON_HEX_AMP
|
|
| JSON_HEX_APOS
|
|
| JSON_HEX_QUOT
|
|
);
|
|
if (!is_string($json_config)) {
|
|
$json_config = '{"sources":[]}';
|
|
}
|
|
|
|
header_remove('X-Frame-Options');
|
|
header_remove('Content-Security-Policy');
|
|
header_remove('X-Content-Security-Policy');
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
header("Content-Security-Policy: default-src * data: blob: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline'; img-src * data: blob:; media-src * data: blob:; frame-src *; connect-src *");
|
|
header('Referrer-Policy: no-referrer-when-downgrade');
|
|
header('Cross-Origin-Resource-Policy: cross-origin');
|
|
header('X-Content-Type-Options: nosniff');
|
|
?><!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="referrer" content="no-referrer-when-downgrade">
|
|
<title><?= $title !== '' ? h($title) . ' - Player' : 'Player' ?></title>
|
|
<script defer src="https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.min.js"></script>
|
|
<script defer src="https://cdn.jsdelivr.net/npm/dashjs@4/dist/dash.all.min.js"></script>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
html, body { width: 100%; height: 100%; margin: 0; }
|
|
body {
|
|
overflow: hidden;
|
|
background: #05060a;
|
|
color: #f4f7fb;
|
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
}
|
|
.player {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 160px;
|
|
background: #000;
|
|
isolation: isolate;
|
|
}
|
|
.accent {
|
|
position: absolute;
|
|
z-index: 8;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
background: linear-gradient(90deg, #ff3f5f, #9d65ff, #2ccfff, #19e68c, #2ccfff, #9d65ff, #ff3f5f);
|
|
background-size: 300% 100%;
|
|
animation: shimmer 5s linear infinite;
|
|
}
|
|
@keyframes shimmer {
|
|
from { background-position: 0 50%; }
|
|
to { background-position: 200% 50%; }
|
|
}
|
|
.top {
|
|
position: absolute;
|
|
z-index: 7;
|
|
top: 10px;
|
|
left: 12px;
|
|
right: 12px;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
pointer-events: none;
|
|
opacity: 1;
|
|
transition: opacity 180ms ease;
|
|
}
|
|
.top.is-hidden { opacity: 0; }
|
|
.title {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
color: #fff;
|
|
font-size: 13px;
|
|
font-weight: 650;
|
|
line-height: 1.25;
|
|
text-overflow: ellipsis;
|
|
text-shadow: 0 1px 8px rgba(0,0,0,.8);
|
|
white-space: nowrap;
|
|
}
|
|
.badges {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
gap: 4px;
|
|
max-width: 45%;
|
|
}
|
|
.badge {
|
|
border: 1px solid rgba(255,255,255,.14);
|
|
border-radius: 4px;
|
|
background: rgba(0,0,0,.48);
|
|
color: rgba(255,255,255,.62);
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
font-size: 10px;
|
|
letter-spacing: .06em;
|
|
line-height: 1;
|
|
padding: 4px 6px;
|
|
text-transform: uppercase;
|
|
white-space: nowrap;
|
|
}
|
|
.mount,
|
|
.frame-mount,
|
|
.media-mount {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
.native-media,
|
|
.embed-frame {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
border: 0;
|
|
background: #000;
|
|
}
|
|
.native-video { object-fit: contain; }
|
|
.audio-layout {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 28px;
|
|
background:
|
|
radial-gradient(circle at 25% 20%, rgba(25,230,140,.2), transparent 26%),
|
|
radial-gradient(circle at 80% 75%, rgba(157,101,255,.18), transparent 24%),
|
|
#080910;
|
|
}
|
|
.audio-card {
|
|
width: min(760px, 100%);
|
|
display: grid;
|
|
grid-template-columns: minmax(96px, 180px) minmax(0, 1fr);
|
|
gap: 18px;
|
|
align-items: center;
|
|
}
|
|
.cover {
|
|
aspect-ratio: 1;
|
|
border-radius: 8px;
|
|
background: linear-gradient(135deg, #141827, #243347);
|
|
background-position: center;
|
|
background-size: cover;
|
|
box-shadow: 0 18px 55px rgba(0,0,0,.35);
|
|
}
|
|
.native-audio { height: 54px; background: transparent; }
|
|
.status {
|
|
position: absolute;
|
|
z-index: 9;
|
|
left: 12px;
|
|
bottom: 12px;
|
|
max-width: calc(100% - 24px);
|
|
border: 1px solid rgba(255,255,255,.12);
|
|
border-radius: 5px;
|
|
background: rgba(5,6,10,.72);
|
|
color: rgba(255,255,255,.72);
|
|
font-size: 12px;
|
|
line-height: 1.35;
|
|
padding: 7px 9px;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
.error,
|
|
.empty {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 10;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 22px;
|
|
background: #080910;
|
|
text-align: center;
|
|
}
|
|
.panel {
|
|
width: min(560px, 100%);
|
|
display: grid;
|
|
gap: 10px;
|
|
justify-items: center;
|
|
}
|
|
.panel h1 {
|
|
margin: 0;
|
|
color: #fff;
|
|
font-size: 16px;
|
|
line-height: 1.25;
|
|
}
|
|
.panel p {
|
|
margin: 0;
|
|
color: rgba(255,255,255,.62);
|
|
font-size: 13px;
|
|
line-height: 1.55;
|
|
}
|
|
.panel a {
|
|
color: #7ee7ff;
|
|
font-size: 13px;
|
|
text-decoration: none;
|
|
}
|
|
.panel code {
|
|
color: rgba(255,255,255,.86);
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
font-size: 12px;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.is-hidden { display: none !important; }
|
|
@media (max-width: 520px) {
|
|
.top { top: 8px; left: 8px; right: 8px; }
|
|
.title { font-size: 12px; }
|
|
.badges { max-width: 38%; }
|
|
.audio-card { grid-template-columns: 1fr; }
|
|
.cover { width: min(180px, 60vw); justify-self: center; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="player" id="player" aria-label="Embedded media player">
|
|
<div class="accent"></div>
|
|
|
|
<?php if (count($sources) > 0): ?>
|
|
<div class="top" id="top-overlay">
|
|
<div class="title"><?= $title !== '' ? h($title) : '' ?></div>
|
|
<div class="badges" aria-hidden="true">
|
|
<?php foreach ($sources as $source): ?>
|
|
<span class="badge"><?= h((string) $source['label']) ?></span>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mount">
|
|
<?php
|
|
$primary_kind = is_array($primary_source) ? (string) ($primary_source['kind'] ?? 'video') : 'video';
|
|
$show_frame = is_array($primary_source) && $primary_kind === 'iframe';
|
|
?>
|
|
<div class="media-mount<?= $show_frame ? ' is-hidden' : '' ?>" id="media-mount">
|
|
<?php if (is_array($primary_source) && !$show_frame): ?>
|
|
<?php if ($primary_kind === 'audio'): ?>
|
|
<div class="audio-layout">
|
|
<div class="audio-card">
|
|
<div class="cover"<?php if ($poster !== ''): ?> style="background-image:url('<?= h($poster) ?>')"<?php endif; ?>></div>
|
|
<audio class="native-media native-audio" <?= media_common_attrs($config) ?>>
|
|
<source src="<?= h((string) $primary_source['url']) ?>"<?= media_type_attr($primary_source) ?>>
|
|
</audio>
|
|
</div>
|
|
</div>
|
|
<?php else: ?>
|
|
<video class="native-media native-video"
|
|
<?= media_common_attrs($config) ?>
|
|
playsinline
|
|
<?php if ($poster !== ''): ?>poster="<?= h($poster) ?>"<?php endif; ?>>
|
|
<source src="<?= h((string) $primary_source['url']) ?>"<?= media_type_attr($primary_source) ?>>
|
|
</video>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="frame-mount<?= $show_frame ? '' : ' is-hidden' ?>" id="frame-mount">
|
|
<?php if (is_array($primary_source) && $show_frame): ?>
|
|
<iframe class="embed-frame"
|
|
src="<?= h((string) ($primary_source['embed'] ?? $primary_source['url'])) ?>"
|
|
title="<?= h($title !== '' ? $title : (string) ($primary_source['label'] ?? 'Embedded media')) ?>"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share"
|
|
allowfullscreen
|
|
referrerpolicy="no-referrer-when-downgrade"></iframe>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<noscript>
|
|
<div class="status">JavaScript is disabled. Direct files and supported provider embeds can still load; HLS and DASH may need JavaScript.</div>
|
|
</noscript>
|
|
|
|
<div class="status is-hidden" id="status"></div>
|
|
|
|
<div class="error is-hidden" id="error-panel">
|
|
<div class="panel">
|
|
<h1 id="error-title">Could not play this URL</h1>
|
|
<p id="error-message"></p>
|
|
<a id="error-link" href="#" target="_blank" rel="noopener">Open source URL</a>
|
|
</div>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="empty">
|
|
<div class="panel">
|
|
<h1>No media URL supplied</h1>
|
|
<p>Use <code>?video=https%3A%2F%2Fexample.com%2Fclip.mp4</code> or <code>?url=...</code>.</p>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</main>
|
|
|
|
<?php if (count($sources) > 0): ?>
|
|
<script>
|
|
window.addEventListener('DOMContentLoaded', function () {
|
|
'use strict';
|
|
|
|
const config = <?= $json_config ?>;
|
|
const sources = Array.isArray(config.sources) ? config.sources : [];
|
|
const mediaMount = document.getElementById('media-mount');
|
|
const frameMount = document.getElementById('frame-mount');
|
|
const statusBox = document.getElementById('status');
|
|
const errorPanel = document.getElementById('error-panel');
|
|
const errorMessage = document.getElementById('error-message');
|
|
const errorLink = document.getElementById('error-link');
|
|
const topOverlay = document.getElementById('top-overlay');
|
|
|
|
let hlsEngine = null;
|
|
let dashEngine = null;
|
|
let currentIndex = -1;
|
|
let attemptedGenericFrame = false;
|
|
const failures = new Map();
|
|
|
|
function setStatus(message) {
|
|
if (!statusBox) return;
|
|
if (!message) {
|
|
statusBox.textContent = '';
|
|
statusBox.classList.add('is-hidden');
|
|
return;
|
|
}
|
|
statusBox.textContent = message;
|
|
statusBox.classList.remove('is-hidden');
|
|
}
|
|
|
|
function hideError() {
|
|
if (errorPanel) errorPanel.classList.add('is-hidden');
|
|
}
|
|
|
|
function destroyEngines() {
|
|
if (hlsEngine) {
|
|
try { hlsEngine.destroy(); } catch (e) {}
|
|
hlsEngine = null;
|
|
}
|
|
if (dashEngine) {
|
|
try { dashEngine.reset(); } catch (e) {}
|
|
dashEngine = null;
|
|
}
|
|
}
|
|
|
|
function clearMounts() {
|
|
destroyEngines();
|
|
if (mediaMount) mediaMount.innerHTML = '';
|
|
if (frameMount) frameMount.innerHTML = '';
|
|
if (mediaMount) mediaMount.classList.remove('is-hidden');
|
|
if (frameMount) frameMount.classList.add('is-hidden');
|
|
}
|
|
|
|
function labelFor(source) {
|
|
return (source && (source.provider || source.label || source.kind)) || 'URL';
|
|
}
|
|
|
|
function mediaErrorMessage(error) {
|
|
if (!error) return 'The browser rejected the media source.';
|
|
const map = {
|
|
1: 'Playback was aborted.',
|
|
2: 'A network error stopped the download.',
|
|
3: 'The media is damaged or uses an unsupported codec.',
|
|
4: 'No compatible media source was found.'
|
|
};
|
|
return map[error.code] || 'The browser rejected the media source.';
|
|
}
|
|
|
|
function showFailure(reason) {
|
|
const first = sources[0] || {};
|
|
clearMounts();
|
|
setStatus('');
|
|
if (errorMessage) {
|
|
errorMessage.textContent = (reason || 'No compatible media source was found.') +
|
|
' Browser playback still depends on supported codecs, server headers, CORS for HLS/DASH, and whether a provider allows embedding.';
|
|
}
|
|
if (errorLink && first.originalUrl) {
|
|
errorLink.href = first.originalUrl;
|
|
errorLink.classList.remove('is-hidden');
|
|
} else if (errorLink) {
|
|
errorLink.classList.add('is-hidden');
|
|
}
|
|
if (errorPanel) errorPanel.classList.remove('is-hidden');
|
|
}
|
|
|
|
function nextSourceIndex(afterIndex) {
|
|
for (let step = 1; step <= sources.length; step += 1) {
|
|
const index = (afterIndex + step) % sources.length;
|
|
if (!failures.has(index)) return index;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function failSource(index, reason) {
|
|
failures.set(index, reason || 'This source failed.');
|
|
|
|
const next = nextSourceIndex(index);
|
|
if (next >= 0 && next !== index) {
|
|
setStatus('Trying fallback source: ' + labelFor(sources[next]) + '.');
|
|
mountSource(next);
|
|
return;
|
|
}
|
|
|
|
const first = sources[0];
|
|
if (config.iframeFallback && first && first.originalUrl && !attemptedGenericFrame) {
|
|
attemptedGenericFrame = true;
|
|
setStatus('Trying the URL as an embedded webpage. If it stays blank, the site blocks iframes.');
|
|
mountIframe(first.originalUrl, first, true);
|
|
return;
|
|
}
|
|
|
|
showFailure(reason);
|
|
}
|
|
|
|
function makeMediaElement(kind) {
|
|
const media = document.createElement(kind === 'audio' ? 'audio' : 'video');
|
|
media.className = kind === 'audio' ? 'native-media native-audio' : 'native-media native-video';
|
|
media.controls = config.controls !== false;
|
|
media.autoplay = config.autoplay === true;
|
|
media.muted = config.muted === true;
|
|
media.loop = config.loop === true;
|
|
media.preload = 'metadata';
|
|
|
|
if (config.crossorigin) {
|
|
media.crossOrigin = config.crossorigin;
|
|
}
|
|
|
|
if (kind !== 'audio') {
|
|
media.playsInline = true;
|
|
media.setAttribute('playsinline', '');
|
|
if (config.poster) media.poster = config.poster;
|
|
}
|
|
|
|
return media;
|
|
}
|
|
|
|
function attachCommonMediaEvents(media, index) {
|
|
media.addEventListener('loadedmetadata', function () {
|
|
setStatus('');
|
|
}, { once: true });
|
|
|
|
media.addEventListener('canplay', function () {
|
|
setStatus('');
|
|
}, { once: true });
|
|
|
|
media.addEventListener('play', function () {
|
|
if (topOverlay) topOverlay.classList.add('is-hidden');
|
|
});
|
|
|
|
media.addEventListener('pause', function () {
|
|
if (topOverlay) topOverlay.classList.remove('is-hidden');
|
|
});
|
|
|
|
media.addEventListener('ended', function () {
|
|
if (topOverlay) topOverlay.classList.remove('is-hidden');
|
|
});
|
|
|
|
media.addEventListener('error', function () {
|
|
failSource(index, mediaErrorMessage(media.error));
|
|
}, { once: true });
|
|
}
|
|
|
|
function appendAudioLayout(media) {
|
|
const layout = document.createElement('div');
|
|
layout.className = 'audio-layout';
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'audio-card';
|
|
|
|
const cover = document.createElement('div');
|
|
cover.className = 'cover';
|
|
if (config.poster) {
|
|
cover.style.backgroundImage = 'url("' + String(config.poster).replace(/"/g, '%22') + '")';
|
|
}
|
|
|
|
card.appendChild(cover);
|
|
card.appendChild(media);
|
|
layout.appendChild(card);
|
|
mediaMount.appendChild(layout);
|
|
}
|
|
|
|
function loadAndMaybePlay(media) {
|
|
try { media.load(); } catch (e) {}
|
|
if (config.autoplay) {
|
|
const playPromise = media.play();
|
|
if (playPromise && typeof playPromise.catch === 'function') {
|
|
playPromise.catch(function () {});
|
|
}
|
|
}
|
|
}
|
|
|
|
function mountDirect(source, index, kind) {
|
|
clearMounts();
|
|
hideError();
|
|
|
|
const media = makeMediaElement(kind);
|
|
attachCommonMediaEvents(media, index);
|
|
|
|
if (kind === 'audio') {
|
|
appendAudioLayout(media);
|
|
} else {
|
|
mediaMount.appendChild(media);
|
|
}
|
|
|
|
media.src = source.url;
|
|
setStatus('Loading ' + labelFor(source) + '.');
|
|
loadAndMaybePlay(media);
|
|
}
|
|
|
|
function mountHls(source, index) {
|
|
clearMounts();
|
|
hideError();
|
|
|
|
const media = makeMediaElement('video');
|
|
attachCommonMediaEvents(media, index);
|
|
mediaMount.appendChild(media);
|
|
setStatus('Loading HLS stream.');
|
|
|
|
if (media.canPlayType('application/vnd.apple.mpegurl') || media.canPlayType('application/x-mpegURL')) {
|
|
media.src = source.url;
|
|
loadAndMaybePlay(media);
|
|
return;
|
|
}
|
|
|
|
if (!window.Hls || !window.Hls.isSupported()) {
|
|
failSource(index, 'HLS is not supported by this browser and hls.js is unavailable.');
|
|
return;
|
|
}
|
|
|
|
let recoveries = 0;
|
|
hlsEngine = new window.Hls({
|
|
enableWorker: true,
|
|
lowLatencyMode: true,
|
|
xhrSetup: function (xhr) {
|
|
if (config.withCredentials) xhr.withCredentials = true;
|
|
}
|
|
});
|
|
|
|
hlsEngine.on(window.Hls.Events.MANIFEST_PARSED, function () {
|
|
setStatus('');
|
|
if (config.autoplay) {
|
|
const playPromise = media.play();
|
|
if (playPromise && typeof playPromise.catch === 'function') {
|
|
playPromise.catch(function () {});
|
|
}
|
|
}
|
|
});
|
|
|
|
hlsEngine.on(window.Hls.Events.ERROR, function (event, data) {
|
|
if (!data || !data.fatal) return;
|
|
if (recoveries < 2 && data.type === window.Hls.ErrorTypes.NETWORK_ERROR) {
|
|
recoveries += 1;
|
|
hlsEngine.startLoad();
|
|
return;
|
|
}
|
|
if (recoveries < 2 && data.type === window.Hls.ErrorTypes.MEDIA_ERROR) {
|
|
recoveries += 1;
|
|
hlsEngine.recoverMediaError();
|
|
return;
|
|
}
|
|
failSource(index, 'The HLS stream failed. The server may not allow CORS for manifests or segments.');
|
|
});
|
|
|
|
hlsEngine.loadSource(source.url);
|
|
hlsEngine.attachMedia(media);
|
|
}
|
|
|
|
function mountDash(source, index) {
|
|
clearMounts();
|
|
hideError();
|
|
|
|
if (!window.dashjs || !window.dashjs.MediaPlayer) {
|
|
failSource(index, 'DASH playback is unavailable because dash.js did not load.');
|
|
return;
|
|
}
|
|
|
|
const media = makeMediaElement('video');
|
|
attachCommonMediaEvents(media, index);
|
|
mediaMount.appendChild(media);
|
|
setStatus('Loading DASH stream.');
|
|
|
|
dashEngine = window.dashjs.MediaPlayer().create();
|
|
if (dashEngine.updateSettings) {
|
|
dashEngine.updateSettings({
|
|
debug: { logLevel: 0 },
|
|
streaming: {
|
|
retryAttempts: { MPD: 2, MediaSegment: 2 },
|
|
retryIntervals: { MPD: 1000, MediaSegment: 1000 }
|
|
}
|
|
});
|
|
}
|
|
if (dashEngine.on && window.dashjs.MediaPlayer.events) {
|
|
dashEngine.on(window.dashjs.MediaPlayer.events.ERROR, function () {
|
|
failSource(index, 'The DASH stream failed. The server may not allow CORS or the manifest may be unsupported.');
|
|
});
|
|
}
|
|
dashEngine.initialize(media, source.url, config.autoplay === true);
|
|
}
|
|
|
|
function mountIframe(url, source, genericFallback) {
|
|
clearMounts();
|
|
hideError();
|
|
|
|
if (mediaMount) mediaMount.classList.add('is-hidden');
|
|
if (frameMount) frameMount.classList.remove('is-hidden');
|
|
|
|
const iframe = document.createElement('iframe');
|
|
iframe.className = 'embed-frame';
|
|
iframe.src = url;
|
|
iframe.title = config.title || labelFor(source) || 'Embedded media';
|
|
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share';
|
|
iframe.allowFullscreen = true;
|
|
iframe.referrerPolicy = 'no-referrer-when-downgrade';
|
|
frameMount.appendChild(iframe);
|
|
|
|
if (genericFallback) {
|
|
setStatus('Showing the URL as a webpage. If it is blank, that site blocks embedding.');
|
|
} else {
|
|
setStatus('');
|
|
}
|
|
}
|
|
|
|
function mountSource(index) {
|
|
currentIndex = index;
|
|
const source = sources[index];
|
|
if (!source) {
|
|
showFailure('No source was available.');
|
|
return;
|
|
}
|
|
|
|
const kind = source.kind || 'unknown';
|
|
if (kind === 'iframe') {
|
|
mountIframe(source.embed || source.url, source, false);
|
|
} else if (kind === 'hls') {
|
|
mountHls(source, index);
|
|
} else if (kind === 'dash') {
|
|
mountDash(source, index);
|
|
} else if (kind === 'audio') {
|
|
mountDirect(source, index, 'audio');
|
|
} else {
|
|
mountDirect(source, index, 'video');
|
|
}
|
|
}
|
|
|
|
if (sources.length === 0) {
|
|
showFailure('No media URL was supplied.');
|
|
return;
|
|
}
|
|
|
|
mountSource(0);
|
|
|
|
window.embedPlayer = {
|
|
playSource: function (index) {
|
|
const next = Number(index);
|
|
if (Number.isInteger(next) && next >= 0 && next < sources.length) {
|
|
failures.delete(next);
|
|
mountSource(next);
|
|
}
|
|
},
|
|
retry: function () {
|
|
failures.clear();
|
|
attemptedGenericFrame = false;
|
|
mountSource(currentIndex >= 0 ? currentIndex : 0);
|
|
},
|
|
sources: sources
|
|
};
|
|
});
|
|
</script>
|
|
<?php endif; ?>
|
|
</body>
|
|
</html>
|