@chatoctopus/timeline

中文文档
0.3.0发布于 2026/06/24MITTypeScriptES Module近 30 天下载 1723

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

原始使用文档
文档目录24 节
  1. Installation
  2. CLI
  3. Commands
  4. Quick Start
  5. Import from an existing project file
  6. Convert between formats
  7. Build a timeline from media files
  8. Construct a timeline manually
  9. API Reference
  10. High-Level Functions
  11. Format-Specific Functions
  12. Time Utilities
  13. Validation
  14. Probing
  15. Types
  16. Supported Formats
  17. Verification
  18. Quick smoke test
  19. Architecture
  20. How It Works
  21. Contributing
  22. Acknowledgments
  23. Trademarks
  24. License

@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. The package is ESM-only. For buildTimelineFromFiles() or probeMediaReference(), FFmpeg/FFprobe must be installed and on your PATH. Converting between formats does not require FFmpeg/FFprobe.

CLI

The package ships with a timeline CLI focused on format conversion and validation:

npx @chatoctopus/timeline convert ./edit.fcpxml --to otio --out ./edit.otio
npx @chatoctopus/timeline validate ./edit.xml
npx @chatoctopus/timeline validate ./edit.otio --json

Commands

CommandDescription
convert <input> --to <fcpx|premiere|resolve|otio> [--out <path>]Auto-detect input format, convert to target editor format, and write to file (--out) or stdout. Import and lossy-export warnings are written to stderr.
validate <input> [--json]Validate timeline integrity and frame alignment; exits with non-zero on hard errors
sanitize <input> [--out <path>]Rewrite absolute media paths to neutral placeholders so a project file is safe to share as a test fixture. Review project/clip names and marker text by hand.

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 media files

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

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

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

const timeline = await buildTimelineFromFiles("Wedding Highlights", [
  { path: "/footage/ceremony.mp4", startAt: 30, duration: 10 },
  { path: "/footage/reception.mp4", duration: 15 },
  { path: "/footage/speeches.mp4", startAt: 120, duration: 20 },
  { path: "/slides/title-card.png", duration: 3 },
])

// 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 OTIO-first Timeline model directly. All timing uses Rational numbers ({ num, den }) to stay frame-aligned.

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

const timeline: Timeline = {
  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)",
  },
  tracks: [
    {
      kind: "video",
      name: "V1",
      items: [
        {
          kind: "clip",
          name: "interview",
          mediaReference: {
            type: "external",
            name: "interview.mp4",
            targetUrl: "file:///footage/interview.mp4",
            mediaKind: "video",
            availableRange: {
              startTime: ZERO,
              duration: rational(9000 * 1001, 30000), // 9000 frames at 29.97fps
            },
          },
          sourceRange: {
            startTime: rational(300 * 1001, 30000), // start from frame 300 in source
            duration: rational(150 * 1001, 30000), // 150 frames = ~5 seconds
          },
          metadata: {
            role: "dialogue",
          },
        },
      ],
    },
  ],
}

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

Timeline is the API for OTIO, FCPXML, and xmeml workflows.

API Reference

High-Level Functions

FunctionDescription
exportTimeline(timeline, editor, options?)Export a Timeline. editor is "fcpx", "premiere", "resolve", or "otio".
importTimeline(content)Parse FCPXML, xmeml, or OTIO into Timeline.
buildTimelineFromFiles(name, files)Probe files with FFprobe and build a linear Timeline with inline media references.
createTimeline(options)Create a Timeline with default format values for synthetic or programmatic edits.

Format-Specific Functions

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

Lossy export note:

exportTimeline(timeline, "fcpx", {
  onWarning(message) {
    console.warn(message)
  },
})

Use onWarning when exporting to FCPXML or xmeml if you want to be notified when transitions, markers, metadata, missing references, or other core-only fields are dropped.

Transitions are overlap items in the core model. computeTimelineDuration() accounts for that overlap, while current FCPXML and xmeml exports flatten transitions into butt cuts and warn because those adapters do not yet emit native transition elements.

Time Utilities

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

FunctionDescription
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_RATESCommon presets: "23.976", "24", "25", "29.97", "30", "50", "59.94", "60"

Validation

FunctionDescription
validateTimeline(timeline)Returns array of ValidationError (checks media refs, source ranges, frame alignment, dimensions)
hasErrors(results)true if any hard errors (not just warnings)
computeTimelineDuration(timeline)Compute total duration from all track items

Probing

FunctionDescription
probeMediaReference(filePath)Run FFprobe on a file and return a populated ExternalReference for video, audio, or image media

Types

interface Timeline {
  name: string
  format: NLEFormat
  tracks: Track[]
  metadata?: Record<string, unknown>
  markers?: Marker[]
  globalStartTime?: Rational
}

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

type TrackItem = Clip | Gap | Transition

interface Track {
  kind: "video" | "audio"
  name?: string
  items: TrackItem[]
  metadata?: Record<string, unknown>
  markers?: Marker[]
  enabled?: boolean
}

interface Clip {
  kind: "clip"
  name: string
  mediaReference: MediaReference
  sourceRange?: TimeRange
  metadata?: Record<string, unknown>
  markers?: Marker[]
  enabled?: boolean
}

interface Gap {
  kind: "gap"
  sourceRange: TimeRange
  metadata?: Record<string, unknown>
  enabled?: boolean
}

interface Transition {
  kind: "transition"
  name?: string
  transitionType?: string
  inOffset: Rational
  outOffset: Rational
  metadata?: Record<string, unknown>
}

interface TimeRange {
  startTime: Rational
  duration: Rational
}

type MediaReference = ExternalReference | MissingReference

interface ExternalReference {
  type: "external"
  targetUrl: string
  name?: string
  mediaKind?: "video" | "audio" | "image" | "unknown"
  availableRange?: TimeRange
  metadata?: Record<string, unknown>
  streamInfo?: StreamInfo
}

interface MissingReference {
  type: "missing"
  name?: string
  metadata?: Record<string, unknown>
}

interface StreamInfo {
  hasVideo?: boolean
  hasAudio?: boolean
  width?: number
  height?: number
  frameRate?: Rational
  audioRate?: number
  audioChannels?: number
  colorSpace?: string
}

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

Still images are modeled as ExternalReference objects with mediaKind: "image" on normal video tracks. The clip carries the display duration via sourceRange, and buildTimelineFromFiles() will populate an availableRange for stills when you provide an explicit duration.

Builder inputs:

interface TimelineFileInput {
  path: string
  startAt?: number
  duration?: number
  track?: number
  kind?: "video" | "audio"
}

interface CreateTimelineOptions {
  name: string
  format?: Partial<NLEFormat>
  tracks?: Track[]
  metadata?: Record<string, unknown>
  markers?: Marker[]
  globalStartTime?: Rational
}

Supported Formats

FormatExtensionEditors / ToolsReadWrite
FCPXML 1.8.fcpxmlFinal Cut ProYesYes
xmeml v5.xmlAdobe Premiere Pro, DaVinci ResolveYesYes
OpenTimelineIO.otioResolve 18+, Hiero, rv, and OTIO ecosystemYesYes

FCPXML import covers asset-clip-style spines -- what DaVinci Resolve exports and this package generates. Final Cut's container elements (<clip>, <sync-clip>, <mc-clip>, <ref-clip>, <audition>, <title>) are not parsed yet; they are skipped with a warning.

Verification

Run the test suite:

npm test

Real project files exported from NLEs live in tests/fixtures/. Every file there is swept automatically by tests/fixtures.test.ts: import, validate, export to all formats, and round-trip. To extend coverage, export a timeline from your editor, sanitize the media paths, and drop the file in -- no test code needed.

Fixture tests prove the package reads what editors write. The reverse -- that editors accept generated files -- still needs a manual import check in the target app before a release.

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,
  },
  tracks: [{
    kind: 'video',
    items: [{
      kind: 'clip',
      name: 'clip',
      mediaReference: {
        type: 'external',
        name: 'clip.mp4',
        targetUrl: 'file:///tmp/clip.mp4',
        mediaKind: 'video',
        availableRange: {
          startTime: ZERO,
          duration: rational(300 * 1001, 30000),
        },
      },
      sourceRange: {
        startTime: ZERO,
        duration: 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, createTimeline, buildTimelineFromFiles
├── types.ts           OTIO-first core types
├── time.ts            Rational arithmetic, frame alignment, SMPTE timecode parsing
├── probe.ts           FFprobe -> ExternalReference probing
├── media-kind.ts      Shared file-extension media kind inference
├── builders.ts        Core-native timeline construction helpers
├── validate.ts        Core validation and duration computation
├── 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. In this package, OTIO now maps directly to the core model, including explicit gaps, transitions, markers, metadata, and inline media references.

Contributing

The most useful contribution is a real timeline exported from your editor -- especially one that exercises features the adapters don't fully cover yet (transitions, markers, multicam, drop-frame timecode, mixed frame rates, nested/compound clips, titles). Every file in tests/fixtures/ is automatically imported, validated, exported to all formats, and round-tripped, so a fixture needs no test code.

Sanitize before sharing. Real exports embed absolute media paths (your username, drives, client/project names). Scrub them first:

npx @chatoctopus/timeline sanitize ./your-export.fcpxml --out ./your-export.sanitized.fcpxml

This rewrites every media path to a neutral placeholder. Project names, clip names, and marker text are not scrubbed -- review those by hand. Then open a pull request adding the file to tests/fixtures/, or open a Share a timeline file issue and attach it (zipped). See CONTRIBUTING.md for the full guide.

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

版本与发布记录

@chatoctopus/timeline 共发布 4 个版本,首次发布于 2026/03/07;过去 12 个月发布了 4 个版本。

版本发布日期距今
0.3.0latest2026/06/244 个月前
0.2.02026/03/197 个月前
0.1.12026/03/078 个月前
0.1.02026/03/078 个月前

关于 @chatoctopus/timeline 的常见问题

@chatoctopus/timeline 是什么?

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

如何安装 @chatoctopus/timeline?

在项目目录运行 npm install @chatoctopus/timeline,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 timeline,可以用 npx timeline 直接运行。

@chatoctopus/timeline 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@chatoctopus/timeline 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@chatoctopus/timeline 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@chatoctopus/timeline 可以商用吗?

@chatoctopus/timeline 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@chatoctopus/timeline 的下载量有多少?

根据 npm 官方下载统计,@chatoctopus/timeline 在 2026/10/10 – 2026/11/08 这 30 天内被下载 1,723 次,日均约 57 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@chatoctopus/timeline 还在维护吗?

最新版本 0.3.0 发布于 2026/06/24(4 个月前);至今共发布 4 个版本,其中过去 12 个月发布了 4 个;首次发布于 2026/03/07。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。