OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer / test / transcode.spawn.js

64 lines · javascript ref:
// The video rungs — real ffmpeg, real bytes. Skips itself on a node without an encoder, because
// §6.6.4 says a node without one is an ordinary node and the pyramid is allowed to be partial.
import test from 'brittle'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import { available, probe, encodeLevel } from '../lib/transcode.js'
import { planLevels, VIDEO_LEVELS } from 'favela-markdown/lib/levels.js'

const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-transcode-'))

// A real 720p clip, generated rather than fixtured — a checked-in binary would be the only opaque
// thing in the suite, and ffmpeg is already required for the test to mean anything.
async function makeClip (dir) {
  const { spawn } = await import('bare-subprocess')
  const out = join(dir, 'src.mp4')
  await new Promise((resolve) => {
    const p = spawn('ffmpeg', ['-y', '-f', 'lavfi', '-i', 'testsrc=size=1280x720:rate=15:duration=1',
      '-c:v', 'libx264', '-pix_fmt', 'yuv420p', out])
    // ⛔ DRAIN BOTH PIPES. ffmpeg writes its whole progress report to stderr; with nothing reading
    // it the pipe fills and the child blocks for ever. Resolve on `close` so the stdio has drained
    // before we move on — `exit` fires while it is still open, which left the runner hanging.
    p.stdout && p.stdout.on('data', () => {})
    p.stderr && p.stderr.on('data', () => {})
    p.on('close', resolve)
    p.on('error', resolve)
  })
  return fs.existsSync(out) ? out : null
}

test('★ the video ladder: probe, then encode the rungs below the source', async (t) => {
  if (!await available()) {
    t.pass('no ffmpeg on this node — skipped, which is a supported state and not a failure')
    return
  }
  const dir = tmp()
  const src = await makeClip(dir)
  t.ok(src, 'generated a source clip')

  const dims = await probe(src)
  t.is(dims.height, 720, 'probed the real height')
  t.is(dims.width, 1280)

  // ⛔ NEVER UPSCALE — 720p source yields the 480p rung and nothing above it.
  const want = planLevels({ mimeType: 'video/mp4', width: dims.width, height: dims.height })
  t.alike(want.map((l) => l.name), ['480p'], 'only the rung genuinely below the source')

  const enc = await encodeLevel(src, want[0])
  t.ok(enc && enc.size > 0, 'encoded a real file')
  const back = await probe(enc.path)
  t.is(back.height, 480, '⛔ and it is actually 480 tall, not merely named so')
  t.ok(back.width % 2 === 0, 'even width — h264 refuses an odd one')
  // ⚠ NOT asserted to be smaller. A synthetic testsrc compresses to ~128 kb/s, so a fixed-bitrate
  // 480p rung comes out LARGER — which is real behaviour, not a test artifact, and is exactly why
  // blob-derive drops a rung that does not beat its source. Asserting "smaller" here would have
  // been asserting something ffmpeg never promised.
  t.ok(enc.size > 0, 'and it has bytes; whether it BEATS the source is what blob-derive checks')
})

test('a node without ffmpeg answers rather than throwing', async (t) => {
  const r = await probe('/nonexistent.mp4', { ffprobePath: 'definitely-not-a-real-binary' })
  t.is(r, null, '⛔ null, not an exception — a missing encoder is a state, not a crash')
})