MLT document model: pure builders that produce xml.cljc node trees.
MLT's XML vocabulary, smallest useful subset: mlt root; LC_NUMERIC="C" so float serialization is locale-invariant profile frame geometry; the document carries one producer a clip source; resource lives in a property child playlist a track: <entry producer=.../> and <blank length=.../> refs tractor the timeline: <multitrack> of <track producer=.../> refs filter an effect on a producer/playlist/tractor transition a compositor between two tracks (a_track/b_track)
Builders return xml.cljc nodes; document assembles and xml/emit
serializes. No IO, no host conditionals — core + string only.
MLT document model: pure builders that produce xml.cljc node trees. MLT's XML vocabulary, smallest useful subset: mlt root; LC_NUMERIC="C" so float serialization is locale-invariant profile frame geometry; the document carries one producer a clip source; resource lives in a property child playlist a track: <entry producer=.../> and <blank length=.../> refs tractor the timeline: <multitrack> of <track producer=.../> refs filter an effect on a producer/playlist/tractor transition a compositor between two tracks (a_track/b_track) Builders return xml.cljc nodes; `document` assembles and `xml/emit` serializes. No IO, no host conditionals — core + string only.
Read an MLT / .kdenlive document back into a summary — the inverse leg of mlt.model. Pure and portable: nodes in, data out, no IO.
A .kdenlive file is an MLT document with Kdenlive conventions: the bin lives in a playlist id="main_bin", the timeline is the tractor whose multitrack references the track playlists, and cuts are in/out attributes (clock strings or frame counts).
Read an MLT / .kdenlive document back into a summary — the inverse leg of mlt.model. Pure and portable: nodes in, data out, no IO. A .kdenlive file is an MLT document with Kdenlive conventions: the bin lives in a playlist id="main_bin", the timeline is the tractor whose multitrack references the track playlists, and cuts are in/out attributes (clock strings or frame counts).
Time arithmetic for MLT in/out/length attributes.
MLT accepts two time vocabularies: frame counts ("150") and clock strings ("HH:MM:SS.mmm"). Conversion between them needs the profile's fps, carried here as a [num den] fraction (a bare number means [n 1]).
Portable: core + string only, integer math where hosts disagree on rounding.
Time arithmetic for MLT in/out/length attributes.
MLT accepts two time vocabularies: frame counts ("150") and clock strings
("HH:MM:SS.mmm"). Conversion between them needs the profile's fps, carried
here as a [num den] fraction (a bare number means [n 1]).
Portable: core + string only, integer math where hosts disagree on rounding.A Kdenlive timeline as data, edited by the SAME verbs the scripting fork answers over HTTP, with no Kdenlive running.
The fork's route catalog (hive-kdenlive.kdenlive.routes) names what an agent
can ask of a timeline: import media, add a track, insert a clip at a frame,
open space, extract a zone. apply-verb answers those route ids against a
value instead of a live application, and ->document turns the value into
an MLT document melt renders. That is the :document transport: the same
vocabulary, a different place for the timeline to live.
State {:name "edit" :profile {:width 1920 :height 1080 :fps 25} :next-id 1 :bin {"1" {:id "1" :resource "/clips/a.mkv" :name "a.mkv" :length 250}} :tracks [{:id "2" :name "V1" :audio? false :clips [{:id "3" :bin "1" :position 0 :in 0 :out 249}]}]}
Frames are integers throughout; :out is inclusive, as in MLT. Clips on a track never overlap: a verb that would make them overlap, or that would have to cut a clip in two, is REFUSED with a named error rather than approximated. Ids are strings, as the fork returns them.
Every verb is (fn [state params ctx] -> {:ok {:state s :result r}} | {:error ...}).
ctx carries effects the pure core cannot perform: :probe, a function from
a media path to {:length frames} or {:error ...}.
Portable: clojure.core and clojure.string only. No for (clojurust
mishandles :when/:let in it), booleans compared with =.
A Kdenlive timeline as data, edited by the SAME verbs the scripting fork
answers over HTTP, with no Kdenlive running.
The fork's route catalog (hive-kdenlive.kdenlive.routes) names what an agent
can ask of a timeline: import media, add a track, insert a clip at a frame,
open space, extract a zone. `apply-verb` answers those route ids against a
value instead of a live application, and `->document` turns the value into
an MLT document melt renders. That is the :document transport: the same
vocabulary, a different place for the timeline to live.
State
{:name "edit"
:profile {:width 1920 :height 1080 :fps 25}
:next-id 1
:bin {"1" {:id "1" :resource "/clips/a.mkv" :name "a.mkv" :length 250}}
:tracks [{:id "2" :name "V1" :audio? false
:clips [{:id "3" :bin "1" :position 0 :in 0 :out 249}]}]}
Frames are integers throughout; :out is inclusive, as in MLT. Clips on a
track never overlap: a verb that would make them overlap, or that would have
to cut a clip in two, is REFUSED with a named error rather than approximated.
Ids are strings, as the fork returns them.
Every verb is (fn [state params ctx] -> {:ok {:state s :result r}} | {:error ...}).
`ctx` carries effects the pure core cannot perform: :probe, a function from
a media path to {:length frames} or {:error ...}.
Portable: clojure.core and clojure.string only. No `for` (clojurust
mishandles :when/:let in it), booleans compared with `=`.Transitions between two clips, as a PLAN of verbs the route catalog already names.
Two clips on different video tracks overlap in time; the UPPER one is
animated over the overlap and the audio is crossed. plan answers the
calls that do it:
{:ok {:overlap [start end) :upper id :calls [[route-id params] ...]}}
Feed each call to timeline/apply-verb (the :document transport) or to the
HTTP client: the same calls mean the same thing in both, and the result
stays editable in the .kdenlive project as ordinary keyframes and fades.
Kinds :dissolve the upper clip's opacity crosses 0..1 :slide-left :slide-right the incoming picture pushes in from a side :slide-up :slide-down
Which clip is animated follows from the tracks, never from a flag: a later track renders on top. When the INCOMING clip is on top it fades or slides in; when the OUTGOING clip is on top it fades or slides out, uncovering the incoming one under it.
Refused by name rather than approximated: the clips do not overlap, they share a track, the incoming clip does not start inside the outgoing one, either is on an audio track, or the upper clip already carries transform keyframes a transition would overwrite.
Portable: clojure.core only. No for.
Transitions between two clips, as a PLAN of verbs the route catalog already
names.
Two clips on different video tracks overlap in time; the UPPER one is
animated over the overlap and the audio is crossed. `plan` answers the
calls that do it:
{:ok {:overlap [start end) :upper id :calls [[route-id params] ...]}}
Feed each call to `timeline/apply-verb` (the :document transport) or to the
HTTP client: the same calls mean the same thing in both, and the result
stays editable in the .kdenlive project as ordinary keyframes and fades.
Kinds
:dissolve the upper clip's opacity crosses 0..1
:slide-left :slide-right the incoming picture pushes in from a side
:slide-up :slide-down
Which clip is animated follows from the tracks, never from a flag: a later
track renders on top. When the INCOMING clip is on top it fades or slides
in; when the OUTGOING clip is on top it fades or slides out, uncovering the
incoming one under it.
Refused by name rather than approximated: the clips do not overlap, they
share a track, the incoming clip does not start inside the outgoing one,
either is on an audio track, or the upper clip already carries transform
keyframes a transition would overwrite.
Portable: clojure.core only. No `for`.XML text <-> element tree, written by hand over strings.
Node shape: {:tag string :attrs [[name value] ...] :content [node-or-string ...]}. Attribute order is a vector so emission is byte-deterministic on every host.
emit : node -> string (declaration, one-space indent, trailing newline).
parse: string -> {:ok node} | {:error :mlt/xml-malformed :at int :message string}.
Portable: clojure.core and clojure.string only, no reader conditionals.
XML text <-> element tree, written by hand over strings.
Node shape: {:tag string :attrs [[name value] ...] :content [node-or-string ...]}.
Attribute order is a vector so emission is byte-deterministic on every host.
`emit` : node -> string (declaration, one-space indent, trailing newline).
`parse`: string -> {:ok node} | {:error :mlt/xml-malformed :at int :message string}.
Portable: clojure.core and clojure.string only, no reader conditionals.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |