added a new node for loading mp4 file.
This commit is contained in:
+29
-12
@@ -5,18 +5,7 @@ import numpy as np
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
|
||||
class LoadImagesFromDirInpaint:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"sequence_id": ("STRING", {"frames_upload": True}),
|
||||
},
|
||||
"optional": {
|
||||
"start_index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
"end_index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
}
|
||||
}
|
||||
class LoadImagesInpaint:
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "IMAGE", "INT")
|
||||
RETURN_NAMES = ("images", "mask_images", "image_count",)
|
||||
@@ -131,3 +120,31 @@ class LoadImagesFromDirInpaint:
|
||||
|
||||
return img_files
|
||||
|
||||
|
||||
class LoadImagesFromDirInpaint(LoadImagesInpaint):
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"sequence_id": ("STRING", {"frames_upload": True}),
|
||||
},
|
||||
"optional": {
|
||||
"start_index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
"end_index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LoadImagesFromFileInpaint(LoadImagesInpaint):
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"sequence_id": ("STRING", {"video_upload": True}),
|
||||
},
|
||||
"optional": {
|
||||
"start_index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
"end_index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -1,10 +1,17 @@
|
||||
import os, shutil
|
||||
import folder_paths
|
||||
from .SequentialImageLoader import LoadImagesFromDirInpaint
|
||||
from .SequentialImageLoader import LoadImagesFromFileInpaint
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = { "VFrame Loader With Mask Editor" : LoadImagesFromDirInpaint }
|
||||
NODE_DISPLAY_NAME_MAPPINGS = { "VFrame Loader With Mask Editor" : "SequentialImageLoader" }
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"VFrame Loader With Mask Editor" : LoadImagesFromDirInpaint,
|
||||
"Video Loader With Mask Editor" : LoadImagesFromFileInpaint
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"VFrame Loader With Mask Editor" : "SequentialImageLoader",
|
||||
"Video Loader With Mask Editor" : "VideoExtractor",
|
||||
}
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 josephrocca
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,47 @@
|
||||
Simple JavaScript library to performantly and reliably break a video down into its individual frames using the new WebCodecs API.
|
||||
|
||||
* Currently only supports mp4 files.
|
||||
* Check browser support: https://caniuse.com/webcodecs
|
||||
* All credit for this code goes to authors of [official WebCodecs demos](https://w3c.github.io/webcodecs/samples/video-decode-display/) and of [mp4box.js](https://github.com/gpac/mp4box.js/).
|
||||
|
||||
### Usage
|
||||
```html
|
||||
<canvas id="canvasEl"></canvas>
|
||||
<br>
|
||||
<input type="file" accept="video/mp4" onchange="start(this.files[0])">
|
||||
<script type="module">
|
||||
import getVideoFrames from "https://deno.land/x/get_video_frames@v0.0.10/mod.js"
|
||||
|
||||
let frameCount = 0;
|
||||
|
||||
window.start = async function(file) {
|
||||
let ctx = canvasEl.getContext("2d");
|
||||
|
||||
// `getVideoFrames` requires a video URL as input.
|
||||
// If you have a file/blob instead of a videoUrl, turn it into a URL like this:
|
||||
let videoUrl = URL.createObjectURL(file);
|
||||
|
||||
await getVideoFrames({
|
||||
videoUrl,
|
||||
onFrame(frame) { // `frame` is a VideoFrame object: https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame
|
||||
ctx.drawImage(frame, 0, 0, canvasEl.width, canvasEl.height);
|
||||
frame.close();
|
||||
frameCount++;
|
||||
},
|
||||
onConfig(config) {
|
||||
canvasEl.width = config.codedWidth;
|
||||
canvasEl.height = config.codedHeight;
|
||||
},
|
||||
onFinish() {
|
||||
console.log("finished!");
|
||||
console.log("frameCount", frameCount);
|
||||
},
|
||||
});
|
||||
|
||||
URL.revokeObjectURL(file); // revoke URL to prevent memory leak
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
* Simple demo: https://jsbin.com/gehaqureca/edit?html,output
|
||||
* Decode mp4 and re-encode as webm: https://josephrocca.github.io/createWebMFromFrames.js/demo/
|
||||
File diff suppressed because one or more lines are too long
+188
-102
@@ -3,8 +3,113 @@ import { app } from "../../../scripts/app.js";
|
||||
import { ComfyApp } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
import MaskEditorForVframes from "./MaskEditorForVframes.js";
|
||||
import getVideoFrames from "./getVideoFrames_js/mod.js"
|
||||
|
||||
let sequentialimageloader_getExtraMenuOptionsOverrided = [];
|
||||
|
||||
|
||||
|
||||
function showImage(name, node) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
node.extVframes = [img];
|
||||
app.graph.setDirtyCanvas(true);
|
||||
};
|
||||
let folder_separator = name.lastIndexOf("/");
|
||||
let subfolder = "";
|
||||
if (folder_separator > -1) {
|
||||
subfolder = name.substring(0, folder_separator);
|
||||
name = name.substring(folder_separator + 1);
|
||||
}
|
||||
img.src = api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}`);
|
||||
node.setSizeForImage?.();
|
||||
}
|
||||
|
||||
async function uploadFile(file, updateNode, vframeId, node) {
|
||||
if (!file.type.startsWith("image/"))
|
||||
return;
|
||||
|
||||
try {
|
||||
// Wrap file in formdata so it includes filename
|
||||
const body = new FormData();
|
||||
body.append("image", file, file.name);
|
||||
body.append("subfolder", "extVideoFrame" + String(vframeId));
|
||||
const resp = await api.fetchApi("/upload/image", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json();
|
||||
// update the widget value
|
||||
let path = data.name;
|
||||
if (data.subfolder) path = data.subfolder + "/" + path;
|
||||
|
||||
if (updateNode) {
|
||||
showImage(path, node);
|
||||
}
|
||||
} else {
|
||||
alert(resp.status + " - " + resp.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function uplaodSequentialDataCore(node, inputName, inputData, app, fileInput) {
|
||||
const idWidget = node.widgets.find((w) => w.name === "sequence_id");
|
||||
|
||||
// Add our own callback to the combo widget to render an image when it changes
|
||||
idWidget.callback = function () {
|
||||
// let imageData = idWidget.value.split(":");
|
||||
// let dirIds = imageData[imageData.length - 1].split("/");
|
||||
// showImage("extVideoFrame" + dirIds[0] + "/" + imageData[0]);
|
||||
};
|
||||
|
||||
// On load if we have a value then render the image
|
||||
requestAnimationFrame(() => {
|
||||
if (idWidget.value) {
|
||||
let imageData = idWidget.value.split(":");
|
||||
let dirIds = imageData[imageData.length - 1].split("/");
|
||||
showImage("extVideoFrame" + dirIds[0] + "/" + imageData[0], node);
|
||||
}
|
||||
});
|
||||
|
||||
document.body.append(fileInput);
|
||||
|
||||
// Create the button widget for selecting the files
|
||||
let uploadWidget = node.addWidget("button", "choose image directory to upload", "image", () => {
|
||||
fileInput.click();
|
||||
});
|
||||
uploadWidget.serialize = false;
|
||||
|
||||
// Add handler to check if an image is being dragged over our node
|
||||
node.onDragOver = function (e) {
|
||||
if (e.dataTransfer && e.dataTransfer.items) {
|
||||
const image = [...e.dataTransfer.items].find((f) => f.kind === "file");
|
||||
return !!image;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// On drop upload files
|
||||
node.onDragDrop = function (e) {
|
||||
// console.log("onDragDrop called");
|
||||
// let handled = false;
|
||||
// for (const file of e.dataTransfer.files) {
|
||||
// if (file.type.startsWith("image/")) {
|
||||
// uploadFile(file, !handled); // Dont await these, any order is fine, only update on first one
|
||||
// handled = true;
|
||||
// }
|
||||
// }
|
||||
|
||||
return handled;
|
||||
};
|
||||
|
||||
return { widget: uploadWidget };
|
||||
}
|
||||
|
||||
let sequentialimageloader_getExtraMenuOptionsOverrided = false;
|
||||
|
||||
app.registerExtension({
|
||||
|
||||
@@ -38,10 +143,11 @@ app.registerExtension({
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app)
|
||||
{
|
||||
if (nodeData.name !== "VFrame Loader With Mask Editor")
|
||||
if (nodeData.name !== "VFrame Loader With Mask Editor"
|
||||
&& nodeData.name !== "Video Loader With Mask Editor")
|
||||
return;
|
||||
|
||||
if (!sequentialimageloader_getExtraMenuOptionsOverrided) {
|
||||
if (!sequentialimageloader_getExtraMenuOptionsOverrided.includes(nodeData.name)) {
|
||||
// menu initializing
|
||||
const originalProto = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function (_, options){
|
||||
@@ -61,7 +167,7 @@ app.registerExtension({
|
||||
});
|
||||
}
|
||||
}
|
||||
sequentialimageloader_getExtraMenuOptionsOverrided = true;
|
||||
sequentialimageloader_getExtraMenuOptionsOverrided.push(nodeData.name);
|
||||
}
|
||||
|
||||
// head image drawing
|
||||
@@ -118,76 +224,16 @@ app.registerExtension({
|
||||
if (nodeData?.input?.required?.sequence_id?.[1]?.frames_upload === true) {
|
||||
nodeData.input.required.upload = ["VFRAMESUPLOAD"];
|
||||
}
|
||||
else if (nodeData?.input?.required?.sequence_id?.[1]?.video_upload === true) {
|
||||
nodeData.input.required.upload = ["VIDEOUPLOAD"];
|
||||
}
|
||||
},
|
||||
async getCustomWidgets()
|
||||
{
|
||||
return {
|
||||
VFRAMESUPLOAD(node, inputName, inputData, app) {
|
||||
const idWidget = node.widgets.find((w) => w.name === "sequence_id");
|
||||
|
||||
function showImage(name) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
node.extVframes = [img];
|
||||
app.graph.setDirtyCanvas(true);
|
||||
};
|
||||
let folder_separator = name.lastIndexOf("/");
|
||||
let subfolder = "";
|
||||
if (folder_separator > -1) {
|
||||
subfolder = name.substring(0, folder_separator);
|
||||
name = name.substring(folder_separator + 1);
|
||||
}
|
||||
img.src = api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}`);
|
||||
node.setSizeForImage?.();
|
||||
}
|
||||
|
||||
// Add our own callback to the combo widget to render an image when it changes
|
||||
idWidget.callback = function () {
|
||||
// let imageData = idWidget.value.split(":");
|
||||
// let dirIds = imageData[imageData.length - 1].split("/");
|
||||
// showImage("extVideoFrame" + dirIds[0] + "/" + imageData[0]);
|
||||
};
|
||||
|
||||
// On load if we have a value then render the image
|
||||
requestAnimationFrame(() => {
|
||||
if (idWidget.value) {
|
||||
let imageData = idWidget.value.split(":");
|
||||
let dirIds = imageData[imageData.length - 1].split("/");
|
||||
showImage("extVideoFrame" + dirIds[0] + "/" + imageData[0]);
|
||||
}
|
||||
});
|
||||
|
||||
async function uploadFile(file, updateNode, vframeId) {
|
||||
if (!file.type.startsWith("image/"))
|
||||
return;
|
||||
|
||||
try {
|
||||
// Wrap file in formdata so it includes filename
|
||||
const body = new FormData();
|
||||
body.append("image", file, file.name);
|
||||
body.append("subfolder", "extVideoFrame" + String(vframeId));
|
||||
const resp = await api.fetchApi("/upload/image", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json();
|
||||
// update the widget value
|
||||
let path = data.name;
|
||||
if (data.subfolder) path = data.subfolder + "/" + path;
|
||||
|
||||
if (updateNode) {
|
||||
showImage(path);
|
||||
}
|
||||
} else {
|
||||
alert(resp.status + " - " + resp.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const fileInput = document.createElement("input");
|
||||
Object.assign(fileInput, {
|
||||
type: "file",
|
||||
@@ -197,50 +243,90 @@ app.registerExtension({
|
||||
if (fileInput.files.length) {
|
||||
const vframeId = Date.now();
|
||||
let vframeData = "";
|
||||
|
||||
|
||||
for (let i = 0; i < fileInput.files.length; i++) {
|
||||
vframeData += fileInput.files[i].name + ":";
|
||||
await uploadFile(
|
||||
fileInput.files[i], i === 0, vframeId);
|
||||
fileInput.files[i], i === 0, vframeId, node);
|
||||
}
|
||||
vframeData += String(vframeId);
|
||||
idWidget.value = vframeData;
|
||||
}
|
||||
},
|
||||
});
|
||||
document.body.append(fileInput);
|
||||
|
||||
// Create the button widget for selecting the files
|
||||
let uploadWidget = node.addWidget("button", "choose image directory to upload", "image", () => {
|
||||
fileInput.click();
|
||||
});
|
||||
uploadWidget.serialize = false;
|
||||
|
||||
// Add handler to check if an image is being dragged over our node
|
||||
node.onDragOver = function (e) {
|
||||
if (e.dataTransfer && e.dataTransfer.items) {
|
||||
const image = [...e.dataTransfer.items].find((f) => f.kind === "file");
|
||||
return !!image;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// On drop upload files
|
||||
node.onDragDrop = function (e) {
|
||||
console.log("onDragDrop called");
|
||||
let handled = false;
|
||||
for (const file of e.dataTransfer.files) {
|
||||
if (file.type.startsWith("image/")) {
|
||||
uploadFile(file, !handled); // Dont await these, any order is fine, only update on first one
|
||||
handled = true;
|
||||
|
||||
return uplaodSequentialDataCore(node, inputName, inputData, app, fileInput);
|
||||
},
|
||||
VIDEOUPLOAD(node, inputName, inputData, app) {
|
||||
const idWidget = node.widgets.find((w) => w.name === "sequence_id");
|
||||
|
||||
async function getFramesFromVideo(file) {
|
||||
const imageType = 'image/png';
|
||||
let frameCount = 0;
|
||||
let tempCanvas = document.createElement('canvas');
|
||||
let ctx = tempCanvas.getContext("2d");
|
||||
let videoUrl = URL.createObjectURL(file);
|
||||
let vframes = [];
|
||||
|
||||
await getVideoFrames({
|
||||
videoUrl,
|
||||
async onFrame(vframe) { // `frame` is a VideoFrame object
|
||||
vframes.push(vframe);
|
||||
},
|
||||
onConfig(config) {
|
||||
tempCanvas.width = config.codedWidth;
|
||||
tempCanvas.height = config.codedHeight;
|
||||
},
|
||||
onFinish() {
|
||||
},
|
||||
});
|
||||
|
||||
let frames = await Promise.all(
|
||||
vframes.map(vframe =>
|
||||
new Promise((resolve, reject) => {
|
||||
ctx.drawImage(vframe, 0, 0, tempCanvas.width, tempCanvas.height);
|
||||
vframe.close();
|
||||
|
||||
tempCanvas.toBlob(blob => {
|
||||
if (blob) {
|
||||
blob.name = ("00000000" + frameCount++).slice(-8) + ".png";
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error('blob creating failed'));
|
||||
}
|
||||
}, imageType);
|
||||
})));
|
||||
|
||||
URL.revokeObjectURL(videoUrl); // revoke URL to prevent memory leak
|
||||
console.log("Frame extraction completed with " + frameCount + " frames");
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
const fileInput = document.createElement("input");
|
||||
Object.assign(fileInput, {
|
||||
type: "file",
|
||||
style: "display: none",
|
||||
accept: "video/*",
|
||||
onchange: async () => {
|
||||
if (fileInput.files.length) {
|
||||
const file = fileInput.files[0];
|
||||
const frames = await getFramesFromVideo(file);
|
||||
|
||||
const vframeId = Date.now();
|
||||
let vframeData = "";
|
||||
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
vframeData += frames[i].name + ":";
|
||||
await uploadFile(frames[i], i === 0, vframeId, node);
|
||||
}
|
||||
vframeData += String(vframeId);
|
||||
idWidget.value = vframeData;
|
||||
}
|
||||
}
|
||||
|
||||
return handled;
|
||||
};
|
||||
|
||||
return { widget: uploadWidget };
|
||||
},
|
||||
});
|
||||
|
||||
return uplaodSequentialDataCore(node, inputName, inputData, app, fileInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user