#include #include #include #include #include #include #include "ytdlsb-utils.h" #include "ytdlsb-tasks.h" #include "ytdlsb-mpv.h" #include #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); struct ytdlsb_sb_fragment *f = &sb->fragments[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){ f->state = YTDLSB_SB_FAILED; if(mtype && strcmp(mtype, "image/jpeg") == 0){ f->state = YTDLSB_SB_JPEG; }else if(mtype && strcmp(mtype, "image/webp") == 0){ f->state = YTDLSB_SB_WEBP; }else{ // twitch returns jpeg images in binary/octet-stream if(f->data && f->data_len >= 12){ if(memcmp(f->data, "\xff\xd8", 2) == 0){ f->state = YTDLSB_SB_JPEG; }else if( memcmp(f->data, "RIFF", 4) == 0 && memcmp(f->data+8, "WEBP", 4) == 0 ){ f->state = YTDLSB_SB_WEBP; } } } if(f->state == YTDLSB_SB_FAILED){ eprintf("invalid storyboard type: %s\n", mtype); } }else{ eprintf("invalid storyboard response: %03ld\n", respc); f->state = YTDLSB_SB_FAILED; } }else{ double time; eprintf("storyboard download error: %d\n", (int)m->data.result); f->state = YTDLSB_SB_FAILED; #define TIME(t) \ if(!curl_easy_getinfo(p->preload_easy, t, &time)){ \ eprintf(#t ": %f\n", time); \ } TIME(CURLINFO_CONNECT_TIME); TIME(CURLINFO_APPCONNECT_TIME); TIME(CURLINFO_PRETRANSFER_TIME); TIME(CURLINFO_STARTTRANSFER_TIME); #undef TIME } if(CKP(err, ytdlsb_done_fragment_preload(p))) continue; break; }else{ break; } } CKP(err, ytdlsb_task_timeout_ms(&p->t.tasks[YTDLSB_TASK_PRELOAD], 300)); CKP(err, ytdlsb_task_fdto_from_curl(&p->t.tasks[YTDLSB_TASK_PRELOAD], 0, 0, 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, 30000l); 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; } } } int ytdlsb_sb_fragment_raw_alloc( struct ytdlsb_sb *sb, struct ytdlsb_sb_fragment *f, size_t width, size_t height ){ CK(err, SIZE_MAX / width / height, >= 4); CK(err, width, <= sb->columns * sb->width); CK(err, width % sb->width, == 0); CK(err, height, <= sb->rows * sb->height); CK(err, height % sb->height, == 0); f->data_len = width * height * 4; free(f->data); f->data = CKAR(malloc(f->data_len)); return 0; err: return -1; } int ytdlsb_sb_fragment_raw_fill_scanline( struct ytdlsb_sb *sb, struct ytdlsb_sb_fragment *f, char *src, size_t width, size_t pos_y ){ CK(err, width*(pos_y+1)*4, <= f->data_len); size_t frame_cnt = width / sb->width; size_t frame_from = (pos_y / sb->height) * frame_cnt; size_t y_in_row = pos_y % sb->height; size_t frame_bytes = sb->width*sb->height*4; for(size_t fi = 0; fi < frame_cnt; fi++){ memcpy(f->data + frame_bytes*(frame_from+fi) + y_in_row*sb->width*4, src + sb->width*fi*4, sb->width*4); } return 0; err: return -1; } struct ytdlsb_jpeg_error{ struct jpeg_error_mgr pub; jmp_buf env; }; void ytdlsb_jpeg_error_exit(j_common_ptr cinfo){ struct ytdlsb_jpeg_error *e = (struct ytdlsb_jpeg_error *)cinfo->err; longjmp(e->env, 1); } int ytdlsb_sb_fragment_decode( struct ytdlsb_sb *sb, struct ytdlsb_sb_fragment *f ){ CKAR(f->data); if(f->state == YTDLSB_SB_WEBP){ char *decbuf = NULL; int width_i, height_i; size_t width, height; if(is_little_endian()){ decbuf = (char *)CKR(webp_err, WebPDecodeBGRA( (uint8_t *)f->data, f->data_len, &width_i, &height_i)); }else{ decbuf = (char *)CKR(webp_err, WebPDecodeARGB( (uint8_t *)f->data, f->data_len, &width_i, &height_i)); } width = TRY_NUMCAST(webp_err, size_t, width_i); height = TRY_NUMCAST(webp_err, size_t, height_i); CKP(webp_err, ytdlsb_sb_fragment_raw_alloc(sb, f, width, height)); for(size_t i = 0; i < height; i++){ CKP(webp_err, ytdlsb_sb_fragment_raw_fill_scanline( sb, f, decbuf + width*i*4, width, i)); } goto webp_ok; webp_err: free(decbuf); goto err; webp_ok: }else if(f->state == YTDLSB_SB_JPEG){ int progress = 0; struct jpeg_decompress_struct jd; struct ytdlsb_jpeg_error je; void *src; JSAMPLE *decbuf; size_t width, height; unsigned long data_len = TRY_NUMCAST(err, unsigned long, f->data_len); if(setjmp(je.env)){ if(progress >= 3) free(decbuf); if(progress >= 2) free(src); if(progress >= 1) jpeg_destroy_decompress(&jd); goto err; } jd.err = jpeg_std_error(&je.pub); je.pub.error_exit = ytdlsb_jpeg_error_exit; jpeg_create_decompress(&jd); progress = 1; src = f->data; f->data = NULL; progress = 2; jpeg_mem_src(&jd, src, data_len); CK(jpeg_err, jpeg_read_header(&jd, 1), == JPEG_HEADER_OK); if(is_little_endian()){ jd.out_color_space = JCS_EXT_BGRA; }else{ jd.out_color_space = JCS_EXT_ARGB; } jpeg_start_decompress(&jd); CK(jpeg_err, jd.output_components, == 4); width = TRY_NUMCAST(jpeg_err, size_t, jd.output_width); height = TRY_NUMCAST(jpeg_err, size_t, jd.output_height); CKP(jpeg_err, ytdlsb_sb_fragment_raw_alloc(sb, f, width, height)); CKA(sizeof(JSAMPLE), == 1); decbuf = CKAR(malloc(width * 4)); progress = 3; for(size_t i = 0; i < height; i++){ CK(jpeg_err, jpeg_read_scanlines(&jd, &decbuf, 1), == 1); CKP(jpeg_err, ytdlsb_sb_fragment_raw_fill_scanline( sb, f, (char *)decbuf, width, i)); } jpeg_finish_decompress(&jd); free(decbuf); free(src); jpeg_destroy_decompress(&jd); goto jpeg_ok; jpeg_err: longjmp(je.env, 1); jpeg_ok: } return 0; err: return -1; } // <0 = error (only on first error) // 0 = not loaded // 1 = ref int ytdlsb_sb_get_frame( struct ytdlsb_sb *sb, double pos, char **data, size_t *len ){ struct ytdlsb_sb_fragment *f = NULL; size_t frame = sb->width * sb->height * 4; size_t findex; 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; } if(!f || f->state == YTDLSB_SB_EMPTY || f->state == YTDLSB_SB_FAILED) return 0; CKP(err, ytdlsb_sb_fragment_decode(sb, f)); f->state = YTDLSB_SB_RAW; 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; err: 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; }