1 /*
2 * This file is part of mpv.
3 *
4 * mpv is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * mpv is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <assert.h>
21
22 #include <libavutil/common.h>
23
24 #include "common/common.h"
25
26 #include "stream/stream.h"
27
28 #include "osdep/timer.h"
29
30 #include "mpv_talloc.h"
31 #include "options/m_config.h"
32 #include "options/options.h"
33 #include "common/global.h"
34 #include "common/msg.h"
35 #include "common/stats.h"
36 #include "player/client.h"
37 #include "player/command.h"
38 #include "osd.h"
39 #include "osd_state.h"
40 #include "dec_sub.h"
41 #include "img_convert.h"
42 #include "draw_bmp.h"
43 #include "video/mp_image.h"
44 #include "video/mp_image_pool.h"
45
46 #define OPT_BASE_STRUCT struct osd_style_opts
47 static const m_option_t style_opts[] = {
48 {"font", OPT_STRING(font)},
49 {"font-size", OPT_FLOAT(font_size), M_RANGE(1, 9000)},
50 {"color", OPT_COLOR(color)},
51 {"border-color", OPT_COLOR(border_color)},
52 {"shadow-color", OPT_COLOR(shadow_color)},
53 {"back-color", OPT_COLOR(back_color)},
54 {"border-size", OPT_FLOAT(border_size)},
55 {"shadow-offset", OPT_FLOAT(shadow_offset)},
56 {"spacing", OPT_FLOAT(spacing), M_RANGE(-10, 10)},
57 {"margin-x", OPT_INT(margin_x), M_RANGE(0, 300)},
58 {"margin-y", OPT_INT(margin_y), M_RANGE(0, 600)},
59 {"align-x", OPT_CHOICE(align_x,
60 {"left", -1}, {"center", 0}, {"right", +1})},
61 {"align-y", OPT_CHOICE(align_y,
62 {"top", -1}, {"center", 0}, {"bottom", +1})},
63 {"blur", OPT_FLOAT(blur), M_RANGE(0, 20)},
64 {"bold", OPT_FLAG(bold)},
65 {"italic", OPT_FLAG(italic)},
66 {"justify", OPT_CHOICE(justify,
67 {"auto", 0}, {"left", 1}, {"center", 2}, {"right", 3})},
68 {"font-provider", OPT_CHOICE(font_provider,
69 {"auto", 0}, {"none", 1}, {"fontconfig", 2}), .flags = UPDATE_SUB_HARD},
70 {0}
71 };
72
73 const struct m_sub_options osd_style_conf = {
74 .opts = style_opts,
75 .size = sizeof(struct osd_style_opts),
76 .defaults = &(const struct osd_style_opts){
77 .font = "sans-serif",
78 .font_size = 55,
79 .color = {255, 255, 255, 255},
80 .border_color = {0, 0, 0, 255},
81 .shadow_color = {240, 240, 240, 128},
82 .border_size = 3,
83 .shadow_offset = 0,
84 .margin_x = 25,
85 .margin_y = 22,
86 .align_x = -1,
87 .align_y = -1,
88 },
89 .change_flags = UPDATE_OSD,
90 };
91
92 const struct m_sub_options sub_style_conf = {
93 .opts = style_opts,
94 .size = sizeof(struct osd_style_opts),
95 .defaults = &(const struct osd_style_opts){
96 .font = "sans-serif",
97 .font_size = 55,
98 .color = {255, 255, 255, 255},
99 .border_color = {0, 0, 0, 255},
100 .shadow_color = {240, 240, 240, 128},
101 .border_size = 3,
102 .shadow_offset = 0,
103 .margin_x = 25,
104 .margin_y = 22,
105 .align_x = 0,
106 .align_y = 1,
107 },
108 .change_flags = UPDATE_OSD,
109 };
110
osd_res_equals(struct mp_osd_res a,struct mp_osd_res b)111 bool osd_res_equals(struct mp_osd_res a, struct mp_osd_res b)
112 {
113 return a.w == b.w && a.h == b.h && a.ml == b.ml && a.mt == b.mt
114 && a.mr == b.mr && a.mb == b.mb
115 && a.display_par == b.display_par;
116 }
117
osd_create(struct mpv_global * global)118 struct osd_state *osd_create(struct mpv_global *global)
119 {
120 assert(MAX_OSD_PARTS >= OSDTYPE_COUNT);
121
122 struct osd_state *osd = talloc_zero(NULL, struct osd_state);
123 *osd = (struct osd_state) {
124 .opts_cache = m_config_cache_alloc(osd, global, &mp_osd_render_sub_opts),
125 .global = global,
126 .log = mp_log_new(osd, global->log, "osd"),
127 .force_video_pts = MP_NOPTS_VALUE,
128 .stats = stats_ctx_create(osd, global, "osd"),
129 };
130 pthread_mutex_init(&osd->lock, NULL);
131 osd->opts = osd->opts_cache->opts;
132
133 for (int n = 0; n < MAX_OSD_PARTS; n++) {
134 struct osd_object *obj = talloc(osd, struct osd_object);
135 *obj = (struct osd_object) {
136 .type = n,
137 .text = talloc_strdup(obj, ""),
138 .progbar_state = {.type = -1},
139 .vo_change_id = 1,
140 };
141 osd->objs[n] = obj;
142 }
143
144 osd->objs[OSDTYPE_SUB]->is_sub = true;
145 osd->objs[OSDTYPE_SUB2]->is_sub = true;
146
147 osd_init_backend(osd);
148 return osd;
149 }
150
osd_free(struct osd_state * osd)151 void osd_free(struct osd_state *osd)
152 {
153 if (!osd)
154 return;
155 osd_destroy_backend(osd);
156 talloc_free(osd->objs[OSDTYPE_EXTERNAL2]->external2);
157 pthread_mutex_destroy(&osd->lock);
158 talloc_free(osd);
159 }
160
osd_set_text(struct osd_state * osd,const char * text)161 void osd_set_text(struct osd_state *osd, const char *text)
162 {
163 pthread_mutex_lock(&osd->lock);
164 struct osd_object *osd_obj = osd->objs[OSDTYPE_OSD];
165 if (!text)
166 text = "";
167 if (strcmp(osd_obj->text, text) != 0) {
168 talloc_free(osd_obj->text);
169 osd_obj->text = talloc_strdup(osd_obj, text);
170 osd_obj->osd_changed = true;
171 osd->want_redraw_notification = true;
172 }
173 pthread_mutex_unlock(&osd->lock);
174 }
175
osd_set_sub(struct osd_state * osd,int index,struct dec_sub * dec_sub)176 void osd_set_sub(struct osd_state *osd, int index, struct dec_sub *dec_sub)
177 {
178 pthread_mutex_lock(&osd->lock);
179 if (index >= 0 && index < 2) {
180 struct osd_object *obj = osd->objs[OSDTYPE_SUB + index];
181 obj->sub = dec_sub;
182 obj->vo_change_id += 1;
183 }
184 osd->want_redraw_notification = true;
185 pthread_mutex_unlock(&osd->lock);
186 }
187
osd_get_render_subs_in_filter(struct osd_state * osd)188 bool osd_get_render_subs_in_filter(struct osd_state *osd)
189 {
190 pthread_mutex_lock(&osd->lock);
191 bool r = osd->render_subs_in_filter;
192 pthread_mutex_unlock(&osd->lock);
193 return r;
194 }
195
osd_set_render_subs_in_filter(struct osd_state * osd,bool s)196 void osd_set_render_subs_in_filter(struct osd_state *osd, bool s)
197 {
198 pthread_mutex_lock(&osd->lock);
199 if (osd->render_subs_in_filter != s) {
200 osd->render_subs_in_filter = s;
201
202 int change_id = 0;
203 for (int n = 0; n < MAX_OSD_PARTS; n++)
204 change_id = MPMAX(change_id, osd->objs[n]->vo_change_id);
205 for (int n = 0; n < MAX_OSD_PARTS; n++)
206 osd->objs[n]->vo_change_id = change_id + 1;
207 }
208 pthread_mutex_unlock(&osd->lock);
209 }
210
osd_set_force_video_pts(struct osd_state * osd,double video_pts)211 void osd_set_force_video_pts(struct osd_state *osd, double video_pts)
212 {
213 pthread_mutex_lock(&osd->lock);
214 osd->force_video_pts = video_pts;
215 pthread_mutex_unlock(&osd->lock);
216 }
217
osd_get_force_video_pts(struct osd_state * osd)218 double osd_get_force_video_pts(struct osd_state *osd)
219 {
220 pthread_mutex_lock(&osd->lock);
221 double pts = osd->force_video_pts;
222 pthread_mutex_unlock(&osd->lock);
223 return pts;
224 }
225
osd_set_progbar(struct osd_state * osd,struct osd_progbar_state * s)226 void osd_set_progbar(struct osd_state *osd, struct osd_progbar_state *s)
227 {
228 pthread_mutex_lock(&osd->lock);
229 struct osd_object *osd_obj = osd->objs[OSDTYPE_OSD];
230 osd_obj->progbar_state.type = s->type;
231 osd_obj->progbar_state.value = s->value;
232 osd_obj->progbar_state.num_stops = s->num_stops;
233 MP_TARRAY_GROW(osd_obj, osd_obj->progbar_state.stops, s->num_stops);
234 if (s->num_stops) {
235 memcpy(osd_obj->progbar_state.stops, s->stops,
236 sizeof(osd_obj->progbar_state.stops[0]) * s->num_stops);
237 }
238 osd_obj->osd_changed = true;
239 osd->want_redraw_notification = true;
240 pthread_mutex_unlock(&osd->lock);
241 }
242
osd_set_external2(struct osd_state * osd,struct sub_bitmaps * imgs)243 void osd_set_external2(struct osd_state *osd, struct sub_bitmaps *imgs)
244 {
245 pthread_mutex_lock(&osd->lock);
246 struct osd_object *obj = osd->objs[OSDTYPE_EXTERNAL2];
247 talloc_free(obj->external2);
248 obj->external2 = sub_bitmaps_copy(NULL, imgs);
249 obj->vo_change_id += 1;
250 osd->want_redraw_notification = true;
251 pthread_mutex_unlock(&osd->lock);
252 }
253
check_obj_resize(struct osd_state * osd,struct mp_osd_res res,struct osd_object * obj)254 static void check_obj_resize(struct osd_state *osd, struct mp_osd_res res,
255 struct osd_object *obj)
256 {
257 if (!osd_res_equals(res, obj->vo_res)) {
258 obj->vo_res = res;
259 mp_client_broadcast_event_external(osd->global->client_api,
260 MP_EVENT_WIN_RESIZE, NULL);
261 }
262 }
263
264 // Optional. Can be called for faster reaction of OSD-generating scripts like
265 // osc.lua. This can achieve that the resize happens first, so that the OSD is
266 // generated at the correct resolution the first time the resized frame is
267 // rendered. Since the OSD doesn't (and can't) wait for the script, this
268 // increases the time in which the script can react, and also gets rid of the
269 // unavoidable redraw delay (though it will still be racy).
270 // Unnecessary for anything else.
osd_resize(struct osd_state * osd,struct mp_osd_res res)271 void osd_resize(struct osd_state *osd, struct mp_osd_res res)
272 {
273 pthread_mutex_lock(&osd->lock);
274 int types[] = {OSDTYPE_OSD, OSDTYPE_EXTERNAL, OSDTYPE_EXTERNAL2, -1};
275 for (int n = 0; types[n] >= 0; n++)
276 check_obj_resize(osd, res, osd->objs[types[n]]);
277 pthread_mutex_unlock(&osd->lock);
278 }
279
render_object(struct osd_state * osd,struct osd_object * obj,struct mp_osd_res osdres,double video_pts,const bool sub_formats[SUBBITMAP_COUNT])280 static struct sub_bitmaps *render_object(struct osd_state *osd,
281 struct osd_object *obj,
282 struct mp_osd_res osdres, double video_pts,
283 const bool sub_formats[SUBBITMAP_COUNT])
284 {
285 int format = SUBBITMAP_LIBASS;
286 if (!sub_formats[format] || osd->opts->force_rgba_osd)
287 format = SUBBITMAP_RGBA;
288
289 struct sub_bitmaps *res = NULL;
290
291 check_obj_resize(osd, osdres, obj);
292
293 if (obj->type == OSDTYPE_SUB) {
294 if (obj->sub)
295 res = sub_get_bitmaps(obj->sub, obj->vo_res, format, video_pts);
296 } else if (obj->type == OSDTYPE_SUB2) {
297 if (obj->sub && sub_is_secondary_visible(obj->sub))
298 res = sub_get_bitmaps(obj->sub, obj->vo_res, format, video_pts);
299 } else if (obj->type == OSDTYPE_EXTERNAL2) {
300 if (obj->external2 && obj->external2->format) {
301 res = sub_bitmaps_copy(NULL, obj->external2); // need to be owner
302 obj->external2->change_id = 0;
303 }
304 } else {
305 res = osd_object_get_bitmaps(osd, obj, format);
306 }
307
308 if (obj->vo_had_output != !!res) {
309 obj->vo_had_output = !!res;
310 obj->vo_change_id += 1;
311 }
312
313 if (res) {
314 obj->vo_change_id += res->change_id;
315
316 res->render_index = obj->type;
317 res->change_id = obj->vo_change_id;
318 }
319
320 return res;
321 }
322
323 // Render OSD to a list of bitmap and return it. The returned object is
324 // refcounted. Typically you should hold it only for a short time, and then
325 // release it.
326 // draw_flags is a bit field of OSD_DRAW_* constants
osd_render(struct osd_state * osd,struct mp_osd_res res,double video_pts,int draw_flags,const bool formats[SUBBITMAP_COUNT])327 struct sub_bitmap_list *osd_render(struct osd_state *osd, struct mp_osd_res res,
328 double video_pts, int draw_flags,
329 const bool formats[SUBBITMAP_COUNT])
330 {
331 pthread_mutex_lock(&osd->lock);
332
333 struct sub_bitmap_list *list = talloc_zero(NULL, struct sub_bitmap_list);
334 list->change_id = 1;
335 list->w = res.w;
336 list->h = res.h;
337
338 if (osd->force_video_pts != MP_NOPTS_VALUE)
339 video_pts = osd->force_video_pts;
340
341 if (draw_flags & OSD_DRAW_SUB_FILTER)
342 draw_flags |= OSD_DRAW_SUB_ONLY;
343
344 for (int n = 0; n < MAX_OSD_PARTS; n++) {
345 struct osd_object *obj = osd->objs[n];
346
347 // Object is drawn into the video frame itself; don't draw twice
348 if (osd->render_subs_in_filter && obj->is_sub &&
349 !(draw_flags & OSD_DRAW_SUB_FILTER))
350 continue;
351 if ((draw_flags & OSD_DRAW_SUB_ONLY) && !obj->is_sub)
352 continue;
353 if ((draw_flags & OSD_DRAW_OSD_ONLY) && obj->is_sub)
354 continue;
355
356 char *stat_type_render = obj->is_sub ? "sub-render" : "osd-render";
357 stats_time_start(osd->stats, stat_type_render);
358
359 struct sub_bitmaps *imgs =
360 render_object(osd, obj, res, video_pts, formats);
361
362 stats_time_end(osd->stats, stat_type_render);
363
364 if (imgs && imgs->num_parts > 0) {
365 if (formats[imgs->format]) {
366 talloc_steal(list, imgs);
367 MP_TARRAY_APPEND(list, list->items, list->num_items, imgs);
368 imgs = NULL;
369 } else {
370 MP_ERR(osd, "Can't render OSD part %d (format %d).\n",
371 obj->type, imgs->format);
372 }
373 }
374
375 list->change_id += obj->vo_change_id;
376
377 talloc_free(imgs);
378 }
379
380 // If this is called with OSD_DRAW_SUB_ONLY or OSD_DRAW_OSD_ONLY set, assume
381 // it will always draw the complete OSD by doing multiple osd_draw() calls.
382 // OSD_DRAW_SUB_FILTER on the other hand is an evil special-case, and we
383 // must not reset the flag when it happens.
384 if (!(draw_flags & OSD_DRAW_SUB_FILTER))
385 osd->want_redraw_notification = false;
386
387 pthread_mutex_unlock(&osd->lock);
388 return list;
389 }
390
391 // Warning: this function should be considered legacy. Use osd_render() instead.
osd_draw(struct osd_state * osd,struct mp_osd_res res,double video_pts,int draw_flags,const bool formats[SUBBITMAP_COUNT],void (* cb)(void * ctx,struct sub_bitmaps * imgs),void * cb_ctx)392 void osd_draw(struct osd_state *osd, struct mp_osd_res res,
393 double video_pts, int draw_flags,
394 const bool formats[SUBBITMAP_COUNT],
395 void (*cb)(void *ctx, struct sub_bitmaps *imgs), void *cb_ctx)
396 {
397 struct sub_bitmap_list *list =
398 osd_render(osd, res, video_pts, draw_flags, formats);
399
400 stats_time_start(osd->stats, "draw");
401
402 for (int n = 0; n < list->num_items; n++)
403 cb(cb_ctx, list->items[n]);
404
405 stats_time_end(osd->stats, "draw");
406
407 talloc_free(list);
408 }
409
410 // Calls mp_image_make_writeable() on the dest image if something is drawn.
411 // draw_flags as in osd_render().
osd_draw_on_image(struct osd_state * osd,struct mp_osd_res res,double video_pts,int draw_flags,struct mp_image * dest)412 void osd_draw_on_image(struct osd_state *osd, struct mp_osd_res res,
413 double video_pts, int draw_flags, struct mp_image *dest)
414 {
415 osd_draw_on_image_p(osd, res, video_pts, draw_flags, NULL, dest);
416 }
417
418 // Like osd_draw_on_image(), but if dest needs to be copied to make it
419 // writeable, allocate images from the given pool. (This is a minor
420 // optimization to reduce "real" image sized memory allocations.)
osd_draw_on_image_p(struct osd_state * osd,struct mp_osd_res res,double video_pts,int draw_flags,struct mp_image_pool * pool,struct mp_image * dest)421 void osd_draw_on_image_p(struct osd_state *osd, struct mp_osd_res res,
422 double video_pts, int draw_flags,
423 struct mp_image_pool *pool, struct mp_image *dest)
424 {
425 struct sub_bitmap_list *list =
426 osd_render(osd, res, video_pts, draw_flags, mp_draw_sub_formats);
427
428 if (!list->num_items) {
429 talloc_free(list);
430 return;
431 }
432
433 if (!mp_image_pool_make_writeable(pool, dest))
434 return; // on OOM, skip
435
436 // Need to lock for the dumb osd->draw_cache thing.
437 pthread_mutex_lock(&osd->lock);
438
439 if (!osd->draw_cache)
440 osd->draw_cache = mp_draw_sub_alloc(osd, osd->global);
441
442 stats_time_start(osd->stats, "draw-bmp");
443
444 if (!mp_draw_sub_bitmaps(osd->draw_cache, dest, list))
445 MP_WARN(osd, "Failed rendering OSD.\n");
446 talloc_steal(osd, osd->draw_cache);
447
448 stats_time_end(osd->stats, "draw-bmp");
449
450 pthread_mutex_unlock(&osd->lock);
451
452 talloc_free(list);
453 }
454
455 // Setup the OSD resolution to render into an image with the given parameters.
456 // The interesting part about this is that OSD has to compensate the aspect
457 // ratio if the image does not have a 1:1 pixel aspect ratio.
osd_res_from_image_params(const struct mp_image_params * p)458 struct mp_osd_res osd_res_from_image_params(const struct mp_image_params *p)
459 {
460 return (struct mp_osd_res) {
461 .w = p->w,
462 .h = p->h,
463 .display_par = p->p_h / (double)p->p_w,
464 };
465 }
466
467 // Typically called to react to OSD style changes.
osd_changed(struct osd_state * osd)468 void osd_changed(struct osd_state *osd)
469 {
470 pthread_mutex_lock(&osd->lock);
471 osd->objs[OSDTYPE_OSD]->osd_changed = true;
472 osd->want_redraw_notification = true;
473 // Done here for a lack of a better place.
474 m_config_cache_update(osd->opts_cache);
475 pthread_mutex_unlock(&osd->lock);
476 }
477
osd_query_and_reset_want_redraw(struct osd_state * osd)478 bool osd_query_and_reset_want_redraw(struct osd_state *osd)
479 {
480 pthread_mutex_lock(&osd->lock);
481 bool r = osd->want_redraw_notification;
482 osd->want_redraw_notification = false;
483 pthread_mutex_unlock(&osd->lock);
484 return r;
485 }
486
osd_get_vo_res(struct osd_state * osd)487 struct mp_osd_res osd_get_vo_res(struct osd_state *osd)
488 {
489 pthread_mutex_lock(&osd->lock);
490 // Any OSDTYPE is fine; but it mustn't be a subtitle one (can have lower res.)
491 struct mp_osd_res res = osd->objs[OSDTYPE_OSD]->vo_res;
492 pthread_mutex_unlock(&osd->lock);
493 return res;
494 }
495
496 // Position the subbitmaps in imgs on the screen. Basically, this fits the
497 // subtitle canvas (of size frame_w x frame_h) onto the screen, such that it
498 // fills the whole video area (especially if the video is magnified, e.g. on
499 // fullscreen). If compensate_par is >0, adjust the way the subtitles are
500 // "stretched" on the screen, and letter-box the result. If compensate_par
501 // is <0, strictly letter-box the subtitles. If it is 0, stretch them.
osd_rescale_bitmaps(struct sub_bitmaps * imgs,int frame_w,int frame_h,struct mp_osd_res res,double compensate_par)502 void osd_rescale_bitmaps(struct sub_bitmaps *imgs, int frame_w, int frame_h,
503 struct mp_osd_res res, double compensate_par)
504 {
505 int vidw = res.w - res.ml - res.mr;
506 int vidh = res.h - res.mt - res.mb;
507 double xscale = (double)vidw / frame_w;
508 double yscale = (double)vidh / frame_h;
509 if (compensate_par < 0)
510 compensate_par = xscale / yscale / res.display_par;
511 if (compensate_par > 0)
512 xscale /= compensate_par;
513 int cx = vidw / 2 - (int)(frame_w * xscale) / 2;
514 int cy = vidh / 2 - (int)(frame_h * yscale) / 2;
515 for (int i = 0; i < imgs->num_parts; i++) {
516 struct sub_bitmap *bi = &imgs->parts[i];
517 bi->x = (int)(bi->x * xscale) + cx + res.ml;
518 bi->y = (int)(bi->y * yscale) + cy + res.mt;
519 bi->dw = (int)(bi->w * xscale + 0.5);
520 bi->dh = (int)(bi->h * yscale + 0.5);
521 }
522 }
523
524 // Copy *in and return a new allocation of it. Free with talloc_free(). This
525 // will contain a refcounted copy of the image data.
526 //
527 // in->packed must be set and must be a refcounted image, unless there is no
528 // data (num_parts==0).
529 //
530 // p_cache: if not NULL, then this points to a struct sub_bitmap_copy_cache*
531 // variable. The function may set this to an allocation and may later
532 // read it. You have to free it with talloc_free() when done.
533 // in: valid struct, or NULL (in this case it also returns NULL)
534 // returns: new copy, or NULL if there was no data in the input
sub_bitmaps_copy(struct sub_bitmap_copy_cache ** p_cache,struct sub_bitmaps * in)535 struct sub_bitmaps *sub_bitmaps_copy(struct sub_bitmap_copy_cache **p_cache,
536 struct sub_bitmaps *in)
537 {
538 if (!in || !in->num_parts)
539 return NULL;
540
541 struct sub_bitmaps *res = talloc(NULL, struct sub_bitmaps);
542 *res = *in;
543
544 // Note: the p_cache thing is a lie and unused.
545
546 // The bitmaps being refcounted is essential for performance, and for
547 // not invalidating in->parts[*].bitmap pointers.
548 assert(in->packed && in->packed->bufs[0]);
549
550 res->packed = mp_image_new_ref(res->packed);
551 MP_HANDLE_OOM(res->packed);
552 talloc_steal(res, res->packed);
553
554 res->parts = NULL;
555 MP_RESIZE_ARRAY(res, res->parts, res->num_parts);
556 memcpy(res->parts, in->parts, sizeof(res->parts[0]) * res->num_parts);
557
558 return res;
559 }
560