Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Change Log

## [0.0.6](https://github.com/JumpCutter/JC-ProjectExportAPI/compare/v0.0.5...v0.0.6) (2021-10-28)
### Changes
- __fixed__: messed up tags on last release...
- __fixed__: Could not import into Premiere Pro. Needs more testing but is now more likely to work.

## [0.0.5](https://github.com/JumpCutter/JC-ProjectExportAPI/compare/v0.0.4...v0.0.5) (2021-10-28)
### Changes
- __fixed__: Could not import into Premiere Pro. Needs more testing but is now more likely to work.

## [0.0.4](https://github.com/JumpCutter/JC-ProjectExportAPI/compare/v0.0.3...v0.0.4) (2021-10-18)
### Changes
- __BREAKING__: Constructor of Generator now takes second parameter - an `Options` object
Expand Down
19 changes: 12 additions & 7 deletions src/XML/common.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
export class baseXMLBUilder {
protected _data: string[] = [];
private tab: string = " ";
private indent: number = 0;

protected putTag(name: string, attr: {[key: string]: string},
contents?: (() => void) | string) {
const attrStr =
Object.entries(attr)
const attrStr = (() => {
const a = Object.entries(attr)
.map(([key, value]) => {return `${key}="${value}"`;})
.join(' ');
return a ? " " + a : "";
})();

if (!contents) {
this._data.push(`<${name} ${attrStr}/>`);
this._data.push(`${this.tab.repeat(this.indent)}<${name}${attrStr} />`);
return;
}

this._data.push(`<${name} ${attrStr}>`);
this._data.push(`${this.tab.repeat(this.indent)}<${name}${attrStr}>`);
if (typeof contents === "function") {
this.indent++;
contents();
this._data.push(`</${name}>`);
this.indent--
this._data.push(`${this.tab.repeat(this.indent)}</${name}>`);
} else {
const tmp: string[] = [contents, `</${name}>`];
this._data[this._data.length - 1] += tmp.join("");
this._data[this._data.length - 1] += `${contents}</${name}>`;
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/XML/genXML.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import path from "path";
import {Generator} from "../baseGenerator";
import {xmlBuilder} from "./helpersXML";

// https://fcp.co/final-cut-pro/tutorials/1912-demystifying-final-cut-pro-xmls-by-philip-hodgetts-and-gregory-clarke
export class XML extends Generator {
generate(): string {
const builder = new xmlBuilder();
builder.buildContext(path.parse(this.clipName).name, this.frameRate, this.cuts[this.cuts.length - 1].end, () => {
const [width, height] = (() => {
const split = this.resolution?.split('x');
return split ? split.map(parseInt) : [null, null];
})();
builder.buildContext(path.parse(this.clipName).name, width, height, this.frameRate, this.cuts[this.cuts.length - 1].end, () => {
this.cuts.forEach((cut) => {
builder.putClipitem(this.clipName, cut.start, cut.end, 720, 1270); // FIXME: needs resolution!!!!!!!
builder.putClipitem(this.clipName, cut.start, cut.end);
});
});
return builder.data;
Expand Down
93 changes: 62 additions & 31 deletions src/XML/helpersXML.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,35 @@ export class xmlBuilder extends baseXMLBUilder {
private clipitemIDs: string[] = [];
private timeBase: number = -1;
private duration: string = "";
public buildContext(name: string, frameRate: number, duration: number, l: () => void) {
private registeredFile: boolean = false;
private width: number | null = -1;
private height: number | null = -1;
public buildContext(name: string, width: number | null, height: number | null, frameRate: number, duration: number, l: () => void) {
this.width = width;
this.height = height;
this.timeBase = frameRate;
this.duration = (frameRate * duration).toFixed(0);
this._data.push("<?xml version=\"1.0\"?>");
this.putTag("xmeml", {version: this.conf.version}, () => {
this.putTag("project", {}, () => {
this.putTag("name", {}, name);
this.putTag("children", {}, () => {
this.putTag("sequence", {id: this.conf.sequenceID}, () => {
this.putTag("name", {}, name);
this.putTag("duration", {}, this.duration);
this.putTag("duration", {}, this.duration); // FIXME: Duration is only up to the end of the last cut
this.putTag("rate", {}, () => {
this.putTag("timebase", {}, this.timeBase.toString());
this.putTag("ntsc", {}, this.putBool(this.conf.ntsc));
this.putTag("ntsc", {}, this.putBool(this.conf.ntsc)); // FIXME: ???
});
this.putTag("media", {}, () => {
this.putTag("video", {}, () => {
this.putTag("track", {}, l);
this.putTag("format", {}, () => {
this.putTag("samplecharacteristics", {}, () => {
this.height && this.putTag("height", {}, this.height.toString());
this.width && this.putTag("width", {}, this.width.toString())
this.putTag("pixelaspectratio", {}, this.conf.pixelAspectRatio);
})
});
});
this.putTag("audio", {}, () => {
this.putTag("track", {}, () => {
Expand All @@ -33,51 +44,44 @@ export class xmlBuilder extends baseXMLBUilder {

});
});
this.putTag("timecode", {}, () => {
this.putTag("rate", {}, () => {
this.putTag("timebase", {}, this.timeBase.toString());
this.putTag("ntsc", {}, this.putBool(this.conf.ntsc));
});
this.putTag("string", {}, this.conf.zeroTimecode); // why???
this.putTag("frame", {}, this.conf.zeroFrame);
this.putTag("displayformat", {}, this.conf.displayFormat);
});
});
});
});
});
}

public putClipitem(filePath: string, start: number, end: number, height: number, width: number) {
public putClipitem(filePath: string, start: number, end: number) {
const rate = () => {
this.putTag("timebase", {}, this.timeBase.toString());
this.putTag("ntsc", {}, this.putBool(this.conf.ntsc));
};


this.putTag("clipitem", {
frameBlend: this.putBool(this.conf.frameBlend),
id: this.genID(),
}, () => {
this.putTag("clipitem", {frameBlend: this.putBool(this.conf.frameBlend), id: this.genID(), }, () => {
this.putTag("media", {}, () => {
this.putTag("video", {}, () => {
this.putTag("samplecharacteristics", {}, () => {
this.putTag("height", {}, height.toString());
this.putTag("width", {}, width.toString());
});
});
});
this.putTag("file", {id: this.conf.fileID}, () => {
this.putTag("pathurl", {}, filePath);
this.putTag("name", {}, path.basename(filePath));
this.putTag("rate", {}, rate);
this.putTag("duration", {}, this.duration);
this.putTag("timecode", {}, () => {
this.putTag("rate", {}, rate);
this.putTag("string", {}, this.conf.zeroTimecode); // why???
this.putTag("frame", {}, this.conf.zeroFrame);
this.putTag("displayformat", {}, this.conf.displayFormat);
this.putTag("media", {}, () => {
this.putTag("video", {});
this.putTag("audio", {});
this.height && this.putTag("height", {}, this.height.toString());
this.width && this.putTag("width", {}, this.width.toString())
});
});
});
this.putTag("file", {id: this.conf.fileID}, this.genFile(filePath, rate));
this.putTag("name", {}, filePath);
this.putTag("rate", {}, rate);
this.putTag("start", {}, (start * this.timeBase).toFixed());
this.putTag("end", {}, (end * this.timeBase).toFixed());
this.putTag("rate", {}, rate); // XXX: no idea. Original did this and original works so ¯\_(ツ)_/¯
const startF = start * this.timeBase;
const endF = end * this.timeBase;
this.putTag("duration", {}, (endF - startF).toFixed());
this.putTag("start", {}, startF.toFixed());
this.putTag("end", {}, endF.toFixed());
});
}

Expand All @@ -91,6 +95,32 @@ export class xmlBuilder extends baseXMLBUilder {
return id;
}

private genFile(filePath: string, rate: () => void): (() => void) | undefined {
/*
XXX: this looks weird, but it is being passed to a function with an optional parameter,
so it's expecting another function or undefined.
*/
if (!this.registeredFile) {
this.registeredFile = true;
return () => {
this.putTag("pathurl", {}, filePath);
this.putTag("name", {}, path.basename(filePath));
this.putTag("rate", {}, rate);
this.putTag("duration", {}, this.duration);
this.putTag("timecode", {}, () => {
this.putTag("rate", {}, rate);
this.putTag("string", {}, this.conf.zeroTimecode); // why???
this.putTag("frame", {}, this.conf.zeroFrame);
this.putTag("displayformat", {}, this.conf.displayFormat);
});
this.putTag("media", {}, () => {
this.putTag("video", {});
this.putTag("audio", {});
});
};
}
}


private conf = {
version: "4",
Expand All @@ -101,6 +131,7 @@ export class xmlBuilder extends baseXMLBUilder {
zeroTimecode: "00:00:00:00",
zeroFrame: "0",
displayFormat: "NDF",
pixelAspectRatio: 1.0.toFixed(1)
} as const;

}
2 changes: 2 additions & 0 deletions src/baseGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export abstract class Generator {
protected frameRate: number;
protected clipName: string;
protected options: Options;
protected resolution: string | null;


constructor(project: Project, options: Options) {
Expand All @@ -20,6 +21,7 @@ export abstract class Generator {
}
this.frameRate = this.project.frameRate;
this.clipName = this.layer.sourceFile;
this.resolution = this.project.resolution;
}


Expand Down