Files
gart/code/docs/specs/02_FCPXML_STRUCTURE.md

9.7 KiB
Executable File

FCPXML Structure Reference

How FCPXML represents different elements and what nodes need modification for each editing operation.


Document Structure

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fcpxml>
<fcpxml version="1.11">
    <resources>
        <!-- Media assets, formats, effects -->
    </resources>
    <library>
        <event name="My Event">
            <project name="My Project">
                <sequence>
                    <spine>
                        <!-- Primary storyline clips -->
                    </spine>
                </sequence>
            </project>
        </event>
    </library>
</fcpxml>

Key Elements

<asset> - Media Reference

<asset id="r1" name="Interview_01" src="file:///path/to/Interview_01.mov" 
       start="0s" duration="300s" hasVideo="1" hasAudio="1">
    <format id="r2" name="FFVideoFormat1080p30"/>
</asset>

<clip> - Timeline Clip

<clip name="Interview_01" offset="0s" duration="120s" start="30s" 
      tcFormat="NDF" ref="r1">
    <!-- offset: position in timeline -->
    <!-- duration: length shown in timeline -->
    <!-- start: in-point in source media -->
    <!-- ref: links to asset id -->
</clip>

<video> / <audio> - A/V Components

<video name="B-Roll" offset="120s" duration="60s" start="0s" ref="r3">
    <audio lane="-1" offset="0s" duration="60s" start="0s" ref="r3"/>
</video>

<gap> - Empty Space

<gap name="Gap" offset="180s" duration="30s"/>

<marker> - Marker

<marker start="45s" duration="1/30s" value="Chapter 1"/>

<!-- Chapter marker -->
<chapter-marker start="90s" duration="1/30s" value="Intro" 
                posterOffset="0s"/>

<!-- To-do marker -->
<marker start="120s" duration="1/30s" value="Fix audio">
    <note>Audio levels too low</note>
</marker>

<keyword> - Keyword Range

<clip ref="r1" ...>
    <keyword start="0s" duration="30s" value="Interview"/>
    <keyword start="30s" duration="15s" value="B-Roll"/>
</clip>

<transition> - Transition Effect

<transition name="Cross Dissolve" offset="119/2s" duration="1s">
    <filter-video ref="r10" name="Cross Dissolve"/>
</transition>

Edit Operations → XML Changes

ADD_MARKER

Target: Inside <clip>, <video>, <audio>, or <spine>

<!-- Before -->
<clip name="Interview" offset="0s" duration="120s" start="0s" ref="r1">
</clip>

<!-- After: Standard marker at 45s -->
<clip name="Interview" offset="0s" duration="120s" start="0s" ref="r1">
    <marker start="45s" duration="1/30s" value="Review this"/>
</clip>

<!-- After: Chapter marker -->
<clip name="Interview" offset="0s" duration="120s" start="0s" ref="r1">
    <chapter-marker start="45s" duration="1/30s" value="Introduction" posterOffset="0s"/>
</clip>

<!-- After: Colored marker -->
<clip name="Interview" offset="0s" duration="120s" start="0s" ref="r1">
    <marker start="45s" duration="1/30s" value="Needs work">
        <marker-color color="3"/>  <!-- 0=blue, 1=cyan, 2=green, 3=yellow... -->
    </marker>
</clip>

TRIM_CLIP

Target: start and duration attributes of <clip>

<!-- Before: 2 minute clip starting at source timecode 30s -->
<clip name="Interview" offset="0s" duration="120s" start="30s" ref="r1"/>

<!-- After: Trim 10s from start (new in-point) -->
<clip name="Interview" offset="0s" duration="110s" start="40s" ref="r1"/>

<!-- After: Trim 10s from end (shorter duration) -->
<clip name="Interview" offset="0s" duration="110s" start="30s" ref="r1"/>

Ripple trim also requires updating offset of all subsequent clips:

<!-- Before -->
<spine>
    <clip name="A" offset="0s" duration="60s" .../>
    <clip name="B" offset="60s" duration="60s" .../>
    <clip name="C" offset="120s" duration="60s" .../>
</spine>

<!-- After: Trim 10s from end of clip A with ripple -->
<spine>
    <clip name="A" offset="0s" duration="50s" .../>
    <clip name="B" offset="50s" duration="60s" .../>   <!-- offset shifted -->
    <clip name="C" offset="110s" duration="60s" .../>  <!-- offset shifted -->
</spine>

REORDER_CLIPS

Target: offset attributes of all affected clips

<!-- Before: A, B, C order -->
<spine>
    <clip name="A" offset="0s" duration="30s" .../>
    <clip name="B" offset="30s" duration="30s" .../>
    <clip name="C" offset="60s" duration="30s" .../>
</spine>

<!-- After: Move C to start → C, A, B -->
<spine>
    <clip name="C" offset="0s" duration="30s" .../>
    <clip name="A" offset="30s" duration="30s" .../>
    <clip name="B" offset="60s" duration="30s" .../>
</spine>

Important: Element order in XML doesn't matter, only offset values determine timeline position.


ADD_TRANSITION

Target: Insert <transition> element between clips

<!-- Before -->
<spine>
    <clip name="A" offset="0s" duration="60s" .../>
    <clip name="B" offset="60s" duration="60s" .../>
</spine>

<!-- After: Add 1s cross-dissolve between A and B -->
<spine>
    <clip name="A" offset="0s" duration="60s" .../>
    <transition name="Cross Dissolve" offset="59s" duration="1s">
        <filter-video ref="r_dissolve" name="Cross Dissolve"/>
    </transition>
    <clip name="B" offset="60s" duration="60s" .../>
</spine>

Note: Transition offset is typically clip_end - (transition_duration / 2)


CHANGE_SPEED

Target: Add <timeMap> or <conform-rate> inside clip

<!-- Constant speed change (50% slow-mo) -->
<clip name="Action" offset="0s" duration="120s" start="0s" ref="r1">
    <conform-rate scaleEnabled="1" srcFrameRate="30"/>
    <timeMap>
        <timept time="0s" value="0s" interp="linear"/>
        <timept time="120s" value="60s" interp="linear"/>
    </timeMap>
</clip>

<!-- Speed ramp (100% → 50% → 100%) -->
<clip name="Action" offset="0s" duration="90s" start="0s" ref="r1">
    <timeMap>
        <timept time="0s" value="0s" interp="linear"/>
        <timept time="30s" value="30s" interp="smooth2"/>
        <timept time="60s" value="45s" interp="smooth2"/>
        <timept time="90s" value="75s" interp="linear"/>
    </timeMap>
</clip>

Interpolation values:

  • linear - constant speed
  • smooth2 - ease in/out
  • smooth - smoother ease

SPLIT_CLIP

Target: Create two clips from one, adjusting start, duration, offset

<!-- Before: Single 60s clip -->
<spine>
    <clip name="Interview" offset="0s" duration="60s" start="0s" ref="r1"/>
</spine>

<!-- After: Split at 30s mark -->
<spine>
    <clip name="Interview" offset="0s" duration="30s" start="0s" ref="r1"/>
    <clip name="Interview" offset="30s" duration="30s" start="30s" ref="r1"/>
</spine>

DELETE_CLIP

Ripple delete: Remove clip, shift subsequent clips

<!-- Before -->
<spine>
    <clip name="A" offset="0s" duration="30s" .../>
    <clip name="B" offset="30s" duration="30s" .../>
    <clip name="C" offset="60s" duration="30s" .../>
</spine>

<!-- After: Delete B with ripple -->
<spine>
    <clip name="A" offset="0s" duration="30s" .../>
    <clip name="C" offset="30s" duration="30s" .../>  <!-- offset updated -->
</spine>

Non-ripple delete: Replace with gap

<!-- After: Delete B without ripple -->
<spine>
    <clip name="A" offset="0s" duration="30s" .../>
    <gap name="Gap" offset="30s" duration="30s"/>
    <clip name="C" offset="60s" duration="30s" .../>
</spine>

Time Format Conversions

FCPXML uses rational time (fractions of seconds):

Timecode FCPXML Time
00:00:01:00 @ 30fps 1s or 30/30s
00:00:00:15 @ 30fps 15/30s or 1/2s
00:01:00:00 60s
00:00:02:10 @ 24fps 58/24s

Conversion formula:

fcpxml_time = (hours * 3600) + (minutes * 60) + seconds + (frames / fps)

Frame-accurate time:

frames_total = (hours * 3600 * fps) + (minutes * 60 * fps) + (seconds * fps) + frames
fcpxml_time = f"{frames_total}/{fps}s"

Resource References

Every clip references resources by id:

<resources>
    <format id="r1" name="FFVideoFormat1080p30" width="1920" height="1080" 
            frameDuration="1/30s"/>
    <asset id="r2" name="Interview" src="file:///path/to/file.mov" 
           start="0s" duration="3600s" format="r1"/>
    <effect id="r3" name="Cross Dissolve" uid=".../Cross Dissolve"/>
</resources>

<!-- In timeline -->
<clip ref="r2" .../>
<transition>
    <filter-video ref="r3"/>
</transition>

Compound Clips & Nested Timelines

<ref-clip name="Nested Sequence" ref="r5" offset="0s" duration="120s">
    <!-- r5 points to another sequence -->
</ref-clip>

<!-- Inline compound (multicam, synchronized) -->
<mc-clip name="Multicam" offset="0s" duration="60s">
    <mc-source angleID="angle1">
        <clip ref="r10" .../>
    </mc-source>
    <mc-source angleID="angle2">
        <clip ref="r11" .../>
    </mc-source>
</mc-clip>

Audio Specifics

<!-- Detached audio -->
<clip ref="r1" ...>
    <audio lane="-1" ref="r1" offset="0s" duration="60s"/>
</clip>

<!-- Audio only clip -->
<audio-clip name="Music" lane="-2" offset="0s" duration="180s" ref="r20"/>

<!-- Audio adjustments -->
<clip ref="r1" ...>
    <adjust-volume amount="-6dB"/>
    <audio lane="-1" ref="r1">
        <adjust-volume amount="3dB"/>
    </audio>
</clip>

Critical Implementation Notes

  1. Always validate XML after modification - FCP will reject malformed FCPXML
  2. Maintain resource integrity - never orphan asset references
  3. Recalculate all offsets after any operation that changes clip positions
  4. Preserve existing attributes - don't strip attributes you don't understand
  5. Handle connected clips - clips on other lanes may connect to spine clips
  6. Time precision matters - use rational fractions, not floats