mirror of
https://github.com/mpv-player/mpv.git
synced 2025-12-28 05:33:14 +00:00
x11 and wayland VOs + backends all have logic that skips drawing frames when the mpv isn't visible for efficiency reasons. This is implemented in each individual VO, but it is useful for the core to be aware of this as well. Currently, the display-sync modes on x11 do not try to be efficient and always render anyways because there is no blocking on the graphics API level if you skip rendering a frame (wayland works here by accident). This means mpv would otherwise blaze through frames which is not what anyone wants. We can instead improve this by implementing the blocking mechanism in the core and using that when appropriate. This commit does not implement that yet, but simply switches the draw_frame to a boolean. Receiving false means that the VO is not drawing frames and it should be treated as if the surface was hidden.
440 lines
11 KiB
C
440 lines
11 KiB
C
/*
|
||
* 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 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;
|
||
bool config_clear, alt_screen;
|
||
bool 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 bool 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))
|
||
goto done;
|
||
|
||
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);
|
||
|
||
done:
|
||
return VO_TRUE;
|
||
}
|
||
|
||
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_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);
|
||
terminal_set_mouse_input(true);
|
||
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);
|
||
terminal_set_mouse_input(false);
|
||
|
||
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 = true,
|
||
.opts.alt_screen = true,
|
||
},
|
||
.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_BOOL(opts.config_clear), },
|
||
{"alt-screen", OPT_BOOL(opts.alt_screen), },
|
||
{"use-shm", OPT_BOOL(opts.use_shm), },
|
||
{0}
|
||
},
|
||
.options_prefix = "vo-kitty",
|
||
};
|