JSPM

@chatoctopus/timeline

0.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 88
  • Score
    100M100P100Q92844F
  • License MIT

Import and export timelines for Final Cut Pro, Adobe Premiere, and DaVinci Resolve

Package Exports

  • @chatoctopus/timeline

Readme

@chatoctopus/timeline

Import and export video editing timelines for Final Cut Pro, Adobe Premiere Pro, DaVinci Resolve, and OpenTimelineIO.

Generates well-formed FCPXML 1.8 (Final Cut Pro), FCP7 XML / xmeml v5 (Premiere, Resolve), and OTIO (OpenTimelineIO) with frame-accurate rational time math -- no floating-point drift.

Installation

npm install @chatoctopus/timeline

Requires Node.js >= 18. For buildTimeline() auto-probing, FFmpeg/FFprobe must be installed and on your PATH. Converting between formats does not require FFmpeg/FFprobe.

Quick Start

Import from an existing project file

Auto-detects FCPXML, xmeml, or OTIO format.

import { importTimeline, exportTimeline } from "@chatoctopus/timeline"
import { readFileSync, writeFileSync } from "fs"

// Read a Final Cut Pro project
const fcpxml = readFileSync("project.fcpxml", "utf-8")
const { timeline, warnings } = importTimeline(fcpxml)

console.log(`Imported "${timeline.name}" with ${timeline.tracks.length} tracks`)
if (warnings.length > 0) console.warn("Warnings:", warnings)

// Convert to Premiere Pro format
writeFileSync("project.xml", exportTimeline(timeline, "premiere"))

Convert between formats

import { importTimeline, exportTimeline } from "@chatoctopus/timeline"
import { readFileSync, writeFileSync } from "fs"

// Premiere XML -> Final Cut Pro
const premiereXml = readFileSync("edit.xml", "utf-8")
const { timeline } = importTimeline(premiereXml)
writeFileSync("edit.fcpxml", exportTimeline(timeline, "fcpx"))

// Final Cut Pro -> DaVinci Resolve
const fcpxml = readFileSync("edit.fcpxml", "utf-8")
const { timeline: tl } = importTimeline(fcpxml)
writeFileSync("edit-resolve.xml", exportTimeline(tl, "resolve"))

// OTIO -> Final Cut Pro
const otio = readFileSync("project.otio", "utf-8")
const { timeline: tl2 } = importTimeline(otio)
writeFileSync("project.fcpxml", exportTimeline(tl2, "fcpx"))

// Any format -> OTIO
const anyFile = readFileSync("timeline.fcpxml", "utf-8")
const { timeline: tl3 } = importTimeline(anyFile)
writeFileSync("timeline.otio", exportTimeline(tl3, "otio"))

Build a timeline from video files

The simplest path: provide file paths and optional trim points. Metadata is extracted automatically via FFprobe.

buildTimeline() validates trim inputs strictly: startAt and duration must be finite, non-negative numbers, 0 is treated as an explicit value, and explicit durations may be rejected for mixed-frame-rate sources when they cannot be represented consistently.

import { buildTimeline, exportTimeline } from "@chatoctopus/timeline"
import { writeFileSync } from "fs"

const timeline = await buildTimeline("Wedding Highlights", [
  { path: "/footage/ceremony.mp4", startAt: 30, duration: 10 },
  { path: "/footage/reception.mp4", duration: 15 },
  { path: "/footage/speeches.mp4", startAt: 120, duration: 20 },
])

// Final Cut Pro
writeFileSync("wedding.fcpxml", exportTimeline(timeline, "fcpx"))

// Adobe Premiere Pro
writeFileSync("wedding.xml", exportTimeline(timeline, "premiere"))

// DaVinci Resolve
writeFileSync("wedding.xml", exportTimeline(timeline, "resolve"))

// OpenTimelineIO (universal interchange)
writeFileSync("wedding.otio", exportTimeline(timeline, "otio"))

Construct a timeline manually

For full control, build the NLETimeline object directly. All timing uses Rational numbers ({ num, den }) to stay frame-aligned.

import {
  exportTimeline,
  rational,
  ZERO,
  FRAME_RATES,
} from "@chatoctopus/timeline"
import type { NLETimeline } from "@chatoctopus/timeline"
import { writeFileSync } from "fs"

const timeline: NLETimeline = {
  name: "My Edit",
  format: {
    width: 1920,
    height: 1080,
    frameRate: FRAME_RATES["29.97"], // { num: 30000, den: 1001 }
    audioRate: 48000,
    colorSpace: "1-1-1 (Rec. 709)",
  },
  assets: [
    {
      id: "r2",
      name: "interview.mp4",
      path: "/footage/interview.mp4",
      duration: rational(9000 * 1001, 30000), // 9000 frames at 29.97fps
      hasVideo: true,
      hasAudio: true,
      audioChannels: 2,
      audioRate: 48000,
      timecodeStart: ZERO,
    },
  ],
  tracks: [
    {
      type: "video",
      clips: [
        {
          assetId: "r2",
          name: "interview",
          offset: ZERO, // starts at timeline 0
          duration: rational(150 * 1001, 30000), // 150 frames = ~5 seconds
          sourceIn: rational(300 * 1001, 30000), // start from frame 300 in source
          sourceDuration: rational(150 * 1001, 30000),
          audioRole: "dialogue",
        },
      ],
    },
  ],
}

writeFileSync("output.fcpxml", exportTimeline(timeline, "fcpx"))

API Reference

High-Level Functions

Function Description
exportTimeline(timeline, editor, options?) Export an NLETimeline. editor is "fcpx", "premiere", "resolve", or "otio".
importTimeline(content) Parse FCPXML, xmeml, or OTIO into an NLETimeline. Auto-detects format.
buildTimeline(name, clips) Build an NLETimeline from ClipInput[] by probing files with FFprobe.

Format-Specific Functions

Function Description
writeFCPXML(timeline, options?) Generate FCPXML 1.8 string
readFCPXML(xmlString) Parse FCPXML into NLETimeline
writeXMEML(timeline, options?) Generate xmeml v5 string
readXMEML(xmlString) Parse xmeml into NLETimeline
writeOTIO(timeline) Generate OTIO JSON string
readOTIO(jsonString) Parse OTIO JSON into NLETimeline

Time Utilities

All timing uses Rational ({ num: number, den: number }) to avoid floating-point drift.

Function Description
rational(num, den) Create a simplified rational number
add(a, b) Add two rationals
subtract(a, b) Subtract (clamps to zero)
toSeconds(r) Convert rational to float seconds
toFCPString(r) Format as FCP time string ("1001/24000s")
parseFCPString(s) Parse FCP time string back to rational
secondsToFrameAligned(secs, frameRate) Convert seconds, snapped to nearest frame boundary
toFrames(duration, frameDuration) Convert rational to frame count
parseTimecode(tc, frameRate) Parse SMPTE timecode ("01:00:00;00") with drop-frame support
FRAME_RATES Common presets: "23.976", "24", "25", "29.97", "30", "59.94", "60"

Validation

Function Description
validateTimeline(timeline) Returns array of ValidationError (checks asset refs, frame alignment, dimensions)
hasErrors(results) true if any hard errors (not just warnings)
computeTimelineDuration(timeline) Compute total duration from all track clips

Probing

Function Description
probeAsset(filePath) Run FFprobe on a file and return a fully populated NLEAsset

Types

interface NLETimeline {
  name: string
  format: NLEFormat
  tracks: NLETrack[]
  assets: NLEAsset[]
}

interface NLEFormat {
  width: number
  height: number
  frameRate: Rational // e.g. { num: 30000, den: 1001 } for 29.97fps
  audioRate: number // e.g. 48000
  colorSpace?: string
}

interface NLETrack {
  type: "video" | "audio"
  clips: NLEClip[]
}

interface NLEClip {
  assetId: string // references NLEAsset.id
  name: string
  offset: Rational // position on timeline
  duration: Rational // clip duration on timeline
  sourceIn: Rational // in-point within source media
  sourceDuration: Rational
  lane?: number
  audioRole?: string
  volume?: number
  enabled?: boolean
}

interface NLEAsset {
  id: string
  name: string
  path: string // absolute file path
  duration: Rational
  hasVideo: boolean
  hasAudio: boolean
  videoFormat?: NLEFormat
  audioChannels?: number
  audioRate?: number
  timecodeStart?: Rational
}

interface ClipInput {
  path: string // absolute file path
  startAt?: number // trim start in seconds
  duration?: number // clip length in seconds
}

type NLEEditor = "fcpx" | "premiere" | "resolve" | "otio"

Supported Formats

Format Extension Editors / Tools Read Write
FCPXML 1.8 .fcpxml Final Cut Pro Yes Yes
xmeml v5 .xml Adobe Premiere Pro, DaVinci Resolve Yes Yes
OpenTimelineIO .otio Resolve 18+, Hiero, rv, and OTIO ecosystem Yes Yes

Verification

Run the test suite:

npm test

Run tests with coverage:

npm run test:coverage

Type-check without emitting:

npm run lint

Build:

npm run build

Quick smoke test

node --input-type=module -e "
import { exportTimeline, rational, ZERO, FRAME_RATES } from './dist/index.js';

const timeline = {
  name: 'Smoke Test',
  format: {
    width: 1920, height: 1080,
    frameRate: FRAME_RATES['29.97'],
    audioRate: 48000,
  },
  assets: [{
    id: 'r2', name: 'clip.mp4', path: '/tmp/clip.mp4',
    duration: rational(300 * 1001, 30000),
    hasVideo: true, hasAudio: true,
    audioChannels: 2, audioRate: 48000, timecodeStart: ZERO,
  }],
  tracks: [{
    type: 'video',
    clips: [{
      assetId: 'r2', name: 'clip', offset: ZERO,
      duration: rational(150 * 1001, 30000),
      sourceIn: ZERO,
      sourceDuration: rational(150 * 1001, 30000),
    }],
  }],
};

const fcpxml = exportTimeline(timeline, 'fcpx');
const xmeml = exportTimeline(timeline, 'premiere');
const otio = exportTimeline(timeline, 'otio');

console.log('FCPXML:', fcpxml.includes('<fcpxml') ? 'OK' : 'FAIL');
console.log('xmeml:', xmeml.includes('<xmeml') ? 'OK' : 'FAIL');
console.log('OTIO:', otio.includes('Timeline.1') ? 'OK' : 'FAIL');
console.log('Done.');
"

Architecture

src/
├── index.ts           Public API: exportTimeline, importTimeline, buildTimeline
├── types.ts           NLETimeline, NLEClip, NLEAsset, NLETrack, NLEFormat, Rational
├── time.ts            Rational arithmetic, frame alignment, SMPTE timecode parsing
├── probe.ts           FFprobe metadata extraction
├── validate.ts        Pre-export validation (reference integrity, frame alignment)
├── fcpxml/
│   ├── writer.ts      FCPXML 1.8 generation
│   └── reader.ts      FCPXML parsing
├── xmeml/
│   ├── writer.ts      xmeml v5 generation (Premiere / Resolve)
│   └── reader.ts      xmeml parsing
└── otio/
    ├── writer.ts      OpenTimelineIO JSON generation
    └── reader.ts      OpenTimelineIO JSON parsing

How It Works

Rational time math is the core of the library. All NLE software uses frame-aligned timing internally -- expressing durations as rational fractions like 1001/30000s (one frame at 29.97fps). Using floating-point seconds causes frame drift and "not on edit frame boundary" errors in Final Cut Pro.

Every clip duration and offset goes through secondsToFrameAligned() which snaps to the nearest frame boundary, matching the behavior of both buttercut (Ruby) and cutlass (Go) which this library draws from.

Three interchange formats cover all major editors and tools:

  • FCPXML 1.8 for Final Cut Pro -- trackless magnetic timeline with <asset-clip> elements inside a <spine>
  • xmeml v5 for Premiere and Resolve -- track-based with linked <clipitem> elements for video and audio
  • OpenTimelineIO (.otio) -- the industry-standard JSON interchange format backed by the Academy Software Foundation. OTIO acts as a universal hub: any tool that speaks OTIO gets instant access to timelines from any other format. Transitions and effects in OTIO files are noted as warnings during import (the core clip/track/timing data is fully preserved).

Acknowledgments

This project draws on ideas and timing behavior from buttercut and cutlass, and we gratefully acknowledge those projects as upstream inspiration.

Trademarks

Final Cut Pro is a trademark of Apple Inc. Adobe Premiere Pro is a trademark of Adobe. DaVinci Resolve is a trademark of Blackmagic Design Pty Ltd. All other product names, logos, and brands are the property of their respective owners.

License

MIT