// ==UserScript==
// @name Ephyra Player → Native Video
// @namespace https://github.com/local/userscripts
// @version 1.0.0
// @description Replaces Ephyra custom video players with plain <video> elements using hls.js.
// @author you
// @match *://kiwifarms.st/*
// @run-at document-body
// @grant none
// @require https://cdn.jsdelivr.net/npm/hls.js@1.6.16/dist/hls.min.js
// ==/UserScript==
console.assert(Hls.isSupported());
const playM3u8 = (video, src) => {
console.log('playing', src, 'on', video);
if (video.hls) {
console.log('already has wrapper');
video.hls.loadSource(src);
return;
}
const hls = new Hls({enableWorker:false, debug: false});
hls.on(Hls.Events.ERROR, (event, data) => {
if (!data.fatal) {
console.warn('unhandled error encountered', event, data);
return;
}
switch (data.type) {
case Hls.ErrorTypes.MEDIA_ERROR:
console.warn('fatal media error encountered, try to recover');
hls.recoverMediaError();
break;
case Hls.ErrorTypes.NETWORK_ERROR:
console.error('fatal network error encountered', data);
// All retries and media options have been exhausted.
// Immediately trying to restart loading could cause loop loading.
// Consider modifying loading policies to best fit your asset and network
// conditions (manifestLoadPolicy, playlistLoadPolicy, fragLoadPolicy).
break;
default:
console.error('unrecoverable error encountered', event, data);
// cannot recover
if (video?.hls === hls) delete video.hls;
hls.destroy();
break;
}
});
video.hls = hls;
hls.loadSource(src);
hls.attachMedia(video);
};
(function () {
'use strict';
/**
* Replace a single .ephyra-player element with a native <video>.
* @param {HTMLElement} player
*/
const replacePlayer = player => {
const manifest = player.dataset.manifest;
if (!manifest) return;
const poster = player.dataset.poster || '';
const width = player.dataset.width || '560';
const height = player.dataset.height || '315';
const video = document.createElement('video');
video.controls = true;
if (poster) video.poster = poster;
// Preserve the original player's layout constraints
video.style.cssText = player.style.cssText;
// Make sure the element itself fills available space
video.style.display = 'block';
video.style.maxWidth = '100%';
video.style.background = '#000';
player.replaceWith(video);
playM3u8(video, manifest);
console.debug('[ephyra→video] replaced', player.id || player, '→', manifest);
}
const selector = '.ephyra-player[data-manifest]';
/** Replace every Ephyra player currently in the DOM. */
const replaceAll = () => {
const players = document.querySelectorAll(selector);
players.forEach(replacePlayer);
return players.length > 0;
}
// Run immediately on existing players
if (replaceAll()) return;
console.log('player not found, installing obeserver');
// Also watch for players injected after page load (infinite scroll, XHR, etc.)
const observer = new MutationObserver(mutations => {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
if (!(node instanceof Element)) continue;
// The node itself might be the player
if (node.matches(selector)) {
replacePlayer(node);
continue;
}
// Or it might contain players
node.querySelectorAll?.(selector).forEach(replacePlayer);
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();