From b7bc3f0d4488b6822506b9f93121249d078c38e3 Mon Sep 17 00:00:00 2001 From: dyknon Date: Sun, 11 Jan 2026 23:04:54 +0900 Subject: Rewritten: stop using ffmpeg. better flexibility about image size. --- ytdlsb-main.c | 664 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 664 insertions(+) create mode 100644 ytdlsb-main.c (limited to 'ytdlsb-main.c') 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 +#include +#include +#include +#include +#include "ytdlsb-utils.h" +#include "ytdlsb-tasks.h" +#include "ytdlsb-mpv.h" +#include +#include +#include + +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; +} -- cgit v1.2.3