本文介绍不使用任何前端开发框架,仅凭借原生的 html + JavaScript,实现一个简单的本地音乐播放器的实现步骤。
先看看这个播放器完成之后的使用效果。
点击选择文件:
从本地选择一个 mp3 文件:
点击解析元数据:
能够显示该 mp3 文件的大小,文件类型,标题,所在专辑名称,时长和采样率等元数据。
点击播放,就能听到悦耳的音乐了,同时还能调整音量和播放进度。
下面是详细的实现步骤。
本应用解析 mp3 文件的元数据,我用的是 jsmediatags
.
jsmediatags
借助 FileReader
, ArrayBuffer
与自建二进制解析器,允许在沙盒内直接解码标签字节,避免额外网络往返。
库在 GitHub 发布的 dist/jsmediatags.min.js
支持全局变量、CommonJS、ESM 任意导入姿势,也能通过 CDNJS 直接 <script>
引入;NPM 包提供相同 API 供打包器 Tree-Shaking。
MediaFileReader
会根据资源来源自动选择:
FileReader
读取 <input type=file>
得到的本地 File
对象。XHR
或 Fetch
Reader 分段加载远程 URL。ArrayBufferReader
针对 Service Worker 缓存或已经在内存中的数据块。开发者甚至可以继承自定义 Reader 以支持 IndexedDB、WebTorrent 或 WASM 管道。
解析 mp3 文件元数据的核心代码:
jsmediatags.read(selectedFile, {
onSuccess: function(tag) {
displayMetadata(tag, selectedFile);
}
}
至于本地 mp3 文件的播放功能,采取了现代浏览器对 Audio 元素的支持:
audioElement = new Audio();
audioElement.src = URL.createObjectURL(file);
基本用法如下:
<audio controls>
<source src='music.ogg' type='audio/ogg'>
<source src='music.mp3' type='audio/mpeg'>
你的浏览器不支持播放音频
</audio>
src
或 <source>
列举实际文件 URL;浏览器将按类型逐个尝试直到成功加载。type
明示 MIME,有助于跳过不支持的编码。可用编解码器取决于浏览器,来自 caniuse 的数据表明 mp3、aac、wav 等在主流桌面与移动环境已实现高覆盖率,而 ogg、opus 在 Safari 仍有限制(caniuse.com)。
添加 controls
显示原生播放界面,开发者可再用 controlslist='nodownload noremoteplayback'
精细控制 UI 选项(developer.mozilla.org)。如需完全自定义,可隐藏原生控制并结合 JavaScript 与 CSS 构建。
自动播放在移动端与桌面均受策略限制。Chrome 新版遵循 用户交互优先 + 声音静音默认允许
的原则:若元素设置 muted
或页面已获得媒体互动分数,浏览器才会放行带声自动播放(developer.chrome.com)。MDN 指出启用 autoplay 同时加 muted
是多数平台上实现无缝背景音乐的现实折中(developer.mozilla.org)。
本例完整的代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MP3元数据解析器</title>
<style>
body {
font-family: 'Arial', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
text-align: center;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.upload-area {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
margin-bottom: 20px;
border-radius: 5px;
cursor: pointer;
}
.upload-area:hover {
border-color: #007bff;
}
.upload-area.active {
border-color: #28a745;
background-color: rgba(40, 167, 69, 0.1);
}
.btn {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin-top: 10px;
}
.btn:hover {
background-color: #0069d9;
}
.btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.btn-success {
background-color: #28a745;
}
.btn-success:hover {
background-color: #218838;
}
.btn-container {
display: flex;
justify-content: center;
gap: 10px;
}
.metadata {
margin-top: 20px;
border-top: 1px solid #eee;
padding-top: 20px;
}
.metadata-item {
margin-bottom: 10px;
display: flex;
}
.metadata-label {
font-weight: bold;
width: 150px;
}
.metadata-value {
flex: 1;
}
.hidden {
display: none;
}
#file-name {
margin-top: 10px;
font-style: italic;
}
.error {
color: #dc3545;
margin-top: 10px;
}
.player-container {
margin-top: 20px;
padding: 15px;
border: 1px solid #eee;
border-radius: 5px;
background-color: #f9f9f9;
}
.audio-controls {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
}
.progress-container {
flex-grow: 1;
height: 8px;
background-color: #ddd;
border-radius: 4px;
margin: 0 15px;
cursor: pointer;
position: relative;
}
.progress-bar {
height: 100%;
background-color: #007bff;
border-radius: 4px;
width: 0;
}
.time-display {
font-size: 14px;
color: #666;
min-width: 45px;
}
.volume-container {
display: flex;
align-items: center;
margin-left: 15px;
}
.volume-slider {
width: 80px;
margin-left: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>MP3元数据解析器</h1>
<div class="upload-area" id="drop-area">
<p>拖放MP3文件到这里,或点击选择文件</p>
<input type="file" id="file-input" accept=".mp3" class="hidden">
<div id="file-name"></div>
</div>
<div class="btn-container">
<button id="parse-btn" class="btn" disabled>解析元数据</button>
<button id="play-btn" class="btn btn-success hidden">播放音乐</button>
</div>
<div id="player-container" class="player-container hidden">
<div class="audio-controls">
<span class="time-display" id="current-time">0:00</span>
<div class="progress-container" id="progress-container">
<div class="progress-bar" id="progress-bar"></div>
</div>
<span class="time-display" id="duration">0:00</span>
<div class="volume-container">
<i class="volume-icon">🔊</i>
<input type="range" class="volume-slider" id="volume-slider" min="0" max="1" step="0.1" value="1">
</div>
</div>
</div>
<div id="metadata-container" class="metadata hidden">
<h2>文件元数据</h2>
<div id="metadata-content"></div>
</div>
<div id="error-message" class="error hidden"></div>
</div>
<script src="jsmediatags.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const dropArea = document.getElementById('drop-area');
const fileInput = document.getElementById('file-input');
const fileName = document.getElementById('file-name');
const parseBtn = document.getElementById('parse-btn');
const playBtn = document.getElementById('play-btn');
const playerContainer = document.getElementById('player-container');
const metadataContainer = document.getElementById('metadata-container');
const metadataContent = document.getElementById('metadata-content');
const errorMessage = document.getElementById('error-message');
const progressBar = document.getElementById('progress-bar');
const progressContainer = document.getElementById('progress-container');
const currentTimeDisplay = document.getElementById('current-time');
const durationDisplay = document.getElementById('duration');
const volumeSlider = document.getElementById('volume-slider');
let selectedFile = null;
let audioElement = null;
let audioContext = null;
let audioSource = null;
let isPlaying = false;
// 点击上传区域触发文件选择
dropArea.addEventListener('click', () => {
fileInput.click();
});
// 处理文件选择
fileInput.addEventListener('change', handleFileSelect);
// 拖放事件处理
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, () => {
dropArea.classList.add('active');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, () => {
dropArea.classList.remove('active');
}, false);
});
dropArea.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length) {
fileInput.files = files;
handleFileSelect(e);
}
}, false);
// 处理文件选择
function handleFileSelect(e) {
resetUI();
const files = e.target.files || e.dataTransfer.files;
if (!files.length) return;
const file = files[0];
// 检查文件类型
if (!file.type.match('audio/mp3') && !file.name.endsWith('.mp3')) {
showError('请选择MP3文件');
return;
}
selectedFile = file;
fileName.textContent = `已选择: ${file.name}`;
parseBtn.disabled = false;
// 创建音频元素
createAudioElement(file);
}
// 创建音频元素
function createAudioElement(file) {
// 如果已存在音频元素,先清除
if (audioElement) {
audioElement.pause();
audioElement.src = '';
audioElement = null;
}
// 创建新的音频元素
audioElement = new Audio();
audioElement.src = URL.createObjectURL(file);
// 音频加载完成后显示播放按钮
audioElement.addEventListener('loadedmetadata', () => {
playBtn.classList.remove('hidden');
durationDisplay.textContent = formatTime(audioElement.duration);
});
// 音频播放时更新进度条
audioElement.addEventListener('timeupdate', updateProgress);
// 音频播放结束时重置
audioElement.addEventListener('ended', () => {
isPlaying = false;
playBtn.textContent = '播放音乐';
progressBar.style.width = '0%';
currentTimeDisplay.textContent = '0:00';
});
// 音频错误处理
audioElement.addEventListener('error', () => {
showError('音频文件加载失败');
});
}
// 播放按钮点击事件
playBtn.addEventListener('click', togglePlay);
// 切换播放/暂停
function togglePlay() {
if (!audioElement) return;
if (isPlaying) {
audioElement.pause();
playBtn.textContent = '播放音乐';
} else {
audioElement.play();
playBtn.textContent = '暂停';
playerContainer.classList.remove('hidden');
}
isPlaying = !isPlaying;
}
// 更新进度条
function updateProgress() {
if (!audioElement) return;
const currentTime = audioElement.currentTime;
const duration = audioElement.duration;
const progressPercent = (currentTime / duration) * 100;
progressBar.style.width = `${progressPercent}%`;
currentTimeDisplay.textContent = formatTime(currentTime);
}
// 点击进度条跳转
progressContainer.addEventListener('click', setProgress);
// 设置播放进度
function setProgress(e) {
if (!audioElement) return;
const width = this.clientWidth;
const clickX = e.offsetX;
const duration = audioElement.duration;
audioElement.currentTime = (clickX / width) * duration;
}
// 音量控制
volumeSlider.addEventListener('input', () => {
if (!audioElement) return;
audioElement.volume = volumeSlider.value;
});
// 解析按钮点击事件
parseBtn.addEventListener('click', () => {
if (!selectedFile) return;
metadataContainer.classList.add('hidden');
errorMessage.classList.add('hidden');
// 使用jsmediatags解析MP3元数据
jsmediatags.read(selectedFile, {
onSuccess: function(tag) {
displayMetadata(tag, selectedFile);
},
onError: function(error) {
console.error('Error reading tags:', error.type, error.info);
showError('无法读取文件元数据: ' + error.info);
// 尝试使用Web Audio API获取基本信息
getAudioDuration(selectedFile);
}
});
});
// 显示元数据
function displayMetadata(tag, file) {
metadataContent.innerHTML = '';
// 添加文件基本信息
addMetadataItem('文件名', file.name);
addMetadataItem('文件大小', formatFileSize(file.size));
addMetadataItem('文件类型', file.type || 'audio/mp3');
// 获取音频时长
getAudioDuration(file);
// 添加ID3标签信息
const tags = tag.tags;
if (tags.title) addMetadataItem('标题', tags.title);
if (tags.artist) addMetadataItem('艺术家', tags.artist);
if (tags.album) addMetadataItem('专辑', tags.album);
if (tags.year) addMetadataItem('年份', tags.year);
if (tags.genre) addMetadataItem('流派', tags.genre);
if (tags.track) addMetadataItem('音轨', tags.track);
// 显示专辑封面
if (tags.picture) {
const picture = tags.picture;
const base64String = arrayBufferToBase64(picture.data);
const imgFormat = picture.format || 'image/jpeg';
const imgSrc = `data:${imgFormat};base64,${base64String}`;
const imgContainer = document.createElement('div');
imgContainer.className = 'metadata-item';
const label = document.createElement('div');
label.className = 'metadata-label';
label.textContent = '专辑封面';
const value = document.createElement('div');
value.className = 'metadata-value';
const img = document.createElement('img');
img.src = imgSrc;
img.style.maxWidth = '200px';
img.style.maxHeight = '200px';
value.appendChild(img);
imgContainer.appendChild(label);
imgContainer.appendChild(value);
metadataContent.appendChild(imgContainer);
}
// 显示其他可用标签
for (const key in tags) {
if (['title', 'artist', 'album', 'year', 'genre', 'track', 'picture'].includes(key)) continue;
if (typeof tags[key] === 'object') {
// 跳过复杂对象
continue;
}
addMetadataItem(key, tags[key]);
}
metadataContainer.classList.remove('hidden');
}
// 获取音频时长
function getAudioDuration(file) {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const reader = new FileReader();
reader.onload = function(e) {
audioContext.decodeAudioData(e.target.result, function(buffer) {
const duration = buffer.duration;
addMetadataItem('时长', formatTime(duration));
// 添加其他音频信息
addMetadataItem('采样率', buffer.sampleRate + ' Hz');
addMetadataItem('声道数', buffer.numberOfChannels);
metadataContainer.classList.remove('hidden');
}, function(error) {
console.error('Error decoding audio data', error);
showError('无法解码音频数据');
});
};
reader.onerror = function() {
showError('读取文件时出错');
};
reader.readAsArrayBuffer(file);
}
// 添加元数据项
function addMetadataItem(label, value) {
if (value === undefined || value === null || value === '') return;
const item = document.createElement('div');
item.className = 'metadata-item';
const labelEl = document.createElement('div');
labelEl.className = 'metadata-label';
labelEl.textContent = label;
const valueEl = document.createElement('div');
valueEl.className = 'metadata-value';
valueEl.textContent = value;
item.appendChild(labelEl);
item.appendChild(valueEl);
metadataContent.appendChild(item);
}
// 显示错误信息
function showError(message) {
errorMessage.textContent = message;
errorMessage.classList.remove('hidden');
}
// 重置UI
function resetUI() {
fileName.textContent = '';
parseBtn.disabled = true;
playBtn.classList.add('hidden');
playerContainer.classList.add('hidden');
metadataContainer.classList.add('hidden');
errorMessage.classList.add('hidden');
metadataContent.innerHTML = '';
// 重置播放状态
if (audioElement) {
audioElement.pause();
audioElement.src = '';
audioElement = null;
}
isPlaying = false;
playBtn.textContent = '播放音乐';
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 格式化时间
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
// ArrayBuffer转Base64
function arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
});
</script>
</body>
</html>
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。