Working versino after a small refactor
This commit is contained in:
parent
eaec1a7f86
commit
3deb39fabb
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/.log/
|
13
audio_props.mjs
Normal file
13
audio_props.mjs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const audio_props = {
|
||||||
|
"stream.handler_name.markup": "SoundHandler",
|
||||||
|
"stream.type": "audio",
|
||||||
|
"codec.sample_fmt": (metadata) => {
|
||||||
|
const type = metadata.data.Format_Settings_Sign == "Signed" ? "s" : "u";
|
||||||
|
const bit_depth = metadata.data.BitDepth;
|
||||||
|
return `${type}${bit_depth}`;
|
||||||
|
},
|
||||||
|
"codec.channels": (metadata) => metadata.data.Channels,
|
||||||
|
"codec.sample_rate": (metadata) => metadata.data.SamplingRate,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default audio_props;
|
104
kdenlive.mjs
Executable file
104
kdenlive.mjs
Executable file
@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/zx --quiet
|
||||||
|
|
||||||
|
import { formatDuration } from "./util.mjs";
|
||||||
|
import Producer from "./producer.mjs";
|
||||||
|
|
||||||
|
export default async function kdenliveProject(
|
||||||
|
source_files,
|
||||||
|
project_settings, // const project_settings = { fps: 30 };
|
||||||
|
clips
|
||||||
|
) {
|
||||||
|
const producers = await Promise.all(source_files.map(Producer.fromFile));
|
||||||
|
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="${
|
||||||
|
project_settings.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(
|
||||||
|
producers.map((producer) => producer.toXML(project_settings))
|
||||||
|
)
|
||||||
|
).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>
|
||||||
|
${producers
|
||||||
|
.map(
|
||||||
|
(producer, index) =>
|
||||||
|
`<entry producer="producer${index}" in="00:00:00.000" out="${producer.getDuration()}"/>`
|
||||||
|
)
|
||||||
|
.join("\n")}
|
||||||
|
</playlist>
|
||||||
|
<producer id="black_track" in="00:00:00.000" out="00:08:20.000">
|
||||||
|
<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>
|
||||||
|
<playlist id="playlist0">
|
||||||
|
<property name="kdenlive:audio_track">1</property>
|
||||||
|
</playlist>
|
||||||
|
<playlist id="playlist1">
|
||||||
|
<property name="kdenlive:audio_track">1</property>
|
||||||
|
</playlist>
|
||||||
|
<tractor id="tractor0" in="00:00:00.000">
|
||||||
|
<property name="kdenlive:audio_track">1</property>
|
||||||
|
<property name="kdenlive:timeline_active">1</property>
|
||||||
|
<track hide="video" producer="playlist0"/>
|
||||||
|
<track hide="video" producer="playlist1"/>
|
||||||
|
</tractor>
|
||||||
|
<playlist id="playlist2">
|
||||||
|
<property name="kdenlive:audio_track">1</property>
|
||||||
|
</playlist>
|
||||||
|
<playlist id="playlist3">
|
||||||
|
<property name="kdenlive:audio_track">1</property>
|
||||||
|
</playlist>
|
||||||
|
<tractor id="tractor1" in="00:00:00.000">
|
||||||
|
<property name="kdenlive:audio_track">1</property>
|
||||||
|
<track hide="video" producer="playlist2"/>
|
||||||
|
<track hide="video" producer="playlist3"/>
|
||||||
|
</tractor>
|
||||||
|
<playlist id="playlist4"/>
|
||||||
|
<playlist id="playlist5"/>
|
||||||
|
<tractor id="tractor2" in="00:00:00.000">
|
||||||
|
<track hide="audio" producer="playlist4"/>
|
||||||
|
<track hide="audio" producer="playlist5"/>
|
||||||
|
</tractor>
|
||||||
|
<playlist id="playlist6"/>
|
||||||
|
<playlist id="playlist7"/>
|
||||||
|
<tractor id="tractor3" in="00:00:00.000">
|
||||||
|
<track hide="audio" producer="playlist6"/>
|
||||||
|
<track hide="audio" producer="playlist7"/>
|
||||||
|
</tractor>
|
||||||
|
<tractor id="tractor4" in="00:00:00.000" out="00:08:20.000">
|
||||||
|
<track producer="black_track"/>
|
||||||
|
<track producer="tractor0"/>
|
||||||
|
<track producer="tractor1"/>
|
||||||
|
<track producer="tractor2"/>
|
||||||
|
<track producer="tractor3"/>
|
||||||
|
</tractor>
|
||||||
|
</mlt>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const project_content = await kdenliveProject(
|
||||||
|
["/home/kuba/Videos/5min.mp4", "/home/kuba/Videos/5min.wav"],
|
||||||
|
{
|
||||||
|
fps: 30,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await $`echo ${project_content} > project-generated.kdenlive`;
|
112
producer.mjs
Normal file
112
producer.mjs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
getStream,
|
||||||
|
indexOf,
|
||||||
|
renderAllProps,
|
||||||
|
getStreamIndex,
|
||||||
|
formatDuration,
|
||||||
|
} from "./util.mjs";
|
||||||
|
import { makeIDGen } from "./util.mjs";
|
||||||
|
import video_props from "./video_props.mjs";
|
||||||
|
import audio_props from "./audio_props.mjs";
|
||||||
|
|
||||||
|
const makeId = makeIDGen(1);
|
||||||
|
const producerIndexGen = makeIDGen(0);
|
||||||
|
|
||||||
|
export default class Producer {
|
||||||
|
constructor(metadata) {
|
||||||
|
this.metadata = metadata;
|
||||||
|
this.index = producerIndexGen.next().value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static prop_types = {
|
||||||
|
length: (metadata, project_settings) => {
|
||||||
|
const duration = getStream(metadata.track, "General").Duration;
|
||||||
|
return `${duration * project_settings.fps}`;
|
||||||
|
},
|
||||||
|
eof: "pause",
|
||||||
|
resource: (metadata) => metadata["@ref"],
|
||||||
|
audio_index: (metadata) => getStreamIndex(metadata.track, "Audio"),
|
||||||
|
video_index: (metadata) => getStreamIndex(metadata.track, "Video"),
|
||||||
|
mute_on_pause: "0",
|
||||||
|
mlt_service: "avformat-novalidate",
|
||||||
|
seekable: "1",
|
||||||
|
aspect_ratio: "1",
|
||||||
|
"kdenlive:clipname": "",
|
||||||
|
"kdenlive:folderid": "-1",
|
||||||
|
"kdenlive:audio_max0": "208",
|
||||||
|
"kdenlive:id": () => makeId.next().value,
|
||||||
|
"kdenlive:file_size": async (metadata) =>
|
||||||
|
(
|
||||||
|
await $`du --bytes ${metadata["@ref"]} | awk '{print $1}'`
|
||||||
|
).stdout.replace("\n", ""),
|
||||||
|
"kdenlive:file_hash": async (metadata) =>
|
||||||
|
(
|
||||||
|
await $`head -c 1000000 ${metadata["@ref"]} && tail -c 1000000 ${metadata["@ref"]}`.pipe(
|
||||||
|
$`md5sum | awk '{print $1}'`
|
||||||
|
)
|
||||||
|
).stdout.replace("\n", ""),
|
||||||
|
"meta.media.nb_streams": (metadata) => metadata.track.length - 1,
|
||||||
|
$$$video: async (metadata, project_settings) => {
|
||||||
|
const video_index = indexOf(metadata.track, (e) => e["@type"] == "Video");
|
||||||
|
if (video_index == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
$replace: await renderAllProps(
|
||||||
|
video_props,
|
||||||
|
{
|
||||||
|
index: video_index,
|
||||||
|
path: metadata["@ref"],
|
||||||
|
data: metadata.track[video_index],
|
||||||
|
},
|
||||||
|
project_settings,
|
||||||
|
`meta.media.${getStreamIndex(metadata.track, "Video")}.`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
$$$audio: async (metadata, project_settings) => {
|
||||||
|
const audio_index = indexOf(metadata.track, (e) => e["@type"] == "Audio");
|
||||||
|
if (audio_index == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
$replace: await renderAllProps(
|
||||||
|
audio_props,
|
||||||
|
{
|
||||||
|
index: audio_index,
|
||||||
|
path: metadata["@ref"],
|
||||||
|
data: metadata.track[audio_index],
|
||||||
|
},
|
||||||
|
project_settings,
|
||||||
|
`meta.media.${getStreamIndex(metadata.track, "Audio")}.`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static async fromFile(file_path) {
|
||||||
|
const metadata = JSON.parse(
|
||||||
|
(await $`mediainfo --Output=JSON ${file_path}`).stdout
|
||||||
|
).media;
|
||||||
|
return new Producer(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDuration() {
|
||||||
|
return formatDuration(
|
||||||
|
parseFloat(getStream(this.metadata.track, "General").Duration)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async toXML(project_settings) {
|
||||||
|
return `<producer id="producer${
|
||||||
|
this.index
|
||||||
|
}" in="00:00:00.000" out="${this.getDuration()}">
|
||||||
|
${await renderAllProps(
|
||||||
|
Producer.prop_types,
|
||||||
|
this.metadata,
|
||||||
|
project_settings
|
||||||
|
)}
|
||||||
|
</producer>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
84
util.mjs
Normal file
84
util.mjs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
export function indexOf(array, predicate) {
|
||||||
|
for (let i in array) {
|
||||||
|
if (predicate(array[i])) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderProperty(
|
||||||
|
name,
|
||||||
|
fn,
|
||||||
|
metadata,
|
||||||
|
project_settings,
|
||||||
|
prefix = ""
|
||||||
|
) {
|
||||||
|
let value;
|
||||||
|
if (typeof fn === "string") {
|
||||||
|
value = fn;
|
||||||
|
} else {
|
||||||
|
value = await fn(metadata, project_settings);
|
||||||
|
}
|
||||||
|
console.log(name, value);
|
||||||
|
if (value === null) {
|
||||||
|
return "";
|
||||||
|
} else if (value && value.$replace) {
|
||||||
|
return value.$replace;
|
||||||
|
} else {
|
||||||
|
return `<property name="${prefix}${name}">${value}</property>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderAllProps(
|
||||||
|
props,
|
||||||
|
metadata,
|
||||||
|
project_settings,
|
||||||
|
prefix = ""
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(props).map(([name, fn]) =>
|
||||||
|
renderProperty(name, fn, metadata, project_settings, prefix)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStream(tracks, type) {
|
||||||
|
return tracks.find((track) => track["@type"] == type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const HOUR = 60 * 60;
|
||||||
|
const MINUTE = 60;
|
||||||
|
|
||||||
|
export function formatDuration(float_s) {
|
||||||
|
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, 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 getStreamIndex(trackarray, type) {
|
||||||
|
const array_index = indexOf(trackarray, (e) => e["@type"] == type);
|
||||||
|
return array_index == -1 ? -1 : trackarray[array_index].StreamOrder || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* makeIDGen(first = 1) {
|
||||||
|
let i = first;
|
||||||
|
while (true) {
|
||||||
|
yield i;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
12
video_props.mjs
Normal file
12
video_props.mjs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
const video_props = {
|
||||||
|
"stream.type": "video",
|
||||||
|
"stream.frame_rate": (metadata) => metadata.data.FrameRate,
|
||||||
|
"stream.sample_aspect_ratio": "1",
|
||||||
|
"codec.width": (metadata) => metadata.data.Width,
|
||||||
|
"codec.height": (metadata) => metadata.data.Height,
|
||||||
|
"codec.rotate": "0",
|
||||||
|
"codec.frame_rate": (metadata) => metadata.data.FrameRate,
|
||||||
|
"stream.handler_name.markup": "VideoHandle",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default video_props
|
Loading…
Reference in New Issue
Block a user