diff options
Diffstat (limited to 'vid-scratch')
-rw-r--r-- | vid-scratch/.gitignore | 14 | ||||
-rw-r--r-- | vid-scratch/Archive.zip | bin | 0 -> 2211849 bytes | |||
-rw-r--r-- | vid-scratch/Makefile | 83 | ||||
-rw-r--r-- | vid-scratch/common.h | 259 | ||||
-rw-r--r-- | vid-scratch/nut-crc32.c | 47 | ||||
-rw-r--r-- | vid-scratch/nutgen-concat.c | 213 | ||||
-rw-r--r-- | vid-scratch/nutgen-elision.c | 278 | ||||
-rw-r--r-- | vid-scratch/nutgen-gif.c | 289 | ||||
-rw-r--r-- | vid-scratch/nutgen-sidedata.c | 252 | ||||
-rw-r--r-- | vid-scratch/nutgen-streams.c | 238 | ||||
-rw-r--r-- | vid-scratch/requirements.md | 21 | ||||
-rwxr-xr-x | vid-scratch/reschange-gif-in-nut-gen.sh | 19 | ||||
-rwxr-xr-x | vid-scratch/reschange-mkvgen.sh | 19 |
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 Binary files differnew file mode 100644 index 0000000..b4415b6 --- /dev/null +++ b/vid-scratch/Archive.zip 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" |