mpv/video/out/vo_sixel.c

422 lines
11 KiB
C

/*
* 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",
};