多媒体播放器
· Html5 · 暂无标签
一个读取本地音视频文件的播放器,相关功能完善中
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>多媒体播放器</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        :root {
            --primary-color: #2196F3;
            --bg-alpha: 0.9;
            --control-height: 80px;
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            background: #1a1a1a;
            font-family: 'Microsoft Yahei', sans-serif;
            color: white;
        }

        .player-container {
            width: 1650px;
            height: 1110px;
            background: linear-gradient(135deg, rgba(44,62,80,var(--bg-alpha)), rgba(52,152,219,var(--bg-alpha)));
            border-radius: 15px;
            position: relative;
            overflow: hidden;
            transition: all 0.3s;
        }
.current-track {
flex-grow: 1;
text-align: center;
padding: 0 20px;
line-height: 40px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
        .top-bar {
            position: absolute;
            top: 0;
            width: 100%;
            display: flex;
            justify-content: space-between;
            padding: 15px;
            background: rgba(0,0,0,0.5);
            opacity: 0;
            transition: opacity 0.3s;
            z-index: 100;
        }

        .sidebar {
            position: absolute;
            left: -300px;
            top: 60px;
            width: 300px;
            height: calc(100% - 120px);
            background: rgba(0,0,0,0.7);
            transition: all 0.3s;
            overflow-y: auto;
            padding: 15px;
        }

        .file-list {
            list-style: none;
            margin-top: 15px;
        }

        .folder-item {
            margin-bottom: 15px;
        }

        .folder-header {
            padding: 8px;
            background: rgba(255,255,255,0.05);
            border-radius: 4px;
            margin-bottom: 5px;
            cursor: pointer;
        }

        .file-sublist {
            padding-left: 20px;
            list-style: none;
            display: none;
        }

        .file-item {
            padding: 8px;
            cursor: pointer;
            border-radius: 4px;
            margin-bottom: 5px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .file-item:hover {
            background: rgba(255,255,255,0.1);
        }

        .vinyl-container {
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            width: 500px;
            height: 500px;
            display: none;
        }

        .vinyl {
            width: 100%;
            height: 100%;
            border-radius: 50%;
            background: radial-gradient(circle, #f70505 50%, #060606 50%);
            animation: rotate 20s linear infinite;
            animation-play-state: paused;
            box-shadow: 0 0 30px rgb(0 0 0 / 50%);
        }

        @keyframes rotate {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }

        .video-player {
            width: 100%;
            height: calc(100% - var(--control-height));
            object-fit: contain;
            display: none;
        }

        .bottom-controls {
            position: absolute;
            bottom: 0;
            width: 100%;
            /* height: var(--control-height); */
            padding: 15px 30px;
            background: rgba(0,0,0,0.6);
            opacity: 0;
            transition: opacity 0.3s;
            display: flex;
            flex-direction: column;
            gap: 10px;
        }

        .progress-container {
            width: 100%;
            height: 5px;
            background: rgba(255,255,255,0.2);
            cursor: pointer;
            margin-bottom: 10px;
        }

        .progress-bar {
            height: 100%;
            background: var(--primary-color);
            width: 0%;
            transition: width 0.1s linear;
        }

        .controls-row {
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 25px;
        }

        .icon-btn {
            background: none;
            border: none;
            color: white;
            font-size: 20px;
            cursor: pointer;
            padding: 8px;
            border-radius: 50%;
            width: 40px;
            height: 40px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.3s;
        }

        .icon-btn:hover {
            background: rgba(255,255,255,0.1);
        }

        .fa-folder {
            margin-right: 8px;
            color: #FFD700;
        }

.tonearm {
        position: absolute;
        right: -120px;
        top: 50%;
        width: 150px;
        height: 4px;
        background: #666;
        transform-origin: right center;
        transform: rotate(-30deg);
        transition: all 0.5s ease;
        z-index: 10;
    }

    .tonearm::after {
        content: '';
        position: absolute;
        right: -10px;
        top: -8px;
        width: 20px;
        height: 20px;
        background: #444;
        border-radius: 50%;
    }

    .playing .tonearm {
        transform: rotate(0deg);
    }

    /* 歌词容器样式 */
    .lyrics-container {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        width: 50%;
        height: 70%;
        overflow-y: auto;
        padding: 20px;
        display: none;
    }

    .lyrics-available {
        width: 50% !important;
        left: 25% !important;
    }

    .lyrics-line {
        margin: 10px 0;
        padding: 8px;
        transition: all 0.3s;
        opacity: 0.5;
    }

    .lyrics-line.active {
        opacity: 1;
        transform: scale(1.1);
        color: var(--primary-color);
    }
    </style>
</head>
<body>
    <div id="player">
        <div id="topBar">
            <div>
                <button title="列表" id="toggleSidebar">
                    <i class="fas fa-bars"></i>
                </button>
            </div>
<div id="currentTrack">暂无播放</div>
            <div>
                <button title="全屏" id="toggleFullscreen">
                    <i class="fas fa-expand"></i>
                </button>
            </div>
        </div>

        <div id="sidebar">
            <input type="file" id="fileInput" multiple webkitdirectory style="display: none;">
            <button onclick="document.getElementById('fileInput').click()">
                <i class="fas fa-folder-open"></i>
            </button>
            <ul id="fileList"></ul>
        </div>

        <div>
            <div id="vinylContainer">
                <div id="vinyl"></div>
                <div id="tonearm"></div>
            </div>
            <div id="lyricsContainer"></div>
            <video id="videoPlayer"></video>
        </div>

        <div id="bottomControls" style="opacity: 0;">

            <div id="progressContainer">
                <div id="progressBar"></div>
            </div>
            <div>
                <button title="上一首" id="prevBtn">
                    <i class="fas fa-step-backward"></i>
                </button>
                <button title="播放/暂停" id="playPauseBtn">
                    <i class="fas fa-play"></i>
                </button>
                <button title="下一首" id="nextBtn">
                    <i class="fas fa-step-forward"></i>
                </button>
                <button title="列表循环" id="loopBtn"><i class="fas fa-retweet"></i></button>
                <input type="range" id="volumeSlider" min="0" max="1" step="0.1" value="1" title="音量">
                <button title="播放速度" id="speedBtn" style="display: none;">
                    1x
                </button>
            </div>
        </div>
    </div>

<script>
class MediaPlayer {
    constructor() {
        this.mediaElements = {
            audio: new Audio(),
            video: document.getElementById('videoPlayer')
        }
        this.playlist = [];
        this.currentIndex = 0;
        this.isPlaying = false;
        this.hideTimeout = null;
        this.sidebarVisible = false;
        this.loopMode = 'none'; // none, all, random
        this.folderStructure = new Map();
this.lyricsData = [];
this.currentLyricIndex = 0;
this.hasLyrics = false;

        this.initEventListeners();
        this.initMediaListeners();
this.updatePlayButtonState();
    }

    initEventListeners() {
        // 鼠标活动检测
        document.addEventListener('mousemove', () => this.showControls());
        
        // 文件选择
        document.getElementById('fileInput').addEventListener('change', e => {
            this.handleFiles([...e.target.files]);
        });

        // 播放控制
        document.getElementById('playPauseBtn').addEventListener('click', () => this.togglePlay());
        document.getElementById('prevBtn').addEventListener('click', () => this.prevTrack());
        document.getElementById('nextBtn').addEventListener('click', () => this.nextTrack());
        document.getElementById('loopBtn').addEventListener('click', () => this.toggleLoopMode());
        
        // 侧边栏控制
        document.getElementById('toggleSidebar').addEventListener('click', () => this.toggleSidebar());
        
        // 点击外部隐藏侧边栏
        document.addEventListener('click', (e) => {
            if (!e.target.closest('.sidebar') && !e.target.closest('#toggleSidebar')) {
                this.hideSidebar();
            }
        });

        // 音量控制
        document.getElementById('volumeSlider').addEventListener('input', (e) => {
            this.setVolume(e.target.value);
        });

        // 文件夹展开/收起
        document.querySelectorAll('.folder-header').forEach(header => {
            header.addEventListener('click', (e) => {
                const sublist = e.currentTarget.nextElementSibling;
                sublist.style.display = sublist.style.display === 'none' ? 'block' : 'none';
            });
        });

        // 全屏切换
        document.getElementById('toggleFullscreen').addEventListener('click', () => {
            if (!document.fullscreenElement) {
                document.documentElement.requestFullscreen();
            } else {
                document.exitFullscreen(); 
            }
        });
window.addEventListener('online', () => this.checkLyrics());
window.addEventListener('offline', () => this.hasLyrics = false);
// 歌词同步
document.getElementById('progressContainer').addEventListener('click', (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const progress = (e.clientX - rect.left) / rect.width;
    this.currentMedia.currentTime = this.currentMedia.duration * progress;
});
    }
async checkLyrics() {
if (!this.hasLyrics) {
await this.loadLyrics(this.currentMedia.name);
this.adjustLayout();
}
}

    initMediaListeners() {
        // 时间更新
        this.mediaElements.audio.addEventListener('timeupdate', () => this.updateProgress());
        this.mediaElements.video.addEventListener('timeupdate', () => this.updateProgress());
        
        // 播放结束处理
        this.mediaElements.audio.addEventListener('ended', () => this.handleEnded());
        this.mediaElements.video.addEventListener('ended', () => this.handleEnded());
    }

    handleFiles(files) {
        const supportedFormats = ['mp3','wav','ogg','mp4','mkv','avi'];
        this.playlist = files.filter(file => {
            const ext = file.name.split('.').pop().toLowerCase();
            return supportedFormats.includes(ext);
        });
        
        this.buildFolderStructure(files);
        this.renderFileList();
this.updatePlayButtonState();
if(this.playlist.length > 0) this.loadMedia(this.playlist[0]);
    }

    buildFolderStructure(files) {
        this.folderStructure.clear();
        files.forEach(file => {
            const path = file.webkitRelativePath.split('/');
            const folder = path.slice(0, -1).join('/');
            if (!this.folderStructure.has(folder)) {
                this.folderStructure.set(folder, []);
            }
            this.folderStructure.get(folder).push(file);
        });
    }

    renderFileList() {
        const list = document.getElementById('fileList');
        list.innerHTML = '';
        
        this.folderStructure.forEach((files, folder) => {
            const folderItem = document.createElement('li');
            folderItem.className = 'folder-item';
            folderItem.innerHTML = `
                <div>
                    <i class="fas fa-folder"></i>
                    ${folder || '根目录'}
                </div>
                <ul style="display:none">
                    ${files.map(file => `
                        <li data-path="${file.webkitRelativePath}">
                            ${file.name}
                        </li>
                    `).join('')}
                </ul>
            `;
            list.appendChild(folderItem);
        });

        // 绑定文件点击事件
        document.querySelectorAll('.file-item').forEach(item => {
            item.addEventListener('click', (e) => {
                e.stopPropagation();
                const path = item.dataset.path;
                const file = [...this.folderStructure.values()]
                    .flat()
                    .find(f => f.webkitRelativePath === path);
                this.loadMedia(file);
                this.hideSidebar();
            });
        });

        // 绑定文件夹点击事件
        document.querySelectorAll('.folder-header').forEach(header => {
            header.addEventListener('click', (e) => {
                const sublist = e.currentTarget.nextElementSibling;
                sublist.style.display = sublist.style.display === 'none' ? 'block' : 'none';
            });
        });
    }

    async loadMedia(file) {
// 解析文件名信息
let fileName = file.name.replace(/\.[^/.]+$/, "");
let trackInfo = fileName.split(' - ');
let displayText = trackInfo.length > 1 
? `${trackInfo[0]} - ${trackInfo.slice(1).join(' ')}`
: fileName;

document.getElementById('currentTrack').textContent = displayText;

// 加载歌词
await this.loadLyrics(file.name);
this.adjustLayout();

        this.stop();
        this.currentIndex = this.playlist.findIndex(f => f === file);
        
        if(file.type.startsWith('audio')) {
            this.mediaElements.video.style.display = 'none';
            this.mediaElements.audio.src = URL.createObjectURL(file);
            document.getElementById('vinylContainer').style.display = 'block';
        } else {
            this.mediaElements.audio.pause();
            this.mediaElements.video.src = URL.createObjectURL(file);
            this.mediaElements.video.style.display = 'block';
            document.getElementById('vinylContainer').style.display = 'none';
        }
this.updatePlayButtonState();
    }
async loadLyrics(filename) {
this.lyricsData = [];
this.currentLyricIndex = 0;
const container = document.getElementById('lyricsContainer');
container.innerHTML = '';

if (!navigator.onLine) {
this.hasLyrics = false;
return;
}

try {
const songName = filename.replace(/\.[^/.]+$/, "");
const response = await fetch(`https://api.example.com/lyrics?q=${encodeURIComponent(songName)}`);
const lyrics = await response.json();

this.parseLyrics(lyrics);
this.hasLyrics = lyrics.length > 0;

if (this.hasLyrics) {
container.innerHTML = lyrics.map(line => 
`<div data-time="${line.time}">${line.text}</div>`
).join('');
}
} catch (error) {
this.hasLyrics = false;
console.error('歌词加载失败:', error);
}
}
parseLyrics(rawLyrics) {
// 示例歌词格式:[00:12.34]歌词内容
const lines = rawLyrics.split('\n');
this.lyricsData = lines.map(line => {
const match = line.match(/\[(\d+):(\d+\.\d+)\](.*)/);
if (match) {
const minutes = parseInt(match[1]);
const seconds = parseFloat(match[2]);
return {
time: minutes * 60 + seconds,
text: match[3].trim()
};
}
return null;
}).filter(Boolean);
}

// 新增布局调整方法
adjustLayout() {
const vinyl = document.getElementById('vinylContainer');
const lyrics = document.getElementById('lyricsContainer');

if (this.hasLyrics) {
vinyl.classList.add('lyrics-available');
lyrics.style.display = 'block';
} else {
vinyl.classList.remove('lyrics-available');
lyrics.style.display = 'none';
}
}


    togglePlay() {
 if(this.playlist.length === 0) return;
        
this.isPlaying = !this.isPlaying;
const icon = this.isPlaying ? 'fa-pause' : 'fa-play';
document.getElementById('playPauseBtn').innerHTML = `<i class="fas ${icon}"></i>`;

        
        if(this.isPlaying) {
            this.currentMedia.play();
            document.getElementById('vinyl').style.animationPlayState = 'running';
        } else {
            this.currentMedia.pause();
            document.getElementById('vinyl').style.animationPlayState = 'paused';
        }
// 控制唱针动画
const vinylContainer = document.getElementById('vinylContainer');
if (this.isPlaying) {
vinylContainer.classList.add('playing');
} else {
vinylContainer.classList.remove('playing');
}
    }


    toggleLoopMode() {
const modes = ['all', 'random', 'none'];
this.loopMode = modes[(modes.indexOf(this.loopMode) + 1) % modes.length];

const titles = ['列表循环', '随机播放', '不循环'];
const icons = ['fa-retweet', 'fa-random', 'fa-retweet'];
document.getElementById('loopBtn').innerHTML = 
`<i class="fas ${icons[modes.indexOf(this.loopMode)]}"></i>`;
document.getElementById('loopBtn').title = titles[modes.indexOf(this.loopMode)];
}

    updateProgress() {
        const media = this.currentMedia;
        const progress = (media.currentTime / media.duration) * 100 || 0;
        document.getElementById('progressBar').style.width = `${progress}%`;

// 歌词同步
if (this.hasLyrics) {
const currentTime = this.currentMedia.currentTime;
const lines = document.querySelectorAll('.lyrics-line');

// 找到当前应显示的歌词
let activeIndex = this.lyricsData.findIndex(
(line, index) => currentTime >= line.time && 
(index === this.lyricsData.length -1 || 
 currentTime < this.lyricsData[index+1].time)
);

if (activeIndex !== -1 && activeIndex !== this.currentLyricIndex) {
lines[this.currentLyricIndex]?.classList.remove('active');
lines[activeIndex]?.classList.add('active');
this.currentLyricIndex = activeIndex;

// 自动滚动
const container = document.getElementById('lyricsContainer');
const activeLine = lines[activeIndex];
container.scrollTo({
top: activeLine.offsetTop - container.offsetHeight/2,
behavior: 'smooth'
});
}
}
    }

    setVolume(value) {
        this.mediaElements.audio.volume = value;
        this.mediaElements.video.volume = value;
    }

    nextTrack() {
        if (this.playlist.length === 0) return;
    
        const wasPlaying = this.isPlaying;
        
        if (this.loopMode === 'random') {
            let newIndex;
            do {
                newIndex = Math.floor(Math.random() * this.playlist.length);
            } while (newIndex === this.currentIndex && this.playlist.length > 1);
            this.currentIndex = newIndex;
        } else {
            // 列表循环时正常递增(到末尾自动回0)
            this.currentIndex = (this.currentIndex + 1) % this.playlist.length;
        }
    
        this.loadMedia(this.playlist[this.currentIndex]);
        
        // 循环模式下强制保持播放
        if (this.loopMode !== 'none' || wasPlaying) {
            this.currentMedia.play().catch(() => {});
            this.isPlaying = true;
            document.getElementById('playPauseBtn').innerHTML = '<i class="fas fa-pause"></i>';
            document.getElementById('vinyl').style.animationPlayState = 'running';
        }
    }


    prevTrack() {
        this.currentIndex = (this.currentIndex - 1 + this.playlist.length) % this.playlist.length;
        this.loadMedia(this.playlist[this.currentIndex]);
        if(this.isPlaying) this.currentMedia.play();
    }

    handleEnded() {
if (this.loopMode === 'none') return;
    
// 强制继续播放状态
this.isPlaying = true;
this.nextTrack();
}
updatePlayButtonState() {
const playBtn = document.getElementById('playPauseBtn');
playBtn.disabled = this.playlist.length === 0;
if(this.playlist.length === 0) {
playBtn.innerHTML = '<i class="fas fa-pause"></i>';
}
}
    stop() {
        this.currentMedia.pause();
        this.currentMedia.currentTime = 0;
        this.isPlaying = false;
        document.getElementById('playPauseBtn').innerHTML = '<i class="fas fa-play"></i>';
    }

    toggleSidebar() {
        this.sidebarVisible = !this.sidebarVisible;
        document.getElementById('sidebar').style.left = this.sidebarVisible ? '0' : '-300px';
        this.showControls();
    }

    hideSidebar() {
        this.sidebarVisible = false;
        document.getElementById('sidebar').style.left = '-300px';
    }

    showControls() {
        clearTimeout(this.hideTimeout);
        document.getElementById('topBar').style.opacity = 1;
        document.getElementById('bottomControls').style.opacity = 1;
        this.hideTimeout = setTimeout(() => {
            document.getElementById('topBar').style.opacity = 0;
            document.getElementById('bottomControls').style.opacity = 0;
            this.hideSidebar();
        }, 10000);
    }

    get currentMedia() {
        return this.playlist[this.currentIndex]?.type.startsWith('audio') ? 
            this.mediaElements.audio : this.mediaElements.video;
    }
}

const player = new MediaPlayer();
</script>
</body>
</html>


本文最后更新时间 2025-05-02
文章链接地址:
https://xzlo.blog/index.php/2025/05/01/player.html
本站文章除注明[转载|引用|原文]出处外,均为本站原生内容,转载前请注明出处


留言