summaryrefslogtreecommitdiffstats
path: root/video/out
diff options
context:
space:
mode:
authorShreesh Adiga <16567adigashreesh@gmail.com>2020-11-01 16:43:31 +0530
committerAvi Halachmi (:avih) <avihpit@yahoo.com>2020-11-07 18:51:49 +0200
commit19913921eb9ee4c35de230c62c46d1591cf3326e (patch)
tree432ddcb28063d6150ca85b7e4e6e0cd77c11c600 /video/out
parentdfa5ae2ad61cba4fab850a41779ce95332c27935 (diff)
downloadmpv-19913921eb9ee4c35de230c62c46d1591cf3326e.tar.bz2
mpv-19913921eb9ee4c35de230c62c46d1591cf3326e.tar.xz
video/out/vo_sixel.c: Implement sixel as a output device
Based on the implementation of ffmpeg's sixel backend output written by Hayaki Saito https://github.com/saitoha/FFmpeg-SIXEL/blob/sixel/libavdevice/sixel.c Sixel is a protocol to display graphics in a terminal. This commit adds support to play videos on a sixel enabled terminal using libsixel. With --vo=sixel, the output will be in sixel format. The input frame will be scaled to the user specified resolution (--vo-sixel-width and --vo-sixel-height) using swscaler and then encoded using libsixel and output to the terminal. This method requires high cpu and there are high frame drops for 720p and higher resolution videos and might require using lesser colors and have drop in quality. Docs have all the supported options listed to fine tune the output quality. TODO: A few parameters of libsixel such as the sixel_encode_policy and the SIXEL_XTERM16 variables are hardcoded, might want to expose them as command line options. Also the initialization resolution is not automatic and if the user doesn't specify the dimensions, it picks 320x240 as the default resolution which is not optimal. So need to automatically pick the best fit resolution for the current open terminal window size.
Diffstat (limited to 'video/out')
-rw-r--r--video/out/vo.c4
-rw-r--r--video/out/vo_sixel.c421
2 files changed, 425 insertions, 0 deletions
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",
+};