added a new node for loading mp4 file.

This commit is contained in:
bruefire
2023-12-13 01:35:15 +09:00
parent 4e7929a08b
commit dd9a66ee85
6 changed files with 455 additions and 116 deletions
+29 -12
View File
@@ -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
View File
@@ -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']
+21
View File
@@ -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.
+47
View File
@@ -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
View File
@@ -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);
}
}
}