#include <moq.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <stdarg.h>
_Static_assert(MOQ_VIDEO_PIXEL_FORMAT_I420 == 0, "I420 discriminant moved");
_Static_assert(MOQ_VIDEO_PIXEL_FORMAT_RGBA == 1, "RGBA discriminant moved");
_Static_assert(offsetof(moq_video_decoder_output, max_age_us) == 0, "max_age_us moved");
_Static_assert(offsetof(moq_video_decoder_output, format) == 8, "format moved");
_Static_assert(offsetof(moq_video_decoder_output, width) == 12, "width moved");
_Static_assert(offsetof(moq_video_decoder_output, height) == 16, "height moved");
_Static_assert(sizeof(moq_video_decoder_output) == 24, "decoder output size changed");
#define MOQ_ERR_INVALID_POINTER -6
#define MOQ_ERR_INVALID_CODE -15
#define MOQ_ERR_CATALOG_NOT_FOUND -25
#define MOQ_ERR_INVALID_CONFIG -40
static _Noreturn void fail(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fflush(stderr);
_exit(1);
}
static void ignore_status(void *user_data, int32_t status) {
(void)user_data;
(void)status;
}
static void check_refusals(void) {
moq_video_decoder_output bad_format = {0, 999, 0, 0};
if (moq_decode_video(1, 0, &bad_format, NULL, NULL) != MOQ_ERR_INVALID_CODE)
fail("error: unknown format was not refused with InvalidCode: %s\n", moq_error());
const uint32_t bad_sizes[][2] = {{320, 0}, {0, 240}, {321, 240}, {320, 241}, {UINT32_MAX - 1, UINT32_MAX - 1}};
for (size_t i = 0; i < sizeof(bad_sizes) / sizeof(bad_sizes[0]); i++) {
moq_video_decoder_output bad_size = {0, MOQ_VIDEO_PIXEL_FORMAT_I420, bad_sizes[i][0], bad_sizes[i][1]};
if (moq_decode_video(1, 0, &bad_size, NULL, NULL) != MOQ_ERR_INVALID_CONFIG)
fail("error: size %ux%u was not refused with InvalidConfig: %s\n", bad_sizes[i][0],
bad_sizes[i][1], moq_error());
}
if (moq_decode_video(1, 0, NULL, NULL, NULL) != MOQ_ERR_INVALID_POINTER)
fail("error: null output was not refused with InvalidPointer: %s\n", moq_error());
moq_video_decoder_output valid = {0, MOQ_VIDEO_PIXEL_FORMAT_RGBA, 160, 120};
if (moq_decode_video(INT32_MAX, 0, &valid, ignore_status, NULL) != MOQ_ERR_CATALOG_NOT_FOUND)
fail("error: valid request did not reach catalog lookup: %s\n", moq_error());
fprintf(stderr, "decoder output refusals ok\n");
}
#define WIDTH 320
#define HEIGHT 240
typedef struct {
pthread_mutex_t mu;
pthread_cond_t cv;
int32_t broadcast; int done_broadcast, done_catalog_sub;
int32_t catalog_snapshot; uint32_t want_format, want_width, want_height, want_size;
int got_frame, done_frame;
} ctx_t;
static void on_broadcast(void *ud, int32_t broadcast) {
ctx_t *c = (ctx_t *)ud;
pthread_mutex_lock(&c->mu);
if (broadcast <= 0) {
c->done_broadcast = 1;
} else if (c->broadcast <= 0) {
c->broadcast = broadcast;
}
pthread_cond_broadcast(&c->cv);
pthread_mutex_unlock(&c->mu);
}
static void on_catalog(void *ud, int32_t catalog) {
ctx_t *c = (ctx_t *)ud;
if (catalog <= 0) {
pthread_mutex_lock(&c->mu);
c->done_catalog_sub = 1;
pthread_cond_broadcast(&c->cv);
pthread_mutex_unlock(&c->mu);
return;
}
pthread_mutex_lock(&c->mu);
if (c->catalog_snapshot <= 0) {
c->catalog_snapshot = catalog;
catalog = 0; }
pthread_cond_broadcast(&c->cv);
pthread_mutex_unlock(&c->mu);
if (catalog > 0) moq_consume_catalog_free((uint32_t)catalog);
}
static void on_frame(void *ud, int32_t frame_id) {
ctx_t *c = (ctx_t *)ud;
if (frame_id <= 0) {
pthread_mutex_lock(&c->mu);
c->done_frame = 1;
pthread_cond_broadcast(&c->cv);
pthread_mutex_unlock(&c->mu);
return;
}
moq_video_frame frame;
memset(&frame, 0, sizeof(frame));
if (moq_decode_video_frame((uint32_t)frame_id, &frame) == 0) {
pthread_mutex_lock(&c->mu);
if (!c->got_frame) {
if (frame.width != c->want_width || frame.height != c->want_height ||
frame.data_size != c->want_size) {
pthread_mutex_unlock(&c->mu);
fail("error: decoded %ux%u size %zu, want %ux%u size %u\n", frame.width, frame.height,
frame.data_size, c->want_width, c->want_height, c->want_size);
}
c->got_frame = 1;
}
pthread_cond_broadcast(&c->cv);
pthread_mutex_unlock(&c->mu);
}
moq_decode_video_frame_free((uint32_t)frame_id);
}
static void wait_for(pthread_mutex_t *mu, pthread_cond_t *cv, const int *flag, double timeout_s) {
struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);
deadline.tv_sec += (time_t)timeout_s;
while (!*flag) {
if (pthread_cond_timedwait(cv, mu, &deadline) != 0) break;
}
if (!*flag) fail("error: timed out waiting\n");
}
static uint8_t gray_rgba[WIDTH * HEIGHT * 4];
static uint64_t next_ts;
static void publish_frames(int32_t producer, int count) {
for (int i = 0; i < count; i++) {
moq_video_encoder_frame frame = {next_ts, gray_rgba, sizeof(gray_rgba)};
next_ts += 33333;
int32_t rc = moq_encode_video_frame((uint32_t)producer, &frame);
if (rc < 0) fail("error: moq_encode_video_frame failed: %d (%s)\n", rc, moq_error());
}
}
static void decode_once(ctx_t *c, int32_t catalog, const moq_video_decoder_output *output, uint32_t want_width,
uint32_t want_height, uint32_t want_size, int32_t producer) {
pthread_mutex_lock(&c->mu);
c->want_format = output->format;
c->want_width = want_width;
c->want_height = want_height;
c->want_size = want_size;
c->got_frame = 0;
c->done_frame = 0;
pthread_mutex_unlock(&c->mu);
int32_t consumer = moq_decode_video((uint32_t)catalog, 0, output, on_frame, c);
if (consumer <= 0) fail("error: moq_decode_video failed: %d (%s)\n", consumer, moq_error());
publish_frames(producer, 15);
pthread_mutex_lock(&c->mu);
wait_for(&c->mu, &c->cv, &c->got_frame, 15.0);
pthread_mutex_unlock(&c->mu);
if (moq_decode_video_cancel((uint32_t)consumer) < 0)
fail("error: moq_decode_video_cancel failed (%s)\n", moq_error());
pthread_mutex_lock(&c->mu);
wait_for(&c->mu, &c->cv, &c->done_frame, 10.0);
pthread_mutex_unlock(&c->mu);
fprintf(stderr, "decode %ux%u format %u ok\n", want_width, want_height, output->format);
}
int main(void) {
memset(gray_rgba, 0x80, sizeof(gray_rgba));
check_refusals();
ctx_t c;
memset(&c, 0, sizeof(c));
pthread_mutex_init(&c.mu, NULL);
pthread_cond_init(&c.cv, NULL);
int32_t origin = moq_origin_create();
if (origin <= 0) fail("error: moq_origin_create failed: %d\n", origin);
const char *path = "c-decoder-output-test";
int32_t broadcast = moq_origin_create_broadcast((uint32_t)origin, path, strlen(path));
if (broadcast <= 0) fail("error: moq_origin_create_broadcast failed: %d\n", broadcast);
if (moq_publish_announce((uint32_t)broadcast, NULL) < 0)
fail("error: moq_publish_announce failed (%s)\n", moq_error());
moq_video_encoder_input input = {MOQ_VIDEO_PIXEL_FORMAT_RGBA, WIDTH, HEIGHT, 30};
moq_video_encoder_output output = {MOQ_VIDEO_CODEC_H264, 0, 0, MOQ_VIDEO_ENCODER_KIND_SOFTWARE, NULL, 0};
int32_t producer = moq_encode_video((uint32_t)broadcast, &input, &output, 0);
if (producer <= 0) fail("error: moq_encode_video failed: %d (%s)\n", producer, moq_error());
if (moq_encode_video_cut((uint32_t)producer) < 0)
fail("error: moq_encode_video_cut failed (%s)\n", moq_error());
publish_frames(producer, 5);
int32_t request = moq_origin_request((uint32_t)origin, path, strlen(path), on_broadcast, &c);
if (request <= 0) fail("error: moq_origin_request failed: %d\n", request);
pthread_mutex_lock(&c.mu);
while (c.broadcast <= 0) wait_for(&c.mu, &c.cv, &c.broadcast, 10.0);
int32_t consume = c.broadcast;
pthread_mutex_unlock(&c.mu);
int32_t catalog_sub = moq_consume_catalog((uint32_t)consume, on_catalog, &c);
if (catalog_sub <= 0) fail("error: moq_consume_catalog failed: %d\n", catalog_sub);
pthread_mutex_lock(&c.mu);
while (c.catalog_snapshot <= 0) wait_for(&c.mu, &c.cv, &c.catalog_snapshot, 10.0);
int32_t catalog = c.catalog_snapshot;
pthread_mutex_unlock(&c.mu);
moq_video_decoder_output native_i420 = {10000000, MOQ_VIDEO_PIXEL_FORMAT_I420, 0, 0};
decode_once(&c, catalog, &native_i420, WIDTH, HEIGHT, WIDTH * HEIGHT * 3 / 2, producer);
moq_video_decoder_output native_rgba = {10000000, MOQ_VIDEO_PIXEL_FORMAT_RGBA, 0, 0};
decode_once(&c, catalog, &native_rgba, WIDTH, HEIGHT, WIDTH * HEIGHT * 4, producer);
moq_video_decoder_output small_i420 = {10000000, MOQ_VIDEO_PIXEL_FORMAT_I420, 160, 120};
decode_once(&c, catalog, &small_i420, 160, 120, 160 * 120 * 3 / 2, producer);
if (moq_consume_catalog_free((uint32_t)catalog) < 0)
fail("error: moq_consume_catalog_free failed (%s)\n", moq_error());
c.catalog_snapshot = 0;
if (moq_consume_catalog_cancel((uint32_t)catalog_sub) < 0)
fail("error: moq_consume_catalog_cancel failed (%s)\n", moq_error());
pthread_mutex_lock(&c.mu);
int request_pending = !c.done_broadcast;
pthread_mutex_unlock(&c.mu);
if (request_pending && moq_origin_request_cancel((uint32_t)request) < 0)
fail("error: moq_origin_request_cancel failed (%s)\n", moq_error());
if (moq_consume_close((uint32_t)consume) < 0)
fail("error: moq_consume_close failed (%s)\n", moq_error());
if (moq_encode_video_finish((uint32_t)producer) < 0)
fail("error: moq_encode_video_finish failed (%s)\n", moq_error());
if (moq_publish_close((uint32_t)broadcast) < 0)
fail("error: moq_publish_close failed (%s)\n", moq_error());
if (moq_origin_close((uint32_t)origin) < 0)
fail("error: moq_origin_close failed (%s)\n", moq_error());
pthread_mutex_lock(&c.mu);
wait_for(&c.mu, &c.cv, &c.done_broadcast, 10.0);
wait_for(&c.mu, &c.cv, &c.done_catalog_sub, 10.0);
pthread_mutex_unlock(&c.mu);
fprintf(stderr, "c decoder output fixture passed\n");
return 0;
}