Files
nullara-Auto-Audio-Player/js/auto_audio_player.js
T
2026-04-21 14:49:42 -07:00

366 lines
12 KiB
JavaScript

import { app } from '../../scripts/app.js'
function formatTime(seconds) {
if (!Number.isFinite(seconds) || seconds < 0) return '0:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${String(secs).padStart(2, '0')}`
}
function getPayload(message) {
const direct = Array.isArray(message?.audio) ? message.audio[0] : message?.audio
if (direct?.url) return direct
const nestedUi = Array.isArray(message?.ui?.audio) ? message.ui.audio[0] : message?.ui?.audio
if (nestedUi?.url) return nestedUi
const nestedResult = Array.isArray(message?.result?.ui?.audio) ? message.result.ui.audio[0] : message?.result?.ui?.audio
if (nestedResult?.url) return nestedResult
return null
}
function styleRange(input) {
input.style.width = '100%'
input.style.margin = '0'
input.style.minWidth = '0'
input.style.accentColor = '#b866ff'
}
function findWidget(node, name) {
return node.widgets?.find((w) => w?.name === name) || null
}
function setWidgetValue(widget, value, node) {
if (!widget) return
widget.value = value
if (typeof widget.callback === 'function') widget.callback(value)
if (typeof node?.setDirtyCanvas === 'function') node.setDirtyCanvas(true, true)
}
function hideWidget(widget) {
if (!widget || widget.__autoAudioHidden) return
widget.__autoAudioHidden = true
widget.hidden = true
widget.type = 'converted-widget'
widget.computeSize = () => [0, 0]
widget.draw = () => {}
widget.mouse = () => false
widget.serializeValue = () => widget.value
if (widget.element) {
widget.element.style.display = 'none'
}
}
app.registerExtension({
name: 'custom.auto_audio_player',
async beforeRegisterNodeDef(nodeType, nodeData) {
const nodeName = nodeData?.name || nodeData?.display_name || nodeType?.comfyClass || nodeType?.type
if (nodeName !== 'AutoAudioPlayer') return
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments)
if (this.__autoAudioPlayerInitialized) return result
this.__autoAudioPlayerInitialized = true
const autoplayWidget = findWidget(this, 'autoplay')
const volumeWidget = findWidget(this, 'default_volume')
const loopWidget = findWidget(this, 'loop')
hideWidget(autoplayWidget)
hideWidget(volumeWidget)
hideWidget(loopWidget)
requestAnimationFrame(() => {
hideWidget(autoplayWidget)
hideWidget(volumeWidget)
hideWidget(loopWidget)
this.setDirtyCanvas?.(true, true)
})
const container = document.createElement('div')
container.style.display = 'flex'
container.style.flexDirection = 'column'
container.style.gap = '10px'
container.style.padding = '12px'
container.style.width = '100%'
container.style.boxSizing = 'border-box'
container.style.background = 'rgba(255,255,255,0.04)'
container.style.border = '1px solid rgba(255,255,255,0.08)'
container.style.borderRadius = '12px'
container.style.color = '#e8e8ea'
container.style.fontSize = '12px'
container.style.fontFamily = 'inherit'
container.style.overflow = 'hidden'
const status = document.createElement('div')
status.textContent = 'Waiting for audio...'
status.style.fontSize = '12px'
status.style.lineHeight = '1.35'
status.style.fontWeight = '600'
status.style.opacity = '0.95'
status.style.wordBreak = 'break-word'
const audio = document.createElement('audio')
audio.preload = 'auto'
audio.style.display = 'none'
const playRow = document.createElement('div')
playRow.style.display = 'grid'
playRow.style.gridTemplateColumns = '64px minmax(0,1fr) auto'
playRow.style.alignItems = 'center'
playRow.style.columnGap = '10px'
playRow.style.rowGap = '6px'
const playPauseButton = document.createElement('button')
playPauseButton.textContent = 'Play'
playPauseButton.style.height = '34px'
playPauseButton.style.minWidth = '64px'
playPauseButton.style.padding = '0 12px'
playPauseButton.style.border = '1px solid rgba(255,255,255,0.10)'
playPauseButton.style.borderRadius = '10px'
playPauseButton.style.background = 'rgba(255,255,255,0.10)'
playPauseButton.style.color = '#f2f2f3'
playPauseButton.style.cursor = 'pointer'
playPauseButton.style.fontWeight = '700'
playPauseButton.style.fontSize = '12px'
playPauseButton.style.lineHeight = '1'
const progress = document.createElement('input')
progress.type = 'range'
progress.min = '0'
progress.max = '1000'
progress.step = '1'
progress.value = '0'
styleRange(progress)
const timeLabel = document.createElement('div')
timeLabel.textContent = '0:00 / 0:00'
timeLabel.style.fontSize = '12px'
timeLabel.style.opacity = '0.92'
timeLabel.style.minWidth = '78px'
timeLabel.style.textAlign = 'right'
timeLabel.style.fontVariantNumeric = 'tabular-nums'
timeLabel.style.whiteSpace = 'nowrap'
timeLabel.style.fontWeight = '600'
playRow.append(playPauseButton, progress, timeLabel)
const controlsGrid = document.createElement('div')
controlsGrid.style.display = 'grid'
controlsGrid.style.gridTemplateColumns = '64px minmax(0,1fr) auto'
controlsGrid.style.alignItems = 'center'
controlsGrid.style.columnGap = '10px'
controlsGrid.style.rowGap = '10px'
const volumeLabel = document.createElement('div')
volumeLabel.textContent = 'Vol'
volumeLabel.style.fontWeight = '700'
volumeLabel.style.opacity = '0.95'
const volume = document.createElement('input')
volume.type = 'range'
volume.min = '0'
volume.max = '1'
volume.step = '0.01'
volume.value = String(volumeWidget?.value ?? 1)
styleRange(volume)
const loopWrap = document.createElement('label')
loopWrap.style.display = 'inline-flex'
loopWrap.style.alignItems = 'center'
loopWrap.style.justifySelf = 'end'
loopWrap.style.gap = '6px'
loopWrap.style.whiteSpace = 'nowrap'
loopWrap.style.cursor = 'pointer'
loopWrap.style.userSelect = 'none'
loopWrap.style.fontWeight = '700'
const loopToggle = document.createElement('input')
loopToggle.type = 'checkbox'
loopToggle.style.margin = '0'
loopToggle.style.accentColor = '#b866ff'
loopToggle.checked = !!loopWidget?.value
const loopText = document.createElement('span')
loopText.textContent = 'Loop'
loopWrap.append(loopToggle, loopText)
const autoplayLabel = document.createElement('div')
autoplayLabel.textContent = 'Autoplay'
autoplayLabel.style.fontWeight = '700'
autoplayLabel.style.opacity = '0.95'
const autoplayToggleWrap = document.createElement('label')
autoplayToggleWrap.style.display = 'inline-flex'
autoplayToggleWrap.style.alignItems = 'center'
autoplayToggleWrap.style.justifySelf = 'start'
autoplayToggleWrap.style.gap = '6px'
autoplayToggleWrap.style.cursor = 'pointer'
autoplayToggleWrap.style.userSelect = 'none'
autoplayToggleWrap.style.fontWeight = '700'
const autoplayToggle = document.createElement('input')
autoplayToggle.type = 'checkbox'
autoplayToggle.style.margin = '0'
autoplayToggle.style.accentColor = '#b866ff'
autoplayToggle.checked = autoplayWidget?.value !== false
autoplayToggleWrap.append(autoplayToggle)
const spacer = document.createElement('div')
spacer.style.minWidth = '1px'
controlsGrid.append(volumeLabel, volume, loopWrap, autoplayLabel, autoplayToggleWrap, spacer)
container.append(status, playRow, controlsGrid, audio)
this.addDOMWidget('auto_audio_player', 'auto_audio_player', container)
this.size = [Math.max(this.size?.[0] || 320, 360), Math.max(this.size?.[1] || 220, 200)]
this.setDirtyCanvas(true, true)
const syncButtonLabel = () => {
playPauseButton.textContent = audio.paused ? 'Play' : 'Pause'
}
const syncTimeLabel = () => {
const current = audio.currentTime || 0
const total = Number.isFinite(audio.duration) ? audio.duration : 0
timeLabel.textContent = `${formatTime(current)} / ${formatTime(total)}`
}
const syncProgress = () => {
const total = Number.isFinite(audio.duration) ? audio.duration : 0
if (total <= 0) {
progress.value = '0'
return
}
progress.value = String(Math.round((audio.currentTime / total) * 1000))
}
playPauseButton.onclick = async () => {
if (!audio.src) return
try {
if (audio.paused) {
await audio.play()
} else {
audio.pause()
}
} catch (err) {
console.warn('[AutoAudioPlayer] play/pause error:', err)
}
syncButtonLabel()
}
progress.oninput = () => {
const total = Number.isFinite(audio.duration) ? audio.duration : 0
if (total <= 0) return
audio.currentTime = (Number(progress.value) / 1000) * total
syncTimeLabel()
}
volume.oninput = () => {
const value = Number(volume.value)
audio.volume = value
setWidgetValue(volumeWidget, value, this)
}
loopToggle.onchange = () => {
audio.loop = loopToggle.checked
setWidgetValue(loopWidget, loopToggle.checked, this)
}
autoplayToggle.onchange = () => {
setWidgetValue(autoplayWidget, autoplayToggle.checked, this)
}
audio.addEventListener('play', syncButtonLabel)
audio.addEventListener('pause', syncButtonLabel)
audio.addEventListener('timeupdate', () => {
syncProgress()
syncTimeLabel()
})
audio.addEventListener('loadedmetadata', () => {
syncProgress()
syncTimeLabel()
})
audio.addEventListener('ended', () => {
syncButtonLabel()
syncProgress()
syncTimeLabel()
})
this.__autoAudioPlayer = {
status,
audio,
progress,
volume,
loopToggle,
autoplayToggle,
syncButtonLabel,
syncProgress,
syncTimeLabel,
}
return result
}
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments)
const ui = this.__autoAudioPlayer
if (!ui) return
const payload = getPayload(message)
if (!payload?.url) {
ui.status.textContent = 'Execution finished, but no audio preview payload was returned.'
console.warn('[AutoAudioPlayer] No usable payload found', message)
return
}
const cacheBustedUrl = `${payload.url}${payload.url.includes('?') ? '&' : '?'}t=${Date.now()}`
const preferredVolume = Number.isFinite(Number(findWidget(this, 'default_volume')?.value))
? Number(findWidget(this, 'default_volume')?.value)
: Number.isFinite(Number(ui.volume.value))
? Number(ui.volume.value)
: Number(payload.default_volume ?? 1)
const preferredLoop = findWidget(this, 'loop')?.value ?? payload.loop
const preferredAutoplay = findWidget(this, 'autoplay')?.value ?? payload.autoplay
ui.audio.pause()
ui.audio.src = cacheBustedUrl
ui.audio.load()
ui.volume.value = String(preferredVolume)
ui.audio.volume = preferredVolume
ui.loopToggle.checked = !!preferredLoop
ui.audio.loop = ui.loopToggle.checked
ui.autoplayToggle.checked = preferredAutoplay !== false
const durationText = Number.isFinite(payload.duration) ? formatTime(payload.duration) : '0:00'
ui.status.textContent = `Audio ready • ${payload.sample_rate ?? '?'} Hz • ${payload.channels ?? '?'} ch • ${durationText}`
ui.syncButtonLabel()
ui.syncProgress()
ui.syncTimeLabel()
if (preferredAutoplay) {
ui.audio.play().catch((err) => {
console.warn('[AutoAudioPlayer] Autoplay blocked or failed:', err)
ui.status.textContent += ' • Autoplay blocked by browser until you interact with the page.'
ui.syncButtonLabel()
})
}
this.setDirtyCanvas(true, true)
}
}
})