import { app } from '../../../scripts/app.js' // import { api } from '../../../scripts/api.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() // console.log(data.queue_running,data.queue_pending) return { // Running action uses a different endpoint for cancelling 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') // console.log(url) const img = await createImage(url) // console.log(img) canvas.width = img.naturalWidth canvas.height = img.naturalHeight ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.drawImage(img, 0, 0) // 将canvas转为blob 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) } } // function switchCamera (deviceId = 'desktop') { // const constraints = { // audio: false, // video: { // width: { ideal: 1920, max: 1920 }, // height: { ideal: 1080, max: 1080 }, // deviceId: mediaDevices[webcamsEl.value].deviceId // } // } // console.log('switchCamera', constraints) // let mediaStreamPro // if (deviceId === 'desktop') { // mediaStreamPro = navigator.mediaDevices.getDisplayMedia(constraints) // } else { // mediaStreamPro = navigator.mediaDevices.getUserMedia(constraints) // } // return mediaStreamPro // } // alert(navigator.mediaDevices) 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) // window._mixlab_screen_x = 0 // window._mixlab_screen_y = 0 // // console.log(webcamVideo) // window._mixlab_screen_width = webcamVideo.videoWidth // window._mixlab_screen_height = webcamVideo.videoHeight } mediaStream.addEventListener('inactive', handleStopSharing) // 停止共享的回调函数 function handleStopSharing () { // console.log('用户停止了共享') // 执行其他操作 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) { // 将 base64 转换为 Image 对象 var previousImg = await createImage(previousImage) var currentImg = await createImage(currentImage) if ( previousImg.naturalWidth != currentImg.naturalWidth || previousImg.naturalHeight != currentImg.naturalHeight ) { return true // 图片有变化 } // 创建一个 canvas 元素 var canvas1 = document.createElement('canvas') canvas1.width = previousImg.naturalWidth canvas1.height = previousImg.naturalHeight var context1 = canvas1.getContext('2d') // 将图片绘制到 canvas 上 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) // 判断平均像素差异是否超过阈值 // console.log( // pixelDiff, // averageDiff, // threshold, // currentImg.naturalWidth, // currentImg.naturalHeight,previousData,currentData // ) 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` // console.log('#ML', 'live run', window._mixlab_screen_time) // if (window._mixlab_screen_time) { // // console.log('#ML', 'live') // return // } const { Pending, Running } = await getQueue() // console.log('#ML', Pending, window._mixlab_screen_blob) if (Pending <= 1 && window._mixlab_screen_blob && Running === 0) { // window._mixlab_screen_time = true const threshold = 1 // 阈值 const previousImage = window._mixlab_screen_imagePath // 上一张图片的 base64 let currentImage = await blobToBase64(window._mixlab_screen_blob) if (previousImage) { // 现在新的图片的 base64 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 // console.log(window._mixlab_screen_imagePath) document.querySelector('#queue-button').click() } // await uploadFile(file) // window._mixlab_screen_time = false await sleep(window._mixlab_screen_refresh_rate || 200) // console.log('#ML', window._mixlab_screen_imagePath) } if (btn) { startLive() return () => { // stop 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 }) // imgElement.src = await blobToBase64(blob) 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) { // 将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) // 将Base64数据转换为Uint8Array for (let i = 0; i < data.length; i++) { uint8Array[i] = data.charCodeAt(i) } // 创建Blob对象 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') } // // 跳转到浏览器的授权设置页面 // window.location.href = 'chrome://settings/content/camera' } return false } /* A method that returns the required style for the html */ function get_position_style (ctx, widget_width, y, node_height, top) { const MARGIN = 4 // the margin around the html element /* Create a transform that deals with all the scrolling and zooming */ 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`, // maxHeight: `${node_height - MARGIN * 2}px`, // we're assuming we have the whole height of the node width: `${widget_width - MARGIN * 2}px`, // height: `${node_height - MARGIN * 2}px`, // background: '#EEEEEE', display: 'flex', flexDirection: 'column', // alignItems: 'center', 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) { // console.log('#Mixlab.image.ScreenShareNode', app) return { CHEESE (node, inputName, inputData, app) { // We return an object containing a field CHEESE which has a function (taking node, name, data, app) const widget = { type: inputData[0], // the type, CHEESE name: inputName, // the name, slice size: [128, 12], // a default size draw (ctx, node, width, y) { // a method to draw the widget (ctx is a CanvasRenderingContext2D) }, computeSize (...args) { return [128, 12] // a method to compute the current size of the widget }, async serializeValue (nodeId, widgetIndex) { return window._mixlab_screen_imagePath || base64Df } } // console.log('#Mixlab.image.ScreenShareNode',widget) node.addCustomWidget(widget) return widget // and returns it. }, PROMPT (node, inputName, inputData, app) { // We return an object containing a field CHEESE which has a function (taking node, name, data, app) const widget = { type: inputData[0], // the type, CHEESE name: inputName, // the name, slice size: [128, 12], // a default size draw (ctx, node, width, y) { // a method to draw the widget (ctx is a CanvasRenderingContext2D) }, computeSize (...args) { return [128, 12] // a method to compute the current size of the widget }, async serializeValue (nodeId, widgetIndex) { return window._mixlab_screen_prompt || '' } } // console.log('###widget', widget) node.addCustomWidget(widget) // adds it to the node return widget // and returns it. }, SLIDE (node, inputName, inputData, app) { // We return an object containing a field CHEESE which has a function (taking node, name, data, app) const widget = { type: inputData[0], // the type, CHEESE name: inputName, // the name, slice size: [128, 12], // a default size draw (ctx, node, width, y) { // a method to draw the widget (ctx is a CanvasRenderingContext2D) }, computeSize (...args) { return [128, 12] // a method to compute the current size of the widget }, async serializeValue (nodeId, widgetIndex) { return window._mixlab_screen_slide_input || 0.5 } } // console.log('###widget', widget) node.addCustomWidget(widget) // adds it to the node return widget // and returns it. }, SEED (node, inputName, inputData, app) { // We return an object containing a field CHEESE which has a function (taking node, name, data, app) const widget = { type: inputData[0], // the type, CHEESE name: inputName, // the name, slice size: [128, 12], // a default size draw (ctx, node, width, y) { // a method to draw the widget (ctx is a CanvasRenderingContext2D) }, computeSize (...args) { return [128, 12] // a method to compute the current size of the widget }, async serializeValue (nodeId, widgetIndex) { return window._mixlab_screen_seed_input || 0 } } // console.log('###widget', widget) node.addCustomWidget(widget) // adds it to the node return widget // and returns it. } } }, 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', // whatever name: 'sreen_share', // whatever draw (ctx, node, widget_width, y, widget_height) { // console.log('ScreenSHare', 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', { // innerText: 'Share Screen', 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.refreshInput = $el('input', { // placeholder: ' Refresh rate:200 ms', // type: 'number', // min: 100, // step: 100, // style: { // cursor: 'pointer', // padding: '8px 24px', // fontWeight: '300', // margin: '2px', // color: 'var(--descrip-text)', // backgroundColor: 'var(--comfy-input-bg)' // } // }); // widget.refreshInput.className='comfy-multiline-input' 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.refreshInput) 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' } } } // updateSetAreaDisplay(widget.previewArea, 200, 200) 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) { // console.log(device) return device.kind === 'videoinput' }) // 创建