diff options
-rw-r--r-- | DOCS/man/vo.rst | 98 | ||||
-rw-r--r-- | video/out/vo_sixel.c | 314 |
2 files changed, 219 insertions, 193 deletions
diff --git a/DOCS/man/vo.rst b/DOCS/man/vo.rst index be5be2e91e..833fbddb1b 100644 --- a/DOCS/man/vo.rst +++ b/DOCS/man/vo.rst @@ -364,19 +364,59 @@ Available video output drivers are: 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. + Graphical output for the terminal, using sixels. Tested with ``mlterm`` and + ``xterm``. 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=<algo>`` - Selects the diffusion algorithm for dithering used by libsixel. + You may need to use ``--profile=sw-fast`` to get decent performance. + + Note: at the time of writing, ``xterm`` does not enable sixel by default - + launching it as ``xterm -ti 340`` is one way to enable it. Also, ``xterm`` + does not display images bigger than 1000x1000 pixels by default. + + To render and align sixel images correctly, mpv needs to know the terminal + size both in cells and in pixels. By default it tries to use values which + the terminal reports, however, due to differences between terminals this is + an error-prone process which cannot be automated with certainty - some + terminals report the size in pixels including the padding - e.g. ``xterm``, + while others report the actual usable number of pixels - like ``mlterm``. + Additionally, they may behave differently when maximized or in fullscreen, + and mpv cannot detect this state using standard methods. + + Sixel size and alignment options: + + ``--vo-sixel-cols=<columns>``, ``--vo-sixel-rows=<rows>`` (default: 0) + Specify the terminal size in character cells, otherwise (0) read it + from the terminal, or fall back to 80x25. Note that mpv doesn't use the + the last row with sixel because this seems to result in scrolling. + + ``--vo-sixel-width=<width>``, ``--vo-sixel-height=<height>`` (default: 0) + Specify the available size in pixels, otherwise (0) read it from the + terminal, or fall back to 320x240. Other than excluding the last line, + the height is also further rounded down to a multiple of 6 (sixel unit + height) to avoid overflowing below the designated size. + + ``--vo-sixel-left=<col>``, ``--vo-sixel-top=<row>`` (default: 0) + Specify the position in character cells where the image starts (1 is + the first column or row). If 0 (default) then try to automatically + determine it according to the other values and the image aspect ratio + and zoom. + + ``--vo-sixel-pad-x=<pad_x>``, ``--vo-sixel-pad-y=<pad_y>`` (default: -1) + Used only when mpv reads the size in pixels from the terminal. + Specify the number of padding pixels (on one side) which are included + at the size which the terminal reports. If -1 (default) then the number + of pixels is rounded down to a multiple of number of cells (per axis), + to take into account padding at the report - this only works correctly + when the overall padding per axis is smaller than the number of cells. + + Sixel image quality options: + + ``--vo-sixel-dither=<algo>`` + Selects the dither algorithm which libsixel should apply. Can be one of the below list as per libsixel's documentation. auto @@ -398,38 +438,26 @@ Available video output drivers are: xor Positionally stable arithmetic xor based dither - ``--vo-sixel-width=<width>`` ``--vo-sixel-height=<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) + ``--vo-sixel-fixedpalette=<yes|no>`` (default: yes) Use libsixel's built-in static palette using the XTERM256 profile - for dither. Fixed palette uses 256 colors for dithering. + for dither. Fixed palette uses 256 colors for dithering. Note that + using ``no`` (at the time of writing) will slow down ``xterm``. ``--vo-sixel-reqcolors=<colors>`` (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. + Set up libsixel to use required number of colors for dynamic palette. + This value depends on the terminal emulator as well. Xterm supports + 256 colors. Can set this to a lower value for faster performance. This option has no effect if fixed palette is used. - ``--vo-sixel-color-threshold=<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=<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=<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. + ``--vo-sixel-threshold=<threshold>`` (default: -1) + When using a dynamic palette, defines the threshold to change the + palette - as percentage of the number of colors, e.g. 20 will change + the palette when the number of colors changed by 20%. It's a simple + measure to reduce the number of palette changes, because it can be slow + in some terminals (``xterm``), however, it seems that in ``mlterm`` it + causes image corruption. The default (-1) will change the palette + on every frame and will have better quality, and no corruption in + ``mlterm``. ``image`` Output each frame into an image file in the current directory. Each file diff --git a/video/out/vo_sixel.c b/video/out/vo_sixel.c index e1c72230b7..10bececa37 100644 --- a/video/out/vo_sixel.c +++ b/video/out/vo_sixel.c @@ -23,7 +23,6 @@ #include <stdio.h> #include <stdlib.h> -#include <unistd.h> #include <libswscale/swscale.h> #include <sixel.h> @@ -38,16 +37,16 @@ #define IMGFMT IMGFMT_RGB24 -#define TERMINAL_FALLBACK_DEFAULT_WIDTH 80 -#define TERMINAL_FALLBACK_DEFAULT_HEIGHT 25 -#define IMG_FALLBACK_DEFAULT_WIDTH 320 -#define IMG_FALLBACK_DEFAULT_HEIGHT 240 +#define TERMINAL_FALLBACK_COLS 80 +#define TERMINAL_FALLBACK_ROWS 25 +#define TERMINAL_FALLBACK_PX_WIDTH 320 +#define TERMINAL_FALLBACK_PX_HEIGHT 240 -#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" +#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 { @@ -60,6 +59,10 @@ struct priv { int opt_threshold; int opt_top; int opt_left; + int opt_pad_y; + int opt_pad_x; + int opt_rows; + int opt_cols; // Internal data sixel_output_t *output; @@ -67,10 +70,6 @@ struct priv { sixel_dither_t *testdither; uint8_t *buffer; - int image_height; - int image_width; - int image_format; - // The dimensions that will be actually // be used after processing user inputs int top; @@ -78,134 +77,42 @@ struct priv { int width; int height; - unsigned int average_r; - unsigned int average_g; - unsigned int average_b; int previous_histgram_colors; + struct mp_rect src_rect; + struct mp_rect dst_rect; + struct mp_osd_res osd; 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->opt_top; - int left = priv->opt_left; - int terminal_width = TERMINAL_FALLBACK_DEFAULT_WIDTH; - int terminal_height = TERMINAL_FALLBACK_DEFAULT_HEIGHT; - - 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; - else - priv->top = top; - - // 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; - else - priv->left = left; -} - -static void set_output_resolution(struct vo* vo) -{ - struct priv *priv = vo->priv; - - // If both dimensions are set, then no need to calculate - if (priv->opt_height && priv->opt_width) { - priv->width = priv->opt_width; - priv->height = priv->opt_height; - return; - } - - int num_rows = TERMINAL_FALLBACK_DEFAULT_WIDTH; - int num_cols = TERMINAL_FALLBACK_DEFAULT_HEIGHT; - int total_px_width = IMG_FALLBACK_DEFAULT_WIDTH; - int total_px_height = IMG_FALLBACK_DEFAULT_HEIGHT; - - terminal_get_size2(&num_rows, &num_cols, &total_px_width, &total_px_height); - - // The maximum width is the full terminal width - int available_px_width = total_px_width; - - // The maximum height is the amount of pixels - // comprising n-1 rows worth pixels. - // This is because sixel dump after sixel_encode adds a newline - // which can't be used for image display. - int available_px_height = (total_px_height * (num_rows - 1)) / num_rows; - - if (priv->opt_width == 0) - priv->width = available_px_width; - - if (priv->opt_height == 0) - priv->height = available_px_height; -} - 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 threshold is set negative, then every frame must be a scene change + if (priv->dither == NULL || priv->opt_threshold < 0) + return 1; - 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); + histgram_colors = sixel_dither_get_num_of_histogram_colors(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]; + int color_difference_count = previous_histgram_colors - histgram_colors; + color_difference_count = (color_difference_count > 0) ? // abs value + color_difference_count : -color_difference_count; + + if (100 * color_difference_count > + priv->opt_threshold * previous_histgram_colors) + { + priv->previous_histgram_colors = histgram_colors; // update history + return 1; + } else { + return 0; } - score = (r - average_r) * (r - average_r) - + (g - average_g) * (g - average_g) - + (b - average_b) * (b - average_b); - if (score > priv->opt_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) @@ -232,9 +139,9 @@ static SIXELSTATUS prepare_static_palette(struct vo* vo) { struct priv* priv = vo->priv; - if (priv->dither) + if (priv->dither) { sixel_dither_set_body_only(priv->dither, 1); - else { + } else { priv->dither = sixel_dither_get(BUILTIN_XTERM256); if (priv->dither == NULL) return SIXEL_FALSE; @@ -268,38 +175,103 @@ static SIXELSTATUS prepare_dynamic_palette(struct vo *vo) return status; sixel_dither_set_diffusion_type(priv->dither, priv->opt_diffuse); - } else + } else { sixel_dither_set_body_only(priv->dither, 1); + } return status; } -static int resize(struct vo *vo) +static void resize(struct vo *vo) { - struct priv *priv = vo->priv; + // this function sets the vo canvas size in pixels vo->dwidth, vo->dheight, + // and the output scaled size in priv->width, priv->height + // and the scaling rectangles in pixels priv->src_rect, priv->dst_rect + // as well as image positioning in cells priv->top, priv->left. + // no other scaling/rendering size values are required past this point. + struct priv *priv = vo->priv; + int num_rows = TERMINAL_FALLBACK_ROWS; + int num_cols = TERMINAL_FALLBACK_COLS; + int total_px_width = 0; + int total_px_height = 0; - dealloc_dithers_and_buffer(vo); + terminal_get_size2(&num_rows, &num_cols, &total_px_width, &total_px_height); - SIXELSTATUS status = sixel_dither_new(&priv->testdither, priv->opt_reqcolors, NULL); - if (SIXEL_FAILED(status)) - return status; + // If the user has specified rows/cols use them for further calculations + num_rows = (priv->opt_rows > 0) ? priv->opt_rows : num_rows; + num_cols = (priv->opt_cols > 0) ? priv->opt_cols : num_cols; + + // If the pad value is set in between 0 and width/2 - 1, then we + // subtract from the detected width. Otherwise, we assume that the width + // output must be a integer multiple of num_cols and accordingly set + // total_width to be an integer multiple of num_cols. So in case the padding + // added by terminal is less than the number of cells in that axis, then rounding + // down will take care of correcting the detected width and remove padding. + if (priv->opt_width > 0) { + // option - set by the user, hard truth + total_px_width = priv->opt_width; + } else { + if (total_px_width <= 0) { + // ioctl failed to read terminal width + total_px_width = TERMINAL_FALLBACK_PX_WIDTH; + } else { + if (priv->opt_pad_x >= 0 && priv->opt_pad_x < total_px_width / 2) { + // explicit padding set by the user + total_px_width -= (2 * priv->opt_pad_x); + } else { + // rounded "auto padding" + total_px_width = total_px_width / num_cols * num_cols; + } + } + } - priv->buffer = - talloc_array(NULL, uint8_t, depth * priv->width * priv->height); + if (priv->opt_height > 0) { + total_px_height = priv->opt_height; + } else { + if (total_px_height <= 0) { + total_px_height = TERMINAL_FALLBACK_PX_HEIGHT; + } else { + if (priv->opt_pad_y >= 0 && priv->opt_pad_y < total_px_height / 2) { + total_px_height -= (2 * priv->opt_pad_y); + } else { + total_px_height = total_px_height / num_rows * num_rows; + } + } + } - return 0; + // use n-1 rows for height + // The last row can't be used for encoding image, because after sixel encode + // the terminal moves the cursor to next line below the image, causing the + // last line to be empty instead of displaying image data. + // TODO: Confirm if the output height must be a multiple of 6, if not, remove + // the / 6 * 6 part which is setting the height to be a multiple of 6. + vo->dheight = total_px_height * (num_rows - 1) / num_rows / 6 * 6; + vo->dwidth = total_px_width; + + vo_get_src_dst_rects(vo, &priv->src_rect, &priv->dst_rect, &priv->osd); + + // priv->width and priv->height are the width and height of dst_rect + // and they are not changed anywhere else outside this function. + // It is the sixel image output dimension which is output by libsixel. + priv->width = priv->dst_rect.x1 - priv->dst_rect.x0; + priv->height = priv->dst_rect.y1 - priv->dst_rect.y0; + + // top/left values must be greater than 1. If it is set, then + // the image will be rendered from there and no further centering is done. + priv->top = (priv->opt_top > 0) ? priv->opt_top : + num_rows * priv->dst_rect.y0 / vo->dheight + 1; + priv->left = (priv->opt_left > 0) ? priv->opt_left : + num_cols * priv->dst_rect.x0 / vo->dwidth + 1; } 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; - - set_output_resolution(vo); + resize(vo); priv->sws->src = *params; + priv->sws->src.w = mp_rect_w(priv->src_rect); + priv->sws->src.h = mp_rect_h(priv->src_rect); priv->sws->dst = (struct mp_image_params) { .imgfmt = IMGFMT, .w = priv->width, @@ -319,7 +291,15 @@ static int reconfig(struct vo *vo, struct mp_image_params *params) printf(ESC_CLEAR_SCREEN); vo->want_redraw = true; - return resize(vo); + dealloc_dithers_and_buffer(vo); + SIXELSTATUS status = sixel_dither_new(&priv->testdither, priv->opt_reqcolors, NULL); + if (SIXEL_FAILED(status)) + return status; + + priv->buffer = + talloc_array(NULL, uint8_t, depth * priv->width * priv->height); + + return 0; } static void draw_image(struct vo *vo, mp_image_t *mpi) @@ -327,6 +307,11 @@ static void draw_image(struct vo *vo, mp_image_t *mpi) struct priv *priv = vo->priv; struct mp_image src = *mpi; + struct mp_rect src_rc = priv->src_rect; + 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(&src, src_rc); + // Downscale the image mp_sws_scale(priv->sws, priv->frame, &src); @@ -334,10 +319,11 @@ static void draw_image(struct vo *vo, mp_image_t *mpi) memcpy_pic(priv->buffer, priv->frame->planes[0], priv->width * depth, priv->height, priv->width * depth, priv->frame->stride[0]); - if (priv->opt_fixedpal) + if (priv->opt_fixedpal) { prepare_static_palette(vo); - else + } else { prepare_dynamic_palette(vo); + } talloc_free(mpi); } @@ -391,16 +377,12 @@ static int preinit(struct vo *vo) if (SIXEL_FAILED(status)) return status; + resize(vo); 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; } @@ -411,7 +393,15 @@ static int query_format(struct vo *vo, int format) static int control(struct vo *vo, uint32_t request, void *data) { - return VO_NOTIMPL; + if (request == VOCTRL_SET_PANSCAN) { + if (!reconfig(vo, vo->params)) { + return VO_TRUE; + } else { + return VO_FALSE; + } + } else { + return VO_NOTIMPL; + } } @@ -452,12 +442,16 @@ const struct vo_driver video_out_sixel = { .opt_height = 0, .opt_reqcolors = 256, .opt_fixedpal = 0, - .opt_threshold = 0, - .opt_top = 1, - .opt_left = 1, + .opt_threshold = -1, + .opt_top = 0, + .opt_left = 0, + .opt_pad_y = -1, + .opt_pad_x = -1, + .opt_rows = 0, + .opt_cols = 0, }, .options = (const m_option_t[]) { - {"diffusion", OPT_CHOICE(opt_diffuse, + {"dither", OPT_CHOICE(opt_diffuse, {"auto", DIFFUSE_AUTO}, {"none", DIFFUSE_NONE}, {"atkinson", DIFFUSE_ATKINSON}, @@ -470,10 +464,14 @@ const struct vo_driver video_out_sixel = { {"width", OPT_INT(opt_width)}, {"height", OPT_INT(opt_height)}, {"reqcolors", OPT_INT(opt_reqcolors)}, - {"fixedpalette", OPT_INT(opt_fixedpal)}, - {"color-threshold", OPT_INT(opt_threshold)}, - {"offset-top", OPT_INT(opt_top)}, - {"offset-left", OPT_INT(opt_left)}, + {"fixedpalette", OPT_FLAG(opt_fixedpal)}, + {"threshold", OPT_INT(opt_threshold)}, + {"top", OPT_INT(opt_top)}, + {"left", OPT_INT(opt_left)}, + {"pad-y", OPT_INT(opt_pad_y)}, + {"pad-x", OPT_INT(opt_pad_x)}, + {"rows", OPT_INT(opt_rows)}, + {"cols", OPT_INT(opt_cols)}, {0} }, .options_prefix = "vo-sixel", |