/*
* Video output device using the kitty terminal graphics protocol
* See https://sw.kovidgoyal.net/kitty/graphics-protocol/
*
* This file is part of mpv.
*
* mpv is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* mpv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with mpv. If not, see .
*/
#include
#include
#include
#include "config.h"
#if HAVE_POSIX
#include
#include
#include
#include
#endif
#include
#include
#include "options/m_config.h"
#include "osdep/terminal.h"
#include "sub/osd.h"
#include "vo.h"
#include "video/sws_utils.h"
#include "video/mp_image.h"
#define IMGFMT IMGFMT_RGB24
#define BYTES_PER_PX 3
#define DEFAULT_WIDTH_PX 320
#define DEFAULT_HEIGHT_PX 240
#define DEFAULT_WIDTH 80
#define DEFAULT_HEIGHT 25
static inline void write_str(const char *s)
{
// On POSIX platforms, write() is the fastest method. It also is the only
// one that allows atomic writes so mpv’s output will not be interrupted
// by other processes or threads that write to stdout, which would cause
// screen corruption. POSIX does not guarantee atomicity for writes
// exceeding PIPE_BUF, but at least Linux does seem to implement it that
// way.
#if HAVE_POSIX
int remain = strlen(s);
while (remain > 0) {
ssize_t written = write(STDOUT_FILENO, s, remain);
if (written < 0)
return;
remain -= written;
s += written;
}
#else
printf("%s", s);
fflush(stdout);
#endif
}
#define KITTY_ESC_IMG "\033_Ga=T,f=24,s=%d,v=%d,C=1,q=2,m=1;"
#define KITTY_ESC_IMG_SHM "\033_Ga=T,t=s,f=24,s=%d,v=%d,C=1,q=2,m=1;%s\033\\"
#define KITTY_ESC_CONTINUE "\033_Gm=%d;"
#define KITTY_ESC_END "\033\\"
#define KITTY_ESC_DELETE_ALL "\033_Ga=d;\033\\"
struct vo_kitty_opts {
int width, height, top, left, rows, cols;
int config_clear, alt_screen;
int use_shm;
};
struct priv {
struct vo_kitty_opts opts;
uint8_t *buffer;
char *output;
char *shm_path, *shm_path_b64;
int buffer_size, output_size;
int shm_fd;
int left, top, width, height, cols, rows;
struct mp_rect src;
struct mp_rect dst;
struct mp_osd_res osd;
struct mp_image *frame;
struct mp_sws_context *sws;
};
#if HAVE_POSIX
static struct sigaction saved_sigaction = {0};
static bool resized;
#endif
static void close_shm(struct priv *p)
{
#if HAVE_POSIX_SHM
if (p->buffer != NULL) {
munmap(p->buffer, p->buffer_size);
p->buffer = NULL;
}
if (p->shm_fd != -1) {
close(p->shm_fd);
p->shm_fd = -1;
}
#endif
}
static void free_bufs(struct vo* vo)
{
struct priv* p = vo->priv;
talloc_free(p->frame);
talloc_free(p->output);
if (p->opts.use_shm) {
close_shm(p);
} else {
talloc_free(p->buffer);
}
}
static void get_win_size(struct vo *vo, int *out_rows, int *out_cols,
int *out_width, int *out_height)
{
struct priv *p = vo->priv;
*out_rows = DEFAULT_HEIGHT;
*out_cols = DEFAULT_WIDTH;
*out_width = DEFAULT_WIDTH_PX;
*out_height = DEFAULT_HEIGHT_PX;
terminal_get_size2(out_rows, out_cols, out_width, out_height);
*out_rows = p->opts.rows > 0 ? p->opts.rows : *out_rows;
*out_cols = p->opts.cols > 0 ? p->opts.cols : *out_cols;
*out_width = p->opts.width > 0 ? p->opts.width : *out_width;
*out_height = p->opts.height > 0 ? p->opts.height : *out_height;
}
static void set_out_params(struct vo *vo)
{
struct priv *p = vo->priv;
vo_get_src_dst_rects(vo, &p->src, &p->dst, &p->osd);
p->width = p->dst.x1 - p->dst.x0;
p->height = p->dst.y1 - p->dst.y0;
p->top = p->opts.top > 0 ?
p->opts.top : p->rows * p->dst.y0 / vo->dheight;
p->left = p->opts.left > 0 ?
p->opts.left : p->cols * p->dst.x0 / vo->dwidth;
p->buffer_size = 3 * p->width * p->height;
p->output_size = AV_BASE64_SIZE(p->buffer_size);
}
static int reconfig(struct vo *vo, struct mp_image_params *params)
{
struct priv *p = vo->priv;
vo->want_redraw = true;
write_str(KITTY_ESC_DELETE_ALL);
if (p->opts.config_clear)
write_str(TERM_ESC_CLEAR_SCREEN);
get_win_size(vo, &p->rows, &p->cols, &vo->dwidth, &vo->dheight);
set_out_params(vo);
free_bufs(vo);
p->sws->src = *params;
p->sws->src.w = mp_rect_w(p->src);
p->sws->src.h = mp_rect_h(p->src);
p->sws->dst = (struct mp_image_params) {
.imgfmt = IMGFMT,
.w = p->width,
.h = p->height,
.p_w = 1,
.p_h = 1,
};
p->frame = mp_image_alloc(IMGFMT, p->width, p->height);
if (!p->frame)
return -1;
if (mp_sws_reinit(p->sws) < 0)
return -1;
if (!p->opts.use_shm) {
p->buffer = talloc_array(NULL, uint8_t, p->buffer_size);
p->output = talloc_array(NULL, char, p->output_size);
}
return 0;
}
static int create_shm(struct vo *vo)
{
#if HAVE_POSIX_SHM
struct priv *p = vo->priv;
p->shm_fd = shm_open(p->shm_path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (p->shm_fd == -1) {
MP_ERR(vo, "Failed to create shared memory object");
return 0;
}
if (ftruncate(p->shm_fd, p->buffer_size) == -1) {
MP_ERR(vo, "Failed to truncate shared memory object");
shm_unlink(p->shm_path);
close(p->shm_fd);
return 0;
}
p->buffer = mmap(NULL, p->buffer_size,
PROT_READ | PROT_WRITE, MAP_SHARED, p->shm_fd, 0);
if (p->buffer == MAP_FAILED) {
MP_ERR(vo, "Failed to mmap shared memory object");
shm_unlink(p->shm_path);
close(p->shm_fd);
return 0;
}
return 1;
#else
return 0;
#endif
}
static void draw_frame(struct vo *vo, struct vo_frame *frame)
{
struct priv *p = vo->priv;
mp_image_t *mpi = NULL;
#if !HAVE_POSIX
int prev_height = vo->dheight;
int prev_width = vo->dwidth;
get_win_size(vo, &p->rows, &p->cols, &vo->dwidth, &vo->dheight);
bool resized = (prev_width != vo->dwidth || prev_height != vo->dheight);
#endif
if (resized)
reconfig(vo, vo->params);
resized = false;
if (frame->current) {
mpi = mp_image_new_ref(frame->current);
struct mp_rect src_rc = p->src;
src_rc.x0 = MP_ALIGN_DOWN(src_rc.x0, mpi->fmt.align_x);
src_rc.y0 = MP_ALIGN_DOWN(src_rc.y0, mpi->fmt.align_y);
mp_image_crop_rc(mpi, src_rc);
mp_sws_scale(p->sws, p->frame, mpi);
} else {
mp_image_clear(p->frame, 0, 0, p->width, p->height);
}
struct mp_osd_res res = { .w = p->width, .h = p->height };
osd_draw_on_image(vo->osd, res, mpi ? mpi->pts : 0, 0, p->frame);
if (p->opts.use_shm && !create_shm(vo))
return;
memcpy_pic(p->buffer, p->frame->planes[0], p->width * BYTES_PER_PX,
p->height, p->width * BYTES_PER_PX, p->frame->stride[0]);
if (!p->opts.use_shm)
av_base64_encode(p->output, p->output_size, p->buffer, p->buffer_size);
talloc_free(mpi);
}
static void flip_page(struct vo *vo)
{
struct priv* p = vo->priv;
if (p->buffer == NULL)
return;
char *cmd = talloc_asprintf(NULL, TERM_ESC_GOTO_YX, p->top, p->left);
if (p->opts.use_shm) {
cmd = talloc_asprintf_append(cmd, KITTY_ESC_IMG_SHM, p->width, p->height, p->shm_path_b64);
} else {
if (p->output == NULL) {
talloc_free(cmd);
return;
}
cmd = talloc_asprintf_append(cmd, KITTY_ESC_IMG, p->width, p->height);
for (int offset = 0, noffset;; offset += noffset) {
if (offset)
cmd = talloc_asprintf_append(cmd, KITTY_ESC_CONTINUE, offset < p->output_size);
noffset = MPMIN(4096, p->output_size - offset);
cmd = talloc_strndup_append(cmd, p->output + offset, noffset);
cmd = talloc_strdup_append(cmd, KITTY_ESC_END);
if (offset >= p->output_size)
break;
}
}
write_str(cmd);
talloc_free(cmd);
#if HAVE_POSIX
if (p->opts.use_shm)
close_shm(p);
#endif
}
#if HAVE_POSIX
static void handle_winch(int sig) {
resized = true;
if (saved_sigaction.sa_handler)
saved_sigaction.sa_handler(sig);
}
#endif
static int preinit(struct vo *vo)
{
struct priv *p = vo->priv;
p->sws = mp_sws_alloc(vo);
p->sws->log = vo->log;
mp_sws_enable_cmdline_opts(p->sws, vo->global);
#if HAVE_POSIX
struct sigaction sa;
sa.sa_handler = handle_winch;
sigaction(SIGWINCH, &sa, &saved_sigaction);
#endif
#if HAVE_POSIX_SHM
if (p->opts.use_shm) {
p->shm_path = talloc_asprintf(vo, "/mpv-kitty-%p", vo);
int p_size = strlen(p->shm_path) - 1;
int b64_size = AV_BASE64_SIZE(p_size);
p->shm_path_b64 = talloc_array(vo, char, b64_size);
av_base64_encode(p->shm_path_b64, b64_size, p->shm_path + 1, p_size);
}
#else
if (p->opts.use_shm) {
MP_ERR(vo, "Shared memory support is not available on this platform.");
return -1;
}
#endif
write_str(TERM_ESC_HIDE_CURSOR);
if (p->opts.alt_screen)
write_str(TERM_ESC_ALT_SCREEN);
return 0;
}
static int query_format(struct vo *vo, int format)
{
return format == IMGFMT;
}
static int control(struct vo *vo, uint32_t request, void *data)
{
if (request == VOCTRL_SET_PANSCAN)
return (vo->config_ok && !reconfig(vo, vo->params)) ? VO_TRUE : VO_FALSE;
return VO_NOTIMPL;
}
static void uninit(struct vo *vo)
{
struct priv *p = vo->priv;
#if HAVE_POSIX
sigaction(SIGWINCH, &saved_sigaction, NULL);
#endif
write_str(TERM_ESC_RESTORE_CURSOR);
if (p->opts.alt_screen) {
write_str(TERM_ESC_NORMAL_SCREEN);
} else {
char *cmd = talloc_asprintf(vo, TERM_ESC_GOTO_YX, p->cols, 0);
write_str(cmd);
}
free_bufs(vo);
}
#define OPT_BASE_STRUCT struct priv
const struct vo_driver video_out_kitty = {
.name = "kitty",
.description = "Kitty terminal graphics protocol",
.preinit = preinit,
.query_format = query_format,
.reconfig = reconfig,
.control = control,
.draw_frame = draw_frame,
.flip_page = flip_page,
.uninit = uninit,
.priv_size = sizeof(struct priv),
.priv_defaults = &(const struct priv) {
.shm_fd = -1,
.opts.config_clear = 1,
.opts.alt_screen = 1,
},
.options = (const m_option_t[]) {
{"width", OPT_INT(opts.width)},
{"height", OPT_INT(opts.height)},
{"top", OPT_INT(opts.top)},
{"left", OPT_INT(opts.left)},
{"rows", OPT_INT(opts.rows)},
{"cols", OPT_INT(opts.cols)},
{"config-clear", OPT_FLAG(opts.config_clear), },
{"alt-screen", OPT_FLAG(opts.alt_screen), },
{"use-shm", OPT_FLAG(opts.use_shm), },
{0}
},
.options_prefix = "vo-kitty",
};