diff --git a/makefile b/makefile index 6fd70a0..8cc4d20 100644 --- a/makefile +++ b/makefile @@ -1,25 +1,25 @@ -CC = gcc -CFLAGS = -Wall -Werror -pthread -O3 -LDFLAGS = -pthread -LDLIBS = -ldl -lwebpdemux - -SOURCE = ./source -BUILD = ./build -TARGET = $(BUILD)/panelplayer - -HEADERS = $(wildcard $(SOURCE)/*.h) -OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(wildcard $(SOURCE)/*.c)) - -.PHONY: clean - -$(TARGET): $(BUILD) $(OBJECTS) - $(CC) $(LDFLAGS) $(OBJECTS) $(LDLIBS) -o $@ - -$(BUILD): - mkdir $(BUILD) - -$(BUILD)/%.o: $(SOURCE)/%.c $(HEADERS) makefile - $(CC) $(CFLAGS) -c $< -o $@ - -clean: +CC = gcc +CFLAGS = -Wall -Werror -pthread -O3 +LDFLAGS = -pthread +LDLIBS = -ldl -lwebpdemux -ljpeg -lpng -lgif + +SOURCE = ./source +BUILD = ./build +TARGET = $(BUILD)/panelplayer + +HEADERS = $(wildcard $(SOURCE)/*.h) +OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(wildcard $(SOURCE)/*.c)) + +.PHONY: clean + +$(TARGET): $(BUILD) $(OBJECTS) + $(CC) $(LDFLAGS) $(OBJECTS) $(LDLIBS) -o $@ + +$(BUILD): + mkdir $(BUILD) + +$(BUILD)/%.o: $(SOURCE)/%.c $(HEADERS) makefile + $(CC) $(CFLAGS) -c $< -o $@ + +clean: rm -r $(BUILD) \ No newline at end of file diff --git a/readme.md b/readme.md index 8866e5e..ef62ceb 100644 --- a/readme.md +++ b/readme.md @@ -1,41 +1,46 @@ -# PanelPlayer -A [WebP](https://developers.google.com/speed/webp) player for Colorlight receiving cards. Tested with the Colorlight 5A-75B. - -## Usage -PanelPlayer can be launched with `panelplayer ` where `` is one or more WebP files. The available options are: - -### `-p ` -Sets which ethernet port to use for sending. This option is required. - -### `-w ` -Set the display width in pixels. This option is required. - -### `-h ` -Set the display height in pixels. This option is required. - -### `-b ` -Set the display brightness between 0 and 255. A value of 255 will be used if not specified. - -### `-m ` -Controls the percentage of the previous frame to be blended with the current frame. Frame blending is disabled when set to 0 or not specified. - -### `-r ` -Overrides the source frame rate if specified. - -### `-e ` -Load an extension from the path given. Only a single extension can be loaded. - -### `-s` -Play sources randomly instead of in a fixed order. If used with a single source, this option will loop playback. - -### `-v` -Enable verbose output. - -## Building -Ensure `libwebp` is installed. PanelPlayer can be built by running `make` from within the root directory. - -## Extensions -Extensions are a way to read or alter frames without modifying PanelPlayer. A minimal extension consists of an `update` function which gets called before each frame is sent. An extension may also include `init` and `destroy` functions. The `destroy` function will always be called if present, even when the `init` function indicates an error has occurred. Example extensions are located in the `extensions` directory. - -## Protocol +# PanelPlayer +A media player for Colorlight receiving cards supporting WebP, JPEG, PNG, GIF, and BMP formats. Tested with the Colorlight 5A-75B. + +## Usage +PanelPlayer can be launched with `panelplayer ` where `` is one or more image/animation files (WebP, JPEG, PNG, GIF, or BMP). The available options are: + +### `-p ` +Sets which ethernet port to use for sending. This option is required. + +### `-w ` +Set the display width in pixels. This option is required. + +### `-h ` +Set the display height in pixels. This option is required. + +### `-b ` +Set the display brightness between 0 and 255. A value of 255 will be used if not specified. + +### `-m ` +Controls the percentage of the previous frame to be blended with the current frame. Frame blending is disabled when set to 0 or not specified. + +### `-r ` +Overrides the source frame rate if specified. + +### `-e ` +Load an extension from the path given. Only a single extension can be loaded. + +### `-s` +Play sources randomly instead of in a fixed order. If used with a single source, this option will loop playback. + +### `-v` +Enable verbose output. + +## Building +Install the required development libraries: +```bash +sudo apt install libwebp-dev libjpeg-dev libpng-dev libgif-dev +``` + +PanelPlayer can be built by running `make` from within the root directory. + +## Extensions +Extensions are a way to read or alter frames without modifying PanelPlayer. A minimal extension consists of an `update` function which gets called before each frame is sent. An extension may also include `init` and `destroy` functions. The `destroy` function will always be called if present, even when the `init` function indicates an error has occurred. Example extensions are located in the `extensions` directory. + +## Protocol Protocol documentation can be found in the `protocol` directory. A Wireshark plugin is included to help with reverse engineering and debugging. \ No newline at end of file diff --git a/source/decoder.c b/source/decoder.c new file mode 100644 index 0000000..8e886b6 --- /dev/null +++ b/source/decoder.c @@ -0,0 +1,510 @@ +#include +#include +#include + +#include +#include +#include +#include + +#include "decoder.h" + +typedef enum +{ + FORMAT_UNKNOWN, + FORMAT_WEBP, + FORMAT_JPEG, + FORMAT_PNG, + FORMAT_GIF, + FORMAT_BMP +} decoder_format; + +typedef struct decoder +{ + decoder_format format; + void *data; + int size; + + union + { + struct + { + WebPData webp_data; + WebPAnimDecoder *webp_decoder; + }; + + struct + { + uint8_t *frame_data; + int frame_width; + int frame_height; + bool frame_consumed; + }; + + struct + { + GifFileType *gif_file; + int gif_current_frame; + int gif_frame_count; + uint8_t *gif_frame_data; + int gif_last_timestamp; + }; + }; +} decoder; + +static decoder_format detect_format(void *data, int size) +{ + if (size < 12) + { + return FORMAT_UNKNOWN; + } + + uint8_t *bytes = data; + + if (bytes[0] == 'R' && bytes[1] == 'I' && bytes[2] == 'F' && bytes[3] == 'F' && + bytes[8] == 'W' && bytes[9] == 'E' && bytes[10] == 'B' && bytes[11] == 'P') + { + return FORMAT_WEBP; + } + + if (bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF) + { + return FORMAT_JPEG; + } + + if (bytes[0] == 0x89 && bytes[1] == 'P' && bytes[2] == 'N' && bytes[3] == 'G') + { + return FORMAT_PNG; + } + + if (bytes[0] == 'G' && bytes[1] == 'I' && bytes[2] == 'F') + { + return FORMAT_GIF; + } + + if (bytes[0] == 'B' && bytes[1] == 'M') + { + return FORMAT_BMP; + } + + return FORMAT_UNKNOWN; +} + +static int gif_read_func(GifFileType *gif, GifByteType *buf, int size) +{ + decoder *instance = gif->UserData; + + static int position = 0; + + if (position + size > instance->size) + { + size = instance->size - position; + } + + if (size <= 0) + { + return 0; + } + + memcpy(buf, (uint8_t *)instance->data + position, size); + position += size; + + return size; +} + +decoder *decoder_init(void *data, int size) +{ + decoder *instance; + + if ((instance = calloc(1, sizeof(*instance))) == NULL) + { + perror("Failed to allocate memory for decoder"); + return NULL; + } + + instance->data = data; + instance->size = size; + instance->format = detect_format(data, size); + + switch (instance->format) + { + case FORMAT_WEBP: + instance->webp_data.bytes = data; + instance->webp_data.size = size; + + if ((instance->webp_decoder = WebPAnimDecoderNew(&instance->webp_data, NULL)) == NULL) + { + goto free_instance; + } + break; + + case FORMAT_JPEG: + { + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, data, size); + jpeg_read_header(&cinfo, TRUE); + + cinfo.out_color_space = JCS_EXT_RGBA; + jpeg_start_decompress(&cinfo); + + instance->frame_width = cinfo.output_width; + instance->frame_height = cinfo.output_height; + + if ((instance->frame_data = malloc(instance->frame_width * instance->frame_height * 4)) == NULL) + { + jpeg_destroy_decompress(&cinfo); + goto free_instance; + } + + int row_stride = cinfo.output_width * 4; + + while (cinfo.output_scanline < cinfo.output_height) + { + uint8_t *row = instance->frame_data + cinfo.output_scanline * row_stride; + jpeg_read_scanlines(&cinfo, &row, 1); + } + + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + instance->frame_consumed = false; + break; + } + + case FORMAT_PNG: + { + png_image image; + memset(&image, 0, sizeof(image)); + image.version = PNG_IMAGE_VERSION; + + if (png_image_begin_read_from_memory(&image, data, size) == 0) + { + goto free_instance; + } + + image.format = PNG_FORMAT_RGBA; + + instance->frame_width = image.width; + instance->frame_height = image.height; + + if ((instance->frame_data = malloc(PNG_IMAGE_SIZE(image))) == NULL) + { + png_image_free(&image); + goto free_instance; + } + + if (png_image_finish_read(&image, NULL, instance->frame_data, 0, NULL) == 0) + { + free(instance->frame_data); + png_image_free(&image); + goto free_instance; + } + + png_image_free(&image); + instance->frame_consumed = false; + break; + } + + case FORMAT_GIF: + { + int error; + + if ((instance->gif_file = DGifOpen(instance, gif_read_func, &error)) == NULL) + { + goto free_instance; + } + + if (DGifSlurp(instance->gif_file) == GIF_ERROR) + { + DGifCloseFile(instance->gif_file, NULL); + goto free_instance; + } + + instance->gif_frame_count = instance->gif_file->ImageCount; + instance->gif_current_frame = 0; + + if ((instance->gif_frame_data = malloc(instance->gif_file->SWidth * instance->gif_file->SHeight * 4)) == NULL) + { + DGifCloseFile(instance->gif_file, NULL); + goto free_instance; + } + + memset(instance->gif_frame_data, 0, instance->gif_file->SWidth * instance->gif_file->SHeight * 4); + instance->gif_last_timestamp = 0; + break; + } + + case FORMAT_BMP: + { + uint8_t *bytes = data; + + if (size < 54) + { + goto free_instance; + } + + int offset = bytes[10] | (bytes[11] << 8) | (bytes[12] << 16) | (bytes[13] << 24); + int header_size = bytes[14] | (bytes[15] << 8) | (bytes[16] << 16) | (bytes[17] << 24); + + if (header_size < 40) + { + goto free_instance; + } + + instance->frame_width = bytes[18] | (bytes[19] << 8) | (bytes[20] << 16) | (bytes[21] << 24); + instance->frame_height = bytes[22] | (bytes[23] << 8) | (bytes[24] << 16) | (bytes[25] << 24); + int bits_per_pixel = bytes[28] | (bytes[29] << 8); + int compression = bytes[30] | (bytes[31] << 8) | (bytes[32] << 16) | (bytes[33] << 24); + + if (compression != 0 || (bits_per_pixel != 24 && bits_per_pixel != 32)) + { + goto free_instance; + } + + bool bottom_up = instance->frame_height > 0; + if (instance->frame_height < 0) + { + instance->frame_height = -instance->frame_height; + } + + if ((instance->frame_data = malloc(instance->frame_width * instance->frame_height * 4)) == NULL) + { + goto free_instance; + } + + int row_size = ((bits_per_pixel * instance->frame_width + 31) / 32) * 4; + uint8_t *src = bytes + offset; + + for (int y = 0; y < instance->frame_height; y++) + { + int dest_y = bottom_up ? (instance->frame_height - 1 - y) : y; + uint8_t *row = src + y * row_size; + + for (int x = 0; x < instance->frame_width; x++) + { + int dest_index = (dest_y * instance->frame_width + x) * 4; + int src_index = x * (bits_per_pixel / 8); + + instance->frame_data[dest_index + 0] = row[src_index + 2]; + instance->frame_data[dest_index + 1] = row[src_index + 1]; + instance->frame_data[dest_index + 2] = row[src_index + 0]; + instance->frame_data[dest_index + 3] = (bits_per_pixel == 32) ? row[src_index + 3] : 255; + } + } + + instance->frame_consumed = false; + break; + } + + default: + goto free_instance; + } + + return instance; + +free_instance: + free(instance); + return NULL; +} + +bool decoder_get_info(decoder *instance, decoder_info *info) +{ + if (instance == NULL || info == NULL) + { + return false; + } + + switch (instance->format) + { + case FORMAT_WEBP: + { + WebPAnimInfo webp_info; + WebPAnimDecoderGetInfo(instance->webp_decoder, &webp_info); + + info->frame_count = webp_info.frame_count; + info->canvas_width = webp_info.canvas_width; + info->canvas_height = webp_info.canvas_height; + break; + } + + case FORMAT_JPEG: + case FORMAT_PNG: + case FORMAT_BMP: + info->frame_count = 1; + info->canvas_width = instance->frame_width; + info->canvas_height = instance->frame_height; + break; + + case FORMAT_GIF: + info->frame_count = instance->gif_frame_count; + info->canvas_width = instance->gif_file->SWidth; + info->canvas_height = instance->gif_file->SHeight; + break; + + default: + return false; + } + + return true; +} + +bool decoder_has_more_frames(decoder *instance) +{ + if (instance == NULL) + { + return false; + } + + switch (instance->format) + { + case FORMAT_WEBP: + return WebPAnimDecoderHasMoreFrames(instance->webp_decoder); + + case FORMAT_JPEG: + case FORMAT_PNG: + case FORMAT_BMP: + return !instance->frame_consumed; + + case FORMAT_GIF: + return instance->gif_current_frame < instance->gif_frame_count; + + default: + return false; + } +} + +bool decoder_get_next(decoder *instance, uint8_t **frame, int *timestamp) +{ + if (instance == NULL || frame == NULL || timestamp == NULL) + { + return false; + } + + switch (instance->format) + { + case FORMAT_WEBP: + return WebPAnimDecoderGetNext(instance->webp_decoder, frame, timestamp); + + case FORMAT_JPEG: + case FORMAT_PNG: + case FORMAT_BMP: + if (instance->frame_consumed) + { + return false; + } + + *frame = instance->frame_data; + *timestamp = 0; + instance->frame_consumed = true; + return true; + + case FORMAT_GIF: + { + if (instance->gif_current_frame >= instance->gif_frame_count) + { + return false; + } + + SavedImage *image = &instance->gif_file->SavedImages[instance->gif_current_frame]; + ColorMapObject *colormap = image->ImageDesc.ColorMap != NULL ? image->ImageDesc.ColorMap : instance->gif_file->SColorMap; + + if (colormap == NULL) + { + return false; + } + + int left = image->ImageDesc.Left; + int top = image->ImageDesc.Top; + int width = image->ImageDesc.Width; + int height = image->ImageDesc.Height; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int gif_index = y * width + x; + int frame_index = ((top + y) * instance->gif_file->SWidth + (left + x)) * 4; + + uint8_t color_index = image->RasterBits[gif_index]; + GifColorType color = colormap->Colors[color_index]; + + int transparent = -1; + for (int i = 0; i < image->ExtensionBlockCount; i++) + { + if (image->ExtensionBlocks[i].Function == GRAPHICS_EXT_FUNC_CODE) + { + uint8_t *ext = image->ExtensionBlocks[i].Bytes; + if (ext[0] & 0x01) + { + transparent = ext[3]; + } + } + } + + if (color_index != transparent) + { + instance->gif_frame_data[frame_index + 0] = color.Red; + instance->gif_frame_data[frame_index + 1] = color.Green; + instance->gif_frame_data[frame_index + 2] = color.Blue; + instance->gif_frame_data[frame_index + 3] = 255; + } + } + } + + int delay = 100; + for (int i = 0; i < image->ExtensionBlockCount; i++) + { + if (image->ExtensionBlocks[i].Function == GRAPHICS_EXT_FUNC_CODE) + { + uint8_t *ext = image->ExtensionBlocks[i].Bytes; + delay = (ext[2] << 8) | ext[1]; + delay *= 10; + } + } + + *frame = instance->gif_frame_data; + *timestamp = instance->gif_last_timestamp + delay; + instance->gif_last_timestamp = *timestamp; + instance->gif_current_frame++; + + return true; + } + + default: + return false; + } +} + +void decoder_destroy(decoder *instance) +{ + if (instance == NULL) + { + return; + } + + switch (instance->format) + { + case FORMAT_WEBP: + WebPAnimDecoderDelete(instance->webp_decoder); + break; + + case FORMAT_JPEG: + case FORMAT_PNG: + case FORMAT_BMP: + free(instance->frame_data); + break; + + case FORMAT_GIF: + free(instance->gif_frame_data); + DGifCloseFile(instance->gif_file, NULL); + break; + + default: + break; + } + + free(instance); +} diff --git a/source/decoder.h b/source/decoder.h new file mode 100644 index 0000000..a44722c --- /dev/null +++ b/source/decoder.h @@ -0,0 +1,22 @@ +#ifndef DECODER_H +#define DECODER_H + +#include +#include + +typedef struct decoder decoder; + +typedef struct decoder_info +{ + int frame_count; + int canvas_width; + int canvas_height; +} decoder_info; + +decoder *decoder_init(void *data, int size); +bool decoder_get_info(decoder *instance, decoder_info *info); +bool decoder_has_more_frames(decoder *instance); +bool decoder_get_next(decoder *instance, uint8_t **frame, int *timestamp); +void decoder_destroy(decoder *instance); + +#endif diff --git a/source/main.c b/source/main.c index 62f1a1d..0f77c63 100644 --- a/source/main.c +++ b/source/main.c @@ -1,383 +1,377 @@ -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "colorlight.h" -#include "loader.h" - -#define QUEUE_SIZE 4 -#define MIX_MAXIMUM 100 -#define UPDATE_DELAY 10 - -bool parse(const char *source, int *destination) -{ - char *end; - *destination = strtol(source, &end, 10); - return end[0] != 0; -} - -long get_time() -{ - struct timespec time; - clock_gettime(CLOCK_MONOTONIC, &time); - return time.tv_sec * 1000 + time.tv_nsec / 1000000; -} - -void await(long time) -{ - long delay = time - get_time(); - - if (delay > 0) - { - usleep(delay * 1000); - } -} - -int main(int argc, char *argv[]) -{ - int status = EXIT_FAILURE; - char *port = NULL; - int width = 0; - int height = 0; - int brightness = 255; - int mix = 0; - int rate = 0; - char *extensionFile = NULL; - bool shuffle = false; - bool verbose = false; - int sourcesLength = 0; - char **sources; - - srand(time(NULL)); - - if ((sources = malloc((argc - 1) * sizeof(*sources))) == NULL) - { - perror("Failed to allocate memory for sources"); - goto exit; - } - - for (int index = 1; index < argc; index++) - { - char *argument = argv[index]; - - if (argument[0] != '-') - { - sources[sourcesLength++] = argument; - continue; - } - - bool failed = false; - - switch (argument[1]) - { - case 'p': - failed = ++index >= argc; - port = argv[index]; - break; - - case 'w': - failed = ++index >= argc || parse(argv[index], &width); - break; - - case 'h': - failed = ++index >= argc || parse(argv[index], &height); - break; - - case 'b': - failed = ++index >= argc || parse(argv[index], &brightness); - break; - - case 'm': - failed = ++index >= argc || parse(argv[index], &mix); - break; - - case 'r': - failed = ++index >= argc || parse(argv[index], &rate); - break; - - case 'e': - failed = ++index >= argc; - extensionFile = argv[index]; - break; - - case 's': - shuffle = true; - break; - - case 'v': - verbose = true; - break; - - default: - failed = true; - } - - if (failed || argument[2] != 0) - { - puts("Usage:"); - puts(" panelplayer -p -w -h [options] "); - puts(""); - puts("Options:"); - puts(" -p Set ethernet port"); - puts(" -w Set display width"); - puts(" -h Set display height"); - puts(" -b Set display brightness"); - puts(" -m Set frame mixing percentage"); - puts(" -r Override source frame rate"); - puts(" -e Load extension from file"); - puts(" -s Shuffle sources"); - puts(" -v Enable verbose output"); - - goto free_sources; - } - } - - if (port == NULL) - { - puts("Port must be specified!"); - goto free_sources; - } - - if (width < 1 || height < 1) - { - puts("Width and height must be specified as positive integers!"); - goto free_sources; - } - - if (brightness < 0 || brightness > 255) - { - puts("Brightness must be an integer between 0 and 255!"); - goto free_sources; - } - - if (mix < 0 || mix >= MIX_MAXIMUM) - { - printf("Mix must be an integer between 0 and %d!\n", MIX_MAXIMUM - 1); - goto free_sources; - } - - if (sourcesLength == 0) - { - puts("At least one source must be specified!"); - goto free_sources; - } - - uint8_t *buffer; - - if ((buffer = malloc(width * height * 3)) == NULL) - { - perror("Failed to allocate frame buffer"); - goto free_sources; - } - - loader *loader; - - if ((loader = loader_init(QUEUE_SIZE)) == NULL) - { - puts("Failed to create loader instance!"); - goto free_buffer; - } - - colorlight *colorlight; - - if ((colorlight = colorlight_init(port)) == NULL) - { - puts("Failed to create Colorlight instance!"); - goto destroy_loader; - } - - void *extension = NULL; - void (*update)() = NULL; - - if (extensionFile != NULL) - { - extension = dlopen(extensionFile, RTLD_NOW); - - if (extension == NULL) - { - puts("Failed to load extension!"); - goto destroy_colorlight; - } - - bool (*init)() = dlsym(extension, "init"); - - if (init != NULL) - { - if (init()) - { - puts("Failed to initialise extension!"); - goto destroy_extension; - } - } - - update = dlsym(extension, "update"); - - if (update == NULL) - { - puts("Extension does not provide update function!"); - goto destroy_extension; - } - } - - int queued = 0; - long next = get_time(); - bool initial = true; - - for (int source = 0; shuffle || source < sourcesLength; source++) - { - while (queued < source + QUEUE_SIZE) - { - if (shuffle) - { - loader_add(loader, sources[rand() % sourcesLength]); - } - else if (queued < sourcesLength) - { - loader_add(loader, sources[queued]); - } - - queued++; - } - - int size; - void *file; - - if ((file = loader_get(loader, &size)) == NULL) - { - continue; - } - - WebPData data = { - .bytes = file, - .size = size - }; - - WebPAnimDecoder *decoder; - - if ((decoder = WebPAnimDecoderNew(&data, NULL)) == NULL) - { - puts("Failed to decode file!"); - goto free_file; - } - - WebPAnimInfo info; - WebPAnimDecoderGetInfo(decoder, &info); - - if (verbose) - { - printf("Decoding %d frames at a resolution of %dx%d.\n", info.frame_count, info.canvas_width, info.canvas_height); - } - - if (info.canvas_width < width || info.canvas_height < height) - { - puts("Image is smaller than display!"); - goto delete_decoder; - } - - int previous = 0; - long start = next; - - while (WebPAnimDecoderHasMoreFrames(decoder)) - { - uint8_t *decoded; - int timestamp; - - WebPAnimDecoderGetNext(decoder, &decoded, ×tamp); - - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - int source = (y * info.canvas_width + x) * 4; - int destination = (y * width + x) * 3; - - int oldFactor = initial ? 0 : mix; - int newFactor = MIX_MAXIMUM - oldFactor; - - buffer[destination] = (buffer[destination] * oldFactor + decoded[source + 2] * newFactor) / MIX_MAXIMUM; - buffer[destination + 1] = (buffer[destination + 1] * oldFactor + decoded[source + 1] * newFactor) / MIX_MAXIMUM; - buffer[destination + 2] = (buffer[destination + 2] * oldFactor + decoded[source] * newFactor) / MIX_MAXIMUM; - } - } - - if (update != NULL) - { - update(width, height, buffer); - } - - for (int y = 0; y < height; y++) - { - colorlight_send_row(colorlight, y, width, buffer + y * width * 3); - } - - if (next - get_time() < UPDATE_DELAY) - { - next = get_time() + UPDATE_DELAY; - } - - await(next); - colorlight_send_update(colorlight, brightness, brightness, brightness); - - if (rate > 0) - { - next = get_time() + 1000 / rate; - } - else - { - next = get_time() + timestamp - previous; - previous = timestamp; - } - - initial = false; - } - - if (verbose) - { - float seconds = (next - start) / 1000.0; - printf("Played %d frames in %.2f seconds at an average rate of %.2f frames per second.\n", info.frame_count, seconds, info.frame_count / seconds); - } - - delete_decoder: - WebPAnimDecoderDelete(decoder); - - free_file: - free(file); - } - - await(next); - status = EXIT_SUCCESS; - -destroy_extension: - if (extension != NULL) - { - void (*destroy)() = dlsym(extension, "destroy"); - - if (destroy != NULL) - { - destroy(); - } - - dlclose(extension); - } - -destroy_colorlight: - colorlight_destroy(colorlight); - -destroy_loader: - loader_destroy(loader); - -free_buffer: - free(buffer); - -free_sources: - free(sources); - -exit: - return status; +#include +#include +#include +#include +#include +#include +#include + +#include "colorlight.h" +#include "decoder.h" +#include "loader.h" + +#define QUEUE_SIZE 4 +#define MIX_MAXIMUM 100 +#define UPDATE_DELAY 10 + +bool parse(const char *source, int *destination) +{ + char *end; + *destination = strtol(source, &end, 10); + return end[0] != 0; +} + +long get_time() +{ + struct timespec time; + clock_gettime(CLOCK_MONOTONIC, &time); + return time.tv_sec * 1000 + time.tv_nsec / 1000000; +} + +void await(long time) +{ + long delay = time - get_time(); + + if (delay > 0) + { + usleep(delay * 1000); + } +} + +int main(int argc, char *argv[]) +{ + int status = EXIT_FAILURE; + char *port = NULL; + int width = 0; + int height = 0; + int brightness = 255; + int mix = 0; + int rate = 0; + char *extensionFile = NULL; + bool shuffle = false; + bool verbose = false; + int sourcesLength = 0; + char **sources; + + srand(time(NULL)); + + if ((sources = malloc((argc - 1) * sizeof(*sources))) == NULL) + { + perror("Failed to allocate memory for sources"); + goto exit; + } + + for (int index = 1; index < argc; index++) + { + char *argument = argv[index]; + + if (argument[0] != '-') + { + sources[sourcesLength++] = argument; + continue; + } + + bool failed = false; + + switch (argument[1]) + { + case 'p': + failed = ++index >= argc; + port = argv[index]; + break; + + case 'w': + failed = ++index >= argc || parse(argv[index], &width); + break; + + case 'h': + failed = ++index >= argc || parse(argv[index], &height); + break; + + case 'b': + failed = ++index >= argc || parse(argv[index], &brightness); + break; + + case 'm': + failed = ++index >= argc || parse(argv[index], &mix); + break; + + case 'r': + failed = ++index >= argc || parse(argv[index], &rate); + break; + + case 'e': + failed = ++index >= argc; + extensionFile = argv[index]; + break; + + case 's': + shuffle = true; + break; + + case 'v': + verbose = true; + break; + + default: + failed = true; + } + + if (failed || argument[2] != 0) + { + puts("Usage:"); + puts(" panelplayer -p -w -h [options] "); + puts(""); + puts("Options:"); + puts(" -p Set ethernet port"); + puts(" -w Set display width"); + puts(" -h Set display height"); + puts(" -b Set display brightness"); + puts(" -m Set frame mixing percentage"); + puts(" -r Override source frame rate"); + puts(" -e Load extension from file"); + puts(" -s Shuffle sources"); + puts(" -v Enable verbose output"); + + goto free_sources; + } + } + + if (port == NULL) + { + puts("Port must be specified!"); + goto free_sources; + } + + if (width < 1 || height < 1) + { + puts("Width and height must be specified as positive integers!"); + goto free_sources; + } + + if (brightness < 0 || brightness > 255) + { + puts("Brightness must be an integer between 0 and 255!"); + goto free_sources; + } + + if (mix < 0 || mix >= MIX_MAXIMUM) + { + printf("Mix must be an integer between 0 and %d!\n", MIX_MAXIMUM - 1); + goto free_sources; + } + + if (sourcesLength == 0) + { + puts("At least one source must be specified!"); + goto free_sources; + } + + uint8_t *buffer; + + if ((buffer = malloc(width * height * 3)) == NULL) + { + perror("Failed to allocate frame buffer"); + goto free_sources; + } + + loader *loader; + + if ((loader = loader_init(QUEUE_SIZE)) == NULL) + { + puts("Failed to create loader instance!"); + goto free_buffer; + } + + colorlight *colorlight; + + if ((colorlight = colorlight_init(port)) == NULL) + { + puts("Failed to create Colorlight instance!"); + goto destroy_loader; + } + + void *extension = NULL; + void (*update)() = NULL; + + if (extensionFile != NULL) + { + extension = dlopen(extensionFile, RTLD_NOW); + + if (extension == NULL) + { + puts("Failed to load extension!"); + goto destroy_colorlight; + } + + bool (*init)() = dlsym(extension, "init"); + + if (init != NULL) + { + if (init()) + { + puts("Failed to initialise extension!"); + goto destroy_extension; + } + } + + update = dlsym(extension, "update"); + + if (update == NULL) + { + puts("Extension does not provide update function!"); + goto destroy_extension; + } + } + + int queued = 0; + long next = get_time(); + bool initial = true; + + for (int source = 0; shuffle || source < sourcesLength; source++) + { + while (queued < source + QUEUE_SIZE) + { + if (shuffle) + { + loader_add(loader, sources[rand() % sourcesLength]); + } + else if (queued < sourcesLength) + { + loader_add(loader, sources[queued]); + } + + queued++; + } + + int size; + void *file; + + if ((file = loader_get(loader, &size)) == NULL) + { + continue; + } + + decoder *decoder; + + if ((decoder = decoder_init(file, size)) == NULL) + { + puts("Failed to decode file!"); + goto free_file; + } + + decoder_info info; + decoder_get_info(decoder, &info); + + if (verbose) + { + printf("Decoding %d frames at a resolution of %dx%d.\n", info.frame_count, info.canvas_width, info.canvas_height); + } + + if (info.canvas_width < width || info.canvas_height < height) + { + puts("Image is smaller than display!"); + goto delete_decoder; + } + + int previous = 0; + long start = next; + + while (decoder_has_more_frames(decoder)) + { + uint8_t *decoded; + int timestamp; + + decoder_get_next(decoder, &decoded, ×tamp); + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int source = (y * info.canvas_width + x) * 4; + int destination = (y * width + x) * 3; + + int oldFactor = initial ? 0 : mix; + int newFactor = MIX_MAXIMUM - oldFactor; + + buffer[destination] = (buffer[destination] * oldFactor + decoded[source + 2] * newFactor) / MIX_MAXIMUM; + buffer[destination + 1] = (buffer[destination + 1] * oldFactor + decoded[source + 1] * newFactor) / MIX_MAXIMUM; + buffer[destination + 2] = (buffer[destination + 2] * oldFactor + decoded[source] * newFactor) / MIX_MAXIMUM; + } + } + + if (update != NULL) + { + update(width, height, buffer); + } + + for (int y = 0; y < height; y++) + { + colorlight_send_row(colorlight, y, width, buffer + y * width * 3); + } + + if (next - get_time() < UPDATE_DELAY) + { + next = get_time() + UPDATE_DELAY; + } + + await(next); + colorlight_send_update(colorlight, brightness, brightness, brightness); + + if (rate > 0) + { + next = get_time() + 1000 / rate; + } + else + { + next = get_time() + timestamp - previous; + previous = timestamp; + } + + initial = false; + } + + if (verbose) + { + float seconds = (next - start) / 1000.0; + printf("Played %d frames in %.2f seconds at an average rate of %.2f frames per second.\n", info.frame_count, seconds, info.frame_count / seconds); + } + + delete_decoder: + decoder_destroy(decoder); + + free_file: + free(file); + } + + await(next); + status = EXIT_SUCCESS; + +destroy_extension: + if (extension != NULL) + { + void (*destroy)() = dlsym(extension, "destroy"); + + if (destroy != NULL) + { + destroy(); + } + + dlclose(extension); + } + +destroy_colorlight: + colorlight_destroy(colorlight); + +destroy_loader: + loader_destroy(loader); + +free_buffer: + free(buffer); + +free_sources: + free(sources); + +exit: + return status; } \ No newline at end of file