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
|
||||
@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",
|
||||
"version": "0.0.1",
|
||||
"description": "module template",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"test": "node test.js",
|
||||
"build": "tsc",
|
||||
"prepare": "npm run build",
|
||||
"test-reports": "npm run build && rm -fr .xunit coverage && npm run test -- --cover --test-report"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
||||
"@typescript-eslint/parser": "^4.29.3",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"eslint-plugin-with-tsc-error": "^0.0.7",
|
||||
"mocha": "^9.1.1",
|
||||
"mri": "^1.1.6",
|
||||
"nyc": "^15.1.0",
|
||||
"prettier": "^2.3.2",
|
||||
"source-map-support": "^0.5.19",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"types": "./@types/index.d.ts"
|
||||
"name": "kdenlive",
|
||||
"version": "0.0.1",
|
||||
"description": "Create kdenlive projects from within JS",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"test": "node test.js",
|
||||
"build": "tsc",
|
||||
"prepare": "npm run build",
|
||||
"test-reports": "npm run build && rm -fr .xunit coverage && npm run test -- --cover --test-report"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
||||
"@typescript-eslint/parser": "^4.29.3",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"eslint-plugin-with-tsc-error": "^0.0.7",
|
||||
"mocha": "^9.1.1",
|
||||
"mri": "^1.1.6",
|
||||
"nyc": "^15.1.0",
|
||||
"prettier": "^2.3.2",
|
||||
"source-map-support": "^0.5.19",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"types": "./@types/index.d.ts",
|
||||
"dependencies": {
|
||||
"zx": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,61 @@
|
||||
import { Example } from "./index";
|
||||
import * as assert from "assert";
|
||||
import { $ } from "zx";
|
||||
import Project from "./kdenlive";
|
||||
|
||||
describe("Example", () => {
|
||||
describe("Kdenlive", () => {
|
||||
describe("example", () => {
|
||||
it("should equal 'example'", () => {
|
||||
const example = new Example();
|
||||
assert.equal(example.example(), "example");
|
||||
it("should equal 'example'", async () => {
|
||||
const project = new Project(30);
|
||||
|
||||
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