Skip to content

Add import-media: put an external file back into the project as a bin source #11

Description

@RichardBray

Summary

Add the ability to put an external file (e.g. a cleaned .m4a from Auphonic, or
a graded .mov) back into a project as a media-bin source — imported but
not placed on the timeline. Scope for v1 is strictly "add to the bin"; no
timeline placement, no clip replacement. That keeps it low-risk and additive.

Proposed command:

camkit import-media <file> [--project PATH] [--dry-run] [--force]

Why this is separate from export-video

Unlike rendering (which must go through Camtasia — see the export-video issue),
adding a bin source is pure project.tscproj JSON mutation plus a metadata
probe
. No Camtasia render, no AppleScript. So it belongs in @camkit/core
behind unit tests, mirroring how rebuild and captions mutate the doc. It was
floated as "part of export-video if easy" — it isn't the same machinery, so it's
its own command. Doing this also makes the round-trip useful: cleaned audio /
graded video currently just sit in ~/Downloads; their value is back in the
project.

How the source bin works (already mapped in the codebase)

packages/core/src/project.ts documents the shape. Each entry in
doc.sourceBin looks like:

{
  "id": <number>,            // unique source id
  "src": "<relative path>",  // resolved against project dir, then media/
  "sourceTracks": [{
    "range": [start, end],   // in that track's editRate units
    "editRate": <number>,
    // ...type, dimensions, channels, etc.
  }]
}

listSources() derives duration as (range[1] - range[0]) / editRate, and
planAudioExport/exportAudio resolve src against the project dir falling
back to media/ (resolveSource in media.ts). A new source must follow the
same conventions.

Implementation notes

  • Reverse-engineer the full entry shape from a real project. project.ts only
    models the fields camkit reads. Open an existing .cmproj, copy the shape of a
    comparable already-imported source (an audio source for .m4a, a video source
    for .mov), and fill in metadata rather than inventing fields. Add a fixture
    for tests.
  • Probe metadata with ffprobe (ffmpeg is already a documented prerequisite).
    Need duration → range/editRate, plus dimensions/channels/framerate for
    video vs audio sources. Put the ffprobe call in the CLI/media layer
    (side-effecting); keep the pure "build a sourceBin entry from probed metadata"
    function in @camkit/core and unit-test it.
  • New source id: Math.max(...doc.sourceBin.map(s => s.id)) + 1 (handle the
    empty bin). Confirm ids are also referenced nowhere else that needs updating
    for a bin-only add (timeline medias reference ids, but we're not touching the
    timeline).
  • File placement: decide and document whether the command copies the file
    into the project's media/ dir (so src stays a stable relative path, matching
    how Camtasia stores imports) or stores the given path. Copying into media/ is
    recommended — it matches resolveSource's lookup and survives the project
    being moved. State the choice in --help.
  • Safety — reuse the rebuild/captions pattern exactly (cmdRebuild /
    cmdCaptions in camkit.ts): back up to project.tscproj.bak, refuse to run
    with a ~project.tscproj lock present or an existing .bak unless --force,
    and support --dry-run to print the planned source entry and write nothing.
  • macOS-independent: this is cross-platform (no AppleScript). Don't gate it
    behind assertDarwin().
  • Reopen note in output: like captions, remind the user that Camtasia must be
    closed when writing and to reopen to see the new bin item.

Where the code goes

  • @camkit/core: a planMediaImport(doc, {file, probed}) → sourceBin entry
    (pure, tested) and the mutation that appends it. Export from core/src/index.ts.
  • @camkit/cli: cmdImportMedia in camkit.ts — arg parse, ffprobe call,
    optional copy into media/, backup/lock/dry-run handling, console output;
    register in COMMANDS, add HELP["import-media"] and a printHelp summary.

Acceptance criteria

  • camkit import-media cleaned.m4a adds the file to doc.sourceBin (and to
    media/ if copying), with correct duration and a unique id, leaving the
    timeline untouched. Verified by camkit sources showing it as bin only.
  • Works for both an audio (.m4a) and a video (.mov) source.
  • --dry-run prints the planned entry and writes nothing; .bak + lock
    safety match rebuild/captions.
  • Unit tests cover the pure entry-builder (audio + video) with a fixture.
  • README + --help updated. bun test and bun run typecheck pass.
  • Manually verified the project reopens cleanly in Camtasia with the new bin
    item (note this in the PR per CONTRIBUTING).

Out of scope (v1)

Placing the source on the timeline, replacing the original audio/video clip with
the cleaned/graded version, and any keyframe/gain handling. Those are a likely
follow-up ("round-trip the cleaned audio onto the timeline") but explicitly not
part of this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    p2Priority 2

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions