Files
multi_render_blender/src/main/VideoGenerator.js

343 lines
13 KiB
JavaScript

const { spawn } = require("child_process");
const path = require("path");
const fs = require("fs");
const os = require("os");
const PathResolver = require("./PathResolver.js");
const STR_TEMP_DIR = path.join(os.tmpdir(), "multi_render_blender_preview");
const NB_PLACEHOLDER_MAX_SIZE = 512;
const VideoGenerator = {
_obj_process: null,
generate: (obj_params, fn_on_log) => {
let str_output_path = obj_params.str_output_path;
let list_cameras = obj_params.list_cameras;
let nb_fps = obj_params.nb_fps || 24;
let str_output_mode = obj_params.str_output_mode || "subfolder";
let str_frame_prefix = obj_params.str_frame_prefix !== undefined ? obj_params.str_frame_prefix : "f_";
let nb_frame_padding = obj_params.nb_frame_padding || 5;
if (!fs.existsSync(STR_TEMP_DIR)) {
fs.mkdirSync(STR_TEMP_DIR, { recursive: true });
}
let str_video_path = path.join(STR_TEMP_DIR, "preview.mp4");
try {
if (fs.existsSync(str_video_path)) {
fs.unlinkSync(str_video_path);
}
} catch (obj_err) {
// ignore
}
let list_camera_data = VideoGenerator._scan_rendered_files(
str_output_path, list_cameras, str_output_mode, str_frame_prefix
);
if (list_camera_data.length === 0) {
return Promise.reject(new Error("Aucune image rendue trouvee."));
}
let nb_width = list_camera_data[0].nb_resolution_x;
let nb_height = list_camera_data[0].nb_resolution_y;
if (nb_width % 2 !== 0) {
nb_width++;
}
if (nb_height % 2 !== 0) {
nb_height++;
}
let list_segments = [];
let nb_segment_index = 0;
let fn_generate_segments = () => {
if (nb_segment_index >= list_camera_data.length) {
return VideoGenerator._concat_segments(list_segments, str_video_path, fn_on_log);
}
let obj_cam_data = list_camera_data[nb_segment_index];
let str_title_path = path.join(STR_TEMP_DIR, "title_" + nb_segment_index + ".mp4");
let str_frames_path = path.join(STR_TEMP_DIR, "frames_" + nb_segment_index + ".mp4");
fn_on_log("Titre : " + obj_cam_data.str_name);
return VideoGenerator._generate_title_card(
str_title_path, obj_cam_data, nb_width, nb_height, nb_fps
)
.then(() => {
fn_on_log("Sequence : " + obj_cam_data.str_name + " (" + obj_cam_data.list_frame_numbers.length + " images)");
return VideoGenerator._generate_frames_video(
str_frames_path, obj_cam_data, nb_width, nb_height, nb_fps
);
})
.then(() => {
list_segments.push(str_title_path);
list_segments.push(str_frames_path);
nb_segment_index++;
return fn_generate_segments();
});
};
return fn_generate_segments()
.then(() => {
for (let str_seg of list_segments) {
try { fs.unlinkSync(str_seg); } catch (obj_e) { /* ignore */ }
}
let str_concat_file = path.join(STR_TEMP_DIR, "concat.txt");
try { if (fs.existsSync(str_concat_file)) { fs.unlinkSync(str_concat_file); } } catch (obj_e) { /* ignore */ }
let str_frames_list = path.join(STR_TEMP_DIR, "frames_list.txt");
try { if (fs.existsSync(str_frames_list)) { fs.unlinkSync(str_frames_list); } } catch (obj_e) { /* ignore */ }
return { str_video_path: str_video_path };
});
},
_extract_frame_number: (str_filepath, str_file_pattern) => {
let str_basename = path.basename(str_filepath);
let str_after = str_basename.substring(str_file_pattern.length);
let str_num = str_after.replace(/\.[^.]+$/, "");
let nb_frame = parseInt(str_num, 10);
return isNaN(nb_frame) ? -1 : nb_frame;
},
_scan_rendered_files: (str_output_path, list_cameras, str_output_mode, str_frame_prefix) => {
let list_camera_data = [];
for (let obj_cam of list_cameras) {
if (!obj_cam.is_enabled) {
continue;
}
let str_cam_dir = "";
let str_file_pattern = "";
if (str_output_mode === "prefix") {
str_cam_dir = str_output_path;
str_file_pattern = obj_cam.str_name + "_" + str_frame_prefix;
} else if (str_output_mode === "both") {
str_cam_dir = path.join(str_output_path, obj_cam.str_name);
str_file_pattern = obj_cam.str_name + "_" + str_frame_prefix;
} else {
str_cam_dir = path.join(str_output_path, obj_cam.str_name);
str_file_pattern = str_frame_prefix;
}
if (!fs.existsSync(str_cam_dir)) {
continue;
}
let list_files = fs.readdirSync(str_cam_dir);
let map_frame_to_file = {};
for (let str_file of list_files) {
if (!str_file.startsWith(str_file_pattern)) {
continue;
}
let str_full = path.join(str_cam_dir, str_file);
try {
let nb_size = fs.statSync(str_full).size;
if (nb_size > NB_PLACEHOLDER_MAX_SIZE) {
let nb_frame = VideoGenerator._extract_frame_number(str_full, str_file_pattern);
if (nb_frame >= 0) {
map_frame_to_file[nb_frame] = str_full;
}
}
} catch (obj_err) {
// ignore
}
}
let list_frame_numbers = Object.keys(map_frame_to_file).map(Number);
list_frame_numbers.sort((a, b) => a - b);
if (list_frame_numbers.length > 0) {
list_camera_data.push({
str_name: obj_cam.str_name,
nb_frame_start: obj_cam.nb_frame_start,
nb_frame_end: obj_cam.nb_frame_end,
nb_resolution_x: obj_cam.nb_resolution_x,
nb_resolution_y: obj_cam.nb_resolution_y,
map_frame_to_file: map_frame_to_file,
list_frame_numbers: list_frame_numbers,
});
}
}
return list_camera_data;
},
_find_font: () => {
let list_paths = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf",
"C:\\Windows\\Fonts\\arial.ttf",
"/System/Library/Fonts/Helvetica.ttc",
];
for (let str_p of list_paths) {
if (fs.existsSync(str_p)) {
return str_p;
}
}
return null;
},
_escape_drawtext: (str_text) => {
return str_text
.replace(/\\/g, "\\\\")
.replace(/'/g, "\u2019")
.replace(/:/g, "\\:")
.replace(/;/g, "\\;")
.replace(/\[/g, "\\[")
.replace(/\]/g, "\\]");
},
_generate_title_card: (str_output, obj_cam_data, nb_width, nb_height, nb_fps) => {
let str_font = VideoGenerator._find_font();
let str_font_prefix = str_font ? "fontfile=" + str_font.replace(/:/g, "\\:") + ":" : "";
let str_cam_name = VideoGenerator._escape_drawtext(obj_cam_data.str_name);
let str_frame_range = VideoGenerator._escape_drawtext(
"Frames " + obj_cam_data.nb_frame_start + " - " + obj_cam_data.nb_frame_end
);
let str_resolution = VideoGenerator._escape_drawtext(
obj_cam_data.nb_resolution_x + " x " + obj_cam_data.nb_resolution_y
);
let str_nb_images = VideoGenerator._escape_drawtext(
obj_cam_data.list_frame_numbers.length + " images rendues"
);
let str_filter = "drawtext=" + str_font_prefix + "text='" + str_cam_name
+ "':fontcolor=white:fontsize=60:x=(w-tw)/2:y=(h/2)-80"
+ ",drawtext=" + str_font_prefix + "text='" + str_frame_range
+ "':fontcolor=0xaaaaaa:fontsize=36:x=(w-tw)/2:y=(h/2)"
+ ",drawtext=" + str_font_prefix + "text='" + str_resolution
+ "':fontcolor=0x888888:fontsize=28:x=(w-tw)/2:y=(h/2)+60"
+ ",drawtext=" + str_font_prefix + "text='" + str_nb_images
+ "':fontcolor=0x888888:fontsize=28:x=(w-tw)/2:y=(h/2)+100";
let list_args = [
"-f", "lavfi",
"-i", "color=c=0x1a1a2e:s=" + nb_width + "x" + nb_height + ":d=1:r=" + nb_fps,
"-vf", str_filter,
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-t", "1",
"-y",
str_output,
];
return VideoGenerator._run_ffmpeg(list_args);
},
_generate_frames_video: (str_output, obj_cam_data, nb_width, nb_height, nb_fps) => {
let str_concat_path = path.join(STR_TEMP_DIR, "frames_list.txt");
let nb_frame_duration = 1 / nb_fps;
let str_content = "";
let list_frame_numbers = obj_cam_data.list_frame_numbers;
let map_frame_to_file = obj_cam_data.map_frame_to_file;
let nb_first = list_frame_numbers[0];
let nb_last = list_frame_numbers[list_frame_numbers.length - 1];
let str_last_file = map_frame_to_file[nb_first];
for (let nb_f = nb_first; nb_f <= nb_last; nb_f++) {
if (map_frame_to_file[nb_f]) {
str_last_file = map_frame_to_file[nb_f];
}
let str_safe = str_last_file.replace(/'/g, "'\\''");
str_content += "file '" + str_safe + "'\n";
str_content += "duration " + nb_frame_duration.toFixed(6) + "\n";
}
let str_safe_last = str_last_file.replace(/'/g, "'\\''");
str_content += "file '" + str_safe_last + "'\n";
fs.writeFileSync(str_concat_path, str_content, "utf8");
let str_scale = "scale=" + nb_width + ":" + nb_height
+ ":force_original_aspect_ratio=decrease"
+ ",pad=" + nb_width + ":" + nb_height + ":(ow-iw)/2:(oh-ih)/2:color=black";
let list_args = [
"-f", "concat",
"-safe", "0",
"-i", str_concat_path,
"-vf", str_scale,
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-r", String(nb_fps),
"-y",
str_output,
];
return VideoGenerator._run_ffmpeg(list_args);
},
_concat_segments: (list_segments, str_output, fn_on_log) => {
let str_concat_path = path.join(STR_TEMP_DIR, "concat.txt");
let str_content = "";
for (let str_seg of list_segments) {
let str_safe = str_seg.replace(/'/g, "'\\''");
str_content += "file '" + str_safe + "'\n";
}
fs.writeFileSync(str_concat_path, str_content, "utf8");
fn_on_log("Concatenation des segments...");
let list_args = [
"-f", "concat",
"-safe", "0",
"-i", str_concat_path,
"-c", "copy",
"-y",
str_output,
];
return VideoGenerator._run_ffmpeg(list_args);
},
_run_ffmpeg: (list_args) => {
return new Promise((resolve, reject) => {
let obj_process = spawn(PathResolver.get_ffmpeg_path(), list_args);
let str_stderr = "";
VideoGenerator._obj_process = obj_process;
obj_process.stderr.on("data", (obj_data) => {
str_stderr += obj_data.toString();
});
obj_process.on("close", (nb_code) => {
VideoGenerator._obj_process = null;
if (nb_code !== 0) {
reject(new Error("ffmpeg erreur (code " + nb_code + "): " + str_stderr.slice(-500)));
return;
}
resolve();
});
obj_process.on("error", (obj_err) => {
VideoGenerator._obj_process = null;
reject(new Error("Impossible de lancer ffmpeg : " + obj_err.message));
});
});
},
cancel: () => {
if (VideoGenerator._obj_process) {
try { VideoGenerator._obj_process.kill("SIGTERM"); } catch (obj_err) { /* ignore */ }
VideoGenerator._obj_process = null;
}
},
};
module.exports = VideoGenerator;