diff options
author | dyknon <dyknon@r5f.jp> | 2025-03-30 21:08:00 +0900 |
---|---|---|
committer | dyknon <dyknon@r5f.jp> | 2025-03-30 21:08:00 +0900 |
commit | 6d990813e92da0296ef80aaa71364b713c762488 (patch) | |
tree | 6ddc5d9df5b03111e9ef284bdedb649942d1b537 |
Initial commit
-rw-r--r-- | ytdl-storyboard/Makefile | 13 | ||||
-rw-r--r-- | ytdl-storyboard/storyboard.c | 625 |
2 files changed, 638 insertions, 0 deletions
diff --git a/ytdl-storyboard/Makefile b/ytdl-storyboard/Makefile new file mode 100644 index 0000000..dfe252d --- /dev/null +++ b/ytdl-storyboard/Makefile @@ -0,0 +1,13 @@ +LIBPKGS=libcjson libavutil libavformat libavcodec libswscale +CFLAGS=$(shell pkg-config --cflags mpv) \ + $(shell pkg-config --cflags $(LIBPKGS)) \ + -pthread \ + -fPIC -Wall -Wno-unused-variable -Wno-parentheses -Wno-unused-function +LDFLAGS=$(shell pkg-config --libs $(LIBPKGS)) \ + -pthread + +test: ytdl-storyboard.so + mpv ytdl://ABH1Vft36aY ytdl://H9uwaNw4DRY + +ytdl-storyboard.so: storyboard.c Makefile + gcc -o $@ $(CFLAGS) $(LDFLAGS) -shared $< diff --git a/ytdl-storyboard/storyboard.c b/ytdl-storyboard/storyboard.c new file mode 100644 index 0000000..cbc2d9f --- /dev/null +++ b/ytdl-storyboard/storyboard.c @@ -0,0 +1,625 @@ +#include <mpv/client.h> +#undef NDEBUG +#include <assert.h> +#include <stdatomic.h> +#include <stdlib.h> +#include <stdint.h> +#include <string.h> +#include <stdio.h> +#include <math.h> +#include <limits.h> +#include <stdarg.h> +#include <cJSON.h> +#include <pthread.h> + +#include <libavutil/pixfmt.h> +#include <libavutil/imgutils.h> +#include <libavformat/avformat.h> +#include <libavcodec/avcodec.h> +#include <libswscale/swscale.h> + +struct plugin; +struct plugin{ + mpv_handle *m; + struct storyboard *sb; + int64_t osd_remove_time_ns; +}; + +enum hooknumber{ + HOOK_PRELOADED, + HOOK_UNLOAD +}; +enum overlay_nums{ + OSD_STORYBOARD = 1, +}; + +#define Z(err) ({ \ + typeof(err) tmp = (err); \ + if(tmp) return -1; \ + tmp; \ +}) +#define NZ(err) ({ \ + typeof(err) tmp = (err); \ + if(!tmp) return -1; \ + tmp; \ +}) +#define TRY_goto(label, err, msg, ...) do{ \ + int tmp = (err); \ + if(tmp){ \ + fprintf(stderr, "error%d at %s:%d(%s): " msg "\n", \ + tmp, __FILE__, __LINE__, __func__, ## __VA_ARGS__); \ + goto label; \ + } \ +}while(0) +#define TRY(err, msg, ...) do{ \ + int tmp = (err); \ + if(tmp){ \ + fprintf(stderr, "error%d at %s:%d(%s): " msg "\n", \ + tmp, __FILE__, __LINE__, __func__, ## __VA_ARGS__); \ + return -1; \ + } \ +}while(0) + +static int msg(struct plugin *self, const char *f, ...){ + int len; + char msg_arr[1024]; + char *msg = msg_arr; + char *freeme = NULL; + + va_list ap; + va_start(ap, f); + len = vsnprintf(msg_arr, sizeof(msg_arr), f, ap); + if(sizeof(msg_arr) < len+1){ + freeme = msg = malloc(len+1); + assert(msg); + assert(vsnprintf(msg, len+1, f, ap) == len); + msg[len] = 0; + } + va_end(ap); + + const char *command[] = { "print-text", msg, NULL }; + TRY(mpv_command(self->m, command), "print-text"); + + free(freeme); + return 0; +} +static int overlay_add(struct plugin *self, + int id, int x, int y, + uint8_t *buf, size_t off, size_t w, size_t h, size_t stride, + size_t dw, size_t dh +){ + assert(off <= INT64_MAX); + assert(w <= INT64_MAX); + assert(h <= INT64_MAX); + assert(stride <= INT64_MAX); + assert(dw <= INT64_MAX); + assert(dh <= INT64_MAX); + + char *command_keys[] = { + "name", "id", "x", "y", + "file", "offset", "fmt", "w", "h", "stride", + "dw", "dh", + }; + char filename[32]; // enough for 64bit int + 1byte prefix + sprintf(filename, "&%tu", (void *)buf - NULL); + mpv_node command_vals[] = { + { + .format = MPV_FORMAT_STRING, + .u.string = (char *)"overlay-add", + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = id, + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = x, + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = y, + }, { + .format = MPV_FORMAT_STRING, + .u.string = filename, + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = off, + }, { + .format = MPV_FORMAT_STRING, + .u.string = (char *)"bgra", + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = w, + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = h, + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = stride, + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = dw, + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = dh, + }, + }; + assert(sizeof(command_keys)/sizeof(command_keys[0]) + == sizeof(command_vals)/sizeof(command_vals[0])); + mpv_node_list command_list = { + .num = sizeof(command_keys)/sizeof(command_keys[0]), + .values = command_vals, + .keys = command_keys, + }; + mpv_node command = { + .format = MPV_FORMAT_NODE_MAP, + .u.list = &command_list, + }; + + TRY(mpv_command_node(self->m, &command, NULL), "mpv_command_node"); + return 0; +} +static int overlay_remove(struct plugin *self, int id){ + char *command_keys[] = { "name", "id" }; + mpv_node command_vals[] = { + { + .format = MPV_FORMAT_STRING, + .u.string = (char *)"overlay-remove", + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = id, + }, + }; + assert(sizeof(command_keys)/sizeof(command_keys[0]) + == sizeof(command_vals)/sizeof(command_vals[0])); + mpv_node_list command_list = { + .num = sizeof(command_keys)/sizeof(command_keys[0]), + .values = command_vals, + .keys = command_keys, + }; + mpv_node command = { + .format = MPV_FORMAT_NODE_MAP, + .u.list = &command_list, + }; + + TRY(mpv_command_node(self->m, &command, NULL), "mpv_command_node"); + return 0; +} +static int url_verifyhttp(const char *url){ + const char *protoend = strstr(url, "://"); + if(!protoend) return 0; + if(protoend - url == 4){ + return !memcmp(url, "http", 4); + }else if(protoend - url == 5){ + return !memcmp(url, "https", 5); + } + return 0; +} +static int load_image(AVFrame **framepp, const char *url){ + int retval = 0; + + const AVInputFormat *iformat = av_find_input_format("image2pipe"); + if(!iformat) return -1; + const AVCodec *decoder = NULL; + AVFormatContext *format_ctx = NULL; + AVCodecContext *codec_ctx = NULL; + AVFrame *frame = NULL; + AVPacket pkt; + + // initialize demuxer + TRY_goto(err, avformat_open_input( + &format_ctx, url, iformat, NULL) < 0, + "avformat_open_input"); + TRY_goto(err, avformat_find_stream_info( + format_ctx, NULL) < 0, + "avformat_find_stream_info"); + + // initialize codec + decoder = avcodec_find_decoder( + format_ctx->streams[0]->codecpar->codec_id); + if(!decoder) goto err; + codec_ctx = avcodec_alloc_context3(decoder); + if(!codec_ctx) goto err; + TRY_goto(err, avcodec_parameters_to_context( + codec_ctx, format_ctx->streams[0]->codecpar) < 0, + "avcodec_parameters_to_context"); + + // decode + TRY_goto(err, avcodec_open2(codec_ctx, decoder, NULL), "avcodec_open2"); + frame = av_frame_alloc(); + if(!frame) goto err; + TRY_goto(err, av_read_frame(format_ctx, &pkt), "av_read_frame"); + int sps = avcodec_send_packet(codec_ctx, &pkt); + av_packet_unref(&pkt); + TRY_goto(err, sps, "avcodec_send_packet"); + TRY_goto(err, avcodec_receive_frame( + codec_ctx, frame), "avcodec_receive_frame"); + *framepp = frame; + + goto final; +err: + retval = -1; + av_frame_free(&frame); +final: + avcodec_free_context(&codec_ctx); + avformat_close_input(&format_ctx); + return retval; +} +static int convert_image_rgb32(AVFrame *frame, uint8_t **bitmap){ + int retval = 0; + uint8_t *dstbuf[4] = {0}; + int linesizes[4]; + struct SwsContext *sws_ctx = NULL; + + TRY_goto(err, + av_image_alloc(dstbuf, linesizes, + frame->width, frame->height, AV_PIX_FMT_RGB32, 16) < 0, + "av_image_alloc"); + sws_ctx = sws_getContext( + frame->width, frame->height, frame->format, + frame->width, frame->height, AV_PIX_FMT_RGB32, + SWS_POINT, NULL, NULL, NULL + ); + TRY_goto(err, sws_ctx == NULL, "sws_getContext"); + sws_scale(sws_ctx, + (const uint8_t *const *)frame->data, frame->linesize, 0, frame->height, + dstbuf, linesizes); + + *bitmap = dstbuf[0]; + goto final; +err: + retval = -1; + av_freep(&dstbuf[0]); +final: + sws_freeContext(sws_ctx); + return retval; +} + +struct storyboard_fragment{ + char url[4096]; + double offset; + double duration; + size_t frames; + uint8_t *buf; +}; +struct storyboard{ + size_t width; + size_t height; + size_t rows; + size_t columns; + double fps; + size_t fragment_cnt; + struct storyboard_fragment *fragments; + + int thread_initialized; + atomic_int ready; + pthread_t thread; +}; +static int storyboard_init(struct storyboard *self, cJSON *src){ +#define READSZ(name) do{ \ + cJSON *tmp = cJSON_GetObjectItem(src, #name); \ + if(!tmp) return -1; \ + double tmp_n = cJSON_GetNumberValue(tmp); \ + if(isnan(tmp_n) || tmp_n != (double)(size_t)tmp_n) return -1; \ + self->name = (size_t)tmp_n; \ +}while(0); + + self->thread_initialized = 0; + + READSZ(width); + READSZ(height); + READSZ(columns); + READSZ(rows); + if((size_t)-1 / self->width / self->height / self->columns / self->rows < 1) + return -1; + + cJSON *j_fps = cJSON_GetObjectItem(src, "fps"); + if(!j_fps) return -1; + double fps = cJSON_GetNumberValue(j_fps); + if(isnan(fps) || fps < 0) return -1; + self->fps = fps; + + cJSON *j_fragments = cJSON_GetObjectItem(src, "fragments"); + if(!j_fragments) return -1; + int fragment_cnt = cJSON_GetArraySize(j_fragments); + if(fragment_cnt <= 0 || 1024 <= fragment_cnt) return -1; + self->fragment_cnt = fragment_cnt; + self->fragments = malloc(fragment_cnt * sizeof(self->fragments[0])); + assert(self->fragments); + int fi; + double offset = 0; + for(fi = 0; fi < fragment_cnt; fi++){ + cJSON *j_fragment = cJSON_GetArrayItem(j_fragments, fi); + if(!j_fragment) goto err; + cJSON *j_url = cJSON_GetObjectItem(j_fragment, "url"); + if(!j_url) goto err; + const char *url = cJSON_GetStringValue(j_url); + if(!url || sizeof(self->fragments[fi].url) <= strlen(url)) goto err; + if(!url_verifyhttp(url)) goto err; + cJSON *j_duration = cJSON_GetObjectItem(j_fragment, "duration"); + if(!j_duration) goto err; + double duration = cJSON_GetNumberValue(j_duration); + if(isnan(duration) || duration < 0) goto err; + strcpy(self->fragments[fi].url, url); + self->fragments[fi].offset = offset; + self->fragments[fi].duration = duration; + self->fragments[fi].buf = NULL; + offset += duration; + } + +#undef READSZ + return 0; +err: + free(self->fragments); + self->fragments = NULL; + return -1; +} +static int storyboard_destroy(struct storyboard *self){ + if(self->thread_initialized){ + self->thread_initialized = 0; + TRY(pthread_cancel(self->thread), "pthread_cancel"); + TRY(pthread_join(self->thread, NULL), "pthread_join"); + } + if(self->fragments){ + for(int fi = 0; fi < self->fragment_cnt; fi++){ + free(self->fragments[fi].buf); + } + free(self->fragments); + } + return 0; +} +static int storyboard_thread_inner(struct storyboard *self){ + int retval = 0; + int oldstate; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate); + +#define CANCELPOINT \ + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstate); \ + pthread_testcancel(); \ + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate) + + for(size_t i = 0; i < self->fragment_cnt; i++){ + AVFrame *frame = NULL; + uint8_t *rgb = NULL; + pthread_cleanup_push((void (*)(void *))av_frame_free, &frame); + pthread_cleanup_push((void (*)(void *))av_freep, &rgb); + + TRY_goto(err, load_image(&frame, self->fragments[i].url), "load_image"); + CANCELPOINT; + + if( + frame->width < 0 || frame->height < 0 + || frame->width % self->width != 0 + || frame->height % self->height != 0 + ){ + goto err; + } + + TRY_goto(err, convert_image_rgb32(frame, &rgb), "convert_image_rgb32"); + CANCELPOINT; + + size_t frag_w = frame->width / self->width; + size_t frag_h = frame->height / self->height; + size_t frag_s = self->width * frag_w; + self->fragments[i].frames = frag_w * frag_h; + uint8_t *buf = self->fragments[i].buf + = malloc(frame->width * frame->height * 4); + if(!buf) goto err; + for(size_t f = 0; f < frag_w * frag_h; f++){ + size_t f_off = frag_s * self->height * (f / frag_w) + + self->width * (f % frag_w); + size_t of_off = self->width * self->height * f; + for(size_t y = 0; y < self->height; y++){ + size_t off = f_off + frag_s * y; + size_t o_off = of_off + self->width * y; + memcpy(&buf[o_off*4], &rgb[off*4], self->width * 4); + } + } + CANCELPOINT; + + goto final; +err: + retval = -1; +final: + pthread_cleanup_pop(1); + pthread_cleanup_pop(1); + if(retval) break; + } + return retval; +#undef CANCELPOINT +} +static void *storyboard_thread(void *self_v){ + struct storyboard *self = self_v; + if(storyboard_thread_inner(self)){ + atomic_store(&self->ready, -1); + }else{ + atomic_store(&self->ready, 1); + } + return NULL; +} +static int storyboard_spawn(struct storyboard *self){ + atomic_init(&self->ready, 0); + TRY(pthread_create(&self->thread, NULL, storyboard_thread, self), + "pthread_create"); + self->thread_initialized = 1; + return 0; +} +static int storyboard_getimg(struct storyboard *self, + uint8_t **ptr, double time +){ + if(!self->thread_initialized) return 1; + if(atomic_load(&self->ready) != 1) return 1; + + struct storyboard_fragment *fragment = NULL; + for(size_t i = 0; i < self->fragment_cnt; i++){ + struct storyboard_fragment *f = &self->fragments[i]; + if(f->offset <= time){ + fragment = f; + } + } + if(!fragment || fragment->offset + fragment->duration < time){ + return 1; + } + + assert(time - fragment->offset >= 0); + size_t frame = (time - fragment->offset) * self->fps; + if(fragment->frames <= frame) frame = fragment->frames - 1; + size_t frame_bi = self->width * self->height * 4 * frame; + + *ptr = &fragment->buf[frame_bi]; + return 0; +} + +static int plugin_init(struct plugin *self, mpv_handle *h){ + self->m = h; + self->sb = NULL; + self->osd_remove_time_ns = 0; + + TRY(mpv_hook_add(self->m, HOOK_PRELOADED, "on_preloaded", 0), "hook_add"); + TRY(mpv_hook_add(self->m, HOOK_UNLOAD, "on_unload", 0), "hook_add"); + + return 0; +} +static int hook_on_preloaded(struct plugin *self){ + char *result_s = mpv_get_property_string( + self->m, + "user-data/mpv/ytdl/json-subprocess-result"); + if(!result_s){ // not ytdl video + goto noytdl; + } + cJSON *result = cJSON_Parse(result_s); + if(!result){ + msg(self, "ytdl subprocess-json parse error"); + goto resultperr; + } + + cJSON *j_status = cJSON_GetObjectItem(result, "status"); + if(!j_status || cJSON_GetNumberValue(j_status) != 0.0){ + goto ytdlfail; + } + cJSON *j_ytdlout_s = cJSON_GetObjectItem(result, "stdout"); + if(!j_ytdlout_s) goto ytdlfail; + const char *ytdlout_s = cJSON_GetStringValue(j_ytdlout_s); + if(!ytdlout_s) goto ytdlfail; + + cJSON *ytdlout = cJSON_Parse(ytdlout_s); + if(!ytdlout) goto ytdlfail; + + cJSON *formats = cJSON_GetObjectItem(ytdlout, "formats"); + if(!formats) goto err; + int formats_len = cJSON_GetArraySize(formats); + struct storyboard *best_sb = NULL; + for(int i = 0; i < formats_len; i++){ + cJSON *format = cJSON_GetArrayItem(formats, i); + if(!format) goto err; + cJSON *j_format_id = cJSON_GetObjectItem(format, "format_id"); + if(!j_format_id) continue; + const char *format_id = cJSON_GetStringValue(j_format_id); + if(!format_id) continue; + if(format_id[0] != 's' || format_id[1] != 'b') continue; + struct storyboard *sb = malloc(sizeof(struct storyboard)); + assert(sb); + if(storyboard_init(sb, format)) continue; + if(!best_sb || best_sb->height < sb->height){ + if(best_sb) storyboard_destroy(best_sb); + free(best_sb); + best_sb = sb; + }else{ + storyboard_destroy(sb); + free(sb); + } + } + if(!best_sb) goto err; + storyboard_spawn(best_sb); + if(self->sb){ + storyboard_destroy(self->sb); + free(self->sb); + } + self->sb = best_sb; + +err: + cJSON_Delete(ytdlout); +ytdlfail: + cJSON_Delete(result); +resultperr: + mpv_free(result_s); +noytdl: + return 0; +} +static int hook_on_unload(struct plugin *self){ + if(self->sb){ + storyboard_destroy(self->sb); + free(self->sb); + self->sb = NULL; + } + return 0; +} +static int event_on_seek(struct plugin *self){ + if(!self->sb) return 0; + uint8_t *img; + + double pos; + TRY(mpv_get_property(self->m, "time-pos", MPV_FORMAT_DOUBLE, &pos), + "mpv_get_property"); + if(storyboard_getimg(self->sb, &img, pos)) return 0; + + overlay_add(self, OSD_STORYBOARD, 0, 0, + img, 0, self->sb->width, self->sb->height, self->sb->width*4, + self->sb->width, self->sb->height + ); + self->osd_remove_time_ns = mpv_get_time_ns(self->m) + 1*1000*1000*1000; + if(self->osd_remove_time_ns == 0) self->osd_remove_time_ns = 1; + return 0; +} +static int timeout_handler_osd_remove(struct plugin *self, int64_t now){ + overlay_remove(self, OSD_STORYBOARD); + return 0; +} +static int event_handler(struct plugin *self, const mpv_event *ev){ + switch(ev->event_id){ + case MPV_EVENT_HOOK: + mpv_event_hook *hook = ev->data; + switch(ev->reply_userdata){ + case HOOK_UNLOAD: + Z(hook_on_unload(self)); + break; + case HOOK_PRELOADED: + Z(hook_on_preloaded(self)); + break; + default: + return -1; + } + TRY(mpv_hook_continue(self->m, hook->id), "hook continue"); + return 0; + case MPV_EVENT_SEEK: + Z(event_on_seek(self)); + return 0; + default: + return 0; + } +} +static double event_wait_timeout(struct plugin *self){ + if(self->osd_remove_time_ns){ + int64_t now = mpv_get_time_ns(self->m); + if(self->osd_remove_time_ns <= now){ + self->osd_remove_time_ns = 0; + timeout_handler_osd_remove(self, now); + return event_wait_timeout(self); + }else{ + return (self->osd_remove_time_ns - now) / 1.0e9; + } + }else{ + return -1; + } +} + +int mpv_open_cplugin(mpv_handle *h){ + struct plugin self; + Z(plugin_init(&self, h)); + + mpv_event *ev; + while(ev = mpv_wait_event(h, event_wait_timeout(&self))){ + Z(event_handler(&self, ev)); + if(ev->event_id == MPV_EVENT_SHUTDOWN){ + break; + } + } + return 0; +} |