Greasy Fork is available in English.
An ultra-fast video script allowing you to drag-and-drop local or web-based video files into your browser for seamless, high-definition viewing.
// ==UserScript==
// @name MP4 Player Pro
// @namespace https://i-ali.ir/
// @version 0.1.0
// @license GNU AGPLv3
// @description An ultra-fast video script allowing you to drag-and-drop local or web-based video files into your browser for seamless, high-definition viewing.
// @author AliAshori
// @icon data:image/png;base64,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
// @match file:///*.mp4
// @grant GM_addStyle
// ==/UserScript==
(function() {
'use strict';
// Add custom CSS styles
GM_addStyle(`
.mp4-viewer-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
z-index: 10000;
display: flex;
flex-direction: column;
font-family: Ink Free, sans-serif;
}
/* cover image shown in video container for audio files */
.mp4-viewer-cover {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* keep cover well sized so it doesn't dominate the screen */
max-width: min(56vw, 900px);
max-height: min(50vh, 700px);
width: auto;
height: auto;
object-fit: contain;
border-radius: 12px;
box-shadow: 0 18px 48px rgba(0,0,0,0.65), 0 6px 16px rgba(0,0,0,0.45);
transition: transform 220ms cubic-bezier(.2,.9,.2,1), opacity 180ms ease;
opacity: 0.98;
z-index: 5;
cursor: move;
background: linear-gradient(180deg, rgba(0,0,0,0.08), rgba(0,0,0,0.18));
}
.mp4-viewer-cover:hover {
transform: translate(-50%, -50%) rotate(-2deg) scale(0.995);
filter: drop-shadow(0 26px 60px rgba(0,0,0,0.65));
}
@media (max-width: 991px) {
.mp4-viewer-cover {
max-width: min(76vw, 900px);
max-height: min(70vh, 700px);
}
}
/* thumbnail image in playlist items */
.mp4-viewer-playlist-thumb-img {
width: 100%;
height: 80px;
object-fit: cover;
display: block;
background: #222;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 0;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
transition: transform 160ms ease, box-shadow 160ms ease;
box-shadow: inset 0 -10px 24px rgba(0,0,0,0.25);
}
.mp4-viewer-playlist-item:hover .mp4-viewer-playlist-thumb-img {
transform: translateY(-4px) scale(1.02);
box-shadow: 0 8px 24px rgba(0,0,0,0.45);
}
/* new: search input in playlist header */
.mp4-viewer-search-input {
margin-right: 4px;
padding: 6px 8px;
border-radius: 8px;
border: 1px solid #4a6278;
background: #162026;
color: #ecf0f1;
font-size: 13px;
min-width: 100px;
font-family: Ink Free, sans-serif;
}
/* new: active control button style (used for loop toggle) */
.mp4-viewer-control-btn.active {
background: #2ecc71;
color: #06320f;
}
.mp4-viewer-container {
flex: 1;
display: flex;
flex-direction: column;
background: #1a252f;
}
.mp4-viewer-header {
background: #34495e;
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #4a6278;
}
.mp4-viewer-title {
font-size: 17px;
font-weight: bold;
}
.mp4-viewer-controls {
display: flex;
gap: 10px;
}
.mp4-viewer-btn {
background: #344edb;
border: none;
color: white;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
transition: background 0.3s;
font-family: Ink Free, sans-serif;
}
.mp4-viewer-btn:hover {
background: #2980b9;
}
.mp4-viewer-btn-danger {
background: #a40000;
}
.mp4-viewer-btn-danger:hover {
background: #c0392b;
}
.mp4-viewer-btn-success {
background: #27ae60;
}
.mp4-viewer-btn-success:hover {
background: #219a52;
}
.mp4-viewer-main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.mp4-viewer-video-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: black;
position: relative;
}
.mp4-viewer-video {
background: rgba(44, 62, 80, 0.50);
max-width: 100%;
max-height: 100%;
inset: auto 0px;
min-height: 100%;
}
.mp4-viewer-dropzone {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #ecf0f1;
text-align: center;
padding: 40px;
cursor: pointer;
}
.mp4-viewer-dropzone-icon {
font-size: 64px;
margin-bottom: 20px;
color: #344edb;
}
.mp4-viewer-dropzone.dragover {
background: rgba(52, 152, 219, 0.1);
border: 3px dashed #344edb;
}
.mp4-viewer-bottom-panel {
height: 200px;
background: #2c3e50;
border-top: 1px solid #4a6278;
display: flex;
flex-direction: column;
}
.mp4-viewer-playlist-header {
padding: 12px 20px;
background: #34495e;
color: white;
font-weight: bold;
border-bottom: 1px solid #4a6278;
display: flex;
justify-content: space-between;
align-items: center;
}
.mp4-viewer-playlist-controls {
display: flex;
gap: 8px;
}
.mp4-viewer-playlist-btn {
background: #344edb;
border: none;
color: white;
padding: 6px 6px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
font-family: Ink Free, sans-serif;
}
.mp4-viewer-playlist-items {
flex: 1;
display: flex;
overflow-x: auto;
padding: 10px;
gap: 10px;
align-items: center;
}
.mp4-viewer-playlist-item {
min-width: 120px;
max-width: 160px;
height: 120px;
background: #34495e;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
border: 2px solid transparent;
position: relative;
}
.mp4-viewer-playlist-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.mp4-viewer-playlist-item.active {
border-color: #344edb;
box-shadow: 0 0 15px rgba(52, 152, 219, 0.5);
}
.mp4-viewer-playlist-item-thumbnail {
width: 100%;
height: 80px;
background: #2c3e50;
display: flex;
align-items: center;
justify-content: center;
color: #7f8c8d;
font-size: 24px;
}
.mp4-viewer-playlist-item-info {
padding: 8px;
color: white;
font-size: 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.mp4-viewer-playlist-item-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.mp4-viewer-playlist-item-remove {
background: #a40000;
border: none;
color: white;
width: 20px;
height: 20px;
border-radius: 50%;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.mp4-viewer-hidden {
display: none !important;
}
.mp4-viewer-video-controls {
padding: 12px;
background: #34495e;
display: flex;
gap: 0;
align-items: center;
}
.mp4-viewer-time#currentTime {
text-align: right;
}
.mp4-viewer-time#durationTime {
text-align: left;
}
.mp4-viewer-video-info {
color: white;
flex: 1;
font-size: 14px;
overflow-y: scroll;
overflow-x: hidden;
scrollbar-width: thin;
line-break: anywhere;
height: 20px;
}
.mp4-viewer-progress-container {
flex: 2;
display: flex;
align-items: center;
gap: 8px;
}
.mp4-viewer-progress {
flex: 1;
height: 6px;
background: #7f8c8d;
border-radius: 3px;
overflow: hidden;
cursor: pointer;
}
.mp4-viewer-progress-bar {
height: 100%;
background: #344edb;
width: 0%;
transition: width 0.1s;
}
.mp4-viewer-time {
color: white;
font-size: 12px;
min-width: 46px;
}
.mp4-viewer-control-buttons {
display: flex;
gap: 8px;
}
.mp4-viewer-control-btn {
background: #344edb;
border: none;
color: white;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12.5px;
font-family: Ink Free, sans-serif;
}
.mp4-viewer-add-more {
min-width: 120px;
height: 120px;
background: #34495e;
border: 2px dashed #7f8c8d;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #7f8c8d;
cursor: pointer;
transition: all 0.3s;
}
.mp4-viewer-add-more:hover {
border-color: #344edb;
color: #344edb;
background: rgba(52, 152, 219, 0.1);
}
.mp4-viewer-add-icon {
font-size: 24px;
margin-bottom: 8px;
}
.mp4-viewer-playlist-dropzone {
border: 2px dashed #7f8c8d;
background: rgba(52, 152, 219, 0.05);
}
.mp4-viewer-playlist-dropzone.dragover {
border-color: #344edb;
background: rgba(52, 152, 219, 0.1);
}
.mp4-viewer-empty-playlist {
min-width: 200px;
height: 120px;
background: #34495e;
border: 2px dashed #7f8c8d;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #7f8c8d;
cursor: pointer;
transition: all 0.3s;
}
.mp4-viewer-empty-playlist:hover {
border-color: #344edb;
color: #344edb;
background: rgba(52, 152, 219, 0.1);
}
.mp4-viewer-empty-icon {
font-size: 32px;
margin-bottom: 8px;
}
.mp4-viewer-launch-btn {
position: fixed;
top: 24px;
right: 24px;
z-index: 99999;
background: #344edb;
color: #fff;
border: none;
border-radius: 50px;
padding: 12px 22px 12px 18px;
font-size: 16px;
font-weight: bold;
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
transition: background 0.2s;
}
.mp4-viewer-launch-btn:hover {
background: #2980b9;
}
.mp4-viewer-launch-close {
margin-left: 8px;
font-size: 18px;
color: #fff;
background: transparent;
border: none;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.mp4-viewer-launch-close:hover {
opacity: 1;
}
.mp4-viewer-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 10001;
display: flex;
align-items: center;
justify-content: center;
}
.mp4-viewer-modal-content {
background: #2c3e50;
padding: 20px;
border-radius: 8px;
width: 400px;
max-width: 90%;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.mp4-viewer-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.mp4-viewer-modal-title {
color: white;
font-size: 16px;
font-weight: bold;
}
.mp4-viewer-modal-close {
background: transparent;
border: none;
color: white;
font-size: 18px;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.mp4-viewer-modal-close:hover {
opacity: 1;
}
.mp4-viewer-modal-body {
margin-bottom: 15px;
}
.mp4-viewer-modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.mp4-viewer-url-input {
width: 100%;
padding: 10px;
border: 1px solid #7f8c8d;
border-radius: 4px;
background: #34495e;
color: white;
font-size: 14px;
}
.mp4-viewer-url-input:focus {
outline: none;
border-color: #344edb;
}
/* Sleep timer UI */
.mp4-viewer-timer-input {
width: 58px;
padding: 6px 4px;
border-radius: 6px;
border: 1px solid #4a6278;
background: #162026;
color: #ecf0f1;
font-size: 13px;
font-family: Ink Free, sans-serif;
text-align: center;
}
.mp4-viewer-timer-display {
color: #e1e7ea;
background: rgba(0,0,0,0.15);
padding: 6px 8px;
border-radius: 6px;
font-size: 13px;
min-width: 35px;
text-align: center;
align-content: center;
}
.mp4-viewer-timer-btn {
background: #f39c12;
border: none;
color: #0e0e0e;
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-family: Ink Free, sans-serif;
}
.mp4-viewer-timer-btn.active {
background: #e67e22;
box-shadow: 0 4px 10px rgba(0,0,0,0.25);
}
.mp4-viewer-playlist-item.dragover-item {
outline: 2px dashed #fff;
}
`);
class MP4ViewerPro {
constructor() {
this.playlist = [];
this.currentVideoIndex = -1;
this.isPlaying = false;
this.loop = false; // new: loop state
this._searchDebounce = null; // new: debounce handle for search
// Sleep timer state
this.sleepTimerId = null;
this.sleepIntervalId = null;
this.sleepEndTime = null;
// Drag state for playlist reordering
this._dragSrcIndex = null;
this.init();
}
init() {
this.createViewer();
this.setupEventListeners();
this.setupMediaSession(); //Use the Media Session API
this.showInitialView();
}
// Media Session handlers bound to this instance
setupMediaSession() {
if (!('mediaSession' in navigator)) return;
const sess = navigator.mediaSession;
try {
sess.setActionHandler('previoustrack', () => this.playPrevious());
sess.setActionHandler('nexttrack', () => this.playNext());
sess.setActionHandler('play', () => { if (this.video.paused) this.video.play(); });
sess.setActionHandler('pause', () => { if (!this.video.paused) this.video.pause(); });
// Seeking handlers
sess.setActionHandler('seekbackward', (details) => {
// details.seekOffset may be provided by UI; otherwise default to 10s
const offset = (details && details.seekOffset) ? details.seekOffset : 10;
try { this.video.currentTime = Math.max(0, this.video.currentTime - offset); } catch(e) {}
});
sess.setActionHandler('seekforward', (details) => {
const offset = (details && details.seekOffset) ? details.seekOffset : 10;
try { this.video.currentTime = Math.min(this.video.duration || Infinity, this.video.currentTime + offset); } catch(e) {}
});
sess.setActionHandler('seekto', (details) => {
// details.seekTime is the absolute time to seek to
if (!details || typeof details.seekTime !== 'number') return;
try {
// prefer fastSeek if available
if (typeof this.video.fastSeek === 'function') {
this.video.fastSeek(details.seekTime);
} else {
this.video.currentTime = details.seekTime;
}
} catch (e) {
try { this.video.currentTime = details.seekTime; } catch (err) {}
}
});
// optional stop handler
sess.setActionHandler('stop', () => {
try { this.video.pause(); this.video.currentTime = 0; } catch(e) {}
});
} catch (err) {
console.warn('MediaSession setup failed', err);
}
}
// Update media session metadata (title, artwork)
updateMediaSessionMetadata(item) {
if (!('mediaSession' in navigator)) return;
try {
const title = item && item.name ? item.name : 'MP4 Player Pro';
const artist = item && item.isAudio ? 'Audio' : 'Video';
const album = '';
const artwork = [];
// Use coverUrl (extracted thumbnail or mp3 cover) when available
if (item && item.coverUrl) {
artwork.push({ src: item.coverUrl, sizes: '300x300', type: 'image/jpeg' });
}
// Set MediaMetadata (browser will ignore unsupported fields)
navigator.mediaSession.metadata = new window.MediaMetadata({
title,
artist,
album,
artwork
});
// Update position state if supported
this.updateMediaSessionPositionState();
} catch (err) {
console.warn('updateMediaSessionMetadata failed', err);
}
}
createViewer() {
// Create overlay
this.overlay = document.createElement('div');
this.overlay.className = 'mp4-viewer-overlay';
this.overlay.innerHTML = `
<div class="mp4-viewer-container">
<div class="mp4-viewer-header">
<div class="mp4-viewer-title">🎬 MP4 Player Pro - Drag & Drop Any File to Play</div>
<div class="mp4-viewer-controls">
<button class="mp4-viewer-btn" id="addUrlBtn">🔗 Add URL</button>
<button class="mp4-viewer-btn mp4-viewer-btn-success" id="addMoreBtn">📂 Add Files</button>
<button class="mp4-viewer-btn" id="fullscreenBtn">🖥️ Fullscreen</button>
<button class="mp4-viewer-btn mp4-viewer-btn-danger" id="closeViewerBtn">❌ Close</button>
</div>
</div>
<div class="mp4-viewer-main-content">
<div class="mp4-viewer-video-container">
<video class="mp4-viewer-video mp4-viewer-hidden" controls></video>
<!-- cover image (used for audio .mp3 files) -->
<img class="mp4-viewer-cover mp4-viewer-hidden" alt="cover" />
<div class="mp4-viewer-dropzone">
<div class="mp4-viewer-dropzone-icon">📁</div>
<h2>Drag & Drop MP4 Files Here</h2>
<p>or click to select files</p>
<p><small>You can also drag files directly to the playlist below</small></p>
</div>
</div>
<div class="mp4-viewer-video-controls mp4-viewer-hidden">
<div class="mp4-viewer-video-info" id="videoInfo">No file selected</div>
<div class="mp4-viewer-progress-container">
<span class="mp4-viewer-time" id="currentTime">0:00</span>
<div class="mp4-viewer-progress" id="progressContainer">
<div class="mp4-viewer-progress-bar" id="progressBar"></div>
</div>
<span class="mp4-viewer-time" id="durationTime">0:00</span>
</div>
<div class="mp4-viewer-control-buttons">
<button class="mp4-viewer-control-btn" id="prevBtn" title="Previous">⏮️Prev</button>
<button class="mp4-viewer-control-btn" id="playPauseBtn" title="Play/Pause">▶️Play</button>
<button class="mp4-viewer-control-btn" id="nextBtn" title="Next">⏭️Next</button>
<button class="mp4-viewer-control-btn" id="loopBtn" title="Loop">🔁Loop</button>
<button class="mp4-viewer-control-btn" id="randomBtn" title="Random">🔀Random</button>
<button class="mp4-viewer-control-btn" id="luckyBtn" title="Lucky" style="padding: 6px 6px;">🍀</button>
</div>
</div>
</div>
<div class="mp4-viewer-bottom-panel">
<div class="mp4-viewer-playlist-header">
<span>🎞️Playlist (<span id="playlistCount">0</span> files)</span>
<div class="mp4-viewer-playlist-controls">
<input id="playlistSearch" class="mp4-viewer-search-input" placeholder="Search playlist..." />
<button class="mp4-viewer-playlist-btn" id="importListBtn">📂Import List</button>
<button class="mp4-viewer-playlist-btn" id="clearPlaylistBtn">🧹Clear All</button>
<div style="display: flex;align-content: center;align-items: center;pointer-events: none;">|</div>
<!-- Sleep timer controls -->
<input id="sleepInput" class="mp4-viewer-timer-input" type="number" min="1" placeholder="mins" title="Minutes" />
<button id="sleepBtn" class="mp4-viewer-timer-btn" title="Start sleep timer">⏱️ Sleep</button>
<button id="cancelSleepBtn" class="mp4-viewer-playlist-btn mp4-viewer-hidden" title="Cancel sleep timer">✖</button>
<span id="sleepDisplay" class="mp4-viewer-timer-display mp4-viewer-hidden" aria-live="polite"></span>
</div>
</div>
<div class="mp4-viewer-playlist-items" id="playlistItems">
<!-- Playlist items will be added here dynamically -->
</div>
</div>
</div>
<div class="mp4-viewer-hidden" id="urlModal" style="position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10001;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;">
<div style="background:#222;padding:24px 32px;border-radius:8px;box-shadow:0 2px 16px #000;color:#fff;display:flex;flex-direction:column;gap:12px;min-width:320px;">
<label for="videoUrlInput">Paste video URL:</label>
<textarea id="videoUrlInput" rows="6" placeholder="https://example.com/video1.mp4" style="padding:8px;font-size:14px;border-radius:4px;border:1px solid #444;background:#111;color:#fff;resize:vertical;"></textarea>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button id="addUrlConfirmBtn" class="mp4-viewer-btn">Add</button>
<button id="addUrlCancelBtn" class="mp4-viewer-btn mp4-viewer-btn-danger">Cancel</button>
</div>
</div>
</div>
`;
document.body.appendChild(this.overlay);
// Get references to elements
this.video = this.overlay.querySelector('.mp4-viewer-video');
this.dropzone = this.overlay.querySelector('.mp4-viewer-dropzone');
this.playlistItems = this.overlay.querySelector('#playlistItems');
this.searchInput = this.overlay.querySelector('#playlistSearch'); // new
this.loopBtn = this.overlay.querySelector('#loopBtn'); // new
this.coverImg = this.overlay.querySelector('.mp4-viewer-cover'); // new
// Sleep timer elements
this.sleepInput = this.overlay.querySelector('#sleepInput');
this.sleepBtn = this.overlay.querySelector('#sleepBtn');
this.cancelSleepBtn = this.overlay.querySelector('#cancelSleepBtn');
this.sleepDisplay = this.overlay.querySelector('#sleepDisplay');
// Add this for vertical wheel to horizontal scroll
this.playlistItems.addEventListener('wheel', (e) => {
if (e.deltaY !== 0) {
e.preventDefault();
this.playlistItems.scrollLeft += e.deltaY;
}
}, { passive: false });
this.playlistCount = this.overlay.querySelector('#playlistCount');
this.videoControls = this.overlay.querySelector('.mp4-viewer-video-controls');
this.videoInfo = this.overlay.querySelector('#videoInfo');
this.progressBar = this.overlay.querySelector('#progressBar');
this.progressContainer = this.overlay.querySelector('#progressContainer');
this.currentTimeEl = this.overlay.querySelector('#currentTime');
this.durationTimeEl = this.overlay.querySelector('#durationTime');
}
showInitialView() {
this.playlistItems.innerHTML = `
<div class="mp4-viewer-empty-playlist" id="emptyPlaylistDropzone">
<div class="mp4-viewer-empty-icon">📁</div>
<div>Drop MP4 Files Here</div>
<div style="font-size: 10px; margin-top: 5px;">or click to browse</div>
</div>
`;
}
setupEventListeners() {
// Control buttons
this.overlay.querySelector('#closeViewerBtn').addEventListener('click', () => this.close());
this.overlay.querySelector('#fullscreenBtn').addEventListener('click', () => this.toggleFullscreen());
this.overlay.querySelector('#addMoreBtn').addEventListener('click', () => this.openFileBrowser());
this.overlay.querySelector('#clearPlaylistBtn').addEventListener('click', () => this.clearPlaylist());
// Video control buttons
this.overlay.querySelector('#prevBtn').addEventListener('click', () => this.playPrevious());
this.overlay.querySelector('#playPauseBtn').addEventListener('click', () => this.togglePlayPause());
this.overlay.querySelector('#nextBtn').addEventListener('click', () => this.playNext());
this.overlay.querySelector('#randomBtn').addEventListener('click', () => this.playRandom());
this.overlay.querySelector('#luckyBtn').addEventListener('click', () => this.luckyPick());
// Play/Pause for audio cover
this.overlay.querySelector('.mp4-viewer-cover').addEventListener('click', () => this.togglePlayPause());
// Loop toggle
if (this.loopBtn) {
this.loopBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.loop = !this.loop;
this.video.loop = this.loop;
this.loopBtn.classList.toggle('active', this.loop);
});
}
// Playlist search (debounced for instant feel)
if (this.searchInput) {
this.searchInput.addEventListener('input', () => {
if (this._searchDebounce) clearTimeout(this._searchDebounce);
this._searchDebounce = setTimeout(() => {
const term = this.searchInput.value.trim().toLowerCase();
this.updatePlaylist(term);
}, 120); // small debounce for responsiveness
});
}
// Dropzone events for main area
this.dropzone.addEventListener('click', () => this.openFileBrowser());
this.dropzone.addEventListener('dragover', (e) => this.handleDragOver(e));
this.dropzone.addEventListener('dragleave', (e) => this.handleDragLeave(e));
this.dropzone.addEventListener('drop', (e) => this.handleDrop(e));
// Playlist area drag & drop events
this.playlistItems.addEventListener('dragover', (e) => this.handlePlaylistDragOver(e));
this.playlistItems.addEventListener('dragleave', (e) => this.handlePlaylistDragLeave(e));
this.playlistItems.addEventListener('drop', (e) => this.handlePlaylistDrop(e));
// Click event for empty playlist dropzone
this.playlistItems.addEventListener('click', (e) => {
if (e.target.closest('#emptyPlaylistDropzone')) {
this.openFileBrowser();
}
});
// Video events
// timeupdate now also updates media session positionState for seeking UI
this.video.addEventListener('timeupdate', () => { this.updateProgress(); this.updateMediaSessionPositionState(); });
this.video.addEventListener('loadedmetadata', () => {
this.updateVideoInfo();
// ensure media session position/duration sync on metadata load
this.updateMediaSessionPositionState();
});
this.video.addEventListener('play', () => this.setPlaying(true));
this.video.addEventListener('pause', () => this.setPlaying(false));
// Advance to next item when current media ends
this.video.addEventListener('ended', () => {
// If the video element is set to loop, the 'ended' event won't fire;
// when it does fire, advance to the next playlist item.
if (this.playlist.length === 0) return;
this.playNext();
});
// Progress bar seeking
this.progressContainer.addEventListener('click', (e) => this.seekVideo(e));
// Global events
document.addEventListener('keydown', (e) => this.handleKeyPress(e));
// URL modal events
this.overlay.querySelector('#addUrlBtn').addEventListener('click', () => {
this.overlay.querySelector('#urlModal').classList.remove('mp4-viewer-hidden');
this.overlay.querySelector('#videoUrlInput').value = '';
this.overlay.querySelector('#videoUrlInput').focus();
});
this.overlay.querySelector('#addUrlCancelBtn').addEventListener('click', () => {
this.overlay.querySelector('#urlModal').classList.add('mp4-viewer-hidden');
});
this.overlay.querySelector('#addUrlConfirmBtn').addEventListener('click', () => {
const text = this.overlay.querySelector('#videoUrlInput').value || '';
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
if (lines.length === 0) {
this.overlay.querySelector('#urlModal').classList.add('mp4-viewer-hidden');
return;
}
this.addUrlsToPlaylist(lines);
this.overlay.querySelector('#urlModal').classList.add('mp4-viewer-hidden');
});
// Import Playlist events
this.overlay.querySelector('#importListBtn').addEventListener('click', () => this.importPlaylistFromFile());
// Sleep timer events
if (this.sleepBtn) {
this.sleepBtn.addEventListener('click', () => {
const mins = parseInt(this.sleepInput.value, 10);
if (isNaN(mins) || mins <= 0) {
alert('Enter a valid number of minutes for the sleep timer.');
return;
}
this.startSleepTimer(mins);
});
}
if (this.cancelSleepBtn) {
this.cancelSleepBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.cancelSleepTimer();
});
}
}
startSleepTimer(minutes) {
// clear any existing timer first
this.cancelSleepTimer();
const ms = Math.max(1, minutes) * 60 * 1000;
this.sleepEndTime = Date.now() + ms;
this.sleepTimerId = setTimeout(() => {
try { this.video.pause(); } catch(e) {}
// stop timer UI
this.cancelSleepTimer();
}, ms);
// show cancel button and active state
if (this.sleepBtn) this.sleepBtn.classList.add('active');
if (this.cancelSleepBtn) {
this.cancelSleepBtn.classList.remove('mp4-viewer-hidden');
this.sleepDisplay.classList.remove('mp4-viewer-hidden');
}
// update display immediately and every second
this.updateSleepDisplay();
this.sleepIntervalId = setInterval(() => this.updateSleepDisplay(), 1000);
}
cancelSleepTimer() {
if (this.sleepTimerId) {
clearTimeout(this.sleepTimerId);
this.sleepTimerId = null;
}
if (this.sleepIntervalId) {
clearInterval(this.sleepIntervalId);
this.sleepIntervalId = null;
}
this.sleepEndTime = null;
if (this.sleepDisplay) this.sleepDisplay.textContent = '';
if (this.sleepBtn) this.sleepBtn.classList.remove('active');
if (this.cancelSleepBtn) {
this.cancelSleepBtn.classList.add('mp4-viewer-hidden');
this.sleepDisplay.classList.add('mp4-viewer-hidden');
}
}
updateSleepDisplay() {
if (!this.sleepEndTime || !this.sleepDisplay) return;
const secondsLeft = Math.max(0, Math.ceil((this.sleepEndTime - Date.now()) / 1000));
const hrs = Math.floor(secondsLeft / 3600);
const mins = Math.floor((secondsLeft % 3600) / 60);
const secs = secondsLeft % 60;
if (hrs > 0) {
// show hours when >= 1 hour (H:MM:SS)
this.sleepDisplay.textContent = `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
// show minutes:seconds when < 1 hour (MM:SS)
this.sleepDisplay.textContent = `${mins}:${secs.toString().padStart(2, '0')}`;
}
if (secondsLeft <= 0) {
this.cancelSleepTimer();
}
}
openFileBrowser() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.mp4';
input.multiple = true;
input.onchange = (e) => this.handleFileSelect(e);
input.click();
}
handleFileSelect(event) {
const files = Array.from(event.target.files);
this.addFilesToPlaylist(files);
}
handleDragOver(e) {
e.preventDefault();
this.dropzone.classList.add('dragover');
}
handleDragLeave(e) {
e.preventDefault();
this.dropzone.classList.remove('dragover');
}
handleDrop(e) {
e.preventDefault();
this.dropzone.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files);
this.addFilesToPlaylist(files);
}
handlePlaylistDragOver(e) {
e.preventDefault();
this.playlistItems.classList.add('mp4-viewer-playlist-dropzone', 'dragover');
}
handlePlaylistDragLeave(e) {
e.preventDefault();
// Only remove classes if we're actually leaving the playlist area
if (!this.playlistItems.contains(e.relatedTarget)) {
this.playlistItems.classList.remove('mp4-viewer-playlist-dropzone', 'dragover');
}
}
handlePlaylistDrop(e) {
e.preventDefault();
this.playlistItems.classList.remove('mp4-viewer-playlist-dropzone', 'dragover');
// Only handle actual file drops. Ignore empty drops (internal reorders use text/plain).
const dt = e.dataTransfer || {};
const hasFiles = dt.files && dt.files.length > 0;
if (!hasFiles) return;
const files = Array.from(dt.files);
this.addFilesToPlaylist(files);
}
// Try to extract cover image from an MP3 file (ID3v2 APIC). Returns blob URL or null.
async extractMp3Cover(file) {
try {
const buffer = await file.arrayBuffer();
const data = new Uint8Array(buffer);
// check ID3 header
if (data[0] !== 0x49 || data[1] !== 0x44 || data[2] !== 0x33) return null;
// size (syncsafe) at bytes 6-9
const size = (data[6] & 0x7f) << 21 | (data[7] & 0x7f) << 14 | (data[8] & 0x7f) << 7 | (data[9] & 0x7f);
let offset = 10;
while (offset < 10 + size) {
// frame header: 4 id, 4 size, 2 flags (ID3v2.3/2.4)
const id = String.fromCharCode(...data.slice(offset, offset + 4));
const frameSize = (data[offset+4] << 24) | (data[offset+5] << 16) | (data[offset+6] << 8) | data[offset+7];
if (frameSize <= 0) break;
if (id === 'APIC') {
const frameStart = offset + 10;
const frame = data.slice(frameStart, frameStart + frameSize);
// frame content: text encoding (1), mime (null-terminated), picture type (1), description (null-terminated), binary image data
let i = 1; // skip text encoding
// read mime
let mime = '';
while (i < frame.length && frame[i] !== 0) { mime += String.fromCharCode(frame[i]); i++; }
i++; // skip null
// skip picture type
i++;
// skip description (terminated by 0)
while (i < frame.length && frame[i] !== 0) i++;
i++; // skip null
// remaining bytes are image data
const imgData = frame.slice(i);
const blob = new Blob([imgData], { type: mime || 'image/jpeg' });
return URL.createObjectURL(blob);
}
offset += 10 + frameSize;
}
} catch (err) {
console.warn('extractMp3Cover error', err);
}
return null;
}
// new: extract a thumbnail from a local video File (returns object URL or null)
async extractVideoThumbnail(file, seekTime = 1) {
try {
const vid = document.createElement('video');
vid.muted = true;
vid.preload = 'metadata';
const tempUrl = URL.createObjectURL(file);
vid.src = tempUrl;
await new Promise((res, rej) => {
const onLoaded = () => {
// clamp seek time to duration
if (isNaN(vid.duration) || vid.duration === Infinity) {
res();
return;
}
if (seekTime > vid.duration) seekTime = Math.max(0, vid.duration / 2);
vid.removeEventListener('loadedmetadata', onLoaded);
res();
};
vid.addEventListener('loadedmetadata', onLoaded);
vid.addEventListener('error', () => { rej(new Error('video load error')); });
});
// Seek to requested time
await new Promise((res, rej) => {
const onSeek = () => { vid.removeEventListener('seeked', onSeek); res(); };
vid.addEventListener('seeked', onSeek);
// some formats instantly have currentTime, wrap in try
try { vid.currentTime = Math.min(seekTime, vid.duration || seekTime); } catch (e) {
// if seeking fails, resolve to continue (will capture at current frame)
res();
}
// timeout fallback
setTimeout(res, 1500);
});
// draw to canvas
const w = vid.videoWidth || 320;
const h = vid.videoHeight || Math.round(w * 9 / 16);
const scale = Math.min(320 / w, 1); // limit width to 320px
const cw = Math.round(w * scale);
const ch = Math.round(h * scale);
const canvas = document.createElement('canvas');
canvas.width = cw;
canvas.height = ch;
const ctx = canvas.getContext('2d');
ctx.drawImage(vid, 0, 0, cw, ch);
// convert to blob and create object URL
const blob = await new Promise((res) => canvas.toBlob(res, 'image/jpeg', 0.8));
// cleanup temp video url
try { URL.revokeObjectURL(tempUrl); } catch (e) {}
if (!blob) return null;
return URL.createObjectURL(blob);
} catch (err) {
console.warn('extractVideoThumbnail error', err);
return null;
}
}
addFilesToPlaylist(files) {
//const mp4Files = files.filter(file => file.type === 'video/mp4' || file.name.toLowerCase().endsWith('.mp4'));
// convert NodeList -> array if needed
const arr = Array.from(files);
// filter supported media
const mp4Files = arr.filter(file => {
// Check MIME types
const validMimeTypes = [
'video/mp4',
'video/mp2t',
'video/quicktime',
'video/x-matroska', // mkv
'video/webm',
'video/ogg',
'video/x-msvideo', // avi
'video/mpeg',
'video/x-dat',
'audio/mpeg',
'audio/mp3'
];
if (validMimeTypes.includes(file.type)) {
return true;
}
// Check file extensions as fallback
const fileName = file.name.toLowerCase();
return (
fileName.endsWith('.mp3') ||
fileName.endsWith('.mp4') ||
fileName.endsWith('.ts') ||
fileName.endsWith('.mov') ||
fileName.endsWith('.mkv') ||
fileName.endsWith('.webm') ||
fileName.endsWith('.avi') ||
fileName.endsWith('.mpeg') ||
fileName.endsWith('.mpg') ||
fileName.endsWith('.ogv') ||
fileName.endsWith('.dat')
);
});
if (mp4Files.length === 0) {
alert('Please select valid MP4 files.');
return;
}
// process files and extract covers for audio files
(async () => {
for (const file of mp4Files) {
const isAudio = (file.type && file.type.startsWith('audio')) || file.name.toLowerCase().endsWith('.mp3');
let coverUrl = null;
if (isAudio) {
coverUrl = await this.extractMp3Cover(file);
} else {
// new: attempt to extract thumbnail from video files
coverUrl = await this.extractVideoThumbnail(file, 1);
}
this.playlist.push({
name: file.name,
file: file,
url: URL.createObjectURL(file),
duration: 0,
isAudio: isAudio,
coverUrl: coverUrl || null
});
}
this.updatePlaylist();
// Auto-play first video/audio if none is playing
if (this.currentVideoIndex === -1 && this.playlist.length > 0) {
this.playVideo(0);
}
})();
}
addUrlToPlaylist(url) {
// Allow a single URL; accept query/fragment after extension
const re = /^https?:\/\/.+\.(mp4|webm|ogg|ogv|mkv|mov|avi|mpeg|mpg|mp3)(?:[?#].*)?$/i;
if (!re.test(url)) {
alert('Please enter a valid video URL ending with a supported extension.');
return;
}
const rawName = url.split('/').pop() || url;
const name = rawName.split(/[?#]/)[0];
const isAudio = /\.mp3(?:[?#].*)?$/i.test(url);
this.playlist.push({
name: name,
file: null,
url: url,
duration: 0,
isUrl: true,
isAudio: isAudio,
coverUrl: null // can't extract remote cover here
});
this.updatePlaylist();
if (this.currentVideoIndex === -1 && this.playlist.length > 0) {
this.playVideo(0);
}
}
// New: add multiple URLs (one per line from textarea)
addUrlsToPlaylist(urls) {
if (!Array.isArray(urls) || urls.length === 0) return;
const re = /^https?:\/\/.+\.(mp4|webm|ogg|ogv|mkv|mov|avi|mpeg|mpg|mp3)(?:[?#].*)?$/i;
const added = [];
for (const url of urls) {
if (!re.test(url)) {
// skip invalid URLs but continue processing others
console.warn('Skipped invalid URL:', url);
continue;
}
const rawName = url.split('/').pop() || url;
const name = rawName.split(/[?#]/)[0];
const isAudio = /\.mp3(?:[?#].*)?$/i.test(url);
added.push({
name,
file: null,
url,
duration: 0,
isUrl: true,
isAudio,
coverUrl: null
});
}
if (added.length === 0) {
alert('No valid URLs were provided.');
return;
}
this.playlist.push(...added);
this.updatePlaylist();
if (this.currentVideoIndex === -1 && this.playlist.length > 0) {
this.playVideo(0);
}
}
// move an item in the playlist array and update currentVideoIndex accordingly
movePlaylistItem(fromIndex, toIndex) {
if (fromIndex === null || toIndex === null) return;
if (fromIndex === toIndex) return;
if (fromIndex < 0 || fromIndex >= this.playlist.length) return;
if (toIndex < 0) toIndex = 0;
if (toIndex >= this.playlist.length) toIndex = this.playlist.length - 1;
const item = this.playlist.splice(fromIndex, 1)[0];
this.playlist.splice(toIndex, 0, item);
// adjust currentVideoIndex
if (this.currentVideoIndex === fromIndex) {
this.currentVideoIndex = toIndex;
} else if (fromIndex < this.currentVideoIndex && this.currentVideoIndex <= toIndex) {
this.currentVideoIndex -= 1;
} else if (toIndex <= this.currentVideoIndex && this.currentVideoIndex < fromIndex) {
this.currentVideoIndex += 1;
}
// Re-render playlist (preserve active element)
this.updatePlaylist(this.searchInput ? this.searchInput.value.trim().toLowerCase() : '');
}
updatePlaylist(filter = '') {
this.playlistCount.textContent = this.playlist.length;
// Clear existing items
this.playlistItems.innerHTML = '';
if (this.playlist.length === 0) {
// Show empty dropzone if no files
this.showInitialView();
return;
}
const term = (filter || '').toLowerCase();
// Add playlist items (only items that match the search OR the currently playing item)
this.playlist.forEach((item, index) => {
const nameLower = item.name.toLowerCase();
if (term && nameLower.indexOf(term) === -1 && index !== this.currentVideoIndex) {
return; // skip non-matching items unless it's the active one
}
const playlistItem = document.createElement('div');
playlistItem.className = `mp4-viewer-playlist-item ${index === this.currentVideoIndex ? 'active' : ''}`;
playlistItem.dataset.index = index; // preserve original index
playlistItem.draggable = true;
const thumbHtml = item.coverUrl
? `<img class="mp4-viewer-playlist-thumb-img" src="${item.coverUrl}" alt="cover">`
: `<div class="mp4-viewer-playlist-item-thumbnail"><span>${item.isAudio ? '🎵' : '🎬'}</span></div>`;
playlistItem.innerHTML = `
${thumbHtml}
<div class="mp4-viewer-playlist-item-info">
<span class="mp4-viewer-playlist-item-name" title="${item.name}">${item.name}</span>
<button class="mp4-viewer-playlist-item-remove" data-index="${index}">×</button>
</div>
`;
// Click to play (unless remove button)
playlistItem.addEventListener('click', (e) => {
if (!e.target.classList.contains('mp4-viewer-playlist-item-remove')) {
const idx = parseInt(playlistItem.dataset.index, 10);
this.playVideo(idx);
}
});
// Remove button
const removeBtn = playlistItem.querySelector('.mp4-viewer-playlist-item-remove');
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
const indexToRemove = parseInt(e.target.getAttribute('data-index'), 10);
this.removeFromPlaylist(indexToRemove);
});
// Drag & drop handlers for reordering
playlistItem.addEventListener('dragstart', (e) => {
e.dataTransfer.effectAllowed = 'move';
try { e.dataTransfer.setData('text/plain', index.toString()); } catch (err) {}
this._dragSrcIndex = index;
playlistItem.classList.add('dragging');
});
playlistItem.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
playlistItem.classList.add('dragover-item');
});
playlistItem.addEventListener('dragleave', () => {
playlistItem.classList.remove('dragover-item');
});
playlistItem.addEventListener('drop', (e) => {
e.stopPropagation(); // prevent parent playlist drop handler from running for internal reorders
e.preventDefault();
const from = this._dragSrcIndex !== null ? this._dragSrcIndex : parseInt(e.dataTransfer.getData('text/plain'), 10);
const to = index;
// Defensive checks
if (isNaN(from) || isNaN(to)) return;
this._dragSrcIndex = null;
this.movePlaylistItem(from, to);
});
playlistItem.addEventListener('dragend', () => {
playlistItem.classList.remove('dragging');
// clear any leftover hover states
this.playlistItems.querySelectorAll('.mp4-viewer-playlist-item').forEach(el => el.classList.remove('dragover-item'));
});
this.playlistItems.appendChild(playlistItem);
});
// Add "Add More" button at the end
const addMoreBtn = document.createElement('div');
addMoreBtn.className = 'mp4-viewer-add-more';
addMoreBtn.innerHTML = `
<div class="mp4-viewer-add-icon">+</div>
<div>Add More</div>
`;
addMoreBtn.addEventListener('click', () => this.openFileBrowser());
this.playlistItems.appendChild(addMoreBtn);
// Ensure active playing item is visible (scroll into view)
const activeEl = this.playlistItems.querySelector('.mp4-viewer-playlist-item.active');
if (activeEl) {
try {
activeEl.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
} catch (err) {
// ignore scroll errors on older browsers
}
}
}
playVideo(index) {
if (index < 0 || index >= this.playlist.length) return;
this.currentVideoIndex = index;
const videoItem = this.playlist[index];
// ensure loop flag plugged into player
this.video.loop = this.loop;
// update media session metadata for this item
try { this.updateMediaSessionMetadata(videoItem); } catch(e) {}
// If it's a URL, just set src directly
if (videoItem.isUrl) {
this.video.src = videoItem.url;
// show cover for audio URLs (if tagged as audio)
if (videoItem.isAudio) {
if (videoItem.coverUrl) {
this.coverImg.src = videoItem.coverUrl;
} else {
this.coverImg.src = ''; // fallback empty -> will be hidden
}
this.coverImg.classList.remove('mp4-viewer-hidden');
} else {
this.coverImg.classList.add('mp4-viewer-hidden');
}
this.video.classList.remove('mp4-viewer-hidden');
this.dropzone.classList.add('mp4-viewer-hidden');
this.videoControls.classList.remove('mp4-viewer-hidden');
this.updatePlaylist(this.searchInput ? this.searchInput.value.trim().toLowerCase() : '');
this.updateVideoInfo();
this.video.play().catch(e => {
console.log('Autoplay prevented:', e);
});
return;
}
// Local file logic
this.video.src = videoItem.url;
if (videoItem.isAudio) {
if (videoItem.coverUrl) {
this.coverImg.src = videoItem.coverUrl;
} else {
this.coverImg.src = '';
}
this.coverImg.classList.remove('mp4-viewer-hidden');
} else {
// hide cover if playing video
this.coverImg.classList.add('mp4-viewer-hidden');
}
this.video.classList.remove('mp4-viewer-hidden');
this.dropzone.classList.add('mp4-viewer-hidden');
this.videoControls.classList.remove('mp4-viewer-hidden');
this.updatePlaylist(this.searchInput ? this.searchInput.value.trim().toLowerCase() : '');
this.updateVideoInfo();
this.video.play().catch(e => {
console.log('Autoplay prevented:', e);
});
}
removeFromPlaylist(index) {
if (this.currentVideoIndex === index) {
// If removing current video, stop it first
this.video.pause();
this.video.src = '';
this.currentVideoIndex = -1;
this.video.classList.add('mp4-viewer-hidden');
this.dropzone.classList.remove('mp4-viewer-hidden');
this.videoControls.classList.add('mp4-viewer-hidden');
} else if (this.currentVideoIndex > index) {
this.currentVideoIndex--;
}
// Revoke object URL to free memory
URL.revokeObjectURL(this.playlist[index].url);
if (this.playlist[index].coverUrl) {
URL.revokeObjectURL(this.playlist[index].coverUrl);
}
this.playlist.splice(index, 1);
this.updatePlaylist();
}
clearPlaylist() {
if (!confirm('Are you sure you want to clear the entire playlist?')) return;
// Revoke all object URLs
this.playlist.forEach(item => {
URL.revokeObjectURL(item.url);
});
// Revoke all object URLs (media + covers)
this.playlist.forEach(item => {
if (item.url) try { URL.revokeObjectURL(item.url); } catch(e) {}
if (item.coverUrl) try { URL.revokeObjectURL(item.coverUrl); } catch(e) {}
});
this.playlist = [];
this.currentVideoIndex = -1;
this.video.pause();
this.video.src = '';
this.video.classList.add('mp4-viewer-hidden');
this.dropzone.classList.remove('mp4-viewer-hidden');
this.videoControls.classList.add('mp4-viewer-hidden');
this.updatePlaylist();
}
updateVideoInfo() {
if (this.currentVideoIndex >= 0) {
const currentItem = this.playlist[this.currentVideoIndex];
this.videoInfo.textContent = currentItem.name;
// store duration when metadata is available
if (this.video && this.video.duration && isFinite(this.video.duration)) {
try { currentItem.duration = this.video.duration; } catch(e) {}
}
// update media session position state now that we have duration
this.updateMediaSessionPositionState();
}
}
// Update the Media Session position state (so external UIs can seek / show progress)
updateMediaSessionPositionState() {
if (!('mediaSession' in navigator)) return;
if (typeof navigator.mediaSession.setPositionState !== 'function') return;
try {
const position = (this.video && !isNaN(this.video.currentTime)) ? Math.max(0, this.video.currentTime) : 0;
// prefer video.duration; fall back to stored playlist duration
let duration = (this.video && isFinite(this.video.duration)) ? this.video.duration : (this.playlist[this.currentVideoIndex] && this.playlist[this.currentVideoIndex].duration) || 0;
if (!isFinite(duration)) duration = 0;
navigator.mediaSession.setPositionState({
duration: duration,
position: position,
playbackRate: (this.video && this.video.playbackRate) ? this.video.playbackRate : 1
});
} catch (err) {
// Some browsers may throw if values invalid — ignore
// console.debug('setPositionState failed', err);
}
}
updateProgress() {
if (this.video.duration) {
const progress = (this.video.currentTime / this.video.duration) * 100;
this.progressBar.style.width = `${progress}%`;
this.currentTimeEl.textContent = this.formatTime(this.video.currentTime);
this.durationTimeEl.textContent = this.formatTime(this.video.duration);
}
}
seekVideo(e) {
if (!this.video.duration) return;
const rect = this.progressContainer.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width;
this.video.currentTime = percent * this.video.duration;
}
setPlaying(playing) {
this.isPlaying = playing;
const playPauseBtn = this.overlay.querySelector('#playPauseBtn');
playPauseBtn.textContent = playing ? '⏸️Pause' : '▶️Play';
}
togglePlayPause() {
if (this.video.paused) {
this.video.play();
} else {
this.video.pause();
}
}
playNext() {
if (this.playlist.length > 0) {
const nextIndex = (this.currentVideoIndex + 1) % this.playlist.length;
this.playVideo(nextIndex);
}
}
playPrevious() {
if (this.playlist.length > 0) {
const prevIndex = (this.currentVideoIndex - 1 + this.playlist.length) % this.playlist.length;
this.playVideo(prevIndex);
}
}
playRandom() {
if (this.playlist.length === 0) return;
let randomIndex;
do {
randomIndex = Math.floor(Math.random() * this.playlist.length);
} while (this.playlist.length > 1 && randomIndex === this.currentVideoIndex);
this.playVideo(randomIndex);
}
luckyPick() {
if (this.playlist.length === 0) return;
const luckyIndex = Math.floor(Math.random() * this.playlist.length);
const luckyItem = this.playlist[luckyIndex];
// Revoke all except lucky
this.playlist.forEach((item, idx) => {
if (idx !== luckyIndex) URL.revokeObjectURL(item.url);
});
this.playlist = [luckyItem];
this.currentVideoIndex = 0;
this.updatePlaylist();
this.playVideo(0);
}
importPlaylistFromFile() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.txt';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const text = await file.text();
// Parse lines and extract file names
const lines = text.split(/\r?\n/).map(line => line.trim().replace(/^"|"$/g, '')).filter(Boolean);
if (lines.length === 0) {
alert('No valid file paths found.');
return;
}
const fileNames = lines.map(path => path.split(/[\\/]/).pop());
alert(
'Please select the following files in the next dialog:\n\n' +
fileNames.join('\n')
);
// Now open file picker for user to select files
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.multiple = true;
fileInput.accept = '.mp4,.mkv,.webm,.avi,.mov,.mpg,.mpeg,.ogv,.ts,.dat,.mp3';
fileInput.onchange = (ev) => {
const selectedFiles = Array.from(ev.target.files);
// Match by file name
const matchedFiles = fileNames.map(name =>
selectedFiles.find(f => f.name === name)
).filter(Boolean);
if (matchedFiles.length === 0) {
alert('No matching files selected.');
return;
}
this.addFilesToPlaylist(matchedFiles);
};
fileInput.click();
};
input.click();
}
/* ONLY (Min:Sec)(0:00)
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
*/
// (Hour:Min:Sec)(0:00:00)
formatTime(seconds) {
const s = Math.max(0, Math.floor(seconds) || 0);
const hrs = Math.floor(s / 3600);
const mins = Math.floor((s % 3600) / 60);
const secs = s % 60;
if (hrs > 0) {
return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
toggleFullscreen() {
if (!document.fullscreenElement) {
this.overlay.requestFullscreen().catch(err => {
console.log(`Error attempting to enable fullscreen: ${err.message}`);
});
} else {
document.exitFullscreen();
}
}
handleKeyPress(e) {
if (!this.overlay.parentNode) return;
switch(e.key) {
case 'Escape':
this.close();
break;
case ' ':
e.preventDefault();
this.togglePlayPause();
break;
case 'ArrowRight':
e.preventDefault();
this.video.currentTime += 10;
break;
case 'ArrowLeft':
e.preventDefault();
this.video.currentTime -= 10;
break;
case 'n':
case 'N':
e.preventDefault();
this.playNext();
break;
case 'p':
case 'P':
e.preventDefault();
this.playPrevious();
break;
}
}
close() {
// Clean up object URLs
this.playlist.forEach(item => {
URL.revokeObjectURL(item.url);
});
// Clear Media Session handlers
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.setActionHandler('previoustrack', null);
navigator.mediaSession.setActionHandler('nexttrack', null);
navigator.mediaSession.setActionHandler('play', null);
navigator.mediaSession.setActionHandler('pause', null);
try { navigator.mediaSession.metadata = null; } catch(e) {}
} catch(e) { /* ignore */ }
}
// Stop any timers
try { this.cancelSleepTimer(); } catch(e) {}
// Stop the viewer's video
if (this.video) {
this.video.pause();
this.video.src = '';
}
this.overlay.remove();
}
}
// Function to open the MP4 Player Pro - now automatically opens
function openMP4Viewer() {
// Close existing viewer if any
const existingViewer = document.querySelector('.mp4-viewer-overlay');
if (existingViewer) {
existingViewer.remove();
}
// Stop any currently playing MP4 files on the page
stopDefaultMP4Playback();
// Create new viewer
new MP4ViewerPro();
}
// Function to stop default MP4 playback on the page
function stopDefaultMP4Playback() {
// Find all video elements on the page
const videos = document.querySelectorAll('video');
videos.forEach(video => {
// Pause the video
video.pause();
// Reset current time
video.currentTime = 0;
// Remove autoplay attributes to prevent restarting
video.removeAttribute('autoplay');
// If it's an MP4 source, also reset the source to stop loading
const source = video.querySelector('source[type="video/mp4"], source[src*=".mp4"]');
if (source || video.src.includes('.mp4')) {
video.src = '';
video.load();
}
});
// Also stop any object or embed elements that might be playing MP4
const objects = document.querySelectorAll('object, embed');
objects.forEach(obj => {
if (obj.src && obj.src.includes('.mp4')) {
obj.src = '';
}
});
}
// Initialize - no visible buttons added to the page
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
addKeyboardShortcut();
addFloatingLaunchButton();
// Stop any autoplaying videos when page loads
//btn.addEventListener('click', function(e) {
// if (e.target.classList.contains('mp4-viewer-launch-close')) return;
// setTimeout(stopDefaultMP4Playback, 100);
//});
setTimeout(stopDefaultMP4Playback, 100);
});
} else {
addKeyboardShortcut();
addFloatingLaunchButton();
// Stop any autoplaying videos immediately
//btn.addEventListener('click', function(e) {
// if (e.target.classList.contains('mp4-viewer-launch-close')) return;
// setTimeout(stopDefaultMP4Playback, 100);
//});
setTimeout(stopDefaultMP4Playback, 100);
}
// Auto-open the viewer (remove this line if you don't want it to open automatically)
//setTimeout(openMP4Viewer, 200);
// Function to close the MP4 Player Pro
function closeMP4Viewer() {
const existingViewer = document.querySelector('.mp4-viewer-overlay');
if (existingViewer) {
existingViewer.remove();
}
}
// Add keyboard shortcut to open viewer (Ctrl+Shift+M)
function addKeyboardShortcut() {
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.shiftKey && e.key === 'M') {
e.preventDefault();
openMP4Viewer();
}
});
}
// Initialize - no visible buttons added to the page
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', addKeyboardShortcut);
} else {
addKeyboardShortcut();
}
// Make functions globally available for manual calling if needed
window.openMP4Viewer = openMP4Viewer;
window.closeMP4Viewer = closeMP4Viewer;
// Auto-open the viewer (remove this line if you don't want it to open automatically)
//openMP4Viewer();
document.querySelector('.mp4-viewer-launch-btn').addEventListener('click', function(e) {
if (e.target.classList.contains('mp4-viewer-launch-close')) return;
openMP4Viewer();
});
// Add floating launch button
function addFloatingLaunchButton() {
if (document.querySelector('.mp4-viewer-launch-btn')) return;
const btn = document.createElement('button');
btn.className = 'mp4-viewer-launch-btn';
btn.innerHTML = `<span>🎬 MP4 Player Pro</span>
<button class="mp4-viewer-launch-close" title="Hide">×</button>`;
// Open viewer on click (ignore close icon)
btn.addEventListener('click', function(e) {
if (e.target.classList.contains('mp4-viewer-launch-close')) return;
openMP4Viewer();
btn.remove();
});
// Hide button on close icon click
btn.querySelector('.mp4-viewer-launch-close').addEventListener('click', function(e) {
e.stopPropagation();
btn.remove();
});
document.body.appendChild(btn);
}
// Add the floating launch button initially
addFloatingLaunchButton();
})();