summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--DOCS/man/vo.rst68
-rw-r--r--video/out/vo.c4
-rw-r--r--video/out/vo_sixel.c421
-rw-r--r--wscript4
-rw-r--r--wscript_build.py1
5 files changed, 498 insertions, 0 deletions
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=<yes|no>`` (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=<algo>``
+ 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=<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)
+ Use libsixel's built-in static palette using the XTERM256 profile
+ for dither. Fixed palette uses 256 colors for dithering.
+
+ ``--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.
+ 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.
+
``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[] =
{
@@ -106,6 +107,9 @@ const struct vo_driver *const video_out_drivers[] =
#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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <libswscale/swscale.h>
+#include <sixel.h>
+
+#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" ),