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++

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

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