在uni-app开发中,使用uni.createInnerAudioContext()播放音频时,很多开发者都遇到过这样的问题:明明音频已经开始播放了,但duration属性却始终返回0。这其实是一个官方已知的bug,主要与音频元数据加载机制有关。
音频文件的duration属性需要等待文件头部的元数据完全加载后才能获取。由于网络延迟、文件大小等因素,这个加载过程可能需要几十毫秒到几秒不等。而onCanplay事件触发时,只是表示音频可以开始播放,并不保证元数据已经完整加载。这就导致我们在onCanplay回调中直接读取duration时,很可能拿到的是0。
我在实际项目中遇到过更极端的情况:某些MP3文件由于编码特殊,在iOS设备上可能需要更长时间才能获取到正确的duration值。这时候如果直接显示音频时长,用户看到的就是"00:00",体验非常糟糕。
官方论坛上有建议使用setTimeout轮询的方案,这也是目前最普遍的解决方法。基本思路是:在onCanplay回调中启动一个定时器,每隔100毫秒检查一次duration,直到获取到非零值。
javascript复制loadDuration() {
let _this = this
setTimeout(() => {
if (_this.audioObj.duration === 0) {
_this.loadDuration()
} else {
console.log(_this.audioObj.duration)
}
}, 100)
}
但这个方案有几个明显的缺点:
setTimeout会持续占用主线程我在一个音乐类App中就踩过这个坑:当用户快速切换歌曲时,前一个音频的轮询可能还在继续,导致多个定时器同时运行,严重时甚至会引起页面卡顿。
经过多次实践,我总结出一个更完善的解决方案,结合了事件监听和超时机制:
javascript复制// 在data中定义
data() {
return {
audioObj: null,
durationCheckTimer: null,
durationTimeout: null
}
},
methods: {
initAudio() {
this.audioObj = uni.createInnerAudioContext()
this.audioObj.src = '你的音频URL'
this.audioObj.onCanplay(() => {
this.startDurationCheck()
})
// 防止无限等待,设置10秒超时
this.durationTimeout = setTimeout(() => {
this.clearDurationCheck()
console.warn('获取duration超时')
}, 10000)
},
startDurationCheck() {
this.durationCheckTimer = setInterval(() => {
if (this.audioObj.duration > 0) {
this.clearDurationCheck()
console.log('获取到duration:', this.audioObj.duration)
// 这里可以更新UI显示
}
}, 50) // 缩短检查间隔
},
clearDurationCheck() {
clearInterval(this.durationCheckTimer)
clearTimeout(this.durationTimeout)
this.durationCheckTimer = null
this.durationTimeout = null
}
}
这个方案有三大改进:
setInterval替代递归的setTimeout,减少调用栈压力实测下来,这个方案在各种机型上表现都很稳定,即使是较大的音频文件(50MB以上)也能在1-2秒内准确获取duration。
对于需要频繁播放音频的应用,我们可以进一步优化用户体验:
方案一:提前预加载
javascript复制// 在页面初始化时就创建audio实例并设置src
onLoad() {
this.audioObj = uni.createInnerAudioContext()
this.audioObj.src = '音频URL'
this.audioObj.autoplay = false
// 不自动播放,仅预加载
}
方案二:duration缓存
javascript复制// 将获取到的duration存入缓存
const audioDurationCache = {}
function getAudioDuration(url) {
return new Promise((resolve) => {
if (audioDurationCache[url]) {
resolve(audioDurationCache[url])
return
}
const audio = uni.createInnerAudioContext()
audio.src = url
const timer = setInterval(() => {
if (audio.duration > 0) {
clearInterval(timer)
audioDurationCache[url] = audio.duration
audio.destroy()
resolve(audio.duration)
}
}, 50)
setTimeout(() => {
clearInterval(timer)
audio.destroy()
resolve(0) // 超时返回0
}, 5000)
})
}
这样当用户第二次播放同一音频时,就可以直接从缓存中读取duration,无需再次等待加载。我在一个有声书项目中采用这种方案后,音频时长显示的延迟问题基本消失了。
不同平台对innerAudioContext的实现有细微差异,需要特别注意:
iOS平台:
Android平台:
微信小程序:
requiredBackgroundModes一个实用的兼容性处理方式是先检查平台,然后调整参数:
javascript复制const systemInfo = uni.getSystemInfoSync()
let checkInterval = 50 // 默认50ms
if (systemInfo.platform === 'android') {
checkInterval = 100 // Android设备适当延长间隔
} else if (systemInfo.platform === 'ios') {
checkInterval = 80 // iOS设备折中处理
}
当duration获取仍然有问题时,可以尝试以下调试方法:
javascript复制let checkCount = 0
const timer = setInterval(() => {
checkCount++
console.log(`第${checkCount}次检查,duration:`, audio.duration)
if (audio.duration > 0) {
clearInterval(timer)
}
}, 50)
网络监控:
使用Charles或Fiddler抓包,确认音频文件是否完整下载
格式转换:
尝试将音频转换为标准MP3格式(44.1kHz, 128kbps)
最小化测试:
创建一个最简单的测试页面,排除其他代码干扰
我在调试一个企业项目时,就发现是因为CDN返回的音频文件缺少部分元数据头,导致duration始终为0。后来让后端修复了CDN配置才解决问题。
即使技术问题解决了,在UI呈现上也要注意细节:
html复制<view v-if="duration === 0">加载中...</view>
<view v-else>{{ formatTime(duration) }}</view>
javascript复制async function getDurationWithRetry(url, retryCount = 3) {
for (let i = 0; i < retryCount; i++) {
const duration = await getAudioDuration(url)
if (duration > 0) return duration
await new Promise(resolve => setTimeout(resolve, 500))
}
return 0
}
这些细节处理能让用户感知不到技术上的延迟问题,我在多个音频类项目中验证过这些方案,用户反馈都非常正面。