Initial commit. Working empty project generation and file bin

This commit is contained in:
Kuba Orlik 2021-10-25 20:55:13 +02:00
parent 3d6ed22e5f
commit 13cd51fa28
10 changed files with 7440 additions and 6658 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ lib
coverage coverage
@types @types
/.log/
/*.kdenlive

624
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"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",
@ -27,5 +27,8 @@
"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"
}
} }

View File

@ -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`;
}); });
}); });
}); });

View File

@ -1,5 +0,0 @@
export class Example {
example(): string {
return "example";
}
}

71
src/kdenlive.ts Normal file
View 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
View 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
View 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
View 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
View 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++;
}
}