{"version":3,"file":"pic-video-action.mjs","names":[],"sources":["../../src/actions/pic-video-action.ts"],"sourcesContent":["import { unlink, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { spawn } from 'node:child_process';\n\nimport sharp, { type Sharp } from 'sharp';\nimport type { Command } from 'commander';\nimport { Presets, SingleBar } from 'cli-progress';\n\nimport { decompress, parseFont, parsePic } from '@4bitlabs/sci0';\nimport { generatePic, menuTextFilter } from '@4bitlabs/sci0-renderer';\nimport { renderPixelData } from '@4bitlabs/image';\nimport { picMatcher, fontMatcher } from '../helpers/resource-matchers.js';\nimport {\n  loadContentFromMap,\n  type ResourceMapPredicate,\n} from './load-content-from-map.js';\nimport type {\n  RenderPicOptions,\n  RenderPipelineOptions,\n} from '../models/render-pic-options.js';\nimport { pickRenderOptions } from './pick-render-options.js';\nimport { getRootOptions } from './get-root-options.js';\nimport { formatGraph } from './filter-graph.js';\nimport { crtFilterGraph, simpleFilterGraph } from './crt-filter-graph.js';\nimport { createPicPipeline } from '../helpers/create-pic-pipeline.js';\nimport { optionalDeferredCrtFilter } from '../helpers/apply-crt-filter.js';\n\ninterface PicVideoActionOptions {\n  readonly output: string;\n  readonly title: string;\n  readonly fps: number;\n}\n\nconst FORMAT_MAPPING = {\n  png: (source: Sharp) => source.png().toBuffer(),\n  jpg: (source: Sharp) => source.jpeg().toBuffer(),\n  webp: (source: Sharp) => source.webp().toBuffer(),\n  raw: (source: Sharp) => source.raw().toBuffer(),\n};\n\nfunction ffmpeg(args: string[]): Promise<void> {\n  console.log(`ffmpeg ${args.join(' ')}\\n`);\n\n  return new Promise((resolve, reject) => {\n    const proc = spawn('ffmpeg', args, { stdio: 'inherit' });\n    proc.on('error', reject);\n    proc.on('close', (code) => {\n      if (code === 0) resolve();\n      else reject(new Error(`ffmpeg exited with code ${code}`));\n    });\n  });\n}\n\nconst mustParse = async <T>(\n  root: string,\n  engine: 'sci0' | 'sci01',\n  matcher: ResourceMapPredicate,\n  parser: (data: Uint8Array) => T,\n) => {\n  const [picHeader, picRaw] = await loadContentFromMap(root, matcher);\n  return parser(decompress(engine, picHeader.compression, picRaw));\n};\n\nexport async function picVideoAction(\n  id: number,\n  options: PicVideoActionOptions & RenderPicOptions & RenderPipelineOptions,\n  cmd: Command,\n) {\n  const progress = new SingleBar(Presets.shades_grey);\n\n  const { root, engine } = getRootOptions(cmd);\n  const [picOptions, renderOptions] = pickRenderOptions(options);\n  const {\n    title,\n    format,\n    fps,\n    forcePal,\n    output = `pic.%num.%d.${format}`,\n  } = picOptions;\n  const pipeline = createPicPipeline(picOptions.layer, renderOptions);\n\n  const pic = await mustParse(root, engine, picMatcher(id), (d) => parsePic(d));\n  const font = await mustParse(root, engine, fontMatcher(0), parseFont);\n\n  const total = pic.length;\n  const padLength = Math.ceil(Math.log10(total));\n\n  const { dir, name, ext } = path.parse(output);\n  const crtFilter = optionalDeferredCrtFilter(options.crt);\n\n  const frames = Array(total)\n    .fill(null)\n    .map((_, idx) =>\n      path.format({\n        dir,\n        name: name\n          .replace(/%num/, id.toString(10).padStart(3, '0'))\n          .replace(/%d/, idx.toString(10).padStart(padLength, '0')),\n        ext,\n      }),\n    );\n\n  progress.start(total, 0);\n\n  for (const [idx, , layers] of generatePic(pic, { forcePal })) {\n    const outfile = frames[idx];\n    const message = {\n      font,\n      left: [title, `PIC.${id.toString(10).padStart(3, '0')}`]\n        .filter((it) => it.length > 0)\n        .join(': '),\n      right:\n        idx === frames.length - 1\n          ? ''\n          : `${idx} : ${((idx / frames.length) * 100).toFixed(0)}%`,\n    };\n\n    const { data, width, height } = renderPixelData(layers[picOptions.layer], {\n      pre: [message && menuTextFilter(1, 1, message), ...(pipeline.pre ?? [])],\n      render: pipeline.render,\n      post: [...(pipeline.post ?? []), crtFilter],\n    });\n\n    const image = sharp(data, { raw: { width, height, channels: 4 } });\n    if (outfile === '-') {\n      process.stdout.write(await FORMAT_MAPPING[format](image));\n    } else {\n      await image.toFile(outfile);\n    }\n\n    progress.increment();\n  }\n\n  progress.stop();\n\n  const seqFn = path.format({\n    dir,\n    name: name\n      .replace(/%num/, id.toString(10).padStart(3, '0'))\n      .replace(/%d/, 'seq'),\n    ext: '.txt',\n  });\n\n  await writeFile(\n    seqFn,\n    [\n      `ffconcat version 1.0`,\n      ``,\n      `file '${path.relative(dir, frames[frames.length - 1])}'`,\n      `duration ${3.0 * (fps / 25)}`,\n      ...frames.flatMap((fn) => [\n        `file '${path.relative(dir, fn)}'`,\n        `duration ${1 / 25}`,\n      ]),\n    ].join('\\n'),\n  );\n\n  const mp4Fn = path.format({\n    dir,\n    name: name\n      .replace(/%num/, id.toString(10).padStart(3, '0'))\n      .replace(/%d/, 'result'),\n    ext: '.mp4',\n  });\n\n  const finalFn = path.format({\n    dir,\n    name: name\n      .replace(/%num/, id.toString(10).padStart(3, '0'))\n      .replace(/%d/, 'final'),\n    ext: '.png',\n  });\n\n  const graphFn = options.crt ? simpleFilterGraph : crtFilterGraph;\n\n  await ffmpeg([\n    '-f',\n    'concat',\n    '-i',\n    seqFn,\n    '-vf',\n    formatGraph(graphFn({ desiredFps: fps })),\n    '-movflags',\n    '+faststart',\n    mp4Fn,\n  ]);\n\n  await ffmpeg([\n    '-i',\n    frames[frames.length - 1],\n    '-vf',\n    formatGraph(graphFn({ image: true, resolution: [-2, 1080] })),\n    finalFn,\n  ]);\n\n  await Promise.all([...frames, seqFn].map((fn) => unlink(fn)));\n}\n"],"mappings":"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"}