Files
tsvm/assets/disk0/tvdos/include/mediadec.mjs
2026-06-08 02:26:23 +09:00

87 lines
4.5 KiB
JavaScript

/*
* mediadec.mjs — the all-in-one media-decoding library for TVDOS movie players.
*
* One simple public API, three internal backends (iPF/MOV, TEV, TAV/TAP),
* sharing the front-end utilities in mediadec_common.mjs. Used by playmov.js.
*
* const mediadec = require("mediadec")
* const dec = mediadec.open("A:\\film.tav", { interactive: true })
* while (true) {
* const ev = dec.step() // [backend] decode the next due frame to RAM
* if (ev.type === 'eof') break
* if (ev.type !== 'frame') { sys.sleep(1); continue }
* dec.blit() // [draw] upload the RAM frame to the screen
* // ...or in ASCII mode (no upload): dec.sampleGray(buf,w,h); aa.render/flush
* // ...or grab the frame yourself: sys.peek(dec.frameBuffer + ...)
* }
* dec.close()
*
* step() decodes the next due frame into a generic RAM RGB888 buffer (exposed as
* .frameBuffer); the caller decides what to do with it — upload it with .blit(),
* sample it for ASCII, or read it directly. (iPF is the exception: it decodes
* straight to the 4bpp display planes, so .frameBuffer is 0 and .sampleGray/.blit
* operate on the planes — see mediadec_ipf.mjs.)
*
* The decoder object every backend returns exposes a uniform interface:
* .info {format,width,height,fps,totalFrames,hasAudio,hasSubtitles,
* isInterlaced,colourSpace,graphicsMode,isStill}
* .step() -> { type:'frame'|'idle'|'eof'|'newfile'|'error', frameCount }
* .frameBuffer RAM RGB888 address of the current frame (0 for iPF; see above)
* .frameWidth/.frameHeight dimensions of the frame in .frameBuffer
* .blit() upload the current RAM frame to the screen (adapter)
* .sampleGray(dst,w,h) fill an ASCII brightness buffer from the RAM frame
* .sampleColour(dst,w,h) fill a per-cell RGB buffer (w*h*3) from the RAM frame
* .subtitle {visible,text,position,useUnicode,dirty} (resolved by the lib)
* .pause(b)/.isPaused() .setVolume(v)/.getVolume()
* .seekSeconds(n) .cue(d) .cues
* .frameCount .currentTimecodeNs .videoRate .frameMode [.qY/.qCo/.qCg]
* .close()
*/
// NOTE: every require() below is deliberately made at call time (inside open()),
// never at module top level. TVDOS's require() loads a module by eval()-ing it,
// and requiring one module *while another module is still being eval()-ed* nests
// the evals — which can collide on the loader's `let exports` binding and throw
// "Identifier 'exports' has already been declared" at load, breaking every file.
// Keeping requires at runtime means each is a single, non-nested eval.
// Open a movie file: sniff the magic, then hand off to the matching backend.
// `opts` (all optional): interactive, debugMotionVectors, enableDeblocking,
// enableBoundaryAwareDecoding, deinterlaceAlgorithm, filmGrainLevel.
function open(fullPathStr, opts) {
opts = opts || {}
const common = require("mediadec_common")
// IMPORTANT: query the file size via files.open() BEFORE preparing seqread.
// On the real disk driver both share the drive's serial port, so a files.open()
// *after* seqread.prepare() clobbers the read position and the first readBytes()
// returns driver leftovers (the size as an ASCII string) instead of the file's
// bytes — which made every file fail the magic check. Every original player
// reads the size first, then prepares seqread.
const fileLength = files.open(fullPathStr).size
const sr = common.openSeqread(fullPathStr)
const magic = common.readMagic(sr)
const fmt = common.detectFormat(magic)
con.clear()
con.curs_set(0)
switch (fmt) {
case 'mov': return require("mediadec_ipf").create(magic, sr, fileLength, opts, common)
case 'tev': return require("mediadec_tev").create(magic, sr, fileLength, opts, common)
case 'tav': return require("mediadec_tav").create(magic, sr, fileLength, opts, common, false)
case 'tap': return require("mediadec_tav").create(magic, sr, fileLength, opts, common, true)
case 'ucf':
throw Error("UCF cue files are not directly playable; play the TAV stream they index")
default:
throw Error("Unrecognised movie file (magic: " + magic.map(b => b.toString(16)).join(' ') + ")")
}
}
exports = {
open: open,
// Lazy require so this module never requires another at load time (see note above).
detectFormat: function (magic) { return require("mediadec_common").detectFormat(magic) }
}