/* * mediadec_ipf.mjs — legacy MOV / iPF backend for the mediadec library. * * Ported from assets/disk0/tvdos/bin/playmv1.js. Decodes iPF1 / iPF1a / * iPF2 / iPF2a / iPF1-delta video packets straight to the 4bpp display planes * (the proven, fast path), plus MP2 and raw-PCM audio and the background-colour * packet. Presents at decode time (so blit() is a no-op); bias lighting is a * separate player-driven stage via the bias() method; the ASCII path reads the * planes back via common.sampleGrayScreen. */ const WIDTH = 560 const HEIGHT = 448 const FBUF_SIZE = WIDTH * HEIGHT function create(magic, sr, fileLength, opts, common) { const audioR = common.makeAudioRouter(sr) // Header (after the 8-byte magic): w, h, fps, frameCount, queue info. let width = sr.readShort() let height = sr.readShort() let fps = sr.readShort(); if (fps == 0) fps = 9999 const FRAME_COUNT = sr.readInt() % 16777216 sr.readShort() // skip unused sr.readShort() // audioQueueInfo (unused for playback) sr.skip(10) graphics.setGraphicsMode(4) graphics.clearPixels(255) graphics.clearPixels2(240) const FRAME_TIME = 1.0 / fps const applyBias = common.makeBias(width, height, 4) const ipfbuf = sys.malloc(FBUF_SIZE) const info = { format: 'ipf', width: width, height: height, fps: fps, totalFrames: FRAME_COUNT, hasAudio: true, hasSubtitles: false, isInterlaced: false, colourSpace: 'YCoCg', graphicsMode: 4, isStill: false } // No subtitles in iPF; expose an inert state object for the uniform API. const subtitle = { visible: false, text: "", position: 0, useUnicode: false, dirty: false } let akku = FRAME_TIME let lastT = sys.nanoTime() let doFrameskip = true let autoBg = true let framesRead = 0 let frameCount = 0 let paused = false function setBackgroundPacket() { autoBg = false let rgbx = sr.readInt() graphics.setBackground((rgbx & 0xFF000000) >>> 24, (rgbx & 0x00FF0000) >>> 16, (rgbx & 0x0000FF00) >>> 8) } function step() { const now = sys.nanoTime() if (paused) { lastT = now; return { type: 'idle' } } akku += (now - lastT) / 1000000000.0 lastT = now if (sr.getReadCount() >= fileLength) return { type: 'eof' } if (akku < FRAME_TIME) return { type: 'idle' } // Drain accumulated time into a frame budget (frameskip drops late frames). let frameUnit = 0 while (akku >= FRAME_TIME) { akku -= FRAME_TIME; frameUnit += 1 } if (!doFrameskip) frameUnit = 1 let displayed = false while (frameUnit >= 1 && sr.getReadCount() < fileLength) { let packetType = sr.readShort() if (0xFFFF === packetType) { // sync — one frame boundary frameUnit -= 1 } else if (0xFEFF === packetType) { // explicit background colour setBackgroundPacket() } else if (packetType < 2047) { // video if (packetType == 4 || packetType == 5 || packetType == 260 || packetType == 261) { let decodefun = (packetType > 255) ? graphics.decodeIpf2 : graphics.decodeIpf1 let payloadLen = sr.readInt() if (framesRead >= FRAME_COUNT) return { type: 'eof' } framesRead += 1 let gz = sr.readBytes(payloadLen) if (frameUnit == 1) { gzip.decompFromTo(gz, payloadLen, ipfbuf) decodefun(ipfbuf, common.DISP_RG, common.DISP_BA, width, height, (packetType & 255) == 5) audioR.fire() displayed = true frameCount += 1 } sys.free(gz) } else if (packetType == 516) { // iPF1-delta doFrameskip = false let payloadLen = sr.readInt() if (framesRead >= FRAME_COUNT) return { type: 'eof' } framesRead += 1 let gz = sr.readBytes(payloadLen) if (frameUnit == 1) { gzip.decompFromTo(gz, payloadLen, ipfbuf) graphics.applyIpf1d(ipfbuf, common.DISP_RG, common.DISP_BA, width, height) audioR.fire() displayed = true frameCount += 1 } sys.free(gz) } else { throw Error(`Unknown iPF video packet type ${packetType} at ${sr.getReadCount() - 2}`) } } else if (4096 <= packetType && packetType <= 6143) { // audio let readLength = (packetType >>> 8 == 17) ? common.MP2_FRAME_SIZE[(packetType & 255) >>> 1] : sr.readInt() if (readLength == 0) throw Error("iPF audio read length is zero") if (packetType >>> 8 == 17) { // MP2 audioR.ensureMp2() sr.readBytes(readLength, audioR.sndBase - 2368) audio.mp2Decode() audio.mp2UploadDecoded(0) } else if (packetType == 0x1000 || packetType == 0x1001) { // raw PCM audioR.rawPcm(readLength) } else { throw Error(`iPF audio packet type ${packetType} at ${sr.getReadCount() - 2}`) } } else { // Unknown — stop to avoid desync (matches old players' break). return { type: 'eof' } } } return displayed ? { type: 'frame', frameCount: frameCount } : { type: 'idle' } } // The frame is already on the display planes (decoded there in step()), so // presenting is a no-op. Bias lighting is a separate, player-driven stage // (bias() below) and is skipped when an explicit background packet disabled it. function blit() { } // iPF decodes straight to the 4bpp display planes (no fast JS planar->RGB // path), so — unlike TEV / TAV — there is no RAM RGB888 frame: the planes ARE // the frame. sampleGray/sampleColour therefore read the planes back; this still // costs no extra upload in ASCII mode, since decoding already wrote the planes. function sampleGray(dst, w, h) { common.sampleGrayScreen(width, height, dst, w, h, 4) } function sampleColour(dst, w, h) { common.sampleColourScreen(width, height, dst, w, h, 4) } return { info: info, subtitle: subtitle, get frameCount() { return frameCount }, get currentTimecodeNs() { return Math.floor(frameCount * (1000000000.0 / fps)) }, get videoRate() { return 0 }, get frameMode() { return ' ' }, cues: [], // No generic RAM frame for iPF: it decodes straight to the display planes, // so frameBuffer is 0. Use sampleGray/sampleColour to read the frame instead. get frameBuffer() { return 0 }, get frameWidth() { return width }, get frameHeight() { return height }, step: step, blit: blit, bias() { if (autoBg) applyBias() }, // skipped when an explicit bg packet set the colour sampleGray: sampleGray, sampleColour: sampleColour, pause(p) { paused = p; if (p) audioR.stop(); else { audioR.resume(); lastT = sys.nanoTime() } }, isPaused() { return paused }, setVolume(v) { audioR.setVolume(v) }, getVolume() { return audioR.getVolume() }, seekSeconds(_n) { /* iPF has no index; seeking unsupported */ }, cue(_d) { /* no cues */ }, close() { sys.free(ipfbuf) audioR.close() } } } exports = { create }