mpv/video/out/vo_kitty.c

434 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include "config.h"
#if HAVE_POSIX
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
#include <libswscale/swscale.h>
#include <libavutil/base64.h>
#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 mpvs 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",
};