Ctrl K
ComunidadGuías…

capcodec/cli/main.tov

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { closeSync, openSync, readFileSync, readSync, writeFileSync, writeSync } from "node:fs"
import { availableParallelism } from "node:os"
import * as path from "node:path"
import { SideInfo, parseSideInfo } from "../src/analysis"
import { Lookahead, LookaheadState } from "../src/lookahead"
import { masterPlaylist, mediaPlaylist, segmentName } from "../src/mux/hls"
import { Mp4Muxer, TIMESCALE } from "../src/mux/mp4"
import { Encoder, FrameStats } from "../src/encoder"

type Args = {
  input: string,
  output: string,
  recon: string,
  stats: string,
  sideinfo: string,
  width: int,
  height: int,
  fpsNum: int,
  fpsDen: int,
  frames: int,
  qp: int,
  preset: string,
  profile: int,
  keyintSec: f64,
  threads: int,
  noSideinfo: bool,
  fullAnalysis: bool,
  cavlc: bool,
  refs: int,
  noVfr: bool,
  zeroLatency: bool,
  crf: f64,
  bitrate: int,
  maxrate: int,
  bufsize: int,
  ipOffset: int,
  rcMode: int,
  aq: int,
  refresh: int,
  lifetime: bool,
  inter8x8: bool,
  decimate: bool,
  idrReuse: bool,
  globalShift: bool,
  weightp: bool,
  aqTexture: int,
  sliceRows: int,
  idrSliceRows: int,
  idrSlices: int,
  bframes: int,
  bQpOffset: int,
  bAdapt: bool,
  bPyramid: int,
  bRefQpOffset: int,
  format: string,
  subme: int,
  subpelDirs: int,
  keyint: int,
  noise: int,
  lookahead: int,
  laStrength: f64,
  readAheadFrames: int,
}

class UsageError extends Error {
  constructor(message: string) {
    super(message)
    this.name = "UsageError"
  }
}

function usage(): string {
  return [
    "usage: capcodec encode --input <file|-> --width W --height H --fps N[/D] --output <file.264|-> [options]",
    "  --frames K          stop after K frames",
    "  --preset P          live | fast | medium | slow (default medium)",
    "  --crf Q             constant quality (default 26)",
    "  --qp Q              constant QP",
    "  --bitrate K         capped VBR target in kbit/s (--maxrate K, --bufsize K)",
    "  --aq-strength N     text-aware adaptive quantisation strength (default 1, 0 disables)",
    "  --aq-texture N      QP raise for textured (non-text) macroblocks (default 0)",
    "  --profile P         high | main (default high)",
    "  --keyint-sec S      seconds between IDR frames (default 2); --keyint N in frames",
    "  --noise N           treat per-pixel changes up to N as unchanged when no side info is given",
    "  --slice-rows N      macroblock rows per slice (default 0: one slice per frame); --idr-slice-rows N for IDR frames",
    "  --idr-slices N      IDR frames as N row bands coded on separate threads (default 4, 1 for one slice)",
    "  --lookahead N       read N frames ahead and set macroblock QPs from how long their content stays (default 10, 2 on live; 0 disables)",
    "  --bframes N         0..3 B-frames between anchors while most of the frame moves (default 3)",
    "  --read-ahead N      with B-frames and threads, read and analyse up to N more frames while a run is coded (default 3)",
    "  --zerolatency       no B-frames and no lookahead: every frame is output as soon as it is read",
    "  --no-b-adapt        use B-frames on static content too; ignores side info and never drops frames",
    "  --b-qp-offset N     B-frame QP relative to the macroblock QP of the following anchor (default 6)",
    "  --b-pyramid         code the middle B-frame of runs of 2 or more as a reference for the others (default on, off",
    "                      with --preset live); --no-b-pyramid",
    "  --b-ref-qp-offset N QP offset of the reference B-frame in a pyramid (default 5)",
    "  --no-idr-reuse      code every IDR macroblock afresh instead of reusing unchanged ones from the last IDR",
    "  --recon FILE        write the reconstruction (yuv420p)",
    "  --stats FILE        write per-frame statistics as JSON",
    "  --sideinfo FILE     per-frame dirty rects, cursor and scroll hints (JSONL)",
    "  --threads N         worker threads (default: all cores)",
  ].join("\n")
}

function parseIntArg(name: string, v: string | undefined): int throws UsageError {
  if (v === undefined) throw new UsageError(`missing value for ${name}`)
  const n = parseInt(v)
  if (n === undefined) throw new UsageError(`${name} expects an integer, got ${v}`)
  return n
}

function parseArgs(argv: string[]): Args throws UsageError {
  const a: Args = {
    input: "", output: "", recon: "", stats: "", sideinfo: "", width: 0, height: 0, fpsNum: 30, fpsDen: 1,
    frames: -1, qp: 26, preset: "medium", profile: 100, keyintSec: 2.0, threads: 0, noSideinfo: false,
    fullAnalysis: false, cavlc: false, refs: 0, noVfr: false, zeroLatency: false, crf: 26.0, bitrate: 0, maxrate: 0,
    bufsize: 0, ipOffset: 3, rcMode: 1, aq: 1, refresh: 3, lifetime: true, inter8x8: true, decimate: true, idrReuse: true, globalShift: true, weightp: true, aqTexture: 0, sliceRows: 0, idrSliceRows: -1, idrSlices: 4, bframes: -1, bQpOffset: 6, bAdapt: true, bPyramid: -1, bRefQpOffset: 5, format: "", subme: -1, subpelDirs: 0, keyint: 0, noise: 0,
    lookahead: -1, laStrength: -1.0, readAheadFrames: -1,
  }
  let i = 0
  while (i < argv.length) {
    const k = argv[i]!
    const v = argv[i + 1]
    if (k === "--input") {
      a.input = v ?? ""
    } else if (k === "--output") {
      a.output = v ?? ""
    } else if (k === "--recon") {
      a.recon = v ?? ""
    } else if (k === "--stats") {
      a.stats = v ?? ""
    } else if (k === "--sideinfo") {
      a.sideinfo = v ?? ""
    } else if (k === "--width") {
      a.width = try parseIntArg(k, v)
    } else if (k === "--height") {
      a.height = try parseIntArg(k, v)
    } else if (k === "--frames") {
      a.frames = try parseIntArg(k, v)
    } else if (k === "--qp") {
      a.qp = try parseIntArg(k, v)
      a.rcMode = 0
    } else if (k === "--crf") {
      a.crf = parseFloat(v ?? "") ?? 26.0
      a.rcMode = 1
    } else if (k === "--bitrate") {
      a.bitrate = try parseIntArg(k, v)
      a.rcMode = 2
    } else if (k === "--maxrate") {
      a.maxrate = try parseIntArg(k, v)
    } else if (k === "--bufsize") {
      a.bufsize = try parseIntArg(k, v)
    } else if (k === "--ipoffset") {
      a.ipOffset = try parseIntArg(k, v)
    } else if (k === "--aq-strength") {
      a.aq = try parseIntArg(k, v)
    } else if (k === "--refresh") {
      a.refresh = try parseIntArg(k, v)
    } else if (k === "--refs") {
      a.refs = try parseIntArg(k, v)
    } else if (k === "--threads") {
      a.threads = try parseIntArg(k, v)
    } else if (k === "--keyint") {
      a.keyint = try parseIntArg(k, v)
    } else if (k === "--noise") {
      a.noise = try parseIntArg(k, v)
    } else if (k === "--lookahead") {
      a.lookahead = try parseIntArg(k, v)
    } else if (k === "--read-ahead") {
      a.readAheadFrames = try parseIntArg(k, v)
    } else if (k === "--la-strength") {
      a.laStrength = parseFloat(v ?? "") ?? -1.0
    } else if (k === "--subme") {
      a.subme = try parseIntArg(k, v)
    } else if (k === "--subpel-dirs") {
      a.subpelDirs = try parseIntArg(k, v)
    } else if (k === "--format") {
      a.format = v ?? ""
    } else if (k === "--aq-texture") {
      a.aqTexture = try parseIntArg(k, v)
    } else if (k === "--slice-rows") {
      a.sliceRows = try parseIntArg(k, v)
    } else if (k === "--idr-slice-rows") {
      a.idrSliceRows = try parseIntArg(k, v)
    } else if (k === "--idr-slices") {
      a.idrSlices = try parseIntArg(k, v)
    } else if (k === "--bframes") {
      a.bframes = try parseIntArg(k, v)
    } else if (k === "--b-qp-offset") {
      a.bQpOffset = try parseIntArg(k, v)
    } else if (k === "--b-ref-qp-offset") {
      a.bRefQpOffset = try parseIntArg(k, v)
    } else if (k === "--preset") {
      a.preset = v ?? "medium"
    } else if (k === "--profile") {
      a.profile = v === "main" ? 77 : 100
    } else if (k === "--keyint-sec") {
      a.keyintSec = parseFloat(v ?? "") ?? 2.0
    } else if (k === "--fps") {
      const s = v ?? ""
      const parts = s.split("/")
      a.fpsNum = try parseIntArg(k, parts[0])
      a.fpsDen = parts.length > 1 ? try parseIntArg(k, parts[1]) : 1
    } else if (k === "--no-sideinfo") {
      a.noSideinfo = true
      i++
      continue
    } else if (k === "--no-inter8x8") {
      a.inter8x8 = false
      i++
      continue
    } else if (k === "--no-decimate") {
      a.decimate = false
      i++
      continue
    } else if (k === "--no-weightp") {
      a.weightp = false
      i++
      continue
    } else if (k === "--no-global-shift") {
      a.globalShift = false
      i++
      continue
    } else if (k === "--no-idr-reuse") {
      a.idrReuse = false
      i++
      continue
    } else if (k === "--no-lifetime-aq") {
      a.lifetime = false
      i++
      continue
    } else if (k === "--no-vfr") {
      a.noVfr = true
      i++
      continue
    } else if (k === "--zerolatency") {
      a.zeroLatency = true
      i++
      continue
    } else if (k === "--no-b-adapt") {
      a.bAdapt = false
      i++
      continue
    } else if (k === "--b-pyramid" || k === "--no-b-pyramid") {
      a.bPyramid = k === "--b-pyramid" ? 1 : 0
      i++
      continue
    } else if (k === "--cavlc") {
      a.cavlc = true
      i++
      continue
    } else if (k === "--full-analysis") {
      a.fullAnalysis = true
      i++
      continue
    } else {
      throw new UsageError(`unknown option ${k}`)
    }
    i += 2
  }
  if (a.input === "" || a.output === "" || a.width <= 0 || a.height <= 0) throw new UsageError("missing required options")
  if ((a.width & 1) !== 0 || (a.height & 1) !== 0) throw new UsageError("width and height must be even")
  if (a.qp < 0 || a.qp > 51) throw new UsageError("qp must be in 0..51")
  if (a.bframes === -1) a.bframes = a.zeroLatency ? 0 : 3
  if (a.bframes < 0 || a.bframes > 3) throw new UsageError("--bframes must be in 0..3")
  if (a.bPyramid === -1) a.bPyramid = a.preset === "live" ? 0 : 1
  if (a.refs === 0) a.refs = a.preset === "slow" ? 3 : a.preset === "medium" ? 2 : 1
  if (a.lookahead < 0) a.lookahead = a.zeroLatency ? 0 : a.preset === "live" ? 2 : 10
  if (a.readAheadFrames < 0) a.readAheadFrames = 3
  return a
}

function readFull(fd: int, buf: Uint8Array, off: int, n: int): int throws Error {
  let got = 0
  while (got < n) {
    const k = try readSync(fd, buf, off + got, n - got)
    if (k === 0) break
    got += k
  }
  return got
}

const RA_READ = 0
const RA_TAKEN = 1
const RA_TOTAL = 2
const RA_FAIL = 3
const RA_DONE = 4
const RA_BASE = 5
const RA_SEQ = 6
const RA_WAITERS = 7
const RA_LINES = 64

type AheadJob = {
  fd: int, ring: Uint8Array, fb: int, slotOf: Int32Array, prog: Int32Array, hz: Int32Array, horizons: Int32Array,
  life: Int32Array[], keep: Uint8Array[], state: LookaheadState, w: int, h: int, depth: int, extra: int, noise: int,
  geometricTail: bool, windowTail: bool, frameStop: int,
}

function raPublish(prog: Int32Array, at: int, v: int) {
  Atomics.store(prog, at, v)
  Atomics.add(prog, RA_SEQ, 1)
  if (Atomics.load(prog, RA_WAITERS) > 0) Atomics.notify(prog, RA_SEQ)
}

// Reads, pushes and takes in the serial order while the main thread leaves room (RA_BASE is the oldest frame it may
// still use), and returns rather than wait for room, so threads that run one after another (wasm) never block on it.
function readBatch(job: AheadJob, lines: string[], from: int, taken: int, total: int, nLines: int, id: int) {
  const prog = job.prog
  const slots = job.slotOf.length
  const look = new Lookahead(job.w, job.h, job.depth + job.extra + 1, true)
  look.attach(job.state, from, job.noise, job.geometricTail, job.windowTail)
  look.total = total
  let k = from
  let t = taken
  try {
    while (true) {
      const base = Atomics.load(prog, RA_BASE)
      const end = base + 1 + job.extra
      if (t <= end && (look.total >= 0 ? t < look.total : t + job.depth < k)) {
        const i = t % job.life.length
        const horizon = job.horizons[t % job.horizons.length]!
        job.hz[i] = horizon
        look.takeAhead(t, horizon, job.life[i]!, job.keep.length > 0 ? job.keep[i]! : new Uint8Array(0))
        t++
        raPublish(prog, RA_TAKEN, t)
      } else if (look.total < 0 && k <= end + job.depth && (k - from < lines.length || k >= nLines)) {
        let slot = 0
        let j = base
        while (j < k) {
          if (job.slotOf[j % slots]! === slot) {
            slot++
            j = base
          } else {
            j++
          }
        }
        const got = job.frameStop >= 0 && k >= job.frameStop ? 0 : try readFull(job.fd, job.ring, slot * job.fb, job.fb)
        if (got < job.fb) {
          look.total = k
          raPublish(prog, RA_TOTAL, k)
        } else {
          const line = k - from < lines.length ? lines[k - from]! : ""
          const side = line !== "" ? try parseSideInfo(line) : undefined
          job.slotOf[k % slots] = slot
          look.push(job.ring, slot * job.fb, job.slotOf[(k - 1) % slots]! * job.fb, side)
          k++
          raPublish(prog, RA_READ, k)
        }
      } else {
        break
      }
    }
  } catch (e) {
    raPublish(prog, RA_FAIL, k)
  }
  raPublish(prog, RA_DONE, id)
}

class ReadAhead {
  job: AheadJob
  sideLines: string[]
  batch: Thread<void> | undefined = undefined
  spawned = 0

  constructor(job: AheadJob, sideLines: string[], read: int) {
    this.job = job
    this.sideLines = sideLines
    Atomics.store(job.prog, RA_READ, read)
    Atomics.store(job.prog, RA_TAKEN, 1)
    Atomics.store(job.prog, RA_TOTAL, -1)
    Atomics.store(job.prog, RA_FAIL, -1)
  }

  join() {
    const b = this.batch
    if (b === undefined) return
    b.join()
    this.batch = undefined
  }

  advance(base: int) {
    const prog = this.job.prog
    if (base > Atomics.load(prog, RA_BASE)) Atomics.store(prog, RA_BASE, base)
    if (this.batch !== undefined && Atomics.load(prog, RA_DONE) === this.spawned) this.join()
    const taken = Atomics.load(prog, RA_TAKEN)
    const total = Atomics.load(prog, RA_TOTAL)
    if (this.batch !== undefined || Atomics.load(prog, RA_FAIL) >= 0 || taken > base + 1 + this.job.extra ||
      (total >= 0 && taken >= total)) {
      return
    }
    const from = Atomics.load(prog, RA_READ)
    const lines = this.sideLines.slice(from, from + RA_LINES)
    const nLines = this.sideLines.length
    const job = this.job
    this.spawned++
    const id = this.spawned
    this.batch = Thread.spawn(() => readBatch(job, lines, from, taken, total, nLines, id))
  }

  wait(n: int): void throws Error {
    const prog = this.job.prog
    // Frame n-1 is done with before this wait; freeing its slot now keeps a reader that is keeping up with real-time
    // input inside the per-frame path's depth + 2 slots, so the extra slots are only touched when the encoder falls behind.
    if (n > Atomics.load(prog, RA_BASE)) Atomics.store(prog, RA_BASE, n)
    while (true) {
      const seq = Atomics.load(prog, RA_SEQ)
      const total = Atomics.load(prog, RA_TOTAL)
      if (Atomics.load(prog, RA_TAKEN) > n || (total >= 0 && n >= total)) return
      const fail = Atomics.load(prog, RA_FAIL)
      if (fail >= 0) {
        if (fail < this.sideLines.length && this.sideLines[fail]! !== "") try parseSideInfo(this.sideLines[fail]!)
        throw new Error("can't read the input")
      }
      if (this.batch === undefined || Atomics.load(prog, RA_DONE) === this.spawned) {
        this.advance(n - 1)
      } else {
        Atomics.add(prog, RA_WAITERS, 1)
        Atomics.wait(prog, RA_SEQ, seq)
        Atomics.sub(prog, RA_WAITERS, 1)
      }
    }
  }

  // What the serial loop would have read when taking frame n: a fallback take must not see further.
  readFor(n: int): int {
    const got = Atomics.load(this.job.prog, RA_READ)
    return got < n + this.job.depth + 1 ? got : n + this.job.depth + 1
  }

  endedFor(n: int): bool {
    const total = Atomics.load(this.job.prog, RA_TOTAL)
    return total >= 0 && total <= n + this.job.depth
  }
}

function writeFragment(format: string, fd: int, dir: string, frag: u8[], index: int): int throws Error {
  if (format === "hls") {
    const sfd = try openSync(path.join(dir, segmentName(index)), "w")
    try writeSync(sfd, frag)
    try closeSync(sfd)
  } else {
    try writeSync(fd, frag)
  }
  return frag.length
}

class Sink {
  format: string
  fd: int
  dir: string
  mux: Mp4Muxer
  initWritten = false
  segDurations: f64[] = []
  bytes = 0

  constructor(format: string, fd: int, dir: string, mux: Mp4Muxer) {
    this.format = format
    this.fd = fd
    this.dir = dir
    this.mux = mux
  }

  fragmented(): bool {
    return this.format === "fmp4" || this.format === "hls"
  }
}

function flushFragment(sink: Sink, nextFrame: int): void throws Error {
  const frag = sink.mux.fragment(sink.mux.sizes.length, nextFrame)
  sink.bytes += try writeFragment(sink.format, sink.fd, sink.dir, frag, sink.segDurations.length + 1)
  sink.segDurations.push(f64(sink.mux.lastFragmentTicks) / f64(TIMESCALE))
}

function emitUnit(sink: Sink, bits: u8[], index: int, idr: bool): void throws Error {
  if (sink.format === "annexb") {
    try writeSync(sink.fd, bits)
    sink.bytes += bits.length
    return
  }
  if (sink.fragmented() && idr && sink.mux.pendingSamples() > 0) try flushFragment(sink, index)
  sink.mux.addSample(bits, index, idr)
  if (sink.fragmented() && !sink.initWritten) {
    const init = sink.mux.initSegment()
    try writeSync(sink.fd, init)
    sink.bytes += init.length
    sink.initWritten = true
  }
}

function emitCoded(sink: Sink, enc: Encoder, bits: u8[], firstStat: int): void throws Error {
  let off = 0
  for (let k = firstStat; k < enc.frames.length; k++) {
    const f = enc.frames[k]!
    if (f.bytes === 0) continue
    try emitUnit(sink, bits.slice(off, off + f.bytes), f.index, f.type === "I")
    off += f.bytes
  }
}

function statsJson(enc: Encoder, args: Args, wallMs: f64, bytes: int, frameMap: int[]): string {
  const frames: string[] = []
  for (const f of enc.frames) {
    frames.push(frameJson(f))
  }
  const mbs = enc.mbw * enc.mbh
  const total = enc.tAnalysis + enc.tMotion + enc.tMode + enc.tTq + enc.tEntropy + enc.tDeblock
  return "{" + [
    `"encoder":"capcodec"`,
    `"preset":${JSON.stringify(args.preset)}`,
    `"width":${args.width}`,
    `"height":${args.height}`,
    `"mbs":${mbs}`,
    `"frames_in":${enc.frameIndex}`,
    `"frames_coded":${enc.codedFrames}`,
    `"frames_dropped":${enc.frameIndex - enc.codedFrames}`,
    `"bytes":${bytes}`,
    `"time_ms":{"total":${wallMs},"stages":${total},"analysis":${enc.tAnalysis},"motion":${enc.tMotion},"mode":${enc.tMode},"tq":${enc.tTq},"entropy":${enc.tEntropy},"deblock":${enc.tDeblock}}`,
    `"frame_map":[${frameMap.join(",")}]`,
    `"frames":[${frames.join(",")}]`,
  ].join(",") + "}"
}

function frameJson(f: FrameStats): string {
  return `{"i":${f.index},"type":"${f.type}","bytes":${f.bytes},"qp":${f.qp},"changed_mbs":${f.changedMbs},` +
    `"ms":{"analysis":${f.analysis},"motion":${f.motion},"mode":${f.mode},"tq":${f.tq},"entropy":${f.entropy},` +
    `"deblock":${f.deblock},"total":${f.total}},"reused":${f.reused}}`
}

function encodeCommand(args: Args): void throws Error {
  const t0 = performance.now()
  const w = args.width
  const h = args.height
  const frameBytes = w * h * 3 / 2
  const fb = Math.trunc(frameBytes)
  const enc = new Encoder({
    width: w, height: h, fpsNum: args.fpsNum, fpsDen: args.fpsDen, qp: args.qp,
    keyint: args.keyint > 0 ? args.keyint : Math.max(1, Math.round(args.keyintSec * f64(args.fpsNum) / f64(args.fpsDen))),
    profile: args.profile,
    preset: args.preset, cabac: !args.cavlc, refs: args.refs, rcMode: args.rcMode, crf: args.crf,
    bitrate: args.bitrate, maxrate: args.maxrate > 0 ? args.maxrate : Math.round(f64(args.bitrate) * 1.5),
    bufsize: args.bufsize, ipOffset: args.ipOffset, weightedPred: args.weightp, worker: false,
  })
  enc.threads = args.threads > 0 ? args.threads : availableParallelism()
  enc.vfr = !args.noVfr && !args.fullAnalysis
  enc.aqStrength = args.aq
  enc.aqTexture = args.aqTexture
  enc.noise = args.noise
  enc.refreshDelta = args.refresh
  enc.lifetimeAq = args.lifetime
  enc.inter8x8 = args.inter8x8
  enc.decimate = args.decimate
  enc.idrReuse = args.idrReuse
  enc.globalShift = args.globalShift
  enc.sliceRowsP = args.sliceRows
  enc.sliceRowsI = args.idrSliceRows >= 0 ? args.idrSliceRows : args.sliceRows
  enc.idrSlices = args.idrSlices < 1 ? 1 : args.idrSlices
  if (args.subme >= 0) enc.subpel = args.subme
  if (args.subpelDirs > 0) {
    enc.subpelDirs = args.subpelDirs
    enc.videoHpelDirs = args.subpelDirs
    enc.videoQpelDirs = args.subpelDirs
  }
  enc.fullAnalysis = args.fullAnalysis
  enc.bQpOffset = args.bQpOffset
  enc.bAdapt = args.bAdapt
  enc.pyramid = args.bPyramid === 1
  enc.bRefQpOffset = args.bRefQpOffset
  if (args.bframes > 0) enc.configureB(args.bframes)
  if (args.laStrength >= 0) enc.laStrength = args.laStrength
  let sideLines: string[] = []
  if (args.sideinfo !== "" && !args.noSideinfo) {
    const text = try readFileSync(args.sideinfo)
    sideLines = text.split("\n")
  }
  const inFd = args.input === "-" ? 0 : try openSync(args.input, "r")
  let format = args.format
  if (format === "") {
    const ext = path.extname(args.output).toLowerCase()
    format = ext === ".mp4" ? "mp4" : ext === ".fmp4" ? "fmp4" : ext === ".m3u8" ? "hls" : "annexb"
  }
  const outDir = path.dirname(args.output)
  const outFd = format === "hls" ? try openSync(path.join(outDir, "init.mp4"), "w") : args.output === "-" ? 1 : try openSync(args.output, "w")
  const mux = new Mp4Muxer(w, h, args.fpsNum, args.fpsDen)
  mux.reorder = enc.bframes > 0
  mux.reorderFrames = enc.pyramid && enc.bframes >= 2 ? 2 : 1
  const sink = new Sink(format, outFd, outDir, mux)
  const reconFd = args.recon === "" ? -1 : try openSync(args.recon, "w")
  const depth = args.lookahead
  const pushLook = depth > 0 && !enc.flatLookahead()
  const readAhead = enc.threads > 1
  const extra = readAhead && enc.bframes > 0 && pushLook ? args.readAheadFrames : 0
  const slots = depth + (readAhead ? 2 : 1) + extra
  const ring = new Uint8Array(fb * slots)
  const slotOf = new Int32Array(slots)
  for (let k = 0; k < slots; k++) slotOf[k] = k
  let sides: (SideInfo | undefined)[] = []
  for (let k = 0; k < slots; k++) sides.push(undefined)
  // While this thread waits for or codes frame n the reader may push up to frame n + depth + extra + 1; a take that
  // falls back to this thread still needs frames n..n+depth in the lookahead's ring, and the encoder frame n's change map.
  const look = new Lookahead(w, h, extra > 0 ? depth + extra + 1 : depth, false)
  const geometricTail = enc.bframes === 0
  const windowTail = geometricTail && enc.keyint > enc.laSpan
  look.noise = args.noise
  look.geometricTail = geometricTail
  look.windowTail = windowTail
  const noise = args.noise
  const laMbs = depth > 0 ? ((w + 15) >> 4) * ((h + 15) >> 4) : 0
  const laLife = [new Int32Array(laMbs), new Int32Array(laMbs)]
  const laKeep = [new Uint8Array(laMbs), new Uint8Array(laMbs)]
  for (let k = 0; k < extra; k++) {
    laLife.push(new Int32Array(laMbs))
    laKeep.push(new Uint8Array(laMbs))
  }
  let ra: ReadAhead | undefined = undefined
  let read = 0
  let eof = false
  let pending: Thread<int> | undefined = undefined
  const aheadRead = new Int32Array(1)
  const inRing = new Int32Array(1)
  Atomics.store(inRing, 0, 1)
  const none = new Uint8Array(0)
  let pendingHorizon = -1
  let aheadHorizon = -1
  let n = 0
  let frameMap: int[] = []
  enc.wantRecon = reconFd >= 0
  enc.frameThreads = !args.zeroLatency
  const pipelined = enc.bframes > 0 || enc.intraParallel()
  while (args.frames < 0 || n < args.frames) {
    if (ra !== undefined) {
      try ra.wait(n)
      read = ra.readFor(n)
      eof = ra.endedFor(n)
      look.pushed = read
      aheadHorizon = ra.job.hz[n % laLife.length]!
    }
    while (!eof && read <= n + depth) {
      const slot = read % slots
      let got = 0
      if (pending !== undefined) {
        got = pending.join()
        pending = undefined
        if (got < 0) throw new Error("can't read the input")
        if (got === fb && pushLook) look.pushed++
        aheadHorizon = pendingHorizon
      } else if (args.frames < 0 || read < args.frames) {
        got = try readFull(inFd, ring, slot * fb, fb)
        if (got === fb) {
          sides[slot] = read < sideLines.length && sideLines[read]! !== "" ? try parseSideInfo(sideLines[read]!) : undefined
          if (pushLook) look.push(ring, slot * fb, ((read + slots - 1) % slots) * fb, sides[slot])
        }
      }
      if (got < fb) {
        eof = true
        break
      }
      read++
    }
    if (n >= read) break
    if (depth > 0) {
      if (eof && look.total < 0) look.finish()
      const life = laLife[n % laLife.length]!
      const keep = laKeep[n % laKeep.length]!
      if (aheadHorizon >= 0 && aheadHorizon === enc.laHorizon()) {
        look.taken++
        enc.useLookahead(life, keep)
      } else {
        if (ra !== undefined) ra.join()
        enc.takeLookahead(look, life, keep)
      }
      aheadHorizon = -1
      enc.useChanges(look.changesOf(n), n, look.noise)
    }
    if (extra > 0 && n === 0 && !eof) {
      const horizons = new Int32Array(enc.keyint)
      for (let k = 0; k < enc.keyint; k++) horizons[k] = enc.laHorizonAt(k)
      ra = new ReadAhead({
        fd: inFd, ring, fb, slotOf, prog: new Int32Array(8), hz: new Int32Array(laLife.length), horizons, life: laLife,
        keep: enc.wantsKeepMap() ? laKeep : [], state: look.share(), w, h, depth, extra, noise, geometricTail, windowTail,
        frameStop: args.frames,
      }, sideLines, read)
    }
    if (ra !== undefined) {
      ra.advance(n)
    } else if (readAhead && !eof && (args.frames < 0 || read < args.frames)) {
      const buf = ring
      const slot = read % slots
      const off = slot * fb
      const prevOff = ((read + slots - 1) % slots) * fb
      const side = read < sideLines.length && sideLines[read]! !== "" ? try parseSideInfo(sideLines[read]!) : undefined
      sides[slot] = side
      const hasSide = side !== undefined
      const dirty = side !== undefined ? side.dirty : []
      const scroll = side !== undefined ? side.scroll : []
      const st = look.share()
      const at = look.pushed
      const target = n + 1
      const horizon = pushLook && (args.frames < 0 || target < args.frames) ? enc.laHorizonAt(target) : -1
      const life = laLife[target % 2]!
      const keep = enc.wantsKeepMap() ? laKeep[target % 2]! : new Uint8Array(0)
      pendingHorizon = horizon
      Atomics.store(aheadRead, 0, 0)
      pending = Thread.spawn(() => {
        try {
          const got = try readFull(inFd, buf, off, fb)
          Atomics.store(aheadRead, 0, got >= fb ? 1 : -1)
          if (pushLook) {
            const worker = new Lookahead(w, h, depth, true)
            worker.attach(st, at, noise, geometricTail, windowTail)
            if (got === fb) {
              let s: SideInfo | undefined = undefined
              if (hasSide) {
                const t = new SideInfo()
                t.dirty = dirty
                t.scroll = scroll
                s = t
              }
              worker.push(buf, off, prevOff, s)
            } else {
              worker.total = at
            }
            if (horizon >= 0) worker.takeAhead(target, horizon, life, keep)
          }
          return got
        } catch (e) {
          Atomics.store(aheadRead, 0, -1)
          return -1
        }
      })
    }
    const off = slotOf[n % slots]! * fb
    const side = ra === undefined || n <= depth ? sides[n % slots] :
      n < sideLines.length && sideLines[n]! !== "" ? try parseSideInfo(sideLines[n]!) : undefined
    const next = n + 1
    let nextFrame = none
    let nextReady = inRing
    if (readAhead && (next >= sideLines.length || sideLines[next]! === "")) {
      if (next < read) {
        nextFrame = ring
      } else if (pending !== undefined) {
        nextFrame = ring
        nextReady = aheadRead
      }
    }
    enc.expectNext(nextFrame, slotOf[next % slots]! * fb, nextReady)
    if (pipelined) {
      const first = enc.frames.length
      const bits = enc.submit(ring, off, side)
      try emitCoded(sink, enc, bits, first)
      for (const r of enc.takeRecon()) try writeSync(reconFd, r)
    } else {
      const bits = enc.encodeFrame(ring, off, side)
      if (bits.length > 0) {
        try emitUnit(sink, bits, n, enc.frames[enc.frames.length - 1]!.type === "I")
        frameMap.push(n)
        if (reconFd >= 0) try writeSync(reconFd, enc.reconFrame())
      }
    }
    n++
  }
  if (ra !== undefined) ra.join()
  if (pipelined) {
    enc.expectNext(none, 0, aheadRead)
    const first = enc.frames.length
    const tail = enc.flush()
    try emitCoded(sink, enc, tail, first)
    for (const r of enc.takeRecon()) try writeSync(reconFd, r)
    for (const f of enc.frames) {
      if (f.bytes === 0) continue
      let j = frameMap.length
      frameMap.push(f.index)
      while (j > 0 && frameMap[j - 1]! > f.index) {
        frameMap[j] = frameMap[j - 1]!
        j--
      }
      frameMap[j] = f.index
    }
  }
  try finishOutput(args, enc, sink, n, frameMap, inFd, reconFd, t0)
}

function finishOutput(args: Args, enc: Encoder, sink: Sink, n: int, frameMap: int[], inFd: int, reconFd: int, t0: f64): void throws Error {
  if (sink.format === "mp4") {
    const file = sink.mux.progressive(n)
    try writeSync(sink.fd, file)
    sink.bytes += file.length
  } else if (sink.fragmented() && sink.mux.pendingSamples() > 0) {
    try flushFragment(sink, n)
  }
  const bytes = sink.bytes
  if (sink.format === "hls") {
    const segDurations = sink.segDurations
    try writeFileSync(args.output, mediaPlaylist(segDurations, "init.mp4"))
    const sps = sink.mux.sps
    const hex = (b: u8): string => {
      const v = int(b)
      const d = "0123456789abcdef"
      return d.slice(v >> 4, (v >> 4) + 1) + d.slice(v & 15, (v & 15) + 1)
    }
    const codecs = sps.length >= 4 ? `avc1.${hex(sps[1]!)}${hex(sps[2]!)}${hex(sps[3]!)}` : "avc1.640028"
    let total = 0.0
    for (const d of segDurations) total += d
    const bw = total > 0.0 ? Math.round(f64(bytes) * 8.0 / total * 1.2) : 1000000
    try writeFileSync(path.join(sink.dir, "master.m3u8"), masterPlaylist(path.basename(args.output), bw, codecs, args.width, args.height, f64(args.fpsNum) / f64(args.fpsDen)))
  }
  if (inFd !== 0) try closeSync(inFd)
  if (sink.fd !== 1) try closeSync(sink.fd)
  if (reconFd >= 0) try closeSync(reconFd)
  const wall = performance.now() - t0
  if (args.stats !== "") try writeFileSync(args.stats, statsJson(enc, args, wall, bytes, frameMap))
  console.error(`capcodec: ${n} frames, ${bytes} bytes, ${(f64(n) * 1000.0 / wall).toFixed(1)} fps`)
}

function main(): int {
  const argv = process.argv.slice(2)
  if (argv.length === 0 || argv[0]! !== "encode") {
    console.error(usage())
    return 2
  }
  try {
    const args = try parseArgs(argv.slice(1))
    try encodeCommand(args)
  } catch (e) {
    console.error(`capcodec: ${e.message}`)
    if (e instanceof UsageError) console.error(usage())
    return 1
  }
  return 0
}