pandora_t_for_time/render_kdenlive.py

658 lines
26 KiB
Python

#!/usr/bin/python3
from collections import defaultdict
import subprocess
import lxml.etree
import uuid
import os
import sys
_CACHE = {}
_IDS = defaultdict(int)
def get_propery(element, name):
return element.xpath('property[@name="%s"]' % name)[0].text
def get_melt():
cmd = ['melt']
if 'XDG_RUNTIME_DIR' not in os.environ:
os.environ['XDG_RUNTIME_DIR'] = '/tmp/runtime-pandora'
if 'DISPLAY' not in os.environ:
cmd = ['xvfb-run', '-a'] + cmd
return cmd
def melt_xml(file):
out = None
real_path = os.path.realpath(file)
if file in _CACHE and isinstance(_CACHE[file], list):
ts, out = _CACHE[file]
if os.stat(real_path).st_mtime != ts:
out = None
if not out:
cmd = get_melt() + [file, '-consumer', 'xml']
out = subprocess.check_output(cmd).decode()
_CACHE[file] = [os.stat(real_path).st_mtime, out]
return out
class KDEnliveProject:
def to_xml(self):
track = self._main_tractor.xpath(".//track")[0]
duration = self.get_duration()
values = {
"in": "0",
"out": str(duration - 1)
}
for key, value in values.items():
track.attrib[key] = value
self._sequence.attrib[key] = value
self._main_tractor.attrib[key] = value
self._audio_tractor.attrib[key] = value
self._tree.remove(self._sequence)
self._tree.append(self._sequence)
self._tree.remove(self._main_bin)
self._tree.append(self._main_bin)
self._tree.remove(self._main_tractor)
self._tree.append(self._main_tractor)
xml = lxml.etree.tostring(self._tree, pretty_print=True).decode()
xml = xml.replace('><', '>\n<')
return "<?xml version='1.0' encoding='utf-8'?>\n" + xml
def __init__(
self, root,
width="1920", height="1080",
display_aspect_num="16", display_aspect_den="9",
frame_rate_num="24", frame_rate_den="1"
):
self._duration = defaultdict(int)
self._counters = defaultdict(int)
self._uuid = '{%s}' % str(uuid.uuid1())
self._width = int(width)
self._height = int(height)
self._fps = int(frame_rate_num) / int(frame_rate_den)
self._tree = self.get_element("mlt", attrib={
"LC_NUMERIC": "C",
"producer": "main_bin",
"version": "7.18.0",
"root": root
}, children=[
self.get_element("profile", attrib={
"frame_rate_num": str(frame_rate_num),
"frame_rate_den": str(frame_rate_den),
"display_aspect_den": str(display_aspect_den),
"display_aspect_num": str(display_aspect_num),
"colorspace": "601",
"progressive": "1",
"description": "%sx%s %0.2ffps" % (self._width, self._height, self._fps),
"width": str(width),
"height": str(height),
"sample_aspect_num": "1",
"sample_aspect_den": "1"
}),
p0 := self.get_element("producer", attrib={
"in": "0",
"out": "2147483647"
}, children=[
["length", "2147483647"],
["eof", "continue"],
["resource", "black"],
["aspect_ratio", "1"],
["mlt_service", "color"],
["kdenlive:playlistid", "black_track"],
["mlt_image_format", "rgba"],
["set.test_audio", "0"],
]),
a4 := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
a4e := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
t_a4 := self.get_element("tractor", children=[
["kdenlive:audio_track", "1"],
["kdenlive:trackheight", "69"],
["kdenlive:timeline_active", "1"],
["kdenlive:collapsed", "0"],
["kdenlive:thumbs_format", None],
["kdenlive:audio_rec", None],
self.get_element("track", attrib={"hide": "video", "producer": a4.attrib["id"]}),
self.get_element("track", attrib={"hide": "video", "producer": a4e.attrib["id"]}),
self.get_element("filter", [
["window", "75"],
["max_gain", "20dB"],
["mlt_service", "volume"],
["internal_added", "237"],
["disable", "1"],
]),
self.get_element("filter", [
["channel", "-1"],
["mlt_service", "panner"],
["internal_added", "237"],
["start", "0.5"],
["disable", "1"],
]),
self.get_element("filter", [
["iec_scale", "0"],
["mlt_service", "audiolevel"],
["dbpeak", "1"],
["disable", "1"],
]),
]),
a3 := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
a3e := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
t_a3 := self.get_element("tractor", children=[
["kdenlive:audio_track", "1"],
["kdenlive:trackheight", "69"],
["kdenlive:timeline_active", "1"],
["kdenlive:collapsed", "0"],
["kdenlive:thumbs_format", None],
["kdenlive:audio_rec", None],
self.get_element("track", attrib={"hide": "video", "producer": a3.attrib["id"]}),
self.get_element("track", attrib={"hide": "video", "producer": a3e.attrib["id"]}),
self.get_element("filter", [
["window", "75"],
["max_gain", "20dB"],
["mlt_service", "volume"],
["internal_added", "237"],
["disable", "1"],
]),
self.get_element("filter", [
["channel", "-1"],
["mlt_service", "panner"],
["internal_added", "237"],
["start", "0.5"],
["disable", "1"],
]),
self.get_element("filter", [
["iec_scale", "0"],
["mlt_service", "audiolevel"],
["dbpeak", "1"],
["disable", "1"],
]),
]),
a2 := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
a2e := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
t_a2 := self.get_element("tractor", children=[
["kdenlive:audio_track", "1"],
["kdenlive:trackheight", "69"],
["kdenlive:timeline_active", "1"],
["kdenlive:collapsed", "0"],
["kdenlive:thumbs_format", None],
["kdenlive:audio_rec", None],
self.get_element("track", attrib={"hide": "video", "producer": a2.attrib["id"]}),
self.get_element("track", attrib={"hide": "video", "producer": a2e.attrib["id"]}),
self.get_element("filter", [
["window", "75"],
["max_gain", "20dB"],
["mlt_service", "volume"],
["internal_added", "237"],
["disable", "1"],
]),
self.get_element("filter", [
["channel", "-1"],
["mlt_service", "panner"],
["internal_added", "237"],
["start", "0.5"],
["disable", "1"],
]),
self.get_element("filter", [
["iec_scale", "0"],
["mlt_service", "audiolevel"],
["dbpeak", "1"],
["disable", "1"],
]),
]),
a1 := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
a1e := self.get_element("playlist", children=[
["kdenlive:audio_track", "1"],
]),
t_a1 := self.get_element("tractor", children=[
["kdenlive:audio_track", "1"],
["kdenlive:trackheight", "69"],
["kdenlive:timeline_active", "1"],
["kdenlive:collapsed", "0"],
["kdenlive:thumbs_format", None],
["kdenlive:audio_rec", None],
self.get_element("track", attrib={"hide": "video", "producer": a1.attrib["id"]}),
self.get_element("track", attrib={"hide": "video", "producer": a1e.attrib["id"]}),
self.get_element("filter", [
["window", "75"],
["max_gain", "20dB"],
["mlt_service", "volume"],
["internal_added", "237"],
["disable", "1"],
]),
self.get_element("filter", [
["channel", "-1"],
["mlt_service", "panner"],
["internal_added", "237"],
["start", "0.5"],
["disable", "1"],
]),
self.get_element("filter", [
["iec_scale", "0"],
["mlt_service", "audiolevel"],
["dbpeak", "1"],
["disable", "1"],
]),
]),
v1 := self.get_element("playlist", children=[
]),
v1e := self.get_element("playlist", children=[
]),
t2 := self.get_element("tractor", attrib={
"in": "00:00:00.000",
"out": "00:00:25.333"
}, children=[
["kdenlive:trackheight", "69"],
["kdenlive:timeline_active", "1"],
["kdenlive:collapsed", "0"],
["kdenlive:thumbs_format", None],
["kdenlive:audio_rec", None],
["kdenlive:locked_track", None],
self.get_element("track", attrib={"hide": "audio", "producer": v1.attrib["id"]}),
self.get_element("track", attrib={"hide": "audio", "producer": v1e.attrib["id"]}),
]),
v2 := self.get_element("playlist", children=[
]),
v2e := self.get_element("playlist", children=[
]),
t3 := self.get_element("tractor", attrib={
"in": "00:00:00.000"
}, children=[
["kdenlive:trackheight", "69"],
["kdenlive:timeline_active", "1"],
["kdenlive:collapsed", "0"],
["kdenlive:thumbs_format", None],
["kdenlive:audio_rec", None],
["kdenlive:locked_track", None],
self.get_element("track", attrib={"hide": "audio", "producer": v2.attrib["id"]}),
self.get_element("track", attrib={"hide": "audio", "producer": v2e.attrib["id"]}),
]),
sequence := self.get_element("tractor", [
["kdenlive:uuid", self._uuid],
["kdenlive:clipname", "Sequence 1"],
["kdenlive:sequenceproperties.hasAudio", "1"],
["kdenlive:sequenceproperties.hasVideo", "1"],
["kdenlive:sequenceproperties.activeTrack", "2"],
["kdenlive:sequenceproperties.tracksCount", "4"],
["kdenlive:sequenceproperties.documentuuid", self._uuid],
["kdenlive:duration", "00:00:25:09"],
["kdenlive:maxduration", "872"],
["kdenlive:producer_type", "17"],
["kdenlive:id", self.get_counter("kdenlive:id")],
["kdenlive:clip_type", "0"],
["kdenlive:folderid", "2"],
["kdenlive:sequenceproperties.audioChannels", "2"],
["kdenlive:sequenceproperties.audioTarget", "1"],
["kdenlive:sequenceproperties.tracks", "4"],
["kdenlive:sequenceproperties.verticalzoom", "1"],
["kdenlive:sequenceproperties.videoTarget", "2"],
["kdenlive:sequenceproperties.zonein", "0"],
["kdenlive:sequenceproperties.zoneout", "75"],
["kdenlive:sequenceproperties.zoom", "8"],
["kdenlive:sequenceproperties.groups", "[]"],
["kdenlive:sequenceproperties.guides", "[]"],
["kdenlive:sequenceproperties.position", "0"],
["kdenlive:sequenceproperties.scrollPos", "0"],
["kdenlive:sequenceproperties.disablepreview", "0"],
self.get_element("track", attrib={"producer": p0.attrib["id"]}),
self.get_element("track", attrib={"producer": t_a4.attrib["id"]}),
self.get_element("track", attrib={"producer": t_a3.attrib["id"]}),
self.get_element("track", attrib={"producer": t_a2.attrib["id"]}),
self.get_element("track", attrib={"producer": t_a1.attrib["id"]}),
self.get_element("track", attrib={"producer": t2.attrib["id"]}),
self.get_element("track", attrib={"producer": t3.attrib["id"]}),
self.get_element("transition", [
["a_track", "0"],
["b_track", "1"],
["mlt_service", "mix"],
["kdenlive_id", "mix"],
["internal_added", "237"],
["always_active", "1"],
["accepts_blanks", "1"],
["sum", "1"],
]),
self.get_element("transition", [
["a_track", "0"],
["b_track", "2"],
["mlt_service", "mix"],
["kdenlive_id", "mix"],
["internal_added", "237"],
["always_active", "1"],
["accepts_blanks", "1"],
["sum", "1"],
]),
self.get_element("transition", [
["a_track", "0"],
["b_track", "3"],
["mlt_service", "mix"],
["kdenlive_id", "mix"],
["internal_added", "237"],
["always_active", "1"],
["accepts_blanks", "1"],
["sum", "1"],
]),
self.get_element("transition", [
["a_track", "0"],
["b_track", "4"],
["mlt_service", "mix"],
["kdenlive_id", "mix"],
["internal_added", "237"],
["always_active", "1"],
["accepts_blanks", "1"],
["sum", "1"],
]),
self.get_element("transition", [
["a_track", "0"],
["b_track", "5"],
["compositing", "0"],
["distort", "0"],
["rotate_center", "0"],
["mlt_service", "qtblend"],
["kdenlive_id", "qtblend"],
["internal_added", "237"],
["always_active", "1"],
["accepts_blanks", "1"],
["sum", "1"],
]),
self.get_element("transition", [
["a_track", "0"],
["b_track", "6"],
["compositing", "0"],
["distort", "0"],
["rotate_center", "0"],
["mlt_service", "qtblend"],
["kdenlive_id", "qtblend"],
["internal_added", "237"],
["always_active", "1"],
["accepts_blanks", "1"],
["sum", "1"],
]),
self.get_element("filter", [
["window", "75"],
["max_gain", "20dB"],
["mlt_service", "volume"],
["internal_added", "237"],
["disable", "1"],
]),
self.get_element("filter", [
["channel", "-1"],
["mlt_service", "panner"],
["internal_added", "237"],
["start", "0.5"],
["disable", "1"],
]),
], {
"id": self._uuid
}),
main_bin := self.get_element("playlist", [
["kdenlive:folder.-1.2", "Sequences"],
["kdenlive:sequenceFolder", "2"],
["kdenlive:docproperties.kdenliveversion", "23.08.0"],
self.get_element("property", attrib={"name": "kdenlive:docproperties.previewextension"}),
self.get_element("property", attrib={"name": "kdenlive:docproperties.previewparameters"}),
["kdenlive:docproperties.seekOffset", "30000"],
["kdenlive:docproperties.uuid", self._uuid],
["kdenlive:docproperties.version", "1.1"],
["kdenlive:expandedFolders", None],
["kdenlive:binZoom", "4"],
self.get_element("property", attrib={"name": "kdenlive:documentnotes"}),
["kdenlive:docproperties.opensequences", self._uuid],
["kdenlive:docproperties.activetimeline", self._uuid],
["xml_retain", "1"],
self.get_element("entry", attrib={"producer": self._uuid, "in": "0", "out": "0"}),
], {
"id": "main_bin"
}),
t4 := self.get_element("tractor", [
["kdenlive:projectTractor", "1"],
self.get_element("track", attrib={"producer": self._uuid}),
])
])
self._sequence = sequence
self._main_bin = main_bin
self._main_tractor = t4
self._audio_tractor = t_a1
self._v1 = v1
self._v2 = v2
self._a1 = a1
self._a2 = a2
self._a3 = a3
self._a4 = a4
def get_counter(self, prefix):
self._counters[prefix] += 1
return str(self._counters[prefix] - 1)
def get_id(self, prefix):
return prefix + self.get_counter(prefix)
def get_chain(self, file, kdenlive_id=None):
out = melt_xml(file)
chain = lxml.etree.fromstring(out).xpath('producer')[0]
chain.tag = 'chain'
chain.attrib['id'] = self.get_id('chain')
# TBD
if kdenlive_id is None:
kdenlive_id = self.get_counter("kdenlive:id")
for name, value in [
("kdenlive:file_size", os.path.getsize(file)),
("kdenlive:clipname", None),
("kdenlive:clip_type", "0"),
("kdenlive:folderid", "-1"),
("kdenlive:id", kdenlive_id),
("set.test_audio", "0"),
("set.test_image", "0"),
("xml", "was here"),
]:
chain.append(
self.get_element(
"property",
attrib={"name": name},
text=str(value) if value is not None else None
)
)
mlt_service = chain.xpath('property[@name="mlt_service"]')[0]
mlt_service.text = "avformat-novalidate"
return chain
def get_duration(self):
if not self._duration:
return 0
return max(self._duration.values())
def get_element(self, tag, children=[], attrib={}, text=None):
element = lxml.etree.Element(tag)
if tag not in (
"blank",
"entry",
"mlt",
"profile",
"property",
"track",
) and "id" not in attrib:
element.attrib['id'] = self.get_id(tag)
if attrib:
for key, value in attrib.items():
element.attrib[key] = value
for child in children:
if isinstance(child, list) and len(child) == 2:
v = child[1]
if v is not None:
v = str(v)
child = self.get_element("property", attrib={"name": child[0]}, text=v)
if isinstance(child, dict):
child = self.get_element(**child)
elif isinstance(child, list):
child = self.get_element(*child)
element.append(child)
if text is not None:
element.text = text
return element
def get_filter(self, name, value):
if name == "transparency":
return [self.get_element("filter", [
["version", "0.9"],
["mlt_service", "frei0r.transparency"],
["kdenlive_id", "frei0r.transparency"],
["0", "00:00:00.000=%s" % value],
["kdenlive:collapsed", "0"],
])]
elif name == "blur":
return [self.get_element("filter", [
["mlt_service", "avfilter.avgblur"],
["kdenlive_id", "avfilter.avgblur"],
["av.sizeX", value],
["av.sizeY", value],
["planes", "7"],
["kdenlive:collapsed", "0"],
])]
elif name == "mask":
mask = [
self.get_element("filter", [
["mlt_service", "frei0r.saturat0r"],
["kdenlive_id", "frei0r.saturat0r"],
["Saturation", "00:00:00.000=0.001"],
]),
self.get_element("filter", [
["mlt_service", "frei0r.select0r"],
["kdenlive_id", "frei0r.select0r"],
["Color to select", "00:00:00.000=0x000000ff"],
["Invert selection", "1"],
["Selection subspace", "0"],
["Subspace shape", "0.5"],
["Edge mode", "0.9"],
["Delta R / A / Hue", "00:00:00.000=0.381"],
["Delta G / B / Chroma", "00:00:00.000=0.772"],
["Delta B / I / I", "00:00:00.000=0.522"],
["Slope", "00:00:00.000=0.515"],
["Operation", "0.5"],
])
]
return mask
elif name == "volume":
return [self.get_element("filter", [
["window", "75"],
["max_gain", "20db"],
["mlt_service", "volume"],
["kdenlive_id", "volume"],
["level", "00:00:00.000=%s" % value],
["kdenlive:collapsed", "0"],
])]
else:
return [
self.get_element("filter", [
["mlt_service", name],
["kdenlive_id", name],
] + value)
]
def properties(self, *props):
return [
self.get_element("property", attrib={"name": name}, text=str(value) if value is not None else value)
for name, value in props
]
def append_clip(self, track_id, clip):
if track_id == "V1":
track = self._v1
elif track_id == "V2":
track = self._v2
elif track_id == "A1":
track = self._a1
elif track_id == "A2":
track = self._a2
elif track_id == "A3":
track = self._a3
elif track_id == "A4":
track = self._a4
else:
print('!!', track_id)
frames = int(self._fps * clip['duration'])
self._duration[track_id] += frames
if clip.get("blank"):
track.append(
self.get_element("blank", attrib={
"length": str(frames),
})
)
return
path = clip['src']
filters = clip.get("filter", {})
#print(path, filters)
chain = self.get_chain(path)
id = get_propery(chain, "kdenlive:id")
if track_id[0] == 'A':
has_audio = False
for prop in chain.xpath('property'):
if prop.attrib['name'].endswith('stream.type') and prop.text == "audio":
has_audio = True
idx = self._tree.index(track) - 1
self._tree.insert(idx, chain)
filters_ = []
if track_id == 'V':
filters_.append({
self.get_element("filter", [
["mlt_service", "qtblend"],
["kdenlive_id", "qtblend"],
["rotate_center", "1"],
["rect", "00:00:00.000=0 0 %s %s 1.000000" % (self._width, self.height)],
["rotation", "00:00:00.000=0"],
["compositing", "0"],
["distort", "0"],
["kdenlive:collapsed", "0"],
["disable", "0"],
])
})
for ft in filters.items():
filters_ += self.get_filter(*ft)
if track_id[0] == 'A' and not has_audio:
track.append(
self.get_element("blank", attrib={
"length": str(frames),
})
)
else:
track.append(
self.get_element("entry", attrib={
"producer": chain.attrib["id"],
"in": chain.attrib["in"],
"out": str(frames - 1)
}, children=[
["kdenlive:id", id],
] + filters_),
)
chain = self.get_chain(path, id)
self._tree.append(chain)
self._main_bin.append(
self.get_element("entry", attrib={
"producer": chain.attrib["id"],
"in": chain.attrib["in"],
"out": chain.attrib["out"],
}),
)