diff --git a/Makefile b/Makefile index d641000..57f8b7c 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ VERSION_STRING := $(shell date +"%Y%m%d_%H%M%S") CFLAGS ?= CFLAGS += -Wno-address-of-packed-member -DVERSION_STRING="\"$(VERSION_STRING)\"" -SRCS := compat.c msposd.c bmp/bitmap.c bmp/region.c bmp/lib/schrift.c bmp/text.c osd/net/network.c osd/msp/msp.c osd/msp/msp_displayport.c libpng/lodepng.c osd/util/interface.c osd/util/settings.c osd/util/ini_parser.c osd/msp/vtxmenu.c +SRCS := compat.c msposd.c bmp/bitmap.c bmp/region.c bmp/lib/schrift.c bmp/text.c osd/net/network.c osd/msp/msp.c osd/msp/msp_displayport.c libpng/lodepng.c osd/util/interface.c osd/util/settings.c osd/util/ini_parser.c osd/msp/vtxmenu.c osd/util/subtitle.c OUTPUT ?= $(PWD) BUILD = $(CC) $(SRCS) -I $(SDK)/include -I$(TOOLCHAIN)/usr/include -I$(PWD) -L$(DRV) $(CFLAGS) $(LIB) -levent_core -Os -s $(CFLAGS) -o $(OUTPUT) diff --git a/README.md b/README.md index 4403f1b..38706d5 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Usage: msposd [OPTIONS] -a --ahi Draw graphic AHI, mode [0-No, 2-Simple 1-Ladder, 3-LadderEx (home indicator on ladder)] -x --matrix OSD matrix [0- 53:20 , 1- 50:18 chars, 11- Variable font size, 9-bottom align 720p mode, 8-center align 720p mode] --mspvtx Enable alpha mspvtx functionality + --subtitle Enable OSD/SRT recording -v --verbose Show debug info --help Display this help ``` @@ -88,6 +89,23 @@ Use `Exit Camera menu` stick command (one or more times) to exit all flightcontr msposd has **alpha** support for mspVTX to Betaflight. use the `--mspvtx` switch to activate this. This will configure Betaflight vtx tables with the supported channles by the vtx. You can switch channels from within Betaflight menu, Betaflight Configurator, SpeedyBee App, ELRS VTXAdmin. + +## OSD/SRT Recoding + +msposd supports recording MSP DisplayPort messages to an OSD file. This can later be used to overlay the OSD if the video was recorded without it. +The OSD file is compatible with the [walksnail-osd-tool](https://github.com/avsaase/walksnail-osd-tool). +The SRT file records the additional MSPOSD.msg. This is not supported by walksnail-osd-tool but can be used in later video editing pipeline tools. + +This feature monitors the recording directory for newly started .mp4 files. +Once detected, the SRT and OSD files will be created with the same name. +As soon as the MP4 file is closed, the recording of SRT and OSD files stops as well. + +Air side needs special config to detect recordings. No recursive file watching is implmented. +Therefor majestic needs to record to a flat directory layout. + +`cli -i /etc/majestic.yaml -s .records.path /mnt/mmcblk0p1/%F/..` + + ## Options. Forwarding of MSP packets via UDP. Can monitor RC Channels values in FC and call the script `channels.sh` (located at /usr/bin or /usr/sbin).Will passing the channel number and its value to it as $1 and $2 parameters. This allows for controlling the camera via the Remote Control Transmitter. diff --git a/msposd.c b/msposd.c index 813f4e8..2b41df1 100644 --- a/msposd.c +++ b/msposd.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -47,6 +48,7 @@ bool ParseMSP = true; bool DrawOSD = false; bool mspVTXenabled = false; bool vtxMenuEnabled = false; +extern char* recording_dir; // libevent base main loop struct event_base *base = NULL; @@ -107,6 +109,7 @@ static void print_usage() { " -x --matrix OSD matrix (0: 53:20, 1: 50:18 chars)\n" " -z --size Set OSD resolution\n" " --mspvtx Enable mspvtx support\n" + " --subtitle Enable OSD/SRT recording\n" " -v --verbose Show debug info\n" " -h --help Display this help\n", default_master, default_baudrate, default_out_addr); @@ -1194,6 +1197,40 @@ static int handle_data(const char *port_name, int baudrate, const char *out_addr event_add(stdin_event, NULL); #endif + //SRT/OSD Recording + if (recording_dir) { + printf("SRT/OSD recording enabled for directory: %s\n",recording_dir); + + int inotify_fd, watch_fd; + + // Initialize inotify + inotify_fd = inotify_init(); + if (inotify_fd < 0) { + perror("inotify_init"); + return 1; + } + + // Add a watch for the directory + watch_fd = inotify_add_watch(inotify_fd, recording_dir, IN_CREATE); + if (watch_fd < 0) { + perror("inotify_add_watch"); + close(inotify_fd); + return 1; + } + + // Create an event for the inotify file descriptor + struct event* inotify_event = event_new(base, inotify_fd, EV_READ | EV_PERSIST, inotify_callback, (void*) recording_dir); + if (!inotify_event) { + fprintf(stderr, "Could not create event!\n"); + event_base_free(base); + close(inotify_fd); + return 1; + } + + // Add the event to the event base + event_add(inotify_event, NULL); + } + if (temp) { if (GetTempSigmaStar() > -90) { temp = 2; // SigmaStar @@ -1290,6 +1327,7 @@ int main(int argc, char **argv) { {"matrix", required_argument, NULL, 'x'}, {"size", required_argument, NULL, 'z'}, {"mspvtx", no_argument, NULL, '1'}, + {"subtitle", required_argument, NULL, 's'}, {"verbose", no_argument, NULL, 'v'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0} @@ -1386,13 +1424,15 @@ int main(int argc, char **argv) { matrix_size = atoi(optarg); break; - case 'z': - char buffer[16]; - strncpy(buffer, optarg, sizeof(buffer)); - char *limit = strchr(buffer, 'x'); - if (limit) { - buffer[limit - buffer] = '\0'; - set_resolution(atoi(buffer), atoi(limit + 1)); + case 'z': + { + char buffer[16]; + strncpy(buffer, optarg, sizeof(buffer)); + char *limit = strchr(buffer, 'x'); + if (limit) { + buffer[limit - buffer] = '\0'; + set_resolution(atoi(buffer), atoi(limit + 1)); + } } break; @@ -1400,6 +1440,10 @@ int main(int argc, char **argv) { mspVTXenabled = true; break; + case 's': + recording_dir = strdup(optarg); + break; + case 'v': verbose = true; printf("Verbose mode!\n"); diff --git a/osd.c b/osd.c index dd9e531..b0f8ebb 100644 --- a/osd.c +++ b/osd.c @@ -54,6 +54,9 @@ #include "osd/util/interface.h" #include "osd/util/settings.h" +#include "osd.h" +#include "osd/util/subtitle.h" + #define CPU_TEMP_PATH "/sys/devices/platform/soc/f0a00000.apb/f0a71000.omc/temp1" #define AU_VOLTAGE_PATH "/sys/devices/platform/soc/f0a00000.apb/f0a71000.omc/voltage4" @@ -96,7 +99,7 @@ char _port_name[80]; static uint8_t message_buffer[256]; // only needs to be the maximum size of an // MSP packet, we only care to fwd MSP -static char current_fc_identifier[4]; +char current_fc_identifier[4]; static char current_fc_identifier_end_of_string = 0x00; /* For compressed full-frame transmission */ @@ -161,6 +164,9 @@ extern bool armed; extern bool vtxInitDone; extern bool DrawOSD; +// SRT/OSD +extern bool recording_running; + static void send_display_size(int serial_fd) { uint8_t buffer[8]; uint8_t payload[2] = {MAX_DISPLAY_X, MAX_DISPLAY_Y}; @@ -1372,6 +1378,7 @@ void remove_carriage_returns(char *out) { } char osdmsg[180]; +char ready_osdmsg[80]; bool DrawTextOnOSDBitmap(char *msg) { char *font; @@ -1441,6 +1448,9 @@ bool DrawTextOnOSDBitmap(char *msg) { msg_buffer, MSP_CMD_DISPLAYPORT, &payload_buffer[0], 80, MSP_INBOUND); sendto(out_sock, msg_buffer, 100, 0, (struct sockaddr *)&sin_out, sizeof(sin_out)); + // store ready osdmsg to be used in subtitle srt writeing + memcpy(&ready_osdmsg[0], &out[0], msglen + 1); + // printf("Sent text msg : %s\n",out); return false; } @@ -1922,6 +1932,12 @@ static void draw_complete() { SetOSDMsg(msg); } + if (recording_running) { + handle_osd_out(); + write_srt_file(); + check_recoding_file(); + } + #ifdef _x86 // sfRenderWindow_display(window); #endif diff --git a/osd.h b/osd.h new file mode 100644 index 0000000..1b51d43 --- /dev/null +++ b/osd.h @@ -0,0 +1,4 @@ + +extern char current_fc_identifier[4]; + +uint64_t get_time_ms(); \ No newline at end of file diff --git a/osd/util/subtitle.c b/osd/util/subtitle.c new file mode 100644 index 0000000..bfa1641 --- /dev/null +++ b/osd/util/subtitle.c @@ -0,0 +1,289 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "subtitle.h" +#include "../../osd.h" + +#define EVENT_SIZE (sizeof(struct inotify_event)) +#define BUF_LEN (1024 * (EVENT_SIZE + NAME_MAX + 1)) + +extern char air_unit_info_msg[255]; +extern int msg_colour; +extern char ready_osdmsg[80]; +extern uint16_t character_map[MAX_OSD_WIDTH][MAX_OSD_HEIGHT]; +extern bool verbose; +uint32_t subtitle_start_time = 0; // Start time in milliseconds +uint32_t subtitle_current_time = 0; // Current FlightTime in seconds +uint32_t sequence_number = 1; // Subtitle sequence number +uint32_t last_flight_time_seconds = 0; // Store the last FlightTime in seconds +char* recording_dir = NULL; +FILE* srt_file = NULL; +FILE* osd_file = NULL; +char* srt_file_name = NULL; +char* osd_file_name = NULL; +bool recording_running = false; + + +// Function to write Walksnail OSD header +void write_osd_header(FILE *file) { + uint8_t header[HEADER_BYTES] = {0}; + memcpy(header, current_fc_identifier, FC_TYPE_BYTES); // Copy FC identifier + // Add any additional header data here if needed + + fwrite(header, 1, HEADER_BYTES, file); +} + +void remove_control_codes(char *str) { + int i, j; + for (i = 0, j = 0; str[i] != '\0'; i++) { + // Check for &L or &F followed by two digits + if (str[i] == '&' && (str[i+1] == 'L' || str[i+1] == 'F') && + str[i+2] >= '0' && str[i+2] <= '9' && + str[i+3] >= '0' && str[i+3] <= '9') { + // Skip the &LXX or &FXX substring + i += 3; + } else { + // Copy the character to the new position + str[j++] = str[i]; + } + } + // Null-terminate the modified string + str[j] = '\0'; +} + +void write_srt_file() { + + // Open the file if it hasn't been opened yet + if (!srt_file) { + srt_file = fopen(srt_file_name, "w"); + if (srt_file == NULL) { + perror("Failed to open file"); + return; + } + } + + // Convert current time to seconds + uint32_t current_flight_time_seconds = subtitle_current_time / 1000; + + // Only write if the FlightTime has changed by at least 1 second + if (current_flight_time_seconds == last_flight_time_seconds) { + return; // No change, do nothing + } + + // Calculate start and end times in SRT format (HH:MM:SS,ms) + uint32_t start_time_ms = current_flight_time_seconds * 1000; // Start time in milliseconds (aligned to the second) + uint32_t end_time_ms = start_time_ms + 1000; // Each subtitle lasts 1 second + + uint32_t start_hours = start_time_ms / 3600000; + uint32_t start_minutes = (start_time_ms % 3600000) / 60000; + uint32_t start_seconds = (start_time_ms % 60000) / 1000; + uint32_t start_milliseconds = start_time_ms % 1000; + + uint32_t end_hours = end_time_ms / 3600000; + uint32_t end_minutes = (end_time_ms % 3600000) / 60000; + uint32_t end_seconds = (end_time_ms % 60000) / 1000; + uint32_t end_milliseconds = end_time_ms % 1000; + + // Write the subtitle to the file + fprintf(srt_file, "%u\n", sequence_number); + fprintf(srt_file, "%02u:%02u:%02u,%03u --> %02u:%02u:%02u,%03u\n", + start_hours, start_minutes, start_seconds, start_milliseconds, + end_hours, end_minutes, end_seconds, end_milliseconds); + if (msg_colour) { // ground mode + fprintf(srt_file, "%s\n\n", air_unit_info_msg); + } else { // air mode + remove_control_codes(ready_osdmsg); + fprintf(srt_file, "%s\n\n", ready_osdmsg); + } + fflush(srt_file); + + // Increment the sequence number and update the last FlightTime written + sequence_number++; + last_flight_time_seconds = current_flight_time_seconds; +} + +void handle_osd_out() { + if (! osd_file) + osd_file = fopen(osd_file_name, "wb"); + if (osd_file) { + if (subtitle_start_time == 0) { + write_osd_header(osd_file); + subtitle_start_time = (uint32_t)get_time_ms(); + } + + // Calculate elapsed time since subtitle_start_time + subtitle_current_time = (uint32_t)get_time_ms() - subtitle_start_time; + + fwrite(&subtitle_current_time, sizeof(uint32_t), 1, osd_file); + // Write OSD data + for (int y = 0; y < MAX_OSD_HEIGHT; y++) { + for (int x = 0; x < MAX_OSD_WIDTH -1; x++) { // -1 ??? no clue why + // Write glyph (2 bytes) + fwrite(&character_map[x][y], sizeof(uint16_t), 1, osd_file); + } + } + fflush(osd_file); + } +} + +int inotify_fd; +int watch_desc; +void setup_recording_watch(char *file_to_watch) { + inotify_fd = inotify_init(); + if (inotify_fd == -1) { + perror("inotify_init"); + exit(EXIT_FAILURE); + } + + // Set the inotify file descriptor to non-blocking mode + int flags = fcntl(inotify_fd, F_GETFL, 0); + if (flags == -1) { + perror("fcntl F_GETFL"); + close(inotify_fd); + exit(EXIT_FAILURE); + } + if (fcntl(inotify_fd, F_SETFL, flags | O_NONBLOCK) == -1) { + perror("fcntl F_SETFL O_NONBLOCK"); + close(inotify_fd); + exit(EXIT_FAILURE); + } + + watch_desc = inotify_add_watch(inotify_fd, file_to_watch, IN_CLOSE_WRITE | IN_CLOSE_NOWRITE); + if (watch_desc == -1) { + perror("inotify_add_watch"); + close(inotify_fd); + exit(EXIT_FAILURE); + } + + printf("Watching %s for close events (non-blocking mode)...\n", file_to_watch); + +} + +void check_recoding_file() { + char buffer[BUF_LEN]; + ssize_t len = read(inotify_fd, buffer, BUF_LEN); + if (len == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // No data available, continue polling + return; + } else { + perror("read"); + return; + } + } + + for (char *ptr = buffer; ptr < buffer + len; ) { + struct inotify_event *event = (struct inotify_event *) ptr; + + if (event->mask & IN_CLOSE_WRITE || event->mask & IN_CLOSE_NOWRITE) { + printf("Stopping OSD/STR writeing\n"); + if (srt_file) { + fclose(srt_file); + srt_file = NULL; + } + if (osd_file) { + fclose(osd_file); + osd_file = NULL; + } + recording_running = false; + } + ptr += EVENT_SIZE + event->len; + } + + inotify_rm_watch(inotify_fd, watch_desc); + close(inotify_fd); +} + +// Function to handle new file creation +void handle_new_file(char* filename) { + printf("New recording detected: %s\r\n", filename); + + // detected a new recodring, closeing current files + if (srt_file) { + fclose(srt_file); + srt_file = NULL; + } + if (osd_file) { + fclose(osd_file); + osd_file = NULL; + } + + // reset values + subtitle_start_time = 0; + subtitle_current_time = 0; + sequence_number = 1; + last_flight_time_seconds = 0; + + setup_recording_watch(filename); + + // Free any previously allocated memory to avoid memory leaks + free(srt_file_name); + free(osd_file_name); + + // Remove the suffix + char* dot = strrchr(filename, '.'); + if (dot) { + *dot = '\0'; + } + + // Allocate memory for the new filenames + srt_file_name = (char*)malloc(strlen(filename) + 5); // +5 for ".srt" and null terminator + osd_file_name = (char*)malloc(strlen(filename) + 5); // +5 for ".osd" and null terminator + + if (srt_file_name == NULL || osd_file_name == NULL) { + fprintf(stderr, "Memory allocation failed\n"); + exit(1); + } + + // Create the new filenames + snprintf(srt_file_name, strlen(filename) + 5, "%s.srt", filename); + snprintf(osd_file_name, strlen(filename) + 5, "%s.osd", filename); + + if (verbose) { + printf("srt file: %s\r\n", srt_file_name); + printf("osd file: %s\r\n", osd_file_name); + } + + recording_running = true; +} + +// Callback function for inotify events +void inotify_callback(evutil_socket_t fd, short events, void* arg) { + char buffer[BUF_LEN]; + ssize_t length = read(fd, buffer, BUF_LEN); + + if (length < 0) { + perror("read"); + return; + } + + // Process inotify events + for (char* ptr = buffer; ptr < buffer + length; ) { + struct inotify_event* event = (struct inotify_event*) ptr; + + if (event->mask & IN_CREATE) { + // Construct the full path + char filename[PATH_MAX]; + snprintf(filename, PATH_MAX, "%s/%s", (const char*) arg, event->name); + + // Filter file names + if (strstr(event->name, ".mp4") != NULL) { + // Handle the new file + handle_new_file(filename); + } else { + printf("Ignoring non-.mp4 file: %s\n", event->name); + } + } + + ptr += EVENT_SIZE + event->len; + } +} diff --git a/osd/util/subtitle.h b/osd/util/subtitle.h new file mode 100644 index 0000000..207a9b3 --- /dev/null +++ b/osd/util/subtitle.h @@ -0,0 +1,13 @@ +#pragma once + +#include "../../osd.h" + +#define HEADER_BYTES 40 +#define FC_TYPE_BYTES 4 +#define MAX_OSD_WIDTH 54 +#define MAX_OSD_HEIGHT 20 + +void write_srt_file(); +void handle_osd_out(); +void inotify_callback(evutil_socket_t fd, short events, void* arg); +void check_recoding_file();