Coverage Report

Created: 2025-12-11 06:55

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mpv/video/out/vo_tct.c
Line
Count
Source
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 <config.h>
20
21
#if HAVE_POSIX
22
#include <sys/ioctl.h>
23
#endif
24
25
#include <libswscale/swscale.h>
26
27
#include "options/m_config.h"
28
#include "config.h"
29
#include "osdep/terminal.h"
30
#include "osdep/io.h"
31
#include "vo.h"
32
#include "sub/osd.h"
33
#include "video/sws_utils.h"
34
#include "video/mp_image.h"
35
36
6.69k
#define IMGFMT IMGFMT_BGR24
37
38
20
#define ALGO_PLAIN 1
39
#define ALGO_HALF_BLOCKS 2
40
41
20
#define DEFAULT_WIDTH 80
42
20
#define DEFAULT_HEIGHT 25
43
44
static const bstr TERM_ESC_COLOR256_BG     = bstr0_lit("\033[48;5");
45
static const bstr TERM_ESC_COLOR256_FG     = bstr0_lit("\033[38;5");
46
static const bstr TERM_ESC_COLOR24BIT_BG   = bstr0_lit("\033[48;2");
47
static const bstr TERM_ESC_COLOR24BIT_FG   = bstr0_lit("\033[38;2");
48
49
static const bstr UNICODE_LOWER_HALF_BLOCK = bstr0_lit("\xe2\x96\x84");
50
51
52
#define WRITE_STR(str) fwrite((str), strlen(str), 1, stdout)
52
53
enum vo_tct_buffering {
54
    VO_TCT_BUFFER_PIXEL,
55
    VO_TCT_BUFFER_LINE,
56
    VO_TCT_BUFFER_FRAME
57
};
58
59
struct vo_tct_opts {
60
    int algo;
61
    int buffering;
62
    int width;   // 0 -> default
63
    int height;  // 0 -> default
64
    bool term256;  // 0 -> true color
65
};
66
67
struct lut_item {
68
    char str[4];
69
    uint8_t width;
70
};
71
72
struct priv {
73
    struct vo_tct_opts opts;
74
    size_t buffer_size;
75
    int swidth;
76
    int sheight;
77
    struct mp_image *frame;
78
    struct mp_rect src;
79
    struct mp_rect dst;
80
    struct mp_sws_context *sws;
81
    bstr frame_buf;
82
    struct lut_item lut[256];
83
};
84
85
// Convert RGB24 to xterm-256 8-bit value
86
// For simplicity, assume RGB space is perceptually uniform.
87
// There are 5 places where one of two outputs needs to be chosen when the
88
// input is the exact middle:
89
// - The r/g/b channels and the gray value: the higher value output is chosen.
90
// - If the gray and color have same distance from the input - color is chosen.
91
static int rgb_to_x256(uint8_t r, uint8_t g, uint8_t b)
92
0
{
93
    // Calculate the nearest 0-based color index at 16 .. 231
94
0
#   define v2ci(v) (v < 48 ? 0 : v < 115 ? 1 : (v - 35) / 40)
95
0
    int ir = v2ci(r), ig = v2ci(g), ib = v2ci(b);   // 0..5 each
96
0
#   define color_index() (36 * ir + 6 * ig + ib)  /* 0..215, lazy evaluation */
97
98
    // Calculate the nearest 0-based gray index at 232 .. 255
99
0
    int average = (r + g + b) / 3;
100
0
    int gray_index = average > 238 ? 23 : (average - 3) / 10;  // 0..23
101
102
    // Calculate the represented colors back from the index
103
0
    static const int i2cv[6] = {0, 0x5f, 0x87, 0xaf, 0xd7, 0xff};
104
0
    int cr = i2cv[ir], cg = i2cv[ig], cb = i2cv[ib];  // r/g/b, 0..255 each
105
0
    int gv = 8 + 10 * gray_index;  // same value for r/g/b, 0..255
106
107
    // Return the one which is nearer to the original input rgb value
108
0
#   define dist_square(A,B,C, a,b,c) ((A-a)*(A-a) + (B-b)*(B-b) + (C-c)*(C-c))
109
0
    int color_err = dist_square(cr, cg, cb, r, g, b);
110
0
    int gray_err  = dist_square(gv, gv, gv, r, g, b);
111
0
    return color_err <= gray_err ? 16 + color_index() : 232 + gray_index;
112
0
}
113
114
static void print_seq3(bstr *frame, struct lut_item *lut, bstr prefix,
115
                       uint8_t r, uint8_t g, uint8_t b)
116
52.8k
{
117
52.8k
    bstr_xappend(NULL, frame, prefix);
118
52.8k
    bstr_xappend(NULL, frame, (bstr){ lut[r].str, lut[r].width });
119
52.8k
    bstr_xappend(NULL, frame, (bstr){ lut[g].str, lut[g].width });
120
52.8k
    bstr_xappend(NULL, frame, (bstr){ lut[b].str, lut[b].width });
121
52.8k
    bstr_xappend0(NULL, frame, "m");
122
52.8k
}
123
124
static void print_seq1(bstr *frame, struct lut_item *lut, bstr prefix, uint8_t c)
125
0
{
126
0
    bstr_xappend(NULL, frame, prefix);
127
0
    bstr_xappend(NULL, frame, (bstr){ lut[c].str, lut[c].width });
128
0
    bstr_xappend0(NULL, frame, "m");
129
0
}
130
131
static void print_buffer(bstr *frame)
132
416
{
133
416
    fwrite(frame->start, frame->len, 1, stdout);
134
416
    frame->len = 0;
135
416
}
136
137
static void write_plain(bstr *frame,
138
    const int dwidth, const int dheight,
139
    const int swidth, const int sheight,
140
    const unsigned char *source, const int source_stride,
141
    bool term256, struct lut_item *lut, enum vo_tct_buffering buffering)
142
0
{
143
0
    mp_assert(source);
144
0
    const int tx = (dwidth - swidth) / 2;
145
0
    const int ty = (dheight - sheight) / 2;
146
0
    for (int y = 0; y < sheight; y++) {
147
0
        const unsigned char *row = source + y * source_stride;
148
0
        bstr_xappend_asprintf(NULL, frame, TERM_ESC_GOTO_YX, ty + y, tx);
149
0
        for (int x = 0; x < swidth; x++) {
150
0
            unsigned char b = *row++;
151
0
            unsigned char g = *row++;
152
0
            unsigned char r = *row++;
153
0
            if (term256) {
154
0
                print_seq1(frame, lut, TERM_ESC_COLOR256_BG, rgb_to_x256(r, g, b));
155
0
            } else {
156
0
                print_seq3(frame, lut, TERM_ESC_COLOR24BIT_BG, r, g, b);
157
0
            }
158
0
            bstr_xappend0(NULL, frame, " ");
159
0
            if (buffering <= VO_TCT_BUFFER_PIXEL)
160
0
                print_buffer(frame);
161
0
        }
162
0
        bstr_xappend0(NULL, frame, TERM_ESC_CLEAR_COLORS);
163
0
        if (buffering <= VO_TCT_BUFFER_LINE)
164
0
            print_buffer(frame);
165
0
    }
166
0
}
167
168
static void write_half_blocks(bstr *frame,
169
    const int dwidth, const int dheight,
170
    const int swidth, const int sheight,
171
    unsigned char *source, int source_stride,
172
    bool term256, struct lut_item *lut, enum vo_tct_buffering buffering)
173
16
{
174
16
    mp_assert(source);
175
16
    const int tx = (dwidth - swidth) / 2;
176
16
    const int ty = (dheight - sheight) / 2;
177
416
    for (int y = 0; y < sheight * 2; y += 2) {
178
400
        const unsigned char *row_up = source + y * source_stride;
179
400
        const unsigned char *row_down = source + (y + 1) * source_stride;
180
400
        bstr_xappend_asprintf(NULL, frame, TERM_ESC_GOTO_YX, ty + y / 2, tx);
181
26.8k
        for (int x = 0; x < swidth; x++) {
182
26.4k
            unsigned char b_up = *row_up++;
183
26.4k
            unsigned char g_up = *row_up++;
184
26.4k
            unsigned char r_up = *row_up++;
185
26.4k
            unsigned char b_down = *row_down++;
186
26.4k
            unsigned char g_down = *row_down++;
187
26.4k
            unsigned char r_down = *row_down++;
188
26.4k
            if (term256) {
189
0
                print_seq1(frame, lut, TERM_ESC_COLOR256_BG, rgb_to_x256(r_up, g_up, b_up));
190
0
                print_seq1(frame, lut, TERM_ESC_COLOR256_FG, rgb_to_x256(r_down, g_down, b_down));
191
26.4k
            } else {
192
26.4k
                print_seq3(frame, lut, TERM_ESC_COLOR24BIT_BG, r_up, g_up, b_up);
193
26.4k
                print_seq3(frame, lut, TERM_ESC_COLOR24BIT_FG, r_down, g_down, b_down);
194
26.4k
            }
195
26.4k
            bstr_xappend(NULL, frame, UNICODE_LOWER_HALF_BLOCK);
196
26.4k
            if (buffering <= VO_TCT_BUFFER_PIXEL)
197
0
                print_buffer(frame);
198
26.4k
        }
199
400
        bstr_xappend0(NULL, frame, TERM_ESC_CLEAR_COLORS);
200
400
        if (buffering <= VO_TCT_BUFFER_LINE)
201
400
            print_buffer(frame);
202
400
    }
203
16
}
204
205
20
static void get_win_size(struct vo *vo, int *out_width, int *out_height) {
206
20
    struct priv *p = vo->priv;
207
20
    *out_width = DEFAULT_WIDTH;
208
20
    *out_height = DEFAULT_HEIGHT;
209
210
20
    terminal_get_size(out_width, out_height);
211
212
20
    if (p->opts.width > 0)
213
0
        *out_width = p->opts.width;
214
20
    if (p->opts.height > 0)
215
0
        *out_height = p->opts.height;
216
20
}
217
218
static int reconfig(struct vo *vo, struct mp_image_params *params)
219
4
{
220
4
    struct priv *p = vo->priv;
221
222
4
    get_win_size(vo, &vo->dwidth, &vo->dheight);
223
224
4
    struct mp_osd_res osd;
225
4
    vo_get_src_dst_rects(vo, &p->src, &p->dst, &osd);
226
4
    p->swidth = p->dst.x1 - p->dst.x0;
227
4
    p->sheight = p->dst.y1 - p->dst.y0;
228
229
4
    p->sws->src = *params;
230
4
    p->sws->dst = (struct mp_image_params) {
231
4
        .imgfmt = IMGFMT,
232
4
        .w = p->swidth,
233
4
        .h = p->sheight,
234
4
        .p_w = 1,
235
4
        .p_h = 1,
236
4
    };
237
238
4
    const int mul = (p->opts.algo == ALGO_PLAIN ? 1 : 2);
239
4
    if (p->frame)
240
0
        talloc_free(p->frame);
241
4
    p->frame = mp_image_alloc(IMGFMT, p->swidth, p->sheight * mul);
242
4
    if (!p->frame)
243
0
        return -1;
244
245
4
    mp_image_clear(p->frame, 0, 0, p->frame->w, p->frame->h);
246
247
4
    if (mp_sws_reinit(p->sws) < 0)
248
0
        return -1;
249
250
4
    WRITE_STR(TERM_ESC_CLEAR_SCREEN);
251
252
4
    vo->want_redraw = true;
253
4
    return 0;
254
4
}
255
256
static bool draw_frame(struct vo *vo, struct vo_frame *frame)
257
16
{
258
16
    struct priv *p = vo->priv;
259
16
    struct mp_image *src = frame->current;
260
16
    if (!src)
261
0
        goto done;
262
    // XXX: pan, crop etc.
263
16
    mp_sws_scale(p->sws, p->frame, src);
264
265
16
done:
266
16
    return VO_TRUE;
267
16
}
268
269
static void flip_page(struct vo *vo)
270
16
{
271
16
    struct priv *p = vo->priv;
272
273
16
    int width, height;
274
16
    get_win_size(vo, &width, &height);
275
276
16
    if (vo->dwidth != width || vo->dheight != height)
277
0
        reconfig(vo, vo->params);
278
279
16
    WRITE_STR(TERM_ESC_SYNC_UPDATE_BEGIN);
280
281
16
    p->frame_buf.len = 0;
282
16
    if (p->opts.algo == ALGO_PLAIN) {
283
0
        write_plain(&p->frame_buf,
284
0
            vo->dwidth, vo->dheight, p->swidth, p->sheight,
285
0
            p->frame->planes[0], p->frame->stride[0],
286
0
            p->opts.term256, p->lut, p->opts.buffering);
287
16
    } else {
288
16
        write_half_blocks(&p->frame_buf,
289
16
            vo->dwidth, vo->dheight, p->swidth, p->sheight,
290
16
            p->frame->planes[0], p->frame->stride[0],
291
16
            p->opts.term256, p->lut, p->opts.buffering);
292
16
    }
293
294
16
    bstr_xappend0(NULL, &p->frame_buf, "\n");
295
16
    if (p->opts.buffering <= VO_TCT_BUFFER_FRAME)
296
16
        print_buffer(&p->frame_buf);
297
298
16
    WRITE_STR(TERM_ESC_SYNC_UPDATE_END);
299
16
    fflush(stdout);
300
16
}
301
302
static void uninit(struct vo *vo)
303
4
{
304
4
    WRITE_STR(TERM_ESC_RESTORE_CURSOR);
305
4
    terminal_set_mouse_input(false);
306
4
    WRITE_STR(TERM_ESC_NORMAL_SCREEN);
307
4
    struct priv *p = vo->priv;
308
4
    talloc_free(p->frame);
309
4
    talloc_free(p->frame_buf.start);
310
4
}
311
312
static int preinit(struct vo *vo)
313
4
{
314
    // most terminal characters aren't 1:1, so we default to 2:1.
315
    // if user passes their own value of choice, it'll be scaled accordingly.
316
4
    vo->monitor_par = vo->opts->monitor_pixel_aspect * 2;
317
318
4
    struct priv *p = vo->priv;
319
4
    p->sws = mp_sws_alloc(vo);
320
4
    p->sws->log = vo->log;
321
4
    mp_sws_enable_cmdline_opts(p->sws, vo->global);
322
323
1.02k
    for (int i = 0; i < MP_ARRAY_SIZE(p->lut); ++i) {
324
1.02k
        char* out = p->lut[i].str;
325
1.02k
        *out++ = ';';
326
1.02k
        if (i >= 100)
327
624
            *out++ = '0' + (i / 100);
328
1.02k
        if (i >= 10)
329
984
            *out++ = '0' + ((i / 10) % 10);
330
1.02k
        *out++ = '0' + (i % 10);
331
1.02k
        p->lut[i].width = out - p->lut[i].str;
332
1.02k
    }
333
334
4
    WRITE_STR(TERM_ESC_HIDE_CURSOR);
335
4
    terminal_set_mouse_input(true);
336
4
    WRITE_STR(TERM_ESC_ALT_SCREEN);
337
338
4
    return 0;
339
4
}
340
341
static int query_format(struct vo *vo, int format)
342
6.68k
{
343
6.68k
    return format == IMGFMT;
344
6.68k
}
345
346
static int control(struct vo *vo, uint32_t request, void *data)
347
132
{
348
132
    return VO_NOTIMPL;
349
132
}
350
351
#define OPT_BASE_STRUCT struct priv
352
353
const struct vo_driver video_out_tct = {
354
    .name = "tct",
355
    .description = "true-color terminals",
356
    .preinit = preinit,
357
    .query_format = query_format,
358
    .reconfig = reconfig,
359
    .control = control,
360
    .draw_frame = draw_frame,
361
    .flip_page = flip_page,
362
    .uninit = uninit,
363
    .priv_size = sizeof(struct priv),
364
    .priv_defaults = &(const struct priv) {
365
        .opts.algo = ALGO_HALF_BLOCKS,
366
        .opts.buffering = VO_TCT_BUFFER_LINE,
367
    },
368
    .options = (const m_option_t[]) {
369
        {"algo", OPT_CHOICE(opts.algo,
370
            {"plain", ALGO_PLAIN},
371
            {"half-blocks", ALGO_HALF_BLOCKS})},
372
        {"width", OPT_INT(opts.width)},
373
        {"height", OPT_INT(opts.height)},
374
        {"256", OPT_BOOL(opts.term256)},
375
        {"buffering", OPT_CHOICE(opts.buffering,
376
            {"pixel", VO_TCT_BUFFER_PIXEL},
377
            {"line", VO_TCT_BUFFER_LINE},
378
            {"frame", VO_TCT_BUFFER_FRAME})},
379
        {0}
380
    },
381
    .options_prefix = "vo-tct",
382
};