You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

468 lines
17 KiB
C++

// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*-
//
// Quick hack based on ffmpeg
// tutorial http://dranger.com/ffmpeg/tutorial01.html
// in turn based on a tutorial by
// Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
//
// HELP NEEDED
// Note, this is known to not be optimal, causing flicker etc. It is at this
// point merely a demonstration of what is possible. It also serves as a
// converter to a 'stream' (-O option) which then can be played quickly with
// the led-image-viewer.
//
// Pull requests are welcome to address
// * Use hardware acceleration if possible. The Pi does have some
// acceleration features IIRC, so if we could use these, that would be
// great.
// * Other improvements that could reduce the flicker on a Raspberry Pi.
// Currently it seems to create flicker in particular when decoding larger
// videos due to memory bandwidth overload (?). Might already be fixed
// with using hardware acceleration.
// * Add sound ? Right now, we don't decode the sound. It is usually
// not very useful as the builtin-sound is disabled when running the
// LED matrix, but if there is an external USB sound adapter, it might
// be nice.
// Ancient AV versions forgot to set this.
#define __STDC_CONSTANT_MACROS
// libav: "U NO extern C in header ?"
extern "C" {
# include <libavcodec/avcodec.h>
# include <libavformat/avformat.h>
# include <libavutil/imgutils.h>
# include <libswscale/swscale.h>
}
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <thread>
#include "led-matrix.h"
#include "content-streamer.h"
using rgb_matrix::FrameCanvas;
using rgb_matrix::RGBMatrix;
using rgb_matrix::StreamWriter;
using rgb_matrix::StreamIO;
volatile bool interrupt_received = false;
static void InterruptHandler(int) {
interrupt_received = true;
}
struct LedPixel {
uint8_t r, g, b;
};
void CopyFrame(AVFrame *pFrame, FrameCanvas *canvas,
int offset_x, int offset_y,
int width, int height) {
for (int y = 0; y < height; ++y) {
LedPixel *pix = (LedPixel*) (pFrame->data[0] + y*pFrame->linesize[0]);
for (int x = 0; x < width; ++x, ++pix) {
canvas->SetPixel(x + offset_x, y + offset_y, pix->r, pix->g, pix->b);
}
}
}
// Scale "width" and "height" to fit within target rectangle of given size.
void ScaleToFitKeepAscpet(int fit_in_width, int fit_in_height,
int *width, int *height) {
if (*height < fit_in_height && *width < fit_in_width) return; // Done.
const float height_ratio = 1.0 * (*height) / fit_in_height;
const float width_ratio = 1.0 * (*width) / fit_in_width;
const float ratio = (height_ratio > width_ratio) ? height_ratio : width_ratio;
*width = roundf(*width / ratio);
*height = roundf(*height / ratio);
}
static int usage(const char *progname, const char *msg = NULL) {
if (msg) {
fprintf(stderr, "%s\n", msg);
}
fprintf(stderr, "Show one or a sequence of video files on the RGB-Matrix\n");
fprintf(stderr, "usage: %s [options] <video> [<video>...]\n", progname);
fprintf(stderr, "Options:\n"
"\t-F : Full screen without black bars; aspect ratio might suffer\n"
"\t-O<streamfile> : Output to stream-file instead of matrix (don't need to be root).\n"
"\t-s <count> : Skip these number of frames in the beginning.\n"
"\t-c <count> : Only show this number of frames (excluding skipped frames).\n"
"\t-V<vsync-multiple> : Instead of native video framerate, playback framerate\n"
"\t is a fraction of matrix refresh. In particular with a stable refresh,\n"
"\t this can result in more smooth playback. Choose multiple for desired framerate.\n"
"\t (Tip: use --led-limit-refresh for stable rate)\n"
"\t-T <threads> : Number of threads used to decode (default 1, max=%d)\n"
"\t-v : verbose; prints video metadata and other info.\n"
"\t-f : Loop forever.\n",
(int)std::thread::hardware_concurrency());
fprintf(stderr, "\nGeneral LED matrix options:\n");
rgb_matrix::PrintMatrixFlags(stderr);
return 1;
}
static void add_nanos(struct timespec *accumulator, long nanoseconds) {
accumulator->tv_nsec += nanoseconds;
while (accumulator->tv_nsec > 1000000000) {
accumulator->tv_nsec -= 1000000000;
accumulator->tv_sec += 1;
}
}
// Convert deprecated color formats to new and manually set the color range.
// YUV has funny ranges (16-235), while the YUVJ are 0-255. SWS prefers to
// deal with the YUV range, but then requires to set the output range.
// https://libav.org/documentation/doxygen/master/pixfmt_8h.html#a9a8e335cf3be472042bc9f0cf80cd4c5
SwsContext *CreateSWSContext(const AVCodecContext *codec_ctx,
int display_width, int display_height) {
AVPixelFormat pix_fmt;
bool src_range_extended_yuvj = true;
// Remap deprecated to new pixel format.
switch (codec_ctx->pix_fmt) {
case AV_PIX_FMT_YUVJ420P: pix_fmt = AV_PIX_FMT_YUV420P; break;
case AV_PIX_FMT_YUVJ422P: pix_fmt = AV_PIX_FMT_YUV422P; break;
case AV_PIX_FMT_YUVJ444P: pix_fmt = AV_PIX_FMT_YUV444P; break;
case AV_PIX_FMT_YUVJ440P: pix_fmt = AV_PIX_FMT_YUV440P; break;
default:
src_range_extended_yuvj = false;
pix_fmt = codec_ctx->pix_fmt;
}
SwsContext *swsCtx = sws_getContext(codec_ctx->width, codec_ctx->height,
pix_fmt,
display_width, display_height,
AV_PIX_FMT_RGB24, SWS_BILINEAR,
NULL, NULL, NULL);
if (src_range_extended_yuvj) {
// Manually set the source range to be extended. Read modify write.
int dontcare[4];
int src_range, dst_range;
int brightness, contrast, saturation;
sws_getColorspaceDetails(swsCtx, (int**)&dontcare, &src_range,
(int**)&dontcare, &dst_range, &brightness,
&contrast, &saturation);
const int* coefs = sws_getCoefficients(SWS_CS_DEFAULT);
src_range = 1; // New src range.
sws_setColorspaceDetails(swsCtx, coefs, src_range, coefs, dst_range,
brightness, contrast, saturation);
}
return swsCtx;
}
int main(int argc, char *argv[]) {
RGBMatrix::Options matrix_options;
rgb_matrix::RuntimeOptions runtime_opt;
// If started with 'sudo': make sure to drop privileges to same user
// we started with, which is the most expected (and allows us to read
// files as that user).
runtime_opt.drop_priv_user = getenv("SUDO_UID");
runtime_opt.drop_priv_group = getenv("SUDO_GID");
if (!rgb_matrix::ParseOptionsFromFlags(&argc, &argv,
&matrix_options, &runtime_opt)) {
return usage(argv[0]);
}
int vsync_multiple = 1;
bool use_vsync_for_frame_timing = false;
bool maintain_aspect_ratio = true;
bool verbose = false;
bool forever = false;
unsigned thread_count = 1;
int stream_output_fd = -1;
unsigned int frame_skip = 0;
int64_t framecount_limit = INT64_MAX;
int opt;
while ((opt = getopt(argc, argv, "vO:R:Lfc:s:FV:T:")) != -1) {
switch (opt) {
case 'v':
verbose = true;
break;
case 'f':
forever = true;
break;
case 'O':
stream_output_fd = open(optarg, O_CREAT|O_TRUNC|O_WRONLY, 0644);
if (stream_output_fd < 0) {
perror("Couldn't open output stream");
return 1;
}
break;
case 'L':
fprintf(stderr, "-L is deprecated. Use\n\t--led-pixel-mapper=\"U-mapper\" --led-chain=4\ninstead.\n");
return 1;
break;
case 'R':
fprintf(stderr, "-R is deprecated. "
"Use --led-pixel-mapper=\"Rotate:%s\" instead.\n", optarg);
return 1;
break;
case 'c':
framecount_limit = atoll(optarg);
break;
case 's':
frame_skip = atoi(optarg);
break;
case 'T':
thread_count = atoi(optarg);
break;
case 'F':
maintain_aspect_ratio = false;
break;
case 'V':
vsync_multiple = atoi(optarg);
if (vsync_multiple <= 0)
return usage(argv[0],
"-V: VSync-multiple needs to be a positive integer");
use_vsync_for_frame_timing = true;
break;
default:
return usage(argv[0]);
}
}
if (optind >= argc) {
fprintf(stderr, "Expected video filename.\n");
return usage(argv[0]);
}
const bool multiple_videos = (argc > optind + 1);
// We want to have the matrix start unless we actually write to a stream.
runtime_opt.do_gpio_init = (stream_output_fd < 0);
RGBMatrix *matrix = RGBMatrix::CreateFromOptions(matrix_options, runtime_opt);
if (matrix == NULL) {
return 1;
}
FrameCanvas *offscreen_canvas = matrix->CreateFrameCanvas();
long frame_count = 0;
StreamIO *stream_io = NULL;
StreamWriter *stream_writer = NULL;
if (stream_output_fd >= 0) {
stream_io = new rgb_matrix::FileStreamIO(stream_output_fd);
stream_writer = new StreamWriter(stream_io);
if (forever) {
fprintf(stderr, "-f (forever) doesn't make sense with -O; disabling\n");
forever = false;
}
}
// If we only have to loop a single video, we can avoid doing the
// expensive video stream set-up and just repeat in an inner loop.
const bool one_video_forever = forever && !multiple_videos;
const bool multiple_video_forever = forever && multiple_videos;
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100)
av_register_all();
#endif
avformat_network_init();
signal(SIGTERM, InterruptHandler);
signal(SIGINT, InterruptHandler);
do {
for (int m = optind; m < argc && !interrupt_received; ++m) {
const char *movie_file = argv[m];
if (strcmp(movie_file, "-") == 0) {
movie_file = "/dev/stdin";
}
AVFormatContext *format_context = avformat_alloc_context();
if (avformat_open_input(&format_context, movie_file, NULL, NULL) != 0) {
perror("Issue opening file: ");
return -1;
}
if (avformat_find_stream_info(format_context, NULL) < 0) {
fprintf(stderr, "Couldn't find stream information\n");
return -1;
}
if (verbose) av_dump_format(format_context, 0, movie_file, 0);
// Find the first video stream
int videoStream = -1;
AVCodecParameters *codec_parameters = NULL;
const AVCodec *av_codec = NULL;
for (int i = 0; i < (int)format_context->nb_streams; ++i) {
codec_parameters = format_context->streams[i]->codecpar;
av_codec = avcodec_find_decoder(codec_parameters->codec_id);
if (!av_codec) continue;
if (codec_parameters->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
break;
}
}
if (videoStream == -1)
return false;
// Frames per second; calculate wait time between frames.
AVStream *const stream = format_context->streams[videoStream];
AVRational rate = av_guess_frame_rate(format_context, stream, NULL);
const long frame_wait_nanos = 1e9 * rate.den / rate.num;
if (verbose) fprintf(stderr, "FPS: %f\n", 1.0*rate.num / rate.den);
AVCodecContext *codec_context = avcodec_alloc_context3(av_codec);
if (thread_count > 1 &&
av_codec->capabilities & AV_CODEC_CAP_FRAME_THREADS &&
std::thread::hardware_concurrency() > 1) {
codec_context->thread_type = FF_THREAD_FRAME;
codec_context->thread_count =
std::min(thread_count, std::thread::hardware_concurrency());
}
if (avcodec_parameters_to_context(codec_context, codec_parameters) < 0)
return -1;
if (avcodec_open2(codec_context, av_codec, NULL) < 0)
return -1;
/*
* Prepare frame to hold the scaled target frame to be send to matrix.
*/
int display_width = codec_context->width;
int display_height = codec_context->height;
if (maintain_aspect_ratio) {
display_width = codec_context->width;
display_height = codec_context->height;
// Make display fit within canvas.
ScaleToFitKeepAscpet(matrix->width(), matrix->height(),
&display_width, &display_height);
} else {
display_width = matrix->width();
display_height = matrix->height();
}
// Letterbox or pillarbox black bars.
const int display_offset_x = (matrix->width() - display_width)/2;
const int display_offset_y = (matrix->height() - display_height)/2;
// The output_frame_ will receive the scaled result.
AVFrame *output_frame = av_frame_alloc();
if (av_image_alloc(output_frame->data, output_frame->linesize,
display_width, display_height, AV_PIX_FMT_RGB24,
64) < 0) {
return -1;
}
if (verbose) {
fprintf(stderr, "Scaling %dx%d -> %dx%d; black border x:%d y:%d\n",
codec_context->width, codec_context->height,
display_width, display_height,
display_offset_x, display_offset_y);
}
// initialize SWS context for software scaling
SwsContext *const sws_ctx = CreateSWSContext(
codec_context, display_width, display_height);
if (!sws_ctx) {
fprintf(stderr, "Trouble doing scaling to %dx%d :(\n",
matrix->width(), matrix->height());
return 1;
}
struct timespec next_frame;
AVPacket *packet = av_packet_alloc();
AVFrame *decode_frame = av_frame_alloc(); // Decode video into this
do {
int64_t frames_left = framecount_limit;
unsigned int frames_to_skip = frame_skip;
if (one_video_forever) {
av_seek_frame(format_context, videoStream, 0, AVSEEK_FLAG_ANY);
avcodec_flush_buffers(codec_context);
}
clock_gettime(CLOCK_MONOTONIC, &next_frame);
int decode_in_flight = 0;
bool state_reading = true;
while (!interrupt_received && frames_left > 0) {
if (state_reading &&
av_read_frame(format_context, packet) != 0) {
state_reading = false; // ran out of packets from input
}
if (!state_reading && decode_in_flight == 0)
break; // Decoder fully drained.
// Is this a packet from the video stream?
if (state_reading && packet->stream_index != videoStream) {
av_packet_unref(packet);
continue; // Not interested in that.
}
if (state_reading) {
// Decode video frame
if (avcodec_send_packet(codec_context, packet) == 0) {
++decode_in_flight;
}
av_packet_unref(packet);
} else {
avcodec_send_packet(codec_context, nullptr); // Trigger decode drain
}
while (decode_in_flight &&
avcodec_receive_frame(codec_context, decode_frame) == 0) {
--decode_in_flight;
if (frames_to_skip) { frames_to_skip--; continue; }
// Determine absolute end of this frame now so that we don't include
// decoding overhead. TODO: skip frames if getting too slow ?
add_nanos(&next_frame, frame_wait_nanos);
// Convert the image from its native format to RGB
sws_scale(sws_ctx, (uint8_t const * const *)decode_frame->data,
decode_frame->linesize, 0, codec_context->height,
output_frame->data, output_frame->linesize);
CopyFrame(output_frame, offscreen_canvas,
display_offset_x, display_offset_y,
display_width, display_height);
frame_count++;
frames_left--;
if (stream_writer) {
if (verbose) fprintf(stderr, "%6ld", frame_count);
stream_writer->Stream(*offscreen_canvas, frame_wait_nanos/1000);
} else {
offscreen_canvas = matrix->SwapOnVSync(offscreen_canvas,
vsync_multiple);
}
if (!stream_writer && !use_vsync_for_frame_timing) {
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_frame, NULL);
}
}
}
} while (one_video_forever && !interrupt_received);
av_packet_free(&packet);
av_frame_free(&output_frame);
av_frame_free(&decode_frame);
avcodec_close(codec_context);
avformat_close_input(&format_context);
}
} while (multiple_video_forever && !interrupt_received);
if (interrupt_received) {
// Feedback for Ctrl-C, but most importantly, force a newline
// at the output, so that commandline-shell editing is not messed up.
fprintf(stderr, "Got interrupt. Exiting\n");
}
delete matrix;
delete stream_writer;
delete stream_io;
fprintf(stderr, "Total of %ld frames decoded\n", frame_count);
return 0;
}