diff options
| -rw-r--r-- | Makefile | 12 | ||||
| -rw-r--r-- | README | 5 | ||||
| -rw-r--r-- | storyboard.c | 728 | ||||
| -rw-r--r-- | ytdlsb-main.c | 664 | ||||
| -rw-r--r-- | ytdlsb-mpv.c | 102 | ||||
| -rw-r--r-- | ytdlsb-mpv.h | 8 | ||||
| -rw-r--r-- | ytdlsb-tasks.c | 234 | ||||
| -rw-r--r-- | ytdlsb-tasks.h | 39 | ||||
| -rw-r--r-- | ytdlsb-utils.h | 45 |
9 files changed, 1100 insertions, 737 deletions
@@ -1,12 +1,12 @@ -LIBPKGS=libcjson libavutil libavformat libavcodec libswscale +LIBPKGS=libcjson libcurl libwebp 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 + -fPIC -Wall -Wno-unused-variable -Wno-parentheses -Wno-unused-function \ + -g -O0 +LDFLAGS=$(shell pkg-config --libs $(LIBPKGS)) all: ytdl-storyboard.so -ytdl-storyboard.so: storyboard.c Makefile - gcc -o $@ $(CFLAGS) $(LDFLAGS) -shared $< +ytdl-storyboard.so: ytdlsb-main.c ytdlsb-tasks.c ytdlsb-mpv.c + gcc -o $@ $(CFLAGS) -shared $^ $(LDFLAGS) @@ -12,7 +12,7 @@ cp ytdl-storyboard.so osc-mod.lua ~/.config/mpv/scripts # Copyright -Copyright dyknon 2025 +Copyright dyknon 2025, 2026 osc-mod.lua is originally written by mpv developers. It was taken from mpv 0.39.0. @@ -22,8 +22,7 @@ There are small changes by dyknon to: * Export information about user interactins with osc to other scripts. * Unload built-in osc. -dyknon is only a copyright holder of some parts of osc-mod.lua. -Other files are written by dyknon in 2025. +Other files are written by dyknon. # License diff --git a/storyboard.c b/storyboard.c deleted file mode 100644 index ff4ccdf..0000000 --- a/storyboard.c +++ /dev/null @@ -1,728 +0,0 @@ -#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; - int64_t osc_check_time_ns; - int64_t osc_last_checked_ns; - int show_on_seek; -}; - -enum hooknumber{ - HOOK_PRELOADED, - HOOK_UNLOAD -}; -enum observenumber{ - OBSERVE_OSC, -}; -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; - self->osc_check_time_ns = 0; - self->osc_last_checked_ns = 0; - self->show_on_seek = 1; - - 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"); - TRY(mpv_observe_property(self->m, OBSERVE_OSC, - "user-data/osc/seekbar/possec", MPV_FORMAT_NONE), - "mpv_observe_property"); - TRY(mpv_observe_property(self->m, OBSERVE_OSC, - "user-data/osc/visible", MPV_FORMAT_NONE), "mpv_observe_property"); - TRY(mpv_observe_property(self->m, OBSERVE_OSC, - "user-data/osc/active-element", MPV_FORMAT_NONE), - "mpv_observe_property"); - - 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 show_storyboard(struct plugin *self, double pos){ - uint8_t *img; - 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 - ); - return 0; -} -static int event_on_seek(struct plugin *self){ - if(!self->sb) return 0; - if(!self->show_on_seek) return 0; - - double pos; - TRY(mpv_get_property(self->m, "time-pos", MPV_FORMAT_DOUBLE, &pos), - "mpv_get_property"); - - Z(show_storyboard(self, pos)); - 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 osc_check(struct plugin *self, int64_t now){ - char *actelem = NULL; - self->osc_last_checked_ns = now ? now : -1; - if(!self->sb) return 0; - - int visible; - if(mpv_get_property(self->m, - "user-data/osc/visible", MPV_FORMAT_FLAG, &visible) - ) goto stop; - if(!visible) goto stop; - self->show_on_seek = 0; - if(mpv_get_property(self->m, - "user-data/osc/active-element", MPV_FORMAT_STRING, &actelem) - ) goto stop; - if(strcmp(actelem, "\"seekbar\"")) goto stop; // XXX: quoted - mpv_free(actelem); - double pos; - if(mpv_get_property(self->m, - "user-data/osc/seekbar/possec", MPV_FORMAT_DOUBLE, &pos) - ) goto stop; - - Z(show_storyboard(self, pos)); - - return 0; -stop: - mpv_free(actelem); - overlay_remove(self, OSD_STORYBOARD); - return 0; -} -static int event_on_observe_osc(struct plugin *self, mpv_event_property *prop){ - int64_t now = mpv_get_time_ns(self->m); - if(self->osc_last_checked_ns - && now - self->osc_last_checked_ns < 20*1000*1000 - ){ - if(!self->osc_check_time_ns){ - self->osc_check_time_ns = now + 20*1000*1000; - if(!self->osc_check_time_ns) self->osc_check_time_ns = 1; - } - }else{ - self->osc_check_time_ns = 0; - Z(osc_check(self, now)); - } - return 0; -} -static int timeout_handler_osd_remove(struct plugin *self, int64_t now){ - overlay_remove(self, OSD_STORYBOARD); - return 0; -} -static int timeout_handler_osc_check(struct plugin *self, int64_t now){ - Z(osc_check(self, now)); - 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; - case MPV_EVENT_PROPERTY_CHANGE: - mpv_event_property *prop = ev->data; - switch(ev->reply_userdata){ - case OBSERVE_OSC: - Z(event_on_observe_osc(self, prop)); - break; - default: - return -1; - } - return 0; - return 0; - default: - return 0; - } -} -static double event_wait_timeout(struct plugin *self){ - double next = -1; - int64_t now = mpv_get_time_ns(self->m); - -recheck_osd_remove: - if(self->osd_remove_time_ns){ - if(self->osd_remove_time_ns <= now){ - self->osd_remove_time_ns = 0; - timeout_handler_osd_remove(self, now); - goto recheck_osd_remove; - }else{ - double mynext = (self->osd_remove_time_ns - now) / 1.0e9; - if(mynext < 0) mynext = 0; - if(next >= 0 && mynext < next){ - next = mynext; - } - } - } - -recheck_osc_check: - if(self->osc_check_time_ns){ - if(self->osc_check_time_ns <= now){ - self->osc_check_time_ns = 0; - timeout_handler_osc_check(self, now); - goto recheck_osc_check; - }else{ - double mynext = (self->osc_check_time_ns - now) / 1.0e9; - if(mynext < 0) mynext = 0; - if(next >= 0 && mynext < next){ - next = mynext; - } - } - } - - return next; -} - -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; -} diff --git a/ytdlsb-main.c b/ytdlsb-main.c new file mode 100644 index 0000000..4956743 --- /dev/null +++ b/ytdlsb-main.c @@ -0,0 +1,664 @@ +#include <mpv/client.h> +#include <assert.h> +#include <string.h> +#include <stdlib.h> +#include <stdio.h> +#include "ytdlsb-utils.h" +#include "ytdlsb-tasks.h" +#include "ytdlsb-mpv.h" +#include <cJSON.h> +#include <curl/curl.h> +#include <webp/decode.h> + +enum ytdlsb_hook{ + YTDLSB_HOOK_PRELOADED, + YTDLSB_HOOK_UNLOAD +}; +enum ytdlsb_observe{ + YTDLSB_OBSERVE_OSC +}; +enum ytdlsb_osd_id{ + YTDLSB_OSD_IMG +}; + +enum ytdlsb_sb_state{ + YTDLSB_SB_EMPTY = 0, + YTDLSB_SB_FAILED, + YTDLSB_SB_JPEG, + YTDLSB_SB_WEBP, + YTDLSB_SB_RAW, +}; +struct ytdlsb_sb_fragment{ + char *url; + enum ytdlsb_sb_state state; + char *data; + size_t data_len; + double start; +}; +struct ytdlsb_sb{ + double fps; + size_t width; + size_t height; + size_t columns; + size_t rows; + struct ytdlsb_sb_fragment *fragments; + size_t fragment_num; +}; + +enum ytdlsb_tasknum{ + YTDLSB_TASK_MPVEV = 0, + YTDLSB_TASK_PRELOAD, + YTDLSB_TASK_NUM +}; + +struct ytdlsb{ + mpv_handle *m; + struct ytdlsb_tasks t; + struct ytdlsb_sb best_sb; + struct ytdlsb_sb fast_sb; + + struct curl_slist *preload_headers; + size_t preload_p; + CURL *preload_easy; + CURLM *preload_multi; + + double current_sb_show; + + int exit; +}; + +int ytdlsb_sb_some(struct ytdlsb_sb *sb){ + return sb->fragments != NULL; +} + +#define FRAG(p, sb, pp) \ + if(p->preload_p < p->fast_sb.fragment_num){ \ + pp = p->preload_p; \ + sb = &p->fast_sb; \ + }else{ \ + pp = p->preload_p - p->fast_sb.fragment_num; \ + assert(pp < p->best_sb.fragment_num); \ + sb = &p->best_sb; \ + } +size_t ytdlsb_fragment_preload_write( + char *data, size_t, size_t nmemb, void *_p +){ + struct ytdlsb *p = _p; + size_t pp; + struct ytdlsb_sb *sb; + FRAG(p, sb, pp); + + //eprintf("received %zubytes\n", nmemb); + + struct ytdlsb_sb_fragment *f = &sb->fragments[pp]; + if(f->data == NULL) f->data_len = 0; + f->data_len += nmemb; + CKA(f->data_len, >= nmemb); + f->data = CKAR(realloc(f->data, f->data_len)); + memcpy(&f->data[f->data_len - nmemb], data, nmemb); + return nmemb; +} +int ytdlsb_start_fragment_preload(struct ytdlsb *p){ + size_t pp; + struct ytdlsb_sb *sb; + + FRAG(p, sb, pp); + assert(sb->fragments[pp].state == YTDLSB_SB_EMPTY); + +#define OPT(k, v) CKZ(err, curl_easy_setopt(p->preload_easy, k, v)) + OPT(CURLOPT_URL, sb->fragments[pp].url); +#undef OPT + return 0; +err: return -1; +} +int ytdlsb_abort_load(struct ytdlsb *p){ + if(p->preload_easy){ + CKZ(err, curl_multi_remove_handle(p->preload_multi, p->preload_easy)); + curl_easy_cleanup(p->preload_easy); + p->preload_easy = NULL; + curl_slist_free_all(p->preload_headers); + p->preload_headers = NULL; + } + p->t.tasks[YTDLSB_TASK_PRELOAD].process = NULL; + return 0; +err: return -1; +} +int ytdlsb_done_fragment_preload(struct ytdlsb *p){ + size_t max_prelp = p->fast_sb.fragment_num + p->best_sb.fragment_num; + while(1){ + size_t pp; + struct ytdlsb_sb *sb; + p->preload_p++; + if(p->preload_p < max_prelp){ + FRAG(p, sb, pp); + if(sb->fragments[pp].state == YTDLSB_SB_EMPTY) break; + }else{ + break; + } + } + if(p->preload_p < max_prelp){ + CKZ(err, curl_multi_remove_handle( + p->preload_multi, p->preload_easy)); + ytdlsb_start_fragment_preload(p); + CKZ(err, curl_multi_add_handle( + p->preload_multi, p->preload_easy)); + eprintf("downloading storyboard: %zu/%zu\n", + p->preload_p, max_prelp); + return 1; + }else{ + eprintf("storyboard downloaded\n"); + CKP(err, ytdlsb_abort_load(p)); + return 0; + } +err: return -1; +} +int ytdlsb_process_preload(struct ytdlsb_task *t){ + struct ytdlsb *p = t->data; + size_t max_prelp = p->fast_sb.fragment_num + p->best_sb.fragment_num; + int running; + while(1){ + CKZ(err, curl_multi_perform(p->preload_multi, &running)); + + if(!running){ + int mq; + size_t pp; + struct ytdlsb_sb *sb; + FRAG(p, sb, pp); + + CURLMsg *m = curl_multi_info_read(p->preload_multi, &mq); + assert(m != NULL); + assert(m->msg == CURLMSG_DONE); + assert(m->easy_handle == p->preload_easy); + assert(mq == 0); + + if(m->data.result == 0){ + long respc; + char *mtype; + CKZ(err, curl_easy_getinfo(p->preload_easy, + CURLINFO_RESPONSE_CODE, &respc)); + CKZ(err, curl_easy_getinfo(p->preload_easy, + CURLINFO_CONTENT_TYPE, &mtype)); + if(respc == 200){ + if(strcmp(mtype, "image/jpeg") == 0){ + sb->fragments[pp].state = YTDLSB_SB_JPEG; + }else if(strcmp(mtype, "image/webp") == 0){ + sb->fragments[pp].state = YTDLSB_SB_WEBP; + }else{ + eprintf("invalid storyboard type: %s\n", mtype); + sb->fragments[pp].state = YTDLSB_SB_FAILED; + } + }else{ + eprintf("invalid storyboard response: %03ld\n", respc); + sb->fragments[pp].state = YTDLSB_SB_FAILED; + } + }else{ + eprintf("storyboard download error: %d\n", (int)m->data.result); + sb->fragments[pp].state = YTDLSB_SB_FAILED; + } + + if(CKP(err, ytdlsb_done_fragment_preload(p))) continue; + break; + }else{ + break; + } + } + CKP(err, ytdlsb_task_fdto_from_curl(&p->t.tasks[YTDLSB_TASK_PRELOAD], + 0, 1, p->preload_multi)); + return 0; +err: return -1; +} +#undef FRAG +int ytdlsb_start_preload(struct ytdlsb *p){ + CKP(err, ytdlsb_abort_load(p)); + + if(!ytdlsb_sb_some(&p->fast_sb)){ + return 0; + } + + if(!p->preload_multi){ + p->preload_multi = CKR(err, curl_multi_init()); + } + p->preload_easy = CKR(err, curl_easy_init()); + + p->preload_headers = CKR(err, + curl_slist_append(p->preload_headers, "Accept: image/webp,*/*;q=0.1")); +#define OPT(k, v) CKZ(err, curl_easy_setopt(p->preload_easy, k, v)) + OPT(CURLOPT_REDIR_PROTOCOLS_STR, "http,https"); + OPT(CURLOPT_PROTOCOLS_STR, "http,https"); + OPT(CURLOPT_FOLLOWLOCATION, 1l); + OPT(CURLOPT_NOSIGNAL, 1l); + OPT(CURLOPT_TIMEOUT_MS, 10000l); + OPT(CURLOPT_CONNECTTIMEOUT_MS, 3000l); + OPT(CURLOPT_HTTPHEADER, p->preload_headers); + OPT(CURLOPT_WRITEFUNCTION, ytdlsb_fragment_preload_write); + OPT(CURLOPT_WRITEDATA, (void *)p); +#undef OPT + + p->preload_p = 0; + ytdlsb_start_fragment_preload(p); + CKZ(err, curl_multi_add_handle(p->preload_multi, p->preload_easy)); + + p->t.tasks[YTDLSB_TASK_PRELOAD].process = ytdlsb_process_preload; + p->t.tasks[YTDLSB_TASK_PRELOAD].data = p; + ytdlsb_process_preload(&p->t.tasks[YTDLSB_TASK_PRELOAD]); + return 0; +err: return -1; +} +int ytdlsb_cleanup_load(struct ytdlsb *p){ + CKP(err, ytdlsb_abort_load(p)); + if(p->preload_multi){ + CKZ(err, curl_multi_cleanup(p->preload_multi)); + p->preload_multi = NULL; + } + free(p->t.tasks[YTDLSB_TASK_PRELOAD].pollfd); + return 0; +err: return -1; +} + +void ytdlsb_sb_destroy(struct ytdlsb_sb *sb){ + if(ytdlsb_sb_some(sb)){ + for(size_t i = 0; i < sb->fragment_num; i++){ + free(sb->fragments[i].url); + free(sb->fragments[i].data); + } + free(sb->fragments); + sb->fragments = NULL; + } +} +void ytdlsb_sb_dup(struct ytdlsb_sb *dst, struct ytdlsb_sb *src){ + ytdlsb_sb_destroy(dst); + if(ytdlsb_sb_some(src)){ + memcpy(dst, src, sizeof(*dst)); + dst->fragments = CKAR(reallocarray(NULL, + dst->fragment_num, sizeof(*dst->fragments))); + for(size_t i = 0; i < dst->fragment_num; i++){ + struct ytdlsb_sb_fragment *dstf = &dst->fragments[i]; + struct ytdlsb_sb_fragment *srcf = &src->fragments[i]; + dstf->url = CKAR(strdup(srcf->url)); + dstf->start = srcf->start; + dstf->state = srcf->state; + if(srcf->data){ + dstf->data = CKAR(malloc(srcf->data_len)); + memcpy(dstf->data, srcf->data, srcf->data_len); + dstf->data_len = srcf->data_len; + }else{ + dstf->data = NULL; + } + } + } +} +double ytdlsb_sb_quality(struct ytdlsb_sb *sb){ + assert(ytdlsb_sb_some(sb)); + return sb->width * sb->height * sb->fps; +} +int ytdlsb_sb_import_json(struct ytdlsb_sb *sb, cJSON *json){ + cJSON *frag, *p; + double start; + ytdlsb_sb_destroy(sb); +#define JSON_OI(json, key) cJSON_GetObjectItemCaseSensitive(json, key) +#define JSON_NUM(json) \ + CK_MSG(err, cJSON_GetNumberValue(json), == cktmp, "NaN check") +#define JSON_STR(json) CKR(err, cJSON_GetStringValue(json)) + char *format_id = JSON_STR(JSON_OI(json, "format_id")); + if(strncmp(format_id, "sb", 2) != 0) goto err; + if(format_id[2] < '0' || '9' < format_id[2]) goto err; + if(format_id[3] != 0) goto err; + + sb->fps = JSON_NUM(JSON_OI(json, "fps")); + CK(err, sb->fps, > 0); +#define SIZEF(f) \ + sb->f = CK(err, TRY_NUMCAST(err, size_t, JSON_NUM(JSON_OI(json, #f))), > 0) + SIZEF(width); + SIZEF(height); + SIZEF(columns); + SIZEF(rows); +#undef SIZEF + frag = JSON_OI(json, "fragments"); + CK(err, cJSON_IsArray(frag),); + sb->fragment_num = CK(err, cJSON_GetArraySize(frag), > 0); + CK_MSG(err, sb->fragment_num, < 4096, "Too many fragments"); + CK(err, sb->fragment_num, > 0); + sb->fragments = CKAR(reallocarray(NULL, + sb->fragment_num, sizeof(*sb->fragments))); + p = frag->child; + start = 0; + for(size_t i = 0; i < sb->fragment_num; i++){ + assert(p); + struct ytdlsb_sb_fragment *f = &sb->fragments[i]; + f->url = CKR(err, strdup(JSON_STR(JSON_OI(p, "url")))); + f->state = YTDLSB_SB_EMPTY; + f->data = NULL; + f->start = start; + start += JSON_NUM(JSON_OI(p, "duration")); + p = p->next; + } + return 0; +#undef JSON_OI +#undef JSON_NUM +#undef JSON_STR +err: + free(sb->fragments); sb->fragments = NULL; + return -1; +} +void ytdlsb_sb_dump(struct ytdlsb_sb *sb){ + if(!ytdlsb_sb_some(sb)){ + eprintf("None\n"); + return; + } + eprintf("FPS: %f\n", sb->fps); + eprintf("WxH, CxR: %zux%zu, %zux%zu\n", + sb->width, sb->height, sb->columns, sb->rows); + for(size_t i = 0; i < sb->fragment_num; i++){ + eprintf("%s\n", sb->fragments[i].url); + if(5 < i){ + eprintf("...\n"); + break; + } + } +} + +// <0 = error (only on first error) +// 0 = not loaded +// 1 = ref +// 2 = copy(?) +int ytdlsb_sb_get_frame( + struct ytdlsb_sb *sb, double pos, char **data, size_t *len +){ + struct ytdlsb_sb_fragment *f = NULL; + for(size_t i = 0; i < sb->fragment_num; i++){ + struct ytdlsb_sb_fragment *cf = &sb->fragments[i]; + if(cf->start <= pos) f = cf; + else break; + } + //eprintf("fragment %d\n", (int)((f-sb->fragments)/sizeof(*f))); + if(!f || f->state == YTDLSB_SB_EMPTY) return 0; + //eprintf(".!!!\n"); + if(f->state == YTDLSB_SB_WEBP){ + //eprintf("webp!\n"); + char *decoded; + int width, height; + if(is_little_endian()){ + decoded = (char *)CKR(decode_failed, WebPDecodeBGRA( + (uint8_t *)f->data, f->data_len, &width, &height)); + }else{ + decoded = (char *)CKR(decode_failed, WebPDecodeRGBA( + (uint8_t *)f->data, f->data_len, &width, &height)); + } + CK(sizemismatch, width, <= sb->columns * sb->width); + CK(sizemismatch, width % sb->width, == 0); + CK(sizemismatch, height, <= sb->rows * sb->height); + CK(sizemismatch, height % sb->height, == 0); + f->data_len = TRY_NUMCAST(sizemismatch, size_t, width) + * TRY_NUMCAST(sizemismatch, size_t, height) * 4; + free(f->data); + f->data = CKAR(malloc(f->data_len)); + size_t frame = sb->width * sb->height * 4; + for(size_t y = 0; y < height/sb->height; y++){ + for(size_t x = 0; x < width/sb->width; x++){ + char *src_orig = decoded + (x*sb->width + y*sb->height*width)*4; + char *dest_orig = f->data + (x + y*(width/sb->width))*frame; + for(size_t i = 0; i < sb->height; i++){ + assert(src_orig + i*width*4 + 4*sb->width + <= decoded+f->data_len); + assert(dest_orig + i*sb->width*4 + 4*sb->width + <= f->data+f->data_len); + memcpy(dest_orig + i*sb->width*4, + src_orig + i*width*4, 4*sb->width); + } + } + } + free(decoded); + f->state = YTDLSB_SB_RAW; + goto webpok; +sizemismatch: + free(decoded); + goto decode_failed; +webpok: + }else if(f->state == YTDLSB_SB_JPEG){ + eprintf("Jpeg decoding is not implemented yet.\n"); + goto decode_failed; + } + if(f->state == YTDLSB_SB_RAW){ + //eprintf("raw!\n"); + size_t frame = sb->width * sb->height * 4; + size_t findex = (pos - f->start) * sb->fps; + if(f->data_len < findex*frame+frame){ + findex = (f->data_len - frame) / frame; + } + *data = f->data + frame*findex; + *len = frame; + return 1; + } + return 0; +decode_failed: + f->state = YTDLSB_SB_FAILED; + return -1; +} +int ytdlsb_get_frame(struct ytdlsb *p, double pos, + char **data, size_t *w, size_t *h +){ + size_t len; + if(ytdlsb_sb_some(&p->best_sb) + && ytdlsb_sb_get_frame(&p->best_sb, pos, data, &len) == 1 + ){ + assert(p->best_sb.width * p->best_sb.height * 4 == len); + *w = p->best_sb.width; + *h = p->best_sb.height; + return 1; + } + if(ytdlsb_sb_some(&p->fast_sb) + && ytdlsb_sb_get_frame(&p->fast_sb, pos, data, &len) == 1 + ){ + assert(p->fast_sb.width * p->fast_sb.height * 4 == len); + *w = p->fast_sb.width; + *h = p->fast_sb.height; + return 1; + } + return 0; +} + +int ytdlsb_hook_on_preloaded(struct ytdlsb *p){ + ytdlsb_sb_destroy(&p->best_sb); + ytdlsb_sb_destroy(&p->fast_sb); + char *ytjson_w, *ytjson; + cJSON *yt_w, *yt, *yt_formats, *fmt; + ytjson_w = mpv_get_property_string( + p->m, "user-data/mpv/ytdl/json-subprocess-result"); + if(!ytjson_w) goto noytdl; + yt_w = CKR(broken_json_w, cJSON_Parse(ytjson_w)); + ytjson = CKR(broken_json, + cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(yt_w, "stdout"))); + yt = CKR(broken_json, cJSON_Parse(ytjson)); + yt_formats = CKR(end_json, + cJSON_GetObjectItemCaseSensitive(yt, "formats")); + CKT(end_json, cJSON_IsArray(yt_formats)); + while(fmt = cJSON_DetachItemFromArray(yt_formats, 0)){ + struct ytdlsb_sb sb = {0}; + if(ytdlsb_sb_import_json(&sb, fmt) >= 0){ + int refed = 0; + if(!ytdlsb_sb_some(&p->best_sb) + || ytdlsb_sb_quality(&p->best_sb) < ytdlsb_sb_quality(&sb) + ){ + ytdlsb_sb_destroy(&p->best_sb); + memcpy(&p->best_sb, &sb, sizeof(sb)); + refed = 1; + } + if(!ytdlsb_sb_some(&p->fast_sb) + || ytdlsb_sb_quality(&p->fast_sb) > ytdlsb_sb_quality(&sb) + ){ + ytdlsb_sb_destroy(&p->fast_sb); + if(refed){ + ytdlsb_sb_dup(&p->fast_sb, &sb); + }else{ + memcpy(&p->fast_sb, &sb, sizeof(sb)); + refed = 1; + } + } + if(!refed) ytdlsb_sb_destroy(&sb); + } + cJSON_Delete(fmt); + } + ytdlsb_start_preload(p); + ytdlsb_sb_dump(&p->fast_sb); + ytdlsb_sb_dump(&p->best_sb); +end_json: + cJSON_Delete(yt); +broken_json: + cJSON_Delete(yt_w); +broken_json_w: + mpv_free(ytjson_w); +noytdl: + return 0; +} + +int ytdlsb_hook_on_unload(struct ytdlsb *p){ + ytdlsb_abort_load(p); + ytdlsb_sb_destroy(&p->best_sb); + ytdlsb_sb_destroy(&p->fast_sb); + return 0; +} +int ytdlsb_propchange_osc(struct ytdlsb *p, mpv_event_property *ep){ + char *image; + size_t width, height; + double pos; + char *osc_activeelem; + int osc_visible; + int ret = -1; + +#define GETPROP(key, ty, ptr, def) { \ + int getres = mpv_get_property(p->m, key, ty, ptr); \ + if(getres < 0 && getres != MPV_ERROR_PROPERTY_NOT_FOUND \ + && getres != MPV_ERROR_PROPERTY_UNAVAILABLE \ + ){ \ + goto err; \ + }else if(getres < 0){ \ + *(ptr) = def; \ + } \ +} + GETPROP("user-data/osc/visible", + MPV_FORMAT_FLAG, &osc_visible, 0); + GETPROP("user-data/osc/active-element", + MPV_FORMAT_STRING, &osc_activeelem, NULL); + GETPROP("user-data/osc/seekbar/possec", + MPV_FORMAT_DOUBLE, &pos, -1); +#undef GETPROP + + if(!osc_visible) pos = -1; + if(!osc_activeelem || strcmp(osc_activeelem, "\"seekbar\"")) pos = -1; + if(pos >= 0 && CKP(err, ytdlsb_get_frame(p, pos, &image, &width, &height))){ + CKP(err, ytdlsb_mpv_overlay_add(p->m, + YTDLSB_OSD_IMG, 0, 0, + image, 0, width, height, width*4, + width, height + )); + p->current_sb_show = pos; + }else{ + CKP(err, ytdlsb_mpv_overlay_remove(p->m, YTDLSB_OSD_IMG)); + p->current_sb_show = -1; + } + ret = 0; +err: + mpv_free(osc_activeelem); + return ret; +} + +int ytdlsb_ev(struct ytdlsb *p, mpv_event *ev){ + switch(ev->event_id){ + case MPV_EVENT_HOOK: + mpv_event_hook *hook = ev->data; + switch(ev->reply_userdata){ + case YTDLSB_HOOK_PRELOADED: + CKP(err, ytdlsb_hook_on_preloaded(p)); + break; + case YTDLSB_HOOK_UNLOAD: + CKP(err, ytdlsb_hook_on_unload(p)); + break; + } + CKM(err, mpv_hook_continue(p->m, hook->id)); + return 0; + case MPV_EVENT_PROPERTY_CHANGE: + switch(ev->reply_userdata){ + case YTDLSB_OBSERVE_OSC: + CKP(err, ytdlsb_propchange_osc(p, ev->data)); + break; + } + return 0; + default: + return 0; + } +err: + return -1; +} + +void ytdlsb_mpvev_waker(void *_p){ + struct ytdlsb *p = _p; + CKAP(ytdlsb_task_event_wake(&p->t.tasks[YTDLSB_TASK_MPVEV])); +} +int ytdlsb_mpvev(void *_p){ + struct ytdlsb *p = _p; + + while(1){ + mpv_event *ev = CKR(err, mpv_wait_event(p->m, 0)); + if(ev->event_id == MPV_EVENT_SHUTDOWN){ + p->exit = 1; + break; + }else if(ev->event_id == MPV_EVENT_NONE){ + break; + }else{ + CKP(err, ytdlsb_ev(p, ev)); + } + } + + return 0; +err: + return -1; +} + +int mpv_open_cplugin(mpv_handle *h){ + int ret = -1; + struct ytdlsb p = {0}; + struct ytdlsb_task tasks[YTDLSB_TASK_NUM] = {{0}}; + curl_version_info_data *curl_v; + + curl_v = CKR(err, curl_version_info(CURLVERSION_NOW)); + // Main thread of mpv is blocked here. + // I think it is safe enough to call this. + // (But can make conflicts with other plugins...) + CKZ(err, curl_global_init(CURL_GLOBAL_ALL)); + + p.m = h; + p.t.tasks = tasks; + p.t.task_num = YTDLSB_TASK_NUM; + p.current_sb_show = -1; + CKM(err, mpv_hook_add(p.m, YTDLSB_HOOK_PRELOADED, "on_preloaded", 0)); + CKM(err, mpv_hook_add(p.m, YTDLSB_HOOK_UNLOAD, "on_unload", 0)); + CKM(err, mpv_observe_property(p.m, YTDLSB_OBSERVE_OSC, + "user-data/osc/visible", MPV_FORMAT_NONE)); + CKM(err, mpv_observe_property(p.m, YTDLSB_OBSERVE_OSC, + "user-data/osc/active-element", MPV_FORMAT_NONE)); + CKM(err, mpv_observe_property(p.m, YTDLSB_OBSERVE_OSC, + "user-data/osc/seekbar/possec", MPV_FORMAT_NONE)); + + CKP(err, ytdlsb_task_event_init(&tasks[YTDLSB_TASK_MPVEV], + ytdlsb_mpvev, &p)); + mpv_set_wakeup_callback(h, ytdlsb_mpvev_waker, &p); + CKP(err, ytdlsb_task_event_wake(&tasks[YTDLSB_TASK_MPVEV])); + + while(!p.exit){ + CKP(err, ytdlsb_tasks_step(&p.t)); + } + + CKP(err, ytdlsb_cleanup_load(&p)); + ytdlsb_task_event_destroy(&tasks[YTDLSB_TASK_MPVEV]); + ytdlsb_sb_destroy(&p.best_sb); + ytdlsb_sb_destroy(&p.fast_sb); + + if(curl_v->features & CURL_VERSION_THREADSAFE){ + // If it is not thread-safe, not clean. + // It may lead to memory leaks. Better than UB. + curl_global_cleanup(); + } + ret = 0; +err: + return ret; +} diff --git a/ytdlsb-mpv.c b/ytdlsb-mpv.c new file mode 100644 index 0000000..f5f7600 --- /dev/null +++ b/ytdlsb-mpv.c @@ -0,0 +1,102 @@ +#include "ytdlsb-mpv.h" +#include "ytdlsb-utils.h" +#include <stdio.h> +#include <assert.h> + +int ytdlsb_mpv_overlay_add(mpv_handle *m, + int id, int x, int y, + char *buf, size_t off, size_t w, size_t h, size_t stride, + size_t dw, size_t dh +){ + char *command_keys[] = { + "name", "id", "x", "y", + "file", "offset", "fmt", "w", "h", "stride", + "dw", "dh", + }; + char filename[32]; // enough for 64bit int + 1byte prefix + CKA(snprintf(filename, 32, "&%tu", (ptrdiff_t)((void *)buf - NULL)), < 32); + mpv_node command_vals[] = { + { + .format = MPV_FORMAT_STRING, + .u.string = "overlay-add", + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = TRY_NUMCAST(a, int64_t, id), + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = TRY_NUMCAST(a, int64_t, x), + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = TRY_NUMCAST(a, int64_t, y), + }, { + .format = MPV_FORMAT_STRING, + .u.string = filename, + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = TRY_NUMCAST(a, int64_t, off), + }, { + .format = MPV_FORMAT_STRING, + .u.string = "bgra", + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = TRY_NUMCAST(a, int64_t, w), + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = TRY_NUMCAST(a, int64_t, h), + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = TRY_NUMCAST(a, int64_t, stride), + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = TRY_NUMCAST(a, int64_t, dw), + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = TRY_NUMCAST(a, int64_t, 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, + }; + CKM(err, mpv_command_node(m, &command, NULL)); + return 0; +a: + abort(); +err: + return -1; +} + +int ytdlsb_mpv_overlay_remove(mpv_handle *m, int id){ + char *command_keys[] = {"name", "id"}; + mpv_node command_vals[] = { + { + .format = MPV_FORMAT_STRING, + .u.string = "overlay-remove", + }, { + .format = MPV_FORMAT_INT64, + .u.int64 = TRY_NUMCAST(a, int64_t, 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, + }; + CKM(err, mpv_command_node(m, &command, NULL)); + return 0; +a: abort(); +err: return -1; +} diff --git a/ytdlsb-mpv.h b/ytdlsb-mpv.h new file mode 100644 index 0000000..11a0562 --- /dev/null +++ b/ytdlsb-mpv.h @@ -0,0 +1,8 @@ +#include <mpv/client.h> + +int ytdlsb_mpv_overlay_add(mpv_handle *m, + int id, int x, int y, + char *buf, size_t off, size_t w, size_t h, size_t stride, + size_t dw, size_t dh +); +int ytdlsb_mpv_overlay_remove(mpv_handle *m, int id); diff --git a/ytdlsb-tasks.c b/ytdlsb-tasks.c new file mode 100644 index 0000000..0e97a94 --- /dev/null +++ b/ytdlsb-tasks.c @@ -0,0 +1,234 @@ +#define _GNU_SOURCE +#include "ytdlsb-tasks.h" +#include <assert.h> +#include <string.h> +#include <stdlib.h> +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> +#include <limits.h> +#include <curl/curl.h> + +static void get_now(struct timespec *t){ + CKAP(clock_gettime(CLOCK_MONOTONIC, t)); +} +static void set_time_ms(struct timespec *dst, int ms){ + dst->tv_sec = ms / 1000; + dst->tv_nsec = ms % 1000 * 1000 * 1000; +} +static int get_time_ms(struct timespec *src){ + if((INT_MAX - 999) / 1000 < src->tv_sec){ + return (INT_MAX - 999) / 1000 * 1000; // lazy saturation + } + return (int)src->tv_sec * 1000 + src->tv_nsec / 1000 / 1000; +} +static void copy_time(struct timespec *dst, struct timespec *src){ + memcpy(dst, src, sizeof(*dst)); +} +static void add_time(struct timespec *dst, struct timespec *src){ + dst->tv_sec += src->tv_sec; + dst->tv_nsec -= 1000 * 1000 * 1000; + dst->tv_nsec += src->tv_nsec; + if(dst->tv_nsec < 0) dst->tv_nsec += 1000 * 1000 * 1000; + else dst->tv_sec += 1; +} +static void sub_time(struct timespec *dst, struct timespec *src){ + dst->tv_sec -= src->tv_sec; + dst->tv_nsec -= src->tv_nsec; + if(src->tv_nsec < 0){ + dst->tv_sec -= 1; + dst->tv_nsec += 1000 * 1000 * 1000; + } +} +static int lt_time(struct timespec *lt, struct timespec *gt){ + return lt->tv_sec < gt->tv_sec + || lt->tv_sec == gt->tv_sec && lt->tv_nsec < gt->tv_nsec; +} + +int ytdlsb_task_timeout_ms(struct ytdlsb_task *task, int ms){ + struct timespec a; + set_time_ms(&a, ms); + get_now(&task->timeout); + add_time(&task->timeout, &a); + return 0; +} +int ytdlsb_task_timeout_ms_min(struct ytdlsb_task *task, int ms){ + struct timespec a, now; + get_now(&now); + set_time_ms(&a, ms); + add_time(&a, &now); + if(get_time_ms(&task->timeout) == 0 || lt_time(&a, &task->timeout)){ + copy_time(&task->timeout, &a); + } + return 0; +} +int ytdlsb_task_timeout_unset(struct ytdlsb_task *task){ + set_time_ms(&task->timeout, 0); + return 0; +} +int ytdlsb_tasks_step(struct ytdlsb_tasks *tasks){ + int ret = -1; + struct pollfd *fds = NULL; + size_t *fdso = NULL; + struct timespec now; + size_t last_fd = 0; + get_now(&now); + fdso = CKAR(reallocarray(NULL, tasks->task_num, sizeof(*fdso))); + int timeout = -1; + nfds_t nfds = 0; + for(size_t i = 0; i < tasks->task_num; i++){ + struct ytdlsb_task *t = &tasks->tasks[i]; + if(!t->process){ + fdso[i] = nfds; + continue; + } + + if(get_time_ms(&t->timeout) == 0){ + // do nothing + }else if(lt_time(&t->timeout, &now)){ + timeout = 0; + }else{ + struct timespec tdiff; + copy_time(&tdiff, &t->timeout); + sub_time(&tdiff, &now); + int ms = get_time_ms(&tdiff); + if(timeout == -1 || timeout < ms) timeout = ms; + } + + fdso[i] = nfds + t->pollfd_num; + if(t->pollfd_num){ + nfds += t->pollfd_num; + fds = CKAR(reallocarray(fds, nfds, sizeof(*fds))); + for(size_t j = 0; j < t->pollfd_num; j++){ + struct pollfd *dst = &fds[nfds - t->pollfd_num + j]; + dst->fd = t->pollfd[j].fd; + dst->events = t->pollfd[j].events; + } + } + } + CKP(err, poll(fds, nfds, timeout)); + get_now(&now); + for(size_t i = 0; i < tasks->task_num; i++){ + struct ytdlsb_task *t = &tasks->tasks[i]; + int want_call = 0; + if(get_time_ms(&t->timeout) && lt_time(&t->timeout, &now)) + want_call |= 1; + for(size_t j = last_fd; !want_call && j < fdso[i]; j++){ + short er = fds[j].events; + short re = fds[j].revents; + CK(err, re & POLLNVAL, == 0); + want_call |= re&POLLHUP || re&POLLERR; + want_call |= er&POLLIN && re&POLLIN; + want_call |= er&POLLOUT && re&POLLOUT; + want_call |= er&POLLPRI && re&POLLPRI; + } + if(t->process && want_call){ + CKP(err, t->process(t)); + } + last_fd = fdso[i]; + } + ret = 0; +err: + free(fds); + free(fdso); + return ret; +} + +struct ytdlsb_task_event{ + int pipe[2]; + int (*cb)(void *); + void *data; +}; +static int ytdlsb_task_event_cb(struct ytdlsb_task *t){ + struct ytdlsb_task_event *e = t->data; + ssize_t rs; + while(1){ + int buf; + rs = read(e->pipe[0], &buf, sizeof(buf)); + if(rs == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) break; + CK_MSG(err, rs, > 0, "pipe read error"); + } + return e->cb(e->data) < 0 ? -1 : 0; +err: return -1; +} +int ytdlsb_task_event_init( + struct ytdlsb_task *task, int (*cb)(void *), void *data +){ + int ret = -1; + struct ytdlsb_task_event *e; + e = CKAR(malloc(sizeof(*e))); + CKP(err, pipe2(e->pipe, O_CLOEXEC|O_NONBLOCK)); + e->cb = cb; + e->data = data; + task->process = ytdlsb_task_event_cb; + task->pollfd = CKAR(malloc(sizeof(*task->pollfd))); + task->pollfd[0].fd = e->pipe[0]; + task->pollfd[0].events = YTDLSB_POLLIN; + task->pollfd_num = 1; + task->data = e; + e = NULL; + ret = 0; +err: + free(e); + return ret; +} +void ytdlsb_task_event_destroy(struct ytdlsb_task *task){ + struct ytdlsb_task_event *e = task->data; + CKAP(close(e->pipe[0])); + CKAP(close(e->pipe[1])); + free(e); + free(task->pollfd); +} +int ytdlsb_task_event_wake(struct ytdlsb_task *task){ + struct ytdlsb_task_event *e = task->data; + ssize_t ws = write(e->pipe[1], "\n", 1); + if(ws == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0; + CK_MSG(err, ws, > 0, "pipe write error"); + return 0; +err: + return -1; +} + +int ytdlsb_task_fdto_from_curl( + struct ytdlsb_task *task, size_t off, int overwrite_timeout, CURLM* cm +){ + int ret = -1; + unsigned int ncfds = 0; + struct curl_waitfd *cfds = NULL; + long timeout; + + CKZ(err, curl_multi_timeout(cm, &timeout)); + if(overwrite_timeout && timeout < 0){ + CKP(err, ytdlsb_task_timeout_unset(task)); + }else if(overwrite_timeout){ + CKP(err, ytdlsb_task_timeout_ms(task, TRY_NUMCAST(err, int, timeout))); + }else{ + CKP(err, ytdlsb_task_timeout_ms_min(task, + TRY_NUMCAST(err, int, timeout))); + } + + CKZ(err, curl_multi_waitfds(cm, NULL, 0, &ncfds)); + if(ncfds == 0){ + task->pollfd_num = off; + return 0; + } + cfds = CKAR(reallocarray(NULL, ncfds, sizeof(*cfds))); + CKZ(err, curl_multi_waitfds(cm, cfds, ncfds, NULL)); + task->pollfd = CKAR(reallocarray(task->pollfd, + off+ncfds, sizeof(*task->pollfd))); + for(size_t i = 0; i < ncfds; i++){ + task->pollfd[off+i].fd = cfds[i].fd; + task->pollfd[off+i].events = 0; +#define PT(t) if(cfds[i].fd & CURL_WAIT_POLL ## t) \ + task->pollfd[off+i].events |= YTDLSB_POLL ## t + PT(IN); + PT(PRI); + PT(OUT); +#undef PT + } + task->pollfd_num = off + ncfds; + ret = ncfds; +err: + free(cfds); + return ret; +} diff --git a/ytdlsb-tasks.h b/ytdlsb-tasks.h new file mode 100644 index 0000000..86d3070 --- /dev/null +++ b/ytdlsb-tasks.h @@ -0,0 +1,39 @@ +#include <poll.h> +#include <time.h> +#include <curl/curl.h> +#include "ytdlsb-utils.h" + +typedef int ytdlsb_sock; +#define YTDLSB_POLLIN POLLIN +#define YTDLSB_POLLPRI POLLPRI +#define YTDLSB_POLLOUT POLLOUT + +struct ytdlsb_pollfd{ + ytdlsb_sock fd; + short events; + short revents; +}; +struct ytdlsb_task{ + int (*process)(struct ytdlsb_task *); + struct ytdlsb_pollfd *pollfd; + size_t pollfd_num; + struct timespec timeout; + void *data; +}; +struct ytdlsb_tasks{ + struct ytdlsb_task *tasks; + size_t task_num; +}; + +int ytdlsb_tasks_step(struct ytdlsb_tasks *tasks); +int ytdlsb_task_timeout_ms(struct ytdlsb_task *task, int ms); +int ytdlsb_task_timeout_ms_min(struct ytdlsb_task *task, int ms); +int ytdlsb_task_timeout_unset(struct ytdlsb_task *task); + +int ytdlsb_task_event_init(struct ytdlsb_task *task, + int (*cb)(void *), void *data); +void ytdlsb_task_event_destroy(struct ytdlsb_task *task); +int ytdlsb_task_event_wake(struct ytdlsb_task *task); + +int ytdlsb_task_fdto_from_curl( + struct ytdlsb_task *task, size_t off, int overwrite_timeout, CURLM* cm); diff --git a/ytdlsb-utils.h b/ytdlsb-utils.h new file mode 100644 index 0000000..abb581b --- /dev/null +++ b/ytdlsb-utils.h @@ -0,0 +1,45 @@ +#ifndef YTDLSB_UTILS_H +#define YTDLSB_UTILS_H + +#include <stdio.h> +#include <stdint.h> +#include <stdlib.h> +#include <inttypes.h> + +inline static int is_little_endian(){ + int i = 1; + return *(char *)(void *)&i; +} + +#define eprintf(fmt, ...) fprintf(stderr, "\n" fmt, ##__VA_ARGS__) +#define CK_INNER(errproc, expr, cond, msg, ...) ({ \ + typeof(expr) cktmp = (expr); \ + if(!(cktmp cond)){ \ + eprintf("E%"PRIdPTR" at %s:%d(%s): assert(%s)" msg "\n", \ + (intptr_t)cktmp, __FILE__, __LINE__, __func__, \ + #expr " " #cond, ##__VA_ARGS__); \ + errproc; \ + } \ + cktmp; \ +}) + +#define CK(label, expr, cond) CK_INNER(goto label, expr, cond, "") +#define CKT(label, expr) CK(label, expr, ) +#define CKP(label, expr) CK(label, expr, >= 0) +#define CKZ(label, expr) CK(label, expr, == 0) +#define CKM(label, expr) CKZ(label, expr) +#define CKR(label, expr) CK(label, expr, != NULL) +#define CKA(expr, cond) CK_INNER(abort(), expr, cond, "") +#define CKAR(expr) CKA(expr, != NULL) +#define CKAP(expr) CKA(expr, >= 0) +#define CK_WARN(expr, cond) CK_INNER(, expr, cond, "") +#define CK_MSG(label, expr, cond, msg, ...) \ + CK_INNER(goto label, expr, cond, ": " msg, ##__VA_ARGS__) +#define TRY_NUMCAST(label, type, expr) ({ \ + typeof(expr) cktmp = (expr); \ + if((type)cktmp != cktmp) goto label; \ + if(cktmp < 0 && 0 < (type)-1) goto label; \ + (type)cktmp; \ +}) + +#endif //YTDLSB_UTILS_H |
