kemono2/client/fluid-player/src/modules/streaming.js
2025-04-11 00:58:59 +02:00

300 lines
12 KiB
JavaScript

// Prevent DASH.js from automatically attaching to video sources by default.
// Whoever thought this is a good idea?!
if (typeof window !== 'undefined' && !window.dashjs) {
window.dashjs = {
skipAutoCreate: true,
isDefaultSubject: true
};
}
export default function (playerInstance, options) {
playerInstance.initialiseStreamers = () => {
playerInstance.detachStreamers();
switch (playerInstance.displayOptions.layoutControls.mediaType) {
case 'application/dash+xml': // MPEG-DASH
if (!playerInstance.dashScriptLoaded && (!window.dashjs || window.dashjs.isDefaultSubject)) {
playerInstance.dashScriptLoaded = true;
import(/* webpackChunkName: "dashjs" */ 'dashjs').then((it) => {
window.dashjs = it.default;
playerInstance.initialiseDash();
});
} else {
playerInstance.initialiseDash();
}
break;
case 'application/x-mpegurl': // HLS
const { displayOptions, domRef } = playerInstance;
const { player } = domRef;
const { hls } = displayOptions;
// Doesn't load hls.js if player can play it natively
if (player.canPlayType('application/x-mpegurl') && !hls.overrideNative) {
playerInstance.debugMessage('Native HLS support found, skipping hls.js');
break;
}
if (!playerInstance.hlsScriptLoaded && !window.Hls) {
playerInstance.hlsScriptLoaded = true;
import(/* webpackChunkName: "hlsjs" */ 'hls.js').then((it) => {
window.Hls = it.default;
playerInstance.initialiseHls();
});
} else {
playerInstance.initialiseHls();
}
break;
}
};
playerInstance.initialiseDash = () => {
if (typeof (window.MediaSource || window.WebKitMediaSource) === 'function') {
// If false we want to override the autoPlay, as it comes from postRoll
const playVideo = !playerInstance.autoplayAfterAd
? playerInstance.autoplayAfterAd
: playerInstance.displayOptions.layoutControls.autoPlay;
const defaultOptions = {
'debug': {
'logLevel': typeof FP_DEBUG !== 'undefined' && FP_DEBUG === true
? dashjs.Debug.LOG_LEVEL_DEBUG
: dashjs.Debug.LOG_LEVEL_FATAL
}
};
const dashPlayer = dashjs.MediaPlayer().create();
const options = playerInstance.displayOptions.modules.configureDash(defaultOptions);
dashPlayer.updateSettings(options);
playerInstance.displayOptions.modules.onBeforeInitDash(dashPlayer);
dashPlayer.initialize(playerInstance.domRef.player, playerInstance.originalSrc, playVideo);
dashPlayer.on('streamInitializing', () => {
playerInstance.toggleLoader(true);
});
dashPlayer.on('canPlay', () => {
playerInstance.toggleLoader(false);
});
dashPlayer.on('playbackPlaying', () => {
playerInstance.toggleLoader(false);
});
playerInstance.displayOptions.modules.onAfterInitDash(dashPlayer);
playerInstance.dashPlayer = dashPlayer;
} else {
playerInstance.nextSource();
console.log('[FP_WARNING] Media type not supported by this browser using DASH.js. (application/dash+xml)');
}
};
playerInstance.initialiseHls = () => {
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
playerInstance.debugMessage('Initializing hls.js');
const defaultOptions = {
debug: typeof FP_DEBUG !== 'undefined' && FP_DEBUG === true,
startPosition: 0,
p2pConfig: {
logLevel: false,
},
enableWebVTT: false,
enableCEA708Captions: false,
};
const options = playerInstance.displayOptions.modules.configureHls(defaultOptions);
const hls = new Hls(options);
playerInstance.displayOptions.modules.onBeforeInitHls(hls);
hls.attachMedia(playerInstance.domRef.player);
hls.loadSource(playerInstance.originalSrc);
playerInstance.displayOptions.modules.onAfterInitHls(hls);
playerInstance.hlsPlayer = hls;
if (!playerInstance.firstPlayLaunched && playerInstance.displayOptions.layoutControls.autoPlay) {
playerInstance.domRef.player.play();
}
playerInstance.createHLSVideoSourceSwitch();
} else {
playerInstance.nextSource();
console.log('[FP_WARNING] Media type not supported by this browser using HLS.js. (application/x-mpegURL)');
}
};
playerInstance.createHLSVideoSourceSwitch = () => {
playerInstance.hlsPlayer.on(Hls.Events.MANIFEST_PARSED, function () {
try {
const levels = createHLSLevels();
const sortedLevels = sortLevels(levels);
playerInstance.videoSources = sortedLevels;
// <=2 because of the added auto function
if (sortedLevels.length <= 2) return;
const sourceChangeButton = playerInstance.domRef.wrapper.querySelector('.fluid_control_video_source');
toggleSourceChangeButtonVisibility(sortedLevels, sourceChangeButton);
const sourceChangeList = createSourceChangeList(sortedLevels);
attachSourceChangeList(sourceChangeButton, sourceChangeList);
// Set initial level based on persisted quality or default to auto
setInitialLevel(sortedLevels);
} catch (err) {
console.error(err);
}
});
};
function createHLSLevels() {
const HLSLevels = playerInstance.hlsPlayer.levels
.map((level, index) => ({
id: index,
title: String(level.width),
isHD: level.videoRange === 'HDR',
bitrate: level.bitrate
}));
const autoLevel = {
id: -1,
title: 'auto',
isHD: false,
bitrate: 0
};
return [...HLSLevels, autoLevel];
}
function toggleSourceChangeButtonVisibility(levels, sourceChangeButton) {
if (levels.length > 1) {
sourceChangeButton.style.display = 'inline-block';
} else {
sourceChangeButton.style.display = 'none';
}
}
function createSourceChangeList(levels) {
const sourceChangeList = document.createElement('div');
sourceChangeList.className = 'fluid_video_sources_list';
sourceChangeList.style.display = 'none';
levels.forEach(level => {
const sourceChangeDiv = createSourceChangeItem(level);
sourceChangeList.appendChild(sourceChangeDiv);
});
return sourceChangeList;
}
function createSourceChangeItem(level) {
const sourceSelectedClass = getSourceSelectedClass(level);
const hdIndicator = level.isHD ? `<sup style="color:${playerInstance.displayOptions.layoutControls.primaryColor}" class="fp_hd_source"></sup>` : '';
const sourceChangeDiv = document.createElement('div');
sourceChangeDiv.className = `fluid_video_source_list_item js-source_${level.title}`;
sourceChangeDiv.innerHTML = `<span class="source_button_icon ${sourceSelectedClass}"></span>${level.title}${hdIndicator}`;
sourceChangeDiv.addEventListener('click', event => onSourceChangeClick(event, level));
return sourceChangeDiv;
}
function getSourceSelectedClass(level) {
const matchingLevels = playerInstance.videoSources.filter(source => source.title === playerInstance.fluidStorage.fluidQuality);
// If there are multiple matching levels, use the first one
if (matchingLevels.length > 1) {
if (matchingLevels[0].id === level.id) {
return "source_selected";
}
} else if (matchingLevels.length === 1) {
return matchingLevels[0].id === level.id ? "source_selected" : "";
}
// Fallback to auto selection if no persistent level exists
if (!matchingLevels.length && level.title === 'auto') {
return "source_selected";
}
return "";
}
function onSourceChangeClick(event, selectedLevel) {
event.stopPropagation();
setPlayerDimensions();
const videoChangedTo = event.currentTarget;
clearSourceSelectedIcons();
videoChangedTo.firstChild.classList.add('source_selected');
playerInstance.videoSources.forEach(source => {
if (source.title === videoChangedTo.innerText.replace(/(\r\n\t|\n|\r\t)/gm, '')) {
playerInstance.hlsPlayer.currentLevel = selectedLevel.id;
playerInstance.fluidStorage.fluidQuality = selectedLevel.title;
}
});
playerInstance.openCloseVideoSourceSwitch();
}
// While changing source the player size can flash, we want to set the pixel dimensions then back to 100% afterwards
function setPlayerDimensions() {
playerInstance.domRef.player.style.width = `${playerInstance.domRef.player.clientWidth}px`;
playerInstance.domRef.player.style.height = `${playerInstance.domRef.player.clientHeight}px`;
}
function clearSourceSelectedIcons() {
const sourceIcons = playerInstance.domRef.wrapper.getElementsByClassName('source_button_icon');
Array.from(sourceIcons).forEach(icon => icon.classList.remove('source_selected'));
}
function attachSourceChangeList(sourceChangeButton, sourceChangeList) {
sourceChangeButton.appendChild(sourceChangeList);
sourceChangeButton.removeEventListener('click', playerInstance.openCloseVideoSourceSwitch);
sourceChangeButton.addEventListener('click', playerInstance.openCloseVideoSourceSwitch);
}
function setInitialLevel(levels) {
// Check if a persistency level exists and set the current level accordingly
const persistedLevel = levels.find(level => level.title === playerInstance.fluidStorage.fluidQuality);
if (persistedLevel) {
playerInstance.hlsPlayer.currentLevel = persistedLevel.id;
} else {
// Default to 'auto' if no persisted level is found
const autoLevel = levels.find(level => level.title === 'auto');
playerInstance.hlsPlayer.currentLevel = autoLevel.id;
}
}
function sortLevels(levels) {
return [...levels].sort((a, b) => {
// First sort by width in descending order
if (b.width !== a.width) {
return b.width - a.width;
}
// If width is the same, sort by bitrate in descending order
return b.bitrate - a.bitrate;
});
}
playerInstance.detachStreamers = () => {
if (playerInstance.dashPlayer) {
playerInstance.dashPlayer.reset();
playerInstance.dashPlayer = false;
} else if (playerInstance.hlsPlayer) {
playerInstance.hlsPlayer.detachMedia();
playerInstance.hlsPlayer = false;
}
};
}