ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
Video Frame Buffers

Files

file  video_frame.c
 ðŸŽ¬ Video frame buffer management for client-specific ASCII rendering
 

Detailed Description

Video Frame README

Overview

The Video Frame module provides structures and utilities for managing video frames throughout the ascii-chat pipeline. It handles RGB image data, frame metadata, and efficient frame lifecycle management.

Implementation: lib/video_frame.h

Key Features:

  • Zero-copy frame transfer (pointer swaps only)
  • Lock-free double-buffering system
  • Latest-frame-wins semantics (old frames dropped, not queued)
  • Pre-allocated buffers for zero-allocation operation
  • RGB24 image frame representation
  • Frame metadata (timestamp, dimensions, client ID)
  • Frame sequence numbering for drop detection
  • Comprehensive quality metrics tracking

Frame Structure

video_frame_t:

typedef struct {
uint8_t *data; // RGB24 pixel data
uint32_t width; // Frame width in pixels
uint32_t height; // Frame height in pixels
size_t data_size; // Total bytes (width * height * 3)
uint32_t client_id; // Client that owns this frame
uint64_t timestamp; // Capture timestamp (microseconds)
bool owns_data; // Whether this frame owns the data buffer
} video_frame_t;

Frame Properties:

  • data: RGB24 format (3 bytes per pixel: R, G, B)
  • width/height: Frame dimensions
  • data_size: Always equals width * height * 3
  • client_id: Which client this frame belongs to
  • timestamp: Microsecond-precision capture time
  • owns_data: Memory ownership flag

Operations

Double-Buffering API

The recommended way to use video frames is through the lock-free double-buffering API:

Writer API (producer thread):

// 1. Get pointer to back buffer for writing
uint8_t *write_buffer = video_frame_begin_write(frame_buffer);
// 2. Write frame data directly to back buffer
memcpy(write_buffer, captured_rgb_data, width * height * 3);
// 3. Atomically swap buffers to make frame available
video_frame_commit(frame_buffer, width, height);
video_frame_t * video_frame_begin_write(video_frame_buffer_t *vfb)
void video_frame_commit(video_frame_buffer_t *vfb)

Reader API (consumer thread):

// Get latest frame (returns NULL if no new frame since last read)
video_frame_t *frame = video_frame_get_latest(frame_buffer);
if (frame) {
// Process frame data
render_ascii(frame->data, frame->width, frame->height);
}
const video_frame_t * video_frame_get_latest(video_frame_buffer_t *vfb)

Manual Creation (for special cases)

For cases where you need standalone frames without the double-buffering system:

Create from Existing Data:

video_frame_t frame = {
.data = existing_rgb_data,
.width = 1920,
.height = 1080,
.data_size = 1920 * 1080 * 3,
.client_id = client_id,
.timestamp = get_timestamp_usec(),
.owns_data = false // Don't free existing_rgb_data
};

Data Access

Pixel Access:

// Get pixel at (x, y)
size_t offset = (y * frame->width + x) * 3;
uint8_t r = frame->data[offset + 0];
uint8_t g = frame->data[offset + 1];
uint8_t b = frame->data[offset + 2];
// Set pixel at (x, y)
frame->data[offset + 0] = 255; // R
frame->data[offset + 1] = 128; // G
frame->data[offset + 2] = 0; // B

Row Access:

// Get pointer to row y
uint8_t *row = &frame->data[y * frame->width * 3];
// Process entire row
for (uint32_t x = 0; x < frame->width; x++) {
uint8_t r = row[x * 3 + 0];
uint8_t g = row[x * 3 + 1];
uint8_t b = row[x * 3 + 2];
// Process pixel...
}

Copying

Deep Copy:

video_frame_t *copy = SAFE_MALLOC(sizeof(video_frame_t), video_frame_t *);
copy->width = src->width;
copy->height = src->height;
copy->data_size = src->data_size;
copy->client_id = src->client_id;
copy->timestamp = src->timestamp;
copy->owns_data = true;
// Allocate and copy pixel data
copy->data = SAFE_MALLOC(copy->data_size, uint8_t *);
memcpy(copy->data, src->data, copy->data_size);

Shallow Copy (reference):

video_frame_t reference = *src;
reference.owns_data = false; // Don't free the data when done

Cleanup

Free Frame:

if (frame->owns_data && frame->data) {
free(frame->data);
frame->data = NULL;
}
free(frame);
frame = NULL;

Validation

Validate Frame:

bool is_valid_frame(const video_frame_t *frame) {
if (!frame) return false;
if (!frame->data) return false;
if (frame->width == 0 || frame->height == 0) return false;
if (frame->data_size != frame->width * frame->height * 3) return false;
return true;
}

Check Frame Dimensions:

if (frame->width > MAX_WIDTH || frame->height > MAX_HEIGHT) {
log_error("Frame dimensions too large: %ux%u", frame->width, frame->height);
return ERROR_INVALID_PARAM;
}

Ownership Rules

Ownership Flag:

  • owns_data = true: Frame owns the data buffer, must free on cleanup
  • owns_data = false: Frame references external data, do NOT free

Ownership Transfer:

// Sender transfers ownership
video_frame_t *frame = create_frame();
frame->owns_data = true;
send_to_queue(frame);
// Sender no longer responsible for freeing frame->data
// Receiver takes ownership
video_frame_t *received = receive_from_queue();
if (received->owns_data) {
// Must free when done
free(received->data);
}

Frame Pipeline

Capture → Process → Encode → Send:

// 1. Capture from webcam
video_frame_t *frame = webcam_capture();
frame->owns_data = true;
frame->timestamp = get_timestamp_usec();
// 2. Process (resize, flip, etc.)
video_frame_t *processed = process_frame(frame);
// 3. Convert to ASCII
ascii_frame_t *ascii = image_to_ascii(processed);
// 4. Send over network
send_ascii_frame(client_id, ascii);
// 5. Cleanup
if (frame->owns_data) free(frame->data);
free(frame);
if (processed->owns_data) free(processed->data);
free(processed);
free(ascii);

Memory Management

Allocation Strategy:

// Calculate size with overflow protection
size_t data_size = (size_t)width * (size_t)height * 3;
// Allocate frame data
uint8_t *data = SAFE_MALLOC(data_size, uint8_t *);
if (!data) {
return SET_ERRNO(ERROR_OUT_OF_MEMORY, "Failed to allocate frame data");
}

Buffer Reuse:

// Reuse existing buffer if same size
if (frame->data_size == new_size) {
// Reuse existing buffer
memcpy(frame->data, new_data, new_size);
} else {
// Reallocate if size changed
if (frame->owns_data) free(frame->data);
frame->data = SAFE_MALLOC(new_size, uint8_t *);
frame->data_size = new_size;
}

Timestamps

Microsecond Precision:

#include <sys/time.h>
uint64_t get_timestamp_usec(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
return (uint64_t)tv.tv_sec * 1000000 + tv.tv_usec;
}
frame->timestamp = get_timestamp_usec();

Calculate Frame Age:

uint64_t now = get_timestamp_usec();
uint64_t age_usec = now - frame->timestamp;
double age_ms = age_usec / 1000.0;
if (age_ms > 100.0) {
log_warn("Frame is stale: %.1fms old", age_ms);
}

Best Practices

DO:

  • Always set owns_data correctly
  • Validate frame dimensions before allocation
  • Use overflow-safe size calculations
  • Set timestamp when capturing frames
  • Free frames when done
  • Check for NULL before dereferencing

DON'T:

  • Don't free frame->data if owns_data is false
  • Don't use frame after freeing
  • Don't assume frame->data is aligned
  • Don't modify shared frame data
  • Don't forget to set client_id
  • Don't use stale frames (check timestamp)

Integration Example

Complete Usage:

// Capture frame
video_frame_t *frame = SAFE_MALLOC(sizeof(video_frame_t), video_frame_t *);
frame->width = 640;
frame->height = 480;
frame->data_size = 640 * 480 * 3;
frame->data = SAFE_MALLOC(frame->data_size, uint8_t *);
frame->owns_data = true;
frame->client_id = my_client_id;
frame->timestamp = get_timestamp_usec();
// Fill with webcam data
webcam_read(frame->data, frame->data_size);
// Process and send
process_and_send_frame(frame);
// Cleanup (if still owning data)
if (frame->owns_data && frame->data) {
free(frame->data);
}
free(frame);
image_t * webcam_read(void)
See also
video_frame.h
video/image.h
os/webcam.h