02 — Decode frames and borrow pixels

Tutorial 01 only read a clip’s shape. Now we decode it. video_decode advances one frame at a time and returns false at end of stream; the frame’s pixels are then borrowed inside a get_data block — a view that’s valid only inside the block and only until the next decode, and is never copied. That borrow is the heart of dasVideo’s “player owns, consumer borrows” model.

The library

decode_stats runs the decode loop, borrows each frame’s packed RGBA8 pixels, and folds them into a checksum. The borrowed rgb is width*height*4 bytes; you do your reading (or your texture upload) inside the block and let go when it returns. replay_count shows video_rewind: decode to the end, rewind, decode again, and you get the same frames back — that’s how looping works.

require video
require daslib/defer

struct DecodeStats {
    frames   : int
    checksum : uint
    opaque   : bool    // every RGBA frame had an opaque first pixel (alpha 255)
}

// Decode every frame of `path`, borrowing each frame's RGBA8 pixels and folding
// them into a cheap checksum. The borrowed `rgb` is width*height*4 bytes, valid
// only inside the block — do your reading (or upload to a texture) there.
def public decode_stats(path : string) : DecodeStats {
    var player = video_open(path)
    if (player == null) return DecodeStats()
    defer() { video_close(player) }
    var st = DecodeStats(opaque = true)
    while (video_decode(player)) {
        st.frames++
        player |> get_data() $(rgb : array<uint8>#) {
            if (rgb[3] != 255u8) {
                st.opaque = false
            }
            for (b in rgb) {
                st.checksum += uint(b)
            }
        }
    }
    return st
}

// `video_rewind` seeks back to the start so you can loop. Decode to the end,
// rewind, decode again — the second pass yields the same frame count.
def public replay_count(path : string) : int {
    var player = video_open(path)
    if (player == null) return 0
    defer() { video_close(player) }
    while (video_decode(player)) {}
    video_rewind(player)
    var n = 0
    while (video_decode(player)) {
        n++
    }
    return n
}

Two things worth noting. The RGBA route (get_data() $(rgb : array<uint8>#)) hands you pixels pl_mpeg has already converted to RGBA8 on the CPU — easy to upload to one texture. There’s also a zero-copy YUV route (get_data() $(y, u, v : array<uint8>#)) that hands you the decoder’s native planes for GPU conversion; that’s a later tutorial. And the borrow is read-only and scoped: never stash the rgb reference to use after the block — by then the next decode has reused the buffer.

Self-verifying

The test decodes the bundled sample.mpg (an ffmpeg testsrc 64×48 clip, 25 frames, no audio) and checks the frame count, that the pixels are non-zero and opaque, and that video_rewind replays the whole stream.

require decode_rgba_tut

[export]
def main() {
    let st = decode_stats("tutorials/02_decode_rgba/sample.mpg")
    verify(st.frames == 25, "the sample decodes to 25 frames")
    verify(st.checksum > 0u, "decoded pixels are non-zero")
    verify(st.opaque, "RGBA frames are opaque (alpha 255)")
    verify(replay_count("tutorials/02_decode_rgba/sample.mpg") == st.frames,
        "video_rewind replays the full stream")
    print("tutorial 02 ok: decoded {st.frames} frames, checksum {st.checksum}\n")
}

Running it

Run it from the repo root — the test opens its sample with a repo-root-relative path:

cd <dasVideo>
daslang -load_module . tutorials/02_decode_rgba/test_decode_rgba.das

prints:

tutorial 02 ok: decoded 25 frames, checksum 0x2e0c99e

Next

03 — RGBA on the screen puts those borrowed pixels on screen: upload the RGBA frame to an OpenGL texture and draw it on a fullscreen quad in a window.