refactor player.js

This commit is contained in:
Anirudh Sevugan 2025-08-17 17:41:39 -05:00 committed by GitHub
parent 34431af1dc
commit be137f829d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,411 +1,264 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const dialogOverlay = document.getElementById('dialogOverlay'); // --- 1. Element Grouping & State Management ---
const chooseFileBtn = document.getElementById('chooseFileBtn'); const elements = {
const enterUrlBtn = document.getElementById('enterUrlBtn'); media: {
const fileInput = document.getElementById('fileInput'); player: document.getElementById('mediaPlayer'),
const mediaPlayer = document.getElementById('mediaPlayer'); customControls: document.getElementById('customControls'),
const playPauseBtn = document.getElementById('playPauseBtn'); playPauseBtn: document.getElementById('playPauseBtn'),
const seekBar = document.getElementById('seekBar'); seekBar: document.getElementById('seekBar'),
const timeDisplay = document.getElementById('timeDisplay'); timeDisplay: document.getElementById('timeDisplay'),
const volumeBar = document.getElementById('volumeBar'); },
const settingsBtn = document.getElementById('settingsBtn'); volume: {
const settingsPanel = document.getElementById('settingsPanel'); volumeBtn: document.getElementById('volumeBtn'),
const autoplayCheckbox = document.getElementById('autoplayCheckbox'); volumeBar: document.getElementById('volumeBar'),
const loopCheckbox = document.getElementById('loopCheckbox'); },
const saveSettingsBtn = document.getElementById('saveSettingsBtn'); dialogs: {
const urlDialogOverlay = document.getElementById('urlDialogOverlay'); mainOverlay: document.getElementById('dialogOverlay'),
const settingsDialogOverlay = document.getElementById('settingsDialogOverlay'); urlOverlay: document.getElementById('urlDialogOverlay'),
const urlInput = document.getElementById('urlInput'); settingsOverlay: document.getElementById('settingsDialogOverlay'),
const submitUrlBtn = document.getElementById('submitUrlBtn'); subtitlesOverlay: document.getElementById('subtitlesOverlay'),
const cancelUrlBtn = document.getElementById('cancelUrlBtn'); },
const ccBtn = document.getElementById('ccBtn'); // CC button buttons: {
const volumeBtn = document.getElementById("volumeBtn") chooseFile: document.getElementById('chooseFileBtn'),
const subtitlesOverlay = document.getElementById('subtitlesOverlay'); enterUrl: document.getElementById('enterUrlBtn'),
const subtitlesInput = document.getElementById('subtitlesInput'); submitUrl: document.getElementById('submitUrlBtn'),
const submitSubtitlesBtn = document.getElementById('submitSubtitlesBtn'); cancelUrl: document.getElementById('cancelUrlBtn'),
const cancelSubtitlesBtn = document.getElementById('cancelSubtitlesBtn'); settings: document.getElementById('settingsBtn'),
const customControls = document.getElementById('customControls'); saveSettings: document.getElementById('saveSettingsBtn'),
let hls = null cc: document.getElementById('ccBtn'),
let player = null submitSubtitles: document.getElementById('submitSubtitlesBtn'),
window.hls = hls; cancelSubtitles: document.getElementById('cancelSubtitlesBtn'),
window.dash = player; 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'),
},
};
async function detectStreamType(url) { const state = {
try { hls: null,
const response = await fetch(url, { method: 'HEAD' }); dashPlayer: null,
const contentType = response.headers.get('Content-Type') || ''; previousObjectURL: null,
};
const isHLS = url.toLowerCase().endsWith('.m3u8') || // --- 2. Utility Functions ---
contentType.includes('application/vnd.apple.mpegurl') || const formatTime = (time) => {
contentType.includes('application/x-mpegURL'); const minutes = Math.floor(time / 60) || 0;
const seconds = Math.floor(time % 60) || 0;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const isDASH = url.toLowerCase().endsWith('.mpd') || const setVisibility = (element, isVisible) => {
contentType.includes('application/dash+xml'); element.style.display = isVisible ? 'block' : 'none';
};
return { isHLS, isDASH, contentType }; const toggleControls = (show) => {
} catch (err) { elements.media.customControls.style.display = show ? 'flex' : 'none';
console.error("Failed to detect stream type:", err); };
return { isHLS: false, isDASH: false, contentType: null };
}
}
// Update media volume when the slider is moved
volumeBar.addEventListener("input", function () {
mediaPlayer.volume = volumeBar.value;
});
// Sync slider with media volume (in case it's changed programmatically)
mediaPlayer.addEventListener("volumechange", function () {
volumeBar.value = mediaPlayer.volume;
if (mediaPlayer.muted || mediaPlayer.volume === 0) {
volumeBtn.textContent = "🔇";
} else {
volumeBtn.textContent = "🔊";
}
});
// Function to add subtitles dynamically (e.g., after URL input)
function addSubtitles(url) {
// Remove any existing subtitle tracks
const existingTracks = mediaPlayer.getElementsByTagName('track');
for (let track of existingTracks) {
track.remove();
}
// Create a new track for subtitles
const track = document.createElement('track');
track.kind = 'subtitles';
track.label = 'English';
track.srclang = 'en';
track.src = url;
// Append the new track
mediaPlayer.appendChild(track);
// Optionally, enable subtitles by default
track.track.mode = 'showing'; // Enable subtitles by default
}
// Handle submit subtitle URL
function clearSubtitles() {
const tracks = mediaPlayer.getElementsByTagName('track');
for (let i = tracks.length - 1; i >= 0; i--) {
tracks[i].remove();
}
}
// Use this function when a new video is loaded
submitSubtitlesBtn.addEventListener('click', () => {
const subtitleUrl = subtitlesInput.value;
if (subtitleUrl) {
addSubtitles(subtitleUrl);
subtitlesOverlay.style.display = 'none';
subtitlesInput.value = '';
}
});
let autoplayEnabled = true;
let loopEnabled = false;
// Handle submit URL button in custom dialog
submitUrlBtn.addEventListener('click', async () => {
let url = urlInput.value;
// Check if URL is a valid URL and doesn't contain "http" or "https"
if (url && !url.startsWith('http') && !url.startsWith('https')) {
// Assuming it's a URL and needs the protocol added
url = 'http://' + url; // You can also choose 'https://' if preferred
}
const { isHLS, isDASH } = await detectStreamType(url);
const resetMediaPlayers = () => {
if (state.hls) {
state.hls.destroy();
state.hls = null;
}
if (state.dashPlayer) {
state.dashPlayer.reset();
state.dashPlayer = null;
}
};
if (url) { const updatePlayPauseButton = () => {
clearSubtitles(); elements.media.playPauseBtn.textContent = elements.media.player.paused ? 'Play' : 'Pause';
if (hls !== null) { };
hls.destroy()
hls = null const updateVolumeButton = () => {
window.hls = hls 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 };
} }
if (player !== null) { };
player.reset()
player = null const playMedia = (source) => {
window.dash = player elements.media.player.src = source;
elements.media.player.load();
if (elements.inputs.autoplayCheckbox.checked) {
elements.media.player.play();
} }
if (url.toLowerCase().endsWith('.m3u8') || url.toLowerCase().endsWith('.m3u') || isHLS) { toggleControls(true);
// HLS stream setVisibility(elements.dialogs.mainOverlay, false);
if (Hls.isSupported()) { setVisibility(elements.dialogs.urlOverlay, false);
mediaPlayer.style.display = 'flex'; // Hide the native video player };
hls = new Hls();
window.hls = hls // --- 3. Event Handlers ---
mediaPlayer.pause(); const handleUrlSubmit = async () => {
hls.loadSource(url); let url = elements.inputs.urlInput.value;
hls.attachMedia(mediaPlayer); if (url && !url.startsWith('http')) {
hls.on(Hls.Events.MANIFEST_PARSED, function() { url = 'https://' + url;
if (autoplayCheckbox.checked) { }
mediaPlayer.play();
} if (!url) return;
urlInput.value = "";
customControls.style.display = 'flex'; resetMediaPlayers();
}); addSubtitles();
window.hls = hls
} else { const { isHLS, isDASH } = await detectStreamType(url);
alert("Your device doesn't support HLS."); elements.media.player.style.display = 'flex';
customControls.style.display = 'flex';
urlInput.value = ""; 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 if (url.toLowerCase().endsWith('.mpd') || isDASH) {
mediaPlayer.style.display = 'flex'; // Hide the native video player
mediaPlayer.pause();
player = dashjs.MediaPlayer().create();
window.dash = player
// MPEG-DASH stream
player.initialize(mediaPlayer, url, true);
customControls.style.display = 'flex';
urlInput.value = "";
if (autoplayCheckbox.checked) {
mediaPlayer.play();
}
window.dash = player
} else { } else {
mediaPlayer.style.display = 'flex'; // Hide the native video player playMedia(url);
mediaPlayer.pause();
mediaPlayer.src = url;
customControls.style.display = 'flex';
urlInput.value = "";
if (autoplayCheckbox.checked) {
mediaPlayer.play();
}
} }
urlDialogOverlay.style.display = 'none'; elements.inputs.urlInput.value = '';
dialogOverlay.style.display = 'none'; toggleControls(true);
} setVisibility(elements.dialogs.urlOverlay, false);
}); setVisibility(elements.dialogs.mainOverlay, false);
};
const handleFileChange = (event) => {
// Handle CC button to show subtitle modal resetMediaPlayers();
ccBtn.addEventListener('click', () => { addSubtitles();
subtitlesOverlay.style.display = 'block'; if (state.previousObjectURL) {
}); URL.revokeObjectURL(state.previousObjectURL);
// Handle cancel subtitle modal
cancelSubtitlesBtn.addEventListener('click', () => {
subtitlesOverlay.style.display = 'none';
});
// Show the dialog on page load
window.onload = function () {
dialogOverlay.style.display = 'block';
};
// Handle "Choose a File" button
chooseFileBtn.addEventListener('click', () => {
fileInput.click();
});
mediaPlayer.addEventListener("volumechange", function () {
if (mediaPlayer.muted) {
volumeBtn.textContent = "🔇"
} else if (mediaPlayer.volume === 0) {
volumeBtn.textContent = "🔊"
}
});
let previousObjectURL = null; // Store the last Object URL
fileInput.addEventListener('change', (event) => {
if (hls !== null) {
hls.destroy()
hls = null
window.hls = hls
}
if (player !== null) {
player.reset()
player = null
window.dash = player
}
const file = event.target.files[0];
if (!file) return;
clearSubtitles(); // Remove any previously loaded subtitles
// Revoke the previous Object URL if it exists
if (previousObjectURL) {
URL.revokeObjectURL(previousObjectURL);
}
// Create a new Object URL for the selected file
const fileURL = URL.createObjectURL(file);
mediaPlayer.src = fileURL;
mediaPlayer.load();
if (autoplayCheckbox.checked) {
mediaPlayer.play();
}
// Store the new Object URL for future cleanup
previousObjectURL = fileURL;
// Hide dialog after selecting a file
dialogOverlay.style.display = 'none';
});
// Handle "Enter a URL" button
enterUrlBtn.addEventListener('click', () => {
urlDialogOverlay.style.display = 'block';
});
// Handle cancel button in URL dialog
cancelUrlBtn.addEventListener('click', () => {
urlDialogOverlay.style.display = 'none';
});
// Handle custom play/pause button
playPauseBtn.addEventListener('click', () => {
if (mediaPlayer.paused) {
mediaPlayer.play();
playPauseBtn.textContent = 'Pause';
} else {
mediaPlayer.pause();
playPauseBtn.textContent = 'Play';
}
});
// Sync button with the video player when it is paused manually
mediaPlayer.addEventListener('pause', () => {
playPauseBtn.textContent = 'Play';
});
// Sync button with the video player when it is played
mediaPlayer.addEventListener('play', () => {
playPauseBtn.textContent = 'Pause';
});
// Volume button toggling mute/unmute
volumeBtn.addEventListener('click', () => {
if (mediaPlayer.muted || mediaPlayer.volume == 0) {
mediaPlayer.muted = false;
volumeBtn.textContent = '🔊'; // Unmute icon
} else {
mediaPlayer.muted = true;
volumeBtn.textContent = '🔇'; // Mute icon
}
});
// Handle URL input on Enter key
urlInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
submitUrlBtn.click();
}
});
// Handle Subtitles input on Enter key
subtitlesInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
submitSubtitlesBtn.click();
}
});
// Handle URL submission
// Update seek bar and time display
mediaPlayer.addEventListener('timeupdate', () => {
seekBar.max = mediaPlayer.duration || 0;
seekBar.value = mediaPlayer.currentTime;
const current = formatTime(mediaPlayer.currentTime);
const total = formatTime(mediaPlayer.duration);
timeDisplay.textContent = `${current} / ${total}`;
});
// Seek media
seekBar.addEventListener('input', () => {
mediaPlayer.currentTime = seekBar.value;
});
// Handle volume
volumeBar.addEventListener('input', () => {
mediaPlayer.volume = volumeBar.value;
if (volumeBar.value == 0 || mediaPlayer.volume == 0) {
volumeBtn.textContent = "🔇";
} else {
volumeBtn.textContent = "🔊";
}
});
// Show settings panel
settingsBtn.addEventListener('click', () => {
settingsDialogOverlay.style.display = 'block';
settingsPanel.style.display = 'block';
});
saveSettingsBtn.addEventListener('click', () => {
autoplayEnabled = autoplayCheckbox.checked;
loopEnabled = loopCheckbox.checked;
mediaPlayer.autoplay = autoplayEnabled;
mediaPlayer.loop = loopEnabled;
const controlsEnabled = document.getElementById('controlsCheckbox').checked;
const colorsEnabled = document.getElementById('colorsCheckbox').checked;
mediaPlayer.controls = controlsEnabled;
if (colorsEnabled) {
mediaPlayer.style.filter = "contrast(1.1) saturate(1.15) brightness(1.03)";
} else {
mediaPlayer.style.filter = "";
}
settingsPanel.style.display = 'none';
settingsDialogOverlay.style.display = 'none';
});
// End of first event listener for DOM content loaded
// Format time
function formatTime(time) {
const minutes = Math.floor(time / 60) || 0;
const seconds = Math.floor(time % 60) || 0;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
} }
const showDialogBtn = document.getElementById('showDialogBtn'); const file = event.target.files[0];
const hideDialogBtn = document.getElementById('hideDialogBtn'); if (!file) return;
const fullscreenBtn = document.getElementById('fullscreenBtn');
// Show dialog const fileURL = URL.createObjectURL(file);
showDialogBtn.addEventListener('click', () => { playMedia(fileURL);
dialogOverlay.style.display = 'block'; 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;
}); });
// Hide dialog elements.volume.volumeBar.addEventListener('input', () => {
hideDialogBtn.addEventListener('click', () => { elements.media.player.volume = elements.volume.volumeBar.value;
dialogOverlay.style.display = 'none'; });
elements.volume.volumeBtn.addEventListener('click', () => {
elements.media.player.muted = !elements.media.player.muted;
updateVolumeButton();
}); });
// Fullscreen functionality // Dialogs & Buttons
fullscreenBtn.addEventListener('click', () => { 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) { if (!document.fullscreenElement) {
mediaPlayer.requestFullscreen(); elements.media.player.requestFullscreen();
} else { } else {
document.exitFullscreen(); document.exitFullscreen();
} }
}); });
// Initialize
window.onload = () => setVisibility(elements.dialogs.mainOverlay, true);
};
// End of code and second event listener for DOM content loaded setupEventListeners();
}); });