summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordyknon dyknonr5fjp2026-01-11 23:04:54 +0900
committerdyknon dyknonr5fjp2026-01-11 23:04:54 +0900
commitb7bc3f0d4488b6822506b9f93121249d078c38e3 (patch)
tree99d287cdefbfeeb20c4470c4f1bfd1eb909e7538
parent8439d0383adaee15bfd9a82a4d76db352690750e (diff)
Rewritten: stop using ffmpeg. better flexibility about image size.
-rw-r--r--Makefile12
-rw-r--r--README5
-rw-r--r--storyboard.c728
-rw-r--r--ytdlsb-main.c664
-rw-r--r--ytdlsb-mpv.c102
-rw-r--r--ytdlsb-mpv.h8
-rw-r--r--ytdlsb-tasks.c234
-rw-r--r--ytdlsb-tasks.h39
-rw-r--r--ytdlsb-utils.h45
9 files changed, 1100 insertions, 737 deletions
diff --git a/Makefile b/Makefile
index 1a3ff40..dbb91e4 100644
--- a/Makefile
+++ b/Makefile
@@ -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)
diff --git a/README b/README
index a95e802..c189c5e 100644
--- a/README
+++ b/README
@@ -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