67d8639861
Deploy / deploy (push) Successful in 6m17s
Manipulating startTime/outPoint on previewComp's SHOT layer touched a comp duplicated from the artist-authored template, whose slate/shot timing relationship the build script doesn't own -- and it dropped the slate frame in production. The trim now happens one level down, on the private single-layer footage precomp this script fully controls; previewComp's own sizing logic is untouched from before this feature existed, since it already generalizes correctly to any shotComp duration. Also threads the picture-lock skip reason through to the export's timeline, so a shot missing sequence timecodes or a matching picture lock file is visible without digging into worker-machine logs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
747 lines
28 KiB
React
747 lines
28 KiB
React
/*
|
|
vfxr_build_preview.jsx — VFXReview render pipeline, preview build stage.
|
|
|
|
Runs headlessly: AfterFX.com -noui -r <wrapper>.jsx
|
|
where the wrapper sets var VFXR_CONTEXT_PATH = "...json"; then
|
|
$.evalFile()s this script.
|
|
|
|
It does NOT render. It opens the studio slate/overlay template, rebuilds
|
|
the shot around the *rendered* EXR sequence (OCIO → show LUT → overlay),
|
|
duplicates the export template into a preview comp with the slate filled
|
|
in, queues the MOV and MP4 output modules, and saves a throwaway AEP.
|
|
The worker then runs `aerender -project <that aep>` with no -comp, which
|
|
renders the whole queue in one launch.
|
|
|
|
Build logic mirrors the VFXReviewConnector panel functions
|
|
buildShotFromCode / updateOverlayForComp / buildPreviewForShot so the
|
|
farm and the artist produce identical slates and burn-ins.
|
|
|
|
Everything is reported back through the result JSON — never a dialog.
|
|
*/
|
|
|
|
(function vfxrBuildPreview() {
|
|
var ctx = null;
|
|
var result = {
|
|
ok: false,
|
|
error: null,
|
|
warnings: [],
|
|
tempAep: null,
|
|
previewComp: null,
|
|
queued: [],
|
|
frameCount: null,
|
|
width: null,
|
|
height: null,
|
|
piclockRefBuilt: false,
|
|
piclockSkippedReason: null
|
|
};
|
|
var resultPath = null;
|
|
|
|
// ── tiny helpers (ES3 / ExtendScript safe) ───────────────────────────────
|
|
|
|
function readTextFile(path) {
|
|
var f = new File(path);
|
|
var text;
|
|
if (!f.exists) {
|
|
return null;
|
|
}
|
|
f.encoding = "UTF-8";
|
|
if (!f.open("r")) {
|
|
return null;
|
|
}
|
|
text = f.read();
|
|
f.close();
|
|
return text;
|
|
}
|
|
|
|
function writeTextFile(path, text) {
|
|
var f = new File(path);
|
|
try {
|
|
f.encoding = "UTF-8";
|
|
if (!f.open("w")) {
|
|
return false;
|
|
}
|
|
f.write(text);
|
|
f.close();
|
|
return true;
|
|
} catch (writeError) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function jsonEscape(value) {
|
|
var text = String(value);
|
|
var out = "";
|
|
var i;
|
|
var ch;
|
|
var code;
|
|
|
|
for (i = 0; i < text.length; i += 1) {
|
|
ch = text.charAt(i);
|
|
code = text.charCodeAt(i);
|
|
if (ch === "\"") {
|
|
out += "\\\"";
|
|
} else if (ch === "\\") {
|
|
out += "\\\\";
|
|
} else if (ch === "\n") {
|
|
out += "\\n";
|
|
} else if (ch === "\r") {
|
|
out += "\\r";
|
|
} else if (ch === "\t") {
|
|
out += "\\t";
|
|
} else if (code < 32) {
|
|
out += "\\u" + ("000" + code.toString(16)).slice(-4);
|
|
} else {
|
|
out += ch;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function toJSON(value) {
|
|
var parts = [];
|
|
var i;
|
|
var key;
|
|
|
|
if (value === null || value === undefined) {
|
|
return "null";
|
|
}
|
|
if (typeof value === "number") {
|
|
return isFinite(value) ? String(value) : "null";
|
|
}
|
|
if (typeof value === "boolean") {
|
|
return String(value);
|
|
}
|
|
if (typeof value === "string") {
|
|
return "\"" + jsonEscape(value) + "\"";
|
|
}
|
|
if (value instanceof Array) {
|
|
for (i = 0; i < value.length; i += 1) {
|
|
parts.push(toJSON(value[i]));
|
|
}
|
|
return "[" + parts.join(",") + "]";
|
|
}
|
|
for (key in value) {
|
|
if (value.hasOwnProperty(key) && value[key] !== undefined) {
|
|
parts.push("\"" + jsonEscape(key) + "\":" + toJSON(value[key]));
|
|
}
|
|
}
|
|
return "{" + parts.join(",") + "}";
|
|
}
|
|
|
|
function warn(message) {
|
|
result.warnings.push(String(message));
|
|
}
|
|
|
|
function findComp(name) {
|
|
var i;
|
|
var item;
|
|
|
|
for (i = 1; i <= app.project.numItems; i += 1) {
|
|
item = app.project.item(i);
|
|
if (item instanceof CompItem && item.name === name) {
|
|
return item;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findFolder(name) {
|
|
var i;
|
|
var item;
|
|
|
|
for (i = 1; i <= app.project.numItems; i += 1) {
|
|
item = app.project.item(i);
|
|
if (item instanceof FolderItem && item.name === name) {
|
|
return item;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Mirrors the panel's parseTimecodeToSeconds exactly.
|
|
function parseTimecodeToSeconds(tc, fps) {
|
|
var parts = String(tc).split(/[:;]/);
|
|
if (parts.length < 4) {
|
|
return 0;
|
|
}
|
|
return parseInt(parts[0], 10) * 3600 + parseInt(parts[1], 10) * 60 +
|
|
parseInt(parts[2], 10) + parseInt(parts[3], 10) / (fps || 24);
|
|
}
|
|
|
|
// Locates the picture-lock reference footage the same way the panel's
|
|
// pullPictureLock does: the _PICLOCKS project folder first, then a disk
|
|
// glob under ctx.piclockDir. Returns the FootageItem, or null (with a
|
|
// warn()) if it cannot be found — never fatal to the preview build.
|
|
function findPiclockFootage(ctx) {
|
|
var piclocksProjectFolder = findFolder("_PICLOCKS");
|
|
var i;
|
|
var item;
|
|
var piclocksDir;
|
|
var files;
|
|
var importOptions;
|
|
|
|
if (piclocksProjectFolder) {
|
|
for (i = 1; i <= app.project.numItems; i += 1) {
|
|
item = app.project.item(i);
|
|
if (item instanceof FootageItem &&
|
|
item.parentFolder === piclocksProjectFolder &&
|
|
item.name.indexOf(ctx.piclockGlobPrefix + "_") === 0) {
|
|
return item;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!ctx.piclockDir) {
|
|
warn("Picture lock: no footage root configured for this project — skipping reference render");
|
|
return null;
|
|
}
|
|
piclocksDir = new Folder(ctx.piclockDir);
|
|
if (!piclocksDir.exists) {
|
|
warn("Picture lock: folder not found: " + piclocksDir.fsName);
|
|
return null;
|
|
}
|
|
files = piclocksDir.getFiles(ctx.piclockGlobPrefix + "_*");
|
|
if (!files || files.length < 1) {
|
|
warn("Picture lock: no file matching " + ctx.piclockGlobPrefix + "_* found in " + piclocksDir.fsName);
|
|
return null;
|
|
}
|
|
for (i = 0; i < files.length; i += 1) {
|
|
if (files[i] instanceof File) {
|
|
try {
|
|
importOptions = new ImportOptions(files[i]);
|
|
return app.project.importFile(importOptions);
|
|
} catch (importError) {
|
|
warn("Picture lock: import failed: " + importError.message);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Mirrors the panel's addOCIOToLayer: effect added but disabled, so the
|
|
// artist/farm chain matches exactly.
|
|
function addOCIOToLayer(layer) {
|
|
var ocio;
|
|
var outputProp;
|
|
|
|
try {
|
|
ocio = layer.property("Effects").addProperty("OCIO Color Space Transform");
|
|
ocio.enabled = false;
|
|
} catch (ocioError) {
|
|
warn("OCIO effect unavailable: " + ocioError.toString());
|
|
return;
|
|
}
|
|
|
|
try {
|
|
outputProp = ocio.property("Output Color Space");
|
|
} catch (byNameError) {
|
|
outputProp = null;
|
|
}
|
|
if (!outputProp) {
|
|
try {
|
|
outputProp = ocio.property(2);
|
|
} catch (byIndexError) {
|
|
outputProp = null;
|
|
}
|
|
}
|
|
if (outputProp) {
|
|
try {
|
|
outputProp.setValue(93);
|
|
} catch (setError) {
|
|
warn("Could not set OCIO output colour space: " + setError.toString());
|
|
}
|
|
}
|
|
}
|
|
|
|
function setEssentialProperty(layer, propName, value) {
|
|
if (value === null || value === undefined || value === "") {
|
|
return false;
|
|
}
|
|
try {
|
|
layer.property("Essential Properties").property(propName).setValue(value);
|
|
return true;
|
|
} catch (setError) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function setEssentialPropertyByIndex(layer, index, value) {
|
|
try {
|
|
layer.property("Essential Properties").property(index).setValue(value);
|
|
return true;
|
|
} catch (setError) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function listEssentialPropertyNames(layer) {
|
|
var names = [];
|
|
var group;
|
|
var i;
|
|
|
|
try {
|
|
group = layer.property("Essential Properties");
|
|
for (i = 1; i <= group.numProperties; i += 1) {
|
|
try {
|
|
names.push(i + ":" + group.property(i).name);
|
|
} catch (oneError) {
|
|
}
|
|
}
|
|
} catch (groupError) {
|
|
}
|
|
return names;
|
|
}
|
|
|
|
// Slate identity fields (version name, shot name). Current templates expose
|
|
// these under a stable name, older ones only positionally, and the two do
|
|
// not always line up — so write both. Setting the same property twice is
|
|
// harmless; the panel uses the same name-with-index-fallback pattern.
|
|
function setSlateIdentity(layer, propName, index, value, label) {
|
|
var byName;
|
|
var byIndex;
|
|
|
|
if (value === null || value === undefined || value === "") {
|
|
return; // leave the template's own default
|
|
}
|
|
byName = setEssentialProperty(layer, propName, value);
|
|
byIndex = setEssentialPropertyByIndex(layer, index, value);
|
|
if (!byName && !byIndex) {
|
|
warn(label + ": neither Essential Property \"" + propName + "\" nor index " +
|
|
index + " could be set. Available: " +
|
|
listEssentialPropertyNames(layer).join(", "));
|
|
}
|
|
}
|
|
|
|
// Per-submission fields (VFX Scope / Submission Note). The property names
|
|
// come from config; if one does not exist, report the template's actual
|
|
// property names so it can be corrected without another build.
|
|
function setSlateField(layer, propName, value, label) {
|
|
if (!propName) {
|
|
return;
|
|
}
|
|
if (value === null || value === undefined || value === "") {
|
|
return; // nothing to write; leave the template's own default
|
|
}
|
|
if (!setEssentialProperty(layer, propName, value)) {
|
|
warn(label + ": Essential Property \"" + propName + "\" not found or not settable. " +
|
|
"Available: " + listEssentialPropertyNames(layer).join(", "));
|
|
}
|
|
}
|
|
|
|
// The production name, burned into both the slate and the overlay. The
|
|
// slate's other fields are title case ("Date", "Scene") while the overlay's
|
|
// are upper ("DATE", "SHOT NAME"), so both spellings are attempted rather
|
|
// than assuming which template uses which — and a template that exposes
|
|
// neither reports its actual property names instead of failing silently.
|
|
function setShowField(layer, value, label) {
|
|
var candidates = ["SHOW", "Show"];
|
|
var i;
|
|
|
|
if (value === null || value === undefined || value === "") {
|
|
return;
|
|
}
|
|
for (i = 0; i < candidates.length; i += 1) {
|
|
if (setEssentialProperty(layer, candidates[i], value)) {
|
|
return;
|
|
}
|
|
}
|
|
warn(label + ": no Essential Property named " + candidates.join(" or ") +
|
|
". Available: " + listEssentialPropertyNames(layer).join(", "));
|
|
}
|
|
|
|
function setCompStartFrame(comp, frameNumber) {
|
|
try {
|
|
comp.displayStartFrame = frameNumber;
|
|
return;
|
|
} catch (frameError) {
|
|
}
|
|
try {
|
|
comp.displayStartTime = frameNumber / comp.frameRate;
|
|
} catch (timeError) {
|
|
}
|
|
}
|
|
|
|
function applyOutputModuleTemplate(outputModule, templateName) {
|
|
var templates;
|
|
var i;
|
|
|
|
try {
|
|
templates = outputModule.templates;
|
|
for (i = 0; i < templates.length; i += 1) {
|
|
if (templates[i] === templateName) {
|
|
outputModule.applyTemplate(templateName);
|
|
return true;
|
|
}
|
|
}
|
|
} catch (templateError) {
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function importExrSequence(dirPath) {
|
|
var folder = new Folder(dirPath);
|
|
var files;
|
|
var importOptions;
|
|
var footage;
|
|
|
|
if (!folder.exists) {
|
|
throw new Error("Render output folder not found: " + dirPath);
|
|
}
|
|
|
|
files = folder.getFiles("*.exr");
|
|
if (!files || files.length < 1) {
|
|
throw new Error("No EXR files found in " + dirPath);
|
|
}
|
|
files.sort();
|
|
|
|
importOptions = new ImportOptions(files[0]);
|
|
importOptions.sequence = true;
|
|
footage = app.project.importFile(importOptions);
|
|
try {
|
|
footage.mainSource.conformFrameRate = ctx.fps || 24;
|
|
} catch (conformError) {
|
|
warn("Could not conform frame rate: " + conformError.toString());
|
|
}
|
|
return footage;
|
|
}
|
|
|
|
function queueOutput(comp, templateName, outputPath, label) {
|
|
var item;
|
|
var outputModule;
|
|
|
|
if (!templateName || !outputPath) {
|
|
warn("Skipping " + label + ": no template or output path configured");
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
item = app.project.renderQueue.items.add(comp);
|
|
outputModule = item.outputModule(1);
|
|
if (!applyOutputModuleTemplate(outputModule, templateName)) {
|
|
item.remove();
|
|
throw new Error("Output module template not found: \"" + templateName + "\"");
|
|
}
|
|
outputModule.file = new File(outputPath);
|
|
result.queued.push({ label: label, template: templateName, output: outputPath });
|
|
return true;
|
|
} catch (queueError) {
|
|
try {
|
|
if (item) {
|
|
item.remove();
|
|
}
|
|
} catch (removeError) {
|
|
}
|
|
throw queueError;
|
|
}
|
|
}
|
|
|
|
// ── main ─────────────────────────────────────────────────────────────────
|
|
|
|
try {
|
|
if (typeof VFXR_CONTEXT_PATH === "undefined" || !VFXR_CONTEXT_PATH) {
|
|
throw new Error("VFXR_CONTEXT_PATH was not set by the wrapper script");
|
|
}
|
|
|
|
var contextText = readTextFile(VFXR_CONTEXT_PATH);
|
|
if (!contextText) {
|
|
throw new Error("Could not read job context: " + VFXR_CONTEXT_PATH);
|
|
}
|
|
ctx = eval("(" + contextText + ")");
|
|
resultPath = ctx.resultPath;
|
|
|
|
try {
|
|
app.beginSuppressDialogs();
|
|
} catch (suppressError) {
|
|
}
|
|
|
|
// 1. Open the studio template (slate, overlay, show LUT, export template)
|
|
var templateFile = new File(ctx.templateAep);
|
|
if (!templateFile.exists) {
|
|
throw new Error("Template project not found: " + ctx.templateAep);
|
|
}
|
|
app.open(templateFile);
|
|
|
|
var templateComp = findComp(ctx.templateComp);
|
|
if (!templateComp) {
|
|
throw new Error("Export template comp \"" + ctx.templateComp + "\" not found in template project");
|
|
}
|
|
|
|
// 2. Import the rendered EXR sequence
|
|
var footage = importExrSequence(ctx.outputDirLocal);
|
|
var footageFolder = findFolder("_FOOTAGE_4K");
|
|
if (footageFolder) {
|
|
footage.parentFolder = footageFolder;
|
|
}
|
|
footage.name = ctx.shotCode + "_" + ctx.versionString;
|
|
|
|
var width = footage.width;
|
|
var height = footage.height;
|
|
var duration = footage.duration;
|
|
var fps = ctx.fps || 24;
|
|
|
|
result.width = width;
|
|
result.height = height;
|
|
result.frameCount = Math.round(duration * fps);
|
|
|
|
// Trim decision (§9, picture lock): the render's frame range is
|
|
// editorial ± a fixed symmetric handle count by studio convention (the
|
|
// same constant pullPictureLock hardcodes), so no picture-lock file is
|
|
// needed for this — only ctx.handleFrames. Applied at the footage/shot
|
|
// comp level (below) rather than by juggling layer timing inside
|
|
// previewComp — previewComp is duplicated from an artist-authored
|
|
// template whose slate/shot timing relationship this script does not
|
|
// own, so it is left completely untouched, exactly as it always
|
|
// rendered correctly before this feature existed.
|
|
var handleSeconds = (ctx.handleFrames || 0) / fps;
|
|
var contentDuration = duration;
|
|
var headSkipSeconds = 0;
|
|
var trimmedToPictureLock = false;
|
|
if (ctx.noPictureLock) {
|
|
result.piclockSkippedReason = "No Picture Lock was set for this export";
|
|
} else if (handleSeconds <= 0) {
|
|
result.piclockSkippedReason = "No handle frame count configured";
|
|
} else if (2 * handleSeconds >= duration) {
|
|
warn("Picture lock: handle count (" + ctx.handleFrames + "fr) covers the whole render (" +
|
|
result.frameCount + "fr) — trim skipped");
|
|
result.piclockSkippedReason = "handle count exceeds the render duration";
|
|
} else {
|
|
headSkipSeconds = handleSeconds;
|
|
contentDuration = duration - (2 * handleSeconds);
|
|
trimmedToPictureLock = true;
|
|
}
|
|
var contentStartFrame = (ctx.frameStart !== null && ctx.frameStart !== undefined)
|
|
? ctx.frameStart + Math.round(headSkipSeconds * fps)
|
|
: null;
|
|
|
|
// 3. Footage precomp (mirrors buildShotFromCode)
|
|
var precompFolder = findFolder("_PRECOMPS");
|
|
var footageComp = app.project.items.addComp(
|
|
ctx.shotCode + "_FOOTAGE", width, height, 1, contentDuration, fps);
|
|
if (precompFolder) {
|
|
footageComp.parentFolder = precompFolder;
|
|
}
|
|
if (contentStartFrame !== null) {
|
|
setCompStartFrame(footageComp, contentStartFrame);
|
|
}
|
|
var footageLayer = footageComp.layers.add(footage);
|
|
if (headSkipSeconds > 0) {
|
|
footageLayer.startTime -= headSkipSeconds;
|
|
}
|
|
addOCIOToLayer(footageLayer);
|
|
|
|
// 4. Shot comp: footage + show LUT + overlay
|
|
var shotComp = app.project.items.addComp(ctx.shotCode, width, height, 1, contentDuration, fps);
|
|
if (contentStartFrame !== null) {
|
|
setCompStartFrame(shotComp, contentStartFrame);
|
|
}
|
|
shotComp.layers.add(footageComp);
|
|
|
|
if (ctx.lutComp) {
|
|
var showLut = findComp(ctx.lutComp);
|
|
if (showLut) {
|
|
var lutLayer = shotComp.layers.add(showLut);
|
|
lutLayer.collapseTransformation = true;
|
|
} else {
|
|
warn("Show LUT comp \"" + ctx.lutComp + "\" not found — rendering without it");
|
|
}
|
|
}
|
|
|
|
if (ctx.overlayComp) {
|
|
var overlayComp = findComp(ctx.overlayComp);
|
|
if (overlayComp) {
|
|
var overlayLayer = shotComp.layers.add(overlayComp);
|
|
overlayLayer.moveToBeginning();
|
|
overlayLayer.enabled = true;
|
|
setEssentialProperty(overlayLayer, "DATE", ctx.slate.date);
|
|
setEssentialProperty(overlayLayer, "SHOT NAME", ctx.slate.versionName);
|
|
setShowField(overlayLayer, ctx.slate.show, "Overlay SHOW");
|
|
} else {
|
|
warn("Overlay comp \"" + ctx.overlayComp + "\" not found — rendering without burn-ins");
|
|
}
|
|
}
|
|
|
|
// 5. Preview comp from the export template (mirrors buildPreviewForShot)
|
|
var previewsFolder = findFolder("_PREVIEWS");
|
|
var previewComp = templateComp.duplicate();
|
|
previewComp.name = ctx.shotCode + "_PREVIEW";
|
|
if (previewsFolder) {
|
|
previewComp.parentFolder = previewsFolder;
|
|
}
|
|
|
|
previewComp.layer("SHOT").replaceSource(shotComp, false);
|
|
try {
|
|
previewComp.layer("THUMBNAIL").replaceSource(shotComp, false);
|
|
} catch (thumbError) {
|
|
warn("THUMBNAIL layer not updated: " + thumbError.toString());
|
|
}
|
|
|
|
var slateLayer = previewComp.layer("NETFLIX_SLATE");
|
|
if (slateLayer) {
|
|
// Identity fields match the panel's indices (3 = version name,
|
|
// 10 = shot name) and are also written by name; the rest are
|
|
// addressed by name only.
|
|
// VersionName carries the full "{shotCode}_cmp_TT_{version}";
|
|
// ShotName is the bare shot code, with no version suffix.
|
|
setSlateIdentity(slateLayer, "VersionName", 3, ctx.slate.versionName, "Version Name");
|
|
setSlateIdentity(slateLayer, "ShotName", 10, ctx.slate.shotCode, "Shot Name");
|
|
setEssentialProperty(slateLayer, "Date", ctx.slate.date);
|
|
setEssentialProperty(slateLayer, "Desc", ctx.slate.description);
|
|
setEssentialProperty(slateLayer, "Episode", ctx.slate.episode);
|
|
setEssentialProperty(slateLayer, "Scene", ctx.slate.scene);
|
|
setEssentialProperty(slateLayer, "Frames", result.frameCount);
|
|
setShowField(slateLayer, ctx.slate.show, "Slate SHOW");
|
|
|
|
setSlateField(slateLayer, ctx.slateScopeProp, ctx.slate.vfxScope, "VFX Scope");
|
|
// The slate's Notes field carries this submission's note; when the
|
|
// artist left it blank, fall back to the shot's own notes so the
|
|
// slate looks the same as an artist-built preview.
|
|
setSlateField(slateLayer, ctx.slateSubmissionProp,
|
|
(ctx.slate.submissionNote !== null &&
|
|
ctx.slate.submissionNote !== undefined &&
|
|
ctx.slate.submissionNote !== "")
|
|
? ctx.slate.submissionNote
|
|
: ctx.slate.notes,
|
|
"Submission Note");
|
|
} else {
|
|
warn("NETFLIX_SLATE layer not found in template — slate fields not filled");
|
|
}
|
|
|
|
// previewComp sizes itself to whatever shotComp.duration actually is —
|
|
// unchanged from before this feature existed. shotComp is already the
|
|
// trimmed (or full, if skipped above) duration, so this needs no
|
|
// special-casing here.
|
|
var shotLayer = previewComp.layer("SHOT");
|
|
var targetOutPoint = shotLayer.inPoint + shotComp.duration;
|
|
if (targetOutPoint > previewComp.duration) {
|
|
previewComp.duration = targetOutPoint;
|
|
}
|
|
shotLayer.outPoint = targetOutPoint;
|
|
previewComp.duration = targetOutPoint;
|
|
|
|
result.previewComp = previewComp.name;
|
|
result.trimmedToPictureLock = trimmedToPictureLock;
|
|
|
|
app.project.renderQueue.showWindow(false);
|
|
while (app.project.renderQueue.numItems > 0) {
|
|
app.project.renderQueue.item(1).remove();
|
|
}
|
|
|
|
// 5b. Picture-lock reference comp: the raw plate only — no OCIO, no
|
|
// show LUT, no overlay — spanning the FULL padded duration (handles
|
|
// included), so a supervisor can check back against it if a delivered
|
|
// edit is ever suspected to be off. Independent of the trim above:
|
|
// this one needs the actual picture-lock file and sequence timecodes,
|
|
// and is skipped gracefully (never fatal) if either is missing.
|
|
if (ctx.noPictureLock) {
|
|
if (!result.piclockSkippedReason) {
|
|
result.piclockSkippedReason = "No Picture Lock was set for this export";
|
|
}
|
|
} else if (!ctx.seqTimecodeStart || !ctx.seqTimecodeEnd) {
|
|
warn("Picture lock: no sequence timecodes for this shot — reference render skipped");
|
|
result.piclockSkippedReason = "no sequence timecodes for this shot";
|
|
} else {
|
|
// A diagnostic aid, not a deliverable: any failure building or
|
|
// queuing it must warn and move on, never take down the mov/mp4
|
|
// that already succeeded (queueOutput() throws on template errors).
|
|
try {
|
|
var piclockFootage = findPiclockFootage(ctx);
|
|
if (piclockFootage) {
|
|
var piclockStartTC = 0;
|
|
try {
|
|
if (piclockFootage.mainSource && typeof piclockFootage.mainSource.startTimecode === "number") {
|
|
piclockStartTC = piclockFootage.mainSource.startTimecode;
|
|
}
|
|
} catch (tcReadError) {
|
|
}
|
|
|
|
var tcIn = parseTimecodeToSeconds(ctx.seqTimecodeStart, fps);
|
|
var tcOut = parseTimecodeToSeconds(ctx.seqTimecodeEnd, fps);
|
|
var refIn = tcIn - piclockStartTC - handleSeconds;
|
|
var refOut = tcOut - piclockStartTC + handleSeconds;
|
|
if (refIn < 0) {
|
|
refIn = 0;
|
|
}
|
|
if (refOut > piclockFootage.duration) {
|
|
refOut = piclockFootage.duration;
|
|
}
|
|
|
|
var piclockRefComp = app.project.items.addComp(
|
|
ctx.shotCode + "_PICLOCK_REF", width, height, 1, refOut - refIn, fps);
|
|
if (previewsFolder) {
|
|
piclockRefComp.parentFolder = previewsFolder;
|
|
}
|
|
var piclockLayer = piclockRefComp.layers.add(piclockFootage);
|
|
var piclockScale = Math.max(
|
|
piclockRefComp.width / piclockFootage.width,
|
|
piclockRefComp.height / piclockFootage.height
|
|
) * 100;
|
|
piclockLayer.property("Scale").setValue([piclockScale, piclockScale]);
|
|
piclockLayer.startTime = -refIn;
|
|
piclockLayer.inPoint = 0;
|
|
piclockLayer.outPoint = refOut - refIn;
|
|
|
|
if (queueOutput(piclockRefComp, ctx.mp4Template, ctx.piclockRefOutput, "piclockRef")) {
|
|
result.piclockRefBuilt = true;
|
|
} else {
|
|
result.piclockSkippedReason = "no mp4 template or output path configured";
|
|
}
|
|
} else if (!result.piclockSkippedReason) {
|
|
result.piclockSkippedReason = "picture-lock file not found";
|
|
}
|
|
} catch (piclockError) {
|
|
warn("Picture lock: reference render skipped — " +
|
|
(piclockError && piclockError.toString ? piclockError.toString() : String(piclockError)));
|
|
result.piclockSkippedReason = "error building the reference render";
|
|
result.piclockRefBuilt = false;
|
|
}
|
|
}
|
|
|
|
// 6. Queue both outputs against the same preview comp. aerender will
|
|
// render the whole queue in one launch.
|
|
queueOutput(previewComp, ctx.movTemplate, ctx.movOutput, "mov");
|
|
queueOutput(previewComp, ctx.mp4Template, ctx.mp4Output, "mp4");
|
|
|
|
if (result.queued.length < 1) {
|
|
throw new Error("Nothing was queued — check output module templates");
|
|
}
|
|
|
|
// 7. Save the throwaway project the worker will hand to aerender
|
|
var tempFile = new File(ctx.tempAep);
|
|
var tempParent = tempFile.parent;
|
|
if (tempParent && !tempParent.exists) {
|
|
tempParent.create();
|
|
}
|
|
app.project.save(tempFile);
|
|
result.tempAep = ctx.tempAep;
|
|
result.ok = true;
|
|
} catch (buildError) {
|
|
result.ok = false;
|
|
result.error = buildError && buildError.toString ? buildError.toString() : String(buildError);
|
|
try {
|
|
if (buildError && buildError.line) {
|
|
result.error += " (line " + buildError.line + ")";
|
|
}
|
|
} catch (lineError) {
|
|
}
|
|
}
|
|
|
|
try {
|
|
app.endSuppressDialogs(false);
|
|
} catch (endSuppressError) {
|
|
}
|
|
|
|
if (!resultPath && ctx && ctx.resultPath) {
|
|
resultPath = ctx.resultPath;
|
|
}
|
|
if (resultPath) {
|
|
writeTextFile(resultPath, toJSON(result));
|
|
}
|
|
|
|
// -noui leaves the app running otherwise; the worker waits on exit.
|
|
try {
|
|
app.quit();
|
|
} catch (quitError) {
|
|
}
|
|
}());
|