Skip to content

Commit ccfdbe1

Browse files
authored
Merge pull request #5 from JumpCutter/dk949/xml-fix
Fixing premiere pro (XML export)
2 parents 7f30aec + 7f6bbe7 commit ccfdbe1

5 files changed

Lines changed: 92 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Change Log
22

3+
## [0.0.6](https://github.com/JumpCutter/JC-ProjectExportAPI/compare/v0.0.5...v0.0.6) (2021-10-28)
4+
### Changes
5+
- __fixed__: messed up tags on last release...
6+
- __fixed__: Could not import into Premiere Pro. Needs more testing but is now more likely to work.
7+
8+
## [0.0.5](https://github.com/JumpCutter/JC-ProjectExportAPI/compare/v0.0.4...v0.0.5) (2021-10-28)
9+
### Changes
10+
- __fixed__: Could not import into Premiere Pro. Needs more testing but is now more likely to work.
11+
312
## [0.0.4](https://github.com/JumpCutter/JC-ProjectExportAPI/compare/v0.0.3...v0.0.4) (2021-10-18)
413
### Changes
514
- __BREAKING__: Constructor of Generator now takes second parameter - an `Options` object

src/XML/common.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
export class baseXMLBUilder {
22
protected _data: string[] = [];
3+
private tab: string = " ";
4+
private indent: number = 0;
35

46
protected putTag(name: string, attr: {[key: string]: string},
57
contents?: (() => void) | string) {
6-
const attrStr =
7-
Object.entries(attr)
8+
const attrStr = (() => {
9+
const a = Object.entries(attr)
810
.map(([key, value]) => {return `${key}="${value}"`;})
911
.join(' ');
12+
return a ? " " + a : "";
13+
})();
1014

1115
if (!contents) {
12-
this._data.push(`<${name} ${attrStr}/>`);
16+
this._data.push(`${this.tab.repeat(this.indent)}<${name}${attrStr} />`);
1317
return;
1418
}
1519

16-
this._data.push(`<${name} ${attrStr}>`);
20+
this._data.push(`${this.tab.repeat(this.indent)}<${name}${attrStr}>`);
1721
if (typeof contents === "function") {
22+
this.indent++;
1823
contents();
19-
this._data.push(`</${name}>`);
24+
this.indent--
25+
this._data.push(`${this.tab.repeat(this.indent)}</${name}>`);
2026
} else {
21-
const tmp: string[] = [contents, `</${name}>`];
22-
this._data[this._data.length - 1] += tmp.join("");
27+
this._data[this._data.length - 1] += `${contents}</${name}>`;
2328
}
2429
}
2530

src/XML/genXML.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import path from "path";
22
import {Generator} from "../baseGenerator";
33
import {xmlBuilder} from "./helpersXML";
44

5+
// https://fcp.co/final-cut-pro/tutorials/1912-demystifying-final-cut-pro-xmls-by-philip-hodgetts-and-gregory-clarke
56
export class XML extends Generator {
67
generate(): string {
78
const builder = new xmlBuilder();
8-
builder.buildContext(path.parse(this.clipName).name, this.frameRate, this.cuts[this.cuts.length - 1].end, () => {
9+
const [width, height] = (() => {
10+
const split = this.resolution?.split('x');
11+
return split ? split.map(parseInt) : [null, null];
12+
})();
13+
builder.buildContext(path.parse(this.clipName).name, width, height, this.frameRate, this.cuts[this.cuts.length - 1].end, () => {
914
this.cuts.forEach((cut) => {
10-
builder.putClipitem(this.clipName, cut.start, cut.end, 720, 1270); // FIXME: needs resolution!!!!!!!
15+
builder.putClipitem(this.clipName, cut.start, cut.end);
1116
});
1217
});
1318
return builder.data;

src/XML/helpersXML.ts

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,35 @@ export class xmlBuilder extends baseXMLBUilder {
55
private clipitemIDs: string[] = [];
66
private timeBase: number = -1;
77
private duration: string = "";
8-
public buildContext(name: string, frameRate: number, duration: number, l: () => void) {
8+
private registeredFile: boolean = false;
9+
private width: number | null = -1;
10+
private height: number | null = -1;
11+
public buildContext(name: string, width: number | null, height: number | null, frameRate: number, duration: number, l: () => void) {
12+
this.width = width;
13+
this.height = height;
914
this.timeBase = frameRate;
1015
this.duration = (frameRate * duration).toFixed(0);
11-
this._data.push("<?xml version=\"1.0\"?>");
1216
this.putTag("xmeml", {version: this.conf.version}, () => {
1317
this.putTag("project", {}, () => {
1418
this.putTag("name", {}, name);
1519
this.putTag("children", {}, () => {
1620
this.putTag("sequence", {id: this.conf.sequenceID}, () => {
1721
this.putTag("name", {}, name);
18-
this.putTag("duration", {}, this.duration);
22+
this.putTag("duration", {}, this.duration); // FIXME: Duration is only up to the end of the last cut
1923
this.putTag("rate", {}, () => {
2024
this.putTag("timebase", {}, this.timeBase.toString());
21-
this.putTag("ntsc", {}, this.putBool(this.conf.ntsc));
25+
this.putTag("ntsc", {}, this.putBool(this.conf.ntsc)); // FIXME: ???
2226
});
2327
this.putTag("media", {}, () => {
2428
this.putTag("video", {}, () => {
2529
this.putTag("track", {}, l);
30+
this.putTag("format", {}, () => {
31+
this.putTag("samplecharacteristics", {}, () => {
32+
this.height && this.putTag("height", {}, this.height.toString());
33+
this.width && this.putTag("width", {}, this.width.toString())
34+
this.putTag("pixelaspectratio", {}, this.conf.pixelAspectRatio);
35+
})
36+
});
2637
});
2738
this.putTag("audio", {}, () => {
2839
this.putTag("track", {}, () => {
@@ -33,51 +44,44 @@ export class xmlBuilder extends baseXMLBUilder {
3344

3445
});
3546
});
47+
this.putTag("timecode", {}, () => {
48+
this.putTag("rate", {}, () => {
49+
this.putTag("timebase", {}, this.timeBase.toString());
50+
this.putTag("ntsc", {}, this.putBool(this.conf.ntsc));
51+
});
52+
this.putTag("string", {}, this.conf.zeroTimecode); // why???
53+
this.putTag("frame", {}, this.conf.zeroFrame);
54+
this.putTag("displayformat", {}, this.conf.displayFormat);
55+
});
3656
});
3757
});
3858
});
3959
});
4060
}
4161

42-
public putClipitem(filePath: string, start: number, end: number, height: number, width: number) {
62+
public putClipitem(filePath: string, start: number, end: number) {
4363
const rate = () => {
4464
this.putTag("timebase", {}, this.timeBase.toString());
4565
this.putTag("ntsc", {}, this.putBool(this.conf.ntsc));
4666
};
47-
48-
49-
this.putTag("clipitem", {
50-
frameBlend: this.putBool(this.conf.frameBlend),
51-
id: this.genID(),
52-
}, () => {
67+
this.putTag("clipitem", {frameBlend: this.putBool(this.conf.frameBlend), id: this.genID(), }, () => {
5368
this.putTag("media", {}, () => {
5469
this.putTag("video", {}, () => {
5570
this.putTag("samplecharacteristics", {}, () => {
56-
this.putTag("height", {}, height.toString());
57-
this.putTag("width", {}, width.toString());
58-
});
59-
});
60-
});
61-
this.putTag("file", {id: this.conf.fileID}, () => {
62-
this.putTag("pathurl", {}, filePath);
63-
this.putTag("name", {}, path.basename(filePath));
64-
this.putTag("rate", {}, rate);
65-
this.putTag("duration", {}, this.duration);
66-
this.putTag("timecode", {}, () => {
67-
this.putTag("rate", {}, rate);
68-
this.putTag("string", {}, this.conf.zeroTimecode); // why???
69-
this.putTag("frame", {}, this.conf.zeroFrame);
70-
this.putTag("displayformat", {}, this.conf.displayFormat);
71-
this.putTag("media", {}, () => {
72-
this.putTag("video", {});
73-
this.putTag("audio", {});
71+
this.height && this.putTag("height", {}, this.height.toString());
72+
this.width && this.putTag("width", {}, this.width.toString())
7473
});
7574
});
7675
});
76+
this.putTag("file", {id: this.conf.fileID}, this.genFile(filePath, rate));
7777
this.putTag("name", {}, filePath);
7878
this.putTag("rate", {}, rate);
79-
this.putTag("start", {}, (start * this.timeBase).toFixed());
80-
this.putTag("end", {}, (end * this.timeBase).toFixed());
79+
this.putTag("rate", {}, rate); // XXX: no idea. Original did this and original works so ¯\_(ツ)_/¯
80+
const startF = start * this.timeBase;
81+
const endF = end * this.timeBase;
82+
this.putTag("duration", {}, (endF - startF).toFixed());
83+
this.putTag("start", {}, startF.toFixed());
84+
this.putTag("end", {}, endF.toFixed());
8185
});
8286
}
8387

@@ -91,6 +95,32 @@ export class xmlBuilder extends baseXMLBUilder {
9195
return id;
9296
}
9397

98+
private genFile(filePath: string, rate: () => void): (() => void) | undefined {
99+
/*
100+
XXX: this looks weird, but it is being passed to a function with an optional parameter,
101+
so it's expecting another function or undefined.
102+
*/
103+
if (!this.registeredFile) {
104+
this.registeredFile = true;
105+
return () => {
106+
this.putTag("pathurl", {}, filePath);
107+
this.putTag("name", {}, path.basename(filePath));
108+
this.putTag("rate", {}, rate);
109+
this.putTag("duration", {}, this.duration);
110+
this.putTag("timecode", {}, () => {
111+
this.putTag("rate", {}, rate);
112+
this.putTag("string", {}, this.conf.zeroTimecode); // why???
113+
this.putTag("frame", {}, this.conf.zeroFrame);
114+
this.putTag("displayformat", {}, this.conf.displayFormat);
115+
});
116+
this.putTag("media", {}, () => {
117+
this.putTag("video", {});
118+
this.putTag("audio", {});
119+
});
120+
};
121+
}
122+
}
123+
94124

95125
private conf = {
96126
version: "4",
@@ -101,6 +131,7 @@ export class xmlBuilder extends baseXMLBUilder {
101131
zeroTimecode: "00:00:00:00",
102132
zeroFrame: "0",
103133
displayFormat: "NDF",
134+
pixelAspectRatio: 1.0.toFixed(1)
104135
} as const;
105136

106137
}

src/baseGenerator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export abstract class Generator {
77
protected frameRate: number;
88
protected clipName: string;
99
protected options: Options;
10+
protected resolution: string | null;
1011

1112

1213
constructor(project: Project, options: Options) {
@@ -20,6 +21,7 @@ export abstract class Generator {
2021
}
2122
this.frameRate = this.project.frameRate;
2223
this.clipName = this.layer.sourceFile;
24+
this.resolution = this.project.resolution;
2325
}
2426

2527

0 commit comments

Comments
 (0)