|
import { app } from '../../../scripts/app.js' |
|
|
|
import { ComfyWidgets } from '../../../scripts/widgets.js' |
|
import { $el } from '../../../scripts/ui.js' |
|
|
|
let api_host = `${window.location.hostname}:${window.location.port}` |
|
let api_base = '' |
|
let url = `${window.location.protocol}//${api_host}${api_base}` |
|
|
|
async function getQueue () { |
|
try { |
|
const res = await fetch(`${url}/queue`) |
|
const data = await res.json() |
|
|
|
return { |
|
|
|
Running: data.queue_running.length, |
|
Pending: data.queue_pending.length |
|
} |
|
} catch (error) { |
|
console.error(error) |
|
return { Running: 0, Pending: 0 } |
|
} |
|
} |
|
|
|
async function interrupt () { |
|
const resp = await fetch(`${url}/interrupt`, { |
|
method: 'POST' |
|
}) |
|
} |
|
|
|
async function clipboardWriteImage (win, url) { |
|
const canvas = document.createElement('canvas') |
|
const ctx = canvas.getContext('2d') |
|
|
|
const img = await createImage(url) |
|
|
|
canvas.width = img.naturalWidth |
|
canvas.height = img.naturalHeight |
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height) |
|
ctx.drawImage(img, 0, 0) |
|
|
|
canvas.toBlob(async blob => { |
|
const data = [ |
|
new ClipboardItem({ |
|
[blob.type]: blob |
|
}) |
|
] |
|
|
|
win.navigator.clipboard |
|
.write(data) |
|
.then(() => { |
|
console.log('Image copied to clipboard') |
|
}) |
|
.catch(error => { |
|
console.error('Failed to copy image to clipboard:', error) |
|
}) |
|
}) |
|
} |
|
|
|
async function uploadFile (file) { |
|
try { |
|
const body = new FormData() |
|
body.append('image', file) |
|
body.append('overwrite', 'true') |
|
body.append('type', 'temp') |
|
|
|
const resp = await fetch(`${url}/upload/image`, { |
|
method: 'POST', |
|
body |
|
}) |
|
|
|
if (resp.status === 200) { |
|
const data = await resp.json() |
|
let path = data.name |
|
if (data.subfolder) path = data.subfolder + '/' + path |
|
return path |
|
} else { |
|
alert(resp.status + ' - ' + resp.statusText) |
|
} |
|
} catch (error) { |
|
alert(error) |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function shareScreen ( |
|
isCamera = false, |
|
webcamVideo, |
|
shareBtn, |
|
liveBtn, |
|
previewArea |
|
) { |
|
try { |
|
let mediaStream |
|
|
|
if (!isCamera) { |
|
mediaStream = await navigator.mediaDevices.getDisplayMedia({ |
|
video: true |
|
}) |
|
} else { |
|
if (!localStorage.getItem('_mixlab_webcamera_select')) return |
|
let constraints = |
|
JSON.parse(localStorage.getItem('_mixlab_webcamera_select')) || {} |
|
mediaStream = await navigator.mediaDevices.getUserMedia(constraints) |
|
} |
|
|
|
webcamVideo.removeEventListener('timeupdate', videoTimeUpdateHandler) |
|
webcamVideo.srcObject = mediaStream |
|
webcamVideo.onloadedmetadata = () => { |
|
let x = 0, |
|
y = 0, |
|
width = webcamVideo.videoWidth, |
|
height = webcamVideo.videoHeight, |
|
imgWidth = webcamVideo.videoWidth, |
|
imgHeight = webcamVideo.videoHeight |
|
|
|
let d = getSetAreaData() |
|
if ( |
|
d && |
|
d.x >= 0 && |
|
d.imgWidth === imgWidth && |
|
d.imgHeight === imgHeight |
|
) { |
|
x = d.x |
|
y = d.y |
|
width = d.width |
|
height = d.height |
|
imgWidth = d.imgWidth |
|
imgHeight = d.imgHeight |
|
console.log('#screen_share::使用上一次选区') |
|
} |
|
updateSetAreaData(x, y, width, height, imgWidth, imgHeight) |
|
|
|
webcamVideo.play() |
|
|
|
createBlobFromVideo(webcamVideo, true) |
|
|
|
webcamVideo.addEventListener('timeupdate', videoTimeUpdateHandler) |
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
mediaStream.addEventListener('inactive', handleStopSharing) |
|
|
|
|
|
function handleStopSharing () { |
|
|
|
|
|
if (window._mixlab_stopVideo) { |
|
window._mixlab_stopVideo() |
|
window._mixlab_stopVideo = null |
|
shareBtn.innerText = 'Share Screen' |
|
} |
|
if (window._mixlab_stopLive) { |
|
window._mixlab_stopLive() |
|
window._mixlab_stopLive = null |
|
liveBtn.innerText = 'Live Run' |
|
} |
|
return |
|
} |
|
|
|
window._mixlab_screen_webcamVideo = webcamVideo |
|
|
|
async function videoTimeUpdateHandler () { |
|
if (!window._mixlab_screen_live) return |
|
createBlobFromVideo(webcamVideo) |
|
} |
|
} catch (error) { |
|
alert('Error accessing screen stream: ' + error) |
|
} |
|
return () => { |
|
webcamVideo.pause() |
|
webcamVideo.srcObject.getTracks().forEach(track => { |
|
track.stop() |
|
}) |
|
webcamVideo.srcObject = null |
|
window._mixlab_screen_live = false |
|
window._mixlab_screen_blob = null |
|
previewArea.innerHTML = '' |
|
interrupt() |
|
} |
|
} |
|
|
|
async function sleep (t = 200) { |
|
return new Promise((res, rej) => { |
|
setTimeout(() => { |
|
res(true) |
|
}, t) |
|
}) |
|
} |
|
|
|
function createImage (url) { |
|
let im = new Image() |
|
return new Promise((res, rej) => { |
|
im.onload = () => res(im) |
|
im.src = url |
|
}) |
|
} |
|
|
|
async function compareImages (threshold, previousImage, currentImage) { |
|
|
|
var previousImg = await createImage(previousImage) |
|
var currentImg = await createImage(currentImage) |
|
|
|
if ( |
|
previousImg.naturalWidth != currentImg.naturalWidth || |
|
previousImg.naturalHeight != currentImg.naturalHeight |
|
) { |
|
return true |
|
} |
|
|
|
|
|
var canvas1 = document.createElement('canvas') |
|
canvas1.width = previousImg.naturalWidth |
|
canvas1.height = previousImg.naturalHeight |
|
var context1 = canvas1.getContext('2d') |
|
|
|
|
|
context1.drawImage(previousImg, 0, 0) |
|
|
|
|
|
var previousData = context1.getImageData( |
|
0, |
|
0, |
|
previousImg.naturalWidth, |
|
previousImg.naturalHeight |
|
).data |
|
|
|
var canvas2 = document.createElement('canvas') |
|
canvas2.width = currentImg.naturalWidth |
|
canvas2.height = currentImg.naturalHeight |
|
var context2 = canvas2.getContext('2d') |
|
context2.drawImage(currentImg, 0, 0) |
|
var currentData = context2.getImageData( |
|
0, |
|
0, |
|
currentImg.naturalWidth, |
|
currentImg.naturalHeight |
|
).data |
|
|
|
|
|
var pixelDiff = 0 |
|
for (var i = 0; i < previousData.length; i += 4) { |
|
var diffR = Math.abs(previousData[i] - currentData[i]) |
|
var diffG = Math.abs(previousData[i + 1] - currentData[i + 1]) |
|
var diffB = Math.abs(previousData[i + 2] - currentData[i + 2]) |
|
|
|
|
|
pixelDiff += diffR + diffG + diffB |
|
} |
|
|
|
|
|
var averageDiff = pixelDiff / (previousData.length / 4) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (averageDiff > threshold) { |
|
return true |
|
} else { |
|
return false |
|
} |
|
} |
|
|
|
async function startLive (btn) { |
|
if (btn) window._mixlab_screen_live = !window._mixlab_screen_live |
|
|
|
if (btn) btn.innerText = `Stop Live` |
|
|
|
|
|
|
|
|
|
|
|
const { Pending, Running } = await getQueue() |
|
|
|
if (Pending <= 1 && window._mixlab_screen_blob && Running === 0) { |
|
|
|
|
|
const threshold = 1 |
|
const previousImage = window._mixlab_screen_imagePath |
|
let currentImage = await blobToBase64(window._mixlab_screen_blob) |
|
|
|
if (previousImage) { |
|
|
|
const imageChanged = await compareImages( |
|
threshold, |
|
previousImage, |
|
currentImage |
|
) |
|
console.log('#图片是否有变化:', imageChanged) |
|
|
|
if (imageChanged) { |
|
window._mixlab_screen_imagePath = currentImage |
|
document.querySelector('#queue-button').click() |
|
} |
|
} else { |
|
window._mixlab_screen_imagePath = currentImage |
|
|
|
document.querySelector('#queue-button').click() |
|
} |
|
|
|
|
|
|
|
|
|
await sleep(window._mixlab_screen_refresh_rate || 200) |
|
|
|
} |
|
|
|
if (btn) { |
|
startLive() |
|
return () => { |
|
|
|
window._mixlab_screen_live = false |
|
window._mixlab_screen_blob = null |
|
interrupt() |
|
} |
|
} else if (window._mixlab_screen_live) { |
|
startLive() |
|
} |
|
} |
|
|
|
async function createBlobFromVideoForArea (webcamVideo) { |
|
const videoW = webcamVideo.videoWidth |
|
const videoH = webcamVideo.videoHeight |
|
const aspectRatio = videoW / videoH |
|
const WIDTH = videoW, |
|
HEIGHT = videoH |
|
const canvas = new OffscreenCanvas(WIDTH, HEIGHT) |
|
const ctx = canvas.getContext('2d') |
|
ctx.drawImage(webcamVideo, 0, 0, videoW, videoH, 0, 0, WIDTH, HEIGHT) |
|
|
|
const blob = await canvas.convertToBlob({ |
|
type: 'image/jpeg', |
|
quality: 1 |
|
}) |
|
|
|
return blob |
|
} |
|
|
|
async function createBlobFromVideo (webcamVideo, updateImageBase64 = false) { |
|
const videoW = webcamVideo.videoWidth |
|
const videoH = webcamVideo.videoHeight |
|
const aspectRatio = videoW / videoH |
|
|
|
const { x, y, width, height } = window._mixlab_share_screen |
|
|
|
const canvas = new OffscreenCanvas(width, height) |
|
const ctx = canvas.getContext('2d') |
|
|
|
ctx.drawImage(webcamVideo, x, y, width, height, 0, 0, width, height) |
|
|
|
const blob = await canvas.convertToBlob({ |
|
type: 'image/jpeg', |
|
quality: 1 |
|
}) |
|
|
|
window._mixlab_screen_blob = blob |
|
|
|
console.log( |
|
'########updateImageBase64 ', |
|
updateImageBase64, |
|
x, |
|
y, |
|
width, |
|
height |
|
) |
|
if (updateImageBase64) { |
|
window._mixlab_screen_imagePath = await blobToBase64(blob) |
|
} |
|
} |
|
|
|
async function blobToBase64 (blob) { |
|
const reader = new FileReader() |
|
return new Promise((res, rej) => { |
|
reader.onload = function (event) { |
|
res(event.target.result) |
|
} |
|
reader.readAsDataURL(blob) |
|
}) |
|
} |
|
function base64ToBlob (base64) { |
|
|
|
const parts = base64.split(';base64,') |
|
const type = parts[0].split(':')[1] |
|
const data = window.atob(parts[1]) |
|
const arrayBuffer = new ArrayBuffer(data.length) |
|
const uint8Array = new Uint8Array(arrayBuffer) |
|
|
|
|
|
for (let i = 0; i < data.length; i++) { |
|
uint8Array[i] = data.charCodeAt(i) |
|
} |
|
|
|
|
|
const blob = new Blob([arrayBuffer], { type }) |
|
|
|
return blob |
|
} |
|
|
|
async function requestCamera () { |
|
|
|
try { |
|
let stream = await navigator.mediaDevices.getUserMedia({ video: true }) |
|
console.log('摄像头授权成功') |
|
|
|
var videoTrack = stream.getVideoTracks()[0] |
|
|
|
|
|
videoTrack.stop() |
|
|
|
return true |
|
} catch (error) { |
|
|
|
console.error('摄像头授权失败:', error) |
|
|
|
|
|
if (error.name === 'NotAllowedError') { |
|
alert('请授权摄像头访问权限 chrome://settings/content/camera') |
|
} else { |
|
alert('摄像头访问权限请求失败,请重试 chrome://settings/content/camera') |
|
} |
|
|
|
|
|
|
|
} |
|
return false |
|
} |
|
|
|
|
|
|
|
|
|
function get_position_style (ctx, widget_width, y, node_height, top) { |
|
const MARGIN = 4 |
|
|
|
|
|
const elRect = ctx.canvas.getBoundingClientRect() |
|
const transform = new DOMMatrix() |
|
.scaleSelf( |
|
elRect.width / ctx.canvas.width, |
|
elRect.height / ctx.canvas.height |
|
) |
|
.multiplySelf(ctx.getTransform()) |
|
.translateSelf(MARGIN, MARGIN + y) |
|
|
|
return { |
|
transformOrigin: '0 0', |
|
transform: transform, |
|
left: `0`, |
|
top: `${top}px`, |
|
cursor: 'pointer', |
|
position: 'absolute', |
|
maxWidth: `${widget_width - MARGIN * 2}px`, |
|
|
|
width: `${widget_width - MARGIN * 2}px`, |
|
|
|
|
|
display: 'flex', |
|
flexDirection: 'column', |
|
|
|
justifyContent: 'space-around' |
|
} |
|
} |
|
|
|
const base64Df = |
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC' |
|
|
|
app.registerExtension({ |
|
name: 'Mixlab.image.ScreenShareNode', |
|
async getCustomWidgets (app) { |
|
|
|
return { |
|
CHEESE (node, inputName, inputData, app) { |
|
|
|
const widget = { |
|
type: inputData[0], |
|
name: inputName, |
|
size: [128, 12], |
|
draw (ctx, node, width, y) { |
|
|
|
}, |
|
computeSize (...args) { |
|
return [128, 12] |
|
}, |
|
async serializeValue (nodeId, widgetIndex) { |
|
return window._mixlab_screen_imagePath || base64Df |
|
} |
|
} |
|
|
|
node.addCustomWidget(widget) |
|
return widget |
|
}, |
|
PROMPT (node, inputName, inputData, app) { |
|
|
|
const widget = { |
|
type: inputData[0], |
|
name: inputName, |
|
size: [128, 12], |
|
draw (ctx, node, width, y) { |
|
|
|
}, |
|
computeSize (...args) { |
|
return [128, 12] |
|
}, |
|
async serializeValue (nodeId, widgetIndex) { |
|
return window._mixlab_screen_prompt || '' |
|
} |
|
} |
|
|
|
node.addCustomWidget(widget) |
|
return widget |
|
}, |
|
SLIDE (node, inputName, inputData, app) { |
|
|
|
const widget = { |
|
type: inputData[0], |
|
name: inputName, |
|
size: [128, 12], |
|
draw (ctx, node, width, y) { |
|
|
|
}, |
|
computeSize (...args) { |
|
return [128, 12] |
|
}, |
|
async serializeValue (nodeId, widgetIndex) { |
|
return window._mixlab_screen_slide_input || 0.5 |
|
} |
|
} |
|
|
|
node.addCustomWidget(widget) |
|
return widget |
|
}, |
|
SEED (node, inputName, inputData, app) { |
|
|
|
const widget = { |
|
type: inputData[0], |
|
name: inputName, |
|
size: [128, 12], |
|
draw (ctx, node, width, y) { |
|
|
|
}, |
|
computeSize (...args) { |
|
return [128, 12] |
|
}, |
|
async serializeValue (nodeId, widgetIndex) { |
|
return window._mixlab_screen_seed_input || 0 |
|
} |
|
} |
|
|
|
node.addCustomWidget(widget) |
|
return widget |
|
} |
|
} |
|
}, |
|
async beforeRegisterNodeDef (nodeType, nodeData, app) { |
|
if (nodeType.comfyClass == 'ScreenShare') { |
|
const orig_nodeCreated = nodeType.prototype.onNodeCreated |
|
nodeType.prototype.onNodeCreated = function () { |
|
orig_nodeCreated?.apply(this, arguments) |
|
|
|
const widget = { |
|
type: 'HTML', |
|
name: 'sreen_share', |
|
draw (ctx, node, widget_width, y, widget_height) { |
|
|
|
Object.assign( |
|
this.card.style, |
|
get_position_style( |
|
ctx, |
|
widget_width, |
|
widget_height * 5, |
|
node.size[1], |
|
40 |
|
) |
|
) |
|
} |
|
} |
|
|
|
widget.card = $el('div', { |
|
color: 'var(--descrip-text)', |
|
backgroundColor: 'var(--comfy-input-bg)' |
|
}) |
|
|
|
widget.previewCard = $el('div', { |
|
color: 'var(--descrip-text)', |
|
backgroundColor: 'var(--comfy-input-bg)' |
|
}) |
|
|
|
widget.preview = $el('video', { |
|
style: { |
|
width: '100%' |
|
}, |
|
controls: true, |
|
poster: |
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC' |
|
}) |
|
|
|
widget.previewArea = $el('div', { |
|
style: { |
|
color: 'var(--descrip-text)', |
|
backgroundColor: 'var(--comfy-input-bg)' |
|
} |
|
}) |
|
|
|
widget.shareDiv = $el('div', { |
|
|
|
style: { |
|
cursor: 'pointer', |
|
fontWeight: '300', |
|
display: 'flex', |
|
color: 'var(--descrip-text)', |
|
backgroundColor: 'var(--comfy-input-bg)' |
|
} |
|
}) |
|
|
|
widget.shareBtn = $el('button', { |
|
innerText: 'Share Screen', |
|
style: { |
|
cursor: 'pointer', |
|
padding: '8px 0', |
|
fontWeight: '300', |
|
margin: '2px', |
|
width: '100%', |
|
color: 'var(--descrip-text)', |
|
backgroundColor: 'var(--comfy-input-bg)', |
|
borderRadius: '8px', |
|
borderColor: 'var(--border-color)', |
|
borderStyle: 'solid' |
|
} |
|
}) |
|
|
|
widget.shareOfWebCamBtn = $el('button', { |
|
innerText: 'Camera', |
|
style: { |
|
cursor: 'pointer', |
|
padding: '8px 0', |
|
fontWeight: '300', |
|
margin: '2px', |
|
width: '100%', |
|
color: 'var(--descrip-text)', |
|
backgroundColor: 'var(--comfy-input-bg)', |
|
borderRadius: '8px', |
|
borderColor: 'var(--border-color)', |
|
borderStyle: 'solid' |
|
} |
|
}) |
|
|
|
widget.openFloatingWinBtn = $el('button', { |
|
innerText: 'Set Area', |
|
style: { |
|
cursor: 'pointer', |
|
padding: '8px 0', |
|
fontWeight: '300', |
|
margin: '2px', |
|
color: 'var(--descrip-text)', |
|
backgroundColor: 'var(--comfy-input-bg)', |
|
borderRadius: '8px', |
|
borderColor: 'var(--border-color)', |
|
borderStyle: 'solid' |
|
} |
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
widget.liveBtn = $el('button', { |
|
innerText: 'Live Run', |
|
style: { |
|
cursor: 'pointer', |
|
padding: '8px 0', |
|
fontWeight: '300', |
|
margin: '2px', |
|
color: 'var(--descrip-text)', |
|
backgroundColor: 'var(--comfy-input-bg)', |
|
borderRadius: '8px', |
|
borderColor: 'var(--border-color)', |
|
borderStyle: 'solid' |
|
} |
|
}) |
|
|
|
document.body.appendChild(widget.card) |
|
|
|
widget.card.appendChild(widget.previewCard) |
|
widget.previewCard.appendChild(widget.preview) |
|
widget.previewCard.appendChild(widget.previewArea) |
|
|
|
widget.card.appendChild(widget.shareDiv) |
|
widget.shareDiv.appendChild(widget.shareBtn) |
|
widget.shareDiv.appendChild(widget.shareOfWebCamBtn) |
|
widget.card.appendChild(widget.openFloatingWinBtn) |
|
|
|
widget.card.appendChild(widget.liveBtn) |
|
|
|
const toggleShare = async (isCamera = false) => { |
|
if (widget.preview.paused) { |
|
window._mixlab_stopVideo = await shareScreen( |
|
isCamera, |
|
widget.preview, |
|
widget.shareBtn, |
|
widget.liveBtn, |
|
widget.previewArea |
|
) |
|
|
|
if (isCamera) { |
|
widget.shareOfWebCamBtn.innerText = 'Stop Share' |
|
widget.shareBtn.innerText = 'Stop' |
|
} else { |
|
widget.shareOfWebCamBtn.innerText = 'Stop' |
|
widget.shareBtn.innerText = 'Stop Share' |
|
} |
|
|
|
console.log('视频已暂停') |
|
if (window._mixlab_stopLive) { |
|
window._mixlab_stopLive() |
|
window._mixlab_stopLive = null |
|
widget.liveBtn.innerText = 'Live Run' |
|
} |
|
|
|
setTimeout(() => updateSetAreaDisplay(), 2000) |
|
} else { |
|
console.log('视频正在播放') |
|
if (window._mixlab_stopVideo) { |
|
window._mixlab_stopVideo() |
|
window._mixlab_stopVideo = null |
|
widget.shareBtn.innerText = 'Share Screen' |
|
widget.shareOfWebCamBtn.innerText = 'Camera' |
|
} |
|
if (window._mixlab_stopLive) { |
|
window._mixlab_stopLive() |
|
window._mixlab_stopLive = null |
|
widget.liveBtn.innerText = 'Live Run' |
|
} |
|
} |
|
} |
|
|
|
|
|
widget.shareOfWebCamBtn.addEventListener('click', async () => { |
|
if (!widget.preview.paused) { |
|
if (window._mixlab_stopVideo) { |
|
window._mixlab_stopVideo() |
|
window._mixlab_stopVideo = null |
|
widget.shareBtn.innerText = 'Share Screen' |
|
widget.shareOfWebCamBtn.innerText = 'Camera' |
|
} |
|
if (window._mixlab_stopLive) { |
|
window._mixlab_stopLive() |
|
window._mixlab_stopLive = null |
|
widget.liveBtn.innerText = 'Live Run' |
|
} |
|
return |
|
} |
|
|
|
let r = await requestCamera() |
|
if (r === false) return |
|
const devices = await navigator.mediaDevices.enumerateDevices() |
|
|
|
|
|
var cameras = devices.filter(function (device) { |
|
|
|
return device.kind === 'videoinput' |
|
}) |
|
|
|
|
|
var select = document.createElement('select') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Array.from(cameras, (camera, i) => { |
|
var option = document.createElement('option') |
|
option.value = camera.deviceId |
|
option.text = camera.label || 'Camera ' + (select.length - 1) |
|
if (i === 0) option.selected = true |
|
select.appendChild(option) |
|
}) |
|
|
|
let modal = document.createElement('div') |
|
modal.className = 'comfy-modal' |
|
modal.style.display = 'flex' |
|
|
|
let modalContent = document.createElement('div') |
|
modalContent.className = 'comfy-modal-content' |
|
|
|
let title = document.createElement('p') |
|
title.innerText = 'Please select a camera' |
|
|
|
modalContent.appendChild(title) |
|
modalContent.appendChild(select) |
|
|
|
let btns = document.createElement('div') |
|
btns.style = `display: flex; |
|
justify-content: space-between; |
|
margin: 24px 0;` |
|
|
|
let btn = document.createElement('button') |
|
btn.innerText = 'OK' |
|
btn.style = `width: 112px;` |
|
|
|
let closeBtn = document.createElement('button') |
|
closeBtn.innerText = 'Cancel' |
|
closeBtn.style = `width: 112px;` |
|
|
|
modalContent.appendChild(btns) |
|
btns.appendChild(btn) |
|
btns.appendChild(closeBtn) |
|
|
|
modal.appendChild(modalContent) |
|
document.body.appendChild(modal) |
|
|
|
btn.addEventListener('click', () => { |
|
|
|
var selectedIndex = select.selectedIndex |
|
|
|
|
|
var selectedValue = select.options[selectedIndex].value |
|
if (selectedValue) { |
|
const constraints = { |
|
audio: false, |
|
video: { |
|
width: { ideal: 1920, max: 1920 }, |
|
height: { ideal: 1080, max: 1080 }, |
|
deviceId: selectedValue |
|
} |
|
} |
|
|
|
localStorage.setItem( |
|
'_mixlab_webcamera_select', |
|
JSON.stringify(constraints) |
|
) |
|
|
|
toggleShare(true) |
|
} |
|
|
|
modal.remove() |
|
}) |
|
|
|
closeBtn.addEventListener('click', () => { |
|
modal.remove() |
|
}) |
|
}) |
|
|
|
widget.shareBtn.addEventListener('click', async () => { |
|
toggleShare() |
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
widget.liveBtn.addEventListener('click', async () => { |
|
if (window._mixlab_stopLive) { |
|
window._mixlab_stopLive() |
|
window._mixlab_stopLive = null |
|
widget.liveBtn.innerText = 'Live Run' |
|
} else { |
|
window._mixlab_stopLive = await startLive(widget.liveBtn) |
|
console.log('window._mixlab_stopLive', window._mixlab_stopLive) |
|
} |
|
}) |
|
|
|
widget.openFloatingWinBtn.addEventListener('click', async () => { |
|
|
|
|
|
|
|
|
|
|
|
let blob = await createBlobFromVideoForArea( |
|
window._mixlab_screen_webcamVideo |
|
) |
|
|
|
setArea(await blobToBase64(blob)) |
|
}) |
|
|
|
|
|
this.setSize([this.size[0], this.size[1] + 450]) |
|
app.canvas.draw(true, true) |
|
|
|
|
|
|
|
|
|
this.addCustomWidget(widget) |
|
this.onRemoved = function () { |
|
widget.preview.remove() |
|
widget.shareDiv.remove() |
|
widget.shareOfWebCamBtn.remove() |
|
widget.shareBtn.remove() |
|
widget.liveBtn.remove() |
|
widget.card.remove() |
|
|
|
widget.previewArea.remove() |
|
widget.previewCard.remove() |
|
} |
|
this.serialize_widgets = true |
|
} |
|
|
|
const onExecuted = nodeType.prototype.onExecuted |
|
nodeType.prototype.onExecuted = function (message) { |
|
onExecuted?.apply(this, arguments) |
|
|
|
window._mixlab_screen_refresh_rate = Math.round( |
|
message.refresh_rate[0] || 500 |
|
) |
|
} |
|
} |
|
} |
|
}) |
|
|
|
function updateSetAreaDisplay () { |
|
try { |
|
let canvas = document.createElement('canvas') |
|
canvas.width = window._mixlab_screen_webcamVideo.videoWidth |
|
canvas.height = window._mixlab_screen_webcamVideo.videoHeight |
|
let ctx = canvas.getContext('2d') |
|
const lineWidth = 2 |
|
const strokeColor = 'red' |
|
|
|
|
|
ctx.strokeStyle = strokeColor |
|
ctx.lineWidth = lineWidth |
|
|
|
ctx.fillStyle = 'rgba(255,0,0,0.35)' |
|
|
|
let x = 0, |
|
y = 0, |
|
width = canvas.width, |
|
height = canvas.height |
|
|
|
if (!window._mixlab_share_screen) { |
|
let d = getSetAreaData() |
|
if (d) { |
|
window._mixlab_share_screen = d |
|
} |
|
} |
|
|
|
if (window._mixlab_share_screen) { |
|
x = window._mixlab_share_screen.x |
|
y = window._mixlab_share_screen.y |
|
width = window._mixlab_share_screen.width |
|
height = window._mixlab_share_screen.height |
|
} |
|
|
|
ctx.strokeRect(x, y, width, height) |
|
ctx.fillRect(x, y, width, height) |
|
|
|
canvas.style.width = '100%' |
|
|
|
let area = graph._nodes |
|
.filter(n => n.type === 'ScreenShare')[0] |
|
.widgets.filter(w => w.name == 'sreen_share')[0].previewArea |
|
|
|
area.innerHTML = '' |
|
area.appendChild(canvas) |
|
area.style = ` |
|
position: absolute; |
|
width:100%%; |
|
left:0; |
|
top:0; |
|
` |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
} |
|
|
|
function updateSetAreaData (left, top, width, height, imgWidth, imgHeight) { |
|
window._mixlab_share_screen = { |
|
x: left, |
|
y: top, |
|
width, |
|
height, |
|
imgWidth, |
|
imgHeight |
|
} |
|
localStorage.setItem( |
|
'_mixlab_share_screen', |
|
JSON.stringify(window._mixlab_share_screen) |
|
) |
|
} |
|
|
|
function getSetAreaData () { |
|
try { |
|
let data = JSON.parse(localStorage.getItem('_mixlab_share_screen')) || {} |
|
if (data.width === 0 || data.height === 0 || data.width === undefined) |
|
return |
|
return data |
|
} catch (error) {} |
|
return |
|
} |
|
|
|
async function setArea (src) { |
|
let displayHeight = Math.round(window.screen.availHeight * 0.6) |
|
let div = document.createElement('div') |
|
div.innerHTML = ` |
|
<div id='ml_overlay' style='position: absolute;top:0;background: #251f1fc4; |
|
height: 100vh; |
|
z-index:999999; |
|
width: 100%;'> |
|
<img id='ml_video' style='position: absolute; |
|
height: ${displayHeight}px;user-select: none; |
|
-webkit-user-drag: none; |
|
outline: 2px solid #eaeaea; |
|
box-shadow: 8px 9px 17px #575757;' /> |
|
<div id='ml_selection' style='position: absolute; |
|
border: 2px dashed red; |
|
pointer-events: none;'></div> |
|
</div>` |
|
|
|
document.body.appendChild(div) |
|
|
|
let im = await createImage(src) |
|
|
|
let img = div.querySelector('#ml_video') |
|
let overlay = div.querySelector('#ml_overlay') |
|
let selection = div.querySelector('#ml_selection') |
|
let startX, startY, endX, endY |
|
let start = false |
|
|
|
img.src = src |
|
|
|
|
|
const data = getSetAreaData() |
|
let x = 0, |
|
y = 0, |
|
width = (im.naturalWidth * displayHeight) / im.naturalHeight, |
|
height = displayHeight |
|
let imgWidth = im.naturalWidth |
|
let imgHeight = im.naturalHeight |
|
|
|
if ( |
|
data && |
|
data.width > 0 && |
|
data.height > 0 && |
|
data.imgWidth === imgWidth && |
|
data.imgHeight === imgHeight && |
|
data.imgHeight > 0 |
|
) { |
|
|
|
x = (img.width * data.x) / data.imgWidth |
|
y = (img.height * data.y) / data.imgHeight |
|
width = (img.width * data.width) / data.imgWidth |
|
height = (img.height * data.height) / data.imgHeight |
|
} |
|
|
|
selection.style.left = x + 'px' |
|
selection.style.top = y + 'px' |
|
selection.style.width = width + 'px' |
|
selection.style.height = height + 'px' |
|
|
|
|
|
img.addEventListener('mousedown', startSelection) |
|
img.addEventListener('mousemove', updateSelection) |
|
img.addEventListener('mouseup', endSelection) |
|
overlay.addEventListener('click', remove) |
|
|
|
function remove () { |
|
overlay.removeEventListener('click', remove) |
|
img.removeEventListener('mousedown', startSelection) |
|
img.removeEventListener('mousemove', updateSelection) |
|
img.removeEventListener('mouseup', endSelection) |
|
div.remove() |
|
} |
|
|
|
function startSelection (event) { |
|
if (start == false) { |
|
startX = event.clientX |
|
startY = event.clientY |
|
updateSelection(event) |
|
start = true |
|
} else { |
|
} |
|
} |
|
|
|
function updateSelection (event) { |
|
endX = event.clientX |
|
endY = event.clientY |
|
|
|
|
|
let width = Math.abs(endX - startX) |
|
let height = Math.abs(endY - startY) |
|
let left = Math.min(startX, endX) |
|
let top = Math.min(startY, endY) |
|
|
|
|
|
selection.style.left = left + 'px' |
|
selection.style.top = top + 'px' |
|
selection.style.width = width + 'px' |
|
selection.style.height = height + 'px' |
|
} |
|
|
|
function endSelection (event) { |
|
endX = event.clientX |
|
endY = event.clientY |
|
|
|
|
|
let imgWidth = img.naturalWidth |
|
let imgHeight = img.naturalHeight |
|
|
|
|
|
let realStartX = (startX / img.offsetWidth) * imgWidth |
|
let realStartY = (startY / img.offsetHeight) * imgHeight |
|
|
|
|
|
let realEndX = (endX / img.offsetWidth) * imgWidth |
|
let realEndY = (endY / img.offsetHeight) * imgHeight |
|
|
|
startX = realStartX |
|
startY = realStartY |
|
endX = realEndX |
|
endY = realEndY |
|
|
|
let width = Math.abs(endX - startX) |
|
let height = Math.abs(endY - startY) |
|
let left = Math.min(startX, endX) |
|
let top = Math.min(startY, endY) |
|
|
|
if (width <= 0 && height <= 0) return remove() |
|
|
|
updateSetAreaData(left, top, width, height, imgWidth, imgHeight) |
|
|
|
updateSetAreaDisplay() |
|
|
|
createBlobFromVideo( |
|
window._mixlab_screen_webcamVideo, |
|
!window._mixlab_screen_live |
|
) |
|
remove() |
|
} |
|
} |
|
|
|
async function save_workflow (json) { |
|
let api_host = `${window.location.hostname}:${window.location.port}` |
|
let api_base = '' |
|
let url = `${window.location.protocol}//${api_host}${api_base}` |
|
|
|
const res = await fetch(`${url}/mixlab/workflow`, { |
|
method: 'POST', |
|
body: JSON.stringify({ |
|
data: json, |
|
task: 'save' |
|
}) |
|
}) |
|
return await res.json() |
|
} |
|
|
|
async function get_my_workflow () { |
|
let api_host = `${window.location.hostname}:${window.location.port}` |
|
let api_base = '' |
|
let url = `${window.location.protocol}//${api_host}${api_base}` |
|
|
|
const res = await fetch(`${url}/mixlab/workflow`, { |
|
method: 'POST', |
|
body: JSON.stringify({ |
|
task: 'list' |
|
}) |
|
}) |
|
let result = await res.json() |
|
return result.data |
|
} |
|
|
|
app.registerExtension({ |
|
name: 'Mixlab.image.FloatingVideo', |
|
async beforeRegisterNodeDef (nodeType, nodeData, app) { |
|
if (nodeType.comfyClass == 'FloatingVideo') { |
|
const orig_nodeCreated = nodeType.prototype.onNodeCreated |
|
nodeType.prototype.onNodeCreated = function () { |
|
orig_nodeCreated?.apply(this, arguments) |
|
|
|
const widget = { |
|
type: 'video', |
|
name: 'FloatingVideo', |
|
draw (ctx, node, widget_width, y, widget_height) { |
|
Object.assign( |
|
this.card.style, |
|
get_position_style(ctx, widget_width, y, node.size[1], 0) |
|
) |
|
} |
|
} |
|
|
|
widget.card = $el('div', {}) |
|
|
|
widget.preview = $el('video', { |
|
controls: true, |
|
draggable: true, |
|
style: { |
|
width: '100%' |
|
}, |
|
poster: |
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAAXNSR0IArs4c6QAAALZJREFUKFOFkLERwjAQBPdbgBkInECGaMLUQDsE0AkRVRAYWqAByxldPPOWHwnw4OBGye1p50UDSoA+W2ABLPN7i+C5dyC6R/uiAUXRQCs0bXoNIu4QPQzAxDKxHoALOrZcqtiyR/T6CXw7+3IGHhkYcy6BOR2izwT8LptG8rbMiCRAUb+CQ6WzQVb0SNOi5Z2/nX35DRyb/ENazhpWKoGwrpD6nICp5c2qogc4of+c7QcrhgF4Aa/aoAFHiL+RAAAAAElFTkSuQmCC' |
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
widget.canvas = $el('canvas', { |
|
style: { |
|
display: 'none' |
|
} |
|
}) |
|
|
|
widget.PictureInPicture = $el('button', { |
|
innerText: 'Picture In Picture', |
|
style: { |
|
display: 'pictureInPictureEnabled' in document ? 'block' : 'none', |
|
cursor: 'pointer', |
|
padding: '8px 0', |
|
fontWeight: '300', |
|
margin: '2px', |
|
color: 'var(--descrip-text)', |
|
backgroundColor: 'var(--comfy-input-bg)', |
|
borderRadius: '8px', |
|
borderColor: 'var(--border-color)', |
|
borderStyle: 'solid' |
|
} |
|
}) |
|
|
|
document.body.appendChild(widget.card) |
|
widget.card.appendChild(widget.PictureInPicture) |
|
widget.card.appendChild(widget.preview) |
|
widget.card.appendChild(widget.canvas) |
|
|
|
widget.preview.addEventListener('click', event => { |
|
const imageUrl = window._mixlab_screen_result || '' |
|
|
|
try { |
|
if (imageUrl) clipboardWriteImage(pipWindow, imageUrl) |
|
} catch (error) { |
|
console.log(error) |
|
if (imageUrl) clipboardWriteImage(window, imageUrl) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
widget.PictureInPicture.addEventListener('click', async () => { |
|
if (window.location.protocol != 'https:') { |
|
let http_workflow = app.graph.serialize() |
|
await save_workflow(http_workflow) |
|
|
|
window.alert( |
|
`Redirecting to HTTPS access due to the requirement of the floating window. https://${ |
|
window.location.hostname |
|
}:${~~window.location.port + 1}` |
|
) |
|
window.open( |
|
`https://${window.location.hostname}:${ |
|
~~window.location.port + 1 |
|
}?workflow=1` |
|
) |
|
} |
|
|
|
let w = 360, |
|
s = widget.preview.videoWidth / widget.preview.videoHeight, |
|
h = w / s || w |
|
|
|
|
|
if (!window.documentPictureInPicture) { |
|
window.alert( |
|
'This feature is available only in secure contexts (HTTPS), in some or all supporting browsers. https://developer.mozilla.org/en-US/docs/Web/API/Document_Picture-in-Picture_API' |
|
) |
|
} |
|
|
|
const pipWindow = await documentPictureInPicture.requestWindow({ |
|
width: w, |
|
height: Math.round(h) + 120 |
|
}) |
|
|
|
pipWindow.document.body.style = `margin: 0px; |
|
overflow: hidden; |
|
background: #2a2c34; |
|
border: 4px solid #878787; |
|
outline: none;background: black;` |
|
|
|
let div = document.createElement('div') |
|
div.style = `display:flex;position: fixed;flex-direction: column; |
|
bottom: 0px; |
|
z-index: 9999; |
|
left: 0px; |
|
width: calc(100% - 24px); |
|
margin: 12px;` |
|
|
|
let inputDiv = document.createElement('div') |
|
inputDiv.style = `width: 100%;` |
|
|
|
|
|
let infoDiv = document.createElement('div') |
|
infoDiv.style = `width: 100%; |
|
display: flex; |
|
justify-content: space-between; |
|
height: 16px; |
|
color: white; |
|
margin-bottom: 4px; |
|
font-size: 12px; |
|
text-shadow: gray 1px 1px; |
|
align-items: center;` |
|
|
|
let infoText = document.createElement('div') |
|
infoText.id = 'info' |
|
|
|
let hideBtn = document.createElement('button') |
|
hideBtn.innerText = '🤖' |
|
hideBtn.style = ` |
|
border: none; |
|
background: none; |
|
cursor: pointer; height: 24px; margin: 4px; color: red;` |
|
|
|
hideBtn.addEventListener('click', () => { |
|
if (fnDiv.style.display == 'none') { |
|
fnDiv.style.display = 'flex' |
|
} else { |
|
fnDiv.style.display = 'none' |
|
} |
|
try { |
|
pipWindow.document.querySelector('#info').innerText = '' |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
}) |
|
|
|
|
|
let input = document.createElement('textarea') |
|
input.style = ` |
|
min-width:90%; |
|
max-width:100%; |
|
background: #24283db3; |
|
color: white; |
|
font-size: 14px; |
|
padding: 8px; |
|
font-weight: 300; |
|
letter-spacing: 1px; |
|
outline: none; |
|
min-height: 98px; |
|
border-radius: 8px; |
|
border: 1px solid rgb(91, 91, 91); |
|
font-family: sans-serif; |
|
` |
|
|
|
const style = document.createElement('style') |
|
|
|
const cssRule = `::-webkit-scrollbar { width: 2px;}` |
|
|
|
style.appendChild(document.createTextNode(cssRule)) |
|
|
|
|
|
pipWindow.document.head.appendChild(style) |
|
|
|
window._mixlab_screen_prompt = |
|
window._mixlab_screen_prompt || |
|
'beautiful scenery nature glass bottle landscape,under water' |
|
input.value = window._mixlab_screen_prompt |
|
|
|
let btnDiv = document.createElement('div') |
|
|
|
btnDiv.style = `cursor: pointer; |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: start; |
|
align-items: center; |
|
width: 24px; |
|
font-size: 16px; |
|
margin-right: 6px;user-select: none;` |
|
|
|
let seedBtn = document.createElement('butotn') |
|
seedBtn.innerText = '🎲' |
|
seedBtn.style = `cursor: pointer;height: 24px;margin:4px; |
|
color: red;` |
|
|
|
seedBtn.addEventListener('click', () => { |
|
window._mixlab_screen_seed_input = Math.round( |
|
Math.floor(Math.random() * 0xffffffffffffffff) |
|
) |
|
|
|
try { |
|
pipWindow.document.querySelector('#info').innerText = |
|
window._mixlab_screen_seed_input |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
|
|
|
|
if (window._mixlab_screen_imagePath) |
|
document.querySelector('#queue-button').click() |
|
}) |
|
|
|
|
|
let pauseBtn = document.createElement('butotn') |
|
pauseBtn.innerText = '⏸' |
|
pauseBtn.style = `cursor: pointer;height: 24px;margin:4px; |
|
color: #03A9F4;` |
|
|
|
pauseBtn.addEventListener('click', async () => { |
|
if (window._mixlab_stopLive) { |
|
pauseBtn.innerText = '▶' |
|
|
|
window._mixlab_stopLive() |
|
window._mixlab_stopLive = null |
|
|
|
let node = this.graph._nodes.filter( |
|
n => n.type === 'ScreenShare' |
|
)[0] |
|
|
|
var w = node.widgets?.filter(w => w.name === 'sreen_share')[0] |
|
if (w) { |
|
w.liveBtn.innerText = 'Live Run' |
|
} |
|
|
|
try { |
|
pipWindow.document.querySelector('#info').innerText = |
|
'Stop Live' |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
} else { |
|
pauseBtn.innerText = '⏸' |
|
let node = this.graph._nodes.filter( |
|
n => n.type === 'ScreenShare' |
|
)[0] |
|
var w = node.widgets?.filter(w => w.name === 'sreen_share')[0] |
|
if (w) { |
|
w.liveBtn.innerText = 'Stop Live' |
|
window._mixlab_stopLive = await startLive(w.liveBtn) |
|
console.log('window._mixlab_stopLive', window._mixlab_stopLive) |
|
} |
|
|
|
try { |
|
pipWindow.document.querySelector('#info').innerText = 'Live' |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
} |
|
}) |
|
|
|
let promptFinishBtn = document.createElement('butotn') |
|
promptFinishBtn.innerText = '🚀' |
|
promptFinishBtn.style = `cursor: pointer;height: 24px;margin:4px;` |
|
promptFinishBtn.addEventListener('click', () => { |
|
console.log('##更新Prompt') |
|
window._mixlab_screen_prompt = |
|
window._mixlab_screen_prompt_input || window._mixlab_screen_prompt |
|
|
|
if (window._mixlab_screen_imagePath) |
|
document.querySelector('#queue-button').click() |
|
|
|
try { |
|
pipWindow.document.querySelector('#info').innerText = |
|
'Update Prompt' |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
}) |
|
|
|
widget.preview.addEventListener('click', event => { |
|
const imageUrl = window._mixlab_screen_result || '' |
|
|
|
try { |
|
if (imageUrl) clipboardWriteImage(pipWindow, imageUrl) |
|
} catch (error) { |
|
console.log(error) |
|
if (imageUrl) clipboardWriteImage(window, imageUrl) |
|
} |
|
|
|
try { |
|
pipWindow.document.querySelector('#info').innerText = |
|
'Image copied to clipboard' |
|
setTimeout( |
|
() => |
|
(pipWindow.document.querySelector('#info').innerText = ''), |
|
8000 |
|
) |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}) |
|
|
|
pipWindow.document.body.append(widget.preview) |
|
pipWindow.document.body.append(div) |
|
|
|
|
|
const createSlide = () => { |
|
let d = document.createElement('div') |
|
d.style = `width: 100%;margin-bottom: 12px;` |
|
let range = document.createElement('input') |
|
range.type = 'range' |
|
d.appendChild(range) |
|
return range |
|
} |
|
|
|
let slideInp = createSlide() |
|
slideInp.addEventListener('change', () => { |
|
console.log(~~slideInp.value / 100) |
|
window._mixlab_screen_slide_input = ~~slideInp.value / 100 |
|
try { |
|
pipWindow.document.querySelector('#info').innerText = |
|
window._mixlab_screen_slide_input |
|
if (window._mixlab_screen_imagePath) |
|
document.querySelector('#queue-button').click() |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
}) |
|
|
|
|
|
|
|
let fnDiv = document.createElement('div') |
|
fnDiv.style = `display: flex;` |
|
|
|
div.appendChild(infoDiv) |
|
div.appendChild(fnDiv) |
|
|
|
infoDiv.appendChild(infoText) |
|
infoDiv.appendChild(hideBtn) |
|
|
|
fnDiv.appendChild(btnDiv) |
|
|
|
btnDiv.appendChild(seedBtn) |
|
btnDiv.appendChild(pauseBtn) |
|
btnDiv.appendChild(promptFinishBtn) |
|
|
|
|
|
fnDiv.appendChild(inputDiv) |
|
inputDiv.appendChild(slideInp) |
|
|
|
inputDiv.appendChild(input) |
|
|
|
input.addEventListener('input', () => { |
|
window._mixlab_screen_prompt_input = input.value |
|
try { |
|
pipWindow.document.querySelector('#info').innerText = '' |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
}) |
|
|
|
input.addEventListener('keydown', handleKeyDown) |
|
|
|
function handleKeyDown (event) { |
|
if (event.key === 'Enter') { |
|
if (!event.shiftKey) { |
|
|
|
event.preventDefault() |
|
|
|
console.log('##更新Prompt') |
|
window._mixlab_screen_prompt = |
|
window._mixlab_screen_prompt_input || |
|
window._mixlab_screen_prompt |
|
|
|
if (window._mixlab_screen_imagePath) |
|
document.querySelector('#queue-button').click() |
|
|
|
try { |
|
pipWindow.document.querySelector('#info').innerText = |
|
'Update Prompt' |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
pipWindow.addEventListener('pagehide', event => { |
|
widget.card.appendChild(widget.preview) |
|
|
|
pipWindow.close() |
|
}) |
|
}) |
|
|
|
this.addCustomWidget(widget) |
|
this.onRemoved = function () { |
|
widget.preview.remove() |
|
widget.canvas.remove() |
|
widget.card.remove() |
|
widget.PictureInPicture.remove() |
|
} |
|
this.serialize_widgets = true |
|
} |
|
|
|
const onExecuted = nodeType.prototype.onExecuted |
|
nodeType.prototype.onExecuted = function (message) { |
|
const r = onExecuted ? onExecuted.apply(this, message) : undefined |
|
|
|
if (this.widgets) { |
|
const video = this.widgets.filter(w => w.type === `video`)[0] |
|
const canvas = video.canvas |
|
|
|
if (video.preview.paused) { |
|
const stream = canvas.captureStream() |
|
const videoTrack = stream.getVideoTracks()[0] |
|
|
|
video.preview.srcObject = new MediaStream([videoTrack]) |
|
try { |
|
video.preview.play() |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
|
|
|
|
if ('pictureInPictureEnabled' in document) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} else { |
|
|
|
console.error('浏览器不支持画中画模式') |
|
} |
|
} |
|
|
|
const context = canvas.getContext('2d') |
|
|
|
if (message?.images_) { |
|
window._mixlab_screen_result = `data:image/png;base64,${message.images_[0]}` |
|
const image = new Image() |
|
image.onload = function () { |
|
canvas.width = image.width |
|
canvas.height = image.height |
|
context.drawImage(image, 0, 0) |
|
} |
|
|
|
image.src = window._mixlab_screen_result |
|
} |
|
const onRemoved = this.onRemoved |
|
this.onRemoved = () => { |
|
|
|
return onRemoved?.() |
|
} |
|
} |
|
this.setSize([ |
|
this.size[0], |
|
this.computeSize([this.size[0], this.size[1]])[1] |
|
]) |
|
return r |
|
} |
|
} |
|
} |
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function run (mutable_prompt, immutable_prompt) { |
|
|
|
const words1 = mutable_prompt.split('\n') |
|
|
|
|
|
const words2 = immutable_prompt.split('\n') |
|
|
|
const prompts = [] |
|
for (let i = 0; i < words1.length; i++) { |
|
words1[i] = words1[i].trim() |
|
for (let j = 0; j < words2.length; j++) { |
|
words2[j] = words2[j].trim() |
|
if (words2[j] && words1[i]) { |
|
prompts.push(words2[j].replaceAll('``', words1[i])) |
|
} |
|
} |
|
} |
|
|
|
return prompts |
|
} |
|
|
|
|
|
const updateUI = node => { |
|
const mutable_prompt_w = node.widgets.filter( |
|
w => w.name === 'mutable_prompt' |
|
)[0] |
|
mutable_prompt_w.inputEl.title = 'Enter keywords, one per line' |
|
const immutable_prompt_w = node.widgets.filter( |
|
w => w.name === 'immutable_prompt' |
|
)[0] |
|
immutable_prompt_w.inputEl.title = |
|
'Enter prompts, one per line, variables represented by ``' |
|
|
|
const max_count = node.widgets.filter(w => w.name === 'max_count')[0] |
|
let prompts = run(mutable_prompt_w.value, immutable_prompt_w.value) |
|
|
|
prompts = prompts.slice(0, max_count.value) |
|
|
|
max_count.value = prompts.length |
|
|
|
|
|
const pw = node.widgets.filter(w => w.name === 'prompts')[0] |
|
if (pw) { |
|
|
|
pw.value = prompts.join('\n\n') |
|
pw.inputEl.title = `Total of ${prompts.length} prompts` |
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const w = ComfyWidgets.STRING( |
|
node, |
|
'prompts', |
|
['STRING', { multiline: true }], |
|
app |
|
).widget |
|
w.inputEl.readOnly = true |
|
w.inputEl.style.opacity = 0.6 |
|
w.value = prompts.join('\n\n') |
|
w.inputEl.title = `Total of ${prompts.length} prompts` |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
node.widgets.length = 5 |
|
node.onResize?.(node.size) |
|
} |
|
|
|
const exportGraph = () => { |
|
const graph = app.graph |
|
|
|
var clipboard_info = { |
|
nodes: [], |
|
links: [] |
|
} |
|
var index = 0 |
|
var selected_nodes_array = [] |
|
for (var i in graph._nodes_in_order) { |
|
var node = graph._nodes_in_order[i] |
|
if (node.clonable === false) continue |
|
node._relative_id = index |
|
selected_nodes_array.push(node) |
|
index += 1 |
|
} |
|
|
|
for (var i = 0; i < selected_nodes_array.length; ++i) { |
|
var node = selected_nodes_array[i] |
|
var cloned = node.clone() |
|
if (!cloned) { |
|
console.warn('node type not found: ' + node.type) |
|
continue |
|
} |
|
|
|
let nc = {} |
|
let n = cloned.serialize() |
|
for (const key in n) { |
|
if ( |
|
[ |
|
'type', |
|
'pos', |
|
'size', |
|
'flags', |
|
'order', |
|
'mode', |
|
'inputs', |
|
'outputs', |
|
'properties', |
|
'widgets_values' |
|
].includes(key) |
|
) { |
|
nc[key] = n[key] |
|
} |
|
} |
|
|
|
clipboard_info.nodes.push(nc) |
|
|
|
if (node.inputs && node.inputs.length) { |
|
for (var j = 0; j < node.inputs.length; ++j) { |
|
var input = node.inputs[j] |
|
if (!input || input.link == null) { |
|
continue |
|
} |
|
var link_info = graph.links[input.link] |
|
if (!link_info) { |
|
continue |
|
} |
|
var target_node = graph.getNodeById(link_info.origin_id) |
|
if (!target_node) { |
|
continue |
|
} |
|
clipboard_info.links.push([ |
|
target_node._relative_id, |
|
link_info.origin_slot, |
|
node._relative_id, |
|
link_info.target_slot, |
|
target_node.id |
|
]) |
|
} |
|
} |
|
} |
|
localStorage.setItem('_Mixlab_clipboard', JSON.stringify(clipboard_info)) |
|
|
|
return clipboard_info |
|
} |
|
|
|
const my = { |
|
nodes: [ |
|
{ |
|
type: 'LoadImage', |
|
pos: [719.5130480797907, 172.9437092123179], |
|
size: { 0: 315, 1: 314 }, |
|
flags: {}, |
|
order: 0, |
|
mode: 0, |
|
outputs: [ |
|
{ name: 'IMAGE', type: 'IMAGE', links: [], shape: 3 }, |
|
{ name: 'MASK', type: 'MASK', links: null, shape: 3 } |
|
], |
|
properties: { 'Node name for S&R': 'LoadImage' }, |
|
widgets_values: ['00204211b3c71288c12ed66516a1a20a.jpg', 'image'] |
|
}, |
|
{ |
|
type: 'ControlNetLoader', |
|
pos: [1199.5130480797907, -331.0562907876821], |
|
size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
|
flags: {}, |
|
order: 1, |
|
mode: 0, |
|
outputs: [ |
|
{ name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
|
], |
|
properties: { 'Node name for S&R': 'ControlNetLoader' }, |
|
widgets_values: ['control_v11p_sd15_canny.pth'] |
|
}, |
|
{ |
|
type: 'ControlNetLoader', |
|
pos: [1204.5130480797907, -169.0562907876821], |
|
size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
|
flags: {}, |
|
order: 2, |
|
mode: 0, |
|
outputs: [ |
|
{ name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
|
], |
|
properties: { 'Node name for S&R': 'ControlNetLoader' }, |
|
widgets_values: ['control_v11f1p_sd15_depth.pth'] |
|
}, |
|
{ |
|
type: 'ControlNetLoader', |
|
pos: [1206.5130480797907, -20.056290787682087], |
|
size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
|
flags: {}, |
|
order: 3, |
|
mode: 0, |
|
outputs: [ |
|
{ name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
|
], |
|
properties: { 'Node name for S&R': 'ControlNetLoader' }, |
|
widgets_values: ['t2iadapter_seg-fp16.safetensors'] |
|
}, |
|
{ |
|
type: 'ControlNetLoader', |
|
pos: [1209.5130480797907, 125.94370921231791], |
|
size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
|
flags: {}, |
|
order: 4, |
|
mode: 0, |
|
outputs: [ |
|
{ name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
|
], |
|
properties: { 'Node name for S&R': 'ControlNetLoader' }, |
|
widgets_values: ['control_v11p_sd15_openpose.pth'] |
|
}, |
|
{ |
|
type: 'ControlNetLoader', |
|
pos: [1214.5130480797907, 293.9437092123179], |
|
size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
|
flags: {}, |
|
order: 5, |
|
mode: 0, |
|
outputs: [ |
|
{ name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
|
], |
|
properties: { 'Node name for S&R': 'ControlNetLoader' }, |
|
widgets_values: ['control_v11e_sd15_ip2p.safetensors'] |
|
}, |
|
{ |
|
type: 'ControlNetLoader', |
|
pos: [1212.5130480797907, 461.9437092123179], |
|
size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
|
flags: {}, |
|
order: 6, |
|
mode: 0, |
|
outputs: [ |
|
{ name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
|
], |
|
properties: { 'Node name for S&R': 'ControlNetLoader' }, |
|
widgets_values: ['control_v11p_sd15_inpaint.pth'] |
|
}, |
|
{ |
|
type: 'ControlNetLoader', |
|
pos: [1216.5130480797907, 636.9437092123179], |
|
size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
|
flags: {}, |
|
order: 7, |
|
mode: 0, |
|
outputs: [ |
|
{ name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
|
], |
|
properties: { 'Node name for S&R': 'ControlNetLoader' }, |
|
widgets_values: ['control_v11f1e_sd15_tile.bin'] |
|
}, |
|
{ |
|
type: 'ControlNetLoader', |
|
pos: [1227.5130480797907, 804.9437092123179], |
|
size: { 0: 415.221923828125, 1: 58.84859848022461 }, |
|
flags: {}, |
|
order: 8, |
|
mode: 0, |
|
outputs: [ |
|
{ name: 'CONTROL_NET', type: 'CONTROL_NET', links: [], shape: 3 } |
|
], |
|
properties: { 'Node name for S&R': 'ControlNetLoader' }, |
|
widgets_values: ['control_v11f1e_sd15_tile.bin'] |
|
}, |
|
{ |
|
type: 'ControlNetApplyAdvanced', |
|
pos: [1816, 94], |
|
size: { 0: 315, 1: 166 }, |
|
flags: {}, |
|
order: 9, |
|
mode: 0, |
|
inputs: [ |
|
{ name: 'positive', type: 'CONDITIONING', link: null }, |
|
{ name: 'negative', type: 'CONDITIONING', link: null }, |
|
{ name: 'control_net', type: 'CONTROL_NET', link: null, slot_index: 2 }, |
|
{ name: 'image', type: 'IMAGE', link: null, slot_index: 3 } |
|
], |
|
outputs: [ |
|
{ name: 'positive', type: 'CONDITIONING', links: null, shape: 3 }, |
|
{ name: 'negative', type: 'CONDITIONING', links: null, shape: 3 } |
|
], |
|
properties: { 'Node name for S&R': 'ControlNetApplyAdvanced' }, |
|
widgets_values: [1, 0, 1] |
|
} |
|
], |
|
links: [ |
|
[1, 0, 9, 2, 2], |
|
[0, 0, 9, 3, 1] |
|
] |
|
} |
|
|
|
|
|
const importWorkflow = my => { |
|
localStorage.setItem('litegrapheditor_clipboard', JSON.stringify(my)) |
|
app.canvas.pasteFromClipboard() |
|
} |
|
|
|
function getURLParameters (url) { |
|
var params = {} |
|
var paramStr = url.split('?')[1] |
|
if (paramStr) { |
|
var paramArr = paramStr.split('&') |
|
for (var i = 0; i < paramArr.length; i++) { |
|
var param = paramArr[i].split('=') |
|
var paramName = decodeURIComponent(param[0]) |
|
var paramValue = decodeURIComponent(param[1] || '') |
|
if (paramName) { |
|
if (params[paramName]) { |
|
params[paramName] = Array.isArray(params[paramName]) |
|
? [...params[paramName], paramValue] |
|
: [params[paramName], paramValue] |
|
} else { |
|
params[paramName] = paramValue |
|
} |
|
} |
|
} |
|
} |
|
return params |
|
} |
|
|
|
|
|
|
|
|
|
|
|
const node = { |
|
name: 'RandomPrompt', |
|
async init (app) { |
|
|
|
|
|
|
|
if (window.location.href.match('/?')) { |
|
const { workflow } = getURLParameters(window.location.href) |
|
if (workflow) |
|
get_my_workflow().then(data => { |
|
|
|
let my_workflow = data.filter( |
|
d => d.filename == 'my_workflow.json' |
|
)[0] |
|
if (my_workflow?.data) { |
|
|
|
localStorage.setItem('workflow', JSON.stringify(my_workflow.data)) |
|
} |
|
}) |
|
} |
|
}, |
|
async setup (a) { |
|
for (const node of app.graph._nodes) { |
|
|
|
if (node.type === 'RandomPrompt') { |
|
updateUI(node) |
|
} |
|
} |
|
|
|
}, |
|
addCustomNodeDefs (defs, app) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}, |
|
loadedGraphNode (node, app) { |
|
if (node.type === 'RandomPrompt') { |
|
try { |
|
let max_count = node.widgets.filter(w => w.name === 'max_count')[0] |
|
max_count.value = node.widgets_values[0] |
|
|
|
} catch (error) { |
|
console.log(error) |
|
} |
|
} |
|
}, |
|
async nodeCreated (node) { |
|
if (node.type === 'RandomPrompt') { |
|
updateUI(node) |
|
} |
|
|
|
if (node.type === 'RunWorkflow') { |
|
const pw = node.widgets.filter(w => w.name === 'workflow')[0] |
|
console.log('nodeCreated', pw) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
node.onResize?.(node.size) |
|
} |
|
}, |
|
async beforeRegisterNodeDef (nodeType, nodeData, app) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (nodeData.name === 'WSServer') { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
if (nodeData.name === 'RandomPrompt') { |
|
const onExecuted = nodeType.prototype.onExecuted |
|
nodeType.prototype.onExecuted = function (message) { |
|
const r = onExecuted?.apply?.(this, arguments) |
|
|
|
let prompts = message.prompts |
|
|
|
|
|
const pw = this.widgets.filter(w => w.name === 'prompts')[0] |
|
|
|
if (pw) { |
|
|
|
pw.value = prompts.join('\n\n') |
|
pw.inputEl.title = `Total of ${prompts.length} prompts` |
|
} else { |
|
|
|
const w = ComfyWidgets.STRING( |
|
this, |
|
'prompts', |
|
['STRING', { multiline: true }], |
|
app |
|
).widget |
|
w.inputEl.readOnly = true |
|
w.inputEl.style.opacity = 0.6 |
|
w.value = prompts.join('\n\n') |
|
w.inputEl.title = `Total of ${prompts.length} prompts` |
|
} |
|
|
|
this.widgets.length = 5 |
|
|
|
this.onResize?.(this.size) |
|
|
|
return r |
|
} |
|
} |
|
} |
|
} |
|
|
|
app.registerExtension(node) |
|
|