summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke T. Shumaker <lukeshu@lukeshu.com>2025-02-20 22:10:05 -0700
committerLuke T. Shumaker <lukeshu@lukeshu.com>2025-06-19 23:28:09 -0600
commit119ebba54b0d4a8656b3966639d9263be370b5a7 (patch)
tree8db77cbb70f608abebe34284e1bb2a8367047c93
parent756eb1635ba61b2888efa1ae2a25acd40cb34d11 (diff)
vid-scratch: Add consolidated versions of my experiments from 2025-02-XXlukeshu/vid-scratch
-rw-r--r--vid-scratch/.gitignore14
-rw-r--r--vid-scratch/Archive.zipbin0 -> 2211849 bytes
-rw-r--r--vid-scratch/Makefile83
-rw-r--r--vid-scratch/common.h259
-rw-r--r--vid-scratch/nut-crc32.c47
-rw-r--r--vid-scratch/nutgen-concat.c213
-rw-r--r--vid-scratch/nutgen-elision.c278
-rw-r--r--vid-scratch/nutgen-gif.c289
-rw-r--r--vid-scratch/nutgen-sidedata.c252
-rw-r--r--vid-scratch/nutgen-streams.c238
-rw-r--r--vid-scratch/requirements.md21
-rwxr-xr-xvid-scratch/reschange-gif-in-nut-gen.sh19
-rwxr-xr-xvid-scratch/reschange-mkvgen.sh19
13 files changed, 1732 insertions, 0 deletions
diff --git a/vid-scratch/.gitignore b/vid-scratch/.gitignore
new file mode 100644
index 0000000..d9fb09b
--- /dev/null
+++ b/vid-scratch/.gitignore
@@ -0,0 +1,14 @@
+# Specific compiled executables
+/nut-crc32
+/nutgen-elision
+/nutgen-sidedata
+/nutgen-concat
+/nutgen-streams
+/nutgen-gif
+
+# File extensions
+*.concat.txt
+*.gif
+*.mkv
+*.nut
+*.webm
diff --git a/vid-scratch/Archive.zip b/vid-scratch/Archive.zip
new file mode 100644
index 0000000..b4415b6
--- /dev/null
+++ b/vid-scratch/Archive.zip
Binary files differ
diff --git a/vid-scratch/Makefile b/vid-scratch/Makefile
new file mode 100644
index 0000000..e3b3453
--- /dev/null
+++ b/vid-scratch/Makefile
@@ -0,0 +1,83 @@
+CFLAGS = -Wall -Werror -Wextra -std=gnu23
+
+all:
+.PHONY: all
+
+.NOTINTERMEDIATE:
+.DELETE_ON_ERROR:
+
+################################################################################
+# NUT's flavor of CRC32
+
+all: nut-crc32
+
+################################################################################
+# Generate RGB8 NUT that changes resolution using elision headers
+# Status: does not play at all
+
+all: nutgen-elision.nut
+nutgen-elision: common.h
+nutgen-elision.nut: %.nut: %
+ ./$< >$@
+
+################################################################################
+# Generate RGB8 NUT that changes resolution using side-data
+# Status: plays, but resolution does not change
+
+all: nutgen-sidedata.nut
+nutgen-sidedata: common.h
+nutgen-sidedata.nut: %.nut: %
+ ./$< >$@
+
+################################################################################
+# Generate RGB8 NUT that changes resolution by starting a new NUT file
+# Status: plays, but resolution does not change
+
+all: nutgen-concat.nut
+nutgen-concat: common.h
+nutgen-concat.nut: %.nut: %
+ ./$< >$@
+
+################################################################################
+# Generate RGB8 NUT that changes resolution by having a separate stream for each resolution
+# Status: only 1 stream plays
+
+all: nutgen-streams.nut
+nutgen-streams: common.h
+nutgen-streams.nut: %.nut: %
+ ./$< >$@
+
+################################################################################
+# Generate NUT with embedded GIF to change resolutions
+# Status: WIP (but below bash/ffmpeg experiment shows it should work)
+
+all: nutgen-gif.nut
+nutgen-gif: common.h
+nutgen-gif.nut: %.nut: %
+ ./$< >$@
+
+################################################################################
+# Look at how .webm files can change resolution
+
+all: reschange-cam.nut
+reschange-cam.nut: %.nut: %.webm
+ ffmpeg -y -i $< -c:v copy -an $@
+reschange-cam.webm: Archive.zip
+ bsdtar xfO $< changing-resolution.webm >$@
+# Archive.zip is from https://github.com/OpenShot/libopenshot/issues/382
+
+################################################################################
+# Replicate the .webm thing in NUT/VP8/yuv420p with bash/ffmpeg
+
+all: reschange-mkvgen.nut
+reschange-mkvgen.nut: %.nut: %.mkv
+ ffmpeg -y -i $< -c:v copy $@
+reschange-mkvgen.mkv: %.mkv: %.sh
+ ./$<
+
+################################################################################
+# Replicate it in gif-in-nut with bash/ffmpeg
+
+all: reschange-gif-in-nut-gen.nut
+reschange-gif-in-nut-gen.nut: %.nut: %.sh
+ ./$<
diff --git a/vid-scratch/common.h b/vid-scratch/common.h
new file mode 100644
index 0000000..91f94fd
--- /dev/null
+++ b/vid-scratch/common.h
@@ -0,0 +1,259 @@
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h> /* for perror() */
+#include <string.h> /* for memcpy() */
+#include <stdlib.h> /* for realloc(), free() */
+#include <unistd.h> /* for write() */
+
+#define LM_ARRAY_LEN(ary) (sizeof(ary)/sizeof((ary)[0]))
+#define LM_CEILDIV(n, d) ( ((n)+(d)-1) / (d) )
+#define LM_NEXT_POWER_OF_2(x) ( (x) ? 1ULL<<((sizeof(unsigned long long)*8)-__builtin_clzll(x)) : 1)
+#define LM_CAT2(a, b) a ## b
+#define LM_CAT2_(a, b) LM_CAT2(a, b)
+#define assert_notreached(msg) assert(false)
+
+struct buf {
+ uint8_t *dat;
+ size_t len;
+ size_t cap;
+};
+
+void append(struct buf *buf, void *dat, size_t len) {
+ assert(buf);
+ assert(len == 0 || dat);
+
+ if (buf->len + len > buf->cap) {
+ buf->cap = LM_NEXT_POWER_OF_2(buf->len + len);
+ buf->dat = realloc(buf->dat, buf->cap);
+ assert(buf->dat);
+ }
+ memcpy(&buf->dat[buf->len], dat, len);
+ buf->len += len;
+}
+
+void append_u32be(struct buf *buf, uint32_t val) {
+ assert(buf);
+
+ uint8_t dat[4] = {
+ (val>>(8*3))&0xFF,
+ (val>>(8*2))&0xFF,
+ (val>>(8*1))&0xFF,
+ (val>>(8*0))&0xFF,
+ };
+ append(buf, dat, sizeof(dat));
+}
+
+bool xwrite(int fd, uint8_t *dat, size_t len) {
+ assert(len == 0 || dat);
+
+ for (size_t done = 0; done < len;) {
+ ssize_t n = write(fd, &dat[done], len-done);
+ if (n < 0) {
+ perror("write");
+ return true;
+ }
+ done += n;
+ }
+ return false;
+}
+
+/* NUT ************************************************************************/
+
+/* Magic values. *****************************************/
+uint8_t nut_file_id_string[] = "nut/multimedia container"; /* including NUL terminator */
+uint8_t nut_startcode_main[] = {'N', 'M', 0x7A, 0x56, 0x1F, 0x5F, 0x04, 0xAD };
+uint8_t nut_startcode_stream[] = {'N', 'S', 0x11, 0x40, 0x5B, 0xF2, 0xF9, 0xDB };
+uint8_t nut_startcode_syncpoint[] = {'N', 'K', 0xE4, 0xAD, 0xEE, 0xCA, 0x45, 0x69 };
+uint8_t nut_startcode_index[] = {'N', 'X', 0xDD, 0x67, 0x2F, 0x23, 0xE6, 0x4E };
+uint8_t nut_startcode_info[] = {'N', 'I', 0xAB, 0x68, 0xB5, 0x96, 0xBA, 0x78 };
+
+#define NUT_FRAMEFLAG_KEY (1<< 0)
+#define NUT_FRAMEFLAG_EOR (1<< 1)
+#define _NUT_FRAMEFLAG_2 (1<< 2)
+#define NUT_FRAMEFLAG_CODED_PTS (1<< 3)
+#define NUT_FRAMEFLAG_STREAM_ID (1<< 4)
+#define NUT_FRAMEFLAG_SIZE_MSB (1<< 5)
+#define NUT_FRAMEFLAG_CHECKSUM (1<< 6)
+#define NUT_FRAMEFLAG_RESERVED (1<< 7)
+#define NUT_FRAMEFLAG_SM_DATA (1<< 8)
+#define _NUT_FRAMEFLAG_9 (1<< 9)
+#define NUT_FRAMEFLAG_HEADER_IDX (1<<10)
+#define NUT_FRAMEFLAG_MATCH_TIME (1<<11)
+#define NUT_FRAMEFLAG_CODED (1<<12)
+#define NUT_FRAMEFLAG_INVALID (1<<13)
+
+#define NUT_MAINFLAG_BROADCAST_MODE (1<< 0)
+#define NUT_MAINFLAG_PIPE_MODE (1<< 1)
+
+#define NUT_UNKNOWN_MATCH_TIME (1-(UINT64_C(1)<<62))
+
+enum nut_stream_class {
+ NUT_STREAMCLASS_VIDEO = 0,
+ NUT_STREAMCLASS_AUDIO = 1,
+ NUT_STREAMCLASS_SUBTITLES = 2,
+ NUT_STREAMCLASS_USERDATA = 3,
+};
+
+#define NUT_STREAMFLAG_FIXED_FPS (1<< 1)
+
+enum nut_colorspace {
+ NUT_COLORSPACE_UNKNOWN = 0,
+ NUT_COLORSPACE_ITU642_TRUNC = 1,
+ NUT_COLORSPACE_ITU709_TRUNC = 2,
+ NUT_COLORSPACE_ITU642_FULL = 17,
+ NUT_COLORSPACE_ITU709_FILL = 18,
+};
+
+/* Basic fixed-length types. *****************************/
+
+void nut_append_u8(struct buf *buf, uint8_t val) {
+ assert(buf);
+
+ append(buf, &val, 1);
+}
+
+void nut_append_u32(struct buf *buf, uint32_t val) {
+ assert(buf);
+
+ append_u32be(buf, val);
+}
+
+/* Basic variable-length types. **************************/
+
+void nut_append_vu(struct buf *buf, uint64_t val) {
+ assert(buf);
+
+ uint8_t dat[10] = {
+ 0x80|((val>>(7*9))&0x7F),
+ 0x80|((val>>(7*8))&0x7F),
+ 0x80|((val>>(7*7))&0x7F),
+ 0x80|((val>>(7*6))&0x7F),
+ 0x80|((val>>(7*5))&0x7F),
+ 0x80|((val>>(7*4))&0x7F),
+ 0x80|((val>>(7*3))&0x7F),
+ 0x80|((val>>(7*2))&0x7F),
+ 0x80|((val>>(7*1))&0x7F),
+ 0x00|((val>>(7*0))&0x7F),
+ };
+ uint8_t skip = 0;
+ while (dat[skip] == 0x80)
+ skip++;
+ append(buf, &dat[skip], sizeof(dat)-skip);
+}
+
+void nut_append_vs(struct buf *buf, int64_t val) {
+ assert(buf);
+ assert((val < 0 ? -val : val) <= UINT64_MAX>>1);
+
+ uint64_t temp;
+ if (val > 0)
+ temp = val << 1;
+ else
+ temp = ((-val) << 1) | 1;
+ temp--;
+ nut_append_vu(buf, temp);
+}
+
+void nut_append_vb(struct buf *buf, void *dat, size_t len) {
+ assert(buf);
+ assert(len == 0 || dat);
+
+ nut_append_vu(buf, len);
+ append(buf, dat, len);
+}
+
+#define nut_append_vb_str(buf, str) nut_append_vb(buf, str, sizeof(str)-1)
+
+/* Mid-level. ********************************************/
+
+uint32_t nut_crc32(uint8_t *dat, int len){
+ assert(len == 0 || dat);
+
+ /* "Generator polynomial is 0x104C11DB7." */
+ static const uint32_t table[16] = {
+ 0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9,
+ 0x130476DC, 0x17C56B6B, 0x1A864DB2, 0x1E475005,
+ 0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61,
+ 0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD,
+ };
+
+ /* "Starting value is zero." */
+ uint32_t crc = 0;
+
+ while (len--) {
+ crc ^= *dat++ << 24;
+ crc = (crc<<4) ^ table[crc>>28];
+ crc = (crc<<4) ^ table[crc>>28];
+ }
+ return crc;
+}
+
+void nut_append_packet(struct buf *buf, uint8_t *startcode, void *data, size_t len) {
+ assert(buf);
+ assert(startcode);
+ assert(len == 0 || data);
+
+ size_t packet_beg = buf->len;
+ append(buf, startcode, 8);
+ nut_append_vu(buf, len+4);
+ if (len+4 > 4096)
+ nut_append_u32(buf, nut_crc32(&buf->dat[packet_beg], buf->len - packet_beg));
+ size_t data_beg = buf->len;
+ append(buf, data, len);
+ nut_append_u32(buf, nut_crc32(&buf->dat[data_beg], buf->len - data_beg));
+}
+
+[[gnu::nonstring]] char font[10][8*4] = {
+ " ## "
+ " # # "
+ " # # "
+ " ## ",
+
+ " # "
+ " ## "
+ " # "
+ " ### ",
+
+ " ## "
+ " # # "
+ " # "
+ " #### ",
+
+ " ## "
+ " # "
+ " # "
+ " ## ",
+
+ " ## "
+ " # # "
+ " #### "
+ " # ",
+
+ " #### "
+ " ### "
+ " # "
+ " ### ",
+
+ " # "
+ " ### "
+ " # # "
+ " ## ",
+
+ " #### "
+ " # "
+ " # "
+ " # ",
+
+ " ## "
+ " # # "
+ " #### "
+ " ## ",
+
+ " ## "
+ " # # "
+ " ### "
+ " # ",
+};
+static_assert(LM_ARRAY_LEN(font) == 10);
+static_assert(sizeof(font[0]) == 32);
diff --git a/vid-scratch/nut-crc32.c b/vid-scratch/nut-crc32.c
new file mode 100644
index 0000000..e81c5d7
--- /dev/null
+++ b/vid-scratch/nut-crc32.c
@@ -0,0 +1,47 @@
+#define _GNU_SOURCE /* for error() */
+#include <errno.h> /* for errno */
+#include <error.h> /* for error() */
+#include <stdint.h> /* for uint{n}_t */
+#include <stdio.h> /* for printf() */
+#include <unistd.h> /* for read() */
+
+void nut_crc32_init(uint32_t *crc) {
+ /* "Starting value is zero." */
+ *crc = 0;
+}
+
+void nut_crc32_write(uint32_t *crc, uint8_t *dat, size_t len) {
+ /* "Generator polynomial is 0x104C11DB7." */
+ static const uint32_t table[16] = {
+ 0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9,
+ 0x130476DC, 0x17C56B6B, 0x1A864DB2, 0x1E475005,
+ 0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61,
+ 0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD,
+ };
+
+ while (len--) {
+ *crc ^= *dat++ << 24;
+ *crc = (*crc<<4) ^ table[*crc>>28];
+ *crc = (*crc<<4) ^ table[*crc>>28];
+ }
+}
+
+int main() {
+ uint32_t crc;
+ uint8_t buf[128];
+
+ nut_crc32_init(&crc);
+
+ size_t tot = 0;
+ for (;;) {
+ ssize_t n = read(0, buf, sizeof(buf));
+ if (n <= 0)
+ break;
+ nut_crc32_write(&crc, buf, n);
+ tot += n;
+ }
+ printf("crc32 = 0x%08x ; size = %zu\n", crc, tot);
+ if (errno)
+ error(1, errno, "read");
+ return 0;
+}
diff --git a/vid-scratch/nutgen-concat.c b/vid-scratch/nutgen-concat.c
new file mode 100644
index 0000000..2b511f9
--- /dev/null
+++ b/vid-scratch/nutgen-concat.c
@@ -0,0 +1,213 @@
+/* nut.c - A simple NUT encoder with the following properties:
+ *
+ * - Each frame is an uncompressed 8-bit RGB (specifically `(msb)2B
+ * 3G 3R(lsb)`, known as the 4CC "BGR\x08") framebuffer.
+ *
+ * - Each pixel in the framebuffer is non-square; it is twice as tall
+ * as it is wide.
+ *
+ * - The display-resolution of each frame may change between 640x480,
+ * 720x480, and 720x576. This is the display-resolution; because
+ * of non-square pixels, the framebuffer for each would be 640x240,
+ * 720x240, and 720x288, respectively.
+ *
+ * - VFR (Variable Frame Rate) - Each frame has its own timestamp.
+ */
+
+#include "common.h"
+
+/* High-level (specific to the properties we want) ****************************/
+
+enum app_res {
+ APP_RES_INVALID = -1,
+ APP_RES_640_480 = 0,
+ APP_RES_720_480 = 1,
+ APP_RES_720_576 = 2,
+ APP_RES_NUM = 3,
+};
+#define APP_RES_MIN APP_RES_640_480
+#define APP_RES_MAX APP_RES_720_576
+
+size_t app_res_w(enum app_res res) {
+ switch (res) {
+ case APP_RES_640_480: return 640;
+ case APP_RES_720_480: return 720;
+ case APP_RES_720_576: return 720;
+ default: assert_notreached("invalid res");
+ }
+}
+size_t app_res_h(enum app_res res) {
+ switch (res) {
+ case APP_RES_640_480: return 480/2;
+ case APP_RES_720_480: return 480/2;
+ case APP_RES_720_576: return 576/2;
+ default: assert_notreached("invalid res");
+ }
+}
+size_t app_res_fb_size(enum app_res res) {
+ return app_res_w(res)*app_res_h(res);
+}
+
+#define APP_FRAME_SIZE_MSB_MUL 0x2000 /* must be less than 0x4000 */
+#define APP_FRAME_MAX_HEADER 16
+
+struct app_state {
+ enum app_res res;
+};
+
+void app_append_intro(struct buf *out, uint64_t time_ns, enum app_res res) {
+ struct buf pkt = {0};
+ unsigned int frame_code = 0;
+#define INC_FRAME_CODE() do { frame_code++; if (frame_code == 'N') frame_code++; } while(0)
+
+ append(out, nut_file_id_string, sizeof(nut_file_id_string));
+
+#define BOGUS(n) n
+ /* main_header ********************************************************/
+ pkt.len = 0;
+
+ /* head *******************************************/
+ nut_append_vu(&pkt, 4); /*! version */
+ nut_append_vu(&pkt, 0); /*! minor_version */
+ nut_append_vu(&pkt, 1); /*! stream_count */
+ nut_append_vu(&pkt, /*! max_distance */
+ APP_FRAME_MAX_HEADER +
+ app_res_fb_size(res));
+
+ /* time bases *************************************/
+ nut_append_vu(&pkt, 1); /*! time_base_count */
+
+ /* time_base[0] = 1ns */
+ nut_append_vu(&pkt, 1); /*! numerator */
+ nut_append_vu(&pkt, 1000000000ULL); /*! denominator */
+
+ /* frame codes ************************************/
+ /* "A muxer SHOULD mark [frame codes] 0x00 and 0xFF as invalid" */
+
+ /* frame_code=0 (invalid) */
+ nut_append_vu(&pkt, NUT_FRAMEFLAG_INVALID); /*! flags */
+ nut_append_vu(&pkt, 2); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, 1); /*! 1: fields.size_msb_nul */
+ INC_FRAME_CODE();
+
+ /* frame_code=1 (res) */
+ nut_append_vu(&pkt, /*! flags */
+ NUT_FRAMEFLAG_KEY | /* Because they're full framebuffers, all frames are keyframes. */
+ NUT_FRAMEFLAG_SIZE_MSB | /* 640*480/2 > 16384 (the max val of data_size_lsb). */
+ NUT_FRAMEFLAG_CODED_PTS | /* framerate is unknown, each frame must have a timestamp. */
+ NUT_FRAMEFLAG_CHECKSUM ); /* framerate is unknown, guard against exceeding max_pts_distance. */
+ nut_append_vu(&pkt, 6); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, APP_FRAME_SIZE_MSB_MUL); /*! 1: fields.size_msb_nul */
+ nut_append_vu(&pkt, 0); /*! 2: fields.stream */
+ nut_append_vu(&pkt, /*! 3: fields.size_lsb */
+ app_res_fb_size(res) % APP_FRAME_SIZE_MSB_MUL);
+ nut_append_vu(&pkt, 0); /*! 4: fields.reserved */
+ nut_append_vu(&pkt, 1); /*! 5: fields.count */
+ INC_FRAME_CODE();
+
+ /* frame_code=N-255 (invalid) */
+ nut_append_vu(&pkt, NUT_FRAMEFLAG_INVALID); /*! flags */
+ nut_append_vu(&pkt, 2); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, /*! 1: fields.size_msb_nul */
+ 256-frame_code-(frame_code < 'N' ? 1 : 0));
+
+ /* tail *******************************************/
+ nut_append_vu(&pkt, 0); /*! header_count_minus_1 */
+ nut_append_vu(&pkt, NUT_MAINFLAG_PIPE_MODE); /*! main_flags */
+
+ nut_append_packet(out, nut_startcode_main, pkt.dat, pkt.len);
+
+ /* stream_header ******************************************************/
+ pkt.len = 0;
+
+ nut_append_vu(&pkt, 0); /*! stream_id */
+ nut_append_vu(&pkt, NUT_STREAMCLASS_VIDEO); /*! stream_class */
+ nut_append_vb(&pkt, "BGR\x08", 4); /*! fourcc */
+ nut_append_vu(&pkt, 0); /*! time_base_id */
+ nut_append_vu(&pkt, 0); /*! msb_pts_shift */
+ nut_append_vu(&pkt, BOGUS(0)); /*! max_pts_distance (all frames have a checksum) */
+ nut_append_vu(&pkt, 0); /*! decode_delay */
+ nut_append_vu(&pkt, 0); /*! stream_flags */
+ nut_append_vb(&pkt, NULL, 0); /*! codec_specific_data */
+ nut_append_vu(&pkt, app_res_w(res)); /*! width */
+ nut_append_vu(&pkt, app_res_h(res)); /*! height */
+ nut_append_vu(&pkt, 1); /*! sample_width */
+ nut_append_vu(&pkt, 2); /*! sample_height */
+ nut_append_vu(&pkt, NUT_COLORSPACE_UNKNOWN); /*! colorspace_type */
+
+ nut_append_packet(out, nut_startcode_stream, pkt.dat, pkt.len);
+
+ /* syncpoint **********************************************************/
+ pkt.len = 0;
+
+ nut_append_vu(&pkt, time_ns); /*! global_key_pts */
+ nut_append_vu(&pkt, 0); /*! back_ptr_div16 */
+
+ nut_append_packet(out, nut_startcode_syncpoint, pkt.dat, pkt.len);
+
+ /* flush **************************************************************/
+#undef BOGUS
+
+ free(pkt.dat);
+}
+
+bool app_write_frame(int fd, struct app_state *state, uint64_t time_ns, enum app_res res, void *framebuffer) {
+ assert(framebuffer);
+ assert(0 <= res && res < APP_RES_NUM);
+
+ struct buf out = {0};
+ struct buf pkt = {0};
+
+ if (res != state->res) {
+ app_append_intro(&out, time_ns, res);
+ state->res = res;
+ }
+
+ /* frame header ( 1+10+1+4 = 16 bytes) */
+ uint64_t frame_beg = out.len;
+ nut_append_u8(&out, 1); /*! frame_code (1 byte) */
+ nut_append_vu(&out, time_ns); /*! coded_pts (<=10 bytes) */
+ nut_append_vu(&out, app_res_fb_size(res) / APP_FRAME_SIZE_MSB_MUL); /*! data_size_msb (1 byte) */
+ nut_append_u32(&out, nut_crc32(&out.dat[frame_beg], out.len - frame_beg)); /*! checksum (4 bytes) */
+ assert(out.len - frame_beg <= APP_FRAME_MAX_HEADER);
+
+ /* flush */
+ bool err = xwrite(fd, out.dat, out.len);
+ free(out.dat);
+ free(pkt.dat);
+ if (err)
+ return true;
+
+ /* frame data */
+ if (xwrite(fd, framebuffer, app_res_fb_size(res)))
+ return true;
+
+ /* return */
+ state->res = res;
+ return false;
+}
+
+/* Demo application ***********************************************************/
+
+int main() {
+ struct app_state state = {
+ .res = APP_RES_INVALID,
+ };
+
+ uint8_t framebuffer[app_res_fb_size(APP_RES_MAX)];
+
+#define SCALE 10
+ for (int i = 0; i < 10; i++) {
+ enum app_res res = (i/2)%APP_RES_NUM;
+ memset(framebuffer, 0b00011000, sizeof(framebuffer));
+ for (int y = 0; y < 4*SCALE; y++)
+ for (int x = 0; x < 8*SCALE; x++)
+ framebuffer[(y*app_res_w(res))+x] = font[i][((y/SCALE)*8)+(x/SCALE)] == ' ' ? 0b11000000 : 0b00000011;
+ if (app_write_frame(1, &state, ((uint64_t)i)*1000000000ULL, res, framebuffer))
+ return 1;
+ }
+ return 0;
+}
diff --git a/vid-scratch/nutgen-elision.c b/vid-scratch/nutgen-elision.c
new file mode 100644
index 0000000..6dd734f
--- /dev/null
+++ b/vid-scratch/nutgen-elision.c
@@ -0,0 +1,278 @@
+/* nut.c - A simple NUT encoder with the following properties:
+ *
+ * - Each frame is an uncompressed 8-bit RGB (specifically `(msb)2B
+ * 3G 3R(lsb)`, known as the 4CC "BGR\x08") framebuffer.
+ *
+ * - Each pixel in the framebuffer is non-square; it is twice as tall
+ * as it is wide.
+ *
+ * - The display-resolution of each frame may change between 640x480,
+ * 720x480, and 720x576. This is the display-resolution; because
+ * of non-square pixels, the framebuffer for each would be 640x240,
+ * 720x240, and 720x288, respectively.
+ *
+ * - VFR (Variable Frame Rate) - Each frame has its own timestamp.
+ */
+
+#include "common.h"
+
+/* High-level (specific to the properties we want) ****************************/
+
+enum app_res {
+ APP_RES_640_480 = 0,
+ APP_RES_720_480 = 1,
+ APP_RES_720_576 = 2,
+};
+
+struct app_state {
+ uint64_t pts;
+ uint64_t len;
+ uint64_t last_syncpoint;
+};
+
+#define APP_COMMON_FLAGS NUT_FRAMEFLAG_KEY|NUT_FRAMEFLAG_SIZE_MSB|NUT_FRAMEFLAG_CHECKSUM|NUT_FRAMEFLAG_SM_DATA
+
+#define APP_SIZE_MSB_MUL 0x2000 /* must be less than 0x4000 */
+
+#define _APP_ELIDE_640_480 19
+#define _APP_ELIDE_720_480 19
+#define _APP_ELIDE_720_576 19
+#define _APP_W_640_480 640
+#define _APP_W_720_480 720
+#define _APP_W_720_576 720
+#define _APP_H_640_480 480
+#define _APP_H_720_480 480
+#define _APP_H_720_576 576
+
+#define APP_ELIDE(res) LM_CAT2_(_APP_ELIDE_, res)
+#define APP_W(res) LM_CAT2_(_APP_W_, res)
+#define APP_H(res) LM_CAT2_(_APP_H_, res)
+#define APP_FB(res) (APP_W(res)*APP_H(res)/2)
+#define APP_SIZE(res) (APP_ELIDE(res)+APP_FB(res))
+/* HERE */
+#define APP_FRAME_MAX_OVERHEAD 31
+
+bool app_write_intro(int fd, struct app_state *state, uint64_t time_ns) {
+ assert(state);
+
+ struct buf out = {0};
+ struct buf pkt = {0};
+ struct buf hdr = {0};
+
+ append(&out, nut_file_id_string, sizeof(nut_file_id_string));
+
+ /* main_header ********************************************************/
+#define BOGUS 0
+ pkt.len = 0;
+
+ nut_append_vu(&pkt, 3); /*! version */
+ nut_append_vu(&pkt, 1); /*! stream_count */
+ nut_append_vu(&pkt, APP_FB(720_576) + /*! max_distance */
+ APP_FRAME_MAX_OVERHEAD);
+ /* time bases *************************************/
+ nut_append_vu(&pkt, 1); /*! time_base_count */
+
+ /* time_base[0] = 1ns */
+ nut_append_vu(&pkt, 1); /*! numerator */
+ nut_append_vu(&pkt, 1000000000ULL); /*! denominator */
+
+ /* frame codes ************************************/
+ /* "A muxer SHOULD mark [frame codes] 0x00 and 0xFF as invalid" */
+
+ /* frame_code=0 (invalid) */
+ nut_append_vu(&pkt, NUT_FRAMEFLAG_INVALID); /*! flags */
+ nut_append_vu(&pkt, 2); /*! field_count */
+ nut_append_vu(&pkt, BOGUS); /*! 0: fields.pts */
+ nut_append_vu(&pkt, 1); /*! 1: fields.size_msb_nul */
+
+ /* frame_code=1 (640x480) */
+ nut_append_vu(&pkt, APP_COMMON_FLAGS); /*! flags */
+ nut_append_vu(&pkt, 8); /*! field_count */
+ nut_append_vu(&pkt, BOGUS); /*! 0: fields.pts */
+ nut_append_vu(&pkt, APP_SIZE_MSB_MUL); /*! 1: fields.size_msb_nul */
+ nut_append_vu(&pkt, 0); /*! 2: fields.stream */
+ nut_append_vu(&pkt, APP_SIZE(640_480) % /*! 3: fields.size_lsb */
+ APP_SIZE_MSB_MUL);
+ nut_append_vu(&pkt, 0); /*! 4: fields.reserved */
+ nut_append_vu(&pkt, 1); /*! 5: fields.count */
+ nut_append_vs(&pkt, NUT_UNKNOWN_MATCH_TIME); /*! 6: fields.match */
+ nut_append_vu(&pkt, 1); /*! 7: fields.elision_header */
+
+ /* frame_code=2 (720x480) */
+ nut_append_vu(&pkt, APP_COMMON_FLAGS); /*! flags */
+ nut_append_vu(&pkt, 8); /*! field_count */
+ nut_append_vu(&pkt, BOGUS); /*! 0: fields.pts */
+ nut_append_vu(&pkt, APP_SIZE_MSB_MUL); /*! 1: fields.size_msb_mul */
+ nut_append_vu(&pkt, 0); /*! 2: fields.stream */
+ nut_append_vu(&pkt, APP_SIZE(720_480) % /*! 3: fields.size_lsb */
+ APP_SIZE_MSB_MUL);
+ nut_append_vu(&pkt, 0); /*! 4: fields.reserved */
+ nut_append_vu(&pkt, 1); /*! 5: fields.count */
+ nut_append_vs(&pkt, NUT_UNKNOWN_MATCH_TIME); /*! 6: fields.match */
+ nut_append_vu(&pkt, 2); /*! 7: fields.elision_header */
+
+ /* frame_code=3 (720x576) */
+ nut_append_vu(&pkt, APP_COMMON_FLAGS); /*! flags */
+ nut_append_vu(&pkt, 8); /*! field_count */
+ nut_append_vu(&pkt, BOGUS); /*! 0: fields.pts */
+ nut_append_vu(&pkt, APP_SIZE_MSB_MUL); /*! 1: fields.size_msb_nul */
+ nut_append_vu(&pkt, 0); /*! 2: fields.stream */
+ nut_append_vu(&pkt, APP_SIZE(720_576) % /*! 3: fields.size_lsb */
+ APP_SIZE_MSB_MUL);
+ nut_append_vu(&pkt, 0); /*! 4: fields.reserved */
+ nut_append_vu(&pkt, 1); /*! 5: fields.count */
+ nut_append_vs(&pkt, NUT_UNKNOWN_MATCH_TIME); /*! 6: fields.match */
+ nut_append_vu(&pkt, 2); /*! 7: fields.elision_header */
+
+ /* frame_code=4-255 (invalid) */
+ nut_append_vu(&pkt, NUT_FRAMEFLAG_INVALID); /*! flags */
+ nut_append_vu(&pkt, 2); /*! field_count */
+ nut_append_vu(&pkt, BOGUS); /*! 0: fields.pts */
+ nut_append_vu(&pkt, 256-4-1); /*! 1: fields.size_msb_nul */
+
+ /* elision headers ********************************/
+ nut_append_vu(&pkt, 3); /*! header_count_minus_1 */
+
+ /* elision_header[1] (640x480) */
+ hdr.len = 0;
+ nut_append_vu(&hdr, 2); /*! side_data_count */
+ nut_append_vb_str(&hdr, "Width"); /*! side_data[0].name */
+ nut_append_vu(&hdr, APP_W(640_480)); /*! side_data[0].value */
+ nut_append_vb_str(&hdr, "Height"); /*! side_data[1].name */
+ nut_append_vu(&hdr, APP_H(640_480)/2); /*! side_data[1].value */
+ nut_append_vu(&hdr, 0); /*! meta_data_count */
+ assert(hdr.len == APP_ELIDE(640_480));
+ nut_append_vb(&pkt, hdr.dat, hdr.len);
+
+ /* elision_header[2] (720x480) */
+ hdr.len = 0;
+ nut_append_vu(&hdr, 2); /*! side_data_count */
+ nut_append_vb_str(&hdr, "Width"); /*! side_data[0].name */
+ nut_append_vu(&hdr, APP_W(720_480)); /*! side_data[0].value */
+ nut_append_vb_str(&hdr, "Height"); /*! side_data[1].name */
+ nut_append_vu(&hdr, APP_H(720_480)/2); /*! side_data[1].value */
+ nut_append_vu(&hdr, 0); /*! meta_Data_count */
+ assert(hdr.len == APP_ELIDE(720_480));
+ nut_append_vb(&pkt, hdr.dat, hdr.len);
+
+ /* elision_header[3] (720x576) */
+ hdr.len = 0;
+ nut_append_vu(&hdr, 2); /*! side_data_count */
+ nut_append_vb_str(&hdr, "Width"); /*! side_data[0].name */
+ nut_append_vu(&hdr, APP_H(720_576)); /*! side_data[0].value */
+ nut_append_vb_str(&hdr, "Height"); /*! side_data[1].name */
+ nut_append_vu(&hdr, APP_H(720_576)/2); /*! side_data[1].value */
+ nut_append_vu(&hdr, 0); /*! meta_data_count */
+ assert(hdr.len == APP_ELIDE(720_576));
+ nut_append_vb(&pkt, hdr.dat, hdr.len);
+
+ nut_append_packet(&out, nut_startcode_main, pkt.dat, pkt.len);
+#undef BOGUS
+
+ /* stream_header ******************************************************/
+#define BOGUS 1
+ pkt.len = 0;
+
+ nut_append_vu(&pkt, 0); /*! stream_id */
+ nut_append_vu(&pkt, NUT_STREAMCLASS_VIDEO); /*! stream_class */
+ nut_append_vb(&pkt, "BGR\x08", 4); /*! fourcc */
+ nut_append_vu(&pkt, 0); /*! time_base_id */
+ nut_append_vu(&pkt, BOGUS); /*! msb_pts_shift (only relevant if FRAMEFLAG_CODED_PTS) */
+ nut_append_vu(&pkt, BOGUS); /*! max_pts_distance (all frames have a checksum) */
+ nut_append_vu(&pkt, 0); /*! decode_delay */
+ nut_append_vu(&pkt, 0); /*! stream_flags */
+ nut_append_vb(&pkt, NULL, 0); /*! codec_specific_data */
+ nut_append_vu(&pkt, BOGUS); /*! width (all frames set width) */
+ nut_append_vu(&pkt, BOGUS); /*! height (all frames set height) */
+ nut_append_vu(&pkt, 1); /*! sample_width */
+ nut_append_vu(&pkt, 2); /*! sample_height */
+ nut_append_vu(&pkt, NUT_COLORSPACE_UNKNOWN); /*! colorspace_type */
+
+ nut_append_packet(&out, nut_startcode_stream, pkt.dat, pkt.len);
+#undef BOGUS
+
+ /* flush **************************************************************/
+
+ bool ret = xwrite(fd, out.dat, out.len);
+ free(out.dat);
+ free(pkt.dat);
+ state->pts = time_ns;
+ state->len = out.len;
+ state->last_syncpoint = 0;
+ return ret;
+}
+
+bool app_write_frame(int fd, struct app_state *state, uint64_t time_ns, enum app_res res, void *framebuffer) {
+ assert(state);
+ assert(framebuffer);
+
+ struct buf out = {0};
+ struct buf pkt = {0};
+ uint64_t beg = state->len;
+
+ size_t full_size, fb_size;
+ switch (res) {
+ case APP_RES_640_480: full_size = APP_SIZE(640_480); fb_size = APP_FB(640_480); break;
+ case APP_RES_720_480: full_size = APP_SIZE(720_480); fb_size = APP_FB(720_480); break;
+ case APP_RES_720_576: full_size = APP_SIZE(720_576); fb_size = APP_FB(720_576); break;
+ }
+
+ /* packet (syncpoint) ( (8+1)+ (10+2) + 4 = 25 bytes) */
+ nut_append_vu(&pkt, time_ns); /*! global_key_pts (<=10 bytes) */
+ nut_append_vu(&pkt, state->last_syncpoint ? (beg - state->last_syncpoint)/16 : 0); /*! back_ptr_div16 (<=2 bytes) */
+ nut_append_packet(&out, nut_startcode_syncpoint, pkt.dat, pkt.len);
+
+ /* frame header ( 1+1+4 = 6 bytes) */
+ uint64_t frame_beg = out.len;
+ nut_append_u8(&out, 1+res); /*! frame_code (4 byte) */
+ nut_append_vu(&out, full_size/APP_SIZE_MSB_MUL); /*! data_size_msb (1 byte) */
+ nut_append_u32(&out, nut_crc32(&out.dat[frame_beg], out.len - frame_beg)); /*! checksum (4 bytes) */
+ /* side_data and meta_data come from elision headers */
+
+ /* flush */
+ assert(out.len <= APP_FRAME_MAX_OVERHEAD);
+ bool err = xwrite(fd, out.dat, out.len);
+ free(out.dat);
+ free(pkt.dat);
+ if (err)
+ return true;
+ state->len += out.len;
+
+ /* frame data */
+ if (xwrite(fd, framebuffer, fb_size))
+ return true;
+ state->len += fb_size;
+
+ /* return */
+ state->pts = time_ns;
+ state->last_syncpoint = beg;
+ return false;
+}
+
+/* Demo application ***********************************************************/
+
+int main() {
+ struct app_state state;
+ if (app_write_intro(1, &state, 0))
+ return 1;
+
+ uint8_t framebuffer[APP_FB(720_576)];
+
+#define SCALE 10
+ for (int i = 0; i < 10; i++) {
+ memset(framebuffer, 0, sizeof(framebuffer));
+ for (int y = 0; y < 4*SCALE; y++) {
+ int width;
+ switch (i%3) {
+ case APP_RES_640_480: width = APP_W(640_480); break;
+ case APP_RES_720_480: width = APP_W(720_480); break;
+ case APP_RES_720_576: width = APP_W(720_576); break;
+ }
+ for (int x = 0; x < 8*SCALE; x++)
+ framebuffer[(y*width)+x] = font[i][((y/SCALE)*8)+(x/SCALE)] == ' ' ? 0b11000000 : 0b00000011;
+ }
+ if (app_write_frame(1, &state, ((uint64_t)i)*((uint64_t)i)*1000000000ULL, i%3, framebuffer))
+ return 1;
+ }
+ return 0;
+}
diff --git a/vid-scratch/nutgen-gif.c b/vid-scratch/nutgen-gif.c
new file mode 100644
index 0000000..15c3b75
--- /dev/null
+++ b/vid-scratch/nutgen-gif.c
@@ -0,0 +1,289 @@
+/* nut.c - A simple NUT encoder with the following properties:
+ *
+ * - Each frame is an uncompressed 8-bit RGB (specifically `(msb)2B
+ * 3G 3R(lsb)`, known as the 4CC "BGR\x08") framebuffer.
+ *
+ * - Each pixel in the framebuffer is non-square; it is twice as tall
+ * as it is wide.
+ *
+ * - The display-resolution of each frame may change between 640x480,
+ * 720x480, and 720x576. This is the display-resolution; because
+ * of non-square pixels, the framebuffer for each would be 640x240,
+ * 720x240, and 720x288, respectively.
+ *
+ * - VFR (Variable Frame Rate) - Each frame has its own timestamp.
+ */
+
+#include <fcntl.h> /* for open() */
+
+#include "common.h"
+
+/* High-level (specific to the properties we want) ****************************/
+
+enum app_res {
+ APP_RES_INVALID = -1,
+ APP_RES_640_480 = 0,
+ APP_RES_720_480 = 1,
+ APP_RES_720_576 = 2,
+ APP_RES_NUM = 3,
+};
+#define APP_RES_MIN APP_RES_640_480
+#define APP_RES_MAX APP_RES_720_576
+
+size_t app_res_w(enum app_res res) {
+ switch (res) {
+ case APP_RES_640_480: return 640;
+ case APP_RES_720_480: return 720;
+ case APP_RES_720_576: return 720;
+ default: assert_notreached("invalid res");
+ }
+}
+size_t app_res_h(enum app_res res) {
+ switch (res) {
+ case APP_RES_640_480: return 480/2;
+ case APP_RES_720_480: return 480/2;
+ case APP_RES_720_576: return 576/2;
+ default: assert_notreached("invalid res");
+ }
+}
+size_t app_res_fb_size(enum app_res res) {
+ return app_res_w(res)*app_res_h(res);
+}
+
+#define APP_FRAME_SIZE_MSB_MUL 0x2000 /* must be less than 0x4000 */
+#define APP_FRAME_MAX_HEADER 16
+
+struct app_state {
+ enum app_res res;
+ uint64_t len;
+ uint64_t last_syncpoint;
+};
+
+#define GIF_SCREENFLAG_GLOBAL_COLOR_TABLE (1<<7)
+#define GIF_SCREENFLAG_BITS_PER_COLOR(x) ({ static_assert((x) <= 8); (((x)-1)&0b111)<<4; })
+#define GIF_SCREENFLAG_GLOBAL_COLOR_TABLE_SORTED (1<<3)
+#define GIF_SCREENFLAG_GLOBAL_COLOR_TABLE_SIZE(x) ({ static_assert(__builtin_popcount(x) == 1 && __builtin_ctz(x) <= 8); __builtin_ctz(x)-1; })
+
+#define GIF_ASPECT_RATIO(w, h) (64*(w)/(h)+15)
+
+#define GIF_IMAGEFLAG_LOCAL_COLOR_TABLE (1<<7)
+#define GIF_IMAGEFLAG_INTERLACE (1<<6)
+#define GIF_IMAGEFLAG_LOCAL_COLOR_TABLE_SORTED (1<<5)
+#define _GIF_IMAGEFLAG_4 (1<<4)
+#define _GIF_IMAGEFLAG_3 (1<<3)
+#define GIF_IMAGEFLAG_LOCAL_COLOR_TABLE_SIZE(x) ({ static_assert(__builtin_popcount(x) == 1 && __builtin_ctz(x) <= 8); __builtin_ctz(x)-1; })
+
+#define UINT16LE(x) ((x)&0xFF), (((x)>>8)&0xFF)
+
+void app_append_intro(struct buf *out, enum app_res res) {
+ [[maybe_unused]] uint8_t intro[] = {
+ /* Header */
+ 'G', 'I', 'F', /*! Signature */
+ '8', '9', 'a', /*! Version */
+
+ /* Logical Screen Descriptor */
+ UINT16LE(app_res_w(res)), /*! Logical Screen Width */
+ UINT16LE(app_res_h(res)), /*! Logical Screen Height */
+ GIF_SCREENFLAG_GLOBAL_COLOR_TABLE| /*! Global Color Table Flag */
+ GIF_SCREENFLAG_BITS_PER_COLOR(8)| /*! Color Resolution */
+ 0| /*! Sort Flag */
+ GIF_SCREENFLAG_GLOBAL_COLOR_TABLE_SIZE(256), /*! Size of Global Color Table */
+ 0, /*! Background Color Index */
+ GIF_ASPECT_RATIO(1, 2), /*! Pixel Aspect Ratio */
+
+ /* Global Color Table */
+ /* identity-map R:G:B 3:3:2 bits */
+#define R3(idx) (((idx)>>5)&0b111)
+#define G3(idx) (((idx)>>2)&0b111)
+#define B2(idx) (((idx)>>0)&0b011)
+#define R8(idx) ( (R3(idx)<<5) | (R3(idx)<<2) | (R3(idx)>>1) )
+#define G8(idx) ( (G3(idx)<<5) | (G3(idx)<<2) | (G3(idx)>>1) )
+#define B8(idx) ( (B2(idx)<<6) | (B2(idx)<<4) | (B2(idx)<<2) | (B2(idx)<<0) )
+#define COLOR(idx) R8(idx), G8(idx), B8(idx)
+#define COLOR_2(base_idx) COLOR (base_idx), COLOR (base_idx+ 1)
+#define COLOR_4(base_idx) COLOR_2 (base_idx), COLOR_2 (base_idx+ 2)
+#define COLOR_8(base_idx) COLOR_4 (base_idx), COLOR_4 (base_idx+ 4)
+#define COLOR_16(base_idx) COLOR_8 (base_idx), COLOR_8 (base_idx+ 8)
+#define COLOR_32(base_idx) COLOR_16 (base_idx), COLOR_16 (base_idx+ 16)
+#define COLOR_64(base_idx) COLOR_32 (base_idx), COLOR_32 (base_idx+ 32)
+#define COLOR_128(base_idx) COLOR_64 (base_idx), COLOR_64 (base_idx+ 64)
+#define COLOR_256(base_idx) COLOR_128(base_idx), COLOR_128(base_idx+128)
+ COLOR_256(0),
+
+ /* Image Descriptor */
+ 0x2C, /*! Image Separator */
+ UINT16LE(0), /*! Image Left Position */
+ UINT16LE(0), /*! Image Top Position */
+ UINT16LE(app_res_w(res)), /*! Image Width */
+ UINT16LE(app_res_h(res)), /*! Image Height */
+ 0| /*! Local Color Table Flag */
+ 0, /*! Interlace Flag */
+
+
+ };
+ struct buf pkt = {0};
+ unsigned int frame_code = 0;
+#define INC_FRAME_CODE() do { frame_code++; if (frame_code == 'N') frame_code++; } while(0)
+
+ append(out, nut_file_id_string, sizeof(nut_file_id_string));
+
+#define BOGUS(n) n
+ /* main_header ********************************************************/
+ pkt.len = 0;
+
+ /* head *******************************************/
+ nut_append_vu(&pkt, 3); /*! version */
+ nut_append_vu(&pkt, 1); /*! stream_count */
+ nut_append_vu(&pkt, /*! max_distance */
+ APP_FRAME_MAX_HEADER +
+ app_res_fb_size(res));
+
+ /* time bases *************************************/
+ nut_append_vu(&pkt, 1); /*! time_base_count */
+
+ /* time_base[0] = 1ns */
+ nut_append_vu(&pkt, 1); /*! numerator */
+ nut_append_vu(&pkt, 1000000000ULL); /*! denominator */
+
+ /* frame codes ************************************/
+ /* "A muxer SHOULD mark [frame codes] 0x00 and 0xFF as invalid" */
+
+ /* frame_code=0 (invalid) */
+ nut_append_vu(&pkt, NUT_FRAMEFLAG_INVALID); /*! flags */
+ nut_append_vu(&pkt, 2); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, 1); /*! 1: fields.size_msb_nul */
+ INC_FRAME_CODE();
+
+ /* frame_code=1 (res) */
+ nut_append_vu(&pkt, /*! flags */
+ NUT_FRAMEFLAG_KEY | /* Because they're full framebuffers, all frames are keyframes. */
+ NUT_FRAMEFLAG_SIZE_MSB | /* 640*480/2 > 16384 (the max val of data_size_lsb). */
+ NUT_FRAMEFLAG_CODED_PTS | /* framerate is unknown, each frame must have a timestamp. */
+ NUT_FRAMEFLAG_CHECKSUM ); /* framerate is unknown, guard against exceeding max_pts_distance. */
+ nut_append_vu(&pkt, 6); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, APP_FRAME_SIZE_MSB_MUL); /*! 1: fields.size_msb_nul */
+ nut_append_vu(&pkt, 0); /*! 2: fields.stream */
+ nut_append_vu(&pkt, /*! 3: fields.size_lsb */
+ app_res_fb_size(res) % APP_FRAME_SIZE_MSB_MUL);
+ nut_append_vu(&pkt, 0); /*! 4: fields.reserved */
+ nut_append_vu(&pkt, 1); /*! 5: fields.count */
+ INC_FRAME_CODE();
+
+ /* frame_code=N-255 (invalid) */
+ nut_append_vu(&pkt, NUT_FRAMEFLAG_INVALID); /*! flags */
+ nut_append_vu(&pkt, 2); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, /*! 1: fields.size_msb_nul */
+ 256-frame_code-(frame_code < 'N' ? 1 : 0));
+
+ /* tail *******************************************/
+ nut_append_vu(&pkt, 0); /*! header_count_minus_1 */
+
+ nut_append_packet(out, nut_startcode_main, pkt.dat, pkt.len);
+
+ /* stream_header ******************************************************/
+ pkt.len = 0;
+
+ nut_append_vu(&pkt, 0); /*! stream_id */
+ nut_append_vu(&pkt, NUT_STREAMCLASS_VIDEO); /*! stream_class */
+ nut_append_vb(&pkt, "BGR\x08", 4); /*! fourcc */
+ nut_append_vu(&pkt, 0); /*! time_base_id */
+ nut_append_vu(&pkt, 0); /*! msb_pts_shift */
+ nut_append_vu(&pkt, BOGUS(0)); /*! max_pts_distance (all frames have a checksum) */
+ nut_append_vu(&pkt, 0); /*! decode_delay */
+ nut_append_vu(&pkt, 0); /*! stream_flags */
+ nut_append_vb(&pkt, NULL, 0); /*! codec_specific_data */
+ nut_append_vu(&pkt, app_res_w(res)); /*! width */
+ nut_append_vu(&pkt, app_res_h(res)); /*! height */
+ nut_append_vu(&pkt, 1); /*! sample_width */
+ nut_append_vu(&pkt, 2); /*! sample_height */
+ nut_append_vu(&pkt, NUT_COLORSPACE_UNKNOWN); /*! colorspace_type */
+
+ nut_append_packet(out, nut_startcode_stream, pkt.dat, pkt.len);
+
+ /* flush **************************************************************/
+#undef BOGUS
+
+ free(pkt.dat);
+}
+
+bool app_write_frame(int fd, struct app_state *state, uint64_t time_ns, enum app_res res, void *framebuffer) {
+ assert(framebuffer);
+ assert(0 <= res && res < APP_RES_NUM);
+
+ struct buf out = {0};
+ struct buf pkt = {0};
+
+ if (res != state->res) {
+ app_append_intro(&out, res);
+ state->res = res;
+ }
+
+ /* syncpoint packet ***************************************************/
+ pkt.len = 0;
+ uint64_t syncpoint_beg = state->len + out.len;
+
+ nut_append_vu(&pkt, time_ns); /*! global_key_pts */
+ nut_append_vu(&pkt, state->last_syncpoint /*! back_ptr_div16 */
+ ? (syncpoint_beg - state->last_syncpoint)/16
+ : 0);
+
+ nut_append_packet(&out, nut_startcode_syncpoint, pkt.dat, pkt.len);
+
+ /* frame header ( 1+10+1+4 = 16 bytes) */
+ uint64_t frame_beg = out.len;
+ nut_append_u8(&out, 1); /*! frame_code (1 byte) */
+ nut_append_vu(&out, time_ns); /*! coded_pts (<=10 bytes) */
+ nut_append_vu(&out, app_res_fb_size(res) / APP_FRAME_SIZE_MSB_MUL); /*! data_size_msb (1 byte) */
+ nut_append_u32(&out, nut_crc32(&out.dat[frame_beg], out.len - frame_beg)); /*! checksum (4 bytes) */
+ assert(out.len - frame_beg <= APP_FRAME_MAX_HEADER);
+
+ /* flush */
+ bool err = xwrite(fd, out.dat, out.len);
+ free(out.dat);
+ free(pkt.dat);
+ if (err)
+ return true;
+
+ /* frame data */
+ if (xwrite(fd, framebuffer, app_res_fb_size(res)))
+ return true;
+
+ /* return */
+ state->res = res;
+ state->len += out.len + app_res_fb_size(res);
+ state->last_syncpoint = syncpoint_beg;
+ return false;
+}
+
+/* Demo application ***********************************************************/
+
+int main() {
+ struct app_state state = {
+ .res = APP_RES_INVALID,
+ };
+
+ uint8_t framebuffer[app_res_fb_size(APP_RES_MAX)];
+
+#define SCALE 10
+ for (int i = 0; i < 10; i++) {
+ enum app_res res = (i/2)%APP_RES_NUM;
+ memset(framebuffer, 0b00011000, sizeof(framebuffer));
+ for (int y = 0; y < 4*SCALE; y++)
+ for (int x = 0; x < 8*SCALE; x++)
+ framebuffer[(y*app_res_w(res))+x] = font[i][((y/SCALE)*8)+(x/SCALE)] == ' ' ? 0b11000000 : 0b00000011;
+ if (i%2 == 0) {
+ char name[] = {'o', 'u', 't', '-', '0'+(i/2), '.', 'g', 'i', 'f', '\0'};
+ int fd = open(name, O_WRONLY|O_TRUNC|O_CREAT, 0666);
+ dup2(fd, 1);
+ close(fd);
+ state.len = 0;
+ state.last_syncpoint = 0;
+ }
+ if (app_write_frame(1, &state, ((uint64_t)i)*1000000000ULL, res, framebuffer))
+ return 1;
+ }
+ return 0;
+}
diff --git a/vid-scratch/nutgen-sidedata.c b/vid-scratch/nutgen-sidedata.c
new file mode 100644
index 0000000..62fc680
--- /dev/null
+++ b/vid-scratch/nutgen-sidedata.c
@@ -0,0 +1,252 @@
+/* nut.c - A simple NUT encoder with the following properties:
+ *
+ * - Each frame is an uncompressed 8-bit RGB (specifically `(msb)2B
+ * 3G 3R(lsb)`, known as the 4CC "BGR\x08") framebuffer.
+ *
+ * - Each pixel in the framebuffer is non-square; it is twice as tall
+ * as it is wide.
+ *
+ * - The display-resolution of each frame may change between 640x480,
+ * 720x480, and 720x576. This is the display-resolution; because
+ * of non-square pixels, the framebuffer for each would be 640x240,
+ * 720x240, and 720x288, respectively.
+ *
+ * - VFR (Variable Frame Rate) - Each frame has its own timestamp.
+ */
+
+#include "common.h"
+
+/* High-level (specific to the properties we want) ****************************/
+
+enum app_res {
+ APP_RES_640_480 = 0,
+ APP_RES_720_480 = 1,
+ APP_RES_720_576 = 2,
+};
+#define APP_RES_MIN APP_RES_640_480
+#define APP_RES_MAX APP_RES_720_576
+
+size_t app_res_w(enum app_res res) {
+ switch (res) {
+ case APP_RES_640_480: return 640;
+ case APP_RES_720_480: return 720;
+ case APP_RES_720_576: return 720;
+ default: assert_notreached("invalid res");
+ }
+}
+size_t app_res_h(enum app_res res) {
+ switch (res) {
+ case APP_RES_640_480: return 480/2;
+ case APP_RES_720_480: return 480/2;
+ case APP_RES_720_576: return 576/2;
+ default: assert_notreached("invalid res");
+ }
+}
+size_t app_res_fb_size(enum app_res res) {
+ return app_res_w(res)*app_res_h(res);
+}
+size_t app_res_sm_size(enum app_res) {
+ /* See app_write_frame() */
+ return 19;
+}
+
+#define APP_FRAME_SIZE_MSB_MUL 0x2000 /* must be less than 0x4000 */
+#define APP_FRAME_MAX_HEADER 16
+
+struct app_state {
+ enum app_res res;
+};
+
+#define APP_COMMON_FLAGS \
+ NUT_FRAMEFLAG_KEY | /* Because they're full framebuffers, all frames are keyframes. */ \
+ NUT_FRAMEFLAG_SIZE_MSB | /* 640*480/2 > 16384 (the max val of data_size_lsb). */ \
+ NUT_FRAMEFLAG_CODED_PTS | /* framerate is unknown, each frame must have a timestamp. */ \
+ NUT_FRAMEFLAG_CHECKSUM /* framerate is unknown, guard against exceeding max_pts_distance. */
+
+
+bool app_write_intro(int fd, struct app_state *state, uint64_t time_ns) {
+ struct buf out = {0};
+ struct buf pkt = {0};
+ unsigned int frame_code = 0;
+#define INC_FRAME_CODE() do { frame_code++; if (frame_code == 'N') frame_code++; } while(0)
+
+ append(&out, nut_file_id_string, sizeof(nut_file_id_string));
+
+ /* Changing resolution mid-stream requires per-fame side-data,
+ * which requires FLAG_SM_DATA, which requires NUT v4.
+ *
+ * Unfortunately, libnut / nututils does not support v4, so
+ * there goes a good debugging tool.
+ */
+
+#define BOGUS(n) n
+ /* main_header ********************************************************/
+ pkt.len = 0;
+
+ /* head *******************************************/
+ nut_append_vu(&pkt, 4); /*! version */
+ nut_append_vu(&pkt, 0); /*! minor_version */
+ nut_append_vu(&pkt, 1); /*! stream_count */
+ nut_append_vu(&pkt, /*! max_distance */
+ APP_FRAME_MAX_HEADER +
+ app_res_sm_size(APP_RES_MAX) +
+ app_res_fb_size(APP_RES_MAX));
+
+ /* time bases *************************************/
+ nut_append_vu(&pkt, 1); /*! time_base_count */
+
+ /* time_base[0] = 1ns */
+ nut_append_vu(&pkt, 1); /*! numerator */
+ nut_append_vu(&pkt, 1000000000ULL); /*! denominator */
+
+ /* frame codes ************************************/
+ /* "A muxer SHOULD mark [frame codes] 0x00 and 0xFF as invalid" */
+
+ /* frame_code=0 (invalid) */
+ nut_append_vu(&pkt, NUT_FRAMEFLAG_INVALID); /*! flags */
+ nut_append_vu(&pkt, 2); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, 1); /*! 1: fields.size_msb_nul */
+ INC_FRAME_CODE();
+
+ for (enum app_res res = APP_RES_MIN; res <= APP_RES_MAX; res++) {
+ /* frame_code=(res*2)+1 (at `res`) */
+ nut_append_vu(&pkt, APP_COMMON_FLAGS); /*! flags */
+ nut_append_vu(&pkt, 6); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, APP_FRAME_SIZE_MSB_MUL); /*! 1: fields.size_msb_nul */
+ nut_append_vu(&pkt, 0); /*! 2: fields.stream */
+ nut_append_vu(&pkt, /*! 3: fields.size_lsb */
+ app_res_fb_size(res) % APP_FRAME_SIZE_MSB_MUL);
+ nut_append_vu(&pkt, 0); /*! 4: fields.reserved */
+ nut_append_vu(&pkt, 1); /*! 5: fields.count */
+ INC_FRAME_CODE();
+
+ /* frame_code=(res*2)+2 (change to `res`) */
+ nut_append_vu(&pkt, APP_COMMON_FLAGS| /*! flags */
+ NUT_FRAMEFLAG_SM_DATA);
+ nut_append_vu(&pkt, 6); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, APP_FRAME_SIZE_MSB_MUL); /*! 1: fields.size_msb_nul */
+ nut_append_vu(&pkt, 0); /*! 2: fields.stream */
+ nut_append_vu(&pkt, /*! 3: fields.size_lsb */
+ (app_res_sm_size(res) + app_res_fb_size(res)) % APP_FRAME_SIZE_MSB_MUL);
+ nut_append_vu(&pkt, 0); /*! 4: fields.reserved */
+ nut_append_vu(&pkt, 1); /*! 5: fields.count */
+ INC_FRAME_CODE();
+ }
+
+ /* frame_code=N-255 (invalid) */
+ nut_append_vu(&pkt, NUT_FRAMEFLAG_INVALID); /*! flags */
+ nut_append_vu(&pkt, 2); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, /*! 1: fields.size_msb_nul */
+ 256-frame_code-(frame_code < 'N' ? 1 : 0));
+
+ /* tail *******************************************/
+ nut_append_vu(&pkt, 0); /*! header_count_minus_1 */
+ nut_append_vu(&pkt, NUT_MAINFLAG_PIPE_MODE); /*! main_flags */
+
+ nut_append_packet(&out, nut_startcode_main, pkt.dat, pkt.len);
+
+ /* stream_header ******************************************************/
+ pkt.len = 0;
+
+ nut_append_vu(&pkt, 0); /*! stream_id */
+ nut_append_vu(&pkt, NUT_STREAMCLASS_VIDEO); /*! stream_class */
+ nut_append_vb(&pkt, "BGR\x08", 4); /*! fourcc */
+ nut_append_vu(&pkt, 0); /*! time_base_id */
+ nut_append_vu(&pkt, BOGUS(0)); /*! msb_pts_shift (only relevant if FRAMEFLAG_CODED_PTS) */
+ nut_append_vu(&pkt, BOGUS(0)); /*! max_pts_distance (all frames have a checksum) */
+ nut_append_vu(&pkt, 0); /*! decode_delay */
+ nut_append_vu(&pkt, 0); /*! stream_flags */
+ nut_append_vb(&pkt, NULL, 0); /*! codec_specific_data */
+ state->res = APP_RES_640_480;
+ nut_append_vu(&pkt, app_res_w(state->res)); /*! width */
+ nut_append_vu(&pkt, app_res_h(state->res)); /*! height */
+ nut_append_vu(&pkt, 1); /*! sample_width */
+ nut_append_vu(&pkt, 2); /*! sample_height */
+ nut_append_vu(&pkt, NUT_COLORSPACE_UNKNOWN); /*! colorspace_type */
+
+ nut_append_packet(&out, nut_startcode_stream, pkt.dat, pkt.len);
+
+ /* syncpoint **********************************************************/
+ pkt.len = 0;
+
+ nut_append_vu(&pkt, time_ns); /*! global_key_pts */
+ nut_append_vu(&pkt, 0); /*! back_ptr_div16 */
+
+ nut_append_packet(&out, nut_startcode_syncpoint, pkt.dat, pkt.len);
+
+ /* flush **************************************************************/
+#undef BOGUS
+
+ bool ret = xwrite(fd, out.dat, out.len);
+ free(out.dat);
+ free(pkt.dat);
+ return ret;
+}
+
+bool app_write_frame(int fd, struct app_state *state, uint64_t time_ns, enum app_res res, void *framebuffer) {
+ assert(framebuffer);
+
+ struct buf out = {0};
+ struct buf pkt = {0};
+
+ /* frame header ( 1+10+1+4 = 16 bytes) */
+ uint64_t frame_beg = out.len;
+ nut_append_u8(&out, 1+(2*res)+(res != state->res ? 1 : 0)); /*! frame_code (1 byte) */
+ nut_append_vu(&out, time_ns); /*! coded_pts (<=10 bytes) */
+ nut_append_vu(&out, /*! data_size_msb (1 byte) */
+ (app_res_fb_size(res) + (res != state->res ? app_res_sm_size(res) : 0)) / APP_FRAME_SIZE_MSB_MUL);
+ nut_append_u32(&out, nut_crc32(&out.dat[frame_beg], out.len - frame_beg)); /*! checksum (4 bytes) */
+ assert(out.len <= APP_FRAME_MAX_HEADER);
+
+ /* side/meta data ( 1+(6+2)+(7+2)+1 = 19 bytes) */
+ if (res != state->res) {
+ uint64_t data_beg = out.len;
+ nut_append_vu(&out, 2); /*! side_data_count (1 byte) */
+ nut_append_vb_str(&out, "Width"); /*! side_data[0].name (1+5 bytes) */
+ nut_append_vu(&out, app_res_w(res)); /*! side_data[0].value (2 bytes) */
+ nut_append_vb_str(&out, "Height"); /*! side_data[1].name (1+6 bytes) */
+ nut_append_vu(&out, app_res_h(res)); /*! side_data[1].value (2 bytes) */
+ nut_append_vu(&out, 0); /*! meta_data_count (1 byte) */
+ assert(out.len - data_beg == app_res_sm_size(res));
+ }
+
+ /* flush */
+ bool err = xwrite(fd, out.dat, out.len);
+ free(out.dat);
+ free(pkt.dat);
+ if (err)
+ return true;
+
+ /* frame data */
+ if (xwrite(fd, framebuffer, app_res_fb_size(res)))
+ return true;
+
+ /* return */
+ state->res = res;
+ return false;
+}
+
+/* Demo application ***********************************************************/
+
+int main() {
+ struct app_state state;
+ if (app_write_intro(1, &state, 0))
+ return 1;
+
+ uint8_t framebuffer[app_res_fb_size(APP_RES_MAX)];
+
+#define SCALE 10
+ for (int i = 0; i < 10; i++) {
+ memset(framebuffer, 0, sizeof(framebuffer));
+ for (int y = 0; y < 4*SCALE; y++)
+ for (int x = 0; x < 8*SCALE; x++)
+ framebuffer[(y*app_res_w(i%3))+x] = font[i][((y/SCALE)*8)+(x/SCALE)] == ' ' ? 0b11000000 : 0b00000011;
+ if (app_write_frame(1, &state, ((uint64_t)i)*1000000000ULL, i%3, framebuffer))
+ return 1;
+ }
+ return 0;
+}
diff --git a/vid-scratch/nutgen-streams.c b/vid-scratch/nutgen-streams.c
new file mode 100644
index 0000000..25c2394
--- /dev/null
+++ b/vid-scratch/nutgen-streams.c
@@ -0,0 +1,238 @@
+/* nut.c - A simple NUT encoder with the following properties:
+ *
+ * - Each frame is an uncompressed 8-bit RGB (specifically `(msb)2B
+ * 3G 3R(lsb)`, known as the 4CC "BGR\x08") framebuffer.
+ *
+ * - Each pixel in the framebuffer is non-square; it is twice as tall
+ * as it is wide.
+ *
+ * - The display-resolution of each frame may change between 640x480,
+ * 720x480, and 720x576. This is the display-resolution; because
+ * of non-square pixels, the framebuffer for each would be 640x240,
+ * 720x240, and 720x288, respectively.
+ *
+ * - VFR (Variable Frame Rate) - Each frame has its own timestamp.
+ */
+
+#include "common.h"
+
+/* High-level (specific to the properties we want) ****************************/
+
+enum app_res {
+ APP_RES_640_480 = 0,
+ APP_RES_720_480 = 1,
+ APP_RES_720_576 = 2,
+};
+#define APP_RES_MIN APP_RES_640_480
+#define APP_RES_MAX APP_RES_720_576
+
+size_t app_res_w(enum app_res res) {
+ switch (res) {
+ case APP_RES_640_480: return 640;
+ case APP_RES_720_480: return 720;
+ case APP_RES_720_576: return 720;
+ default: assert_notreached("invalid res");
+ }
+}
+size_t app_res_h(enum app_res res) {
+ switch (res) {
+ case APP_RES_640_480: return 480/2;
+ case APP_RES_720_480: return 480/2;
+ case APP_RES_720_576: return 576/2;
+ default: assert_notreached("invalid res");
+ }
+}
+size_t app_res_fb_size(enum app_res res) {
+ return app_res_w(res)*app_res_h(res);
+}
+
+#define APP_FRAME_SIZE_MSB_MUL 0x2000 /* must be less than 0x4000 */
+#define APP_FRAME_MAX_HEADER 16
+
+struct app_state {
+ enum app_res res;
+};
+
+bool app_write_intro(int fd, uint64_t time_ns) {
+ struct buf out = {0};
+ struct buf pkt = {0};
+ unsigned int frame_code = 0;
+#define INC_FRAME_CODE() do { frame_code++; if (frame_code == 'N') frame_code++; } while(0)
+
+ append(&out, nut_file_id_string, sizeof(nut_file_id_string));
+
+#define BOGUS(n) n
+ /* main_header ********************************************************/
+ pkt.len = 0;
+
+ /* head *******************************************/
+ nut_append_vu(&pkt, 4); /*! version */
+ nut_append_vu(&pkt, 0); /*! minor_version */
+ nut_append_vu(&pkt, APP_RES_MAX+1); /*! stream_count */
+ nut_append_vu(&pkt, /*! max_distance */
+ APP_FRAME_MAX_HEADER +
+ app_res_fb_size(APP_RES_MAX));
+
+ /* time bases *************************************/
+ nut_append_vu(&pkt, 1); /*! time_base_count */
+
+ /* time_base[0] = 1ns */
+ nut_append_vu(&pkt, 1); /*! numerator */
+ nut_append_vu(&pkt, 1000000000ULL); /*! denominator */
+
+ /* frame codes ************************************/
+ /* "A muxer SHOULD mark [frame codes] 0x00 and 0xFF as invalid" */
+
+ /* frame_code=0 (invalid) */
+ nut_append_vu(&pkt, NUT_FRAMEFLAG_INVALID); /*! flags */
+ nut_append_vu(&pkt, 2); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, 1); /*! 1: fields.size_msb_nul */
+ INC_FRAME_CODE();
+
+ for (enum app_res res = APP_RES_MIN; res <= APP_RES_MAX; res++) {
+ /* frame_code=(res*2)+1 (`res` keyframe) */
+ nut_append_vu(&pkt, /*! flags */
+ NUT_FRAMEFLAG_KEY | /* Because they're full framebuffers, all frames are keyframes. */
+ NUT_FRAMEFLAG_SIZE_MSB | /* 640*480/2 > 16384 (the max val of data_size_lsb). */
+ NUT_FRAMEFLAG_CODED_PTS | /* framerate is unknown, each frame must have a timestamp. */
+ NUT_FRAMEFLAG_CHECKSUM ); /* framerate is unknown, guard against exceeding max_pts_distance. */
+ nut_append_vu(&pkt, 6); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, APP_FRAME_SIZE_MSB_MUL); /*! 1: fields.size_msb_nul */
+ nut_append_vu(&pkt, res); /*! 2: fields.stream */
+ nut_append_vu(&pkt, /*! 3: fields.size_lsb */
+ app_res_fb_size(res) % APP_FRAME_SIZE_MSB_MUL);
+ nut_append_vu(&pkt, 0); /*! 4: fields.reserved */
+ nut_append_vu(&pkt, 1); /*! 5: fields.count */
+ INC_FRAME_CODE();
+
+ /* frame_code=(res*2)+2 (`res` EOR frame) */
+ nut_append_vu(&pkt, /*! flags */
+ NUT_FRAMEFLAG_KEY | /* Because they're full framebuffers, all frames are keyframes. */
+ NUT_FRAMEFLAG_EOR |
+ NUT_FRAMEFLAG_CODED_PTS | /* framerate is unknown, each frame must have a timestamp. */
+ NUT_FRAMEFLAG_CHECKSUM ); /* framerate is unknown, guard against exceeding max_pts_distance. */
+ nut_append_vu(&pkt, 6); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, BOGUS(1)); /*! 1: fields.size_msb_nul */
+ nut_append_vu(&pkt, res); /*! 2: fields.stream */
+ nut_append_vu(&pkt, 0); /*! 3: fields.size_lsb */
+ nut_append_vu(&pkt, 0); /*! 4: fields.reserved */
+ nut_append_vu(&pkt, 1); /*! 5: fields.count */
+ INC_FRAME_CODE();
+ }
+
+ /* frame_code=N-255 (invalid) */
+ nut_append_vu(&pkt, NUT_FRAMEFLAG_INVALID); /*! flags */
+ nut_append_vu(&pkt, 2); /*! field_count */
+ nut_append_vu(&pkt, BOGUS(0)); /*! 0: fields.pts */
+ nut_append_vu(&pkt, /*! 1: fields.size_msb_nul */
+ 256-frame_code-(frame_code < 'N' ? 1 : 0));
+
+ /* tail *******************************************/
+ nut_append_vu(&pkt, 0); /*! header_count_minus_1 */
+ nut_append_vu(&pkt, NUT_MAINFLAG_PIPE_MODE); /*! main_flags */
+
+ nut_append_packet(&out, nut_startcode_main, pkt.dat, pkt.len);
+
+ /* stream_header ******************************************************/
+ for (enum app_res res = APP_RES_MIN; res <= APP_RES_MAX; res++) {
+ pkt.len = 0;
+
+ nut_append_vu(&pkt, res); /*! stream_id */
+ nut_append_vu(&pkt, NUT_STREAMCLASS_VIDEO); /*! stream_class */
+ nut_append_vb(&pkt, "BGR\x08", 4); /*! fourcc */
+ nut_append_vu(&pkt, 0); /*! time_base_id */
+ nut_append_vu(&pkt, BOGUS(0)); /*! msb_pts_shift (only relevant if FRAMEFLAG_CODED_PTS) */
+ nut_append_vu(&pkt, BOGUS(0)); /*! max_pts_distance (all frames have a checksum) */
+ nut_append_vu(&pkt, 0); /*! decode_delay */
+ nut_append_vu(&pkt, 0); /*! stream_flags */
+ nut_append_vb(&pkt, NULL, 0); /*! codec_specific_data */
+ nut_append_vu(&pkt, app_res_w(res)); /*! width */
+ nut_append_vu(&pkt, app_res_h(res)); /*! height */
+ nut_append_vu(&pkt, 1); /*! sample_width */
+ nut_append_vu(&pkt, 2); /*! sample_height */
+ nut_append_vu(&pkt, NUT_COLORSPACE_UNKNOWN); /*! colorspace_type */
+
+ nut_append_packet(&out, nut_startcode_stream, pkt.dat, pkt.len);
+ }
+
+ /* syncpoint **********************************************************/
+ pkt.len = 0;
+
+ nut_append_vu(&pkt, time_ns); /*! global_key_pts */
+ nut_append_vu(&pkt, 0); /*! back_ptr_div16 */
+
+ nut_append_packet(&out, nut_startcode_syncpoint, pkt.dat, pkt.len);
+
+ /* flush **************************************************************/
+#undef BOGUS
+
+ bool ret = xwrite(fd, out.dat, out.len);
+ free(out.dat);
+ free(pkt.dat);
+ return ret;
+}
+
+bool app_write_frame(int fd, struct app_state *state, uint64_t time_ns, enum app_res res, void *framebuffer) {
+ assert(framebuffer);
+
+ struct buf out = {0};
+ struct buf pkt = {0};
+
+ if (res != state->res) {
+ /* EOR frame ( 1+10+4 = 15 bytes) */
+ uint64_t frame_beg = out.len;
+ nut_append_u8(&out, (2*(state->res))+2); /*! frame_code (1 byte) */
+ nut_append_vu(&out, time_ns); /*! coded_pts (<=10 bytes) */
+ nut_append_u32(&out, nut_crc32(&out.dat[frame_beg], out.len - frame_beg)); /*! checksum (4 bytes) */
+ }
+
+ /* frame header ( 1+10+1+4 = 16 bytes) */
+ uint64_t frame_beg = out.len;
+ nut_append_u8(&out, (2*res)+1); /*! frame_code (1 byte) */
+ nut_append_vu(&out, time_ns); /*! coded_pts (<=10 bytes) */
+ nut_append_vu(&out, app_res_fb_size(res) / APP_FRAME_SIZE_MSB_MUL); /*! data_size_msb (1 byte) */
+ nut_append_u32(&out, nut_crc32(&out.dat[frame_beg], out.len - frame_beg)); /*! checksum (4 bytes) */
+ assert(out.len - frame_beg <= APP_FRAME_MAX_HEADER);
+
+ /* flush */
+ bool err = xwrite(fd, out.dat, out.len);
+ free(out.dat);
+ free(pkt.dat);
+ if (err)
+ return true;
+
+ /* frame data */
+ if (xwrite(fd, framebuffer, app_res_fb_size(res)))
+ return true;
+
+ /* return */
+ state->res = res;
+ return false;
+}
+
+/* Demo application ***********************************************************/
+
+int main() {
+ if (app_write_intro(1, 0))
+ return 1;
+
+ struct app_state state = {
+ .res = 0,
+ };
+
+ uint8_t framebuffer[app_res_fb_size(APP_RES_MAX)];
+
+#define SCALE 10
+ for (int i = 0; i < 10; i++) {
+ memset(framebuffer, 0, sizeof(framebuffer));
+ for (int y = 0; y < 4*SCALE; y++)
+ for (int x = 0; x < 8*SCALE; x++)
+ framebuffer[(y*app_res_w(i%(APP_RES_MAX+1)))+x] = font[i][((y/SCALE)*8)+(x/SCALE)] == ' ' ? 0b11000000 : 0b00000011;
+ if (app_write_frame(1, &state, ((uint64_t)i)*1000000000ULL, i%(APP_RES_MAX+1), framebuffer))
+ return 1;
+ }
+ return 0;
+}
diff --git a/vid-scratch/requirements.md b/vid-scratch/requirements.md
new file mode 100644
index 0000000..4d88a09
--- /dev/null
+++ b/vid-scratch/requirements.md
@@ -0,0 +1,21 @@
+I need to select a video file format to use. I have the following
+requirements:
+
+ - Each frame is an uncompressed 8bpp RGB framebuffer. The CPU does
+ not have enough cycles to process the framebuffer in any way; I
+ need to be able to DMA the framebuffer straight into the file
+ stream, without the pixel data going through the main CPU.
+
+ - The pixels are non-square; each pixel is twice as tall as it is
+ wide.
+
+ - VFR (Variable Frame Rate), up to a maximum of 60 FPS.
+
+ - The resolution must be able to change mid-stream, between the
+ pre-determined values 640x480, 720x480, and 720x576. This is the
+ display-resolution; because of non-square pixels, the framebuffer
+ for each would be 640x240, 720x240, and 720x288, respectively.
+
+ - The resulting video file must be playable using standard/common
+ video players (such as vlc or mpv) and tools (such as ffmpeg)
+ without any special flags or settings.
diff --git a/vid-scratch/reschange-gif-in-nut-gen.sh b/vid-scratch/reschange-gif-in-nut-gen.sh
new file mode 100755
index 0000000..dff17a9
--- /dev/null
+++ b/vid-scratch/reschange-gif-in-nut-gen.sh
@@ -0,0 +1,19 @@
+name=${0##*/}
+name=${name%.sh}
+
+rm -f -- "${name}.part1.gif" "${name}.part2.gif" "${name}.concat.txt" "${name}.gif"
+
+# Create a 320x240 clip
+ffmpeg -f lavfi -t 2 -i testsrc=r=30:s=320x240 "${name}.part1.gif"
+
+# Create a 640x480 clip
+ffmpeg -f lavfi -t 2 -i testsrc=r=30:s=640x480 "${name}.part2.gif"
+
+# Create a text file with the list of files
+{
+ echo "file '${name}.part1.gif'"
+ echo "file '${name}.part2.gif'"
+} > "${name}.concat.txt"
+
+# Concatenate the clips
+ffmpeg -f concat -safe 0 -i "${name}.concat.txt" -c:v copy "${name}.nut"
diff --git a/vid-scratch/reschange-mkvgen.sh b/vid-scratch/reschange-mkvgen.sh
new file mode 100755
index 0000000..cbe637d
--- /dev/null
+++ b/vid-scratch/reschange-mkvgen.sh
@@ -0,0 +1,19 @@
+name=${0##*/}
+name=${name%.sh}
+
+rm -f -- "${name}.part1.mkv" "${name}.part2.mkv" "${name}.concat.txt" "${name}.mkv"
+
+# Create a 320x240 clip
+ffmpeg -f lavfi -t 2 -i testsrc=r=30:s=320x240 -c:v vp8 -level 3 -pix_fmt yuv420p "${name}.part1.mkv"
+
+# Create a 640x480 clip
+ffmpeg -f lavfi -t 2 -i testsrc=r=30:s=640x480 -c:v vp8 -level 3 -pix_fmt yuv420p "${name}.part2.mkv"
+
+# Create a text file with the list of files
+{
+ echo "file '${name}.part1.mkv'"
+ echo "file '${name}.part2.mkv'"
+} > "${name}.concat.txt"
+
+# Concatenate the clips
+ffmpeg -f concat -safe 0 -i "${name}.concat.txt" -c:v copy "${name}.mkv"