OverviewFilesCommitsIssuesDocsReleasesPeers

favela-markdown / lib / levels.js

142 lines · javascript ref:
// levels.js — the mip pyramid. Pure: no I/O, no clock, no throw.
//
// ⛔ IT LIVES HERE BECAUSE IT IS RECORD VOCABULARY. `spec/record-vocabulary.md` §3.5 defines the
// pointer; the peer writes one, and BOTH bridges read one to choose a level. A copy per consumer is
// how this monorepo ended up with six copies of one signing format, three of which ate the first
// character of a hash — and a ladder that drifts between the writer and the reader is worse than
// that, because each side stays internally consistent while disagreeing about what a level means.
//
// ⛔ THE PYRAMID IS THE OBJECT (SOCIAL-MODEL §6.6.1). A texture in a game is not a file plus a set
// of related smaller files; it is one asset containing a pyramid, and the consumer picks a level by
// how much of the screen it covers. Media works the same way here. There is no `variant-of` edge
// because the levels were never separate objects — the relation is the structure of the asset, not
// something asserted and then maintained.
//
// ⛔ AND IT SIDESTEPS THE OVERLAY'S LIMIT RATHER THAN FIGHTING IT. hyper-overlay addresses a file by
// a whole-file hash, so a prefix cannot be authenticated and sparse fetch of ONE blob is impossible.
// A pyramid needs no prefix: every level is a whole file with its own oid, which is exactly what the
// overlay already does well. Fetch the pointer, choose a level, fetch that oid. Sparseness BY LEVEL.
//
// ⛔ BRIDGES SELECT, THEY DO NOT DOWNSAMPLE. Encoding happens once, at authoring, deterministically —
// like generating mips at asset build time — instead of per delivery on every bridge. `selectLevel`
// is that choice, and it reports which level it picked so the loss is never silent.
//
// ⚠ Cost is the mip cost and it is bounded: 1 + ¼ + 1/16 + … = 4/3 of the base. Known in advance,
// unlike an open-ended set of variants.

// ── the ladders ───────────────────────────────────────────────────────────────────────────────
//
// ⚠ Levels are chosen against REAL NETWORK CEILINGS, not powers of two (§6.6.1, "where the analogy
// breaks" #1). bsky takes 2,000,000 bytes for a post image and 1,000,000 for a profile picture;
// Mastodon resizes on receipt and is not the binding constraint. The edges below bracket those.
export const IMAGE_LEVELS = [
  { name: 'full', edge: 2000, targetBytes: 2000000 },
  { name: 'large', edge: 1280, targetBytes: 1000000 },
  { name: 'small', edge: 640, targetBytes: 300000 },
  { name: 'thumb', edge: 320, targetBytes: 80000 }
]

// The bitrate ladder IS a mip chain for video — hypernote's HLS_LEVELS, which the spec names as the
// one we already ship. Same shape, same never-upscale filter.
export const VIDEO_LEVELS = [
  { name: '1080p', edge: 1080, bitrate: '5000k', maxrate: '6000k', profile: 'high' },
  { name: '720p', edge: 720, bitrate: '2500k', maxrate: '3000k', profile: 'main' },
  { name: '480p', edge: 480, bitrate: '1000k', maxrate: '1200k', profile: 'baseline' }
]

export const laddersFor = (mimeType) =>
  /^video\//.test(String(mimeType || '')) ? VIDEO_LEVELS
    : /^image\//.test(String(mimeType || '')) ? IMAGE_LEVELS
      : []

/**
 * Which levels should be GENERATED for a source asset.
 *
 * ⛔ NEVER UPSCALE (§6.6.1 #2). A level is a floor, not a guarantee: an asset smaller than the
 * lowest level has exactly one level, and the pointer must not imply levels it lacks. hypernote
 * already encodes this by filtering `l.height <= srcHeight`; this is the same filter, stated once.
 *
 * The source itself is always level 0 — the author's own bytes, unaltered.
 */
export function planLevels ({ mimeType = '', width = 0, height = 0 } = {}) {
  const ladder = laddersFor(mimeType)
  if (!ladder.length) return []
  const longest = Math.max(Number(width) || 0, Number(height) || 0)
  // For video the ladder is keyed on HEIGHT, which is how every encoder ladder is expressed.
  const source = /^video\//.test(mimeType) ? (Number(height) || 0) : longest
  if (!source) return []
  return ladder.filter((l) => l.edge < source)
}

/**
 * Scale a source to a level's edge, preserving aspect. Whole pixels, never zero.
 */
export function scaleTo ({ width, height }, edge) {
  const w = Number(width) || 0
  const h = Number(height) || 0
  const longest = Math.max(w, h)
  if (!longest || edge >= longest) return { width: w, height: h }
  const s = edge / longest
  return { width: Math.max(1, Math.round(w * s)), height: Math.max(1, Math.round(h * s)) }
}

// ── the pointer ───────────────────────────────────────────────────────────────────────────────
//
// ⛔ THERE IS NO MANIFEST FILE (record-vocabulary §3.5). The pointer carries the levels — one
// pointer, N oids, zero extra artifacts. A manifest file would be a third artifact for a job the
// pointer already does, and the only per-asset duplicate in a design whose whole point is that
// identical bytes are stored once however many records reference them.
//
//     { mimeType, size, levels: [ { oid, w, h, size, by }, … ] }
//
// ⛔ `by` IS LOAD-BEARING, not a nicety (§6.6.4 #2/#3). A pyramid is allowed to be PARTIAL, and a
// level may be derived later by a bridge or a well-resourced peer. Deriving is not authoring: a peer
// adding a level has produced a derived artifact and must say so, or an author's bytes and somebody
// else's re-encode become indistinguishable. §8's "never re-encode a record you did not author"
// already cost two of wj62's posts.
export const AUTHOR = 'author'
export const derivedBy = (who) => 'derived:' + String(who || 'unknown').slice(0, 32)

export function makePointer ({ mimeType, size, levels = [] }) {
  return {
    mimeType: String(mimeType || 'application/octet-stream'),
    size: Number(size) || 0,
    // Largest first, so "the biggest that fits" is a scan from the front and the base is level 0.
    levels: [...levels].sort((a, b) => (b.w * b.h) - (a.w * a.h)).map((l) => ({
      oid: l.oid, w: Number(l.w) || 0, h: Number(l.h) || 0, size: Number(l.size) || 0, by: l.by || AUTHOR
    }))
  }
}

/**
 * The level nearest a destination's ceiling. `{ level, reason }` — level is null when none fits.
 *
 * ⛔ A CLEAR ABSENCE, NEVER A 404 (§6.6.4 #1). "This asset has no level under your ceiling" and "the
 * fetch failed" are different facts and a consumer must be able to tell them apart, so this returns
 * a reason rather than nothing.
 *
 * ⚠ It reports what it picked even on success, because §6.6.1 makes that the honesty requirement:
 * a bridge that quietly serves a smaller picture has altered what the author published.
 */
export function selectLevel (pointer, { maxBytes = Infinity, maxEdge = Infinity } = {}) {
  const levels = (pointer && Array.isArray(pointer.levels)) ? pointer.levels : []
  if (!levels.length) return { level: null, reason: 'this asset has no levels' }
  for (const l of levels) {
    const edge = Math.max(l.w || 0, l.h || 0)
    if ((l.size || 0) <= maxBytes && (!edge || edge <= maxEdge)) {
      const isBase = l === levels[0]
      return {
        level: l,
        reason: isBase ? 'full size' : 'level ' + edge + 'px (' + l.size + ' bytes) chosen for a ceiling of ' + maxBytes
      }
    }
  }
  const smallest = levels[levels.length - 1]
  return {
    level: null,
    reason: 'no level fits — the smallest is ' + (smallest.size || 0) + ' bytes against a ceiling of ' + maxBytes
  }
}

export default { IMAGE_LEVELS, VIDEO_LEVELS, planLevels, scaleTo, makePointer, selectLevel, AUTHOR, derivedBy }