Cơ chế playlist đơn giản
Sử dụng sự kiện ended của video/audio để chuyển sang media tiếp theo.
Ví dụ cơ bản: Playlist Video
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>HTML5 Video Playlist</title>
<style>
.player-container { max-width: 800px; margin: 2rem auto; }
.video-wrapper { position: relative; width: 100%; aspect-ratio: 16/9; background: #000; border-radius: 8px; overflow: hidden; }
video { width: 100%; height: 100%; display: block; }
.playlist { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 1rem; }
.playlist-item {
display: flex; align-items: center; gap: 1rem;
padding: 0.75rem; background: #f8f9fa; border-radius: 8px; cursor: pointer;
transition: background 0.2s; border: 2px solid transparent;
}
.playlist-item:hover { background: #e9ecef; }
.playlist-item.active { border-color: #3498db; background: #ebf5fb; }
.playlist-item.playing::before { content: "▶ "; color: #3498db; }
.thumb { width: 120px; height: 68px; object-fit: cover; border-radius: 4px; flex-shrink: 0; }
.info { flex: 1; min-width: 0; }
.title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.duration { font-size: 0.875rem; color: #666; }
</style>
</head>
<body>
<div class="player-container">
<div class="video-wrapper">
<video id="player" controls playsinline>
<source src="" type="video/mp4">
Trình duyệt không hỗ trợ video.
</video>
</div>
<div class="playlist" id="playlist"></div>
</div>
<script>
const playlistData = [
{ title: 'Video 1: Giới thiệu', src: 'video1.mp4', thumb: 'thumb1.jpg', duration: '10:30' },
{ title: 'Video 2: Tính năng', src: 'video2.mp4', thumb: 'thumb2.jpg', duration: '15:45' },
{ title: 'Video 3: Hướng dẫn', src: 'video3.mp4', thumb: 'thumb3.jpg', duration: '8:20' },
{ title: 'Video 4: Kết luận', src: 'video4.mp4', thumb: 'thumb4.jpg', duration: '5:10' },
];
const player = document.getElementById('player');
const source = player.querySelector('source');
const playlistEl = document.getElementById('playlist');
let currentIndex = 0;
// Render playlist UI
function renderPlaylist() {
playlistEl.innerHTML = '';
playlistData.forEach((item, index) => {
const div = document.createElement('div');
div.className = 'playlist-item' + (index === currentIndex ? ' active playing' : '');
div.innerHTML = `
<img class="thumb" src="${item.thumb}" alt="">
<div class="info">
<div class="title">${item.title}</div>
<div class="duration">${item.duration}</div>
</div>
`;
div.addEventListener('click', () => playVideo(index));
playlistEl.appendChild(div);
});
}
// Phát video tại index
function playVideo(index) {
currentIndex = index;
const item = playlistData[index];
source.src = item.src;
player.load(); // Quan trọng: load lại source mới
player.play().catch(e => console.log('Autoplay blocked:', e));
renderPlaylist();
}
// Tự động chuyển video tiếp theo khi hết
player.addEventListener('ended', () => {
const nextIndex = (currentIndex + 1) % playlistData.length;
playVideo(nextIndex);
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight' || e.key === 'n') {
playVideo((currentIndex + 1) % playlistData.length);
} else if (e.key === 'ArrowLeft' || e.key === 'p') {
playVideo((currentIndex - 1 + playlistData.length) % playlistData.length);
}
});
// Khởi tạo
renderPlaylist();
playVideo(0);
</script>
</body>
</html>
Playlist Audio (Music Player)
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Audio Playlist</title>
<style>
.audio-player { max-width: 500px; margin: 2rem auto; padding: 1.5rem; border: 1px solid #ddd; border-radius: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
.track-info { text-align: center; margin-bottom: 1rem; }
.track-title { font-size: 1.25rem; font-weight: 600; }
.track-artist { opacity: 0.8; font-size: 0.9rem; }
.progress-container { margin: 1rem 0; }
.progress-bar { width: 100%; height: 6px; background: rgba(255,255,255,0.3); border-radius: 3px; cursor: pointer; position: relative; }
.progress-fill { height: 100%; background: white; border-radius: 3px; width: 0%; transition: width 0.1s linear; }
.controls { display: flex; justify-content: center; align-items: center; gap: 1.5rem; }
.controls button { background: none; border: none; color: white; font-size: 1.5rem; cursor: pointer; opacity: 0.8; transition: opacity 0.2s; }
.controls button:hover { opacity: 1; }
.controls button:disabled { opacity: 0.3; cursor: not-allowed; }
.volume { display: flex; align-items: center; gap: 0.5rem; }
.playlist { margin-top: 1.5rem; max-height: 200px; overflow-y: auto; }
.playlist-item { display: flex; justify-content: space-between; padding: 0.5rem; border-radius: 4px; cursor: pointer; }
.playlist-item:hover { background: rgba(255,255,255,0.1); }
.playlist-item.active { background: rgba(255,255,255,0.2); }
</style>
</head>
<body>
<div class="audio-player">
<audio id="audioPlayer" preload="metadata"></audio>
<div class="track-info">
<div class="track-title" id="trackTitle">Chọn bài hát</div>
<div class="track-artist" id="trackArtist"></div>
</div>
<div class="progress-container">
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div style="display:flex; justify-content:space-between; font-size:0.75rem; opacity:0.8; margin-top:0.25rem;">
<span id="currentTime">0:00</span>
<span id="duration">0:00</span>
</div>
</div>
<div class="controls">
<button id="prevBtn" onclick="prevTrack()" disabled>⏮</button>
<button id="playPauseBtn" onclick="togglePlay()">▶</button>
<button id="nextBtn" onclick="nextTrack()" disabled>⏭</button>
<div class="volume">
<span>🔊</span>
<input type="range" id="volume" min="0" max="1" step="0.1" value="0.7" oninput="setVolume(this.value)">
</div>
</div>
<div class="playlist" id="playlist"></div>
</div>
<script>
const playlist = [
{ title: 'Bài 1', artist: 'Nghệ sĩ A', src: 'song1.mp3', cover: 'cover1.jpg' },
{ title: 'Bài 2', artist: 'Nghệ sĩ B', src: 'song2.mp3', cover: 'cover2.jpg' },
{ title: 'Bài 3', artist: 'Nghệ sĩ C', src: 'song3.mp3', cover: 'cover3.jpg' },
];
const audio = document.getElementById('audioPlayer');
const playlistEl = document.getElementById('playlist');
let currentIndex = 0;
let isPlaying = false;
function renderPlaylist() {
playlistEl.innerHTML = '';
playlist.forEach((track, i) => {
const div = document.createElement('div');
div.className = 'playlist-item' + (i === currentIndex ? ' active' : '');
div.innerHTML = `<span>${track.title}</span> <span>${track.artist}</span>`;
div.onclick = () => loadTrack(i);
playlistEl.appendChild(div);
});
}
function loadTrack(index) {
currentIndex = index;
const track = playlist[index];
audio.src = track.src;
document.getElementById('trackTitle').textContent = track.title;
document.getElementById('trackArtist').textContent = track.artist;
renderPlaylist();
audio.load();
audio.play().then(() => {
isPlaying = true;
updatePlayPauseBtn();
}).catch(e => console.log('Autoplay blocked'));
}
function togglePlay() {
if (audio.paused) {
audio.play();
isPlaying = true;
} else {
audio.pause();
isPlaying = false;
}
updatePlayPauseBtn();
}
function updatePlayPauseBtn() {
document.getElementById('playPauseBtn').textContent = isPlaying ? '⏸' : '▶';
}
function nextTrack() {
loadTrack((currentIndex + 1) % playlist.length);
}
function prevTrack() {
loadTrack((currentIndex - 1 + playlist.length) % playlist.length);
}
function setVolume(val) {
audio.volume = val;
}
// Progress bar
audio.addEventListener('timeupdate', () => {
const percent = (audio.currentTime / audio.duration) * 100 || 0;
document.getElementById('progressFill').style.width = percent + '%';
document.getElementById('currentTime').textContent = formatTime(audio.currentTime);
});
audio.addEventListener('loadedmetadata', () => {
document.getElementById('duration').textContent = formatTime(audio.duration);
});
audio.addEventListener('ended', nextTrack);
// Seek on progress bar click
document.getElementById('progressBar').addEventListener('click', (e) => {
const rect = e.target.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width;
audio.currentTime = percent * audio.duration;
});
function formatTime(sec) {
if (isNaN(sec)) return '0:00';
const m = Math.floor(sec / 60);
const s = Math.floor(sec % 60).toString().padStart(2, '0');
return `${m}:${s}`;
}
// Init
renderPlaylist();
audio.volume = 0.7;
</script>
</body>
</html>
Mở rộng: Playlist động từ JSON
// Load playlist từ API
async function loadPlaylist() {
const response = await fetch('/api/playlist');
const data = await response.json();
playlistData = data.tracks;
renderPlaylist();
playVideo(0);
}
Lưu ý quan trọng
player.load()bắt buộc sau khi đổisource.src- Xử lý
play()promise rejection (autoplay policy) - Dọn dẹp event listeners khi destroy player
- Preload strategy:
metadatacho playlist,autocho single video - Keyboard accessibility: Space, Arrow keys, Enter