Initial commit. Working empty project generation and file bin
This commit is contained in:
parent
3d6ed22e5f
commit
13cd51fa28
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@ lib
|
|||||||
coverage
|
coverage
|
||||||
@types
|
@types
|
||||||
|
|
||||||
|
/.log/
|
||||||
|
/*.kdenlive
|
||||||
|
13692
package-lock.json
generated
13692
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
61
package.json
61
package.json
@ -1,31 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "module-starter",
|
"name": "kdenlive",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "module template",
|
"description": "Create kdenlive projects from within JS",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node test.js",
|
"test": "node test.js",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"prepare": "npm run build",
|
"prepare": "npm run build",
|
||||||
"test-reports": "npm run build && rm -fr .xunit coverage && npm run test -- --cover --test-report"
|
"test-reports": "npm run build && rm -fr .xunit coverage && npm run test -- --cover --test-report"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^9.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
||||||
"@typescript-eslint/parser": "^4.29.3",
|
"@typescript-eslint/parser": "^4.29.3",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^3.4.1",
|
"eslint-plugin-prettier": "^3.4.1",
|
||||||
"eslint-plugin-with-tsc-error": "^0.0.7",
|
"eslint-plugin-with-tsc-error": "^0.0.7",
|
||||||
"mocha": "^9.1.1",
|
"mocha": "^9.1.1",
|
||||||
"mri": "^1.1.6",
|
"mri": "^1.1.6",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"source-map-support": "^0.5.19",
|
"source-map-support": "^0.5.19",
|
||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5"
|
||||||
},
|
},
|
||||||
"types": "./@types/index.d.ts"
|
"types": "./@types/index.d.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"zx": "^4.2.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,61 @@
|
|||||||
import { Example } from "./index";
|
import { $ } from "zx";
|
||||||
import * as assert from "assert";
|
import Project from "./kdenlive";
|
||||||
|
|
||||||
describe("Example", () => {
|
describe("Kdenlive", () => {
|
||||||
describe("example", () => {
|
describe("example", () => {
|
||||||
it("should equal 'example'", () => {
|
it("should equal 'example'", async () => {
|
||||||
const example = new Example();
|
const project = new Project(30);
|
||||||
assert.equal(example.example(), "example");
|
|
||||||
|
project.addProducer("/home/kuba/Videos/5min.mp4");
|
||||||
|
project.addProducer("/home/kuba/Videos/5min.wav");
|
||||||
|
|
||||||
|
await $`echo ${await project.toXML()} > project-generated.kdenlive`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("tracks", () => {
|
||||||
|
it("should generate a project with three audio tracks", async () => {
|
||||||
|
const project = new Project(30);
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
project.addAudioTractor();
|
||||||
|
}
|
||||||
|
|
||||||
|
await $`echo ${await project.toXML()} > three-audio-tracks.kdenlive`;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate a project with one video track", async () => {
|
||||||
|
const project = new Project(30);
|
||||||
|
project.addVideoTractor();
|
||||||
|
|
||||||
|
await $`echo ${await project.toXML()} > 1v-tracks.kdenlive`;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate a project with one video track and one audio track", async () => {
|
||||||
|
const project = new Project(30);
|
||||||
|
project.addVideoTractor();
|
||||||
|
project.addAudioTractor();
|
||||||
|
|
||||||
|
await $`echo ${await project.toXML()} > 1a1v-tracks.kdenlive`;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate a project with three audio and video tracks", async () => {
|
||||||
|
const project = new Project(30);
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
project.addVideoTractor();
|
||||||
|
project.addAudioTractor();
|
||||||
|
}
|
||||||
|
|
||||||
|
await $`echo ${await project.toXML()} > 3a3v-tracks.kdenlive`;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate a project with 20 audio and video tracks", async () => {
|
||||||
|
const project = new Project(30);
|
||||||
|
for (let i = 1; i <= 20; i++) {
|
||||||
|
project.addVideoTractor();
|
||||||
|
project.addAudioTractor();
|
||||||
|
}
|
||||||
|
|
||||||
|
await $`echo ${await project.toXML()} > 20a20v-tracks.kdenlive`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
export class Example {
|
|
||||||
example(): string {
|
|
||||||
return "example";
|
|
||||||
}
|
|
||||||
}
|
|
71
src/kdenlive.ts
Normal file
71
src/kdenlive.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { BlackTrack, ConcreteProducer, Producer } from "./producer";
|
||||||
|
import { AudioTractor, trackIndexGen, Tractor, VideoTractor } from "./tractor";
|
||||||
|
|
||||||
|
export default class Project {
|
||||||
|
producers: Producer[] = [];
|
||||||
|
tractors: Tractor[] = [];
|
||||||
|
constructor(public fps: number) {
|
||||||
|
this.producers.push(new BlackTrack());
|
||||||
|
}
|
||||||
|
|
||||||
|
addProducer(file: string): ConcreteProducer {
|
||||||
|
const producer = new ConcreteProducer(file);
|
||||||
|
this.producers.push(producer);
|
||||||
|
return producer;
|
||||||
|
}
|
||||||
|
|
||||||
|
addAudioTractor(): AudioTractor {
|
||||||
|
const tractor = new AudioTractor();
|
||||||
|
this.tractors.push(tractor);
|
||||||
|
return tractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
addVideoTractor(): AudioTractor {
|
||||||
|
const tractor = new VideoTractor();
|
||||||
|
this.tractors.push(tractor);
|
||||||
|
return tractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
async toXML() {
|
||||||
|
return `<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<mlt LC_NUMERIC="C" producer="main_bin" version="7.0.0" root="/home/kuba/Downloads">
|
||||||
|
<profile frame_rate_num="${
|
||||||
|
this.fps
|
||||||
|
}" sample_aspect_num="1" display_aspect_den="9" colorspace="601" progressive="1" description="1920x1080 29.90fps" display_aspect_num="16" frame_rate_den="1" width="1920" height="1080" sample_aspect_den="1"/>
|
||||||
|
${(
|
||||||
|
await Promise.all(
|
||||||
|
this.producers.map((producer) => producer.toXML(this.fps))
|
||||||
|
)
|
||||||
|
).join("\n")}
|
||||||
|
<playlist id="main_bin">
|
||||||
|
<property name="kdenlive:docproperties.activeTrack">2</property>
|
||||||
|
<property name="kdenlive:docproperties.audioChannels">2</property>
|
||||||
|
<property name="kdenlive:docproperties.audioTarget">1</property>
|
||||||
|
<property name="kdenlive:docproperties.disablepreview">0</property>
|
||||||
|
<property name="kdenlive:docproperties.documentid">1633881496938</property>
|
||||||
|
<property name="kdenlive:docproperties.groups">[
|
||||||
|
]
|
||||||
|
</property>
|
||||||
|
<property name="kdenlive:docproperties.kdenliveversion">21.08.1</property>
|
||||||
|
<property name="kdenlive:docproperties.version">1.02</property>
|
||||||
|
<property name="kdenlive:expandedFolders"/>
|
||||||
|
<property name="kdenlive:documentnotes"/>
|
||||||
|
<property name="xml_retain">1</property>
|
||||||
|
${this.producers
|
||||||
|
.filter((e) => !(e instanceof BlackTrack))
|
||||||
|
.map((producer) => `<entry producer="producer${producer.index}"/>`)
|
||||||
|
.join("\n")}
|
||||||
|
</playlist>
|
||||||
|
${this.tractors.map((tractor) => tractor.toXML()).join("\n")}
|
||||||
|
<tractor id="tractor${
|
||||||
|
trackIndexGen.next().value
|
||||||
|
}" in="00:00:00.000" out="00:08:20.000">
|
||||||
|
<track producer="black_track"/>
|
||||||
|
${this.tractors
|
||||||
|
.map((tractor) => `<track producer="tractor${tractor.index}"/>`)
|
||||||
|
.join("\n")}
|
||||||
|
</tractor>
|
||||||
|
</mlt>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
23
src/playlist.ts
Normal file
23
src/playlist.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { makeIDGen } from "./util";
|
||||||
|
|
||||||
|
const playlistIndexGen = makeIDGen(0);
|
||||||
|
|
||||||
|
export abstract class Playlist {
|
||||||
|
constructor(public index = playlistIndexGen.next().value) {}
|
||||||
|
|
||||||
|
abstract toXML(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AudioPlaylist extends Playlist {
|
||||||
|
toXML() {
|
||||||
|
return /* HTML */ ` <playlist id="playlist${this.index}">
|
||||||
|
<property name="kdenlive:audio_track">1</property>
|
||||||
|
</playlist>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VideoPlaylist extends Playlist {
|
||||||
|
toXML() {
|
||||||
|
return /* HTML */ ` <playlist id="playlist${this.index}"></playlist>`;
|
||||||
|
}
|
||||||
|
}
|
90
src/producer.ts
Normal file
90
src/producer.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { makeIDGen } from "./util";
|
||||||
|
import { $ } from "zx";
|
||||||
|
|
||||||
|
const producerIndexGen = makeIDGen(0);
|
||||||
|
|
||||||
|
export abstract class Producer {
|
||||||
|
index: number;
|
||||||
|
constructor(public path: string) {
|
||||||
|
this.index = producerIndexGen.next().value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNativeMltXml(fps: number) {
|
||||||
|
const xml = (
|
||||||
|
await $`melt ${
|
||||||
|
this.path
|
||||||
|
} -consumer xml ${`frame_rate_num=${fps}`} | htmlq producer`
|
||||||
|
).stdout;
|
||||||
|
return xml.replace("producer0", `producer${this.index}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async toXML(fps: number) {
|
||||||
|
return await this.getNativeMltXml(fps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConcreteProducer extends Producer {
|
||||||
|
video_only: VideoOnlyProducer;
|
||||||
|
audio_only: AudioOnlyProducer;
|
||||||
|
constructor(path: string) {
|
||||||
|
super(path);
|
||||||
|
this.video_only = new VideoOnlyProducer(path);
|
||||||
|
this.audio_only = new AudioOnlyProducer(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async toXML(fps: number) {
|
||||||
|
return (
|
||||||
|
await Promise.all([
|
||||||
|
super.toXML(fps),
|
||||||
|
this.video_only.toXML(fps),
|
||||||
|
this.audio_only.toXML(fps),
|
||||||
|
])
|
||||||
|
).join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoOnlyProducer extends Producer {
|
||||||
|
async toXML(fps: number) {
|
||||||
|
const xml = await super.toXML(fps);
|
||||||
|
return xml.replace(
|
||||||
|
"</producer>",
|
||||||
|
` <property name="set.test_audio">0</property>
|
||||||
|
<property name="set.test_image">1</property>
|
||||||
|
</producer>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AudioOnlyProducer extends Producer {
|
||||||
|
async toXML(fps: number) {
|
||||||
|
const xml = await super.toXML(fps);
|
||||||
|
return xml.replace(
|
||||||
|
"</producer>",
|
||||||
|
` <property name="set.test_audio">1</property>
|
||||||
|
<property name="set.test_image">0</property>
|
||||||
|
</producer>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BlackTrack extends Producer {
|
||||||
|
constructor() {
|
||||||
|
super("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async toXML() {
|
||||||
|
return /* HTML */ `<producer
|
||||||
|
id="black_track"
|
||||||
|
in="00:00:00.000"
|
||||||
|
out="00:16:43.344"
|
||||||
|
>
|
||||||
|
<property name="length">2147483647</property>
|
||||||
|
<property name="eof">continue</property>
|
||||||
|
<property name="resource">black</property>
|
||||||
|
<property name="aspect_ratio">1</property>
|
||||||
|
<property name="mlt_service">color</property>
|
||||||
|
<property name="mlt_image_format">rgb24a</property>
|
||||||
|
<property name="set.test_audio">0</property>
|
||||||
|
</producer>`;
|
||||||
|
}
|
||||||
|
}
|
64
src/tractor.ts
Normal file
64
src/tractor.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { AudioPlaylist, Playlist, VideoPlaylist } from "./playlist";
|
||||||
|
import { makeIDGen } from "./util";
|
||||||
|
|
||||||
|
export const trackIndexGen = makeIDGen(0);
|
||||||
|
|
||||||
|
export abstract class Tractor {
|
||||||
|
main_playlist: Playlist;
|
||||||
|
secondary_playlist: Playlist; // not sure what these are for, but Kdenlive generates them, sooo
|
||||||
|
public index = trackIndexGen.next().value;
|
||||||
|
|
||||||
|
abstract toXML(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AudioTractor extends Tractor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.main_playlist = new AudioPlaylist();
|
||||||
|
this.secondary_playlist = new AudioPlaylist();
|
||||||
|
}
|
||||||
|
|
||||||
|
toXML() {
|
||||||
|
return [
|
||||||
|
this.main_playlist.toXML(),
|
||||||
|
this.secondary_playlist.toXML(),
|
||||||
|
/* HTML */ ` <tractor id="tractor${this.index}" in="00:00:00.000">
|
||||||
|
<property name="kdenlive:audio_track">1</property>
|
||||||
|
<property name="kdenlive:trackheight">67</property>
|
||||||
|
<property name="kdenlive:timeline_active">1</property>
|
||||||
|
<property name="kdenlive:collapsed">0</property>
|
||||||
|
<property name="kdenlive:thumbs_format" />
|
||||||
|
<property name="kdenlive:audio_rec" />
|
||||||
|
<track
|
||||||
|
hide="video"
|
||||||
|
producer="playlist${this.main_playlist.index}"
|
||||||
|
/>
|
||||||
|
<track
|
||||||
|
hide="video"
|
||||||
|
producer="playlist${this.secondary_playlist.index}"
|
||||||
|
/>
|
||||||
|
</tractor>`,
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VideoTractor extends Tractor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.main_playlist = new VideoPlaylist();
|
||||||
|
this.secondary_playlist = new VideoPlaylist();
|
||||||
|
}
|
||||||
|
|
||||||
|
toXML() {
|
||||||
|
return [
|
||||||
|
this.main_playlist.toXML(),
|
||||||
|
this.secondary_playlist.toXML(),
|
||||||
|
/* HTML */ ` <tractor id="tractor${this.index}" in="00:00:00.000">
|
||||||
|
<property name="kdenlive:trackheight">67</property>
|
||||||
|
<property name="kdenlive:timeline_active">1</property>
|
||||||
|
<track producer="playlist${this.main_playlist.index}" />
|
||||||
|
<track producer="playlist${this.secondary_playlist.index}" />
|
||||||
|
</tractor>`,
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
}
|
28
src/util.ts
Normal file
28
src/util.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
const HOUR = 60 * 60;
|
||||||
|
const MINUTE = 60;
|
||||||
|
|
||||||
|
export function formatDuration(float_s: number) {
|
||||||
|
const hours = Math.floor(float_s / HOUR);
|
||||||
|
float_s = float_s - hours * HOUR;
|
||||||
|
const minutes = Math.floor(float_s / MINUTE);
|
||||||
|
const seconds = float_s - minutes * MINUTE;
|
||||||
|
return `${twoDigits(hours)}:${twoDigits(minutes)}:${twoDigits(seconds, 5)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function twoDigits(number: number, decimal = 0) {
|
||||||
|
let [int, dec] = number.toFixed(decimal).split(".");
|
||||||
|
dec = dec || "";
|
||||||
|
if (dec == "") {
|
||||||
|
return int.padStart(2, "0");
|
||||||
|
} else {
|
||||||
|
return int.padStart(2, "0") + "." + dec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* makeIDGen(first = 1): Generator<number, number> {
|
||||||
|
let i = first;
|
||||||
|
while (true) {
|
||||||
|
yield i;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user