Skip to content

When seeking near the end of an audio track, it always jumps back to the beginning and starts playback from the start position. #4282

@AsherAla

Description

@AsherAla

Bug description

For example, with a 33-second audio clip, if playback reaches around the 31-second mark and you attempt to seek, it always automatically jumps back to the beginning and starts playing from the start. Even using the ws.play(start, end) method no longer works as expected.
https://github.com/user-attachments/assets/da3debc0-e9de-4788-8978-08abd47d8679

`

时间轴
音频概览
当前:{{ formatTime(currentTime) }} / 总时长:{{ formatTime(duration) }} {{ rateLabel }} 0.5x 1x 1.5x 2x
<script setup> import { ref, reactive, computed, onMounted, nextTick, onUnmounted } from 'vue' import WaveSurfer from 'wavesurfer.js' import Hover from 'wavesurfer.js/dist/plugins/hover.js' import Timeline from 'wavesurfer.js/dist/plugins/timeline.js' import Minimap from 'wavesurfer.js/dist/plugins/minimap.js' import Regions from 'wavesurfer.js/dist/plugins/regions.js' import { ElMessage } from 'element-plus' import { string } from 'vue-types' import { VideoPause, VideoPlay } from '@element-plus/icons-vue' import { formatTime } from './utils' defineOptions({ name: 'AudioCut' }) const props = defineProps({ src: { type: String, required: true, default: '' }, canEditOrNot: { type: Boolean, default: true } }) // 或者:声明带选项的 "modelValue" prop const regions = defineModel() const waveformRef = ref(null) const timelineRef = ref(null) const minimapRef = ref(null) const ws = ref() // wavesurfer实例 const isPlay = ref(false) // 是否播放 const isLoop = ref(false) // 是否循环播放 const duration = ref(0) // 音频时长 const loading = ref(false) // 加载中 const rate = ref(1) // 播放速度 const rateLabel = computed(() => rate.value + 'x') // 播放速度标签 const pxPerSec = ref(10) // 每秒像素数 const currentTime = ref(0) // 当前播放时间 const currentRegion = ref(null) // 当前播放的区域 const currentClickedRegion = ref(null) // 当前点击的区域 用于计算指针在当前区域的位置 const wsRegions = Regions.create({ // 区域插件 dragSelection: { slop: 3 }, // 允许鼠标拖拽 maxRegions: 10, // 区域数量限制 removeButton: true // 显示删除按钮 }) /* ====== 历史记录与撤销 ====== */ const lastSnapshot = ref(null) const isUndoing = ref(false) const setLastSnapshot = (arr) => { if (isUndoing.value) return lastSnapshot.value = JSON.parse(JSON.stringify(arr)) } const undo = () => { if (!props.canEditOrNot || !lastSnapshot.value) return isUndoing.value = true wsRegions.clearRegions() lastSnapshot.value.forEach((r) => { wsRegions.addRegion({ id: r.id, start: r.start, end: r.end, color: r.color, loop: true, drag: props.canEditOrNot, resize: props.canEditOrNot, content: getRegionContent(r.id) }) }) regions.value = lastSnapshot.value lastSnapshot.value = null isUndoing.value = false } const handleKeydown = (e) => { // 检查焦点是否在输入框或文本区域 const activeElement = document.activeElement const isInput = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable) if (isInput) return if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') { e.preventDefault() undo() } } /* ====== 生命周期 ====== */ onMounted(() => { loading.value = true window.addEventListener('keydown', handleKeydown) nextTick(() => initWaveSurfer()) // 保证 DOM 已渲染 }) /* ====== 初始化 ====== */ function initWaveSurfer() { const container = waveformRef.value ws.value = WaveSurfer.create({ url: props.src, barRadius: 5, barWidth: 3, barGap: 1, container: container, // 音频容器 waveColor: '#7793a3', // 主波形颜色 progressColor: '#057373', // 进度条颜色 cursorColor: 'rgb(0,185,255)', // 光标颜色 cursorWidth: 1, // 光标宽度 height: 70, // 音频高度 width: '100%', responsive: true, // 自适应 scrollParent: true, // 滚动父级 minPxPerSec: pxPerSec.value, // 每秒像素数 fillParent: false, // 填充父级 interaction: true, // 允许用户交互 plugins: (() => { const arr = [Timeline.create({ container: timelineRef.value }), wsRegions] arr.unshift( Hover.create({ lineColor: 'rgb(23,171,225)', lineWidth: 2, labelBackground: '#555', labelColor: '#fff', labelSize: '11px' }) ) arr.push( Minimap.create({ container: minimapRef.value, waveColor: '#7793a3', progressColor: '#3B8686', height: 30, waveShadowColor: 'rgb(0,183,200)', waveStyle: 'bar', overlayColor: 'rgba(202,202,202,0.73)' }) ) return arr })() }) /* ===== 主波形图事件触发 ====== */ // 准备就绪时触发 ws.value.on('ready', () => { /* ===== 1、获取音频时长 ====== */ duration.value = ws.value.getDuration() regions.value.forEach((region) => { wsRegions.addRegion({ id: region.id, start: region.start, // 秒 end: region.end, color: region.color, // 覆盖颜色 loop: true, // 是否循环 drag: props.canEditOrNot, // 能否拖动 resize: props.canEditOrNot, // 能否拉边缘 content: getRegionContent(region.id) }) }) wsRegions.on('region-created', (region) => { if (!props.canEditOrNot) return const currentList = wsRegions.getRegions() const prevPlain = currentList .filter((r) => r.id !== region.id) .map((r) => ({ id: r.id, start: r.start, end: r.end, color: r.color })) setLastSnapshot(prevPlain) regions.value = currentList syncRegions() }) wsRegions.on('region-update', () => { if (!props.canEditOrNot) return regions.value = wsRegions.getRegions() syncRegions() }) wsRegions.on('region-removed', (region) => { if (!props.canEditOrNot) return const currentList = wsRegions.getRegions() const prevPlain = [ ...currentList.map((r) => ({ id: r.id, start: r.start, end: r.end, color: r.color })), { id: region.id, start: region.start, end: region.end, color: region.color } ] setLastSnapshot(prevPlain) regions.value = currentList syncRegions() }) /* ===== 2、鼠标滚动逻辑 ====== */ container.addEventListener( 'wheel', (e) => { /* ---- 缩放逻辑 ---- */ if (e.ctrlKey) { e.preventDefault() // 关键:阻止浏览器页面缩放 e.stopPropagation() const rect = container.getBoundingClientRect() const x = (e.clientX - rect.left) / rect.width // 0~1 const delta = e.deltaY < 0 ? 1.15 : 0.87 const next = Math.max(10, Math.min(2000, (pxPerSec.value ?? 20) * delta)) pxPerSec.value = next ws.value.zoom(next, x) // 真正让波形缩放 //TODO: 实现缩放逻辑居中,基于鼠标选中的时间点 } else { /* ---- 滚动逻辑 ---- */ e.preventDefault() //TODO: 实现左右滚动逻辑 } }, { passive: false } ) // passive:false 才能 preventDefault loading.value = false }) /* ===== 3、区域获取逻辑 ====== */ ws.value.on('dblclick', () => { if (!props.canEditOrNot) { return } // 获取双击时的选中时间 const time = ws.value.getCurrentTime() const prefix = getNextRegionPrefix() // 根据时间创建区域 wsRegions.addRegion({ id: prefix + '-' + Date.now(), start: time, // 秒 end: time + 8, color: 'rgb(255,64,64,.3)', // 覆盖颜色 loop: true, // 是否循环 drag: true, // 能否拖动 resize: true, // 能否拉边缘 content: getRegionContent(prefix + '-' + Date.now()) }) regions.value = wsRegions.getRegions() syncRegions() }) // 错误监听,出现错误时触发 ws.value.on('error', (error) => { console.error('WaveSurfer error:', error) loading.value = false // ElMessage.error('音频播放器错误: ' + error) }) // 开始播放时触发 ws.value.on('play', () => { isPlay.value = true }) // 暂停播放时触发 ws.value.on('pause', () => { isPlay.value = false }) // 音频进度监听 ws.value.on('audioprocess', () => { currentTime.value = ws.value.getCurrentTime() }) // 音频结束时触发 用于循环播放 ws.value.on('finish', () => { // 如果开启了循环播放且有当前区域,则重新播放当前区域 if (isLoop.value) { isPlay.value = false if (currentRegion.value === null) { // 播放全部 setTimeout(() => { ws.value.play() isPlay.value = true }, 300) // 短暂延迟后重新播放 } else if (currentRegion.value) { // 播放当前区域 这里就是音频段最后的区域 setTimeout(() => { ws.value.play(currentRegion.value.start, currentRegion.value.end) isPlay.value = true }, 300) // 短暂延迟后重新播放 } } }) // 添加区域播放结束事件监听 用于循环播放 wsRegions.on('region-out', (region) => { // 如果开启了循环播放且有当前区域,则重新播放当前区域 if (isLoop.value && currentRegion.value) { if (region.id === currentRegion.value.audioClipId) { isPlay.value = false setTimeout(() => { ws.value.play(currentRegion.value.start, currentRegion.value.end) isPlay.value = true }, 300) // 短暂延迟后重新播放 } } }) // 添加区域播放结束事件监听 用于循环播放 wsRegions.on('region-clicked', (region) => { currentClickedRegion.value = region }) } /* ===== 2、主音频播放控制区 ===== */ // 切换播放状态 function togglePlay() { if (ws.value) { currentRegion.value = null ws.value.playPause() } } // 设置播放速度 function changeSpeed(val) { rate.value = val if (ws.value) { ws.value.setPlaybackRate(val) } } /* ====== 4、区域操作 ====== */ //更新区域信息 function syncRegions() { regions.value = wsRegions.getRegions().map((r) => ({ id: r.id, start: r.start, end: r.end, duration: r.end - r.start, color: r.color })) } // 删除区域 function removeRegion(region) { wsRegions.getRegions().forEach((r) => { if (r.id === region.id) r.remove() // 删除区域 }) syncRegions() } // 区域播放控制 function togglePlayRegion(region) { if (isPlay.value) { // 如果正在播放,暂停播放 ws.value.pause() isPlay.value = false currentRegion.value = null } else { // 如果没有播放,检查是否已经播放过这个区域 const currentPosition = ws.value.getCurrentTime() currentRegion.value = region // 设置当前播放区域 // 如果当前播放位置在该区域范围内,则从当前位置继续播放 if (currentPosition >= region.start && currentPosition < region.end) { ws.value.play(currentPosition, region.end) } else { // 否则从区域开始位置播放 ws.value.play(region.start, region.end) } isPlay.value = true } } // 监听播放状态变化 watch(isPlay, (newVal, oldVal) => { // 可以在这里添加其他逻辑,比如触发事件等 isPlayChange(newVal) }) // 暴露给父组件的函数 用于监听播放状态变化 const emits = defineEmits(['isPlayChange']) const isPlayChange = (val) => { emits('isPlayChange', val) } // 获取下一个区域前缀的函数 const getNextRegionPrefix = () => { const count = regions.value ? regions.value.length : 0 const letterIndex = Math.floor(count / 9999) const letter = String.fromCharCode(65 + letterIndex) const number = String((count % 9999) + 1).padStart(4, '0') return `${letter}-${number}` } // 添加区域标题 const getRegionContent = (text) => { const element = document.createElement('span') element.textContent = text element.style.fontSize = 'small' element.style.marginLeft = '5px' return element } const getCurrentTime = () => { const currentTime = ws.value.getCurrentTime() if (currentClickedRegion.value == null) { return null } let time = currentTime - currentClickedRegion.value.start return formatTime(time) } defineExpose({ togglePlayRegion, removeRegion, getCurrentTime }) /* ====== 释放内存 ====== */ onUnmounted(() => { window.removeEventListener('keydown', handleKeydown) ws.value?.destroy() // 销毁播放器 }) </script> <style scoped lang="scss"> /* 自定义区域边界线样式 */ #waveform ::part(region) { border-left: 1px solid rgba(112, 31, 31, 0.3); border-right: 1px solid rgba(112, 31, 31, 0.3); } </style>

`

Environment

  • Browser: Chrome
  • Version: 7.10.1

Minimal code snippet

Expected result

Obtained result

Screenshots

Image

Metadata

Metadata

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions