首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >使用纯 html + javascript 开发一个本地音乐播放器

使用纯 html + javascript 开发一个本地音乐播放器

原创
作者头像
编程小妖女
发布于 2025-06-08 12:45:12
发布于 2025-06-08 12:45:12
27100
代码可运行
举报
文章被收录于专栏:前端开发前端开发
运行总次数:0
代码可运行

本文介绍不使用任何前端开发框架,仅凭借原生的 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 对象。
  • XHRFetch Reader 分段加载远程 URL。
  • ArrayBufferReader 针对 Service Worker 缓存或已经在内存中的数据块。

开发者甚至可以继承自定义 Reader 以支持 IndexedDB、WebTorrent 或 WASM 管道。

解析 mp3 文件元数据的核心代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
jsmediatags.read(selectedFile, {
  onSuccess: function(tag) {
    displayMetadata(tag, selectedFile);
  }
}

至于本地 mp3 文件的播放功能,采取了现代浏览器对 Audio 元素的支持:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
audioElement = new Audio();
audioElement.src = URL.createObjectURL(file);

基本用法如下:

代码语言:html
AI代码解释
复制
<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

添加 controls 显示原生播放界面,开发者可再用 controlslist='nodownload noremoteplayback' 精细控制 UI 选项(developer.mozilla.org)。如需完全自定义,可隐藏原生控制并结合 JavaScript 与 CSS 构建。

autoplay、muted 与浏览器策略

自动播放在移动端与桌面均受策略限制。Chrome 新版遵循 用户交互优先 + 声音静音默认允许 的原则:若元素设置 muted 或页面已获得媒体互动分数,浏览器才会放行带声自动播放(developer.chrome.com)。MDN 指出启用 autoplay 同时加 muted 是多数平台上实现无缝背景音乐的现实折中(developer.mozilla.org)。

本例完整的代码如下:

代码语言:html
AI代码解释
复制
<!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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • controls 与 controlslist
  • autoplay、muted 与浏览器策略
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档