diff --git a/.github/workflows/axoverlay2.yml b/.github/workflows/axoverlay2.yml new file mode 100644 index 00000000..6bf12f70 --- /dev/null +++ b/.github/workflows/axoverlay2.yml @@ -0,0 +1,34 @@ +name: Build axoverlay2 application +on: + workflow_dispatch: + push: + paths: + - 'axoverlay2/**' + - '!axoverlay2/README.md' + - '.github/workflows/axoverlay2.yml' +jobs: + test-app: + name: Test app + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + axis-os: ["12.10.68"] + arch: ["armv7hf", "aarch64"] + env: + EXREPO: acap-native-examples + EXNAME: axoverlay2 + steps: + - uses: actions/checkout@v6 + - uses: docker/setup-buildx-action@v3 + + - name: Build ${{ env.EXNAME }} application + env: + imagetag: ${{ env.EXREPO }}_${{ env.EXNAME }}:${{ matrix.arch }} + run: | + docker image rm -f $imagetag + cd $EXNAME + docker build --no-cache --tag $imagetag --build-arg ARCH=${{ matrix.arch }} . + docker cp $(docker create $imagetag):/opt/app ./build + cd .. + docker image rm -f $imagetag diff --git a/README.md b/README.md index 45c1f96d..57003008 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,12 @@ The examples are organized into logical groups to help you find the most relevan ### Graphical components +- [axoverlay2](./axoverlay2/) + - An example in C that illustrates how to draw icons as overlays in a stream using the modern + [Overlay API](https://developer.axis.com/acap/api#overlay2-API). - [axoverlay](./axoverlay/) - - An example in C that illustrates how to draw plain boxes and text as overlays in a stream. + - An example in C that illustrates how to draw plain boxes and text as overlays in a stream using + the [Axoverlay API (Legacy)](https://developer.axis.com/acap/api#axoverlay-api-legacy). - [bounding-box](./bounding-box/) - An example in C that demonstrates how to portably draw burnt-in bounding boxes on selected video sources or channels. - [vdo-opencl-filtering](./vdo-opencl-filtering/) diff --git a/axoverlay2/Dockerfile b/axoverlay2/Dockerfile new file mode 100644 index 00000000..8ad290eb --- /dev/null +++ b/axoverlay2/Dockerfile @@ -0,0 +1,12 @@ +ARG ARCH=armv7hf +ARG VERSION=12.10.0 +ARG UBUNTU_VERSION=24.04 +ARG REPO=axisecp +ARG SDK=acap-native-sdk + +FROM ${REPO}/${SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION} + +# Building the ACAP application +COPY ./app /opt/app/ +WORKDIR /opt/app +RUN . /opt/axis/acapsdk/environment-setup* && acap-build . diff --git a/axoverlay2/README.md b/axoverlay2/README.md new file mode 100644 index 00000000..09bc5982 --- /dev/null +++ b/axoverlay2/README.md @@ -0,0 +1,118 @@ +*Copyright (C) 2026, Axis Communications AB, Lund, Sweden. All Rights Reserved.* + +# ACAP application drawing video overlays + +This README file explains how to build an ACAP application that uses the axoverlay API, version 2.0. + +Together with this README file, you should be able to find a directory called app. That directory contains the "axoverlay2" application source code which can easily be compiled and run with the help of the tools and step by step below. + +For more information on Axoverlay2 API, please see the [Axis developer documentation](https://developer.axis.com/acap/4/api/#axoverlay2-api). + +This example illustrates how to draw overlays in a video stream and Cairo is used as rendering API, see [documentation](https://www.cairographics.org/). In this example, a rotating and color-shifting icon is drawn. + +## Getting started + +These instructions will guide you on how to execute the code. Below is the structure and scripts used in the example: + +```sh +axoverlay2 +├── app +│ ├── axoverlay2.c +│ ├── LICENSE +│ ├── Makefile +│ └── manifest.json +├── Dockerfile +└── README.md +``` + +- **app/axoverlay2.c** - Application to draw overlays using axoverlay 2.0 in C. +- **app/LICENSE** - Text file that lists all open source licensed source code distributed with the application. +- **app/Makefile** - Build and link instructions for the application. +- **app/manifest.json** - Defines the application and its configuration. +- **Dockerfile** - Assembles an image containing the ACAP Native SDK and builds the application using it. +- **README.md** - Step-by-step instructions on how to run the example. + +### Supported devices + +- ARTPEC-9, ARTPEC-8, and ARTPEC-7 based cameras and other video devices. + +## Build the application + +Standing in your working directory, run the following commands: + +> [!NOTE] +> +> Depending on the network your local build machine is connected to, you may need to add proxy +> settings for Docker. See +> [Proxy in build time](https://developer.axis.com/acap/develop/proxy/#proxy-in-build-time). + +```sh +docker build --platform=linux/amd64 --tag --build-arg ARCH= . +``` + +- `` is the name to tag the image with, e.g., `axoverlay2:1.0` +- `` is the architecture of the camera you are using, e.g., `armv7hf` (default) or `aarch64` + +Copy the result from the container image to a local directory called `build`: + +```sh +docker cp $(docker create --platform=linux/amd64 ):/opt/app ./build +``` + +The `build` directory contains the build artifacts, where the ACAP application +is found with suffix `.eap`, depending on which SDK architecture that was +chosen, one of these files should be found: + +- `Axoverlay2_example_application_1_0_0_aarch64.eap` +- `Axoverlay2_example_application_1_0_0_armv7hf.eap` + +> [!NOTE] +> +> For detailed information on how to build, install, and run ACAP applications, refer to the official ACAP documentation: [Build, install, and run](https://developer.axis.com/acap/develop/build-install-run/). + +## Install and start the application + +Browse to the application page of the Axis device: + +```sh +http:///index.html#apps +``` + +1. Click on the tab **Apps** in the device GUI +2. Enable the **Allow unsigned apps** toggle +3. Click the **(+ Add app)** button to upload the application file +4. Select the newly built application package, depending on architecture: + + - `Axoverlay2_example_application_1_0_0_aarch64.eap` + - `Axoverlay2_example_application_1_0_0_armv7hf.eap` + +5. Click **Install** +6. Run the application by enabling the **Start** switch. + +## Expected output + +A green smiley face superimposed on a picture of a roof + +While the application is running, a rotating and colour-shifting icon should appear in the top-left +corner of video streams. + +The application log can be found directly at: + +```sh +http:///axis-cgi/admin/systemlog.cgi?appname=axoverlay2 +``` + +During normal operation, the application prints log entries for each connecting and disconnecting +video stream: + +```text +axoverlay2[2095]: Created overlay 1 on stream 1041, stream_size=3840x2160 overlay_used_size=135x135 overlay_full_size=136x136 +axoverlay2[2095]: Removed overlay 1 from stream 1041 +``` + +In addition, the code contains some debug prints which may be enabled using the DEBUG flag found in +axoverlay2.c. + +## License + +**[MIT](../LICENSE)** diff --git a/axoverlay2/app/.gitignore b/axoverlay2/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/axoverlay2/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/axoverlay2/app/LICENSE b/axoverlay2/app/LICENSE new file mode 100644 index 00000000..bdb43d2c --- /dev/null +++ b/axoverlay2/app/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Axis Communications AB + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/axoverlay2/app/Makefile b/axoverlay2/app/Makefile new file mode 100644 index 00000000..2c7710eb --- /dev/null +++ b/axoverlay2/app/Makefile @@ -0,0 +1,30 @@ +PROG1 = $(shell jq -r '.acapPackageConf.setup.appName' manifest.json) + +PKGS = gio-2.0 glib-2.0 cairo vdostream axoverlay2 +CFLAGS += $(shell pkg-config --cflags $(PKGS)) +LDLIBS += $(shell pkg-config --libs $(PKGS)) -lm + +CFLAGS += -Wall \ + -Wextra \ + -Wformat=2 \ + -Wpointer-arith \ + -Wbad-function-cast \ + -Wstrict-prototypes \ + -Wmissing-prototypes \ + -Winline \ + -Wdisabled-optimization \ + -Wfloat-equal \ + -W \ + -Werror + +all: axoverlay2 + +$(PROG1): $(PROG1).c + mkdir -p debug + $(CC) $^ $(CFLAGS) $(LDFLAGS) $(LDLIBS) -o debug/$@ + cp debug/$@ . + $(STRIP) $@ + +.PHONY: clean +clean: + rm -rf $(PROG1) *.o *.eap* *_LICENSE.txt package.conf* param.conf tmp* debug diff --git a/axoverlay2/app/axoverlay2.c b/axoverlay2/app/axoverlay2.c new file mode 100644 index 00000000..a02766dc --- /dev/null +++ b/axoverlay2/app/axoverlay2.c @@ -0,0 +1,547 @@ +// Copyright (C) 2026 Axis Communications AB, Lund, Sweden +// Licensed under the MIT License. See LICENSE file for details. + +/** + * - axoverlay2 - + * + * This application demonstrates how the use the axoverlay API version 2.0. + * + * Here we use the GLib event loop and the Cairo graphics toolkit for demonstation purposes. In + * your application, you may use any event library and any graphics toolkit. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Record of an overlay that we own */ +struct overlay { + int overlay_id; /* Overlay ID number (from Axoverlay) */ + unsigned stream_id; /* Stream ID number (from VDO) */ + + /* The used portion of the overlay in pixels */ + unsigned used_width; + unsigned used_height; + + /* The total size of the overlay in pixels, including padding */ + unsigned full_width; + unsigned full_height; + + /* Cairo-related implementation details */ + cairo_surface_t* surface; +}; + +static void overlay_record_deleter(void* overlay_void); +static int signal_callback(void* userdata); +static int animation_tick_callback(void* userdata); +static int stream_event_callback(GIOChannel* channel, GIOCondition condition, void* userdata); +static void create_overlay(unsigned stream_id, unsigned stream_width, unsigned stream_height); +static void remove_overlay(unsigned stream_id); +static void process_next_frame(struct overlay* overlay); +static void render_frame(struct overlay* overlay, char* target_buffer); + +static VdoStream* vdo_event_stream; + +/* A table of all overlays we currently own. Key: int stream_id; Value: struct overlay * */ +static GHashTable* overlay_table; + +/* + * Parameters for overlay animation. In a real application we would probably use an external data + * source. Here we use a fixed 30 fps tick to advance our placeholder animation + */ +static unsigned animation_state; +static unsigned tick_period_us = 1000000 / 30; + +/* GLib main loop */ +static GMainLoop* main_loop; + +/* Enable debug logging? */ +static const bool debug = false; + +int main(void) { + GError* error = NULL; + axo_err* axo_error = NULL; + VdoMap* stream_filter = NULL; + bool axo_running = false; + GIOChannel* vdo_channel = NULL; + unsigned vdo_watch_id = 0; + int ret = 0; + + /* Start Axoverlay */ + if (!axo_start(NULL, &axo_error)) { + syslog(LOG_ERR, "Failed to start Axoverlay: %s", axo_err_get_message(axo_error)); + goto out; + } + + axo_running = true; + + /* Create overlay table */ + overlay_table = + g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, overlay_record_deleter); + + /* + * Set up a GLib main loop for our application. More external event sources may of course also + * be hooked into the same loop depending on the application's needs. + */ + main_loop = g_main_loop_new(NULL, FALSE); + + /* Set up a simple timer to advance the overlay animation (period in milliseconds) */ + g_timeout_add(tick_period_us / 1000, animation_tick_callback, NULL); + + /* + * Stream 0 in VDO is a magic pseudo-stream which is always present in the system. We can use + * stream 0 to get events about all other streams. + */ + vdo_event_stream = vdo_stream_get(0, &error); + if (!vdo_event_stream) { + syslog(LOG_ERR, "Failed to open vdo stream 0: %s", error->message); + goto out; + } + + /* + * Set up VDO filter to disregard streams that do not want overlays. Drawing overlays for a + * stream that will not use them is wasteful. + */ + stream_filter = vdo_map_new(); + vdo_map_set_string(stream_filter, "filter", "overlay"); + + if (!vdo_stream_attach(vdo_event_stream, stream_filter, &error)) { + syslog(LOG_ERR, "Failed to attach filter to vdo stream 0: %s", error->message); + goto out; + } + + int stream_event_fd = vdo_stream_get_event_fd(vdo_event_stream, &error); + if (stream_event_fd < 0) { + syslog(LOG_ERR, "Failed to get stream 0 event fd: %s", error->message); + goto out; + } + + /* Hook VDO stream event fd into our GLib event loop via a GIOChannel */ + vdo_channel = g_io_channel_unix_new(stream_event_fd); + vdo_watch_id = g_io_add_watch(vdo_channel, + G_IO_IN | G_IO_PRI | G_IO_ERR | G_IO_HUP, + stream_event_callback, + NULL); + if (!vdo_watch_id) { + syslog(LOG_ERR, "Failed to add stream event fd to event loop"); + goto out; + } + + /* Set up signal handling to gracefully stop the main loop */ + g_unix_signal_add(SIGINT, signal_callback, main_loop); + g_unix_signal_add(SIGTERM, signal_callback, main_loop); + + /* Enter main loop */ + g_main_loop_run(main_loop); + +out: + if (vdo_watch_id) + g_source_remove(vdo_watch_id); + if (vdo_channel) + g_io_channel_unref(vdo_channel); + if (axo_running) + axo_stop(NULL); + g_clear_object(&vdo_event_stream); + g_clear_object(&stream_filter); + g_clear_error(&error); + if (main_loop) + g_main_loop_unref(main_loop); + if (overlay_table) + g_hash_table_unref(overlay_table); + + return ret; +} + +static void overlay_record_deleter(void* overlay_void) { + struct overlay* overlay = overlay_void; + + cairo_surface_destroy(overlay->surface); + g_free(overlay); +} + +static int signal_callback(void* userdata) { + (void)userdata; + + g_main_loop_quit(main_loop); + return G_SOURCE_REMOVE; +} + +/* + * Callback issued at a regular interval. This is a placeholder, representing your application's + * external data sources that would trigger the overlay to be updated. + * + * Returning G_SOURCE_CONTINUE keeps the timer running. + */ +static int animation_tick_callback(void* userdata) { + (void)userdata; + + /* Move the animation forward to the next frame */ + animation_state++; + + /* Process next frame for each existing overlay */ + GHashTableIter iter; + g_hash_table_iter_init(&iter, overlay_table); + + void *key, *value; + while (g_hash_table_iter_next(&iter, &key, &value)) process_next_frame(value); + + return G_SOURCE_CONTINUE; +} + +/* + * Callback issued when VDO tells us there is a new stream event. We are interested in stream + * connection and disconnection events. + * + * Returning G_SOURCE_CONTINUE keeps the watch active; G_SOURCE_REMOVE tears it down. + */ +static int stream_event_callback(GIOChannel* channel, GIOCondition condition, void* userdata) { + (void)channel; + (void)userdata; + + GError* error = NULL; + VdoMap* vdo_event = NULL; + VdoStream* vdo_stream = NULL; + VdoMap* stream_info = NULL; + int ret = G_SOURCE_CONTINUE; + + if (condition & (G_IO_ERR | G_IO_HUP)) { + syslog(LOG_ERR, "Connection to vdo was broken, condition=0x%04x", condition); + g_main_loop_quit(main_loop); + ret = G_SOURCE_REMOVE; + goto out; + } + + vdo_event = vdo_stream_get_event(vdo_event_stream, &error); + if (!vdo_event) { + if (g_error_matches(error, VDO_ERROR, VDO_ERROR_NO_EVENT)) + goto out; + + syslog(LOG_ERR, "Failed to get vdo stream event: %s", error->message); + g_main_loop_quit(main_loop); + ret = G_SOURCE_REMOVE; + goto out; + } + + unsigned event_type = vdo_map_get_uint32(vdo_event, "event", 0); + unsigned stream_id = vdo_map_get_uint32(vdo_event, "id", 0); + + if (event_type == VDO_STREAM_EVENT_EXISTING || event_type == VDO_STREAM_EVENT_CREATED) { + /* + * A new stream or a stream which already existed at the time our application started. In + * this example we want to add one overlay to every such stream. + */ + vdo_stream = vdo_stream_get(stream_id, &error); + if (!vdo_stream) { + syslog(LOG_ERR, "Failed to get stream information from vdo: %s", error->message); + g_main_loop_quit(main_loop); + ret = G_SOURCE_REMOVE; + goto out; + } + + stream_info = vdo_stream_get_info(vdo_stream, NULL); + if (!stream_info) { + syslog(LOG_ERR, "Vdo stream is missing info"); + g_main_loop_quit(main_loop); + ret = G_SOURCE_REMOVE; + goto out; + } + + if (debug) + vdo_map_dump(stream_info); + + unsigned width = vdo_map_get_uint32(stream_info, "width", 0); + unsigned height = vdo_map_get_uint32(stream_info, "height", 0); + if (!width || !height) { + syslog(LOG_ERR, "Vdo reported invalid stream size %ux%u", width, height); + g_main_loop_quit(main_loop); + ret = G_SOURCE_REMOVE; + goto out; + } + + create_overlay(stream_id, width, height); + } else if (event_type == VDO_STREAM_EVENT_CLOSED) { + /* A stream closed down, so we need to clean up the overlay on that stream */ + remove_overlay(stream_id); + } + +out: + g_clear_error(&error); + g_clear_object(&vdo_event); + g_clear_object(&vdo_stream); + g_clear_object(&stream_info); + return ret; +} + +/* + * Create an overlay on the specified stream. + * + * Note that it is possible to create multiple overlays per stream, or we could filter out streams + * further based on the properties we get from VDO. In this example we just create one overlay on + * every stream. + */ +static void create_overlay(unsigned stream_id, unsigned stream_width, unsigned stream_height) { + axo_err* axo_error = NULL; + axo_props* props = NULL; + axo_match* match = NULL; + + /* In this example we scale the overlay to a given fraction of the stream size */ + unsigned overlay_size = MIN(stream_width, stream_height) / 8; + + /* + * For streams of high resolution, the overlay will become very big. This can require excessive + * memory and CPU/GPU time. + * + * In such cases it is useful to enable the built-in upscaling function. The upscaling function + * lets us draw in half resolution compared to what will be visible in the stream. + * + * Here we use a threshold of 4 megapixel for when to enable upscaling. + */ + bool use_upscale = stream_width * stream_height > 4000000; + + if (use_upscale) + overlay_size /= 2; + + unsigned overlay_used_width = overlay_size, overlay_used_height = overlay_size; + + /* + * It is important to ensure that the size is properly aligned. The calculations above can + * result in odd numbers and other dimensions which are not supported by the overlay system. + * This utility function shall always be used to calculate the required padding. + */ + unsigned overlay_full_width, overlay_full_height; + if (!axo_get_aligned_size(AXO_FORMAT_ARGB32, + overlay_used_width, + overlay_used_height, + &overlay_full_width, + &overlay_full_height, + &axo_error)) { + syslog(LOG_ERR, "Failed to get aligned overlay size: %s", axo_err_get_message(axo_error)); + goto out; + } + + /* Create the overlay */ + props = axo_props_new(); + axo_props_set_format(props, AXO_FORMAT_ARGB32); + axo_props_set_size(props, overlay_full_width, overlay_full_height); + axo_props_set_upscale_x2(props, use_upscale); + + match = axo_match_new(); + axo_match_stream_id(match, stream_id); + + int overlay_id = axo_create_overlay(props, match, &axo_error); + if (overlay_id < 0) { + /* + * It can happen that the stream closes down before we have time to create an overlay on + * it. This is not an error. The condition is indicated by a special code. In this case we + * will soon receive a disconnect event from VDO, so just ignore it and keep going. + */ + if (axo_err_get_code(axo_error) != AXO_ERR_NO_STREAM) + syslog(LOG_ERR, + "Failed to create overlay on stream %d: %s", + stream_id, + axo_err_get_message(axo_error)); + + goto out; + } + + syslog(LOG_INFO, + "Created overlay %u on stream %u, stream_size=%ux%u overlay_used_size=%ux%u " + "overlay_full_size=%ux%u", + overlay_id, + stream_id, + stream_width, + stream_height, + overlay_used_width, + overlay_used_height, + overlay_full_width, + overlay_full_height); + + /* Create a re-usable cairo surface. This is further explained in the render function */ + cairo_surface_t* surface = + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, overlay_full_width, overlay_full_height); + + /* Check that cairo didn't add any extra padding. This should never happen */ + assert(overlay_full_width * sizeof(uint32_t) == + (unsigned)cairo_image_surface_get_stride(surface)); + + /* Create and store a record of this overlay in the overlay table */ + struct overlay* overlay = g_malloc(sizeof(*overlay)); + *overlay = (struct overlay){ + .overlay_id = overlay_id, + .stream_id = stream_id, + .used_width = overlay_used_width, + .used_height = overlay_used_height, + .full_width = overlay_full_width, + .full_height = overlay_full_height, + .surface = surface, + }; + + g_hash_table_insert(overlay_table, GINT_TO_POINTER(stream_id), overlay); + + /* Process the first frame for this new overlay */ + process_next_frame(overlay); + +out: + axo_err_clear(&axo_error); + + if (props) + axo_props_free(props); + + if (match) + axo_match_free(match); +} + +/* + * Remove the overlay on the specified stream, if one existed. + */ +static void remove_overlay(unsigned stream_id) { + axo_err* axo_error = NULL; + const struct overlay* overlay = g_hash_table_lookup(overlay_table, GINT_TO_POINTER(stream_id)); + + if (!overlay) + goto out; + + if (!axo_remove_overlay(overlay->overlay_id, &axo_error)) { + syslog(LOG_ERR, + "Failed to remove overlay %u on stream %u: %s", + overlay->overlay_id, + stream_id, + axo_err_get_message(axo_error)); + goto out; + } + + syslog(LOG_INFO, "Removed overlay %u from stream %u", overlay->overlay_id, stream_id); + +out: + g_hash_table_remove(overlay_table, GINT_TO_POINTER(stream_id)); + + axo_err_clear(&axo_error); +} + +/* + * Draw and submit a new frame of animation for this overlay. + */ +static void process_next_frame(struct overlay* overlay) { + axo_err* axo_error = NULL; + + /* Get a free buffer to draw into from the overlay system */ + axo_buffer* buffer = axo_get_buffer(overlay->overlay_id, NULL, &axo_error); + if (!buffer) { + axo_err_code code = axo_err_get_code(axo_error); + + /* + * There are some situations where a buffer is not available within a reasonable time. This + * happens during normal camera usage. All clients must handle these conditions by checking + * for specific error codes. Please see the API documentation for axo_get_buffer. + */ + if (code == AXO_ERR_NO_STREAM || code == AXO_ERR_WAIT) + goto out; + + syslog(LOG_ERR, + "Failed to get buffer for overlay %u: %s", + overlay->overlay_id, + axo_err_get_message(axo_error)); + goto out; + } + + char* target_buffer = axo_buffer_get_data(buffer, &axo_error); + if (!target_buffer) { + syslog(LOG_ERR, "Failed to get buffer data: %s", axo_err_get_message(axo_error)); + goto out; + } + + /* Draw graphics into the buffer */ + render_frame(overlay, target_buffer); + + /* Submit the buffer to be shown on the stream */ + if (!axo_submit_buffer(buffer, NULL, &axo_error)) { + syslog(LOG_ERR, + "Failed to submit buffer for overlay %u: %s", + overlay->overlay_id, + axo_err_get_message(axo_error)); + goto out; + } + +out: + axo_err_clear(&axo_error); +} + +/* + * Render a frame of animation. This is just a placeholder for demo purposes. It should be replaced + * with your own rendering logic. + * + * In this example we use the Cairo graphics library. Another suitable graphics toolkit could be + * used instead. For details on how Cairo works, please consult the Cairo documentation: + * https://www.cairographics.org/manual/index.html. + * + * The are two things to keep in mind here: + * + * - It may not be possible to draw directly into the target buffer using a CPU-based toolkit like + * Cairo. Overlay buffers are a type of device memory and the caches may not be set up in a way + * that is compatible with direct drawing from the CPU. + * + * The solution to this is to draw into a separate buffer (overlay->surface here) and then copy + * the final result into the target buffer. For efficiency, a single surface can be re-used + * across frames. + * + * - Due to alignment there may be some extra padding pixels at the right and bottom sides of the + * buffer. We need to clear these pixels to transparency, but during the actual drawing we do not + * normally touch them. + */ +static void render_frame(struct overlay* overlay, char* target_buffer) { + cairo_t* cr = cairo_create(overlay->surface); + + /* Clear the separate buffer (all of it, not just the used area) */ + cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); + cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.0); + cairo_paint(cr); + cairo_set_operator(cr, CAIRO_OPERATOR_OVER); + + /* + * Rescale coordinates so that (1.0, 1.0) is the bottom-right edge of the used overlay area. + * The padding may extend slightly past this. + */ + cairo_scale(cr, overlay->used_width, overlay->used_height); + + /* Animate the overlay with a simple rotation around the centre of the used area */ + double t = M_PI * animation_state / 180.0; + + cairo_translate(cr, 0.5, 0.5); + cairo_rotate(cr, t); + cairo_translate(cr, -0.5, -0.5); + + /* Animate colour selection */ + double r = sin(t) * sin(t), g = cos(t) * cos(t); + + /* Draw an example icon */ + cairo_set_line_width(cr, 0.04); + cairo_set_source_rgb(cr, r, g, 0.0); + + cairo_arc(cr, 0.5, 0.5, 0.45, 0.0, 2.0 * M_PI); + cairo_stroke(cr); + + cairo_arc(cr, 0.4, 0.4, 0.05, 0.0, 2.0 * M_PI); + cairo_stroke(cr); + + cairo_arc(cr, 0.6, 0.4, 0.05, 0.0, 2.0 * M_PI); + cairo_stroke(cr); + + cairo_arc(cr, 0.5, 0.5, 0.30, 0.0, M_PI); + cairo_stroke(cr); + + /* Finish and copy the resulting image to the overlay target buffer */ + cairo_destroy(cr); + + unsigned byte_size = overlay->full_width * overlay->full_height * sizeof(uint32_t); + memcpy(target_buffer, cairo_image_surface_get_data(overlay->surface), byte_size); +} diff --git a/axoverlay2/app/manifest.json b/axoverlay2/app/manifest.json new file mode 100644 index 00000000..59361e66 --- /dev/null +++ b/axoverlay2/app/manifest.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": "2.0.0", + "resources": { + "overlay": { + "enabled": true, + "required": true + } + }, + "acapPackageConf": { + "setup": { + "friendlyName": "Axoverlay2 example application", + "appName": "axoverlay2", + "vendor": "Axis Communications", + "vendorId": "0123456789", + "vendorUrl": "https://www.axis.com", + "runMode": "never", + "version": "1.0.0", + "compatibleOsVersions": [ + { + "max": "13" + } + ] + } + } +} diff --git a/axoverlay2/screenshot.jpg b/axoverlay2/screenshot.jpg new file mode 100644 index 00000000..75d960dd Binary files /dev/null and b/axoverlay2/screenshot.jpg differ