Skip to content

Video Export

This example records a deterministic offscreen point animation to video.

Preview

Run And Adapt

Commands below assume a Datoviz source checkout and start at the repository root. Use your configured build environment; Python routes additionally require local bindings.

Route Availability Command or action
C Canonical native source just example-c runtime/video_export (build and run), or rerun ./build/examples/c/runtime/video_export
Python No verified adaptation on this page Start from the C source.
Browser Native only video export uses native app/canvas capture backends, including CPU readback and optional external/NVENC paths

This example is approved as a starting point for user code and coding agents. Keep the object lifetimes and data shapes intact while adapting the data and styling.

What To Look For

A row of tick points stays fixed while one larger point moves through a sinusoidal path; each frame updates the position, color, and diameter arrays before rendering. The default path writes video_export.mp4 through the app capture API, while --png renders a single smoke frame for quick validation.

This runtime workflow is the reproducible offscreen video-export path: the scene data is updated in a deterministic loop, frames are captured from an offscreen view, and the output location can be controlled with DVZ_CAPTURE_DIR and DVZ_CAPTURE_BASENAME. Live window recording uses the same capture API on a window-backed view, but it is documented separately because window size, controller input, and wall-clock timing can affect the result.

Source

/*
 * Copyright (c) 2021 Cyrille Rossant and contributors. All rights reserved.
 * Licensed under the MIT license. See LICENSE file in the project root for details.
 * SPDX-License-Identifier: MIT
 */

/* video_export - This example records a deterministic offscreen point animation to video.
 *
 * What to look for: a row of tick points stays fixed while one larger point moves through a
 * sinusoidal path; each frame updates the retained position, color, and diameter arrays before
 * rendering. The default path writes video_export.mp4 through the app capture API, while --png
 * renders a single smoke frame for quick validation.
 *
 * This runtime workflow is the reproducible offscreen video-export path: the scene data is updated
 * in a deterministic loop, frames are captured from an offscreen view, and the output location can
 * be controlled with DVZ_CAPTURE_DIR and DVZ_CAPTURE_BASENAME. Live window recording uses the same
 * capture API on a window-backed view, but it is documented separately because window size,
 * controller input, and wall-clock timing can affect the result.
 *
 * Scenario: runtime_video_export
 * Style: runtime, graphite_cyan, 1920x1080 output target
 *
 * Build:  just example-c runtime/video_export
 * Run:    ./build/examples/c/runtime/video_export
 * Smoke:  ./build/examples/c/runtime/video_export --png
 */



/*************************************************************************************************/
/*  Includes                                                                                     */
/*************************************************************************************************/

#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "datoviz/app.h"
#include "datoviz/canvas/enums.h"
#include "datoviz/scene.h"
#include "datoviz/video.h"
#include "example_common.h"
#include "example_style.h"



/*************************************************************************************************/
/*  Constants                                                                                    */
/*************************************************************************************************/

#define WIDTH       EXAMPLE_OUTPUT_WIDTH
#define HEIGHT      EXAMPLE_OUTPUT_HEIGHT
#define FPS         60.0
#define FRAME_COUNT 120u
#define TICK_COUNT  9u
#define POINT_COUNT (TICK_COUNT + 1u)
#define PATH_SIZE   512u
#define PROGRESS_WIDTH 32u

static const float TAU = 6.28318530718f;



/*************************************************************************************************/
/*  Structs                                                                                      */
/*************************************************************************************************/

typedef struct VideoExportState
{
    DvzVisual* point;
    vec3 positions[POINT_COUNT];
    DvzColor colors[POINT_COUNT];
    float diameters[POINT_COUNT];
} VideoExportState;



/*************************************************************************************************/
/*  Helpers                                                                                      */
/*************************************************************************************************/

/**
 * Return the frame count requested by the command line.
 *
 * @param argc command-line argument count
 * @param argv command-line argument vector
 * @param fallback default frame count
 * @return bounded frame count
 */
static uint32_t _frame_count(int argc, char** argv, uint32_t fallback)
{
    const char* value = NULL;
    uint32_t out = 0;
    if (example_arg_value(argc, argv, "--frames", &value) && example_parse_u32(value, &out) &&
        out != 0)
        return out;

    out = example_frame_count_any(argc, argv);
    return out != 0 ? out : fallback;
}



/**
 * Print command-line usage.
 */
static void _print_usage(void)
{
    dvz_fprintf(stdout, "usage: video_export [--frames N] [--png]\n");
    dvz_fprintf(stdout, "  default      record video_export.mp4 with the app capture API\n");
    dvz_fprintf(stdout, "  --frames N   record N frames instead of the default 120\n");
    dvz_fprintf(stdout, "  --png        render one offscreen PNG smoke image\n");
}



/**
 * Fill the deterministic point scene at one time.
 *
 * @param state video-export state
 * @param t time in seconds
 */
static void _fill_points(VideoExportState* state, double t)
{
    if (state == NULL)
        return;

    for (uint32_t i = 0; i < TICK_COUNT; i++)
    {
        const float u = TICK_COUNT > 1 ? (float)i / (float)(TICK_COUNT - 1u) : 0.0f;
        state->positions[i][0] = -0.80f + 1.60f * u;
        state->positions[i][1] = -0.38f;
        state->positions[i][2] = 0.0f;
        state->diameters[i] = i == TICK_COUNT / 2u ? 18.0f : 12.0f;
        state->colors[i] = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_GRID);
    }

    const float phase = 0.12f * TAU * (float)t;
    const uint32_t cursor = POINT_COUNT - 1u;
    state->positions[cursor][0] = 0.78f * sinf(phase);
    state->positions[cursor][1] = 0.18f * cosf(0.65f * phase);
    state->positions[cursor][2] = 0.0f;
    state->diameters[cursor] = 58.0f + 10.0f * sinf(1.7f * phase);
    state->colors[cursor] = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
}



/**
 * Upload all retained point attributes.
 *
 * @param state video-export state
 * @return true on success
 */
static bool _upload_points(VideoExportState* state)
{
    if (state == NULL || state->point == NULL)
        return false;

    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position", .data = state->positions, .item_count = POINT_COUNT},
        {.attr_name = "color", .data = state->colors, .item_count = POINT_COUNT},
        {.attr_name = "diameter_px", .data = state->diameters, .item_count = POINT_COUNT},
    };
    return dvz_visual_set_data_many(state->point, updates, 3) == 0;
}



/**
 * Fill the final capture path for display.
 *
 * @param capture capture configuration
 * @param extension output file extension
 * @param out output path buffer
 * @param size output path buffer size
 */
static void _capture_path(
    const DvzAppCaptureConfig* capture, const char* extension, char* out, size_t size)
{
    if (capture == NULL || extension == NULL || out == NULL || size == 0)
        return;

    const char* directory =
        capture->directory != NULL && capture->directory[0] != '\0' ? capture->directory : ".";
    const char* basename =
        capture->basename != NULL && capture->basename[0] != '\0' ? capture->basename
                                                                  : "video_export";
    const size_t dir_len = strlen(directory);
    if (strcmp(directory, ".") == 0)
        dvz_snprintf(out, size, "./%s%s", basename, extension);
    else if (dir_len > 0 && directory[dir_len - 1] == '/')
        dvz_snprintf(out, size, "%s%s%s", directory, basename, extension);
    else
        dvz_snprintf(out, size, "%s/%s%s", directory, basename, extension);
}



/**
 * Print bounded video export progress.
 *
 * @param completed number of rendered frames
 * @param total total frame count
 */
static void _print_progress(uint32_t completed, uint32_t total)
{
    if (total == 0)
        return;

    const uint32_t filled = (uint32_t)(((uint64_t)completed * PROGRESS_WIDTH) / total);
    dvz_fprintf(stdout, "\rvideo_export: [");
    for (uint32_t i = 0; i < PROGRESS_WIDTH; i++)
        fputc(i < filled ? '#' : '.', stdout);
    dvz_fprintf(stdout, "] %u/%u frames", completed, total);
    fflush(stdout);
}



/**
 * Create the retained scene used by the video export.
 *
 * @param scene scene
 * @param state state to initialize
 * @return figure, or NULL on error
 */
static DvzFigure* _create_scene(DvzScene* scene, VideoExportState* state)
{
    if (scene == NULL || state == NULL)
        return NULL;

    DvzFigure* figure = dvz_figure(scene, WIDTH, HEIGHT, 0);
    DvzPanel* panel = figure != NULL ? dvz_panel_full(figure) : NULL;
    state->point = dvz_point(scene, 0);
    if (figure == NULL || panel == NULL || state->point == NULL)
        return NULL;
    example_graphite_cyan_set_panel_background(panel);

    _fill_points(state, 0.0);
    if (!_upload_points(state))
        return NULL;

    DvzPointStyleDesc style = dvz_point_style_desc();
    style.aspect = DVZ_SHAPE_ASPECT_FILLED;
    style.stroke_width_px = 0.0f;
    if (dvz_point_set_style(state->point, &style) != 0)
        return NULL;
    if (dvz_visual_set_depth_test(state->point, false) != 0)
        return NULL;
    if (dvz_panel_add_visual(panel, state->point, NULL) != 0)
        return NULL;
    return figure;
}



/**
 * Render one PNG smoke frame.
 *
 * @param view offscreen view
 * @param state video-export state
 * @param path PNG output path
 * @return true on success
 */
static bool _write_png(DvzView* view, VideoExportState* state, const char* path)
{
    if (view == NULL || state == NULL || path == NULL)
        return false;

    _fill_points(state, 0.75);
    if (!_upload_points(state))
        return false;
    if (dvz_view_render_once(view) != DVZ_CANVAS_FRAME_READY)
        return false;
    return dvz_view_capture_png(view, path) == 0;
}



/**
 * Record the deterministic animation as a video.
 *
 * @param view offscreen view
 * @param state video-export state
 * @param capture capture configuration
 * @param frame_count number of frames to record
 * @return true on success
 */
static bool _write_video(
    DvzView* view, VideoExportState* state, const DvzAppCaptureConfig* capture,
    uint32_t frame_count)
{
    if (view == NULL || state == NULL || capture == NULL || frame_count == 0)
        return false;

    if (dvz_view_capture_start(view, capture) != 0)
        return false;

    bool ok = true;
    _print_progress(0, frame_count);
    for (uint32_t frame = 0; frame < frame_count; frame++)
    {
        const double t = (double)frame / FPS;
        _fill_points(state, t);
        if (!_upload_points(state) || dvz_view_render_once(view) != DVZ_CANVAS_FRAME_READY)
        {
            ok = false;
            break;
        }
        _print_progress(frame + 1u, frame_count);
    }
    dvz_fprintf(stdout, "\n");
    return dvz_view_capture_stop(view) == 0 && ok;
}



/*************************************************************************************************/
/*  Main                                                                                         */
/*************************************************************************************************/

int main(int argc, char** argv)
{
    if (example_arg_has(argc, argv, "--help") || example_arg_has(argc, argv, "-h"))
    {
        _print_usage();
        return 0;
    }

    const bool png_mode = example_arg_has(argc, argv, "--png");
    const uint32_t frame_count = _frame_count(argc, argv, FRAME_COUNT);

    int ret = 1;
    DvzScene* scene = NULL;
    DvzApp* app = NULL;
    VideoExportState state = {0};

    scene = dvz_scene();
    EXAMPLE_CHECK(scene != NULL, "dvz_scene() failed");
    dvz_scene_set_clock_mode(scene, DVZ_SCENE_CLOCK_FIXED_STEP);
    dvz_scene_set_fps(scene, FPS);

    DvzFigure* figure = _create_scene(scene, &state);
    EXAMPLE_CHECK(figure != NULL, "failed to create video export scene");

    app = dvz_app(scene);
    EXAMPLE_CHECK(app != NULL, "dvz_app() failed (no GPU?)");

    DvzView* view = dvz_view_offscreen(app, figure, WIDTH, HEIGHT);
    EXAMPLE_CHECK(view != NULL, "dvz_view_offscreen() failed");

    if (png_mode)
    {
        char png_path[PATH_SIZE] = {0};
        example_outpath(argv[0], "video_export.png", png_path, sizeof(png_path));
        EXAMPLE_CHECK(_write_png(view, &state, png_path), "PNG smoke capture failed");
        dvz_fprintf(stdout, "video_export: wrote %s (%ux%u exact pixels)\n", png_path, WIDTH, HEIGHT);
    }
    else
    {
        DvzAppCaptureConfig capture = dvz_app_capture_config();
        capture.flags = DVZ_APP_CAPTURE_VIDEO;
        const char* capture_dir = getenv("DVZ_CAPTURE_DIR");
        const char* capture_basename = getenv("DVZ_CAPTURE_BASENAME");
        capture.directory =
            capture_dir != NULL && capture_dir[0] != '\0' ? capture_dir : ".";
        capture.basename = capture_basename != NULL && capture_basename[0] != '\0'
                               ? capture_basename
                               : "video_export";
        capture.fps = FPS;
        capture.video_capture_mode = DVZ_VIDEO_CAPTURE_CPU_READBACK;

        char video_path[PATH_SIZE] = {0};
        _capture_path(&capture, ".mp4", video_path, sizeof(video_path));
        EXAMPLE_CHECK(
            _write_video(view, &state, &capture, frame_count), "video capture failed");
        dvz_fprintf(
            stdout, "video_export: wrote %s (%u frames at %.0f FPS)\n", video_path, frame_count,
            FPS);
    }
    ret = 0;

cleanup:
    if (app != NULL)
        dvz_app_destroy(app);
    if (scene != NULL)
        dvz_scene_destroy(scene);
    return ret;
}
Example details
  • ID: runtime_video_export
  • Category: runtime
  • Lane: runtime
  • Status: experimental
  • Source: examples/c/runtime/video_export.c
  • Approved adaptation starter: yes
  • Browser support: Native only
  • Browser note: video export uses native app/canvas capture backends, including CPU readback and optional external/NVENC paths
  • Browser capability tags: native-capture
  • Validation: smoke+optional-video

Tags

app-capture, video, animation, offscreen

Data

Field Value
kind synthetic