diff --git a/DOCS/man/vo.rst b/DOCS/man/vo.rst index 570e244e5c..be5be2e91e 100644 --- a/DOCS/man/vo.rst +++ b/DOCS/man/vo.rst @@ -363,6 +363,74 @@ Available video output drivers are: ``--vo-tct-256=`` (default: no) Use 256 colors - for terminals which don't support true color. +``sixel`` + Sixel graphics video output driver based on libsixel that works on a + console that has sixel graphics enabled such as ``xterm`` or ``mlterm``. + Additionally some terminals have limitation on the dimensions, so may + not display images bigger than 1000x1000 for example. Make sure that + ``img2sixel`` can display images of the corresponding resolution. + You may need to use ``--profile=sw-fast`` to get decent performance. + + Note: the Sixel image output is not synchronized with other terminal output + from mpv, which can lead to broken images. The option ``--really-quiet`` + can help with that, and is recommended. + + ``--vo-sixel-diffusion=`` + Selects the diffusion algorithm for dithering used by libsixel. + Can be one of the below list as per libsixel's documentation. + + auto + Choose diffuse type automatically + none + Don't diffuse + atkinson + Diffuse with Bill Atkinson's method. (Default) + fs + Diffuse with Floyd-Steinberg method + jajuni + Diffuse with Jarvis, Judice & Ninke method + stucki + Diffuse with Stucki's method + burkes + Diffuse with Burkes' method + arithmetic + Positionally stable arithmetic dither + xor + Positionally stable arithmetic xor based dither + + ``--vo-sixel-width=`` ``--vo-sixel-height=`` + The output video resolution will be set to width and height + These default to 320x240 if not set. The terminal window must + be bigger than this resolution to have smooth playback. + Additionally the last row will be a blank line and can't be + used to display pixel data. + + ``--vo-sixel-fixedpalette=<0|1>`` (default: 0) + Use libsixel's built-in static palette using the XTERM256 profile + for dither. Fixed palette uses 256 colors for dithering. + + ``--vo-sixel-reqcolors=`` (default: 256) + Setup libsixel to use required number of colors for dynamic palette. + This value depends on the console as well. Xterm supports 256. + Can set this to a lower value for faster performance. + This option has no effect if fixed palette is used. + + ``--vo-sixel-color-threshold=`` (default: 0) + This threshold value is used in dynamic palette mode to + recompute the palette based on the scene changes. + + ``--vo-sixel-offset-top=`` (default: 1) + The output video playback will start from the specified row number. + If this is greater than 1, then those many rows will be skipped. + This option can be used to shift video below in the terminal. + If it is greater than number of rows in terminal, then it is ignored. + + ``--vo-sixel-offset-left=`` (default: 1) + The output video playback will start from the specified column number. + If this is greater than 1, then those many columns will be skipped. + This option can be used to shift video to the right in the terminal. + If it is greater than number of columns in terminal, then it is ignored. + ``image`` Output each frame into an image file in the current directory. Each file takes the frame number padded with leading zeros as name. diff --git a/video/out/vo.c b/video/out/vo.c index 27be4735ab..4cb15123ab 100644 --- a/video/out/vo.c +++ b/video/out/vo.c @@ -64,6 +64,7 @@ extern const struct vo_driver video_out_vaapi; extern const struct vo_driver video_out_wlshm; extern const struct vo_driver video_out_rpi; extern const struct vo_driver video_out_tct; +extern const struct vo_driver video_out_sixel; const struct vo_driver *const video_out_drivers[] = { @@ -105,6 +106,9 @@ const struct vo_driver *const video_out_drivers[] = #endif #if HAVE_RPI_MMAL &video_out_rpi, +#endif +#if HAVE_SIXEL + &video_out_sixel, #endif &video_out_lavc, NULL diff --git a/video/out/vo_sixel.c b/video/out/vo_sixel.c new file mode 100644 index 0000000000..cce6a3da9d --- /dev/null +++ b/video/out/vo_sixel.c @@ -0,0 +1,421 @@ +/* + * Sixel mpv output device implementation based on ffmpeg libavdevice implementation + * by Hayaki Saito + * https://github.com/saitoha/FFmpeg-SIXEL/blob/sixel/libavdevice/sixel.c + * + * Copyright (c) 2014 Hayaki Saito + * + * 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 +#include + +#include "config.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 ESC_HIDE_CURSOR "\033[?25l" +#define ESC_RESTORE_CURSOR "\033[?25h" +#define ESC_CLEAR_SCREEN "\033[2J" +#define ESC_GOTOXY "\033[%d;%df" +#define ESC_USE_GLOBAL_COLOR_REG "\033[?1070l" + +struct priv { + + // User specified options + int diffuse; + int width; + int height; + int reqcolors; + int fixedpal; + int threshold; + int top; + int left; + + // Internal data + sixel_output_t *output; + sixel_dither_t *dither; + sixel_dither_t *testdither; + uint8_t *buffer; + + int image_height; + int image_width; + int image_format; + + unsigned int average_r; + unsigned int average_g; + unsigned int average_b; + int previous_histgram_colors; + + struct mp_image *frame; + struct mp_sws_context *sws; +}; + +static const unsigned int depth = 3; + +static void validate_offset_values(struct vo* vo) +{ + struct priv* priv = vo->priv; + int top = priv->top; + int left = priv->left; + int terminal_width = 0; + int terminal_height = 0; + + terminal_get_size(&terminal_width, &terminal_height); + + // Make sure that the user specified top offset + // lies in the range 1 to TERMINAL_HEIGHT + // Otherwise default to the topmost row + if (top <= 0 || top > terminal_height) + priv->top = 1; + + // Make sure that the user specified left offset + // lies in the range 1 to TERMINAL_WIDTH + // Otherwise default to the leftmost column + if (left <= 0 || left > terminal_width) + priv->left = 1; +} + +static int detect_scene_change(struct vo* vo) +{ + struct priv* priv = vo->priv; + int score; + int i; + unsigned int r = 0; + unsigned int g = 0; + unsigned int b = 0; + + unsigned int average_r = priv->average_r; + unsigned int average_g = priv->average_g; + unsigned int average_b = priv->average_b; + int previous_histgram_colors = priv->previous_histgram_colors; + + int histgram_colors = 0; + int palette_colors = 0; + unsigned char const* palette; + + histgram_colors = sixel_dither_get_num_of_histogram_colors(priv->testdither); + + if (priv->dither == NULL) + goto detected; + + /* detect scene change if number of colors increses 20% */ + if (previous_histgram_colors * 6 < histgram_colors * 5) + goto detected; + + /* detect scene change if number of colors decreses 20% */ + if (previous_histgram_colors * 4 > histgram_colors * 5) + goto detected; + + palette_colors = sixel_dither_get_num_of_palette_colors(priv->testdither); + palette = sixel_dither_get_palette(priv->testdither); + + /* compare color difference between current + * palette and previous one */ + for (i = 0; i < palette_colors; i++) { + r += palette[i * 3 + 0]; + g += palette[i * 3 + 1]; + b += palette[i * 3 + 2]; + } + score = (r - average_r) * (r - average_r) + + (g - average_g) * (g - average_g) + + (b - average_b) * (b - average_b); + if (score > priv->threshold * palette_colors + * palette_colors) + goto detected; + + return 0; + +detected: + priv->previous_histgram_colors = histgram_colors; + priv->average_r = r; + priv->average_g = g; + priv->average_b = b; + return 1; +} + +static void dealloc_dithers_and_buffer(struct vo* vo) +{ + struct priv* priv = vo->priv; + + talloc_free(priv->buffer); + + if (priv->dither) { + sixel_dither_unref(priv->dither); + priv->dither = NULL; + } + + if (priv->testdither) { + sixel_dither_unref(priv->testdither); + priv->testdither = NULL; + } +} + +static SIXELSTATUS prepare_static_palette(struct vo* vo) +{ + struct priv* priv = vo->priv; + + if (priv->dither) + sixel_dither_set_body_only(priv->dither, 1); + else { + priv->dither = sixel_dither_get(BUILTIN_XTERM256); + if (priv->dither == NULL) + return SIXEL_FALSE; + sixel_dither_set_diffusion_type(priv->dither, priv->diffuse); + } + return SIXEL_OK; +} + +static SIXELSTATUS prepare_dynamic_palette(struct vo *vo) +{ + SIXELSTATUS status = SIXEL_FALSE; + struct priv *priv = vo->priv; + + /* create histgram and construct color palette + * with median cut algorithm. */ + status = sixel_dither_initialize(priv->testdither, priv->buffer, + priv->width, priv->height, 3, + LARGE_NORM, REP_CENTER_BOX, + QUALITY_LOW); + if (SIXEL_FAILED(status)) + return status; + + if (detect_scene_change(vo)) { + if (priv->dither) + sixel_dither_unref(priv->dither); + + priv->dither = priv->testdither; + status = sixel_dither_new(&priv->testdither, priv->reqcolors, NULL); + + if (SIXEL_FAILED(status)) + return status; + + sixel_dither_set_diffusion_type(priv->dither, priv->diffuse); + } else + sixel_dither_set_body_only(priv->dither, 1); + + return status; +} + +static int resize(struct vo *vo) +{ + struct priv *priv = vo->priv; + + dealloc_dithers_and_buffer(vo); + + SIXELSTATUS status = sixel_dither_new(&priv->testdither, priv->reqcolors, NULL); + if (SIXEL_FAILED(status)) + return status; + + priv->buffer = + talloc_array(NULL, uint8_t, depth * priv->width * priv->height); + + return 0; +} + +static int reconfig(struct vo *vo, struct mp_image_params *params) +{ + struct priv *priv = vo->priv; + priv->image_height = params->h; + priv->image_width = params->w; + priv->image_format = params->imgfmt; + + priv->sws->src = *params; + priv->sws->dst = (struct mp_image_params) { + .imgfmt = IMGFMT, + .w = priv->width, + .h = priv->height, + .p_w = 1, + .p_h = 1, + }; + + priv->frame = mp_image_alloc(IMGFMT, priv->width, priv->height); + if (!priv->frame) + return -1; + + if (mp_sws_reinit(priv->sws) < 0) + return -1; + + printf(ESC_HIDE_CURSOR); + printf(ESC_CLEAR_SCREEN); + vo->want_redraw = true; + + return resize(vo); +} + +static void draw_image(struct vo *vo, mp_image_t *mpi) +{ + struct priv *priv = vo->priv; + struct mp_image src = *mpi; + + // Downscale the image + mp_sws_scale(priv->sws, priv->frame, &src); + + // Copy from mpv to RGB format as required by libsixel + memcpy_pic(priv->buffer, priv->frame->planes[0], priv->width * depth, priv->height, + priv->width * depth, priv->frame->stride[0]); + + if (priv->fixedpal) + prepare_static_palette(vo); + else + prepare_dynamic_palette(vo); + + talloc_free(mpi); +} + +static int sixel_write(char *data, int size, void *priv) +{ + return fwrite(data, 1, size, (FILE *)priv); +} + +static void flip_page(struct vo *vo) +{ + struct priv* priv = vo->priv; + + // Go to the offset row and column, then display the image + printf(ESC_GOTOXY, priv->top, priv->left); + sixel_encode(priv->buffer, priv->width, priv->height, + PIXELFORMAT_RGB888, + priv->dither, priv->output); + fflush(stdout); +} + +static int preinit(struct vo *vo) +{ + struct priv *priv = vo->priv; + SIXELSTATUS status = SIXEL_FALSE; + FILE* sixel_output_file = stdout; + + // Parse opts set by CLI or conf + priv->sws = mp_sws_alloc(vo); + priv->sws->log = vo->log; + mp_sws_enable_cmdline_opts(priv->sws, vo->global); + + status = sixel_output_new(&priv->output, sixel_write, sixel_output_file, NULL); + if (SIXEL_FAILED(status)) + return status; + + sixel_output_set_encode_policy(priv->output, SIXEL_ENCODEPOLICY_FAST); + + printf(ESC_HIDE_CURSOR); + + /* don't use private color registers for each frame. */ + printf(ESC_USE_GLOBAL_COLOR_REG); + + priv->dither = NULL; + status = sixel_dither_new(&priv->testdither, priv->reqcolors, NULL); + + if (SIXEL_FAILED(status)) + return status; + + priv->buffer = + talloc_array(NULL, uint8_t, depth * priv->width * priv->height); + + priv->average_r = 0; + priv->average_g = 0; + priv->average_b = 0; + priv->previous_histgram_colors = 0; + + validate_offset_values(vo); + + 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) +{ + return VO_NOTIMPL; +} + + +static void uninit(struct vo *vo) +{ + struct priv *priv = vo->priv; + + printf(ESC_RESTORE_CURSOR); + + printf(ESC_CLEAR_SCREEN); + printf(ESC_GOTOXY, 1, 1); + fflush(stdout); + + if (priv->output) { + sixel_output_unref(priv->output); + priv->output = NULL; + } + + dealloc_dithers_and_buffer(vo); +} + +#define OPT_BASE_STRUCT struct priv + +const struct vo_driver video_out_sixel = { + .name = "sixel", + .description = "libsixel", + .preinit = preinit, + .query_format = query_format, + .reconfig = reconfig, + .control = control, + .draw_image = draw_image, + .flip_page = flip_page, + .uninit = uninit, + .priv_size = sizeof(struct priv), + .priv_defaults = &(const struct priv) { + .diffuse = DIFFUSE_ATKINSON, + .width = 320, + .height = 240, + .reqcolors = 256, + .fixedpal = 0, + .threshold = 0, + .top = 1, + .left = 1, + }, + .options = (const m_option_t[]) { + {"diffusion", OPT_CHOICE(diffuse, + {"auto", DIFFUSE_AUTO}, + {"none", DIFFUSE_NONE}, + {"atkinson", DIFFUSE_ATKINSON}, + {"fs", DIFFUSE_FS}, + {"jajuni", DIFFUSE_JAJUNI}, + {"stucki", DIFFUSE_STUCKI}, + {"burkes", DIFFUSE_BURKES}, + {"arithmetic", DIFFUSE_A_DITHER}, + {"xor", DIFFUSE_X_DITHER})}, + {"width", OPT_INT(width)}, + {"height", OPT_INT(height)}, + {"reqcolors", OPT_INT(reqcolors)}, + {"fixedpalette", OPT_INT(fixedpal)}, + {"color-threshold", OPT_INT(threshold)}, + {"offset-top", OPT_INT(top)}, + {"offset-left", OPT_INT(left)}, + {0} + }, + .options_prefix = "vo-sixel", +}; diff --git a/wscript b/wscript index 3d9430baf9..878e4f2596 100644 --- a/wscript +++ b/wscript @@ -741,6 +741,10 @@ video_output_features = [ 'desc': 'EGL helper functions', 'deps': 'egl || rpi || egl-angle-win32 || egl-android', 'func': check_true + }, { + 'name': '--sixel', + 'desc': 'Sixel', + 'func': check_pkg_config('libsixel', '>= 1.5'), } ] diff --git a/wscript_build.py b/wscript_build.py index 4de6bdbf27..14c254e1ec 100644 --- a/wscript_build.py +++ b/wscript_build.py @@ -497,6 +497,7 @@ def build(ctx): ( "video/out/vo_null.c" ), ( "video/out/vo_rpi.c", "rpi-mmal" ), ( "video/out/vo_sdl.c", "sdl2-video" ), + ( "video/out/vo_sixel.c", "sixel" ), ( "video/out/vo_tct.c" ), ( "video/out/vo_vaapi.c", "vaapi-x11 && gpl" ), ( "video/out/vo_vdpau.c", "vdpau" ),