Fix doubled player on VHS LoadAudio in vue (#8206)

In vue mode, the VHS Load Audio (Upload) node had 2 audio previews. This
occurred because the native AudioPreview widget was being applied to any
combo widget with the name `audio`. This native preview does not support
the advanced preview functions VHS provides like seeking to specific
start time, trimming to a target duration, or converting from formats
the browser may not support.

This is fixed through a fairly involved cleanup to instead display the
litegraph AudioUI widget as an AudioPreview widget when in vue mode.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8206-Fix-doubled-player-on-VHS-LoadAudio-in-vue-2ef6d73d365081ce8907dca2706214a1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
AustinMroz
2026-01-21 00:14:37 -08:00
committed by GitHub
parent 7a1a2c1abb
commit 9a6ead37cb
11 changed files with 84 additions and 259 deletions

View File

@@ -1,17 +1,13 @@
<template>
<div class="relative">
<div
v-if="!hidden"
:class="
cn(
'bg-component-node-widget-background box-border flex gap-4 items-center justify-start relative rounded-lg w-full h-16 px-4 py-0',
{ hidden: hideWhenEmpty && !hasAudio }
)
"
v-if="!hideWhenEmpty || modelValue"
class="bg-component-node-widget-background box-border flex gap-4 items-center justify-start relative rounded-lg w-full h-16 px-4 py-0"
>
<!-- Hidden audio element -->
<audio
ref="audioRef"
:src="modelValue"
@loadedmetadata="handleLoadedMetadata"
@timeupdate="handleTimeUpdate"
@ended="handleEnded"
@@ -137,18 +133,13 @@
<script setup lang="ts">
import Slider from 'primevue/slider'
import TieredMenu from 'primevue/tieredmenu'
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { computed, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { whenever } from '@vueuse/core'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { getLocatorIdFromNodeData } from '@/utils/graphTraversalUtil'
import { isOutputNode } from '@/utils/nodeFilterUtil'
import { cn } from '@/utils/tailwindUtil'
import { formatTime, getResourceURL } from '../../utils/audioUtils'
import { formatTime } from '../../utils/audioUtils'
const { t } = useI18n()
@@ -156,8 +147,6 @@ const props = withDefaults(
defineProps<{
hideWhenEmpty?: boolean
showOptionsButton?: boolean
nodeId?: string
audioUrl?: string
}>(),
{
hideWhenEmpty: true
@@ -165,14 +154,13 @@ const props = withDefaults(
)
// Refs
const audioRef = ref<HTMLAudioElement>()
const audioRef = useTemplateRef('audioRef')
const optionsMenu = ref()
const isPlaying = ref(false)
const isMuted = ref(false)
const volume = ref(1)
const currentTime = ref(0)
const duration = ref(0)
const hasAudio = ref(false)
const playbackRate = ref(1)
// Computed
@@ -180,61 +168,11 @@ const progressPercentage = computed(() => {
if (!duration.value || duration.value === 0) return 0
return (currentTime.value / duration.value) * 100
})
const modelValue = defineModel<string>()
const showVolumeTwo = computed(() => !isMuted.value && volume.value > 0.5)
const showVolumeOne = computed(() => isMuted.value && volume.value > 0)
const litegraphNode = computed(() => {
if (!props.nodeId || !app.canvas.graph) return null
return app.canvas.graph.getNodeById(props.nodeId) as LGraphNode | null
})
const hidden = computed(() => {
if (!litegraphNode.value) return false
// dont show if its a LoadAudio and we have nodeId
const isLoadAudio =
litegraphNode.value.constructor?.comfyClass === 'LoadAudio'
return isLoadAudio && !!props.nodeId
})
// Check if this is an output node
const isOutputNodeRef = computed(() => {
const node = litegraphNode.value
return !!node && isOutputNode(node)
})
const nodeLocatorId = computed(() => {
const node = litegraphNode.value
if (!node) return null
return getLocatorIdFromNodeData(node)
})
const nodeOutputStore = useNodeOutputStore()
// Computed audio URL from node output (for output nodes)
const audioUrlFromOutput = computed(() => {
if (!isOutputNodeRef.value || !nodeLocatorId.value) return ''
const nodeOutput = nodeOutputStore.nodeOutputs[nodeLocatorId.value]
if (!nodeOutput?.audio || nodeOutput.audio.length === 0) return ''
const audio = nodeOutput.audio[0]
if (!audio.filename) return ''
return api.apiURL(
getResourceURL(
audio.subfolder || '',
audio.filename,
audio.type || 'output'
)
)
})
// Combined audio URL (output takes precedence for output nodes)
const finalAudioUrl = computed(() => {
return audioUrlFromOutput.value || props.audioUrl || ''
})
// Playback controls
const togglePlayPause = () => {
if (!audioRef.value || !audioRef.value.src) {
@@ -335,36 +273,15 @@ const menuItems = computed(() => [
}
])
// Load audio from URL
const loadAudioFromUrl = (url: string) => {
if (!audioRef.value) return
isPlaying.value = false
audioRef.value.pause()
audioRef.value.src = url
void audioRef.value.load()
hasAudio.value = !!url
}
// Watch for finalAudioUrl changes
watch(
finalAudioUrl,
(newUrl) => {
if (newUrl) {
void nextTick(() => {
loadAudioFromUrl(newUrl)
})
}
whenever(
modelValue,
() => {
isPlaying.value = false
audioRef.value?.pause()
void audioRef.value?.load()
},
{ immediate: true }
)
// Cleanup
onUnmounted(() => {
if (audioRef.value) {
audioRef.value.pause()
audioRef.value.src = ''
}
})
</script>
<style scoped>