simpliplay-desktop/simpliplay/player.js
2025-08-17 17:41:39 -05:00

265 lines
9.8 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
// --- 1. Element Grouping & State Management ---
const elements = {
media: {
player: document.getElementById('mediaPlayer'),
customControls: document.getElementById('customControls'),
playPauseBtn: document.getElementById('playPauseBtn'),
seekBar: document.getElementById('seekBar'),
timeDisplay: document.getElementById('timeDisplay'),
},
volume: {
volumeBtn: document.getElementById('volumeBtn'),
volumeBar: document.getElementById('volumeBar'),
},
dialogs: {
mainOverlay: document.getElementById('dialogOverlay'),
urlOverlay: document.getElementById('urlDialogOverlay'),
settingsOverlay: document.getElementById('settingsDialogOverlay'),
subtitlesOverlay: document.getElementById('subtitlesOverlay'),
},
buttons: {
chooseFile: document.getElementById('chooseFileBtn'),
enterUrl: document.getElementById('enterUrlBtn'),
submitUrl: document.getElementById('submitUrlBtn'),
cancelUrl: document.getElementById('cancelUrlBtn'),
settings: document.getElementById('settingsBtn'),
saveSettings: document.getElementById('saveSettingsBtn'),
cc: document.getElementById('ccBtn'),
submitSubtitles: document.getElementById('submitSubtitlesBtn'),
cancelSubtitles: document.getElementById('cancelSubtitlesBtn'),
showDialog: document.getElementById('showDialogBtn'),
hideDialog: document.getElementById('hideDialogBtn'),
fullscreen: document.getElementById('fullscreenBtn'),
},
inputs: {
fileInput: document.getElementById('fileInput'),
urlInput: document.getElementById('urlInput'),
subtitlesInput: document.getElementById('subtitlesInput'),
autoplayCheckbox: document.getElementById('autoplayCheckbox'),
loopCheckbox: document.getElementById('loopCheckbox'),
controlsCheckbox: document.getElementById('controlsCheckbox'),
colorsCheckbox: document.getElementById('colorsCheckbox'),
},
};
const state = {
hls: null,
dashPlayer: null,
previousObjectURL: null,
};
// --- 2. Utility Functions ---
const formatTime = (time) => {
const minutes = Math.floor(time / 60) || 0;
const seconds = Math.floor(time % 60) || 0;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const setVisibility = (element, isVisible) => {
element.style.display = isVisible ? 'block' : 'none';
};
const toggleControls = (show) => {
elements.media.customControls.style.display = show ? 'flex' : 'none';
};
const resetMediaPlayers = () => {
if (state.hls) {
state.hls.destroy();
state.hls = null;
}
if (state.dashPlayer) {
state.dashPlayer.reset();
state.dashPlayer = null;
}
};
const updatePlayPauseButton = () => {
elements.media.playPauseBtn.textContent = elements.media.player.paused ? 'Play' : 'Pause';
};
const updateVolumeButton = () => {
const isMuted = elements.volume.volumeBar.value === 0 || elements.media.player.muted;
elements.volume.volumeBtn.textContent = isMuted ? '🔇' : '🔊';
};
const addSubtitles = (url) => {
// Remove old subtitle tracks
Array.from(elements.media.player.getElementsByTagName('track')).forEach(track => track.remove());
if (!url) return;
const track = document.createElement('track');
track.kind = 'subtitles';
track.label = 'English';
track.srclang = 'en';
track.src = url;
elements.media.player.appendChild(track);
track.track.mode = 'showing';
};
const detectStreamType = async (url) => {
try {
const response = await fetch(url, { method: 'HEAD' });
const contentType = response.headers.get('Content-Type') || '';
const isHLS = url.toLowerCase().endsWith('.m3u8') || contentType.includes('mpegurl');
const isDASH = url.toLowerCase().endsWith('.mpd') || contentType.includes('dash+xml');
return { isHLS, isDASH };
} catch (err) {
console.error("Failed to detect stream type:", err);
return { isHLS: false, isDASH: false };
}
};
const playMedia = (source) => {
elements.media.player.src = source;
elements.media.player.load();
if (elements.inputs.autoplayCheckbox.checked) {
elements.media.player.play();
}
toggleControls(true);
setVisibility(elements.dialogs.mainOverlay, false);
setVisibility(elements.dialogs.urlOverlay, false);
};
// --- 3. Event Handlers ---
const handleUrlSubmit = async () => {
let url = elements.inputs.urlInput.value;
if (url && !url.startsWith('http')) {
url = 'https://' + url;
}
if (!url) return;
resetMediaPlayers();
addSubtitles();
const { isHLS, isDASH } = await detectStreamType(url);
elements.media.player.style.display = 'flex';
if (isHLS && typeof Hls !== 'undefined' && Hls.isSupported()) {
state.hls = new Hls();
state.hls.loadSource(url);
state.hls.attachMedia(elements.media.player);
state.hls.on(Hls.Events.MANIFEST_PARSED, () => {
if (elements.inputs.autoplayCheckbox.checked) {
elements.media.player.play();
}
});
} else if (isDASH && typeof dashjs !== 'undefined') {
state.dashPlayer = dashjs.MediaPlayer().create();
state.dashPlayer.initialize(elements.media.player, url, true);
if (elements.inputs.autoplayCheckbox.checked) {
elements.media.player.play();
}
} else {
playMedia(url);
}
elements.inputs.urlInput.value = '';
toggleControls(true);
setVisibility(elements.dialogs.urlOverlay, false);
setVisibility(elements.dialogs.mainOverlay, false);
};
const handleFileChange = (event) => {
resetMediaPlayers();
addSubtitles();
if (state.previousObjectURL) {
URL.revokeObjectURL(state.previousObjectURL);
}
const file = event.target.files[0];
if (!file) return;
const fileURL = URL.createObjectURL(file);
playMedia(fileURL);
state.previousObjectURL = fileURL;
};
const handleSettingsSave = () => {
elements.media.player.autoplay = elements.inputs.autoplayCheckbox.checked;
elements.media.player.loop = elements.inputs.loopCheckbox.checked;
elements.media.player.controls = elements.inputs.controlsCheckbox.checked;
const filter = elements.inputs.colorsCheckbox.checked
? 'contrast(1.1) saturate(1.15) brightness(1.03)'
: '';
elements.media.player.style.filter = filter;
setVisibility(elements.dialogs.settingsOverlay, false);
};
// --- 4. Event Listener Setup ---
const setupEventListeners = () => {
// Media & Controls
elements.media.player.addEventListener('timeupdate', () => {
elements.media.seekBar.max = elements.media.player.duration || 0;
elements.media.seekBar.value = elements.media.player.currentTime;
elements.media.timeDisplay.textContent = `${formatTime(elements.media.player.currentTime)} / ${formatTime(elements.media.player.duration)}`;
});
elements.media.player.addEventListener('play', updatePlayPauseButton);
elements.media.player.addEventListener('pause', updatePlayPauseButton);
elements.media.player.addEventListener('volumechange', updateVolumeButton);
elements.media.playPauseBtn.addEventListener('click', () => {
elements.media.player.paused ? elements.media.player.play() : elements.media.player.pause();
});
elements.media.seekBar.addEventListener('input', () => {
elements.media.player.currentTime = elements.media.seekBar.value;
});
elements.volume.volumeBar.addEventListener('input', () => {
elements.media.player.volume = elements.volume.volumeBar.value;
});
elements.volume.volumeBtn.addEventListener('click', () => {
elements.media.player.muted = !elements.media.player.muted;
updateVolumeButton();
});
// Dialogs & Buttons
elements.buttons.chooseFile.addEventListener('click', () => elements.inputs.fileInput.click());
elements.buttons.enterUrl.addEventListener('click', () => setVisibility(elements.dialogs.urlOverlay, true));
elements.buttons.cancelUrl.addEventListener('click', () => setVisibility(elements.dialogs.urlOverlay, false));
elements.buttons.submitUrl.addEventListener('click', handleUrlSubmit);
elements.inputs.urlInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') handleUrlSubmit();
});
elements.inputs.fileInput.addEventListener('change', handleFileChange);
elements.buttons.settings.addEventListener('click', () => setVisibility(elements.dialogs.settingsOverlay, true));
elements.buttons.saveSettings.addEventListener('click', handleSettingsSave);
elements.buttons.cc.addEventListener('click', () => setVisibility(elements.dialogs.subtitlesOverlay, true));
elements.buttons.cancelSubtitles.addEventListener('click', () => setVisibility(elements.dialogs.subtitlesOverlay, false));
elements.buttons.submitSubtitles.addEventListener('click', () => {
addSubtitles(elements.inputs.subtitlesInput.value);
elements.inputs.subtitlesInput.value = '';
setVisibility(elements.dialogs.subtitlesOverlay, false);
});
elements.inputs.subtitlesInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') elements.buttons.submitSubtitles.click();
});
elements.buttons.showDialog.addEventListener('click', () => setVisibility(elements.dialogs.mainOverlay, true));
elements.buttons.hideDialog.addEventListener('click', () => setVisibility(elements.dialogs.mainOverlay, false));
elements.buttons.fullscreen.addEventListener('click', () => {
if (!document.fullscreenElement) {
elements.media.player.requestFullscreen();
} else {
document.exitFullscreen();
}
});
// Initialize
window.onload = () => setVisibility(elements.dialogs.mainOverlay, true);
};
setupEventListeners();
});