/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 | | }; |