webrtc/src/music.js

414 lines
19 KiB
JavaScript
Raw Normal View History

2023-10-22 02:07:36 +08:00
import { Img, Span, Button, List, ListItem, UploadMusic, createElement } from './weigets.js'
2023-09-28 15:20:02 +08:00
export default class MusicList {
2023-10-02 05:30:20 +08:00
constructor({ list = [], EventListeners = {}, onplay, onstop, onadd, onremove, onlike, onunlike, onban, onload }) {
this.event = { onplay, onstop, onadd, onremove, onlike, onunlike, onban, onload }
2023-09-28 15:20:02 +08:00
// 添加音乐播放器
this.audio = new Audio()
2023-10-19 00:14:29 +08:00
this.audio.autoplay = true
this.audio.controls = true
2023-10-22 12:30:31 +08:00
this.audio.style.margin = '0 auto'
2023-10-19 09:02:07 +08:00
this.audio.style.flexShrink = 0 // 防止在flex中被挤压变形
2023-10-19 00:14:29 +08:00
this.audio.addEventListener('play', () => {
this.event.onplay(this.playing)
})
2023-09-28 15:20:02 +08:00
this.audio.addEventListener('ended', () => {
this.next()
})
2023-09-28 17:59:23 +08:00
//this.audio.addEventListener('timeupdate', () => {
// console.log(this.audio.currentTime)
//})
2023-10-12 03:00:30 +08:00
//this.audio.addEventListener('error', event => {
// console.error('音乐播放器错误:', event)
//})
this.ul = List({
classList: ['music-list'],
style: {
2023-10-19 00:54:43 +08:00
flex: 1,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
2023-10-19 00:54:43 +08:00
overflowX: 'hidden', // 溢出时隐藏
overflowY: 'auto', // 溢出时显示滚动条
listStyle: 'disc', // 实心圆
padding: '0 1.1rem', // 列表左右留白
}
})
this.EventListeners = EventListeners
this.list = []
list.forEach(item => this.add(item)) // 列表逐一添加
2023-10-24 04:55:22 +08:00
this.封面 = Img({
src: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
style: {
width: '6rem',
height: '6rem',
borderRadius: '1rem',
backgroundColor: '#eee',
border: 'none',
},
onerror: event => {
// img 标签错误时, 替换为1x1透明gif图片
event.target.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
}
})
this.标题 = Span({
style: {
fontSize: '1.2rem',
fontWeight: 'bold',
margin: '0 0 .5rem 0',
},
textContent: '音乐名',
})
2023-10-19 00:14:29 +08:00
// 收起到右上角, 音乐播放器基于浮窗定位, 不再占用页面空间
const element = createElement({
style: {
position: 'fixed', top: '5rem', right: '1rem',
2023-10-19 00:54:43 +08:00
backgroundColor: '#fff', padding: '.5rem',
borderRadius: '1rem', cursor: 'pointer',
2023-10-22 12:30:31 +08:00
width: '20rem', Height: '70vh',
minWidth: '20rem', minHeight: '13rem',
maxWidth: '80vw', maxHeight: '80vh',
2023-10-19 00:54:43 +08:00
overflow: 'hidden', boxShadow: '0 0 1rem #eee',
display: 'flex', flexDirection: 'column',
2023-10-20 03:23:01 +08:00
fontSize: '12px', userSelect: 'none',
2023-10-22 12:30:31 +08:00
zIndex: '10', resize: 'auto',
},
onclick: event => {
2023-10-19 00:14:29 +08:00
this.ul.classList.toggle('disable')
},
2023-10-22 12:30:31 +08:00
onmousedown({ srcElement: dom, clientX, clientY, offsetX, offsetY }) {
2023-10-20 03:23:01 +08:00
if (dom !== element) return
2023-10-22 12:30:31 +08:00
if (offsetX > dom.clientWidth - 20 && offsetY > dom.clientHeight - 20) {
document.onmouseup = () => {
const [w, h] = [innerWidth - dom.clientWidth, innerHeight - dom.clientHeight]
if (dom.offsetLeft > w) dom.style.width = innerWidth - dom.offsetLeft + 'px'
if (dom.offsetTop > h) dom.style.height = innerHeight - dom.offsetTop + 'px'
document.onmouseup = null
localStorage.setItem('playerWH', dom.style.width + ',' + dom.style.height)
}
return
}
2023-10-20 03:23:01 +08:00
document.onmouseup = () => {
2023-10-22 12:30:31 +08:00
const [w, h] = [innerWidth - dom.clientWidth, innerHeight - dom.clientHeight]
2023-10-20 03:23:01 +08:00
if (dom.offsetLeft < 0) dom.style.left = '0px'
if (dom.offsetTop < 0) dom.style.top = '0px'
if (dom.offsetLeft > w) dom.style.left = w + 'px'
if (dom.offsetTop > h) dom.style.top = h + 'px'
document.onmouseup = document.onmousemove = null
localStorage.setItem('playerXY', dom.style.left + ',' + dom.style.top)
}
document.onmousemove = ({ clientX: x, clientY: y }) => {
dom.style.left = dom.offsetLeft - clientX + x + 'px'
dom.style.top = dom.offsetTop - clientY + y + 'px';
[clientX, clientY] = [x, y]
}
},
2023-10-19 00:14:29 +08:00
children: [
2023-10-24 04:55:22 +08:00
createElement({
style: {
display: 'flex', alignItems: 'center',
justifyContent: 'space-between', flexShrink: 0,
marginBottom: '.5rem',
},
children: [
this.封面,
createElement({
style: {
display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center',
flex: 1, padding: '0 .5rem',
},
children: [
this.标题,
createElement({
style: {
display: 'flex', alignItems: 'center',
justifyContent: 'center', flex: 1,
},
children: [
Button({
textContent: '上一首',
onclick: event => {
event.stopPropagation()
this.prev()
}
}),
Button({
textContent: '暂停',
onclick: event => {
event.stopPropagation()
if (this.audio.paused) {
this.audio.play()
event.target.textContent = '暂停'
} else {
this.audio.pause()
event.target.textContent = '开始'
}
}
}),
Button({
textContent: '下一首',
onclick: event => {
event.stopPropagation()
this.next()
}
}),
]
}),
]
}),
]
}),
2023-10-19 00:14:29 +08:00
this.audio,
2023-10-19 00:21:45 +08:00
this.ul,
UploadMusic({
2023-10-19 01:01:52 +08:00
style: { height: '5rem', margin: '1rem 0' },
2023-10-19 00:21:45 +08:00
onchange: files => {
for (const file of files) {
const id = 'music' + Date.now()
const { name, size, type } = file
const reader = new FileReader()
reader.onload = async event => {
const arrayBuffer = event.target.result
this.add({ id, name, size, type, arrayBuffer }) // 添加到列表(默认并不存储)
this.like({ id, name, size, type, arrayBuffer }) // 本地缓存的必要条件是喜欢
}
reader.readAsArrayBuffer(file)
}
}
})
2023-10-19 00:14:29 +08:00
]
})
document.body.appendChild(element)
2023-10-01 20:09:28 +08:00
// 写入 css 样式到 head
2023-10-01 03:57:01 +08:00
const style = document.createElement('style')
style.innerText = `
2023-10-19 01:01:52 +08:00
ul.music-list {
scrollbar-width: none;
ms-overflow-style: none;
}
ul.music-list::-webkit-scrollbar { display: none; }
2023-10-19 00:54:43 +08:00
ul.music-list > li {
padding: 0;
margin: 0;
2023-10-04 01:03:20 +08:00
}
2023-10-02 21:59:43 +08:00
ul.music-list > li > span {
2023-10-01 03:57:01 +08:00
cursor: pointer;
}
2023-10-02 23:09:53 +08:00
ul.music-list > li.play > span {
2023-10-02 06:44:37 +08:00
color: #02be08;
}
2023-10-01 19:59:22 +08:00
ul.music-list > li.cache::marker {
color: #02be08;
font-size: 1em;
contentx: '⚡';
}
ul.music-list > li.disable {
color: #999999;
}
2023-10-01 03:57:01 +08:00
ul.music-list > li > button {
margin-left: 10px;
border: none;
2023-10-02 07:07:33 +08:00
border-radius: 1em;
2023-10-01 03:57:01 +08:00
cursor: pointer;
2023-10-02 22:08:12 +08:00
user-select: none;
2023-10-02 07:07:33 +08:00
font-size: .5rem;
padding: 0 .5rem;
color: #555555;
2023-10-01 03:57:01 +08:00
}
ul.music-list > li > button:hover {
background-color: #ccc;
}
2023-10-04 01:03:20 +08:00
`
2023-10-01 03:57:01 +08:00
document.head.appendChild(style)
2023-09-28 15:20:02 +08:00
}
2023-09-29 21:21:26 +08:00
add(item) {
2023-10-02 00:53:17 +08:00
// 如果ID已存在则不添加
if (this.list.find(i => i.id === item.id)) {
return
}
2023-10-01 03:20:16 +08:00
// 将字节转换为可读的单位
const bytesToSize = bytes => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
}
2023-09-29 21:21:26 +08:00
this.list.push(item)
this.ul.appendChild(ListItem({
2023-09-28 15:20:02 +08:00
id: item.id,
2023-10-01 19:59:22 +08:00
classList: item.arrayBuffer ? ['cache'] : [],
2023-09-28 15:20:02 +08:00
children: [
2023-10-22 02:07:36 +08:00
...(item.picture ? [Img({
src: item.picture,
style: { width: '2em', height: '2em', borderRadius: '.25em' }
})] : []),
2023-10-02 22:24:38 +08:00
Span({
title: `${item.name} - ${bytesToSize(item.size)} - ${item.type}`,
2023-10-02 22:24:38 +08:00
textContent: `${item.name} - ${bytesToSize(item.size)}`,
2023-10-02 21:59:43 +08:00
onclick: event => {
2023-10-06 13:52:09 +08:00
event.stopPropagation() // !如果使用async则此处不能阻止冒泡传递
2023-10-02 23:09:53 +08:00
const li = event.target.parentElement // ListItem
const ul = li.parentElement // List
const list = Array.from(ul.children) // ListItems
list.forEach(li => li.classList.remove('play'))
if (!this.audio.paused && this.playing === item) {
li.classList.remove('play')
2023-10-02 21:59:43 +08:00
this.stop(item)
} else {
2023-10-02 23:09:53 +08:00
li.classList.add('play')
2023-10-02 21:59:43 +08:00
this.play(item)
}
}
}),
2023-09-28 15:20:02 +08:00
Button({
textContent: item.save ? '移除' : '缓存',
2023-09-28 15:20:02 +08:00
onclick: event => {
2023-10-06 13:52:09 +08:00
event.stopPropagation() // !如果使用async则此处不能阻止冒泡传递
if (item.save) {
2023-10-02 22:24:38 +08:00
event.target.textContent = '缓存'
2023-10-04 06:59:24 +08:00
this.ul.querySelector(`#${item.id}`).classList.remove('cache')
2023-10-02 06:30:20 +08:00
this.unlike(item)
} else {
2023-10-04 06:59:24 +08:00
item.save = true
2023-10-02 22:24:38 +08:00
event.target.textContent = '移除'
2023-10-02 06:30:20 +08:00
this.ul.querySelector(`#${item.id}`).classList.add('cache')
this.like(item)
}
2023-09-29 18:36:05 +08:00
}
2023-09-28 15:20:02 +08:00
})
]
2023-09-29 21:21:26 +08:00
}))
2023-10-02 00:30:47 +08:00
this.event.onadd(item, this.list)
2023-09-29 17:31:37 +08:00
}
2023-10-02 00:30:47 +08:00
async remove(item) {
2023-10-01 11:28:35 +08:00
this.ul.querySelector(`#${item.id}`)?.remove()
if (!this.audio.paused) this.stop() // 停止播放
2023-10-03 00:28:02 +08:00
this.list = this.list.filter(i => i.id !== item.id)
2023-10-02 00:30:47 +08:00
this.event.onremove(item)
2023-09-28 17:59:23 +08:00
}
2023-09-30 00:13:30 +08:00
async load(item) {
2023-10-02 00:30:47 +08:00
await this.event.onload(item)
2023-09-30 00:13:30 +08:00
}
2023-09-29 20:20:00 +08:00
async play(item) {
2023-10-04 10:16:21 +08:00
if (!item.arrayBuffer) {
2023-10-08 00:17:28 +08:00
console.log('加载音乐类型:', item.type)
// 不支持流式加载wav和flac和m4a, 需要全部加载完毕才能播放
if (item.type === 'audio/wav' || item.type === 'audio/flac' || item.type === 'audio/x-m4a') {
await this.load(item)
this.audio.src = URL.createObjectURL(new Blob([item.arrayBuffer], { type: item.type }))
this.audio.play()
} else {
// 边加载边播放
const mediaSource = new MediaSource()
this.audio.src = URL.createObjectURL(mediaSource)
if (!item.arrayBufferChunks) item.arrayBufferChunks = []
mediaSource.addEventListener('sourceopen', async () => {
const sourceBuffer = mediaSource.addSourceBuffer(item.type)
const arrayBufferLoader = async (index = 0) => {
console.log('开始加载====================================')
// 按照数据长度计算出分片应有数量, 如果数量不到且没有停止加载则一直读取
const chunkNumber = Math.ceil(item.size / 1024 / 64) // 64KB每片
console.log({ index, chunkNumber, paused: this.audio.paused })
while (index < chunkNumber && !this.audio.paused) {
const 播放状态 = !this.audio.paused && this.playing === item
const 加载状态 = item.arrayBufferChunks.length < chunkNumber
const 结束时间 = sourceBuffer.buffered.length && sourceBuffer.buffered.end(0)
const 缓冲时间 = 结束时间 - this.audio.currentTime
if (!播放状态 && !加载状态) break // 播放停止且加载完毕则退出
if (this.audio.paused || this.playing !== item) break // 播放停止或已经切歌则退出
if (缓冲时间 > 60) { // 缓冲超过60秒则等待30秒
await new Promise(resolve => setTimeout(resolve, 30000))
continue
}
if (sourceBuffer.updating) { // sourceBuffer正在更新则等待更新结束
await new Promise(resolve => sourceBuffer.addEventListener('updateend', resolve))
continue
}
if (item.arrayBufferChunks.length <= index) { // 分片数量不足则等待
await new Promise(resolve => setTimeout(resolve, 200))
continue
}
console.log('播放器加载分片:', item.name, `${index + 1}/${chunkNumber}`)
const chunk = item.arrayBufferChunks[index] // 顺序取出一个arrayBuffer分片
sourceBuffer.appendBuffer(chunk) // 添加到sourceBuffer
index++
2023-10-04 10:16:21 +08:00
}
console.log('加载完毕====================================')
item.arrayBufferChunks = null // 加载完毕释放分片内存
2023-10-04 10:16:21 +08:00
}
this.event.onload(item)
this.audio.play()
arrayBufferLoader()
})
}
2023-10-03 11:23:20 +08:00
} else {
2023-10-24 04:55:22 +08:00
this.标题.textContent = item.name // 替换标题
this.封面.src = item.picture // 替换封面图像
//// item.picture 是一个base64编码的图片, 获取图片的宽高和格式
//const [width, height, format] = (() => {
// const [, base64] = item.picture.split(',')
// const binary = atob(base64)
// const bytes = new Uint8Array(binary.length)
// for (let i = 0; i < binary.length; i++) {
// bytes[i] = binary.charCodeAt(i)
// }
// const blob = new Blob([bytes.buffer], { type: item.type })
// const url = URL.createObjectURL(blob)
// const img = new Image()
// img.src = url
// return new Promise(resolve => {
// img.onload = () => {
// resolve([img.width, img.height, img.src.split('.').pop()])
// }
// })
//})
//console.log('封面信息:', width, height, format)
// 替换浏览器媒体信息(使系统通知栏显示歌曲信息)
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: item.name,
artist: '艺术家名',
album: '专辑名',
artwork: [
{ src: item.picture, sizes: '96x96', type: 'image/png' },
]
});
}
2023-10-03 11:23:20 +08:00
// 本地缓存直接播放
this.audio.src = URL.createObjectURL(new Blob([item.arrayBuffer], { type: item.type }))
this.audio.play()
2023-09-29 20:20:00 +08:00
}
2023-10-02 00:30:47 +08:00
this.playing = item
this.event.onplay(item)
2023-09-28 17:59:23 +08:00
}
2023-10-02 00:30:47 +08:00
async stop() {
2023-10-04 07:10:49 +08:00
if (this.audio.paused) {
return console.error('暂停播放:音乐播放器不是播放状态!')
}
2023-09-28 17:59:23 +08:00
this.audio.pause()
2023-10-02 00:30:47 +08:00
this.event.onstop(this.playing)
2023-10-02 06:30:20 +08:00
this.playing = null
2023-09-28 15:20:02 +08:00
}
2023-10-02 00:30:47 +08:00
async like(item) {
2023-09-29 18:36:05 +08:00
if (!item.arrayBuffer) {
2023-10-02 00:30:47 +08:00
await this.load(item)
2023-09-29 18:36:05 +08:00
}
2023-10-02 05:38:44 +08:00
this.event.onlike(item, this.list)
2023-10-02 00:30:47 +08:00
}
2023-10-02 05:30:20 +08:00
async unlike(item) {
2023-10-02 05:38:44 +08:00
this.event.onunlike(item, this.list)
2023-10-02 05:30:20 +08:00
}
2023-10-02 00:30:47 +08:00
async ban(item) {
this.event.onban(item)
2023-09-29 18:36:05 +08:00
}
2023-09-28 15:20:02 +08:00
next() { }
prev() { }
}